taskforce-loop-engineering 0.10.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/MIGRATING.md +47 -2
  3. package/README.md +70 -0
  4. package/bin/loop-engineering.mjs +192 -0
  5. package/docs/architecture.md +444 -0
  6. package/docs/multi-agent-control-plane.md +31 -0
  7. package/docs/operator-dashboard.md +27 -0
  8. package/docs/production-operations.md +29 -0
  9. package/docs/production-trust-backlog.json +13 -0
  10. package/docs/production-trust-contract.md +54 -0
  11. package/docs/release-0.12-acceptance.md +35 -0
  12. package/lib/action-reservations.mjs +196 -0
  13. package/lib/core.mjs +219 -2
  14. package/lib/durable-journal.mjs +90 -0
  15. package/lib/operator-dashboard.mjs +198 -0
  16. package/lib/runtime-adapter-v1.mjs +36 -0
  17. package/lib/todo-control-plane.mjs +287 -0
  18. package/lib/upgrade-planner.mjs +24 -0
  19. package/package.json +4 -2
  20. package/scripts/action-reservation-self-test.mjs +65 -0
  21. package/scripts/async-acceptance-refresh-self-test.mjs +46 -0
  22. package/scripts/durable-journal-self-test.mjs +24 -0
  23. package/scripts/human-gate-lifecycle-v2-self-test.mjs +81 -0
  24. package/scripts/live-runtime-soak.mjs +86 -0
  25. package/scripts/operator-dashboard-self-test.mjs +74 -0
  26. package/scripts/production-acceptance.mjs +8 -0
  27. package/scripts/production-soak.mjs +19 -0
  28. package/scripts/route-notify-self-test.mjs +1 -0
  29. package/scripts/runtime-adapter-contract-self-test.mjs +14 -0
  30. package/scripts/todo-control-plane-self-test.mjs +74 -0
  31. package/scripts/upgrade-planner-self-test.mjs +9 -0
  32. package/templates/operator-projection.schema.json +1 -0
  33. package/templates/todo.schema.json +28 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.13.0 - 2026-08-14
6
+
7
+ - Add versioned OpenClaw, Hermes, and custom runtime adapter contracts with a shared conformance suite.
8
+ - Add a checksummed, fsync-backed durable journal with replay, snapshots, migration, backup/restore, and fail-closed unknown-outcome handling.
9
+ - Add deterministic multi-worker canary and isolated live-runtime soak tooling, a credential-free demo, and a non-destructive customized-Ironman upgrade planner.
10
+ - Add unified production-trust acceptance and an idempotent acceptance refresh command so detached long-running evidence invalidates stale final judgements.
11
+
12
+ ## 0.12.0 - 2026-08-13
13
+
14
+ - Add P3 read-only Operator Dashboard, normalized schema, loopback HTTP/JSON API, static export, inspect and health commands.
15
+ - Integrate P0 gates, P1 reconciliation and P2 ownership/lease/handoff projections with legacy artifacts.
16
+ - Add redaction, traversal/XSS/bind protections and deterministic/security/performance coverage.
17
+
18
+ ## 0.11.0 - 2026-08-13
19
+
20
+ - Add Human-Gate Lifecycle v2 parked waits for human input and external conditions, configurable timeout/reminder/escalation policy, durable idempotent notification evidence, verified recovery signals, and exactly-once execution-boundary metadata.
21
+ - Extend `queue-status` with operator-visible waiting states and add `queue-park`, `queue-wait-tick`, and `queue-wait-resume` commands.
22
+ - Add a VPS-down/SSH-banner-timeout regression fixture proving throttled reminders, preserved unconsumed authorization, verified resume, and restart-safe idempotency.
23
+ - Add the durable Action Idempotency and Reservation Contract with immutable request fingerprints, scoped authorization ledger, atomic leased claims and fencing tokens, settlement/release evidence, unknown-outcome reconciliation, paid-call/notification/deployment adapters, operator CLI commands, and backward-compatible artifact import.
24
+ - Add concurrency and crash-restart acceptance coverage while preserving Human-Gate Lifecycle v2.
25
+ - Add the P2 Multi-Agent Control Plane: typed todos, agent capability registration, deterministic atomic claim with lease/fencing, dependency and quota eligibility, durable handoff, orphan recovery, legacy import, ownership audit, CLI, schema, docs, and P0/P1 safety integration.
26
+
3
27
  ## 0.10.0 - 2026-08-11
