taskforce-loop-engineering 0.13.0 → 0.15.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 +10 -0
- package/README.md +15 -1
- package/docs/adapter-sdk-terminal-contract.json +19 -0
- package/docs/production-trust-backlog.json +13 -8
- package/docs/production-trust-contract.md +53 -54
- 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 +473 -74
- 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/package.json +6 -2
- package/scripts/execution-ledger-self-test.mjs +10 -0
- package/scripts/final-judgement-self-test.mjs +20 -1
- package/scripts/live-runtime-soak.mjs +14 -3
- package/scripts/operator-dashboard-self-test.mjs +10 -0
- package/scripts/production-acceptance.mjs +2 -0
- package/scripts/production-evidence-self-test.mjs +17 -0
- package/scripts/production-soak.mjs +41 -17
- package/scripts/project-gate-reconciliation-self-test.mjs +107 -0
- package/scripts/route-notify-self-test.mjs +51 -0
- package/scripts/runtime-adapter-conformance.mjs +21 -0
- package/templates/github-production-trust.yml +20 -0
- package/templates/production-evidence.schema.json +20 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { doctorReport, enqueueTask, notifyHumanInputRequests, projectStatus, queueStatus, reconcileProjectGates, routeLoopMessage, taskRuntimeDirFor } from '../lib/core.mjs';
|
|
6
|
+
|
|
7
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'loop-project-gates-'));
|
|
8
|
+
const queue = 'shared';
|
|
9
|
+
const projectFile = path.join(root, 'configs', 'loops', 'projects', 'openreel.json');
|
|
10
|
+
const spec = { schemaVersion: 1, project: 'openreel', type: 'code_project', queues: [{ queue, kind: 'standard' }], backlog: [
|
|
11
|
+
{ id: 'B-01', required: true, status: 'human_gated', dependsOn: [] },
|
|
12
|
+
{ id: 'S-01', required: true, status: 'human_gated', dependsOn: [] }
|
|
13
|
+
] };
|
|
14
|
+
await mkdir(path.dirname(projectFile), { recursive: true });
|
|
15
|
+
await writeFile(projectFile, `${JSON.stringify(spec, null, 2)}\n`);
|
|
16
|
+
|
|
17
|
+
const b = await enqueueTask(root, { queue, title: 'OpenReel B-01', task: 'Project openreel requirement B-01', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
|
|
18
|
+
const checkpointDir = path.join(taskRuntimeDirFor(root, queue, b.task.id), 'checkpoints');
|
|
19
|
+
await mkdir(checkpointDir, { recursive: true });
|
|
20
|
+
await writeFile(path.join(checkpointDir, 'cp1.json'), `${JSON.stringify({ version: 1, task_id: b.task.id, checkpoint_id: 'cp1', milestone_id: 'B-01', requirement_ids: ['B-01'], status: 'needs_human_input', blockers: ['Authorize B-01'] }, null, 2)}\n`);
|
|
21
|
+
const notified = await notifyHumanInputRequests(root, { queue, notifyCommand: '/bin/true' });
|
|
22
|
+
assert.equal(notified.sent, 1);
|
|
23
|
+
assert.equal((await queueStatus(root, queue)).waiting, 1);
|
|
24
|
+
const gateFile = path.join(root, notified.results[0].ledger);
|
|
25
|
+
const gate = JSON.parse(await readFile(gateFile, 'utf8'));
|
|
26
|
+
assert.equal(gate.project_id, 'openreel');
|
|
27
|
+
assert.equal(gate.milestone_id, 'B-01');
|
|
28
|
+
assert.deepEqual(gate.requirement_ids, ['B-01']);
|
|
29
|
+
assert.equal(typeof gate.contract_hash, 'string');
|
|
30
|
+
|
|
31
|
+
spec.backlog[0] = { ...spec.backlog[0], required: false, status: 'out_of_scope', disposition: 'canceled_by_owner' };
|
|
32
|
+
await writeFile(projectFile, `${JSON.stringify(spec, null, 2)}\n`);
|
|
33
|
+
const status = await queueStatus(root, queue);
|
|
34
|
+
assert.equal(status.waiting, 0, 'B-01 waiting gate must disappear after B-01 leaves project scope');
|
|
35
|
+
assert.equal(status.canceled, 1);
|
|
36
|
+
assert.equal(JSON.parse(await readFile(gateFile, 'utf8')).status, 'canceled');
|
|
37
|
+
|
|
38
|
+
const other = await enqueueTask(root, { queue, title: 'Other project task', task: 'Project other', projectId: 'other' });
|
|
39
|
+
const openreel = await enqueueTask(root, { queue, title: 'OpenReel target', task: 'Project openreel', projectId: 'openreel' });
|
|
40
|
+
await rename(path.join(root, other.file), path.join(root, 'runtime', 'loops', queue, 'active', path.basename(other.file)));
|
|
41
|
+
await rename(path.join(root, openreel.file), path.join(root, 'runtime', 'loops', queue, 'active', path.basename(openreel.file)));
|
|
42
|
+
const replacement = await routeLoopMessage(root, { route: true, confirmExecute: true, supersedeActive: true, queue, message: '走 loop:继续 openreel 项目' });
|
|
43
|
+
assert.equal(replacement.supersededTaskId, openreel.task.id);
|
|
44
|
+
assert.equal(await readFile(path.join(taskRuntimeDirFor(root, queue, other.task.id), 'supersede_request.json'), 'utf8').then(() => true, () => false), false);
|
|
45
|
+
|
|
46
|
+
const invalidGate = { ...gate, gate_id: `${openreel.task.id}:bad`, task_id: openreel.task.id, checkpoint_id: 'bad', status: 'waiting_for_human', requirement_ids: ['MISSING'] };
|
|
47
|
+
const invalidFile = path.join(root, 'runtime', 'loops', queue, 'human-input', 'gates', `${openreel.task.id}.bad.json`);
|
|
48
|
+
await writeFile(invalidFile, `${JSON.stringify(invalidGate, null, 2)}\n`);
|
|
49
|
+
const doctor = await doctorReport(root);
|
|
50
|
+
assert.equal(doctor.ok, false);
|
|
51
|
+
assert(doctor.checks.some((check) => check.id === `human-gate:${invalidGate.gate_id}` && !check.ok));
|
|
52
|
+
await reconcileProjectGates(root, { queue });
|
|
53
|
+
|
|
54
|
+
// project-status treats the configured ledger/backlog as authoritative and
|
|
55
|
+
// exposes registry drift instead of silently reporting stale completion data.
|
|
56
|
+
const authoritativeBacklog = path.join(root, 'project', 'backlog.json');
|
|
57
|
+
const authoritativeLedger = path.join(root, 'project', 'acceptance-ledger.json');
|
|
58
|
+
await mkdir(path.dirname(authoritativeBacklog), { recursive: true });
|
|
59
|
+
await writeFile(authoritativeBacklog, `${JSON.stringify({ status: 'ongoing', items: [{ id: 'S-01', required: true, status: 'accepted' }] }, null, 2)}\n`);
|
|
60
|
+
await writeFile(authoritativeLedger, `${JSON.stringify({ status: 'ongoing', unmet: ['S-01'], blockers: [{ id: 'S-01' }] }, null, 2)}\n`);
|
|
61
|
+
await writeFile(projectFile, `${JSON.stringify({ ...spec, backlogSource: 'project/backlog.json', acceptanceLedger: 'project/acceptance-ledger.json', terminalContract: 'project/terminal.md' }, null, 2)}\n`);
|
|
62
|
+
const drifted = await projectStatus(root, { project: 'openreel' });
|
|
63
|
+
assert.equal(drifted.projectCompletion, 'in_progress');
|
|
64
|
+
assert.equal(drifted.authority.acceptanceLedger, 'project/acceptance-ledger.json');
|
|
65
|
+
assert.equal(drifted.consistency.ok, false);
|
|
66
|
+
assert(drifted.needsAttention.includes('authoritative_source_drift'));
|
|
67
|
+
|
|
68
|
+
// A ready milestone with project in progress and a deferred authorization is
|
|
69
|
+
// converted into a structured waiting gate, not left as prose on a done task.
|
|
70
|
+
const deferred = await enqueueTask(root, { queue, title: 'OpenReel deferred S-01', task: 'Project openreel S-01', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
|
|
71
|
+
const deferredDir = path.join(taskRuntimeDirFor(root, queue, deferred.task.id), 'checkpoints');
|
|
72
|
+
await mkdir(deferredDir, { recursive: true });
|
|
73
|
+
await writeFile(path.join(deferredDir, 'cp-ready.json'), `${JSON.stringify({
|
|
74
|
+
version: 1, task_id: deferred.task.id, checkpoint_id: 'cp-ready', milestone_id: 'S-01', requirement_ids: ['S-01'],
|
|
75
|
+
status: 'ready_for_acceptance', blockers: [], verification: ['local phase passed'], risks: [],
|
|
76
|
+
project_completion: { status: 'in_progress' },
|
|
77
|
+
deferred_gates: [{ id: 'S-01-production', action: 'production_rollback_drill', required_authority: 'Owner authorization for the exact production rollback drill scope.' }]
|
|
78
|
+
}, null, 2)}\n`);
|
|
79
|
+
const deferredNotice = await notifyHumanInputRequests(root, { queue, notifyCommand: '/bin/true' });
|
|
80
|
+
const deferredResult = deferredNotice.results.find((item) => item.taskId === deferred.task.id);
|
|
81
|
+
assert.equal(deferredResult.outcome, 'sent');
|
|
82
|
+
const deferredGate = JSON.parse(await readFile(path.join(root, deferredResult.ledger), 'utf8'));
|
|
83
|
+
assert.equal(deferredGate.gate_kind, 'deferred_authorization');
|
|
84
|
+
assert.equal(deferredGate.authorization_requirements[0].action, 'production_rollback_drill');
|
|
85
|
+
assert.match(deferredGate.authorization_requirements[0].required_authority, /Owner authorization/);
|
|
86
|
+
assert.equal((await queueStatus(root, queue)).waiting >= 1, true);
|
|
87
|
+
|
|
88
|
+
// Once the authoritative project ledger accepts the terminal contract, an
|
|
89
|
+
// optional post-completion deferred action stays in operations backlog and
|
|
90
|
+
// neither creates nor retains a project-queue waiting gate.
|
|
91
|
+
await writeFile(authoritativeLedger, `${JSON.stringify({ status: 'accepted', unmet: [], blockers: [] }, null, 2)}\n`);
|
|
92
|
+
await reconcileProjectGates(root, { queue });
|
|
93
|
+
assert.equal((await queueStatus(root, queue)).waiting, 0);
|
|
94
|
+
assert.equal(JSON.parse(await readFile(path.join(root, deferredResult.ledger), 'utf8')).status, 'superseded');
|
|
95
|
+
const acceptedOptional = await enqueueTask(root, { queue, title: 'OpenReel optional operations transfer', task: 'Project openreel optional post-completion transfer', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
|
|
96
|
+
const acceptedOptionalDir = path.join(taskRuntimeDirFor(root, queue, acceptedOptional.task.id), 'checkpoints');
|
|
97
|
+
await mkdir(acceptedOptionalDir, { recursive: true });
|
|
98
|
+
await writeFile(path.join(acceptedOptionalDir, 'cp1.json'), `${JSON.stringify({
|
|
99
|
+
version: 1, task_id: acceptedOptional.task.id, checkpoint_id: 'cp1', milestone_id: 'cp1',
|
|
100
|
+
status: 'ready_for_acceptance', blockers: [], project_completion: { status: 'accepted' },
|
|
101
|
+
deferred_gates: [{ id: 'optional-transfer', required_authority: 'Separate authorization for optional production transfer.' }]
|
|
102
|
+
}, null, 2)}\n`);
|
|
103
|
+
const acceptedNotice = await notifyHumanInputRequests(root, { queue, notifyCommand: '/bin/true' });
|
|
104
|
+
assert.equal(acceptedNotice.results.some((item) => item.taskId === acceptedOptional.task.id), false);
|
|
105
|
+
assert.equal((await queueStatus(root, queue)).waiting, 0);
|
|
106
|
+
|
|
107
|
+
console.log(JSON.stringify({ status: 'ok', assertions: ['structured gate context', 'B-01 waiting to zero', 'project-isolated supersede', 'doctor strong validation', 'authoritative ledger drift', 'ready milestone deferred gate', 'accepted project optional deferred gate stays out of queue'] }));
|
|
@@ -89,6 +89,27 @@ const contract = await readJson(path.join(taskRuntimeDirFor(root, queue, routed.
|
|
|
89
89
|
assert.equal(contract.risk_level, 'model_assessed');
|
|
90
90
|
assert.equal(contract.requires_human_gate, false);
|
|
91
91
|
|
|
92
|
+
const largeBodyQueue = 'large-body-smoke';
|
|
93
|
+
const largeBody = `走 loop 验证超长任务正文\n${'checkpoint evidence '.repeat(10_000)}`;
|
|
94
|
+
const largeBodyRouted = await routeLoopMessage(root, {
|
|
95
|
+
route: true,
|
|
96
|
+
confirmExecute: true,
|
|
97
|
+
queue: largeBodyQueue,
|
|
98
|
+
message: largeBody,
|
|
99
|
+
sourceChannel: 'feishu',
|
|
100
|
+
sourceTarget: 'user-1'
|
|
101
|
+
});
|
|
102
|
+
const largeBodyRun = await runQueueOnce(root, {
|
|
103
|
+
queue: largeBodyQueue,
|
|
104
|
+
dispatcher: `node -e "const fs=require('fs');const t=JSON.parse(fs.readFileSync(process.env.LOOP_TASK_FILE,'utf8'));if(process.env.LOOP_TASK_BODY!==undefined||process.env.LOOP_TASK_BODY_MODE!=='task_file'||t.body.length<100000)process.exit(7)"`,
|
|
105
|
+
progressNotifyCommand: '/bin/true',
|
|
106
|
+
timeoutMs: 10_000,
|
|
107
|
+
leaseMs: 20_000,
|
|
108
|
+
staleActiveMs: 60_000
|
|
109
|
+
});
|
|
110
|
+
assert.equal(largeBodyRun.processed, true);
|
|
111
|
+
assert.equal(largeBodyRun.run.dispatch.exitCode, 0);
|
|
112
|
+
|
|
92
113
|
const orphanQueue = 'orphan-recovery-smoke';
|
|
93
114
|
const orphanRouted = await routeLoopMessage(root, {
|
|
94
115
|
route: true,
|
|
@@ -483,5 +504,35 @@ assert.equal(sent.sent, 1);
|
|
|
483
504
|
const repeated = await notifyTerminalTasks(root, { queue, notifyCommand: '/bin/true' });
|
|
484
505
|
assert.equal(repeated.results[0].outcome, 'already_notified');
|
|
485
506
|
|
|
507
|
+
// Real directory-level regression: terminal history plus the newest project
|
|
508
|
+
// carrier yields exactly one durable gate, one waiting task, and zero replay.
|
|
509
|
+
const regressionRoot = await mkdtemp(path.join(tmpdir(), 'loop-gate-regression-'));
|
|
510
|
+
const regressionQueue = 'project-regression';
|
|
511
|
+
await writeJson(path.join(regressionRoot, 'configs', 'loops', 'projects', 'demo.json'), {
|
|
512
|
+
schemaVersion: 1, project: 'demo', queues: [{ queue: regressionQueue }],
|
|
513
|
+
backlog: [{ id: 'R-1', status: 'human_gated', required: true }]
|
|
514
|
+
});
|
|
515
|
+
for (const [id, enqueuedAt] of [['history-done', '2026-01-01T00:00:00Z'], ['latest-done', '2026-01-02T00:00:00Z']]) {
|
|
516
|
+
await writeJson(path.join(queueSubdirFor(regressionRoot, regressionQueue, 'done'), `${id}.json`), {
|
|
517
|
+
id, title: `demo ${id}`, body: 'demo R-1', projectId: 'demo', status: 'completed', enqueuedAt,
|
|
518
|
+
source: { channel: 'test', target: 'owner' }
|
|
519
|
+
});
|
|
520
|
+
await writeJson(path.join(taskRuntimeDirFor(regressionRoot, regressionQueue, id), 'checkpoints', 'cp1.json'), {
|
|
521
|
+
version: 1, task_id: id, checkpoint_id: 'cp1', milestone_id: 'R-1', sequence: 1,
|
|
522
|
+
status: 'ready_for_acceptance', blockers: [], deferred_gates: [{ id: 'R-1', required_authority: 'Approve R-1.' }],
|
|
523
|
+
verification: [], risks: [], project_completion: 'in_progress', next_action: 'wait'
|
|
524
|
+
});
|
|
525
|
+
await writeJson(path.join(taskRuntimeDirFor(regressionRoot, regressionQueue, id), 'final_judgement.json'), {
|
|
526
|
+
version: 1, task_id: id, outcome: 'project_in_progress', coverage: { effective_review_ids: ['cp1'] }
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
const regressionFirst = await notifyHumanInputRequests(regressionRoot, { queue: regressionQueue, notifyCommand: '/bin/true' });
|
|
530
|
+
assert.equal(regressionFirst.sent, 1);
|
|
531
|
+
assert.equal((await queueStatus(regressionRoot, regressionQueue)).waiting, 1);
|
|
532
|
+
assert.equal((await readdir(path.join(queueDirFor(regressionRoot, regressionQueue), 'human-input', 'gates'))).length, 1);
|
|
533
|
+
assert.equal((await readdir(queueSubdirFor(regressionRoot, regressionQueue, 'done'))).length, 1);
|
|
534
|
+
const regressionRepeated = await notifyHumanInputRequests(regressionRoot, { queue: regressionQueue, notifyCommand: '/bin/true' });
|
|
535
|
+
assert.equal(regressionRepeated.sent, 0);
|
|
536
|
+
|
|
486
537
|
console.log('route/notify self-test passed');
|
|
487
538
|
await import('./config-drift-self-test.mjs');
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { AdapterError, createClaudeCodeAdapter, createCodexCliAdapter, createHermesAdapter, createMemoryTransport, createOpenClawAdapter, validateAdapter } from '../lib/runtime-adapter-sdk.mjs';
|
|
3
|
+
|
|
4
|
+
export async function conform(factory) {
|
|
5
|
+
const transport = createMemoryTransport(); const adapter = validateAdapter(factory(transport));
|
|
6
|
+
const session = await adapter.createSession({ key: 'offline-demo', metadata: { apiKey: 'must-not-leak' } });
|
|
7
|
+
const run = await adapter.startRun({ sessionId: session.sessionId, requestId: 'request-1', input: { prompt: 'local only' } });
|
|
8
|
+
await adapter.recordStep({ runId: run.runId, stepId: 'step-1', evidence: [{ local: true, token: 'hidden' }] });
|
|
9
|
+
await assert.rejects(adapter.prepareEffect({ runId: run.runId, effectId: 'send', idempotencyKey: 'send:1' }), (error) => error instanceof AdapterError && error.code === 'EFFECT_NOT_AUTHORIZED');
|
|
10
|
+
const effect = await adapter.prepareEffect({ runId: run.runId, effectId: 'write-local', idempotencyKey: 'write:1', authorized: true, payload: { password: 'hidden' } });
|
|
11
|
+
assert.equal(effect.state, 'authorized'); assert.equal(effect.payload.password, '[REDACTED]');
|
|
12
|
+
await adapter.resolveGate({ gateId: 'gate-1', decision: 'approved', responseRef: 'local-ref' });
|
|
13
|
+
const beat = await adapter.heartbeat({ runId: run.runId });
|
|
14
|
+
assert.equal((await adapter.continueRun({ runId: run.runId, continuationToken: beat.continuationToken })).status, 'running');
|
|
15
|
+
assert.equal(JSON.stringify(adapter.telemetry).includes('must-not-leak'), false);
|
|
16
|
+
return { runtime: adapter.runtime, version: adapter.version, capabilities: adapter.capabilities, assertions: 11 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const results = [];
|
|
20
|
+
for (const factory of [createOpenClawAdapter, createHermesAdapter, createCodexCliAdapter, createClaudeCodeAdapter]) results.push(await conform(factory));
|
|
21
|
+
console.log(JSON.stringify({ status: 'passed', boundary: 'credential-free simulated transport', results }));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: production-trust
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
jobs:
|
|
6
|
+
deterministic-canary:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@v4
|
|
10
|
+
- uses: actions/setup-node@v4
|
|
11
|
+
with:
|
|
12
|
+
node-version: 22
|
|
13
|
+
- run: npm install --ignore-scripts --no-audit --no-fund
|
|
14
|
+
working-directory: packages/loop-engineering
|
|
15
|
+
- run: npm run check:production-trust
|
|
16
|
+
working-directory: packages/loop-engineering
|
|
17
|
+
- uses: actions/upload-artifact@v4
|
|
18
|
+
with:
|
|
19
|
+
name: production-trust-evidence
|
|
20
|
+
path: packages/loop-engineering/.production-evidence/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/ambitioncn/taskforce-loop-engineering/schemas/production-evidence-v1.json",
|
|
4
|
+
"title": "Loop Engineering production trust evidence",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schema", "schema_version", "generated_at", "scenarios", "compatibility", "metrics", "thresholds", "evaluation", "integrity", "passed"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schema": { "const": "loop.production_trust_evidence" },
|
|
9
|
+
"schema_version": { "const": 1 },
|
|
10
|
+
"generated_at": { "type": "string", "format": "date-time" },
|
|
11
|
+
"scenarios": { "type": "array", "minItems": 8 },
|
|
12
|
+
"compatibility": { "type": "array", "minItems": 3 },
|
|
13
|
+
"metrics": { "type": "object" },
|
|
14
|
+
"thresholds": { "type": "object" },
|
|
15
|
+
"evaluation": { "type": "object", "required": ["checks", "trend", "passed"] },
|
|
16
|
+
"integrity": { "type": "object", "required": ["algorithm", "digest"] },
|
|
17
|
+
"passed": { "type": "boolean" }
|
|
18
|
+
},
|
|
19
|
+
"additionalProperties": true
|
|
20
|
+
}
|