taskforce-loop-engineering 0.10.0 → 0.12.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.12.0 - 2026-08-13
6
+
7
+ - Add P3 read-only Operator Dashboard, normalized schema, loopback HTTP/JSON API, static export, inspect and health commands.
8
+ - Integrate P0 gates, P1 reconciliation and P2 ownership/lease/handoff projections with legacy artifacts.
9
+ - Add redaction, traversal/XSS/bind protections and deterministic/security/performance coverage.
10
+
11
+ ## 0.11.0 - 2026-08-13
12
+
13
+ - 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.
14
+ - Extend `queue-status` with operator-visible waiting states and add `queue-park`, `queue-wait-tick`, and `queue-wait-resume` commands.
15
+ - Add a VPS-down/SSH-banner-timeout regression fixture proving throttled reminders, preserved unconsumed authorization, verified resume, and restart-safe idempotency.
16
+ - 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.
17
+ - Add concurrency and crash-restart acceptance coverage while preserving Human-Gate Lifecycle v2.
18
+ - 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.
19
+
3
20
  ## 0.10.0 - 2026-08-11
4
21
 
5
22
  - 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,63 @@
1
1
  # Taskforce Loop Engineering
2
2
 
3
+ ## Read-only operator dashboard (P3)
4
+
5
+ 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.
6
+
7
+ 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.
8
+
9
+ ## Action idempotency and reservation
10
+
11
+ Every side-effecting action must reserve a durable idempotency key before an
12
+ adapter is invoked. The reservation binds a canonical request fingerprint to an
13
+ authorization scope. A worker then atomically claims a lease and receives a
14
+ monotonic fencing token; only that token can settle the action or release its
15
+ reservation. Paid API, notification, and deployment adapters expose the same
16
+ lifecycle from `lib/action-reservations.mjs`.
17
+
18
+ ```bash
19
+ loop-engineering action-reserve --idempotency-key task:step:attempt \
20
+ --kind paid_api --authorization-scope approval:task:provider \
21
+ --request-json '{"model":"example","requestDigest":"sha256"}'
22
+ loop-engineering action-claim --idempotency-key task:step:attempt \
23
+ --owner worker-1 --lease-ms 60000
24
+ loop-engineering action-settle --idempotency-key task:step:attempt \
25
+ --fencing-token 1 --evidence upstream-request-id
26
+ ```
27
+
28
+ An expired claim becomes `unknown`, not runnable. Operators must inspect and
29
+ reconcile it as `accepted` (durably settle and consume authorization) or
30
+ `not_accepted` (return to reserved) using `action-inspect` and
31
+ `action-reconcile`. This is the crash boundary that prevents blind replay and
32
+ double spend after an upstream acceptance whose local commit was interrupted.
33
+ Terminal records retain settlement or release evidence and an append-only event
34
+ history under `runtime/loops/action-reservations/`.
35
+
36
+ ## Human-Gate Lifecycle v2
37
+
38
+ Park work without treating an external dependency as failure or repeatedly
39
+ retrying a privileged action:
40
+
41
+ ```bash
42
+ loop-engineering queue-park --queue agent-tasks --task-id <id> \
43
+ --wait-kind external_condition --reason "SSH banner timed out" \
44
+ --wait-timeout-ms 86400000 --reminder-interval-ms 3600000 \
45
+ --escalation-interval-ms 86400000 --max-reminders 3
46
+
47
+ loop-engineering queue-wait-tick --queue agent-tasks \
48
+ --notify-command '<source-bound notifier>'
49
+
50
+ loop-engineering queue-wait-resume --queue agent-tasks --task-id <id> \
51
+ --verified --recovery-signal 'probe=vps-1;ssh_banner=verified'
52
+ ```
53
+
54
+ `queue-status --json` distinguishes `waiting_for_human`,
55
+ `external_condition_wait`, `timed_out_or_escalated`, and `runnable` state.
56
+ Reminder and escalation sends are throttled and leave durable evidence under
57
+ `runtime/loops/<queue>/wait-notifications/`. Timeout never rejects a task,
58
+ consumes a stored authorization, or repeats an action. Verified resume preserves
59
+ the original task/wait identity and exactly-once execution boundary.
60
+
3
61
  Durable loop engineering for repeated agent work on OpenClaw and Hermes Agent. It provides a small
4
62
  Node CLI that executes JSON loop specs, records append-only run artifacts, and
5
63
  uses a circuit breaker to escalate repeated failures.
