opencode-swarm 7.200.9 → 7.200.10
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/.opencode/skills/ci-fix-monitor/SKILL.md +49 -0
- package/.opencode/skills/swarm-pr-review/references/lane-output-recoverability.md +11 -2
- package/binaries/win32-arm64/swarm-sandbox-runner.exe +0 -0
- package/binaries/win32-x64/swarm-sandbox-runner.exe +0 -0
- package/dist/background/completion-observer.d.ts +2 -1
- package/dist/background/pending-delegations.d.ts +9 -1
- package/dist/background/pr-review-contract.d.ts +8 -0
- package/dist/cli/{attest-task-79gp34gf.js → attest-task-yx2t832n.js} +78 -76
- package/dist/cli/{auto-checkpoint-kt9mr7ma.js → auto-checkpoint-15mvj9zm.js} +12 -12
- package/dist/cli/{capability-probe-dwqn2609.js → capability-probe-qjndmx6z.js} +1 -1
- package/dist/cli/{checkpoint-e7t1zbz2.js → checkpoint-65whgvwa.js} +23 -23
- package/dist/cli/{coder-settlement-rehndcey.js → coder-settlement-sdg1he8p.js} +14 -13
- package/dist/cli/{config-doctor-hcemxnra.js → config-doctor-4af2p2y4.js} +19 -19
- package/dist/cli/{core-k8mvhsty.js → core-8swqc6kw.js} +6 -3
- package/dist/cli/{curation-policy-gy868ec3.js → curation-policy-1a23ftnv.js} +6 -6
- package/dist/cli/{curator-ayenbz0y.js → curator-6ze04fz3.js} +53 -52
- package/dist/cli/{curator-drift-fwzrv9tw.js → curator-drift-ktf137da.js} +10 -10
- package/dist/cli/curator-llm-factory-7vq4rgg7.js +83 -0
- package/dist/cli/{doctor-knowledge-receipt-phase-id-jdy6651j.js → doctor-knowledge-receipt-phase-id-0smwq3ar.js} +10 -10
- package/dist/cli/{evidence-summary-service-4jm1hd1r.js → evidence-summary-service-3bhq2gef.js} +26 -25
- package/dist/cli/{gate-evidence-anef2bqv.js → gate-evidence-gcv0xgfc.js} +10 -9
- package/dist/cli/guardrail-explain-9t3mvm92.js +84 -0
- package/dist/cli/{guardrail-log-pv3bq9hj.js → guardrail-log-ehgpan5n.js} +8 -8
- package/dist/cli/{guardrail-reset-k8b9yrec.js → guardrail-reset-q7m265wy.js} +53 -52
- package/dist/cli/{hive-promoter-zqscx6pb.js → hive-promoter-857ranjf.js} +53 -52
- package/dist/cli/{index-4krkevgc.js → index-001tkp0j.js} +1 -1
- package/dist/cli/{index-y16v2q1x.js → index-06sj5bjh.js} +5 -5
- package/dist/cli/{index-hh87yny7.js → index-2dey2mtg.js} +6 -6
- package/dist/cli/{index-9ws7m02r.js → index-3h5ztpn5.js} +2 -2
- package/dist/cli/{index-7c4gckzq.js → index-43zgg04r.js} +5 -5
- package/dist/cli/{index-casbh4y7.js → index-4dvqvn4k.js} +12905 -12414
- package/dist/cli/{index-6q2s7r67.js → index-60m7qnds.js} +8 -8
- package/dist/cli/{index-zy943kpp.js → index-66b4f2qd.js} +18 -13
- package/dist/cli/{index-c2k7q723.js → index-6cppzd9c.js} +1 -1
- package/dist/cli/{index-tvdn9gr0.js → index-6e7jxxys.js} +15 -15
- package/dist/cli/{index-ss8nfrbb.js → index-6ehx0pj4.js} +15 -15
- package/dist/cli/{index-yyrnkc3a.js → index-6m551cng.js} +2 -2
- package/dist/cli/{index-4fr3r8k5.js → index-8c6zshfj.js} +1 -1
- package/dist/cli/{index-8srvdpw6.js → index-8xtdxy91.js} +2 -2
- package/dist/cli/{index-mn75z39t.js → index-9qy3tzt5.js} +1 -1
- package/dist/cli/{index-x3rxa3sh.js → index-bt3pxs7r.js} +6 -6
- package/dist/cli/{index-4tpzdegx.js → index-c34tgnwm.js} +7 -7
- package/dist/cli/{index-y7c4hqcj.js → index-ck7jpn0y.js} +1 -1
- package/dist/cli/{index-p2dst45x.js → index-ckq78kq1.js} +1 -1
- package/dist/cli/{index-8mrentmw.js → index-dp6hy23p.js} +58 -53
- package/dist/cli/{index-4fkxdnc7.js → index-erp7fk72.js} +1 -1
- package/dist/cli/{index-2c4hjeea.js → index-ev46f132.js} +8 -8
- package/dist/cli/{index-hhq5f82z.js → index-fbn2mv67.js} +6 -6
- package/dist/cli/{index-qawbxp81.js → index-ffm3d4cn.js} +17 -17
- package/dist/cli/{index-f8nb470m.js → index-gsnj3a0h.js} +4 -4
- package/dist/cli/{index-hzy6n59p.js → index-h9qhy8r6.js} +6 -6
- package/dist/cli/{index-tecp3a77.js → index-kd1n3ne2.js} +9 -9
- package/dist/cli/{index-qw2wfz92.js → index-kxxy7zq4.js} +18 -20
- package/dist/cli/{index-czsx86m5.js → index-mamw83wm.js} +53 -52
- package/dist/cli/{index-2k5348b6.js → index-mx34b7bm.js} +58 -57
- package/dist/cli/{index-94s844qn.js → index-nv8tx9v4.js} +4 -4
- package/dist/cli/{index-q8vfw61p.js → index-pjrx4jkq.js} +1 -1
- package/dist/cli/{index-mhd6kdzr.js → index-qyj4383m.js} +1 -1
- package/dist/cli/{index-4ya4krts.js → index-r3wyt8za.js} +611 -1376
- package/dist/cli/{index-2x015vjy.js → index-rck7v4r4.js} +1 -1
- package/dist/cli/{index-vtpt8yns.js → index-rjsqf4hv.js} +2 -2
- package/dist/cli/{index-9jty56xr.js → index-rnm6c33a.js} +5 -5
- package/dist/cli/{index-bk8r4m9j.js → index-rpvp88js.js} +2 -2
- package/dist/cli/{index-v839c9mv.js → index-tggyracy.js} +1 -1
- package/dist/cli/{index-kxp43wbw.js → index-tm6nwe43.js} +29 -29
- package/dist/cli/{index-xhxbv7mz.js → index-vajbtn3v.js} +11 -11
- package/dist/cli/{index-rdyfrdcp.js → index-vm3j8fvn.js} +201 -33
- package/dist/cli/{index-08jd4gz2.js → index-vqknke9n.js} +121 -40
- package/dist/cli/{index-4ph7ad0b.js → index-vx1j2t5p.js} +2 -2
- package/dist/cli/{index-7h22y3ew.js → index-w0afnznk.js} +2 -2
- package/dist/cli/{index-02vgzrpg.js → index-ww5ne0g6.js} +7 -7
- package/dist/cli/index-xkavxqf2.js +1330 -0
- package/dist/cli/{index-xh50xsgf.js → index-ywwazz81.js} +8 -8
- package/dist/cli/{index-6901y99d.js → index-zmp1k0mb.js} +2 -2
- package/dist/cli/index.js +57 -56
- package/dist/cli/{knowledge-escalator-m4b4q218.js → knowledge-escalator-6p7m6gkc.js} +18 -18
- package/dist/cli/{knowledge-events-wwg0cc7m.js → knowledge-events-w3ep12sq.js} +15 -15
- package/dist/cli/{knowledge-link-13t3nfrz.js → knowledge-link-r7e2t5c1.js} +5 -5
- package/dist/cli/{knowledge-store-07fwtdzy.js → knowledge-store-g4cpw0hz.js} +7 -7
- package/dist/cli/{knowledge-validator-8492vmn3.js → knowledge-validator-zhy6mgxv.js} +9 -9
- package/dist/cli/{mcp-b1wyrebx.js → mcp-j1dy60mx.js} +1 -1
- package/dist/cli/{model-preflight-315wr8nt.js → model-preflight-962y2z1d.js} +56 -55
- package/dist/cli/{pending-delegations-tpa7wpny.js → pending-delegations-erjwa1y6.js} +14 -14
- package/dist/cli/{pr-subscriptions-ev2bkbrz.js → pr-subscriptions-7fcawjg9.js} +13 -13
- package/dist/cli/{pr-workflow-gate-3cg64r61.js → pr-workflow-gate-j8ee9c6g.js} +53 -52
- package/dist/cli/{runner-q1g9atrv.js → runner-s4422dq4.js} +6 -6
- package/dist/cli/{scan-cursor-3pfx6mbh.js → scan-cursor-msz8j44s.js} +8 -8
- package/dist/cli/{schema-km0fywpx.js → schema-kep9t01t.js} +2 -2
- package/dist/cli/{scope-persistence-xctbs0aj.js → scope-persistence-tx71g0ac.js} +18 -18
- package/dist/cli/{server-d1mcxa8s.js → server-6enk6sfw.js} +70 -69
- package/dist/cli/{skill-generator-tce30v8g.js → skill-generator-p9fkx1g3.js} +20 -20
- package/dist/cli/{snapshot-coordination-init-t7vfaqhw.js → snapshot-coordination-init-kgftbdmq.js} +53 -52
- package/dist/cli/{speckit-checkoff-tcn1dqt0.js → speckit-checkoff-jfdwsy5n.js} +5 -5
- package/dist/cli/{telemetry-cr8q16wk.js → telemetry-j7zr1dht.js} +1 -1
- package/dist/cli/workflow-wal-file-xvvwcxr1.js +32 -0
- package/dist/cli/{workspace-snapshot-7hws89e9.js → workspace-snapshot-ksdspd74.js} +3 -3
- package/dist/cli/{worktree-collision-ownership-5jb0p5aa.js → worktree-collision-ownership-hac3t2xv.js} +15 -15
- package/dist/cli/{worktree-isolation-cxg8fgba.js → worktree-isolation-tjvxcsr9.js} +53 -52
- package/dist/hooks/delegation-gate/worktree-isolation.d.ts +3 -0
- package/dist/hooks/shell-write-detect.d.ts +10 -0
- package/dist/index.js +686 -684
- package/dist/memory/redaction.d.ts +13 -3
- package/dist/pr-review/legacy-transcript-adapter.d.ts +15 -4
- package/dist/sandbox/macos/sandbox-exec-executor.d.ts +24 -0
- package/dist/workflow/coder-settlement.d.ts +5 -0
- package/dist/worktree/core.d.ts +3 -0
- package/dist/worktree/merge.d.ts +15 -1
- package/dist/worktree/ownership.d.ts +3 -0
- package/dist/worktree/settlement-artifact-lifecycle.d.ts +32 -0
- package/dist/worktree/settlement-source-recovery.d.ts +12 -0
- package/package.json +1 -1
- package/dist/cli/curator-llm-factory-kja1c81k.js +0 -82
- package/dist/cli/guardrail-explain-agaet7r5.js +0 -83
|
@@ -0,0 +1,1330 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
derivePlanId,
|
|
4
|
+
derivePlanIdentityHash
|
|
5
|
+
} from "./index-7pmbnjvj.js";
|
|
6
|
+
import {
|
|
7
|
+
readWorkflowWalFileSync
|
|
8
|
+
} from "./index-r3wyt8za.js";
|
|
9
|
+
import {
|
|
10
|
+
validateSwarmPath
|
|
11
|
+
} from "./index-001tkp0j.js";
|
|
12
|
+
import {
|
|
13
|
+
atomicWriteFile,
|
|
14
|
+
taskEvidencePath,
|
|
15
|
+
withTaskEvidenceLock
|
|
16
|
+
} from "./index-6m551cng.js";
|
|
17
|
+
import {
|
|
18
|
+
assertProjectRoot
|
|
19
|
+
} from "./index-t7hmqgjy.js";
|
|
20
|
+
import {
|
|
21
|
+
telemetry
|
|
22
|
+
} from "./index-6cppzd9c.js";
|
|
23
|
+
import {
|
|
24
|
+
exports_external,
|
|
25
|
+
init_zod
|
|
26
|
+
} from "./index-gknz3bv1.js";
|
|
27
|
+
|
|
28
|
+
// src/gate-evidence.ts
|
|
29
|
+
init_zod();
|
|
30
|
+
import { createHash as createHash2 } from "crypto";
|
|
31
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync } from "fs";
|
|
32
|
+
import * as path2 from "path";
|
|
33
|
+
|
|
34
|
+
// src/evidence/task-gate-requirements.ts
|
|
35
|
+
init_zod();
|
|
36
|
+
import { createHash } from "crypto";
|
|
37
|
+
import * as fs from "fs";
|
|
38
|
+
import * as path from "path";
|
|
39
|
+
var MAX_TASK_GATE_REQUIREMENTS_BYTES = 256 * 1024;
|
|
40
|
+
var TaskGateRequirementsReceiptSchema = exports_external.object({
|
|
41
|
+
schemaVersion: exports_external.literal(1),
|
|
42
|
+
taskId: exports_external.string().min(1).max(120),
|
|
43
|
+
generation: exports_external.number().int().min(0),
|
|
44
|
+
normalizedRole: exports_external.string().min(1).max(120),
|
|
45
|
+
testEngineerExempt: exports_external.boolean().nullable(),
|
|
46
|
+
requiredGates: exports_external.array(exports_external.string()).min(1).max(16),
|
|
47
|
+
sourceEvent: exports_external.string().min(1).max(120),
|
|
48
|
+
sourceTransitionId: exports_external.string().min(1).max(200).nullable(),
|
|
49
|
+
recordedAt: exports_external.string().min(1).max(128),
|
|
50
|
+
routeBinding: exports_external.object({
|
|
51
|
+
role: exports_external.enum(["reviewer", "test_engineer"]),
|
|
52
|
+
identity: exports_external.string().trim().min(1).max(256),
|
|
53
|
+
sessionId: exports_external.string().trim().min(1).max(256),
|
|
54
|
+
taskId: exports_external.string().trim().min(1).max(120),
|
|
55
|
+
slotId: exports_external.string().trim().min(1).max(256),
|
|
56
|
+
callId: exports_external.string().trim().min(1).max(256),
|
|
57
|
+
childSessionId: exports_external.string().trim().min(1).max(256),
|
|
58
|
+
generation: exports_external.number().int().nonnegative()
|
|
59
|
+
}).strict().optional(),
|
|
60
|
+
previousChainHash: exports_external.string().regex(/^[a-f0-9]{64}$/).nullable(),
|
|
61
|
+
chainHash: exports_external.string().regex(/^[a-f0-9]{64}$/)
|
|
62
|
+
}).strict();
|
|
63
|
+
function samePath(left, right) {
|
|
64
|
+
const normalize = (value) => process.platform === "win32" ? path.resolve(value).toLowerCase() : path.resolve(value);
|
|
65
|
+
return normalize(left) === normalize(right);
|
|
66
|
+
}
|
|
67
|
+
function assertSafeRequirementsParent(directory, create) {
|
|
68
|
+
assertProjectRoot(directory, undefined, "task gate requirements");
|
|
69
|
+
const rootStat = fs.lstatSync(directory);
|
|
70
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
71
|
+
throw new Error("TASK_GATE_REQUIREMENTS_UNREADABLE: project root is not a regular directory");
|
|
72
|
+
}
|
|
73
|
+
const canonicalRoot = fs.realpathSync(directory);
|
|
74
|
+
const segments = [".swarm", "evidence", "task-gate-requirements"];
|
|
75
|
+
let current = canonicalRoot;
|
|
76
|
+
for (const segment of segments) {
|
|
77
|
+
current = path.join(current, segment);
|
|
78
|
+
let info;
|
|
79
|
+
try {
|
|
80
|
+
info = fs.lstatSync(current);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error.code === "ENOENT") {
|
|
83
|
+
if (!create)
|
|
84
|
+
return current;
|
|
85
|
+
try {
|
|
86
|
+
fs.mkdirSync(current);
|
|
87
|
+
} catch (mkdirError) {
|
|
88
|
+
if (mkdirError.code !== "EEXIST") {
|
|
89
|
+
throw mkdirError;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
info = fs.lstatSync(current);
|
|
93
|
+
} else {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
98
|
+
throw new Error("TASK_GATE_REQUIREMENTS_UNREADABLE: receipt parent is redirected or not a directory");
|
|
99
|
+
}
|
|
100
|
+
const canonicalCurrent = fs.realpathSync(current);
|
|
101
|
+
if (!samePath(canonicalCurrent, current)) {
|
|
102
|
+
throw new Error("TASK_GATE_REQUIREMENTS_UNREADABLE: receipt parent escaped the project root");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return current;
|
|
106
|
+
}
|
|
107
|
+
function safeTaskGateRequirementsReceiptPath(directory, taskId, createParent) {
|
|
108
|
+
const parent = assertSafeRequirementsParent(directory, createParent);
|
|
109
|
+
return path.join(parent, `${taskId}.jsonl`);
|
|
110
|
+
}
|
|
111
|
+
function sha256(value) {
|
|
112
|
+
return createHash("sha256").update(value).digest("hex");
|
|
113
|
+
}
|
|
114
|
+
function canonicalReceiptPayload(receipt) {
|
|
115
|
+
const routeBinding = receipt.routeBinding ? {
|
|
116
|
+
role: receipt.routeBinding.role,
|
|
117
|
+
identity: receipt.routeBinding.identity,
|
|
118
|
+
sessionId: receipt.routeBinding.sessionId,
|
|
119
|
+
taskId: receipt.routeBinding.taskId,
|
|
120
|
+
slotId: receipt.routeBinding.slotId,
|
|
121
|
+
callId: receipt.routeBinding.callId,
|
|
122
|
+
childSessionId: receipt.routeBinding.childSessionId,
|
|
123
|
+
generation: receipt.routeBinding.generation
|
|
124
|
+
} : undefined;
|
|
125
|
+
return JSON.stringify({
|
|
126
|
+
schemaVersion: receipt.schemaVersion,
|
|
127
|
+
taskId: receipt.taskId,
|
|
128
|
+
generation: receipt.generation,
|
|
129
|
+
normalizedRole: receipt.normalizedRole,
|
|
130
|
+
testEngineerExempt: receipt.testEngineerExempt,
|
|
131
|
+
requiredGates: [...receipt.requiredGates],
|
|
132
|
+
sourceEvent: receipt.sourceEvent,
|
|
133
|
+
sourceTransitionId: receipt.sourceTransitionId,
|
|
134
|
+
recordedAt: receipt.recordedAt,
|
|
135
|
+
...routeBinding ? { routeBinding } : {},
|
|
136
|
+
previousChainHash: receipt.previousChainHash
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function computeChainHash(receipt) {
|
|
140
|
+
return sha256(canonicalReceiptPayload(receipt));
|
|
141
|
+
}
|
|
142
|
+
function routeBindingFromEvent(event) {
|
|
143
|
+
if (event.type !== "stage_b_completed" || !event.routeBinding)
|
|
144
|
+
return;
|
|
145
|
+
const binding = event.routeBinding;
|
|
146
|
+
if (binding.role !== event.gate || !binding.sessionId || !binding.taskId || !binding.slotId || !binding.callId || !binding.childSessionId || typeof binding.generation !== "number") {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
return binding;
|
|
150
|
+
}
|
|
151
|
+
function readBoundedTextFile(filePath) {
|
|
152
|
+
let pathStat;
|
|
153
|
+
try {
|
|
154
|
+
pathStat = fs.lstatSync(filePath);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error.code === "ENOENT")
|
|
157
|
+
return null;
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
if (pathStat.isSymbolicLink() || !pathStat.isFile()) {
|
|
161
|
+
throw new Error("TASK_GATE_REQUIREMENTS_UNREADABLE: receipt path is not a regular file");
|
|
162
|
+
}
|
|
163
|
+
if (pathStat.size > MAX_TASK_GATE_REQUIREMENTS_BYTES) {
|
|
164
|
+
throw new Error(`TASK_GATE_REQUIREMENTS_OVERSIZED: ${pathStat.size} bytes exceeds ${MAX_TASK_GATE_REQUIREMENTS_BYTES}`);
|
|
165
|
+
}
|
|
166
|
+
const descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
167
|
+
try {
|
|
168
|
+
const openedStat = fs.fstatSync(descriptor);
|
|
169
|
+
if (!openedStat.isFile() || openedStat.dev !== pathStat.dev || openedStat.ino !== pathStat.ino) {
|
|
170
|
+
throw new Error("TASK_GATE_REQUIREMENTS_UNREADABLE: receipt changed while being opened");
|
|
171
|
+
}
|
|
172
|
+
if (openedStat.size > MAX_TASK_GATE_REQUIREMENTS_BYTES) {
|
|
173
|
+
throw new Error(`TASK_GATE_REQUIREMENTS_OVERSIZED: ${openedStat.size} bytes exceeds ${MAX_TASK_GATE_REQUIREMENTS_BYTES}`);
|
|
174
|
+
}
|
|
175
|
+
return fs.readFileSync(descriptor, "utf-8");
|
|
176
|
+
} finally {
|
|
177
|
+
fs.closeSync(descriptor);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function readTaskGateRequirementsReceipts(directory, taskId) {
|
|
181
|
+
const filePath = safeTaskGateRequirementsReceiptPath(directory, taskId, false);
|
|
182
|
+
const text = readBoundedTextFile(filePath);
|
|
183
|
+
if (text === null)
|
|
184
|
+
return [];
|
|
185
|
+
const receipts = [];
|
|
186
|
+
for (const line of text.split(`
|
|
187
|
+
`)) {
|
|
188
|
+
if (!line.trim())
|
|
189
|
+
continue;
|
|
190
|
+
const parsed = TaskGateRequirementsReceiptSchema.parse(JSON.parse(line));
|
|
191
|
+
if (parsed.taskId !== taskId || parsed.routeBinding !== undefined && parsed.routeBinding.taskId !== taskId) {
|
|
192
|
+
throw new Error(`TASK_GATE_REQUIREMENTS_IDENTITY_MISMATCH: expected ${taskId}, found ${parsed.taskId}`);
|
|
193
|
+
}
|
|
194
|
+
const { chainHash, ...withoutChainHash } = parsed;
|
|
195
|
+
const expectedHash = computeChainHash(withoutChainHash);
|
|
196
|
+
if (expectedHash !== chainHash) {
|
|
197
|
+
throw new Error(`TASK_GATE_REQUIREMENTS_CHAIN_MISMATCH: receipt for ${taskId} failed hash validation`);
|
|
198
|
+
}
|
|
199
|
+
const previousChainHash = receipts.at(-1)?.chainHash ?? null;
|
|
200
|
+
if (parsed.previousChainHash !== previousChainHash) {
|
|
201
|
+
throw new Error(`TASK_GATE_REQUIREMENTS_CHAIN_MISMATCH: receipt for ${taskId} failed linkage validation`);
|
|
202
|
+
}
|
|
203
|
+
receipts.push(parsed);
|
|
204
|
+
}
|
|
205
|
+
return receipts;
|
|
206
|
+
}
|
|
207
|
+
function readTaskGateRequirementsReceiptsSync(directory, taskId) {
|
|
208
|
+
const filePath = safeTaskGateRequirementsReceiptPath(directory, taskId, false);
|
|
209
|
+
const text = readBoundedTextFile(filePath);
|
|
210
|
+
if (text === null)
|
|
211
|
+
return [];
|
|
212
|
+
const receipts = [];
|
|
213
|
+
for (const line of text.split(`
|
|
214
|
+
`)) {
|
|
215
|
+
if (!line.trim())
|
|
216
|
+
continue;
|
|
217
|
+
const parsed = TaskGateRequirementsReceiptSchema.parse(JSON.parse(line));
|
|
218
|
+
if (parsed.taskId !== taskId || parsed.routeBinding !== undefined && parsed.routeBinding.taskId !== taskId) {
|
|
219
|
+
throw new Error(`TASK_GATE_REQUIREMENTS_IDENTITY_MISMATCH: expected ${taskId}, found ${parsed.taskId}`);
|
|
220
|
+
}
|
|
221
|
+
const { chainHash, ...withoutChainHash } = parsed;
|
|
222
|
+
if (computeChainHash(withoutChainHash) !== chainHash) {
|
|
223
|
+
throw new Error(`TASK_GATE_REQUIREMENTS_CHAIN_MISMATCH: receipt for ${taskId} failed hash validation`);
|
|
224
|
+
}
|
|
225
|
+
const previousChainHash = receipts.at(-1)?.chainHash ?? null;
|
|
226
|
+
if (parsed.previousChainHash !== previousChainHash) {
|
|
227
|
+
throw new Error(`TASK_GATE_REQUIREMENTS_CHAIN_MISMATCH: receipt for ${taskId} failed linkage validation`);
|
|
228
|
+
}
|
|
229
|
+
receipts.push(parsed);
|
|
230
|
+
}
|
|
231
|
+
return receipts;
|
|
232
|
+
}
|
|
233
|
+
function routeEvidenceFromTaskGateRequirements(receipts) {
|
|
234
|
+
const latestByDispatch = new Map;
|
|
235
|
+
for (const receipt of receipts) {
|
|
236
|
+
const binding = receipt.routeBinding;
|
|
237
|
+
if (!binding)
|
|
238
|
+
continue;
|
|
239
|
+
const dispatchKey = [
|
|
240
|
+
binding.role,
|
|
241
|
+
binding.sessionId,
|
|
242
|
+
binding.taskId,
|
|
243
|
+
binding.callId,
|
|
244
|
+
binding.childSessionId,
|
|
245
|
+
String(binding.generation)
|
|
246
|
+
].join("\x00");
|
|
247
|
+
latestByDispatch.set(dispatchKey, binding);
|
|
248
|
+
}
|
|
249
|
+
return [...latestByDispatch.values()];
|
|
250
|
+
}
|
|
251
|
+
function isLegacyTaskGateRequirementsChain(receipts) {
|
|
252
|
+
return receipts.length > 0 && receipts.every((receipt) => !receipt.routeBinding);
|
|
253
|
+
}
|
|
254
|
+
function deriveReceiptRole(event) {
|
|
255
|
+
switch (event.type) {
|
|
256
|
+
case "accepted_mutation":
|
|
257
|
+
case "dispatch_attempted":
|
|
258
|
+
case "dispatch_no_mutation":
|
|
259
|
+
if (typeof event.agentType === "string" && event.agentType.trim().length > 0) {
|
|
260
|
+
return {
|
|
261
|
+
normalizedRole: event.agentType.trim(),
|
|
262
|
+
testEngineerExempt: event.agentType === "coder" ? event.context?.testEngineerExempt === true : null
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
case "stage_b_completed":
|
|
267
|
+
case "gate_recorded":
|
|
268
|
+
return {
|
|
269
|
+
normalizedRole: event.gate,
|
|
270
|
+
testEngineerExempt: null
|
|
271
|
+
};
|
|
272
|
+
default:
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function shouldAppendReceipt(current, next, role) {
|
|
277
|
+
if (next.required_gates.length === 0)
|
|
278
|
+
return false;
|
|
279
|
+
const currentGeneration = current?.workflow?.generation ?? -1;
|
|
280
|
+
const nextGeneration = next.workflow?.generation ?? -1;
|
|
281
|
+
if (current === null)
|
|
282
|
+
return true;
|
|
283
|
+
if (JSON.stringify(current.required_gates) !== JSON.stringify(next.required_gates)) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
if ((current.test_engineer_exempt ?? null) !== role.testEngineerExempt) {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
return currentGeneration !== nextGeneration;
|
|
290
|
+
}
|
|
291
|
+
async function appendTaskGateRequirementsReceiptIfNeeded(directory, taskId, current, next, event) {
|
|
292
|
+
const role = deriveReceiptRole(event);
|
|
293
|
+
const routeBinding = routeBindingFromEvent(event);
|
|
294
|
+
if (routeBinding && routeBinding.taskId !== taskId) {
|
|
295
|
+
throw new Error(`TASK_GATE_REQUIREMENTS_ROUTE_IDENTITY_MISMATCH: expected ${taskId}, found ${routeBinding.taskId}`);
|
|
296
|
+
}
|
|
297
|
+
if (!role || !shouldAppendReceipt(current, next, role) && !routeBinding) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const filePath = safeTaskGateRequirementsReceiptPath(directory, taskId, true);
|
|
301
|
+
const receipts = await readTaskGateRequirementsReceipts(directory, taskId);
|
|
302
|
+
const latest = receipts.at(-1);
|
|
303
|
+
const baseReceipt = {
|
|
304
|
+
schemaVersion: 1,
|
|
305
|
+
taskId,
|
|
306
|
+
generation: next.workflow?.generation ?? 0,
|
|
307
|
+
normalizedRole: role.normalizedRole,
|
|
308
|
+
testEngineerExempt: role.testEngineerExempt,
|
|
309
|
+
requiredGates: [...next.required_gates],
|
|
310
|
+
sourceEvent: event.type,
|
|
311
|
+
sourceTransitionId: event.transitionId ?? null,
|
|
312
|
+
recordedAt: new Date().toISOString(),
|
|
313
|
+
previousChainHash: latest?.chainHash ?? null,
|
|
314
|
+
...routeBinding ? { routeBinding } : {}
|
|
315
|
+
};
|
|
316
|
+
const receipt = {
|
|
317
|
+
...baseReceipt,
|
|
318
|
+
chainHash: computeChainHash(baseReceipt)
|
|
319
|
+
};
|
|
320
|
+
if (latest && latest.generation === receipt.generation && latest.normalizedRole === receipt.normalizedRole && latest.testEngineerExempt === receipt.testEngineerExempt && JSON.stringify(latest.requiredGates) === JSON.stringify(receipt.requiredGates) && latest.sourceTransitionId === receipt.sourceTransitionId && JSON.stringify(latest.routeBinding ?? null) === JSON.stringify(receipt.routeBinding ?? null)) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const lines = [...receipts, receipt].map((entry) => JSON.stringify(entry));
|
|
324
|
+
assertSafeRequirementsParent(directory, false);
|
|
325
|
+
await atomicWriteFile(filePath, `${lines.join(`
|
|
326
|
+
`)}
|
|
327
|
+
`);
|
|
328
|
+
assertSafeRequirementsParent(directory, false);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/validation/task-id.ts
|
|
332
|
+
var STRICT_TASK_ID_PATTERN = /^\d+\.\d+(\.\d+)*$/;
|
|
333
|
+
var RETRO_TASK_ID_REGEX = /^retro-\d+$/;
|
|
334
|
+
var INTERNAL_TOOL_ID_REGEX = /^(?:sast_scan|quality_budget|syntax_check|placeholder_scan|sbom_generate|build|secretscan)$/;
|
|
335
|
+
var GENERAL_TASK_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
336
|
+
function checkUnsafeChars(taskId) {
|
|
337
|
+
if (!taskId || taskId.length === 0) {
|
|
338
|
+
return "Invalid task ID: empty string";
|
|
339
|
+
}
|
|
340
|
+
if (/\0/.test(taskId)) {
|
|
341
|
+
return "Invalid task ID: contains null bytes";
|
|
342
|
+
}
|
|
343
|
+
for (let i = 0;i < taskId.length; i++) {
|
|
344
|
+
if (taskId.charCodeAt(i) < 32) {
|
|
345
|
+
return "Invalid task ID: contains control characters";
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (taskId.includes("..") || taskId.includes("/") || taskId.includes("\\")) {
|
|
349
|
+
return "Invalid task ID: path traversal detected";
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
function isStrictTaskId(taskId) {
|
|
354
|
+
if (!taskId)
|
|
355
|
+
return false;
|
|
356
|
+
const unsafeMsg = checkUnsafeChars(taskId);
|
|
357
|
+
if (unsafeMsg)
|
|
358
|
+
return false;
|
|
359
|
+
return STRICT_TASK_ID_PATTERN.test(taskId);
|
|
360
|
+
}
|
|
361
|
+
function assertStrictTaskId(taskId) {
|
|
362
|
+
if (!isStrictTaskId(taskId)) {
|
|
363
|
+
throw new Error(`Invalid taskId: "${taskId}". Must match N.M or N.M.P (e.g. "1.1", "1.2.3").`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function sanitizeTaskId(taskId) {
|
|
367
|
+
const unsafeMsg = checkUnsafeChars(taskId);
|
|
368
|
+
if (unsafeMsg) {
|
|
369
|
+
throw new Error(unsafeMsg);
|
|
370
|
+
}
|
|
371
|
+
if (STRICT_TASK_ID_PATTERN.test(taskId) || RETRO_TASK_ID_REGEX.test(taskId) || INTERNAL_TOOL_ID_REGEX.test(taskId) || GENERAL_TASK_ID_REGEX.test(taskId)) {
|
|
372
|
+
return taskId;
|
|
373
|
+
}
|
|
374
|
+
throw new Error(`Invalid task ID: must be alphanumeric (ASCII) with optional hyphens, underscores, or dots, got "${taskId}"`);
|
|
375
|
+
}
|
|
376
|
+
function validateTaskIdFormat(taskId) {
|
|
377
|
+
if (!STRICT_TASK_ID_PATTERN.test(taskId)) {
|
|
378
|
+
return `Invalid taskId "${taskId}". Must match pattern N.M or N.M.P (e.g., "1.1", "1.2.3")`;
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/gate-evidence.ts
|
|
384
|
+
var _internals = {
|
|
385
|
+
appendTaskGateRequirementsReceiptIfNeeded
|
|
386
|
+
};
|
|
387
|
+
var TASK_WORKFLOW_SCHEMA_MARKER = "exact-task-v1";
|
|
388
|
+
var TASK_WORKFLOW_STATES = [
|
|
389
|
+
"idle",
|
|
390
|
+
"coder_delegated",
|
|
391
|
+
"pre_check_passed",
|
|
392
|
+
"reviewer_run",
|
|
393
|
+
"tests_run",
|
|
394
|
+
"rework_required",
|
|
395
|
+
"complete",
|
|
396
|
+
"blocked",
|
|
397
|
+
"closed"
|
|
398
|
+
];
|
|
399
|
+
var TASK_WORKFLOW_OUTCOMES = [
|
|
400
|
+
"none",
|
|
401
|
+
"dispatch_attempted",
|
|
402
|
+
"dispatch_no_mutation",
|
|
403
|
+
"accepted_mutation",
|
|
404
|
+
"accepted_mutation_failed",
|
|
405
|
+
"stage_a_passed",
|
|
406
|
+
"stage_a_failed",
|
|
407
|
+
"stage_b_completed",
|
|
408
|
+
"stage_b_failed",
|
|
409
|
+
"gate_recorded",
|
|
410
|
+
"task_completed",
|
|
411
|
+
"task_blocked",
|
|
412
|
+
"task_closed",
|
|
413
|
+
"repair_idle",
|
|
414
|
+
"external_evidence_attested",
|
|
415
|
+
"external_evidence_revoked"
|
|
416
|
+
];
|
|
417
|
+
var MAX_PLAN_SCOPE_INSPECTION_BYTES = 4 * 1024 * 1024;
|
|
418
|
+
function readCurrentTaskDeclaredFiles(directory, taskId) {
|
|
419
|
+
try {
|
|
420
|
+
const planPath = validateSwarmPath(directory, "plan.json");
|
|
421
|
+
const stat = statSync(planPath);
|
|
422
|
+
if (!stat.isFile() || stat.size > MAX_PLAN_SCOPE_INSPECTION_BYTES) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
const parsed = JSON.parse(readFileSync2(planPath, "utf8"));
|
|
426
|
+
if (!Array.isArray(parsed.phases))
|
|
427
|
+
return null;
|
|
428
|
+
let declaredFiles = null;
|
|
429
|
+
for (const phase of parsed.phases) {
|
|
430
|
+
if (!phase || typeof phase !== "object")
|
|
431
|
+
return null;
|
|
432
|
+
const tasks = phase.tasks;
|
|
433
|
+
if (!Array.isArray(tasks))
|
|
434
|
+
return null;
|
|
435
|
+
for (const task of tasks) {
|
|
436
|
+
if (!task || typeof task !== "object")
|
|
437
|
+
return null;
|
|
438
|
+
const record = task;
|
|
439
|
+
if (record.id !== taskId)
|
|
440
|
+
continue;
|
|
441
|
+
if (!Array.isArray(record.files_touched) || !record.files_touched.every((file) => typeof file === "string")) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
if (declaredFiles !== null)
|
|
445
|
+
return null;
|
|
446
|
+
declaredFiles = [...record.files_touched];
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return declaredFiles;
|
|
450
|
+
} catch {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
var TASK_GATE_REQUIREMENTS_RECONSTRUCTION_SENTINEL = "requirements_reconstruction";
|
|
455
|
+
function assertTaskEvidenceWriteAllowed(directory, taskId, event) {
|
|
456
|
+
assertValidTaskId(taskId);
|
|
457
|
+
const coderWalPath = validateSwarmPath(directory, `coder-settlements/${taskId}.json`);
|
|
458
|
+
const coderWal = readWorkflowWalFileSync("coder-settlement", coderWalPath, taskId);
|
|
459
|
+
if ((coderWal?.state === "DISPATCHED" || coderWal?.state === "PREPARED") && !(event != null && event.transitionId === coderWal.transitionId && (event.type === "dispatch_attempted" || event.type === "dispatch_no_mutation" || event.type === "accepted_mutation"))) {
|
|
460
|
+
throw new Error(`CODER_SETTLEMENT_IN_PROGRESS: transition ${String(coderWal.transitionId)} owns evidence for task ${taskId}. Wait for the owning transition to settle or run /swarm recover ${taskId} (or /swarm reset-session), then retry; do not remove the WAL by hand.`);
|
|
461
|
+
}
|
|
462
|
+
const terminalWalPath = validateSwarmPath(directory, `task-terminals/${taskId}.json`);
|
|
463
|
+
const terminalWal = readWorkflowWalFileSync("task-terminal", terminalWalPath, taskId);
|
|
464
|
+
if (terminalWal?.state === "PREPARED") {
|
|
465
|
+
const matchingTerminalEvent = event != null && event.transitionId === terminalWal.transitionId && (event.type === "task_completed" && terminalWal.newPlanStatus === "completed" || event.type === "task_blocked" && terminalWal.newPlanStatus === "blocked" || event.type === "task_closed" && terminalWal.newPlanStatus === "closed");
|
|
466
|
+
if (!matchingTerminalEvent) {
|
|
467
|
+
throw new Error(`TASK_TERMINAL_PREPARED: transition ${String(terminalWal.transitionId)} owns evidence for task ${taskId}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const repairWalPath = validateSwarmPath(directory, `task-repairs/${taskId}.json`);
|
|
471
|
+
const repairWal = readWorkflowWalFileSync("task-repair", repairWalPath, taskId);
|
|
472
|
+
if (repairWal?.state === "PREPARED" && !(event?.type === "repair_idle" && event.transitionId === repairWal.transitionId)) {
|
|
473
|
+
throw new Error(`TASK_REPAIR_PREPARED: transition ${String(repairWal.transitionId)} owns evidence for task ${taskId}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
var GateEvidenceSchema = exports_external.object({
|
|
477
|
+
sessionId: exports_external.string(),
|
|
478
|
+
timestamp: exports_external.string(),
|
|
479
|
+
agent: exports_external.string()
|
|
480
|
+
}).passthrough();
|
|
481
|
+
var ExternalEvidenceBundleSchema = exports_external.object({
|
|
482
|
+
class: exports_external.literal("external_git"),
|
|
483
|
+
transitionId: exports_external.string().min(1).max(512),
|
|
484
|
+
declarationDigest: exports_external.string().regex(/^[a-f0-9]{64}$/),
|
|
485
|
+
anchors: exports_external.array(exports_external.object({
|
|
486
|
+
command: exports_external.string().min(1).max(400),
|
|
487
|
+
expect: exports_external.string().max(4096).optional(),
|
|
488
|
+
expect_prefix: exports_external.string().min(1).max(4096).optional(),
|
|
489
|
+
matched: exports_external.boolean(),
|
|
490
|
+
exit_code: exports_external.number().int(),
|
|
491
|
+
stdout: exports_external.string().max(4096)
|
|
492
|
+
})).min(1).max(16).superRefine((anchors, ctx) => {
|
|
493
|
+
anchors.forEach((anchor, index) => {
|
|
494
|
+
if (anchor.expect === undefined && anchor.expect_prefix === undefined) {
|
|
495
|
+
ctx.addIssue({
|
|
496
|
+
code: exports_external.ZodIssueCode.custom,
|
|
497
|
+
message: `anchor ${index} must carry expect and/or a non-empty expect_prefix`,
|
|
498
|
+
path: [index]
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
}),
|
|
503
|
+
attestedAt: exports_external.string().min(1)
|
|
504
|
+
});
|
|
505
|
+
var TaskWorkflowMetadataSchema = exports_external.object({
|
|
506
|
+
schema: exports_external.literal(TASK_WORKFLOW_SCHEMA_MARKER),
|
|
507
|
+
generation: exports_external.number().int().min(0),
|
|
508
|
+
state: exports_external.enum(TASK_WORKFLOW_STATES),
|
|
509
|
+
retryCount: exports_external.number().int().min(0).default(0),
|
|
510
|
+
retryHistory: exports_external.array(exports_external.enum(TASK_WORKFLOW_OUTCOMES)).max(3).default([]),
|
|
511
|
+
retryEpoch: exports_external.number().int().min(0).default(0),
|
|
512
|
+
lastOutcome: exports_external.enum(TASK_WORKFLOW_OUTCOMES).default("none"),
|
|
513
|
+
lastTransitionId: exports_external.string().min(1).nullable().optional().default(null),
|
|
514
|
+
updatedAt: exports_external.string(),
|
|
515
|
+
forcedCompletion: exports_external.boolean().optional(),
|
|
516
|
+
supervisedRecovery: exports_external.boolean().optional(),
|
|
517
|
+
settlementRecovery: exports_external.boolean().optional(),
|
|
518
|
+
noMutationSettlement: exports_external.object({
|
|
519
|
+
generation: exports_external.literal(0),
|
|
520
|
+
transitionId: exports_external.string().min(1).max(512),
|
|
521
|
+
declaredFiles: exports_external.array(exports_external.string()).length(0)
|
|
522
|
+
}).optional(),
|
|
523
|
+
externalEvidence: ExternalEvidenceBundleSchema.optional()
|
|
524
|
+
});
|
|
525
|
+
var TaskEvidenceSchema = exports_external.object({
|
|
526
|
+
taskId: exports_external.string(),
|
|
527
|
+
required_gates: exports_external.array(exports_external.string()).default([]),
|
|
528
|
+
gates: exports_external.record(exports_external.string(), GateEvidenceSchema),
|
|
529
|
+
turbo: exports_external.boolean().optional(),
|
|
530
|
+
requirements_state: exports_external.enum(["known", "unknown"]).optional(),
|
|
531
|
+
test_engineer_exempt: exports_external.boolean().optional(),
|
|
532
|
+
planId: exports_external.string().optional(),
|
|
533
|
+
planIdentityHash: exports_external.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
534
|
+
workflow: TaskWorkflowMetadataSchema.optional(),
|
|
535
|
+
repair_provenance: exports_external.object({
|
|
536
|
+
source_sha256: exports_external.string().regex(/^[a-f0-9]{64}$/).nullable(),
|
|
537
|
+
source_generation: exports_external.number().int().min(0).nullable(),
|
|
538
|
+
requirements_receipt_hash: exports_external.string().regex(/^[a-f0-9]{64}$/).nullable()
|
|
539
|
+
}).optional(),
|
|
540
|
+
todo_scan: exports_external.object({
|
|
541
|
+
priority: exports_external.string(),
|
|
542
|
+
count: exports_external.number().int().min(0),
|
|
543
|
+
details: exports_external.array(exports_external.string()).max(200).optional(),
|
|
544
|
+
recorded_at: exports_external.string().optional()
|
|
545
|
+
}).optional()
|
|
546
|
+
});
|
|
547
|
+
function parseTaskEvidence(raw, expectedTaskId) {
|
|
548
|
+
const parsed = TaskEvidenceSchema.parse(JSON.parse(raw));
|
|
549
|
+
if (parsed.taskId !== expectedTaskId) {
|
|
550
|
+
throw new Error(`TASK_EVIDENCE_IDENTITY_MISMATCH: expected ${expectedTaskId}, found ${parsed.taskId}`);
|
|
551
|
+
}
|
|
552
|
+
return parsed;
|
|
553
|
+
}
|
|
554
|
+
function resolveCurrentPlanIdentitySync(directory) {
|
|
555
|
+
if (!directory)
|
|
556
|
+
return null;
|
|
557
|
+
try {
|
|
558
|
+
const planPath = path2.join(directory, ".swarm", "plan.json");
|
|
559
|
+
const raw = readFileSync2(planPath, "utf8");
|
|
560
|
+
const plan = JSON.parse(raw);
|
|
561
|
+
if (typeof plan.swarm !== "string" || typeof plan.title !== "string")
|
|
562
|
+
return null;
|
|
563
|
+
return {
|
|
564
|
+
planId: derivePlanId(plan),
|
|
565
|
+
planIdentityHash: derivePlanIdentityHash(plan)
|
|
566
|
+
};
|
|
567
|
+
} catch {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function evidenceStampedForDifferentPlan(evidence, currentPlanIdentityHash) {
|
|
572
|
+
if (evidence == null || currentPlanIdentityHash == null)
|
|
573
|
+
return false;
|
|
574
|
+
return typeof evidence.planIdentityHash === "string" && evidence.planIdentityHash !== currentPlanIdentityHash;
|
|
575
|
+
}
|
|
576
|
+
function planStampForPersist(nextEvidence, previous, planIdentity, campaignRestart) {
|
|
577
|
+
if (nextEvidence.planIdentityHash !== undefined)
|
|
578
|
+
return {};
|
|
579
|
+
const carriesGates = Object.keys(nextEvidence.gates ?? {}).length > 0;
|
|
580
|
+
if (carriesGates && previous != null && !campaignRestart) {
|
|
581
|
+
return previous.planIdentityHash !== undefined ? {
|
|
582
|
+
...previous.planId !== undefined ? { planId: previous.planId } : {},
|
|
583
|
+
planIdentityHash: previous.planIdentityHash
|
|
584
|
+
} : {};
|
|
585
|
+
}
|
|
586
|
+
return planIdentity ? {
|
|
587
|
+
planId: planIdentity.planId,
|
|
588
|
+
planIdentityHash: planIdentity.planIdentityHash
|
|
589
|
+
} : {};
|
|
590
|
+
}
|
|
591
|
+
var DEFAULT_REQUIRED_GATES = ["reviewer", "test_engineer"];
|
|
592
|
+
var DEFAULT_WORKFLOW_STATE = "idle";
|
|
593
|
+
var WORKFLOW_STATE_RANK = {
|
|
594
|
+
idle: 0,
|
|
595
|
+
coder_delegated: 1,
|
|
596
|
+
pre_check_passed: 2,
|
|
597
|
+
reviewer_run: 3,
|
|
598
|
+
tests_run: 4,
|
|
599
|
+
rework_required: 5,
|
|
600
|
+
blocked: 6,
|
|
601
|
+
closed: 7,
|
|
602
|
+
complete: 8
|
|
603
|
+
};
|
|
604
|
+
function isValidTaskId(taskId) {
|
|
605
|
+
return isStrictTaskId(taskId);
|
|
606
|
+
}
|
|
607
|
+
function assertValidTaskId(taskId) {
|
|
608
|
+
assertStrictTaskId(taskId);
|
|
609
|
+
}
|
|
610
|
+
function createDefaultWorkflowMetadata(nowIso) {
|
|
611
|
+
return {
|
|
612
|
+
schema: TASK_WORKFLOW_SCHEMA_MARKER,
|
|
613
|
+
generation: 0,
|
|
614
|
+
state: DEFAULT_WORKFLOW_STATE,
|
|
615
|
+
retryCount: 0,
|
|
616
|
+
retryHistory: [],
|
|
617
|
+
retryEpoch: 0,
|
|
618
|
+
lastOutcome: "none",
|
|
619
|
+
lastTransitionId: null,
|
|
620
|
+
updatedAt: nowIso
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function getEventOutcome(event) {
|
|
624
|
+
switch (event.type) {
|
|
625
|
+
case "dispatch_attempted":
|
|
626
|
+
return "dispatch_attempted";
|
|
627
|
+
case "dispatch_no_mutation":
|
|
628
|
+
return "dispatch_no_mutation";
|
|
629
|
+
case "accepted_mutation":
|
|
630
|
+
return event.context?.settlementFailed === true ? "accepted_mutation_failed" : "accepted_mutation";
|
|
631
|
+
case "stage_a_passed":
|
|
632
|
+
return "stage_a_passed";
|
|
633
|
+
case "stage_a_failed":
|
|
634
|
+
return "stage_a_failed";
|
|
635
|
+
case "stage_b_completed":
|
|
636
|
+
return "stage_b_completed";
|
|
637
|
+
case "stage_b_failed":
|
|
638
|
+
return "stage_b_failed";
|
|
639
|
+
case "gate_recorded":
|
|
640
|
+
return "gate_recorded";
|
|
641
|
+
case "task_completed":
|
|
642
|
+
return "task_completed";
|
|
643
|
+
case "task_blocked":
|
|
644
|
+
return "task_blocked";
|
|
645
|
+
case "task_closed":
|
|
646
|
+
return "task_closed";
|
|
647
|
+
case "repair_idle":
|
|
648
|
+
return "repair_idle";
|
|
649
|
+
case "external_evidence_attested":
|
|
650
|
+
return "external_evidence_attested";
|
|
651
|
+
case "external_evidence_revoked":
|
|
652
|
+
return "external_evidence_revoked";
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function isAuthoritativeWorkflowMetadata(workflow) {
|
|
656
|
+
return workflow?.schema === TASK_WORKFLOW_SCHEMA_MARKER;
|
|
657
|
+
}
|
|
658
|
+
function getTaskWorkflowSnapshot(evidence) {
|
|
659
|
+
if (isAuthoritativeWorkflowMetadata(evidence?.workflow)) {
|
|
660
|
+
return { ...evidence.workflow, authoritative: true };
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
...createDefaultWorkflowMetadata(""),
|
|
664
|
+
authoritative: false
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function maybeExpandRequiredGates(existing, event) {
|
|
668
|
+
if (event.type === "accepted_mutation") {
|
|
669
|
+
const agentType = event.agentType;
|
|
670
|
+
if (typeof agentType === "string" && agentType.trim() !== "") {
|
|
671
|
+
return existing ? expandRequiredGates(existing.required_gates.filter((gate) => gate !== TASK_GATE_REQUIREMENTS_RECONSTRUCTION_SENTINEL), agentType, event.context ?? {}) : deriveRequiredGates(agentType, event.context ?? {});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (event.type === "stage_b_completed" && event.ensureDefaultStageB === true) {
|
|
675
|
+
return expandRequiredGates(existing?.required_gates ?? [], "coder", {
|
|
676
|
+
testEngineerExempt: false
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
if (event.type === "stage_b_completed" || event.type === "gate_recorded") {
|
|
680
|
+
return existing ? expandRequiredGates(existing.required_gates, event.gate) : deriveRequiredGates(event.gate);
|
|
681
|
+
}
|
|
682
|
+
return existing?.required_gates ?? [];
|
|
683
|
+
}
|
|
684
|
+
function hasAllRequiredGatesPassed(requiredGates, gates) {
|
|
685
|
+
return requiredGates.length > 0 && requiredGates.every((gate) => gates[gate] != null);
|
|
686
|
+
}
|
|
687
|
+
function shouldTreatGateAsStageB(gate) {
|
|
688
|
+
return gate === "reviewer" || gate === "test_engineer";
|
|
689
|
+
}
|
|
690
|
+
function clearWorkflowGateProof(gates) {
|
|
691
|
+
const retained = { ...gates };
|
|
692
|
+
delete retained.pre_check;
|
|
693
|
+
delete retained.reviewer;
|
|
694
|
+
delete retained.test_engineer;
|
|
695
|
+
delete retained.council;
|
|
696
|
+
delete retained.critic;
|
|
697
|
+
delete retained.critic_sounding_board;
|
|
698
|
+
return retained;
|
|
699
|
+
}
|
|
700
|
+
function assertExpectedGeneration(snapshot, event) {
|
|
701
|
+
if (typeof event.expectedGeneration === "number" && event.expectedGeneration !== snapshot.generation) {
|
|
702
|
+
throw new Error(`TASK_WORKFLOW_GENERATION_MISMATCH: expected ${event.expectedGeneration}, current ${snapshot.generation}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function isDuplicateTransition(snapshot, event) {
|
|
706
|
+
if (event.type === "dispatch_no_mutation" && event.context?.declaredFiles?.length === 0 && event.context.settlementTransitionId === event.transitionId && !snapshot.noMutationSettlement) {
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
return snapshot.authoritative && typeof event.transitionId === "string" && event.transitionId.length > 0 && snapshot.lastTransitionId === event.transitionId && snapshot.lastOutcome === getEventOutcome(event);
|
|
710
|
+
}
|
|
711
|
+
function isNoMutationSettlementMetadata(workflow) {
|
|
712
|
+
const settlement = workflow.noMutationSettlement;
|
|
713
|
+
if (!workflow.authoritative || workflow.generation !== 0 || settlement?.generation !== 0 || typeof settlement.transitionId !== "string" || settlement.transitionId.length === 0 || !Array.isArray(settlement.declaredFiles) || settlement.declaredFiles.length !== 0)
|
|
714
|
+
return false;
|
|
715
|
+
return workflow.state === "idle" || workflow.state === "complete";
|
|
716
|
+
}
|
|
717
|
+
function deriveApplicableGateSet(evidence, options) {
|
|
718
|
+
if (!evidence) {
|
|
719
|
+
return {
|
|
720
|
+
requiredGates: ["pre_check"],
|
|
721
|
+
satisfiedGates: [],
|
|
722
|
+
missingGates: ["pre_check"],
|
|
723
|
+
readOnlyNoMutation: false
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const workflow = getTaskWorkflowSnapshot(evidence);
|
|
727
|
+
const readOnlyNoMutation = isNoMutationSettlementMetadata(workflow) && (options?.currentDeclaredFiles === undefined ? true : options.currentDeclaredFiles !== null && options.currentDeclaredFiles.length === 0);
|
|
728
|
+
const externalEvidenceMatched = workflow.lastOutcome === "external_evidence_attested" && (workflow.externalEvidence?.anchors.length ?? 0) > 0 && workflow.externalEvidence.anchors.every((anchor) => anchor.matched);
|
|
729
|
+
const requiredGates = [...new Set(evidence.required_gates ?? [])];
|
|
730
|
+
if (!readOnlyNoMutation && !externalEvidenceMatched && !requiredGates.includes("pre_check")) {
|
|
731
|
+
requiredGates.unshift("pre_check");
|
|
732
|
+
}
|
|
733
|
+
const satisfiedGates = requiredGates.filter((gate) => evidence.gates?.[gate] != null);
|
|
734
|
+
return {
|
|
735
|
+
requiredGates,
|
|
736
|
+
satisfiedGates,
|
|
737
|
+
missingGates: requiredGates.filter((gate) => evidence.gates?.[gate] == null),
|
|
738
|
+
readOnlyNoMutation
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
function isReadOnlyNoMutationEligible(evidence, options) {
|
|
742
|
+
return deriveApplicableGateSet(evidence, options).readOnlyNoMutation;
|
|
743
|
+
}
|
|
744
|
+
function computeExternalDeclarationDigest(input) {
|
|
745
|
+
const canonical = {
|
|
746
|
+
taskId: input.taskId,
|
|
747
|
+
evidence_class: input.evidence_class,
|
|
748
|
+
evidence_anchors: input.evidence_anchors.map((anchor) => ({
|
|
749
|
+
command: anchor.command,
|
|
750
|
+
expect: anchor.expect,
|
|
751
|
+
expect_prefix: anchor.expect_prefix
|
|
752
|
+
})),
|
|
753
|
+
plan_title: input.plan_title,
|
|
754
|
+
plan_swarm: input.plan_swarm,
|
|
755
|
+
generation: input.generation
|
|
756
|
+
};
|
|
757
|
+
return createHash2("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
758
|
+
}
|
|
759
|
+
function externalProofCurrent(plan, planTask, evidence) {
|
|
760
|
+
if (planTask.evidence_class !== "external_git")
|
|
761
|
+
return false;
|
|
762
|
+
const workflow = evidence?.workflow;
|
|
763
|
+
if (!workflow)
|
|
764
|
+
return false;
|
|
765
|
+
const bundle = workflow.externalEvidence;
|
|
766
|
+
if (!bundle || bundle.anchors.length === 0)
|
|
767
|
+
return false;
|
|
768
|
+
if (!bundle.anchors.every((anchor) => anchor.matched))
|
|
769
|
+
return false;
|
|
770
|
+
return bundle.declarationDigest === computeExternalDeclarationDigest({
|
|
771
|
+
taskId: planTask.id,
|
|
772
|
+
evidence_class: "external_git",
|
|
773
|
+
evidence_anchors: planTask.evidence_anchors ?? [],
|
|
774
|
+
plan_title: plan.title,
|
|
775
|
+
plan_swarm: plan.swarm,
|
|
776
|
+
generation: workflow.generation
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
function isExternalEvidenceInitialCompletionEligible(plan, planTask, evidence) {
|
|
780
|
+
if (!externalProofCurrent(plan, planTask, evidence))
|
|
781
|
+
return false;
|
|
782
|
+
const snapshot = getTaskWorkflowSnapshot(evidence);
|
|
783
|
+
return snapshot.authoritative && snapshot.state === "idle";
|
|
784
|
+
}
|
|
785
|
+
function reduceTaskWorkflowSnapshot(current, event, context) {
|
|
786
|
+
if ((current.state === "complete" || current.state === "blocked" || current.state === "closed") && event.type !== "repair_idle" && !(current.state === "complete" && event.type === "task_completed" || current.state === "blocked" && event.type === "task_blocked" || current.state === "blocked" && event.type === "task_closed" || current.state === "blocked" && event.type === "stage_a_passed" && event.settlementRecovery === true)) {
|
|
787
|
+
throw new Error(`TASK_WORKFLOW_TERMINAL: cannot apply ${event.type} from ${current.state}`);
|
|
788
|
+
}
|
|
789
|
+
const outcome = getEventOutcome(event);
|
|
790
|
+
const base = {
|
|
791
|
+
schema: TASK_WORKFLOW_SCHEMA_MARKER,
|
|
792
|
+
generation: current.generation,
|
|
793
|
+
state: current.state,
|
|
794
|
+
retryCount: current.retryCount,
|
|
795
|
+
retryHistory: current.retryHistory,
|
|
796
|
+
retryEpoch: current.retryEpoch,
|
|
797
|
+
lastOutcome: outcome,
|
|
798
|
+
lastTransitionId: event.transitionId ?? current.lastTransitionId,
|
|
799
|
+
updatedAt: context.nowIso,
|
|
800
|
+
...current.forcedCompletion === true ? { forcedCompletion: true } : {},
|
|
801
|
+
...current.supervisedRecovery === true ? { supervisedRecovery: true } : {},
|
|
802
|
+
...current.settlementRecovery === true ? { settlementRecovery: true } : {},
|
|
803
|
+
...current.noMutationSettlement ? { noMutationSettlement: current.noMutationSettlement } : {},
|
|
804
|
+
...current.externalEvidence ? { externalEvidence: current.externalEvidence } : {}
|
|
805
|
+
};
|
|
806
|
+
switch (event.type) {
|
|
807
|
+
case "dispatch_attempted": {
|
|
808
|
+
const {
|
|
809
|
+
noMutationSettlement: _clearedSettlement,
|
|
810
|
+
externalEvidence: _clearedExternalEvidence,
|
|
811
|
+
...withoutSettlement
|
|
812
|
+
} = base;
|
|
813
|
+
return withoutSettlement;
|
|
814
|
+
}
|
|
815
|
+
case "dispatch_no_mutation": {
|
|
816
|
+
const {
|
|
817
|
+
noMutationSettlement: _clearedSettlement,
|
|
818
|
+
externalEvidence: _clearedExternalEvidence,
|
|
819
|
+
...withoutSettlement
|
|
820
|
+
} = base;
|
|
821
|
+
return {
|
|
822
|
+
...withoutSettlement,
|
|
823
|
+
retryCount: Math.min(current.retryCount + 1, 3),
|
|
824
|
+
retryHistory: [...current.retryHistory, outcome].slice(-3),
|
|
825
|
+
retryEpoch: current.retryEpoch || current.generation + 1,
|
|
826
|
+
...current.generation === 0 && event.transitionId && Array.isArray(event.context?.declaredFiles) && event.context.declaredFiles.length === 0 && event.context.settlementTransitionId === event.transitionId ? {
|
|
827
|
+
noMutationSettlement: {
|
|
828
|
+
generation: 0,
|
|
829
|
+
transitionId: event.transitionId,
|
|
830
|
+
declaredFiles: []
|
|
831
|
+
}
|
|
832
|
+
} : {}
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
case "stage_b_failed":
|
|
836
|
+
if (current.state !== "pre_check_passed" && current.state !== "reviewer_run" && current.state !== "tests_run" && current.state !== "rework_required") {
|
|
837
|
+
throw new Error(`TASK_WORKFLOW_STAGE_A_REQUIRED: cannot reject ${event.gate} from ${current.state}`);
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
...base,
|
|
841
|
+
state: "rework_required",
|
|
842
|
+
retryCount: Math.min(current.retryCount + 1, 3),
|
|
843
|
+
retryHistory: [...current.retryHistory, outcome].slice(-3),
|
|
844
|
+
retryEpoch: current.retryEpoch || current.generation + 1
|
|
845
|
+
};
|
|
846
|
+
case "accepted_mutation": {
|
|
847
|
+
const {
|
|
848
|
+
noMutationSettlement: _clearedSettlement,
|
|
849
|
+
externalEvidence: _clearedExternalEvidence,
|
|
850
|
+
...withoutSettlement
|
|
851
|
+
} = base;
|
|
852
|
+
if (event.context?.settlementFailed === true) {
|
|
853
|
+
return {
|
|
854
|
+
...withoutSettlement,
|
|
855
|
+
generation: current.generation + 1,
|
|
856
|
+
state: "rework_required",
|
|
857
|
+
retryCount: Math.min(current.retryCount + 1, 3),
|
|
858
|
+
retryHistory: [...current.retryHistory, outcome].slice(-3),
|
|
859
|
+
retryEpoch: current.retryEpoch || current.generation + 1,
|
|
860
|
+
supervisedRecovery: undefined,
|
|
861
|
+
settlementRecovery: undefined
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
return {
|
|
865
|
+
...withoutSettlement,
|
|
866
|
+
generation: current.generation + 1,
|
|
867
|
+
state: "coder_delegated",
|
|
868
|
+
supervisedRecovery: undefined,
|
|
869
|
+
settlementRecovery: undefined
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
case "stage_a_passed":
|
|
873
|
+
if (current.state !== "coder_delegated" && current.state !== "pre_check_passed" && !(current.state === "rework_required" && event.supervisedRecovery === true) && !(event.settlementRecovery === true && (current.state === "idle" || current.state === "blocked"))) {
|
|
874
|
+
throw new Error(`TASK_WORKFLOW_CODER_MUTATION_REQUIRED: cannot pass Stage A from ${current.state}`);
|
|
875
|
+
}
|
|
876
|
+
return {
|
|
877
|
+
...base,
|
|
878
|
+
...event.supervisedRecovery === true ? { supervisedRecovery: true } : {},
|
|
879
|
+
...event.settlementRecovery === true ? { settlementRecovery: true } : {},
|
|
880
|
+
state: "pre_check_passed"
|
|
881
|
+
};
|
|
882
|
+
case "stage_a_failed":
|
|
883
|
+
if (current.state !== "coder_delegated" && current.state !== "pre_check_passed" && current.state !== "rework_required") {
|
|
884
|
+
throw new Error(`TASK_WORKFLOW_CODER_MUTATION_REQUIRED: cannot fail Stage A from ${current.state}`);
|
|
885
|
+
}
|
|
886
|
+
return {
|
|
887
|
+
...base,
|
|
888
|
+
state: "rework_required",
|
|
889
|
+
retryCount: Math.min(current.retryCount + 1, 3),
|
|
890
|
+
retryHistory: [...current.retryHistory, outcome].slice(-3),
|
|
891
|
+
retryEpoch: current.retryEpoch || current.generation + 1
|
|
892
|
+
};
|
|
893
|
+
case "stage_b_completed":
|
|
894
|
+
if (current.state !== "pre_check_passed" && current.state !== "reviewer_run" && current.state !== "tests_run") {
|
|
895
|
+
throw new Error(`TASK_WORKFLOW_STAGE_A_REQUIRED: cannot record ${event.gate} from ${current.state}`);
|
|
896
|
+
}
|
|
897
|
+
if (event.routeComplete === false) {
|
|
898
|
+
return {
|
|
899
|
+
...base,
|
|
900
|
+
state: event.gate === "reviewer" || current.state === "reviewer_run" ? "reviewer_run" : current.state
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
if (event.routeComplete === true) {
|
|
904
|
+
return { ...base, state: "tests_run" };
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
...base,
|
|
908
|
+
state: hasAllRequiredGatesPassed(context.requiredGates, context.gates) ? "tests_run" : "reviewer_run"
|
|
909
|
+
};
|
|
910
|
+
case "gate_recorded":
|
|
911
|
+
return {
|
|
912
|
+
...base,
|
|
913
|
+
state: current.state
|
|
914
|
+
};
|
|
915
|
+
case "external_evidence_attested": {
|
|
916
|
+
if (current.state !== "idle") {
|
|
917
|
+
throw new Error(`TASK_WORKFLOW_EXTERNAL_ATTEST_STATE: external evidence can only be attested from idle (task never entered the coder QA lifecycle), not ${current.state}`);
|
|
918
|
+
}
|
|
919
|
+
if (current.externalEvidence?.transitionId === event.bundle.transitionId) {
|
|
920
|
+
return { ...base };
|
|
921
|
+
}
|
|
922
|
+
return { ...base, externalEvidence: event.bundle };
|
|
923
|
+
}
|
|
924
|
+
case "external_evidence_revoked": {
|
|
925
|
+
const {
|
|
926
|
+
externalEvidence: _revokedExternalEvidence,
|
|
927
|
+
...withoutExternalEvidence
|
|
928
|
+
} = base;
|
|
929
|
+
return withoutExternalEvidence;
|
|
930
|
+
}
|
|
931
|
+
case "task_completed": {
|
|
932
|
+
const readOnlyNoMutation = event.readOnlyNoMutation === true && current.state === "idle" && current.generation === 0 && isNoMutationSettlementMetadata(current) && context.requiredGates.every((gate) => context.gates[gate] != null);
|
|
933
|
+
const externalAdmitted = event.externalEvidence === true && current.externalEvidence !== undefined && current.externalEvidence.anchors.length > 0 && current.externalEvidence.anchors.every((anchor) => anchor.matched) && (current.state === "idle" || current.state === "complete");
|
|
934
|
+
if (event.qaExempt !== true && current.state !== "tests_run" && current.state !== "complete" && !(current.state === "pre_check_passed" && context.gates.council != null) && !readOnlyNoMutation && !externalAdmitted) {
|
|
935
|
+
throw new Error(`TASK_WORKFLOW_QA_REQUIRED: cannot complete from ${current.state}`);
|
|
936
|
+
}
|
|
937
|
+
return {
|
|
938
|
+
...base,
|
|
939
|
+
state: "complete",
|
|
940
|
+
...event.qaExempt === true ? { forcedCompletion: true } : {}
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
case "task_blocked":
|
|
944
|
+
return {
|
|
945
|
+
...base,
|
|
946
|
+
state: "blocked"
|
|
947
|
+
};
|
|
948
|
+
case "task_closed":
|
|
949
|
+
return {
|
|
950
|
+
...base,
|
|
951
|
+
state: "closed"
|
|
952
|
+
};
|
|
953
|
+
case "repair_idle": {
|
|
954
|
+
const {
|
|
955
|
+
forcedCompletion: _cleared,
|
|
956
|
+
supervisedRecovery: _clearedMarker,
|
|
957
|
+
settlementRecovery: _clearedSettlementMarker,
|
|
958
|
+
noMutationSettlement: _clearedSettlement,
|
|
959
|
+
externalEvidence: _clearedExternalEvidence,
|
|
960
|
+
...withoutForced
|
|
961
|
+
} = base;
|
|
962
|
+
return {
|
|
963
|
+
...withoutForced,
|
|
964
|
+
generation: current.generation + 1,
|
|
965
|
+
state: "idle",
|
|
966
|
+
retryCount: 0,
|
|
967
|
+
retryHistory: [],
|
|
968
|
+
retryEpoch: 0
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function getEvidenceDir(directory) {
|
|
974
|
+
const swarmDir = path2.resolve(directory, ".swarm");
|
|
975
|
+
const evidenceDir = path2.join(swarmDir, "evidence");
|
|
976
|
+
mkdirSync2(evidenceDir, { recursive: true });
|
|
977
|
+
let resolvedSwarmDir;
|
|
978
|
+
let resolvedEvidenceDir;
|
|
979
|
+
try {
|
|
980
|
+
resolvedSwarmDir = path2.normalize(realpathSync2(swarmDir));
|
|
981
|
+
resolvedEvidenceDir = path2.normalize(realpathSync2(evidenceDir));
|
|
982
|
+
} catch (error) {
|
|
983
|
+
throw new Error(`Unable to resolve evidence directory: ${error.message}`);
|
|
984
|
+
}
|
|
985
|
+
const swarmPrefix = `${resolvedSwarmDir}${path2.sep}`;
|
|
986
|
+
const withinSwarmBoundary = process.platform === "win32" ? resolvedEvidenceDir.toLowerCase().startsWith(swarmPrefix.toLowerCase()) : resolvedEvidenceDir.startsWith(swarmPrefix);
|
|
987
|
+
if (!withinSwarmBoundary) {
|
|
988
|
+
throw new Error(`Evidence path escapes .swarm boundary: ${resolvedEvidenceDir}`);
|
|
989
|
+
}
|
|
990
|
+
return resolvedEvidenceDir;
|
|
991
|
+
}
|
|
992
|
+
function getEvidencePath(directory, taskId) {
|
|
993
|
+
assertValidTaskId(taskId);
|
|
994
|
+
return taskEvidencePath(directory, taskId);
|
|
995
|
+
}
|
|
996
|
+
function bindTrustedNoMutationSettlement(directory, taskId, event) {
|
|
997
|
+
if (event.type !== "dispatch_no_mutation")
|
|
998
|
+
return event;
|
|
999
|
+
const context = event.context;
|
|
1000
|
+
if (!context || event.transitionId === undefined) {
|
|
1001
|
+
return { ...event, context: undefined };
|
|
1002
|
+
}
|
|
1003
|
+
try {
|
|
1004
|
+
const wal = readWorkflowWalFileSync("coder-settlement", validateSwarmPath(directory, `coder-settlements/${taskId}.json`), taskId);
|
|
1005
|
+
const declaredFiles = wal?.context?.declaredFiles;
|
|
1006
|
+
const trusted = wal !== null && (wal.state === "PREPARED" || wal.state === "COMMITTED") && wal.accepted === false && wal.transitionId === event.transitionId && Array.isArray(declaredFiles) && declaredFiles.length === 0 && Array.isArray(context.declaredFiles) && context.declaredFiles.length === 0;
|
|
1007
|
+
return {
|
|
1008
|
+
...event,
|
|
1009
|
+
context: trusted ? {
|
|
1010
|
+
...context,
|
|
1011
|
+
declaredFiles,
|
|
1012
|
+
settlementTransitionId: wal.transitionId
|
|
1013
|
+
} : { ...context, settlementTransitionId: undefined }
|
|
1014
|
+
};
|
|
1015
|
+
} catch {
|
|
1016
|
+
return {
|
|
1017
|
+
...event,
|
|
1018
|
+
context: { ...context, settlementTransitionId: undefined }
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function readExisting(evidencePath, taskId) {
|
|
1023
|
+
try {
|
|
1024
|
+
const raw = readFileSync2(evidencePath, "utf-8");
|
|
1025
|
+
return parseTaskEvidence(raw, taskId);
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
if (error.code === "ENOENT")
|
|
1028
|
+
return null;
|
|
1029
|
+
telemetry.gateParseError(taskId, error);
|
|
1030
|
+
throw error;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function deriveRequiredGates(agentType, context = {}) {
|
|
1034
|
+
switch (agentType) {
|
|
1035
|
+
case "coder":
|
|
1036
|
+
return context.testEngineerExempt === true ? ["reviewer"] : ["reviewer", "test_engineer"];
|
|
1037
|
+
case "docs":
|
|
1038
|
+
return ["docs"];
|
|
1039
|
+
case "designer":
|
|
1040
|
+
return ["designer", "reviewer", "test_engineer"];
|
|
1041
|
+
case "explorer":
|
|
1042
|
+
return ["explorer"];
|
|
1043
|
+
case "sme":
|
|
1044
|
+
return ["sme"];
|
|
1045
|
+
case "reviewer":
|
|
1046
|
+
return ["reviewer"];
|
|
1047
|
+
case "test_engineer":
|
|
1048
|
+
return ["test_engineer"];
|
|
1049
|
+
case "critic":
|
|
1050
|
+
return ["critic"];
|
|
1051
|
+
case "critic_sounding_board":
|
|
1052
|
+
return ["critic_sounding_board"];
|
|
1053
|
+
case "critic_drift_verifier":
|
|
1054
|
+
return ["critic_drift_verifier"];
|
|
1055
|
+
case "critic_hallucination_verifier":
|
|
1056
|
+
return ["critic_hallucination_verifier"];
|
|
1057
|
+
case "critic_architecture_supervisor":
|
|
1058
|
+
return ["critic_architecture_supervisor"];
|
|
1059
|
+
default:
|
|
1060
|
+
return ["reviewer", "test_engineer"];
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function expandRequiredGates(existingGates, newAgentType, context = {}) {
|
|
1064
|
+
const newGates = deriveRequiredGates(newAgentType, context);
|
|
1065
|
+
const combined = [...new Set([...existingGates ?? [], ...newGates])];
|
|
1066
|
+
return combined.sort();
|
|
1067
|
+
}
|
|
1068
|
+
function updateEvidenceForTransition(existing, event) {
|
|
1069
|
+
const nowIso = new Date().toISOString();
|
|
1070
|
+
const requiredGates = maybeExpandRequiredGates(existing, event);
|
|
1071
|
+
let gates = {
|
|
1072
|
+
...existing?.gates ?? {}
|
|
1073
|
+
};
|
|
1074
|
+
if (event.type === "accepted_mutation" || event.type === "repair_idle") {
|
|
1075
|
+
gates = clearWorkflowGateProof(gates);
|
|
1076
|
+
}
|
|
1077
|
+
if (event.type === "stage_a_passed") {
|
|
1078
|
+
gates = {
|
|
1079
|
+
...gates,
|
|
1080
|
+
pre_check: {
|
|
1081
|
+
sessionId: "system",
|
|
1082
|
+
timestamp: nowIso,
|
|
1083
|
+
agent: "pre_check"
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
if (event.type === "stage_a_failed") {
|
|
1088
|
+
delete gates.pre_check;
|
|
1089
|
+
}
|
|
1090
|
+
if (event.type === "stage_b_failed") {
|
|
1091
|
+
delete gates.reviewer;
|
|
1092
|
+
delete gates.test_engineer;
|
|
1093
|
+
}
|
|
1094
|
+
if (event.type === "stage_b_completed" || event.type === "gate_recorded") {
|
|
1095
|
+
gates = {
|
|
1096
|
+
...gates,
|
|
1097
|
+
[event.gate]: {
|
|
1098
|
+
sessionId: event.sessionId,
|
|
1099
|
+
timestamp: nowIso,
|
|
1100
|
+
agent: event.gate
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
const snapshot = getTaskWorkflowSnapshot(existing);
|
|
1105
|
+
assertExpectedGeneration(snapshot, event);
|
|
1106
|
+
if (isDuplicateTransition(snapshot, event) && existing) {
|
|
1107
|
+
return existing;
|
|
1108
|
+
}
|
|
1109
|
+
const workflow = reduceTaskWorkflowSnapshot(snapshot, event, {
|
|
1110
|
+
requiredGates,
|
|
1111
|
+
gates,
|
|
1112
|
+
nowIso
|
|
1113
|
+
});
|
|
1114
|
+
return {
|
|
1115
|
+
taskId: existing?.taskId ?? "",
|
|
1116
|
+
required_gates: requiredGates,
|
|
1117
|
+
gates,
|
|
1118
|
+
turbo: "turbo" in event && event.turbo === true ? true : ("turbo" in event) && event.turbo === false ? existing?.turbo : existing?.turbo,
|
|
1119
|
+
test_engineer_exempt: (event.type === "dispatch_attempted" || event.type === "dispatch_no_mutation" || event.type === "accepted_mutation") && event.agentType === "coder" ? event.context?.testEngineerExempt === true && !requiredGates.includes("test_engineer") : existing?.test_engineer_exempt,
|
|
1120
|
+
workflow,
|
|
1121
|
+
requirements_state: existing?.requirements_state,
|
|
1122
|
+
...existing?.repair_provenance ? { repair_provenance: existing.repair_provenance } : {},
|
|
1123
|
+
...existing?.todo_scan ? { todo_scan: existing.todo_scan } : {}
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
async function transitionTaskWorkflowEvidence(directory, taskId, event) {
|
|
1127
|
+
const { evidence } = await transitionTaskWorkflowEvidenceWithStatus(directory, taskId, event);
|
|
1128
|
+
return evidence;
|
|
1129
|
+
}
|
|
1130
|
+
async function transitionTaskWorkflowEvidenceWithStatus(directory, taskId, event) {
|
|
1131
|
+
assertValidTaskId(taskId);
|
|
1132
|
+
let updatedEvidence = null;
|
|
1133
|
+
let duplicated = false;
|
|
1134
|
+
await withTaskEvidenceTransaction(directory, taskId, event.type, async (transaction) => {
|
|
1135
|
+
if (transaction.transitionWithStatus) {
|
|
1136
|
+
const result = await transaction.transitionWithStatus(event);
|
|
1137
|
+
updatedEvidence = result.evidence;
|
|
1138
|
+
duplicated = result.duplicated;
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
updatedEvidence = await transaction.transition(event);
|
|
1142
|
+
});
|
|
1143
|
+
return {
|
|
1144
|
+
evidence: updatedEvidence ?? {
|
|
1145
|
+
taskId,
|
|
1146
|
+
required_gates: [],
|
|
1147
|
+
gates: {},
|
|
1148
|
+
workflow: createDefaultWorkflowMetadata(new Date().toISOString())
|
|
1149
|
+
},
|
|
1150
|
+
duplicated
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
async function withTaskEvidenceTransaction(directory, taskId, agent, callback) {
|
|
1154
|
+
assertValidTaskId(taskId);
|
|
1155
|
+
return withTaskEvidenceLock(directory, taskId, agent, async () => {
|
|
1156
|
+
const resolvedEvidenceDir = getEvidenceDir(directory);
|
|
1157
|
+
const evidencePath = path2.join(resolvedEvidenceDir, `${taskId}.json`);
|
|
1158
|
+
let current = null;
|
|
1159
|
+
try {
|
|
1160
|
+
current = readExisting(evidencePath, taskId);
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
telemetry.gateParseError(taskId, error);
|
|
1163
|
+
throw error;
|
|
1164
|
+
}
|
|
1165
|
+
const persist = async (nextEvidence, event) => {
|
|
1166
|
+
const planIdentity = resolveCurrentPlanIdentitySync(directory);
|
|
1167
|
+
const validated = TaskEvidenceSchema.parse({
|
|
1168
|
+
...nextEvidence,
|
|
1169
|
+
taskId,
|
|
1170
|
+
...planStampForPersist(nextEvidence, current, planIdentity, event?.type === "accepted_mutation" && event.agentType === "coder" && nextEvidence !== current)
|
|
1171
|
+
});
|
|
1172
|
+
await atomicWriteFile(evidencePath, JSON.stringify(validated, null, 2));
|
|
1173
|
+
if (event) {
|
|
1174
|
+
await _internals.appendTaskGateRequirementsReceiptIfNeeded(directory, taskId, current, validated, event);
|
|
1175
|
+
}
|
|
1176
|
+
current = validated;
|
|
1177
|
+
return validated;
|
|
1178
|
+
};
|
|
1179
|
+
const transitionWithStatusImpl = async (event) => {
|
|
1180
|
+
const boundEvent = bindTrustedNoMutationSettlement(directory, taskId, event);
|
|
1181
|
+
assertTaskEvidenceWriteAllowed(directory, taskId, boundEvent);
|
|
1182
|
+
const nextEvidence = updateEvidenceForTransition(current, boundEvent);
|
|
1183
|
+
const duplicated = nextEvidence === current;
|
|
1184
|
+
nextEvidence.taskId = taskId;
|
|
1185
|
+
const evidence = await persist(nextEvidence, boundEvent);
|
|
1186
|
+
return { evidence, duplicated };
|
|
1187
|
+
};
|
|
1188
|
+
const saveImpl = async (nextEvidence) => {
|
|
1189
|
+
assertTaskEvidenceWriteAllowed(directory, taskId, undefined);
|
|
1190
|
+
nextEvidence.taskId = taskId;
|
|
1191
|
+
return persist(nextEvidence);
|
|
1192
|
+
};
|
|
1193
|
+
return callback({
|
|
1194
|
+
taskId,
|
|
1195
|
+
read: () => current,
|
|
1196
|
+
transition: async (event) => {
|
|
1197
|
+
const { evidence } = await transitionWithStatusImpl(event);
|
|
1198
|
+
return evidence;
|
|
1199
|
+
},
|
|
1200
|
+
transitionWithStatus: transitionWithStatusImpl,
|
|
1201
|
+
save: saveImpl
|
|
1202
|
+
});
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
async function ensureTaskGateRequirementsReceiptForSettledTransition(directory, taskId, evidence, event) {
|
|
1206
|
+
assertValidTaskId(taskId);
|
|
1207
|
+
const validated = TaskEvidenceSchema.parse({ ...evidence, taskId });
|
|
1208
|
+
await _internals.appendTaskGateRequirementsReceiptIfNeeded(directory, taskId, null, validated, event);
|
|
1209
|
+
}
|
|
1210
|
+
async function recordGateEvidence(directory, taskId, gate, sessionId, turbo, options = {}) {
|
|
1211
|
+
assertValidTaskId(taskId);
|
|
1212
|
+
if (shouldTreatGateAsStageB(gate) && typeof options.expectedGeneration !== "number") {
|
|
1213
|
+
throw new Error(`TASK_WORKFLOW_GENERATION_REQUIRED: ${gate} for task ${taskId} must be bound at dispatch`);
|
|
1214
|
+
}
|
|
1215
|
+
await transitionTaskWorkflowEvidence(directory, taskId, shouldTreatGateAsStageB(gate) ? {
|
|
1216
|
+
type: "stage_b_completed",
|
|
1217
|
+
gate,
|
|
1218
|
+
sessionId,
|
|
1219
|
+
routeBinding: options.routeBinding,
|
|
1220
|
+
routeComplete: options.routeComplete,
|
|
1221
|
+
turbo,
|
|
1222
|
+
ensureDefaultStageB: options.ensureDefaultStageB,
|
|
1223
|
+
expectedGeneration: options.expectedGeneration,
|
|
1224
|
+
transitionId: options.transitionId
|
|
1225
|
+
} : {
|
|
1226
|
+
type: "gate_recorded",
|
|
1227
|
+
gate,
|
|
1228
|
+
sessionId,
|
|
1229
|
+
turbo,
|
|
1230
|
+
expectedGeneration: options.expectedGeneration,
|
|
1231
|
+
transitionId: options.transitionId
|
|
1232
|
+
});
|
|
1233
|
+
telemetry.gatePassed(sessionId, gate, taskId);
|
|
1234
|
+
}
|
|
1235
|
+
async function recordTodoScanEvidence(directory, taskId, scan) {
|
|
1236
|
+
assertValidTaskId(taskId);
|
|
1237
|
+
await withTaskEvidenceTransaction(directory, taskId, "todo_scan_recorded", async (transaction) => {
|
|
1238
|
+
const existing = transaction.read();
|
|
1239
|
+
const base = existing ?? {
|
|
1240
|
+
taskId,
|
|
1241
|
+
required_gates: [],
|
|
1242
|
+
gates: {},
|
|
1243
|
+
workflow: createDefaultWorkflowMetadata(new Date().toISOString())
|
|
1244
|
+
};
|
|
1245
|
+
if (!transaction.save) {
|
|
1246
|
+
throw new Error(`TASK_EVIDENCE_SAVE_UNAVAILABLE: transaction for task ${taskId} does not support supplementary writes`);
|
|
1247
|
+
}
|
|
1248
|
+
await transaction.save({ ...base, todo_scan: scan });
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
async function recordAgentDispatch(directory, taskId, agentType, turbo, context = {}) {
|
|
1252
|
+
assertValidTaskId(taskId);
|
|
1253
|
+
const expectedGeneration = getTaskWorkflowSnapshot(await readTaskEvidence(directory, taskId)).generation;
|
|
1254
|
+
await transitionTaskWorkflowEvidence(directory, taskId, agentType === "coder" ? {
|
|
1255
|
+
type: "accepted_mutation",
|
|
1256
|
+
agentType,
|
|
1257
|
+
turbo,
|
|
1258
|
+
context,
|
|
1259
|
+
expectedGeneration
|
|
1260
|
+
} : {
|
|
1261
|
+
type: "dispatch_attempted",
|
|
1262
|
+
agentType,
|
|
1263
|
+
turbo,
|
|
1264
|
+
context,
|
|
1265
|
+
expectedGeneration
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
async function readTaskEvidence(directory, taskId) {
|
|
1269
|
+
const state = await readTaskEvidenceState(directory, taskId);
|
|
1270
|
+
return state.kind === "ok" ? state.evidence : null;
|
|
1271
|
+
}
|
|
1272
|
+
async function readTaskEvidenceState(directory, taskId) {
|
|
1273
|
+
try {
|
|
1274
|
+
assertValidTaskId(taskId);
|
|
1275
|
+
} catch {
|
|
1276
|
+
return { kind: "missing" };
|
|
1277
|
+
}
|
|
1278
|
+
try {
|
|
1279
|
+
const evidencePath = getEvidencePath(directory, taskId);
|
|
1280
|
+
const raw = readFileSync2(evidencePath, "utf-8");
|
|
1281
|
+
const evidence = parseTaskEvidence(raw, taskId);
|
|
1282
|
+
return { kind: "ok", evidence };
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
const evidencePath = getEvidencePathSafe(directory, taskId);
|
|
1285
|
+
const code = error.code;
|
|
1286
|
+
if (code === "ENOENT" || code === "ENAMETOOLONG") {
|
|
1287
|
+
return { kind: "missing" };
|
|
1288
|
+
}
|
|
1289
|
+
telemetry.gateParseError(taskId, error);
|
|
1290
|
+
return { kind: "unparseable", evidencePath, error };
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
function getEvidencePathSafe(directory, taskId) {
|
|
1294
|
+
try {
|
|
1295
|
+
return getEvidencePath(directory, taskId);
|
|
1296
|
+
} catch {
|
|
1297
|
+
return `${directory}/.swarm/evidence/${taskId}.json`;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function readTaskEvidenceRaw(directory, taskId) {
|
|
1301
|
+
assertValidTaskId(taskId);
|
|
1302
|
+
const evidencePath = getEvidencePath(directory, taskId);
|
|
1303
|
+
try {
|
|
1304
|
+
const raw = readFileSync2(evidencePath, "utf-8");
|
|
1305
|
+
return parseTaskEvidence(raw, taskId);
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
const code = error.code;
|
|
1308
|
+
if (code === "ENOENT" || code === "ENAMETOOLONG")
|
|
1309
|
+
return null;
|
|
1310
|
+
throw error;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
async function hasPassedAllGates(directory, taskId, currentDeclaredFiles) {
|
|
1314
|
+
const evidence = await readTaskEvidence(directory, taskId);
|
|
1315
|
+
if (!evidence)
|
|
1316
|
+
return false;
|
|
1317
|
+
if (!Array.isArray(evidence.required_gates))
|
|
1318
|
+
return false;
|
|
1319
|
+
if (currentDeclaredFiles === undefined) {
|
|
1320
|
+
if (deriveApplicableGateSet(evidence).readOnlyNoMutation)
|
|
1321
|
+
return false;
|
|
1322
|
+
return hasAllRequiredGatesPassed(evidence.required_gates, evidence.gates);
|
|
1323
|
+
}
|
|
1324
|
+
return deriveApplicableGateSet(evidence, { currentDeclaredFiles }).missingGates.length === 0;
|
|
1325
|
+
}
|
|
1326
|
+
function compareTaskWorkflowStateRank(left, right) {
|
|
1327
|
+
return WORKFLOW_STATE_RANK[left] - WORKFLOW_STATE_RANK[right];
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
export { readTaskGateRequirementsReceiptsSync, routeEvidenceFromTaskGateRequirements, isLegacyTaskGateRequirementsChain, isStrictTaskId, sanitizeTaskId, validateTaskIdFormat, _internals, TASK_WORKFLOW_SCHEMA_MARKER, readCurrentTaskDeclaredFiles, TASK_GATE_REQUIREMENTS_RECONSTRUCTION_SENTINEL, assertTaskEvidenceWriteAllowed, ExternalEvidenceBundleSchema, parseTaskEvidence, resolveCurrentPlanIdentitySync, evidenceStampedForDifferentPlan, DEFAULT_REQUIRED_GATES, isValidTaskId, getTaskWorkflowSnapshot, deriveApplicableGateSet, isReadOnlyNoMutationEligible, computeExternalDeclarationDigest, externalProofCurrent, isExternalEvidenceInitialCompletionEligible, reduceTaskWorkflowSnapshot, deriveRequiredGates, expandRequiredGates, transitionTaskWorkflowEvidence, transitionTaskWorkflowEvidenceWithStatus, withTaskEvidenceTransaction, ensureTaskGateRequirementsReceiptForSettledTransition, recordGateEvidence, recordTodoScanEvidence, recordAgentDispatch, readTaskEvidence, readTaskEvidenceState, readTaskEvidenceRaw, hasPassedAllGates, compareTaskWorkflowStateRank };
|