4
28
 
5
29
  - Add OpenClaw installer language adaptation with `--language auto|en|zh`.
package/MIGRATING.md CHANGED
@@ -1,4 +1,24 @@
1
- # Migrating to Taskforce Loop Engineering 0.7.0
1
+ # Migrating to Taskforce Loop Engineering 0.12.0
2
+
3
+ ## Operator projection
4
+
5
+ No runtime artifact migration is required. P3 reads P0/P1/P2 and legacy queue/project artifacts in place and emits projection schema `1.0.0`; existing writers remain authoritative. Consumers should use `schema_version`, tolerate additive fields, and treat degraded health as a refresh/investigation signal. `dashboard-serve` is loopback-only unless `--allow-non-loopback` is explicit.
6
+
7
+ ## Action reservation artifacts
8
+
9
+ Existing queue, notification, and Human-Gate Lifecycle v2 artifacts remain
10
+ valid. New side-effecting integrations should migrate their prior idempotency
11
+ records through `migrateLegacyActionArtifact()` from
12
+ `lib/action-reservations.mjs`. Import is idempotent: the legacy key, canonical
13
+ request, action kind, and authorization scope become an immutable version 1
14
+ reservation; importing the same artifact again is a no-op, while conflicting
15
+ content fails closed. There is no eager workspace rewrite.
16
+
17
+ The version 1 record schema includes `idempotency_key`, `kind`, `state`,
18
+ `request_fingerprint`, immutable `request`, `fencing_counter`, `claim`,
19
+ `authorization`, `settlement`, `release`, `reconciliation`, and `events`.
20
+ Legacy send ledgers continue to suppress duplicates; adapters should adopt this
21
+ contract before their next side-effecting execution boundary.
2
22
 
3
23
  ## Package rename
4
24
 
@@ -11,7 +31,7 @@ workspace scripts do not need to change.
11
31
  Upgrade the CLI normally, then run the package checks before changing an existing OpenClaw integration:
12
32
 
13
33
  ```bash
14
- npm install -g taskforce-loop-engineering@0.7.0
34
+ npm install -g taskforce-loop-engineering@0.12.0
15
35
  loop-engineering-openclaw-manage --root /path/to/workspace --action upgrade-plan
16
36
  ```
17
37
 
@@ -58,3 +78,28 @@ After installation or upgrade, validate the integration:
58
78
  loop-engineering-openclaw-doctor --root /path/to/workspace --queue agent-tasks --worker-agent main
59
79
  loop-engineering-openclaw-smoke --root /path/to/workspace --queue agent-tasks --worker-agent main