@@ -1270,3 +1328,6 @@ ClawHub:
1270
1328
  ```text
1271
1329
  https://clawhub.ai/ambitioncn/skills/taskforce-loop-engineering
1272
1330
  ```
1331
+ # P2 multi-agent control plane
1332
+
1333
+ 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.
@@ -38,6 +38,8 @@ import {
38
38
  nextState,
39
39
  notifyTerminalTasks,
40
40
  notifyHumanInputRequests,
41
+ parkQueueTask,
42
+ resumeParkedTask,
41
43
  resolveHumanInput,
42
44
  enqueueTask,
43
45
  projectIntake,
@@ -56,6 +58,7 @@ import {
56
58
  queueSchedulerTick,
57
59
  queueSubdirFor,
58
60
  queueStatus,
61
+ tickParkedTasks,
59
62
  routeLoopMessage,
60
63
  summarizeLoopRuns,
61
64
  runCheck,
@@ -68,6 +71,34 @@ import {
68
71
  workflowTuningPlan,
69
72
  writeJson
70
73
  } from '../lib/core.mjs';
74
+ import {
75
+ claimAction,
76
+ inspectAction,
77
+ reconcileAction,
78
+ releaseAction,
79
+ reserveAction,
80
+ settleAction
81
+ } from '../lib/action-reservations.mjs';
82
+ import {
83
+ claimTodo,
84
+ createTodo,
85
+ decideHandoff,
86
+ handoffTodo,
87
+ importLegacyTodos,
88
+ inspectTodo,
89
+ listTodos,
90
+ recoverTodos,
91
+ registerAgent,
92
+ releaseTodo,
93
+ renewTodo
94
+ } from '../lib/todo-control-plane.mjs';
95
+ import {
96
+ buildOperatorProjection,
97
+ createDashboardServer,
98
+ dashboardHealth,
99
+ exportDashboard,
100
+ filterProjection
101
+ } from '../lib/operator-dashboard.mjs';
71
102
 
72
103
  function parseArgs(argv) {
73
104
  const args = { _: [], root: process.cwd(), json: false, force: false };
@@ -107,6 +138,13 @@ function parseArgs(argv) {
107
138
  args.checks.push(argv[++i]);
108
139
  }
109
140
  else if (a === '--task-id') args.taskId = argv[++i];
141
+ else if (a === '--todo-id') args.todoId = argv[++i];
142
+ else if (a === '--agent-id') args.agentId = argv[++i];
143
+ else if (a === '--target-agent-id') args.targetAgentId = argv[++i];
144
+ else if (a === '--handoff-id') args.handoffId = argv[++i];
145
+ else if (a === '--todo-json') args.todoJson = argv[++i];
146
+ else if (a === '--agent-json') args.agentJson = argv[++i];
147
+ else if (a === '--state') args.todoState = argv[++i];
110
148
  else if (a === '--run-id') args.runId = argv[++i];
111
149
  else if (a === '--output') args.output = argv[++i];
112
150
  else if (a === '--output-dir') {
@@ -147,10 +185,31 @@ function parseArgs(argv) {
147
185
  else if (a === '--stale-after') args.staleAfter = argv[++i];
148
186
  else if (a === '--until') args.until = argv[++i];
149
187
  else if (a === '--reason') args.reason = argv[++i];
188
+ else if (a === '--wait-kind') args.waitKind = argv[++i];
189
+ else if (a === '--wait-id') args.waitId = argv[++i];
190
+ else if (a === '--execution-key') args.executionKey = argv[++i];
191
+ else if (a === '--idempotency-key') args.idempotencyKey = argv[++i];
192
+ else if (a === '--kind') args.kind = argv[++i];
193
+ else if (a === '--authorization-scope') args.authorizationScope = argv[++i];
194
+ else if (a === '--request-json') args.requestJson = argv[++i];
195
+ else if (a === '--fencing-token') args.fencingToken = Number.parseInt(argv[++i], 10);
196
+ else if (a === '--completed') args.completed = true;
197
+ else if (a === '--outcome') args.outcome = argv[++i];
198
+ else if (a === '--evidence') args.evidence = argv[++i];
199
+ else if (a === '--recovery-signal') args.recoverySignal = argv[++i];
200
+ else if (a === '--now') args.now = argv[++i];
201
+ else if (a === '--reminder-interval-ms') args.reminderIntervalMs = Number.parseInt(argv[++i], 10);
202
+ else if (a === '--escalation-interval-ms') args.escalationIntervalMs = Number.parseInt(argv[++i], 10);
203
+ else if (a === '--wait-timeout-ms') args.waitTimeoutMs = Number.parseInt(argv[++i], 10);
204
+ else if (a === '--max-reminders') args.maxReminders = Number.parseInt(argv[++i], 10);
150
205
  else if (a === '--decision') args.decision = argv[++i];
151
206
  else if (a === '--comment') args.comment = argv[++i];
152
207
  else if (a === '--reviewer') args.reviewer = argv[++i];
153
208
  else if (a === '--limit') args.limit = Number.parseInt(argv[++i], 10);
209
+ else if (a === '--host') args.host = argv[++i];
210
+ else if (a === '--port') args.port = Number.parseInt(argv[++i], 10);
211
+ else if (a === '--query') args.query = argv[++i];
212
+ else if (a === '--max-age-seconds') args.maxAgeSeconds = Number.parseInt(argv[++i], 10);
154
213
  else if (a === '--notify-command') args.notifyCommand = argv[++i];
155
214
  else if (a === '--gate-id') args.gateId = argv[++i];
156
215
  else if (a === '--input') args.input = argv[++i];
@@ -185,8 +244,10 @@ function parseArgs(argv) {
185
244
  else if (a === '--supersede-active') args.supersedeActive = true;
186
245
  else if (a === '--amend-active') args.amendActive = true;
187
246
  else if (a === '--dry-run') args.dryRun = true;
247
+ else if (a === '--verified') args.verified = true;
188
248
  else if (a === '--allow-dirty') args.allowDirty = true;
189
249
  else if (a === '--include-orphans') args.includeOrphans = true;
250
+ else if (a === '--allow-non-loopback') args.allowNonLoopback = true;
190
251
  else if (a === '--json') args.json = true;
191
252
  else if (a === '--force') args.force = true;
192
253
  else if (a === '--help' || a === '-h') args.help = true;
@@ -1552,6 +1613,10 @@ Usage:
1552
1613
  loop-engineering status [--config configs/loops/name.json] [--root <workspace>]
1553
1614
  loop-engineering summarize [--id name | --queue name] [--limit 20] [--root <workspace>] [--json]
1554
1615
  loop-engineering doctor [--root <workspace>] [--json]
1616
+ loop-engineering dashboard-inspect [--id todo-id] [--state state] [--query text] [--root <workspace>] [--json]
1617
+ loop-engineering dashboard-health [--max-age-seconds 3600] [--root <workspace>] [--json]
1618
+ loop-engineering dashboard-export --output-dir directory [--root <workspace>] [--json]
1619
+ loop-engineering dashboard-serve [--host 127.0.0.1] [--port 0] [--allow-non-loopback] [--root <workspace>]
1555
1620
  loop-engineering repair-plan --id name [--output repair-plan.json] [--root <workspace>] [--json] [--force]
1556
1621
  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
1622
  loop-engineering project-plan --project project [--root <workspace>] [--json] [--force]
@@ -1562,6 +1627,15 @@ Usage:
1562
1627
  loop-engineering run-queue --queue name --dispatcher "command" [--preflight-config configs/loops/name.json] [--root <workspace>]
1563
1628
  loop-engineering run-queue-drain --config configs/loops/queues/name.json [--max-tasks 100] [--root <workspace>]
1564
1629
  loop-engineering queue-status --queue name [--root <workspace>] [--json]
1630
+ 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]
1631
+ 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>]
1632
+ loop-engineering action-claim --idempotency-key key --owner worker [--lease-ms N] [--root <workspace>]
1633
+ loop-engineering action-inspect --idempotency-key key [--root <workspace>]
1634
+ loop-engineering action-settle --idempotency-key key --fencing-token N [--evidence text] [--root <workspace>]
1635
+ loop-engineering action-release --idempotency-key key [--fencing-token N] --reason text [--evidence text] [--root <workspace>]
1636
+ loop-engineering action-reconcile --idempotency-key key --outcome accepted|not_accepted --evidence text [--root <workspace>]
1637
+ loop-engineering queue-wait-tick --queue name (--notify-command "command" | --dry-run) [--now ISO] [--root <workspace>] [--json]
1638
+ loop-engineering queue-wait-resume --queue name --task-id id --verified --recovery-signal "..." [--root <workspace>] [--json]
1565
1639
  loop-engineering queue-terminal-notify --queue name (--notify-command "command" | --dry-run) [--root <workspace>] [--json]
1566
1640
  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
1641
  loop-engineering queue-init --queue name [--root <workspace>] [--force]
@@ -2012,6 +2086,34 @@ async function queueHumanInputResolveCommand(args) {
2012
2086
  return 0;
2013
2087
  }
2014
2088
 
2089
+ async function queueParkCommand(args) {
2090
+ if (!args.queue || !args.taskId) throw new Error('queue-park requires --queue and --task-id.');
2091
+ const result = await parkQueueTask(args.root, {
2092
+ ...args,
2093
+ kind: args.waitKind,
2094
+ policy: { timeoutMs: args.waitTimeoutMs, reminderIntervalMs: args.reminderIntervalMs, escalationIntervalMs: args.escalationIntervalMs, maxReminders: args.maxReminders }
2095
+ });
2096
+ if (args.json) console.log(JSON.stringify(result, null, 2));
2097
+ else console.log(`${args.taskId}: ${result.outcome}`);
2098
+ return 0;
2099
+ }
2100
+
2101
+ async function queueWaitTickCommand(args) {
2102
+ if (!args.queue) throw new Error('queue-wait-tick requires --queue.');
2103
+ const result = await tickParkedTasks(args.root, args);
2104
+ if (args.json) console.log(JSON.stringify(result, null, 2));
2105
+ else console.log(`${result.queue}: inspected=${result.inspected} sent=${result.sent} failed=${result.failed}`);
2106
+ return result.failed > 0 ? 1 : 0;
2107
+ }
2108
+
2109
+ async function queueWaitResumeCommand(args) {
2110
+ if (!args.queue || !args.taskId) throw new Error('queue-wait-resume requires --queue and --task-id.');
2111
+ const result = await resumeParkedTask(args.root, args);
2112
+ if (args.json) console.log(JSON.stringify(result, null, 2));
2113
+ else console.log(`${args.taskId}: ${result.outcome}`);
2114
+ return 0;
2115
+ }
2116
+
2015
2117
  async function queueSchedulerTickCommand(args) {
2016
2118
  const inferredConfig = args.queue ? `configs/loops/queues/${args.queue}.json` : undefined;
2017
2119
  const inferredConfigExists = inferredConfig
@@ -5501,6 +5603,9 @@ async function main() {
5501
5603
  if (command === 'status') return statusCommand(args);
5502
5604
  if (command === 'summarize') return summarizeCommand(args);
5503
5605
  if (command === 'doctor') return doctorCommand(args);
5606
+ if (command.startsWith('dashboard-')) return dashboardCommand(command, args);
5607
+ if (command === 'agent-register' || command.startsWith('todo-')) return todoControlPlaneCommand(command, args);
5608
+ if (command.startsWith('action-')) return actionReservationCommand(command, args);
5504
5609
  if (command === 'repair-plan') return repairPlanCommand(args);
5505
5610
  if (command === 'project-intake') return projectIntakeCommand(args);
5506
5611
  if (command === 'project-plan') return projectPlanCommand(args);
@@ -5510,6 +5615,9 @@ async function main() {
5510
5615
  if (command === 'run-queue') return runQueueCommand(args);
5511
5616
  if (command === 'run-queue-drain') return runQueueDrainCommand(args);
5512
5617
  if (command === 'queue-status') return queueStatusCommand(args);
5618
+ if (command === 'queue-park') return queueParkCommand(args);
5619
+ if (command === 'queue-wait-tick') return queueWaitTickCommand(args);
5620
+ if (command === 'queue-wait-resume') return queueWaitResumeCommand(args);
5513
5621
  if (command === 'queue-terminal-notify') return queueTerminalNotifyCommand(args);
5514
5622
  if (command === 'queue-human-input-notify') return queueHumanInputNotifyCommand(args);
5515
5623
  if (command === 'queue-human-input-resolve') return queueHumanInputResolveCommand(args);
@@ -5561,6 +5669,79 @@ async function main() {
5561
5669
  throw new Error(`Unknown command: ${command}`);
5562
5670
  }
5563
5671
 
5672
+ async function dashboardCommand(command, args) {
5673
+ if (command === 'dashboard-serve') {
5674
+ const server = await createDashboardServer(args.root, { host: args.host, port: args.port, allowNonLoopback: args.allowNonLoopback });
5675
+ const address = server.address();
5676
+ console.log(JSON.stringify({ status: 'serving', read_only: true, address }, null, 2));
5677
+ return new Promise((resolve) => {
5678
+ const stop = () => server.close(() => resolve(0));
5679
+ process.once('SIGINT', stop); process.once('SIGTERM', stop);
5680
+ });
5681
+ }
5682
+ if (command === 'dashboard-export') {
5683
+ if (!args.outputDir || args.outputDir === true) throw new Error('dashboard-export requires --output-dir.');
5684
+ console.log(JSON.stringify(await exportDashboard(args.root, args.outputDir, { now: args.now }), null, 2));
5685
+ return 0;
5686
+ }
5687
+ const projection = await buildOperatorProjection(args.root, { now: args.now });
5688
+ if (command === 'dashboard-health') {
5689
+ const result = dashboardHealth(projection, { maxAgeSeconds: args.maxAgeSeconds });
5690
+ console.log(JSON.stringify(result, null, 2)); return result.status === 'ok' ? 0 : 2;
5691
+ }
5692
+ if (command === 'dashboard-inspect') {
5693
+ const filtered = filterProjection(projection, { query: args.query, state: args.todoState });
5694
+ 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;
5695
+ console.log(JSON.stringify(result, null, 2)); return result === null ? 1 : 0;
5696
+ }
5697
+ throw new Error(`Unknown command: ${command}`);
5698
+ }
5699
+
5700
+ async function jsonInput(raw, label) {
5701
+ if (!raw) throw new Error(`${label} is required.`);
5702
+ if (raw.trim().startsWith('{')) return JSON.parse(raw);
5703
+ return JSON.parse(await readFile(path.resolve(raw), 'utf8'));
5704
+ }
5705
+
5706
+ async function todoControlPlaneCommand(command, args) {
5707
+ let result;
5708
+ if (command === 'agent-register') result = await registerAgent(args.root, await jsonInput(args.agentJson, '--agent-json'));
5709
+ else if (command === 'todo-create') result = await createTodo(args.root, await jsonInput(args.todoJson, '--todo-json'));
5710
+ else if (command === 'todo-list') result = await listTodos(args.root, { state: args.todoState });
5711
+ else if (command === 'todo-inspect') result = await inspectTodo(args.root, args.todoId);
5712
+ else if (command === 'todo-claim') result = await claimTodo(args.root, args);
5713
+ else if (command === 'todo-renew') result = await renewTodo(args.root, args);
5714
+ else if (command === 'todo-release') result = await releaseTodo(args.root, args);
5715
+ else if (command === 'todo-handoff') result = await handoffTodo(args.root, args);
5716
+ else if (command === 'todo-accept') result = await decideHandoff(args.root, { ...args, accept: true });
5717
+ else if (command === 'todo-reject') result = await decideHandoff(args.root, { ...args, accept: false });
5718
+ else if (command === 'todo-recover') result = await recoverTodos(args.root, args);
5719
+ else if (command === 'todo-import-legacy') result = await importLegacyTodos(args.root, args);
5720
+ else throw new Error(`Unknown command: ${command}`);
5721
+ console.log(JSON.stringify(result, null, 2));
5722
+ return result === null ? 1 : 0;
5723
+ }
5724
+
5725
+ async function actionReservationCommand(command, args) {
5726
+ if (!args.idempotencyKey) throw new Error(`${command} requires --idempotency-key.`);
5727
+ let result;
5728
+ if (command === 'action-reserve') {
5729
+ result = await reserveAction(args.root, {
5730
+ idempotencyKey: args.idempotencyKey,
5731
+ kind: args.kind,
5732
+ authorizationScope: args.authorizationScope,
5733
+ request: JSON.parse(args.requestJson ?? '{}')
5734
+ });
5735
+ } else if (command === 'action-inspect') result = await inspectAction(args.root, args.idempotencyKey);
5736
+ else if (command === 'action-claim') result = await claimAction(args.root, { idempotencyKey: args.idempotencyKey, owner: args.owner, leaseMs: args.leaseMs });
5737
+ else if (command === 'action-settle') result = await settleAction(args.root, { idempotencyKey: args.idempotencyKey, fencingToken: args.fencingToken, evidence: args.evidence });
5738
+ else if (command === 'action-release') result = await releaseAction(args.root, { idempotencyKey: args.idempotencyKey, fencingToken: args.fencingToken, reason: args.reason, evidence: args.evidence });
5739
+ else if (command === 'action-reconcile') result = await reconcileAction(args.root, { idempotencyKey: args.idempotencyKey, outcome: args.outcome, evidence: args.evidence });
5740
+ else throw new Error(`Unknown command: ${command}`);
5741
+ console.log(JSON.stringify(result, null, 2));
5742
+ return result === null ? 1 : 0;
5743
+ }
5744
+
5564
5745
  main()
5565
5746
  .then((code) => { process.exitCode = code; })
5566
5747
  .catch((err) => {