diff --git a/scripts/install_long_task_watchdog_cron.sh b/scripts/install_long_task_watchdog_cron.sh index 1d1fd03..f083dd5 100755 --- a/scripts/install_long_task_watchdog_cron.sh +++ b/scripts/install_long_task_watchdog_cron.sh @@ -4,15 +4,28 @@ set -euo pipefail ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" CRON_FILE="$ROOT_DIR/state/cron/long-task-watchdog.cron" LOG_DIR="$ROOT_DIR/state/long-task-watchdog" -RUNNER="$ROOT_DIR/scripts/long_task_watchdog.mjs" +ORCHESTRATOR="$ROOT_DIR/plugins/reporting-governance/scripts/watchdog_auto_notify_orchestrator.mjs" STATE_FILE="$ROOT_DIR/memory/watchdog-state.json" +WATCHDOG_EVIDENCE_DIR="$ROOT_DIR/state/long-task-watchdog" +WATCHDOG_EVENT_DIR="$ROOT_DIR/state/long-task-watchdog-events" +WATCHDOG_QUEUE_DIR="$ROOT_DIR/state/operator-notify-queue" +WATCHDOG_SPOOL_DIR="$ROOT_DIR/state/operator-notify-dispatch-spool" +WATCHDOG_RECEIPT_DIR="$ROOT_DIR/state/operator-notify-bridge-receipts" +PACKAGE_WATCHDOG_SCRIPT="$ROOT_DIR/plugins/reporting-governance/scripts/long_task_watchdog.mjs" +PACKAGE_DISPATCHER_SCRIPT="$ROOT_DIR/plugins/reporting-governance/scripts/operator_notify_dispatcher.mjs" +PACKAGE_SUPERVISOR_SCRIPT="$ROOT_DIR/plugins/reporting-governance/scripts/operator_notify_bridge_supervisor.mjs" +PACKAGE_SENDER_BINDING_SCRIPT="$ROOT_DIR/plugins/reporting-governance/scripts/operator_notify_sender_binding.mjs" mkdir -p "$(dirname "$CRON_FILE")" "$LOG_DIR" cat >"$CRON_FILE" <> "$LOG_DIR/cron.log" 2>&1 +*/10 * * * * cd "$ROOT_DIR" && /usr/bin/env node "$ORCHESTRATOR" --write-state --state "$STATE_FILE" --evidence-dir "$WATCHDOG_EVIDENCE_DIR" --event-dir "$WATCHDOG_EVENT_DIR" --queue-dir "$WATCHDOG_QUEUE_DIR" --spool-dir "$WATCHDOG_SPOOL_DIR" --receipt-dir "$WATCHDOG_RECEIPT_DIR" --watchdog-script "$PACKAGE_WATCHDOG_SCRIPT" --dispatcher-script "$PACKAGE_DISPATCHER_SCRIPT" --supervisor-script "$PACKAGE_SUPERVISOR_SCRIPT" --sender-command "/usr/bin/env node \"$PACKAGE_SENDER_BINDING_SCRIPT\" --queue-item \"__QUEUE_ITEM_PATH__\"" --dry-run >> "$LOG_DIR/cron.log" 2>&1 EOF printf 'Wrote cron snippet: %s\n' "$CRON_FILE" +printf 'Default mode is dry-run orchestration: runner -> dispatcher -> bridge, without pretending message.send was delivered.\n' printf 'To install for current user, run:\n' printf ' (crontab -l 2>/dev/null; cat "%s") | crontab -\n' "$CRON_FILE" +printf '\nTo enable real delivery after you have a trusted sender binding, replace --dry-run with either:\n' +printf ' --sender-mode openclaw-cli\n' +printf 'or an explicit --sender-command .\n' diff --git a/scripts/long_task_watchdog.mjs b/scripts/long_task_watchdog.mjs index 9df559d..f92fb93 100755 --- a/scripts/long_task_watchdog.mjs +++ b/scripts/long_task_watchdog.mjs @@ -1,263 +1,3 @@ #!/usr/bin/env node -import fs from 'node:fs'; -import path from 'node:path'; -import process from 'node:process'; - -const ROOT_DIR = path.resolve(import.meta.dirname, '..'); -const DEFAULT_STATE_PATH = path.join(ROOT_DIR, 'memory', 'watchdog-state.json'); -const DEFAULT_EVIDENCE_DIR = path.join(ROOT_DIR, 'state', 'long-task-watchdog'); - -function parseArgs(argv) { - const args = { - compact: false, - state: DEFAULT_STATE_PATH, - now: null, - evidenceDir: DEFAULT_EVIDENCE_DIR, - writeState: false, - help: false, - }; - - for (let i = 0; i < argv.length; i += 1) { - const token = argv[i]; - if (token === '--compact') { - args.compact = true; - continue; - } - if (token === '--write-state') { - args.writeState = true; - continue; - } - if (token === '--help' || token === '-h') { - args.help = true; - continue; - } - if (token === '--state') { - args.state = argv[i + 1] ?? args.state; - i += 1; - continue; - } - if (token.startsWith('--state=')) { - args.state = token.slice('--state='.length) || args.state; - continue; - } - if (token === '--now') { - args.now = argv[i + 1] ?? null; - i += 1; - continue; - } - if (token.startsWith('--now=')) { - args.now = token.slice('--now='.length) || null; - continue; - } - if (token === '--evidence-dir') { - args.evidenceDir = argv[i + 1] ?? args.evidenceDir; - i += 1; - continue; - } - if (token.startsWith('--evidence-dir=')) { - args.evidenceDir = token.slice('--evidence-dir='.length) || args.evidenceDir; - continue; - } - } - - return args; -} - -function printHelp() { - process.stdout.write([ - 'Usage: node scripts/long_task_watchdog.mjs [--compact] [--write-state] [--state ] [--now ] [--evidence-dir ]', - '', - 'Minimal file-backed long-task watchdog runner.', - ].join('\n') + '\n'); -} - -function parseJsonFile(filePath) { - const raw = fs.readFileSync(filePath, 'utf8'); - return JSON.parse(raw); -} - -function parseTime(value) { - if (typeof value !== 'string' || value.length === 0) return null; - const timestamp = Date.parse(value); - return Number.isNaN(timestamp) ? null : timestamp; -} - -function toIso(value) { - return new Date(value).toISOString(); -} - -function toSafeName(value) { - return String(value || 'watchdog') - .replace(/[^a-zA-Z0-9._-]+/g, '-') - .replace(/^-+|-+$/g, '') - .slice(0, 80) || 'watchdog'; -} - -function evaluateWatchdog(watchdog, nowMs) { - const intervalMinutes = Number.isFinite(watchdog?.intervalMinutes) - ? watchdog.intervalMinutes - : Number.parseInt(String(watchdog?.intervalMinutes ?? '0'), 10); - const intervalMs = intervalMinutes > 0 ? intervalMinutes * 60 * 1000 : 0; - const milestoneMs = parseTime(watchdog?.lastMilestoneAt); - const lastAlertMs = parseTime(watchdog?.lastAlertAt); - const active = watchdog?.status === 'active'; - - if (!active) { - return { - id: watchdog?.id ?? null, - active: false, - overdue: false, - action: 'skip_inactive', - reason: 'watchdog is not active', - }; - } - - if (!intervalMs || milestoneMs === null) { - return { - id: watchdog?.id ?? null, - active: true, - overdue: false, - action: 'invalid_contract', - reason: 'intervalMinutes or lastMilestoneAt is missing/invalid', - }; - } - - const dueAtMs = milestoneMs + intervalMs; - const overdue = nowMs >= dueAtMs; - - if (!overdue) { - return { - id: watchdog?.id ?? null, - active: true, - overdue: false, - action: 'within_interval', - reason: 'last milestone is still within interval', - dueAt: toIso(dueAtMs), - minutesOverdue: 0, - }; - } - - const lastAlertStillFresh = lastAlertMs !== null && lastAlertMs >= dueAtMs; - if (lastAlertStillFresh) { - return { - id: watchdog?.id ?? null, - active: true, - overdue: true, - action: 'already_alerted_this_interval', - reason: 'lastAlertAt already covers current overdue interval', - dueAt: toIso(dueAtMs), - minutesOverdue: Math.floor((nowMs - dueAtMs) / 60000), - }; - } - - return { - id: watchdog?.id ?? null, - active: true, - overdue: true, - action: 'emit_external_evidence', - reason: 'active watchdog is overdue and has not been externally evidenced for this interval', - dueAt: toIso(dueAtMs), - minutesOverdue: Math.floor((nowMs - dueAtMs) / 60000), - }; -} - -function ensureDir(dirPath) { - fs.mkdirSync(dirPath, { recursive: true }); -} - -function writeEvidence(evidenceDir, watchdog, evaluation, nowIso) { - ensureDir(evidenceDir); - const fileName = `${nowIso.replace(/[:]/g, '').replace(/\.\d{3}Z$/, 'Z')}-${toSafeName(watchdog.id)}.json`; - const filePath = path.join(evidenceDir, fileName); - const payload = { - generatedAt: nowIso, - tool: 'long_task_watchdog', - watchdog: { - id: watchdog.id, - task: watchdog.task, - ownerSession: watchdog.ownerSession ?? null, - ownerSessionKey: watchdog.ownerSessionKey ?? null, - reportChannel: watchdog.reportChannel ?? watchdog.channel ?? null, - reportTarget: watchdog.reportTarget ?? watchdog.target ?? null, - intervalMinutes: watchdog.intervalMinutes, - lastMilestoneAt: watchdog.lastMilestoneAt ?? null, - lastAlertAt: watchdog.lastAlertAt ?? null, - }, - evaluation, - nextExpectedExternalAction: [ - 'nudge owner session', - 'report owner-visible checkpoint', - 'or respawn / inspect locally if owner appears stalled', - ], - }; - fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); - return filePath; -} - -function main() { - const args = parseArgs(process.argv.slice(2)); - if (args.help) { - printHelp(); - process.exit(0); - } - - const nowMs = args.now ? parseTime(args.now) : Date.now(); - if (nowMs === null) { - process.stderr.write('Invalid --now value\n'); - process.exit(1); - } - const nowIso = toIso(nowMs); - - const state = parseJsonFile(args.state); - const watchdogs = Array.isArray(state.watchdogs) ? state.watchdogs : []; - const evaluations = watchdogs.map((watchdog) => ({ - watchdogId: watchdog?.id ?? null, - ...evaluateWatchdog(watchdog, nowMs), - })); - - const evidenceWrites = []; - const nextWatchdogs = watchdogs.map((watchdog, index) => { - const evaluation = evaluations[index]; - if (evaluation.action !== 'emit_external_evidence') { - return watchdog; - } - const evidencePath = writeEvidence(args.evidenceDir, watchdog, evaluation, nowIso); - evidenceWrites.push({ watchdogId: watchdog.id, path: evidencePath }); - return { - ...watchdog, - lastAlertAt: nowIso, - lastObservedActivityAt: watchdog.lastObservedActivityAt ?? watchdog.lastMilestoneAt ?? null, - lastNudgeAt: watchdog.lastNudgeAt ?? null, - }; - }); - - if (args.writeState) { - const nextState = { - ...state, - watchdogs: nextWatchdogs, - }; - fs.writeFileSync(args.state, `${JSON.stringify(nextState, null, 2)}\n`, 'utf8'); - } - - const response = { - ok: true, - tool: 'long_task_watchdog', - version: 'mvp-v1', - statePath: path.resolve(args.state), - evidenceDir: path.resolve(args.evidenceDir), - now: nowIso, - writeState: args.writeState, - result: { - activeCount: watchdogs.filter((item) => item?.status === 'active').length, - overdueCount: evaluations.filter((item) => item.overdue === true).length, - emittedCount: evidenceWrites.length, - evaluations, - evidenceWrites, - }, - }; - - process.stdout.write(`${JSON.stringify(response, null, args.compact ? 0 : 2)}\n`); -} - -main(); +import '../plugins/reporting-governance/scripts/long_task_watchdog.mjs'; diff --git a/scripts/run_watchdog_auto_notify.sh b/scripts/run_watchdog_auto_notify.sh new file mode 100755 index 0000000..6da5da5 --- /dev/null +++ b/scripts/run_watchdog_auto_notify.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT="/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin" +cd "$ROOT" +/usr/bin/env node "$ROOT/plugins/reporting-governance/scripts/watchdog_auto_notify_orchestrator.mjs" \ + --write-state \ + --state "$ROOT/memory/watchdog-state.json" \ + --evidence-dir "$ROOT/state/long-task-watchdog" \ + --event-dir "$ROOT/state/long-task-watchdog-events" \ + --queue-dir "$ROOT/state/operator-notify-queue" \ + --spool-dir "$ROOT/state/operator-notify-dispatch-spool" \ + --receipt-dir "$ROOT/state/operator-notify-bridge-receipts" \ + --watchdog-script "$ROOT/plugins/reporting-governance/scripts/long_task_watchdog.mjs" \ + --dispatcher-script "$ROOT/plugins/reporting-governance/scripts/operator_notify_dispatcher.mjs" \ + --supervisor-script "$ROOT/plugins/reporting-governance/scripts/operator_notify_bridge_supervisor.mjs" \ + --sender-mode openclaw-cli \ + >> "$ROOT/state/long-task-watchdog/cron.log" 2>&1 diff --git a/scripts/test_long_task_watchdog.mjs b/scripts/test_long_task_watchdog.mjs index 450f742..9c19d71 100755 --- a/scripts/test_long_task_watchdog.mjs +++ b/scripts/test_long_task_watchdog.mjs @@ -8,13 +8,18 @@ import process from 'node:process'; import { spawnSync } from 'node:child_process'; const ROOT_DIR = path.resolve(import.meta.dirname, '..'); -const WATCHDOG_SCRIPT = path.join(ROOT_DIR, 'scripts', 'long_task_watchdog.mjs'); +const REPO_SHIM = path.join(ROOT_DIR, 'scripts', 'long_task_watchdog.mjs'); +const PACKAGE_ENTRY = path.join(ROOT_DIR, 'plugins', 'reporting-governance', 'scripts', 'long_task_watchdog.mjs'); -function createFixtureRunner() { +function createFixtureRunner(scriptPath) { const fixtureRoot = mkdtempSync(path.join(tmpdir(), 'long-task-watchdog-test-')); const statePath = path.join(fixtureRoot, 'watchdog-state.json'); const evidenceDir = path.join(fixtureRoot, 'evidence'); + const eventDir = path.join(fixtureRoot, 'events'); + const notificationDir = path.join(fixtureRoot, 'notifications'); mkdirSync(evidenceDir, { recursive: true }); + mkdirSync(eventDir, { recursive: true }); + mkdirSync(notificationDir, { recursive: true }); function writeState(content) { const body = typeof content === 'string' ? content : JSON.stringify(content, null, 2); @@ -23,10 +28,21 @@ function createFixtureRunner() { } function run(args = []) { - const result = spawnSync(process.execPath, [WATCHDOG_SCRIPT, '--state', statePath, '--evidence-dir', evidenceDir, ...args], { - cwd: ROOT_DIR, - encoding: 'utf8', - }); + const result = spawnSync( + process.execPath, + [ + scriptPath, + '--state', statePath, + '--evidence-dir', evidenceDir, + '--event-dir', eventDir, + '--notification-dir', notificationDir, + ...args, + ], + { + cwd: ROOT_DIR, + encoding: 'utf8', + }, + ); return { status: result.status, stdout: result.stdout ?? '', @@ -42,11 +58,36 @@ function createFixtureRunner() { return readdirSync(evidenceDir).sort(); } + function listEvents() { + return readdirSync(eventDir).sort(); + } + + function listNotifications() { + return readdirSync(notificationDir).sort(); + } + + function readJson(dirPath, fileName) { + return JSON.parse(readFileSync(path.join(dirPath, fileName), 'utf8')); + } + function cleanup() { rmSync(fixtureRoot, { recursive: true, force: true }); } - return { statePath, evidenceDir, writeState, run, readState, listEvidence, cleanup }; + return { + statePath, + evidenceDir, + eventDir, + notificationDir, + writeState, + run, + readState, + listEvidence, + listEvents, + listNotifications, + readJson, + cleanup, + }; } const tests = []; @@ -56,8 +97,59 @@ function printResult(prefix, name, detail = '') { process.stdout.write(`${prefix} ${name}${detail ? ` ${detail}` : ''}\n`); } -test('inactive watchdogs do not emit evidence', () => { - const runner = createFixtureRunner(); +function normalizePayload(payload) { + return { + ...payload, + statePath: '', + evidenceDir: '', + eventDir: '', + notificationDir: '', + result: { + ...payload.result, + evidenceWrites: payload.result.evidenceWrites.map((item) => ({ ...item, path: '' })), + eventWrites: payload.result.eventWrites.map((item) => ({ ...item, path: '', eventId: '' })), + notificationWrites: payload.result.notificationWrites.map((item) => ({ ...item, path: '', notificationId: '' })), + }, + }; +} + +test('repo shim matches package entrypoint for same overdue watchdog payload', () => { + const shim = createFixtureRunner(REPO_SHIM); + const pkg = createFixtureRunner(PACKAGE_ENTRY); + const state = { + version: 1, + watchdogs: [ + { + id: 'reporting-governance-plugin-watchdog', + task: 'reporting-governance plugin spec development', + status: 'active', + ownerSessionKey: 'agent:coder:main', + reportChannel: 'telegram', + reportTarget: '864811879', + intervalMinutes: 10, + lastMilestoneAt: '2026-05-07T08:00:00.000Z', + lastAlertAt: null, + }, + ], + }; + + try { + shim.writeState(state); + pkg.writeState(state); + const shimResult = shim.run(['--compact', '--write-state', '--now', '2026-05-07T08:20:00.000Z']); + const pkgResult = pkg.run(['--compact', '--write-state', '--now', '2026-05-07T08:20:00.000Z']); + assert.equal(shimResult.status, 0, shimResult.stderr); + assert.equal(pkgResult.status, 0, pkgResult.stderr); + assert.deepEqual(normalizePayload(JSON.parse(shimResult.stdout)), normalizePayload(JSON.parse(pkgResult.stdout))); + assert.deepEqual(shim.readState(), pkg.readState()); + } finally { + shim.cleanup(); + pkg.cleanup(); + } +}); + +test('inactive watchdogs do not emit evidence, event, or notification queue items', () => { + const runner = createFixtureRunner(PACKAGE_ENTRY); try { runner.writeState({ version: 1, @@ -76,14 +168,18 @@ test('inactive watchdogs do not emit evidence', () => { assert.equal(result.status, 0, result.stderr); const payload = JSON.parse(result.stdout); assert.equal(payload.result.emittedCount, 0); + assert.equal(payload.result.eventCount, 0); + assert.equal(payload.result.notificationCount, 0); assert.deepEqual(runner.listEvidence(), []); + assert.deepEqual(runner.listEvents(), []); + assert.deepEqual(runner.listNotifications(), []); } finally { runner.cleanup(); } }); -test('overdue active watchdog emits external evidence and updates lastAlertAt when write-state is enabled', () => { - const runner = createFixtureRunner(); +test('overdue active watchdog emits evidence, canonical event, notification queue item, and updates lastAlertAt', () => { + const runner = createFixtureRunner(PACKAGE_ENTRY); try { runner.writeState({ version: 1, @@ -106,18 +202,39 @@ test('overdue active watchdog emits external evidence and updates lastAlertAt wh assert.equal(result.status, 0, result.stderr); const payload = JSON.parse(result.stdout); assert.equal(payload.result.emittedCount, 1); + assert.equal(payload.result.eventCount, 1); + assert.equal(payload.result.notificationCount, 1); + const evidenceFiles = runner.listEvidence(); + const eventFiles = runner.listEvents(); + const notificationFiles = runner.listNotifications(); assert.equal(evidenceFiles.length, 1); + assert.equal(eventFiles.length, 1); + assert.equal(notificationFiles.length, 1); + + const eventPayload = runner.readJson(runner.eventDir, eventFiles[0]); + assert.equal(eventPayload.event_type, 'watchdog_fired'); + assert.equal(eventPayload.operator_context.channel, 'telegram'); + assert.equal(eventPayload.operator_context.operator_id, '864811879'); + + const notificationPayload = runner.readJson(runner.notificationDir, notificationFiles[0]); + assert.equal(notificationPayload.kind, 'notify_operator'); + assert.equal(notificationPayload.status, 'pending'); + assert.equal(notificationPayload.operator_notice.channel, 'telegram'); + assert.equal(notificationPayload.operator_notice.target, '864811879'); + assert.equal(notificationPayload.blocked_gap, null); + assert.equal(notificationPayload.dispatch_hint.tool, 'message.send'); const nextState = runner.readState(); assert.equal(nextState.watchdogs[0].lastAlertAt, '2026-05-07T08:20:00.000Z'); + assert.equal(nextState.watchdogs[0].lastNudgeAt, '2026-05-07T08:20:00.000Z'); } finally { runner.cleanup(); } }); test('same interval is not alerted twice once lastAlertAt covers the overdue window', () => { - const runner = createFixtureRunner(); + const runner = createFixtureRunner(PACKAGE_ENTRY); try { runner.writeState({ version: 1, @@ -137,7 +254,43 @@ test('same interval is not alerted twice once lastAlertAt covers the overdue win assert.equal(result.status, 0, result.stderr); const payload = JSON.parse(result.stdout); assert.equal(payload.result.emittedCount, 0); + assert.equal(payload.result.eventCount, 0); + assert.equal(payload.result.notificationCount, 0); assert.deepEqual(runner.listEvidence(), []); + assert.deepEqual(runner.listEvents(), []); + assert.deepEqual(runner.listNotifications(), []); + } finally { + runner.cleanup(); + } +}); + +test('notification queue item records blocked gap when report channel/target is missing', () => { + const runner = createFixtureRunner(PACKAGE_ENTRY); + try { + runner.writeState({ + version: 1, + watchdogs: [ + { + id: 'missing-target-watchdog', + task: 'targetless task', + status: 'active', + ownerSessionKey: 'agent:coder:main', + intervalMinutes: 10, + lastMilestoneAt: '2026-05-07T08:00:00.000Z', + lastAlertAt: null, + }, + ], + }); + + const result = runner.run(['--compact', '--now', '2026-05-07T08:20:00.000Z']); + assert.equal(result.status, 0, result.stderr); + const payload = JSON.parse(result.stdout); + assert.equal(payload.result.notificationCount, 1); + assert.equal(payload.result.notificationWrites[0].dispatchReady, false); + + const notificationFiles = runner.listNotifications(); + const notificationPayload = runner.readJson(runner.notificationDir, notificationFiles[0]); + assert.match(notificationPayload.blocked_gap, /reportChannel\/reportTarget/); } finally { runner.cleanup(); } diff --git a/state/cron/long-task-watchdog.cron b/state/cron/long-task-watchdog.cron index c26b798..ebedd64 100644 --- a/state/cron/long-task-watchdog.cron +++ b/state/cron/long-task-watchdog.cron @@ -1 +1 @@ -*/10 * * * * cd "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin" && /usr/bin/env node "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin/scripts/long_task_watchdog.mjs" --write-state --state "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin/memory/watchdog-state.json" --evidence-dir "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin/state/long-task-watchdog" >> "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin/state/long-task-watchdog/cron.log" 2>&1 +*/10 * * * * cd "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin" && /usr/bin/env node "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin/plugins/reporting-governance/scripts/watchdog_auto_notify_orchestrator.mjs" --write-state --state "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin/memory/watchdog-state.json" --evidence-dir "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin/state/long-task-watchdog" --event-dir "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin/state/long-task-watchdog-events" --queue-dir "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin/state/operator-notify-queue" --spool-dir "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin/state/operator-notify-dispatch-spool" --receipt-dir "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin/state/operator-notify-bridge-receipts" --watchdog-script "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin/plugins/reporting-governance/scripts/long_task_watchdog.mjs" --dispatcher-script "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin/plugins/reporting-governance/scripts/operator_notify_dispatcher.mjs" --supervisor-script "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin/plugins/reporting-governance/scripts/operator_notify_bridge_supervisor.mjs" --sender-command "/usr/bin/env node \"/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin/plugins/reporting-governance/scripts/operator_notify_sender_binding.mjs\" --queue-item \"__QUEUE_ITEM_PATH__\"" --dry-run >> "/home/alice/.openclaw/workspace/.worktrees/reporting-governance-plugin/state/long-task-watchdog/cron.log" 2>&1