taskforce-loop-engineering 0.12.0 → 0.14.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 +11 -0
- package/README.md +23 -0
- package/bin/loop-engineering.mjs +11 -0
- package/docs/adapter-sdk-terminal-contract.json +19 -0
- package/docs/production-operations.md +29 -0
- package/docs/production-trust-backlog.json +18 -0
- package/docs/production-trust-contract.md +53 -0
- package/docs/runtime-adapter-sdk.md +50 -0
- package/examples/adapter-sdk-demo.mjs +9 -0
- package/examples/code-worktree-queue.json +35 -0
- package/examples/queue-runner.json +37 -0
- package/examples/safe-canary.mjs +5 -0
- package/examples/workspace-health.json +35 -0
- package/lib/action-reservations.mjs +7 -0
- package/lib/core.mjs +66 -0
- package/lib/durable-journal.mjs +90 -0
- package/lib/execution-ledger.mjs +41 -0
- package/lib/operator-dashboard.mjs +18 -4
- package/lib/production-evidence.mjs +54 -0
- package/lib/runtime-adapter-sdk.mjs +115 -0
- package/lib/runtime-adapter-v1.mjs +36 -0
- package/lib/upgrade-planner.mjs +24 -0
- package/package.json +5 -1
- package/scripts/async-acceptance-refresh-self-test.mjs +46 -0
- package/scripts/durable-journal-self-test.mjs +24 -0
- package/scripts/execution-ledger-self-test.mjs +10 -0
- package/scripts/live-runtime-soak.mjs +97 -0
- package/scripts/operator-dashboard-self-test.mjs +10 -0
- package/scripts/production-acceptance.mjs +10 -0
- package/scripts/production-evidence-self-test.mjs +17 -0
- package/scripts/production-soak.mjs +43 -0
- package/scripts/runtime-adapter-conformance.mjs +21 -0
- package/scripts/runtime-adapter-contract-self-test.mjs +14 -0
- package/scripts/upgrade-planner-self-test.mjs +9 -0
- package/templates/github-production-trust.yml +20 -0
- package/templates/production-evidence.schema.json +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.14.0 - 2026-08-14
|
|
6
|
+
|
|
7
|
+
- Add the platform-neutral runtime adapter SDK v1 with OpenClaw, Hermes, Codex CLI, and Claude Code factories, shared conformance tests, fail-closed effects, redacted telemetry, migration guidance, and a credential-free demo.
|
|
8
|
+
|
|
9
|
+
## 0.13.0 - 2026-08-14
|
|
10
|
+
|
|
11
|
+
- Add versioned OpenClaw, Hermes, and custom runtime adapter contracts with a shared conformance suite.
|
|
12
|
+
- Add a checksummed, fsync-backed durable journal with replay, snapshots, migration, backup/restore, and fail-closed unknown-outcome handling.
|
|
13
|
+
- Add deterministic multi-worker canary and isolated live-runtime soak tooling, a credential-free demo, and a non-destructive customized-Ironman upgrade planner.
|
|
14
|
+
- Add unified production-trust acceptance and an idempotent acceptance refresh command so detached long-running evidence invalidates stale final judgements.
|
|
15
|
+
|
|
5
16
|
## 0.12.0 - 2026-08-13
|
|
6
17
|
|
|
7
18
|
- Add P3 read-only Operator Dashboard, normalized schema, loopback HTTP/JSON API, static export, inspect and health commands.
|
package/README.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Taskforce Loop Engineering
|
|
2
2
|
|
|
3
|
+
[](https://github.com/ambitioncn/taskforce-loop-engineering/actions/workflows/production-trust.yml)
|
|
4
|
+
|
|
5
|
+
## Platform-neutral adapter SDK
|
|
6
|
+
|
|
7
|
+
OpenClaw, Hermes, Codex CLI, and Claude Code share the versioned runtime
|
|
8
|
+
contract in `lib/runtime-adapter-sdk.mjs`. Start without credentials or network
|
|
9
|
+
access with `npm run demo:adapter`, then verify every runtime using
|
|
10
|
+
`npm run check:adapters`. See [docs/runtime-adapter-sdk.md](docs/runtime-adapter-sdk.md)
|
|
11
|
+
for the contract, compatibility matrix, migration, and extension guide.
|
|
12
|
+
|
|
13
|
+
## 0.13 production trust
|
|
14
|
+
|
|
15
|
+
The local production-trust contract, runtime adapter v1, durable journal,
|
|
16
|
+
multi-worker canary, non-destructive Ironman upgrade planner, safe demo and
|
|
17
|
+
unified acceptance are documented in
|
|
18
|
+
[docs/production-trust-contract.md](docs/production-trust-contract.md). Run
|
|
19
|
+
`npm run check:production-trust`; external publishing and deployment remain
|
|
20
|
+
separately authorized actions. The command writes integrity-sealed evidence and
|
|
21
|
+
a redacted public summary to `.production-evidence/`. The default canary is
|
|
22
|
+
offline and fixture-only: it performs no model call or external side effect.
|
|
23
|
+
Copy `templates/github-production-trust.yml` into `.github/workflows/` only
|
|
24
|
+
when publication is separately approved.
|
|
25
|
+
|
|
3
26
|
## Read-only operator dashboard (P3)
|
|
4
27
|
|
|
5
28
|
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.
|
package/bin/loop-engineering.mjs
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
mergeQueueOptions,
|
|
38
38
|
nextState,
|
|
39
39
|
notifyTerminalTasks,
|
|
40
|
+
refreshTaskAcceptance,
|
|
40
41
|
notifyHumanInputRequests,
|
|
41
42
|
parkQueueTask,
|
|
42
43
|
resumeParkedTask,
|
|
@@ -1637,6 +1638,7 @@ Usage:
|
|
|
1637
1638
|
loop-engineering queue-wait-tick --queue name (--notify-command "command" | --dry-run) [--now ISO] [--root <workspace>] [--json]
|
|
1638
1639
|
loop-engineering queue-wait-resume --queue name --task-id id --verified --recovery-signal "..." [--root <workspace>] [--json]
|
|
1639
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]
|
|
1640
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]
|
|
1641
1643
|
loop-engineering queue-init --queue name [--root <workspace>] [--force]
|
|
1642
1644
|
loop-engineering code-queue-init --queue name [--root <workspace>] [--force]
|
|
@@ -2070,6 +2072,14 @@ async function queueTerminalNotifyCommand(args) {
|
|
|
2070
2072
|
return result.failed > 0 ? 1 : 0;
|
|
2071
2073
|
}
|
|
2072
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
|
+
|
|
2073
2083
|
async function queueHumanInputNotifyCommand(args) {
|
|
2074
2084
|
if (!args.queue) throw new Error('queue-human-input-notify requires --queue.');
|
|
2075
2085
|
const result = await notifyHumanInputRequests(args.root, args);
|
|
@@ -5619,6 +5629,7 @@ async function main() {
|
|
|
5619
5629
|
if (command === 'queue-wait-tick') return queueWaitTickCommand(args);
|
|
5620
5630
|
if (command === 'queue-wait-resume') return queueWaitResumeCommand(args);
|
|
5621
5631
|
if (command === 'queue-terminal-notify') return queueTerminalNotifyCommand(args);
|
|
5632
|
+
if (command === 'queue-acceptance-refresh') return queueAcceptanceRefreshCommand(args);
|
|
5622
5633
|
if (command === 'queue-human-input-notify') return queueHumanInputNotifyCommand(args);
|
|
5623
5634
|
if (command === 'queue-human-input-resolve') return queueHumanInputResolveCommand(args);
|
|
5624
5635
|
if (command === 'queue-scheduler-tick') return queueSchedulerTickCommand(args);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"project": "P2 platform-neutral runtime adapter SDK",
|
|
4
|
+
"terminal_acceptance": [
|
|
5
|
+
"versioned platform-neutral contract",
|
|
6
|
+
"OpenClaw Hermes Codex CLI and Claude Code reproducible paths",
|
|
7
|
+
"unified capability session run step effect gate continuation telemetry and errors",
|
|
8
|
+
"compatibility matrix conformance migration and extension docs",
|
|
9
|
+
"credential-free demo and read-only dashboard path",
|
|
10
|
+
"redaction fail-closed 0.13 P0 P1 regression package and clean-install evidence"
|
|
11
|
+
],
|
|
12
|
+
"backlog": [
|
|
13
|
+
{"id":"P2-1","status":"done","evidence":"lib/runtime-adapter-sdk.mjs"},
|
|
14
|
+
{"id":"P2-2","status":"done","evidence":"scripts/runtime-adapter-conformance.mjs"},
|
|
15
|
+
{"id":"P2-3","status":"done","evidence":"docs/runtime-adapter-sdk.md"},
|
|
16
|
+
{"id":"P2-4","status":"done","evidence":"examples/adapter-sdk-demo.mjs"},
|
|
17
|
+
{"id":"P2-5","status":"done","evidence":"npm run check; npm pack --pack-destination; clean npm install and packaged conformance"}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Production Trust Operations
|
|
2
|
+
|
|
3
|
+
Run `npm run check:production-trust` before release review. The safe demo is
|
|
4
|
+
`node examples/safe-canary.mjs`; it uses an in-memory I/O boundary, contains no
|
|
5
|
+
credential, makes no paid call, and performs no external write. The soak command
|
|
6
|
+
writes a local audit report when passed `--output <file>`.
|
|
7
|
+
|
|
8
|
+
Back up both `events.jsonl` and `snapshot.json` before migration or upgrade.
|
|
9
|
+
Restore into a new directory, replay it, compare event count/checksum and only
|
|
10
|
+
then switch the configured path. Never truncate a checksum error; a malformed
|
|
11
|
+
final partial line may be ignored as a torn write, while corruption elsewhere
|
|
12
|
+
fails closed. Reconcile every `unknown` external outcome against the upstream
|
|
13
|
+
provider before permitting a retry.
|
|
14
|
+
|
|
15
|
+
Upgrade plans are read-only. A customized dispatcher/config receives
|
|
16
|
+
`preserve_customized`, which is not apply-ready. Review a byte-exact backup and
|
|
17
|
+
merge plan; do not use force overwrite. Publishing, real canary traffic,
|
|
18
|
+
production deployment, credentials, process control and paid services require
|
|
19
|
+
separate operator authorization.
|
|
20
|
+
|
|
21
|
+
Support boundaries are listed in `production-trust-contract.md`. In particular,
|
|
22
|
+
the custom adapter is an example/contract surface, not an operated runtime, and
|
|
23
|
+
the local journal is not a distributed consensus database.
|
|
24
|
+
|
|
25
|
+
When detached verification finishes after a task was judged, first write a
|
|
26
|
+
successor checkpoint that revises the blocked checkpoint, then run
|
|
27
|
+
`loop-engineering queue-acceptance-refresh --queue <queue> --task-id <task>`.
|
|
28
|
+
The refresh runs only when a checkpoint is newer than the final judgement;
|
|
29
|
+
repeated calls return `already_current` and do not re-run acceptance.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"project": "p1-production-trust-evidence",
|
|
4
|
+
"terminal_contract": "production-trust-contract.md",
|
|
5
|
+
"milestone_completion_is_project_completion": false,
|
|
6
|
+
"items": [
|
|
7
|
+
{"id":"P1-1","outcome":"schema-versioned evidence, public summary, integrity and redaction","evidence":["lib/production-evidence.mjs","templates/production-evidence.schema.json","scripts/production-evidence-self-test.mjs"],"required":true},
|
|
8
|
+
{"id":"P1-2","outcome":"ledger-backed multi-agent soak and kill/restart chaos","evidence":["scripts/production-soak.mjs","lib/execution-ledger.mjs"],"required":true},
|
|
9
|
+
{"id":"P1-3","outcome":"claim lease handoff parked gate and stale-fence rejection","evidence":["scripts/production-soak.mjs",".production-evidence/evidence.json"],"required":true},
|
|
10
|
+
{"id":"P1-4","outcome":"unknown outcome crash boundaries and accepted-before-settle reconciliation","evidence":["scripts/production-soak.mjs","lib/execution-ledger.mjs"],"required":true},
|
|
11
|
+
{"id":"P1-5","outcome":"step replay resume divergence and zero duplicate effects","evidence":["scripts/production-soak.mjs","scripts/execution-ledger-self-test.mjs"],"required":true},
|
|
12
|
+
{"id":"P1-6","outcome":"baseline trend threshold attribution recovery cost and error metrics","evidence":["lib/production-evidence.mjs","scripts/production-evidence-self-test.mjs"],"required":true},
|
|
13
|
+
{"id":"P1-7","outcome":"OpenClaw Hermes and custom adapter compatibility matrix","evidence":["lib/runtime-adapter-v1.mjs","scripts/production-soak.mjs"],"required":true},
|
|
14
|
+
{"id":"P1-8","outcome":"GitHub CI candidate badge doctor and dashboard projection","evidence":["templates/github-production-trust.yml","README.md","lib/core.mjs","lib/operator-dashboard.mjs"],"required":true},
|
|
15
|
+
{"id":"P1-9","outcome":"offline determinism privacy and tamper negative tests","evidence":["scripts/production-evidence-self-test.mjs","scripts/production-soak.mjs"],"required":true},
|
|
16
|
+
{"id":"P1-10","outcome":"full regression package content and clean-install acceptance","evidence":["package.json","scripts/production-acceptance.mjs"],"required":true}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# P1 Production-Trust Evidence Terminal Contract
|
|
2
|
+
|
|
3
|
+
Status: local release candidate. Project completion requires every required P1
|
|
4
|
+
backlog item to have repeatable evidence; a scenario or milestone alone is not
|
|
5
|
+
project completion. Push, publication, deployment, production credentials,
|
|
6
|
+
real paid calls, production process control, and external effects are excluded.
|
|
7
|
+
|
|
8
|
+
## Terminal outcome
|
|
9
|
+
|
|
10
|
+
P1 turns the P0 schema-v2 execution ledger and effect protocol into a sustainable
|
|
11
|
+
production-trust evidence system. CI/canary runs emit schema-versioned,
|
|
12
|
+
integrity-sealed evidence plus a secret-redacted public summary. Baselines,
|
|
13
|
+
trends, explicit thresholds, failure attribution, runtime compatibility, cost,
|
|
14
|
+
error rate, and recovery time remain independently reviewable.
|
|
15
|
+
|
|
16
|
+
## Required acceptance
|
|
17
|
+
|
|
18
|
+
1. The deterministic multi-agent canary exercises long soak, exclusive claim,
|
|
19
|
+
lease expiry, kill/restart handoff, stale fencing, parked gate, crash before
|
|
20
|
+
and after submit, accepted-before-local-settle, reconciliation, checkpoint
|
|
21
|
+
resume, reusable replay, and replay divergence through the P0 ledger/effect
|
|
22
|
+
protocol. It does not maintain a second execution state store.
|
|
23
|
+
2. Duplicate settled effects and accepted stale fences are exactly zero;
|
|
24
|
+
unreconciled unknown outcomes are zero at terminal acceptance. Recovery time,
|
|
25
|
+
error rate, model calls, and paid-call cost meet recorded thresholds.
|
|
26
|
+
3. OpenClaw, Hermes, and custom runtime-adapter fixtures pass contract v1 using
|
|
27
|
+
simulated I/O. The boundary is explicit: fixtures prove adapter compatibility,
|
|
28
|
+
not availability of a real gateway or provider.
|
|
29
|
+
4. Evidence schema v1 supports baselines and metric deltas, threshold failures
|
|
30
|
+
with attribution, SHA-256 tamper detection, credential-shaped field redaction,
|
|
31
|
+
and a minimized public summary. Offline reruns require no network or secret.
|
|
32
|
+
5. GitHub CI template/artifact upload and badge markup are release candidates;
|
|
33
|
+
no workflow is published in this local task. Doctor and dashboard project the
|
|
34
|
+
latest evidence state without mutating it.
|
|
35
|
+
6. Full regression, package dry-run, package content inspection, and clean local
|
|
36
|
+
install pass. The packaged candidate includes schema, CI template, library,
|
|
37
|
+
canary, tests, docs, and backlog.
|
|
38
|
+
|
|
39
|
+
## Real/simulated boundary and deferred canary
|
|
40
|
+
|
|
41
|
+
`production-soak.mjs` is the authoritative offline CI canary. The separate
|
|
42
|
+
`live-runtime-soak.mjs` may perform runtime probes and is not invoked by release
|
|
43
|
+
acceptance. A real long-duration OpenClaw/Hermes run, production credentials,
|
|
44
|
+
paid inference, or external side effect needs a separate human authorization and
|
|
45
|
+
must produce a successor evidence artifact clearly labeled `real_runtime`.
|
|
46
|
+
|
|
47
|
+
## Trust limits
|
|
48
|
+
|
|
49
|
+
SHA-256 detects later artifact changes but is not an external timestamp or
|
|
50
|
+
signature. Local filesystem leases provide single-host coordination, not
|
|
51
|
+
distributed consensus or Byzantine-worker protection. Exactly-once effects still
|
|
52
|
+
depend on an upstream idempotency/reconciliation API. Unknown outcomes lacking
|
|
53
|
+
authoritative evidence fail closed and remain reconciliation debt.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Runtime Adapter SDK v1
|
|
2
|
+
|
|
3
|
+
`lib/runtime-adapter-sdk.mjs` is the platform-neutral contract. Contract id is
|
|
4
|
+
`loop.runtime-adapter`, semantic version `1.0.0`; consumers must reject unknown
|
|
5
|
+
major versions. The SDK normalizes capabilities, session/run identity, step
|
|
6
|
+
ledger events, authorized effects, human gates, heartbeat/continuation,
|
|
7
|
+
redacted evidence/telemetry, and stable `AdapterError` codes.
|
|
8
|
+
|
|
9
|
+
## Ten-minute, credential-free path
|
|
10
|
+
|
|
11
|
+
From this package, run `npm run demo:adapter`, `npm run check:adapters`, then
|
|
12
|
+
`npm run check`. All four paths use the same in-memory transport and make no
|
|
13
|
+
network calls. The existing dashboard remains a read-only projection; the demo
|
|
14
|
+
prints the command for opening it against a local workspace.
|
|
15
|
+
|
|
16
|
+
## Runtime paths
|
|
17
|
+
|
|
18
|
+
| Runtime | Factory | Integration transport |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| OpenClaw | `createOpenClawAdapter` | map invoke to trusted session/task tools |
|
|
21
|
+
| Hermes | `createHermesAdapter` | map invoke to Hermes run lifecycle |
|
|
22
|
+
| Codex CLI | `createCodexCliAdapter` | map invoke to local Codex exec/resume |
|
|
23
|
+
| Claude Code | `createClaudeCodeAdapter` | map invoke to local Claude session/resume |
|
|
24
|
+
|
|
25
|
+
Each transport implements `invoke(operation, payload)`. Operations are
|
|
26
|
+
`run.start`, `run.heartbeat`, and `run.continue`. Side effects never pass through
|
|
27
|
+
that generic transport: call `prepareEffect` first, persist its idempotency key
|
|
28
|
+
in the P0/P1 ledger, obtain explicit authorization, then submit through the
|
|
29
|
+
product-specific effect adapter. Missing authorization fails closed.
|
|
30
|
+
|
|
31
|
+
## Compatibility and migration
|
|
32
|
+
|
|
33
|
+
| Surface | Status |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| package 0.13 / P0 / P1 ledgers | compatible; unchanged |
|
|
36
|
+
| `runtime-adapter-v1.mjs` OpenClaw/Hermes/custom fixtures | retained |
|
|
37
|
+
| SDK v1 four-runtime contract | additive and preferred |
|
|
38
|
+
| future SDK major | rejected until explicitly supported |
|
|
39
|
+
|
|
40
|
+
Migrate by replacing fixture imports with a `create*Adapter(transport)` factory,
|
|
41
|
+
creating a session then run, recording every step, and routing effects through
|
|
42
|
+
`prepareEffect`. Convert caught errors using `AdapterError.toJSON()`; never log
|
|
43
|
+
raw credentials. To extend, add a runtime to `RUNTIMES`, a thin factory, and run
|
|
44
|
+
the exported conformance function with an offline transport before connecting a
|
|
45
|
+
real runtime.
|
|
46
|
+
|
|
47
|
+
Error codes are `INVALID_INPUT`, `UNSUPPORTED_CONTRACT`,
|
|
48
|
+
`UNSUPPORTED_VERSION`, `UNSUPPORTED_RUNTIME`, `INVALID_ADAPTER`,
|
|
49
|
+
`INVALID_STEP_STATE`, `EFFECT_KEY_REQUIRED`, `EFFECT_NOT_AUTHORIZED`,
|
|
50
|
+
`INVALID_GATE_DECISION`, and retryable `TRANSPORT_FAILURE`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createMemoryTransport, createOpenClawAdapter } from '../lib/runtime-adapter-sdk.mjs';
|
|
2
|
+
|
|
3
|
+
const adapter = createOpenClawAdapter(createMemoryTransport());
|
|
4
|
+
const session = await adapter.createSession({ key: 'ten-minute-demo' });
|
|
5
|
+
const run = await adapter.startRun({ sessionId: session.sessionId, requestId: 'demo', input: { prompt: 'credential-free' } });
|
|
6
|
+
await adapter.recordStep({ runId: run.runId, stepId: 'hello', evidence: [{ kind: 'local-demo' }] });
|
|
7
|
+
const heartbeat = await adapter.heartbeat({ runId: run.runId });
|
|
8
|
+
await adapter.continueRun({ runId: run.runId, continuationToken: heartbeat.continuationToken });
|
|
9
|
+
console.log(JSON.stringify({ session, run, heartbeat, dashboard: 'run `loop-engineering dashboard --root .` for the existing read-only projection', telemetry: adapter.telemetry }, null, 2));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"queue": "code-tasks",
|
|
3
|
+
"description": "L2 assisted code task queue. Each task runs in an isolated git worktree and records verification plus diff summaries. It never pushes, merges, or deletes worktrees.",
|
|
4
|
+
"dispatcher": "node scripts/dispatch-code-task.mjs",
|
|
5
|
+
"preflightConfig": "configs/loops/workspace-health.json",
|
|
6
|
+
"timeoutMs": 1800000,
|
|
7
|
+
"leaseMs": 1860000,
|
|
8
|
+
"staleActiveMs": 3600000,
|
|
9
|
+
"retry": {
|
|
10
|
+
"maxAttempts": 1,
|
|
11
|
+
"retryDelayMs": 0,
|
|
12
|
+
"retryExitCodes": [
|
|
13
|
+
1
|
|
14
|
+
],
|
|
15
|
+
"requiresHumanActionPatterns": [
|
|
16
|
+
"INSTALL_FAILED_USER_RESTRICTED",
|
|
17
|
+
"device unauthorized",
|
|
18
|
+
"no devices/emulators found",
|
|
19
|
+
"Permission denied",
|
|
20
|
+
"Operation not permitted",
|
|
21
|
+
"requires human",
|
|
22
|
+
"需要人工",
|
|
23
|
+
"权限未开"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"worktree": {
|
|
27
|
+
"enabled": true,
|
|
28
|
+
"baseDir": "runtime/loops/code-tasks/worktrees",
|
|
29
|
+
"branchPrefix": "loop/code-tasks",
|
|
30
|
+
"verifyCommands": [
|
|
31
|
+
"npm test"
|
|
32
|
+
],
|
|
33
|
+
"keepOnSuccess": true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"queue": "agent-tasks",
|
|
3
|
+
"description": "Generic queue runner config. Keep dispatcher local to the target workspace.",
|
|
4
|
+
"dispatcher": "node scripts/dispatch-task.mjs",
|
|
5
|
+
"preflightConfig": "configs/loops/workspace-health.json",
|
|
6
|
+
"timeoutMs": 1800000,
|
|
7
|
+
"leaseMs": 1860000,
|
|
8
|
+
"staleActiveMs": 3600000,
|
|
9
|
+
"scheduler": {
|
|
10
|
+
"initialInterval": "10m",
|
|
11
|
+
"minInterval": "1m",
|
|
12
|
+
"maxInterval": "4h",
|
|
13
|
+
"speedupFactor": 0.5,
|
|
14
|
+
"backoffFactor": 2,
|
|
15
|
+
"idleBackoffFactor": 2,
|
|
16
|
+
"humanGateBackoffFactor": 3,
|
|
17
|
+
"longRunHeadroomFactor": 1.25,
|
|
18
|
+
"jitter": "30s"
|
|
19
|
+
},
|
|
20
|
+
"retry": {
|
|
21
|
+
"maxAttempts": 1,
|
|
22
|
+
"retryDelayMs": 0,
|
|
23
|
+
"retryExitCodes": [
|
|
24
|
+
1
|
|
25
|
+
],
|
|
26
|
+
"requiresHumanActionPatterns": [
|
|
27
|
+
"INSTALL_FAILED_USER_RESTRICTED",
|
|
28
|
+
"device unauthorized",
|
|
29
|
+
"no devices/emulators found",
|
|
30
|
+
"Permission denied",
|
|
31
|
+
"Operation not permitted",
|
|
32
|
+
"requires human",
|
|
33
|
+
"需要人工",
|
|
34
|
+
"权限未开"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { customAdapterExample } from '../lib/runtime-adapter-v1.mjs';
|
|
2
|
+
const audit = [];
|
|
3
|
+
const io = { invoke: async (binary, args) => (audit.push({ binary, args, externalWrite: false, paid: false }), { accepted: true }), now: () => new Date().toISOString(), lookup: async () => ({ status: 'not_accepted' }) };
|
|
4
|
+
await customAdapterExample.dispatch({ prompt: 'credential-free local canary', worker: 'demo' }, io);
|
|
5
|
+
console.log(JSON.stringify({ support: 'example-contract-only', credentialsUsed: false, externalWrites: false, paidCalls: false, audit }, null, 2));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "workspace-health",
|
|
3
|
+
"goal": "Keep this workspace loop-ready and detect obvious drift.",
|
|
4
|
+
"level": "L1",
|
|
5
|
+
"mode": "report-only",
|
|
6
|
+
"maxRuntimeMs": 120000,
|
|
7
|
+
"description": "A safe first loop: local read-only checks plus durable run ledger.",
|
|
8
|
+
"humanGates": [
|
|
9
|
+
"source edits",
|
|
10
|
+
"external messages",
|
|
11
|
+
"destructive commands",
|
|
12
|
+
"production config changes"
|
|
13
|
+
],
|
|
14
|
+
"breaker": {
|
|
15
|
+
"maxConsecutiveFailures": 3,
|
|
16
|
+
"sameFailureThreshold": 2
|
|
17
|
+
},
|
|
18
|
+
"checks": [
|
|
19
|
+
{
|
|
20
|
+
"id": "git-status",
|
|
21
|
+
"type": "command",
|
|
22
|
+
"cmd": "git status --short",
|
|
23
|
+
"expectExitCode": 0,
|
|
24
|
+
"timeoutMs": 10000,
|
|
25
|
+
"allowNonEmptyOutput": true
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "loop-config-dir",
|
|
29
|
+
"type": "files",
|
|
30
|
+
"paths": [
|
|
31
|
+
"configs/loops"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { migrateActionReservation } from './execution-ledger.mjs';
|
|
4
5
|
|
|
5
6
|
const ACTION_KINDS = new Set(['paid_api', 'notification', 'deployment', 'process_control', 'publication', 'external_message', 'gated_mutation']);
|
|
6
7
|
const TERMINAL_STATES = new Set(['settled', 'released']);
|
|
@@ -194,3 +195,9 @@ export async function migrateLegacyActionArtifact(root, legacy) {
|
|
|
194
195
|
const scope = legacy.authorization_scope ?? legacy.authorization?.scope ?? 'legacy:unscoped';
|
|
195
196
|
return reserveAction(root, { idempotencyKey: key, kind: legacy.kind ?? 'gated_mutation', request, authorizationScope: scope });
|
|
196
197
|
}
|
|
198
|
+
|
|
199
|
+
export async function projectActionToEffectProtocol(root, idempotencyKey) {
|
|
200
|
+
const reservation = await inspectAction(root, idempotencyKey);
|
|
201
|
+
if (!reservation) throw new Error('Action reservation not found.');
|
|
202
|
+
return migrateActionReservation(root, reservation);
|
|
203
|
+
}
|
package/lib/core.mjs
CHANGED
|
@@ -4,6 +4,8 @@ import { spawn } from 'node:child_process';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
|
+
import { listSteps } from './execution-ledger.mjs';
|
|
8
|
+
import { readAndVerifyEvidence } from './production-evidence.mjs';
|
|
7
9
|
|
|
8
10
|
export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
11
|
|
|
@@ -747,6 +749,20 @@ export async function doctorReport(root, options = {}) {
|
|
|
747
749
|
add('configs-dir', 'warn', await exists(configsDir), path.relative(root, configsDir));
|
|
748
750
|
const runtimeDir = path.join(root, 'runtime', 'loops');
|
|
749
751
|
add('runtime-dir', 'warn', await exists(runtimeDir), path.relative(root, runtimeDir));
|
|
752
|
+
try {
|
|
753
|
+
const steps = await listSteps(root);
|
|
754
|
+
const invalid = steps.filter((step) => step.version !== 2 || !step.step_id || !step.input_fingerprint || !['llm', 'tool', 'effect'].includes(step.kind));
|
|
755
|
+
const reconciliation = steps.filter((step) => step.status === 'unknown' || step.reconciliation?.required);
|
|
756
|
+
add('execution-ledger', 'fail', invalid.length === 0, { schema_version: 2, steps: steps.length, invalid: invalid.map((step) => step.step_id), reconciliation_required: reconciliation.map((step) => step.step_id) });
|
|
757
|
+
if (reconciliation.length) add('execution-ledger:reconciliation', 'warn', false, `${reconciliation.length} step(s) require evidence-backed reconciliation`);
|
|
758
|
+
} catch (error) {
|
|
759
|
+
add('execution-ledger', 'fail', false, error instanceof Error ? error.message : String(error));
|
|
760
|
+
}
|
|
761
|
+
const evidenceFile = path.join(root, '.production-evidence', 'evidence.json');
|
|
762
|
+
if (await exists(evidenceFile)) {
|
|
763
|
+
try { const evidence = await readAndVerifyEvidence(evidenceFile); add('production-evidence', 'fail', evidence.verification.valid && evidence.report.passed, { schema_version: evidence.report.schema_version, integrity: evidence.verification.valid, passed: evidence.report.passed, digest: evidence.report.integrity?.digest }); }
|
|
764
|
+
catch (error) { add('production-evidence', 'fail', false, error instanceof Error ? error.message : String(error)); }
|
|
765
|
+
} else add('production-evidence', 'warn', false, '.production-evidence/evidence.json not generated');
|
|
750
766
|
|
|
751
767
|
const loopConfigs = await configFilesFromArgs(root, []);
|
|
752
768
|
add('loop-configs-found', 'warn', loopConfigs.length > 0, `${loopConfigs.length} loop config(s)`);
|
|
@@ -1956,6 +1972,56 @@ export async function notifyTerminalTasks(root, options = {}) {
|
|
|
1956
1972
|
};
|
|
1957
1973
|
}
|
|
1958
1974
|
|
|
1975
|
+
export async function refreshTaskAcceptance(root, options = {}) {
|
|
1976
|
+
const queue = normalizeLoopId(options.queue);
|
|
1977
|
+
if (!options.taskId) throw new Error('queue-acceptance-refresh requires --task-id.');
|
|
1978
|
+
await ensureQueueDirs(root, queue);
|
|
1979
|
+
const located = await findTaskFile(root, queue, options.taskId);
|
|
1980
|
+
if (!located) throw new Error(`Queue task not found: ${options.taskId}`);
|
|
1981
|
+
const task = await readJson(located.file);
|
|
1982
|
+
const dir = taskRuntimeDirFor(root, queue, task.id);
|
|
1983
|
+
const checkpointsDir = path.join(dir, 'checkpoints');
|
|
1984
|
+
const checkpointFiles = await listJson(checkpointsDir);
|
|
1985
|
+
if (checkpointFiles.length === 0) return { queue, taskId: task.id, outcome: 'no_checkpoints' };
|
|
1986
|
+
const judgementFile = path.join(dir, 'final_judgement.json');
|
|
1987
|
+
const newestCheckpointMs = Math.max(...await Promise.all(checkpointFiles.map(async (file) => (await stat(path.join(checkpointsDir, file))).mtimeMs)));
|
|
1988
|
+
if (await exists(judgementFile) && (await stat(judgementFile)).mtimeMs >= newestCheckpointMs) {
|
|
1989
|
+
return { queue, taskId: task.id, outcome: 'already_current', judgement: path.relative(root, judgementFile) };
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
const contractFile = path.join(dir, 'task_contract.json');
|
|
1993
|
+
const acceptanceFile = path.join(dir, 'acceptance_plan.json');
|
|
1994
|
+
const devFile = path.join(dir, 'dev_plan.json');
|
|
1995
|
+
const taskContract = { contract: await readJson(contractFile), file: path.relative(root, contractFile) };
|
|
1996
|
+
const acceptancePlan = { plan: await readJson(acceptanceFile), file: path.relative(root, acceptanceFile) };
|
|
1997
|
+
const devPlan = {
|
|
1998
|
+
plan: await readJson(devFile),
|
|
1999
|
+
file: path.relative(root, devFile),
|
|
2000
|
+
checkpointsDir: path.relative(root, checkpointsDir),
|
|
2001
|
+
reviewsDir: path.relative(root, path.join(dir, 'reviews'))
|
|
2002
|
+
};
|
|
2003
|
+
const checkpoints = await checkpointSummary(root, devPlan);
|
|
2004
|
+
const acceptanceReviews = await writeAcceptanceReviews(root, queue, task, taskContract, acceptancePlan, devPlan);
|
|
2005
|
+
const finalJudgement = await writeFinalJudgement(root, queue, task, taskContract, acceptancePlan, devPlan, checkpoints, acceptanceReviews, {
|
|
2006
|
+
dispatchStatus: 'completed'
|
|
2007
|
+
});
|
|
2008
|
+
const status = queueStatusFromFinalJudgement('completed', finalJudgement);
|
|
2009
|
+
const destinationName = status === 'completed' ? 'done' : status === 'project_in_progress' ? 'inbox' : 'failed';
|
|
2010
|
+
const destination = path.join(queueSubdirFor(root, queue, destinationName), path.basename(located.file));
|
|
2011
|
+
await writeJson(destination, { ...task, status: status === 'project_in_progress' ? 'queued' : status, acceptanceRefreshedAt: new Date().toISOString() });
|
|
2012
|
+
if (destination !== located.file) await rm(located.file, { force: true });
|
|
2013
|
+
return {
|
|
2014
|
+
queue,
|
|
2015
|
+
taskId: task.id,
|
|
2016
|
+
outcome: 'refreshed',
|
|
2017
|
+
status,
|
|
2018
|
+
checkpointCount: checkpoints.count,
|
|
2019
|
+
accepted: acceptanceReviews.accepted,
|
|
2020
|
+
judgement: finalJudgement.file,
|
|
2021
|
+
task: path.relative(root, destination)
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
|
|
1959
2025
|
function humanInputMessage(queue, task, checkpoint, gateId, language = 'en') {
|
|
1960
2026
|
const blockers = Array.isArray(checkpoint.blockers) ? checkpoint.blockers : [];
|
|
1961
2027
|
const blockerText = blockers.length
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { appendFile, copyFile, mkdir, open, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const digest = (value) => createHash('sha256').update(value).digest('hex');
|
|
6
|
+
const canonical = (value) => JSON.stringify(sortValue(value));
|
|
7
|
+
function sortValue(value) {
|
|
8
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
9
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortValue(value[key])]));
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class DurableJournal {
|
|
14
|
+
constructor(directory) {
|
|
15
|
+
this.directory = directory;
|
|
16
|
+
this.logFile = path.join(directory, 'events.jsonl');
|
|
17
|
+
this.snapshotFile = path.join(directory, 'snapshot.json');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async append(type, payload, transactionId = randomUUID()) {
|
|
21
|
+
await mkdir(this.directory, { recursive: true });
|
|
22
|
+
const previous = (await this.replay()).lastChecksum ?? null;
|
|
23
|
+
const event = { version: 1, transactionId, type, payload, previous };
|
|
24
|
+
event.checksum = digest(canonical(event));
|
|
25
|
+
const handle = await open(this.logFile, 'a');
|
|
26
|
+
try { await handle.write(`${JSON.stringify(event)}\n`); await handle.sync(); } finally { await handle.close(); }
|
|
27
|
+
return event;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async replay(reducer = (state, event) => ({ ...state, [event.type]: event.payload }), initial = {}) {
|
|
31
|
+
let raw = '';
|
|
32
|
+
try { raw = await readFile(this.logFile, 'utf8'); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
33
|
+
let state = initial; let lastChecksum = null; let count = 0;
|
|
34
|
+
const lines = raw.split('\n');
|
|
35
|
+
for (let index = 0; index < lines.length; index++) {
|
|
36
|
+
const line = lines[index];
|
|
37
|
+
if (!line) continue;
|
|
38
|
+
let event;
|
|
39
|
+
try { event = JSON.parse(line); } catch (error) {
|
|
40
|
+
if (index === lines.length - 1) break;
|
|
41
|
+
throw new Error(`journal corruption at line ${index + 1}: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
const checksum = event.checksum; const unsigned = { ...event }; delete unsigned.checksum;
|
|
44
|
+
if (digest(canonical(unsigned)) !== checksum || event.previous !== lastChecksum) throw new Error(`journal checksum chain invalid at line ${index + 1}`);
|
|
45
|
+
state = reducer(state, event); lastChecksum = checksum; count++;
|
|
46
|
+
}
|
|
47
|
+
return { state, count, lastChecksum };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async checkpoint(state) {
|
|
51
|
+
await mkdir(this.directory, { recursive: true });
|
|
52
|
+
const replay = await this.replay();
|
|
53
|
+
const snapshot = { version: 1, eventCount: replay.count, lastChecksum: replay.lastChecksum, state };
|
|
54
|
+
const temporary = `${this.snapshotFile}.${process.pid}.tmp`;
|
|
55
|
+
await writeFile(temporary, `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
56
|
+
const handle = await open(temporary, 'r'); try { await handle.sync(); } finally { await handle.close(); }
|
|
57
|
+
await rename(temporary, this.snapshotFile);
|
|
58
|
+
return snapshot;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async backup(destination) {
|
|
62
|
+
await mkdir(destination, { recursive: true });
|
|
63
|
+
for (const name of ['events.jsonl', 'snapshot.json']) {
|
|
64
|
+
try { await copyFile(path.join(this.directory, name), path.join(destination, name)); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static async restore(backup, destination) {
|
|
69
|
+
await mkdir(destination, { recursive: true });
|
|
70
|
+
for (const name of ['events.jsonl', 'snapshot.json']) {
|
|
71
|
+
try { await copyFile(path.join(backup, name), path.join(destination, name)); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
72
|
+
}
|
|
73
|
+
return new DurableJournal(destination).replay();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static async migrateV1(stateFile, directory) {
|
|
77
|
+
const journal = new DurableJournal(directory);
|
|
78
|
+
if ((await journal.replay()).count) return journal;
|
|
79
|
+
const state = JSON.parse(await readFile(stateFile, 'utf8'));
|
|
80
|
+
await journal.append('legacy_state_imported', { sourceVersion: state.version, state }, 'migration-v1');
|
|
81
|
+
await journal.checkpoint(state); return journal;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function externalEffectBoundary({ status, idempotencyKey, upstreamEvidence }) {
|
|
86
|
+
if (!idempotencyKey) throw new Error('external side effect requires idempotencyKey');
|
|
87
|
+
if (status === 'accepted' && !upstreamEvidence) throw new Error('accepted side effect requires upstreamEvidence');
|
|
88
|
+
if (!['reserved', 'in_flight', 'unknown', 'accepted', 'not_accepted'].includes(status)) throw new Error('invalid side effect status');
|
|
89
|
+
return { status, idempotencyKey, upstreamEvidence: upstreamEvidence ?? null, replayable: ['reserved', 'not_accepted'].includes(status) };
|
|
90
|
+
}
|