60
80
  ```
81
+ # Human-Gate Lifecycle v2
82
+
83
+ Existing tasks with `status: "waiting_for_human"` remain supported and continue
84
+ to appear in `waiting/`. New parked tasks add a versioned `parked` object while
85
+ using the same directory, so no queue migration is required. Operators can
86
+ adopt v2 incrementally:
87
+
88
+ ```bash
89
+ loop-engineering queue-park --queue agent-tasks --task-id <id> \
90
+ --wait-kind external_condition --reason "VPS unavailable"
91
+ loop-engineering queue-wait-tick --queue agent-tasks --notify-command '<sender>'
92
+ loop-engineering queue-wait-resume --queue agent-tasks --task-id <id> \
93
+ --verified --recovery-signal 'probe=vps-1;ssh_banner=verified'
94
+ ```
95
+
96
+ Timeout changes visibility and enables throttled escalation; it never rejects
97
+ the task, consumes authorization, or retries a privileged action. Resume is
98
+ fail-closed without a verified recovery signal. The original task id, wait id,
99
+ authorization state, notification evidence, and execution-boundary key remain
100
+ durable across restart.
101
+ # P2 typed todo migration
102
+
103
+ Run `loop-engineering todo-import-legacy --root <workspace>` to import existing `runtime/loops/*/{inbox,waiting,active}/*.json` artifacts. Import is additive and idempotent: stable ids use `legacy:<queue>:<task-id>`, source files are not modified, and waiting items retain their parked gate. Imported items receive explicit legacy acceptance/evidence defaults and can then be inspected or enriched before claim.
104
+
105
+ New integrations should register agents with `agent-register`, create typed todos with `todo-create`, and use the claim fencing token for every renew, release, and handoff operation. Continue reconciling P1 `unknown` action outcomes before recovery or reassignment.
package/README.md CHANGED
@@ -1,5 +1,72 @@
1
1
  # Taskforce Loop Engineering
2
2
 
3
+ ## 0.13 production trust
4
+
5
+ The local production-trust contract, runtime adapter v1, durable journal,
6
+ multi-worker canary, non-destructive Ironman upgrade planner, safe demo and
7
+ unified acceptance are documented in
8
+ [docs/production-trust-contract.md](docs/production-trust-contract.md). Run
9
+ `npm run check:production-trust`; external publishing and deployment remain
10
+ separately authorized actions.
11
+
12
+ ## Read-only operator dashboard (P3)
13
+
14
+ Version 0.12 adds a dependency-free operator projection over projects, queues, P0 gates, P1 action reservations and P2 typed todo ownership. Use `dashboard-inspect`, `dashboard-health`, `dashboard-export`, or the loopback-only `dashboard-serve`. See [docs/operator-dashboard.md](docs/operator-dashboard.md) for API, security and schema details.
15
+
16
+ The consolidated P0-P3 local release contract and evidence ledger are recorded in [docs/release-0.12-acceptance.md](docs/release-0.12-acceptance.md). Publishing, tagging, pushing, and production installation remain separate release actions.
17
+
18
+ ## Action idempotency and reservation
19
+
20
+ Every side-effecting action must reserve a durable idempotency key before an
21
+ adapter is invoked. The reservation binds a canonical request fingerprint to an
22
+ authorization scope. A worker then atomically claims a lease and receives a
23
+ monotonic fencing token; only that token can settle the action or release its
24
+ reservation. Paid API, notification, and deployment adapters expose the same
25
+ lifecycle from `lib/action-reservations.mjs`.
26
+
27
+ ```bash
28
+ loop-engineering action-reserve --idempotency-key task:step:attempt \
29
+ --kind paid_api --authorization-scope approval:task:provider \
30
+ --request-json '{"model":"example","requestDigest":"sha256"}'
31
+ loop-engineering action-claim --idempotency-key task:step:attempt \
32
+ --owner worker-1 --lease-ms 60000
33
+ loop-engineering action-settle --idempotency-key task:step:attempt \
34
+ --fencing-token 1 --evidence upstream-request-id
35
+ ```
36
+
37
+ An expired claim becomes `unknown`, not runnable. Operators must inspect and
38
+ reconcile it as `accepted` (durably settle and consume authorization) or
39
+ `not_accepted` (return to reserved) using `action-inspect` and
40
+ `action-reconcile`. This is the crash boundary that prevents blind replay and
41
+ double spend after an upstream acceptance whose local commit was interrupted.
42
+ Terminal records retain settlement or release evidence and an append-only event
43
+ history under `runtime/loops/action-reservations/`.
44
+
45
+ ## Human-Gate Lifecycle v2
46
+
47
+ Park work without treating an external dependency as failure or repeatedly
48
+ retrying a privileged action:
49
+
50
+ ```bash
51
+ loop-engineering queue-park --queue agent-tasks --task-id <id> \
52
+ --wait-kind external_condition --reason "SSH banner timed out" \
53
+ --wait-timeout-ms 86400000 --reminder-interval-ms 3600000 \
54
+ --escalation-interval-ms 86400000 --max-reminders 3
55
+
56
+ loop-engineering queue-wait-tick --queue agent-tasks \
57
+ --notify-command '<source-bound notifier>'
58
+
59
+ loop-engineering queue-wait-resume --queue agent-tasks --task-id <id> \
60
+ --verified --recovery-signal 'probe=vps-1;ssh_banner=verified'
61
+ ```
62
+
63
+ `queue-status --json` distinguishes `waiting_for_human`,
64
+ `external_condition_wait`, `timed_out_or_escalated`, and `runnable` state.
65
+ Reminder and escalation sends are throttled and leave durable evidence under
66
+ `runtime/loops/<queue>/wait-notifications/`. Timeout never rejects a task,
67
+ consumes a stored authorization, or repeats an action. Verified resume preserves
68
+ the original task/wait identity and exactly-once execution boundary.
69
+
3
70
  Durable loop engineering for repeated agent work on OpenClaw and Hermes Agent. It provides a small
4
71
  Node CLI that executes JSON loop specs, records append-only run artifacts, and
5
72
  uses a circuit breaker to escalate repeated failures.
@@ -1270,3 +1337,6 @@ ClawHub:
1270
1337
  ```text
1271
1338
  https://clawhub.ai/ambitioncn/skills/taskforce-loop-engineering
1272
1339
  ```
1340
+ # P2 multi-agent control plane
1341
+
1342
+ Loop Engineering includes typed executable todos with capability/authority-aware atomic claim, lease and fencing, deterministic dependency/quota scheduling, durable peer handoff, orphan recovery, and P0/P1 safety integration. See [docs/multi-agent-control-plane.md](docs/multi-agent-control-plane.md) for the data contract, CLI, and migration path.
@@ -37,7 +37,10 @@ import {
37
37
  mergeQueueOptions,
38
38
  nextState,
39
39
  notifyTerminalTasks,
40
+ refreshTaskAcceptance,
40
41
  notifyHumanInputRequests,
42
+ parkQueueTask,
43
+ resumeParkedTask,
41
44
  resolveHumanInput,
42
45
  enqueueTask,
43
46
  projectIntake,
@@ -56,6 +59,7 @@ import {
56
59
  queueSchedulerTick,
57
60
  queueSubdirFor,
58
61
  queueStatus,
62
+ tickParkedTasks,
59
63
  routeLoopMessage,
60
64
  summarizeLoopRuns,
61
65
  runCheck,
@@ -68,6 +72,34 @@ import {
68
72
  workflowTuningPlan,
69
73
  writeJson
70
74
  } from '../lib/core.mjs';
75
+ import {
76
+ claimAction,
77
+ inspectAction,
78
+ reconcileAction,
79
+ releaseAction,
80
+ reserveAction,
81
+ settleAction
82
+ } from '../lib/action-reservations.mjs';
83
+ import {
84
+ claimTodo,
85
+ createTodo,
86
+ decideHandoff,
87
+ handoffTodo,
88
+ importLegacyTodos,
89
+ inspectTodo,
90
+ listTodos,
91
+ recoverTodos,
92
+ registerAgent,
93
+ releaseTodo,
94
+ renewTodo
95
+ } from '../lib/todo-control-plane.mjs';
96
+ import {
97
+ buildOperatorProjection,
98
+ createDashboardServer,
99
+ dashboardHealth,
100
+ exportDashboard,
101
+ filterProjection
102
+ } from '../lib/operator-dashboard.mjs';
71
103
 
72
104
  function parseArgs(argv) {
73
105
  const args = { _: [], root: process.cwd(), json: false, force: false };
@@ -107,6 +139,13 @@ function parseArgs(argv) {
107
139
  args.checks.push(argv[++i]);
108
140
  }
109
141
  else if (a === '--task-id') args.taskId = argv[++i];
142
+ else if (a === '--todo-id') args.todoId = argv[++i];
143
+ else if (a === '--agent-id') args.agentId = argv[++i];
144
+ else if (a === '--target-agent-id') args.targetAgentId = argv[++i];
145
+ else if (a === '--handoff-id') args.handoffId = argv[++i];
146
+ else if (a === '--todo-json') args.todoJson = argv[++i];
147
+ else if (a === '--agent-json') args.agentJson = argv[++i];
148
+ else if (a === '--state') args.todoState = argv[++i];
110
149
  else if (a === '--run-id') args.runId = argv[++i];
111
150
  else if (a === '--output') args.output = argv[++i];
112
151
  else if (a === '--output-dir') {
@@ -147,10 +186,31 @@ function parseArgs(argv) {
147
186
  else if (a === '--stale-after') args.staleAfter = argv[++i];
148
187
  else if (a === '--until') args.until = argv[++i];
149
188
  else if (a === '--reason') args.reason = argv[++i];
189
+ else if (a === '--wait-kind') args.waitKind = argv[++i];
190
+ else if (a === '--wait-id') args.waitId = argv[++i];
191
+ else if (a === '--execution-key') args.executionKey = argv[++i];
192
+ else if (a === '--idempotency-key') args.idempotencyKey = argv[++i];
193
+ else if (a === '--kind') args.kind = argv[++i];
194
+ else if (a === '--authorization-scope') args.authorizationScope = argv[++i];
195
+ else if (a === '--request-json') args.requestJson = argv[++i];
196
+ else if (a === '--fencing-token') args.fencingToken = Number.parseInt(argv[++i], 10);
197
+ else if (a === '--completed') args.completed = true;
198
+ else if (a === '--outcome') args.outcome = argv[++i];
199
+ else if (a === '--evidence') args.evidence = argv[++i];
200
+ else if (a === '--recovery-signal') args.recoverySignal = argv[++i];
201
+ else if (a === '--now') args.now = argv[++i];
202
+ else if (a === '--reminder-interval-ms') args.reminderIntervalMs = Number.parseInt(argv[++i], 10);
203
+ else if (a === '--escalation-interval-ms') args.escalationIntervalMs = Number.parseInt(argv[++i], 10);
204
+ else if (a === '--wait-timeout-ms') args.waitTimeoutMs = Number.parseInt(argv[++i], 10);
205
+ else if (a === '--max-reminders') args.maxReminders = Number.parseInt(argv[++i], 10);
150
206
  else if (a === '--decision') args.decision = argv[++i];
151
207
  else if (a === '--comment') args.comment = argv[++i];
152
208
  else if (a === '--reviewer') args.reviewer = argv[++i];
153
209
  else if (a === '--limit') args.limit = Number.parseInt(argv[++i], 10);
210
+ else if (a === '--host') args.host = argv[++i];
211
+ else if (a === '--port') args.port = Number.parseInt(argv[++i], 10);
212
+ else if (a === '--query') args.query = argv[++i];
213
+ else if (a === '--max-age-seconds') args.maxAgeSeconds = Number.parseInt(argv[++i], 10);
154
214
  else if (a === '--notify-command') args.notifyCommand = argv[++i];
155
215
  else if (a === '--gate-id') args.gateId = argv[++i];
156
216
  else if (a === '--input') args.input = argv[++i];
@@ -185,8 +245,10 @@ function parseArgs(argv) {
185
245
  else if (a === '--supersede-active') args.supersedeActive = true;
186
246
  else if (a === '--amend-active') args.amendActive = true;
187
247
  else if (a === '--dry-run') args.dryRun = true;
248
+ else if (a === '--verified') args.verified = true;
188
249
  else if (a === '--allow-dirty') args.allowDirty = true;
189
250
  else if (a === '--include-orphans') args.includeOrphans = true;
251
+ else if (a === '--allow-non-loopback') args.allowNonLoopback = true;
190
252
  else if (a === '--json') args.json = true;
191
253
  else if (a === '--force') args.force = true;
192
254
  else if (a === '--help' || a === '-h') args.help = true;
@@ -1552,6 +1614,10 @@ Usage:
1552
1614
  loop-engineering status [--config configs/loops/name.json] [--root <workspace>]
1553
1615
  loop-engineering summarize [--id name | --queue name] [--limit 20] [--root <workspace>] [--json]
1554
1616
  loop-engineering doctor [--root <workspace>] [--json]
1617
+ loop-engineering dashboard-inspect [--id todo-id] [--state state] [--query text] [--root <workspace>] [--json]
1618
+ loop-engineering dashboard-health [--max-age-seconds 3600] [--root <workspace>] [--json]
1619
+ loop-engineering dashboard-export --output-dir directory [--root <workspace>] [--json]
1620
+ loop-engineering dashboard-serve [--host 127.0.0.1] [--port 0] [--allow-non-loopback] [--root <workspace>]
1555
1621
  loop-engineering repair-plan --id name [--output repair-plan.json] [--root <workspace>] [--json] [--force]
1556
1622
  loop-engineering project-intake --name project --brief "Project brief" [--type auto|web_app|code_project|research|content|ops|qa|knowledge_base|infra_audit|assistant_workflow] [--queue name] [--check "npm test"] [--root <workspace>] [--json]
1557
1623
  loop-engineering project-plan --project project [--root <workspace>] [--json] [--force]
@@ -1562,7 +1628,17 @@ Usage:
1562
1628
  loop-engineering run-queue --queue name --dispatcher "command" [--preflight-config configs/loops/name.json] [--root <workspace>]
1563
1629
  loop-engineering run-queue-drain --config configs/loops/queues/name.json [--max-tasks 100] [--root <workspace>]
1564
1630
  loop-engineering queue-status --queue name [--root <workspace>] [--json]
1631
+ loop-engineering queue-park --queue name --task-id id --wait-kind human_input|external_condition --reason "..." [--wait-timeout-ms N --reminder-interval-ms N --escalation-interval-ms N --max-reminders N] [--root <workspace>] [--json]
1632
+ loop-engineering action-reserve --idempotency-key key --kind paid_api|notification|deployment|process_control|publication|external_message|gated_mutation --authorization-scope scope --request-json '{}' [--root <workspace>]
1633
+ loop-engineering action-claim --idempotency-key key --owner worker [--lease-ms N] [--root <workspace>]
1634
+ loop-engineering action-inspect --idempotency-key key [--root <workspace>]
1635
+ loop-engineering action-settle --idempotency-key key --fencing-token N [--evidence text] [--root <workspace>]
1636
+ loop-engineering action-release --idempotency-key key [--fencing-token N] --reason text [--evidence text] [--root <workspace>]
1637
+ loop-engineering action-reconcile --idempotency-key key --outcome accepted|not_accepted --evidence text [--root <workspace>]
1638
+ loop-engineering queue-wait-tick --queue name (--notify-command "command" | --dry-run) [--now ISO] [--root <workspace>] [--json]
1639
+ loop-engineering queue-wait-resume --queue name --task-id id --verified --recovery-signal "..." [--root <workspace>] [--json]
1565
1640
  loop-engineering queue-terminal-notify --queue name (--notify-command "command" | --dry-run) [--root <workspace>] [--json]
1641
+ loop-engineering queue-acceptance-refresh --queue name --task-id id [--root <workspace>] [--json]
1566
1642
  loop-engineering queue-scheduler-tick --queue name [--config configs/loops/queues/name.json] [--plan-only] [--force-due] [--initial-interval 10m] [--min-interval 1m] [--max-interval 4h] [--jitter 30s] [--no-progress-report] [--progress-report-interval 30m] [--progress-notify-command "command"] [--root <workspace>] [--json]
1567
1643
  loop-engineering queue-init --queue name [--root <workspace>] [--force]
1568
1644
  loop-engineering code-queue-init --queue name [--root <workspace>] [--force]
@@ -1996,6 +2072,14 @@ async function queueTerminalNotifyCommand(args) {
1996
2072
  return result.failed > 0 ? 1 : 0;
1997
2073
  }
1998
2074
 
2075
+ async function queueAcceptanceRefreshCommand(args) {
2076
+ if (!args.queue || !args.taskId) throw new Error('queue-acceptance-refresh requires --queue and --task-id.');
2077
+ const result = await refreshTaskAcceptance(args.root, args);
2078
+ if (args.json) console.log(JSON.stringify(result, null, 2));
2079
+ else console.log(`${result.queue}: ${result.taskId} ${result.outcome}${result.status ? ` (${result.status})` : ''}`);
2080
+ return 0;
2081
+ }
2082
+
1999
2083
  async function queueHumanInputNotifyCommand(args) {
2000
2084
  if (!args.queue) throw new Error('queue-human-input-notify requires --queue.');
2001
2085
  const result = await notifyHumanInputRequests(args.root, args);
@@ -2012,6 +2096,34 @@ async function queueHumanInputResolveCommand(args) {
2012
2096
  return 0;
2013
2097
  }
2014
2098
 
2099
+ async function queueParkCommand(args) {
2100
+ if (!args.queue || !args.taskId) throw new Error('queue-park requires --queue and --task-id.');
2101
+ const result = await parkQueueTask(args.root, {
2102
+ ...args,
2103
+ kind: args.waitKind,
2104
+ policy: { timeoutMs: args.waitTimeoutMs, reminderIntervalMs: args.reminderIntervalMs, escalationIntervalMs: args.escalationIntervalMs, maxReminders: args.maxReminders }
2105
+ });
2106
+ if (args.json) console.log(JSON.stringify(result, null, 2));
2107
+ else console.log(`${args.taskId}: ${result.outcome}`);
2108
+ return 0;
2109
+ }
2110
+
2111
+ async function queueWaitTickCommand(args) {
2112
+ if (!args.queue) throw new Error('queue-wait-tick requires --queue.');
2113
+ const result = await tickParkedTasks(args.root, args);
2114
+ if (args.json) console.log(JSON.stringify(result, null, 2));
2115
+ else console.log(`${result.queue}: inspected=${result.inspected} sent=${result.sent} failed=${result.failed}`);
2116
+ return result.failed > 0 ? 1 : 0;
2117
+ }
2118
+
2119
+ async function queueWaitResumeCommand(args) {
2120
+ if (!args.queue || !args.taskId) throw new Error('queue-wait-resume requires --queue and --task-id.');
2121
+ const result = await resumeParkedTask(args.root, args);
2122
+ if (args.json) console.log(JSON.stringify(result, null, 2));
2123
+ else console.log(`${args.taskId}: ${result.outcome}`);
2124
+ return 0;
2125
+ }
2126
+
2015
2127
  async function queueSchedulerTickCommand(args) {
2016
2128
  const inferredConfig = args.queue ? `configs/loops/queues/${args.queue}.json` : undefined;
2017
2129
  const inferredConfigExists = inferredConfig
@@ -5501,6 +5613,9 @@ async function main() {
5501
5613
  if (command === 'status') return statusCommand(args);
5502
5614
  if (command === 'summarize') return summarizeCommand(args);
5503
5615
  if (command === 'doctor') return doctorCommand(args);
5616
+ if (command.startsWith('dashboard-')) return dashboardCommand(command, args);
5617
+ if (command === 'agent-register' || command.startsWith('todo-')) return todoControlPlaneCommand(command, args);
5618
+ if (command.startsWith('action-')) return actionReservationCommand(command, args);
5504
5619
  if (command === 'repair-plan') return repairPlanCommand(args);
5505
5620
  if (command === 'project-intake') return projectIntakeCommand(args);
5506
5621
  if (command === 'project-plan') return projectPlanCommand(args);
@@ -5510,7 +5625,11 @@ async function main() {
5510
5625
  if (command === 'run-queue') return runQueueCommand(args);
5511
5626
  if (command === 'run-queue-drain') return runQueueDrainCommand(args);
5512
5627
  if (command === 'queue-status') return queueStatusCommand(args);
5628
+ if (command === 'queue-park') return queueParkCommand(args);
5629
+ if (command === 'queue-wait-tick') return queueWaitTickCommand(args);
5630
+ if (command === 'queue-wait-resume') return queueWaitResumeCommand(args);
5513
5631
  if (command === 'queue-terminal-notify') return queueTerminalNotifyCommand(args);
5632
+ if (command === 'queue-acceptance-refresh') return queueAcceptanceRefreshCommand(args);
5514
5633
  if (command === 'queue-human-input-notify') return queueHumanInputNotifyCommand(args);
5515
5634
  if (command === 'queue-human-input-resolve') return queueHumanInputResolveCommand(args);
5516
5635
  if (command === 'queue-scheduler-tick') return queueSchedulerTickCommand(args);
@@ -5561,6 +5680,79 @@ async function main() {
5561
5680
  throw new Error(`Unknown command: ${command}`);
5562
5681
  }
5563
5682
 
5683
+ async function dashboardCommand(command, args) {
5684
+ if (command === 'dashboard-serve') {
5685
+ const server = await createDashboardServer(args.root, { host: args.host, port: args.port, allowNonLoopback: args.allowNonLoopback });
5686
+ const address = server.address();
5687
+ console.log(JSON.stringify({ status: 'serving', read_only: true, address }, null, 2));
5688
+ return new Promise((resolve) => {
5689
+ const stop = () => server.close(() => resolve(0));
5690
+ process.once('SIGINT', stop); process.once('SIGTERM', stop);
5691
+ });
5692
+ }
5693
+ if (command === 'dashboard-export') {
5694
+ if (!args.outputDir || args.outputDir === true) throw new Error('dashboard-export requires --output-dir.');
5695
+ console.log(JSON.stringify(await exportDashboard(args.root, args.outputDir, { now: args.now }), null, 2));
5696
+ return 0;
5697
+ }
5698
+ const projection = await buildOperatorProjection(args.root, { now: args.now });
5699
+ if (command === 'dashboard-health') {
5700
+ const result = dashboardHealth(projection, { maxAgeSeconds: args.maxAgeSeconds });
5701
+ console.log(JSON.stringify(result, null, 2)); return result.status === 'ok' ? 0 : 2;
5702
+ }
5703
+ if (command === 'dashboard-inspect') {
5704
+ const filtered = filterProjection(projection, { query: args.query, state: args.todoState });
5705
+ const result = args.id ? filtered.todos.find((item) => item.id === args.id) ?? filtered.queues.flatMap((queue) => queue.tasks).find((item) => item.id === args.id) ?? null : filtered;
5706
+ console.log(JSON.stringify(result, null, 2)); return result === null ? 1 : 0;
5707
+ }
5708
+ throw new Error(`Unknown command: ${command}`);
5709
+ }
5710
+
5711
+ async function jsonInput(raw, label) {
5712
+ if (!raw) throw new Error(`${label} is required.`);
5713
+ if (raw.trim().startsWith('{')) return JSON.parse(raw);
5714
+ return JSON.parse(await readFile(path.resolve(raw), 'utf8'));
5715
+ }
5716
+
5717
+ async function todoControlPlaneCommand(command, args) {
5718
+ let result;
5719
+ if (command === 'agent-register') result = await registerAgent(args.root, await jsonInput(args.agentJson, '--agent-json'));
5720
+ else if (command === 'todo-create') result = await createTodo(args.root, await jsonInput(args.todoJson, '--todo-json'));
5721
+ else if (command === 'todo-list') result = await listTodos(args.root, { state: args.todoState });
5722
+ else if (command === 'todo-inspect') result = await inspectTodo(args.root, args.todoId);
5723
+ else if (command === 'todo-claim') result = await claimTodo(args.root, args);
5724
+ else if (command === 'todo-renew') result = await renewTodo(args.root, args);
5725
+ else if (command === 'todo-release') result = await releaseTodo(args.root, args);
5726
+ else if (command === 'todo-handoff') result = await handoffTodo(args.root, args);
5727
+ else if (command === 'todo-accept') result = await decideHandoff(args.root, { ...args, accept: true });
5728
+ else if (command === 'todo-reject') result = await decideHandoff(args.root, { ...args, accept: false });
5729
+ else if (command === 'todo-recover') result = await recoverTodos(args.root, args);
5730
+ else if (command === 'todo-import-legacy') result = await importLegacyTodos(args.root, args);
5731
+ else throw new Error(`Unknown command: ${command}`);
5732
+ console.log(JSON.stringify(result, null, 2));
5733
+ return result === null ? 1 : 0;
5734
+ }
5735
+
5736
+ async function actionReservationCommand(command, args) {
5737
+ if (!args.idempotencyKey) throw new Error(`${command} requires --idempotency-key.`);
5738
+ let result;
5739
+ if (command === 'action-reserve') {
5740
+ result = await reserveAction(args.root, {
5741
+ idempotencyKey: args.idempotencyKey,
5742
+ kind: args.kind,
5743
+ authorizationScope: args.authorizationScope,
5744
+ request: JSON.parse(args.requestJson ?? '{}')
5745
+ });
5746
+ } else if (command === 'action-inspect') result = await inspectAction(args.root, args.idempotencyKey);
5747
+ else if (command === 'action-claim') result = await claimAction(args.root, { idempotencyKey: args.idempotencyKey, owner: args.owner, leaseMs: args.leaseMs });
5748
+ else if (command === 'action-settle') result = await settleAction(args.root, { idempotencyKey: args.idempotencyKey, fencingToken: args.fencingToken, evidence: args.evidence });
5749
+ else if (command === 'action-release') result = await releaseAction(args.root, { idempotencyKey: args.idempotencyKey, fencingToken: args.fencingToken, reason: args.reason, evidence: args.evidence });
5750
+ else if (command === 'action-reconcile') result = await reconcileAction(args.root, { idempotencyKey: args.idempotencyKey, outcome: args.outcome, evidence: args.evidence });
5751
+ else throw new Error(`Unknown command: ${command}`);
5752
+ console.log(JSON.stringify(result, null, 2));
5753
+ return result === null ? 1 : 0;
5754
+ }
5755
+
5564
5756
  main()
5565
5757
  .then((code) => { process.exitCode = code; })
5566
5758
  .catch((err) => {