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.
- package/CHANGELOG.md +24 -0
- package/MIGRATING.md +47 -2
- package/README.md +70 -0
- package/bin/loop-engineering.mjs +192 -0
- package/docs/architecture.md +444 -0
- package/docs/multi-agent-control-plane.md +31 -0
- package/docs/operator-dashboard.md +27 -0
- package/docs/production-operations.md +29 -0
- package/docs/production-trust-backlog.json +13 -0
- package/docs/production-trust-contract.md +54 -0
- package/docs/release-0.12-acceptance.md +35 -0
- package/lib/action-reservations.mjs +196 -0
- package/lib/core.mjs +219 -2
- package/lib/durable-journal.mjs +90 -0
- package/lib/operator-dashboard.mjs +198 -0
- package/lib/runtime-adapter-v1.mjs +36 -0
- package/lib/todo-control-plane.mjs +287 -0
- package/lib/upgrade-planner.mjs +24 -0
- package/package.json +4 -2
- package/scripts/action-reservation-self-test.mjs +65 -0
- package/scripts/async-acceptance-refresh-self-test.mjs +46 -0
- package/scripts/durable-journal-self-test.mjs +24 -0
- package/scripts/human-gate-lifecycle-v2-self-test.mjs +81 -0
- package/scripts/live-runtime-soak.mjs +86 -0
- package/scripts/operator-dashboard-self-test.mjs +74 -0
- package/scripts/production-acceptance.mjs +8 -0
- package/scripts/production-soak.mjs +19 -0
- package/scripts/route-notify-self-test.mjs +1 -0
- package/scripts/runtime-adapter-contract-self-test.mjs +14 -0
- package/scripts/todo-control-plane-self-test.mjs +74 -0
- package/scripts/upgrade-planner-self-test.mjs +9 -0
- package/templates/operator-projection.schema.json +1 -0
- 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.
|
|
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.
|
|
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.
|
package/bin/loop-engineering.mjs
CHANGED
|
@@ -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) => {
|