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
|
@@ -1,35 +1,22 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
|
+
assertSettlementArtifactsRemovable,
|
|
3
4
|
autoCommitDirty,
|
|
4
|
-
cleanUntrackedFiles
|
|
5
|
-
|
|
5
|
+
cleanUntrackedFiles,
|
|
6
|
+
withSettlementArtifactLifecycle
|
|
7
|
+
} from "./index-vm3j8fvn.js";
|
|
6
8
|
import {
|
|
7
9
|
isSpawnCwdMissing,
|
|
8
10
|
isSpawnCwdUnreadable
|
|
9
11
|
} from "./index-spggmr30.js";
|
|
10
|
-
import {
|
|
11
|
-
validateSwarmPath
|
|
12
|
-
} from "./index-4krkevgc.js";
|
|
13
|
-
import {
|
|
14
|
-
derivePlanId,
|
|
15
|
-
derivePlanIdentityHash
|
|
16
|
-
} from "./index-7pmbnjvj.js";
|
|
17
12
|
import {
|
|
18
13
|
init_path_identity,
|
|
19
14
|
isPathWithinDeclaredScope,
|
|
20
15
|
unsafePathTextReason
|
|
21
16
|
} from "./index-9rwhas09.js";
|
|
22
17
|
import {
|
|
23
|
-
atomicWriteFile
|
|
24
|
-
|
|
25
|
-
withTaskEvidenceLock
|
|
26
|
-
} from "./index-yyrnkc3a.js";
|
|
27
|
-
import {
|
|
28
|
-
assertProjectRoot
|
|
29
|
-
} from "./index-t7hmqgjy.js";
|
|
30
|
-
import {
|
|
31
|
-
telemetry
|
|
32
|
-
} from "./index-c2k7q723.js";
|
|
18
|
+
atomicWriteFile
|
|
19
|
+
} from "./index-6m551cng.js";
|
|
33
20
|
import {
|
|
34
21
|
advisoryWarn,
|
|
35
22
|
init_git_executable,
|
|
@@ -43,368 +30,9 @@ import {
|
|
|
43
30
|
import {
|
|
44
31
|
log
|
|
45
32
|
} from "./index-zgwm4ryv.js";
|
|
46
|
-
import {
|
|
47
|
-
exports_external,
|
|
48
|
-
init_zod
|
|
49
|
-
} from "./index-gknz3bv1.js";
|
|
50
|
-
|
|
51
|
-
// src/gate-evidence.ts
|
|
52
|
-
init_zod();
|
|
53
|
-
import { createHash as createHash2 } from "crypto";
|
|
54
|
-
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, realpathSync as realpathSync2, statSync as statSync2 } from "fs";
|
|
55
|
-
import * as path4 from "path";
|
|
56
|
-
|
|
57
|
-
// src/evidence/task-gate-requirements.ts
|
|
58
|
-
init_zod();
|
|
59
|
-
import { createHash } from "crypto";
|
|
60
|
-
import * as fs from "fs";
|
|
61
|
-
import * as path from "path";
|
|
62
|
-
var MAX_TASK_GATE_REQUIREMENTS_BYTES = 256 * 1024;
|
|
63
|
-
var TaskGateRequirementsReceiptSchema = exports_external.object({
|
|
64
|
-
schemaVersion: exports_external.literal(1),
|
|
65
|
-
taskId: exports_external.string().min(1).max(120),
|
|
66
|
-
generation: exports_external.number().int().min(0),
|
|
67
|
-
normalizedRole: exports_external.string().min(1).max(120),
|
|
68
|
-
testEngineerExempt: exports_external.boolean().nullable(),
|
|
69
|
-
requiredGates: exports_external.array(exports_external.string()).min(1).max(16),
|
|
70
|
-
sourceEvent: exports_external.string().min(1).max(120),
|
|
71
|
-
sourceTransitionId: exports_external.string().min(1).max(200).nullable(),
|
|
72
|
-
recordedAt: exports_external.string().min(1).max(128),
|
|
73
|
-
routeBinding: exports_external.object({
|
|
74
|
-
role: exports_external.enum(["reviewer", "test_engineer"]),
|
|
75
|
-
identity: exports_external.string().trim().min(1).max(256),
|
|
76
|
-
sessionId: exports_external.string().trim().min(1).max(256),
|
|
77
|
-
taskId: exports_external.string().trim().min(1).max(120),
|
|
78
|
-
slotId: exports_external.string().trim().min(1).max(256),
|
|
79
|
-
callId: exports_external.string().trim().min(1).max(256),
|
|
80
|
-
childSessionId: exports_external.string().trim().min(1).max(256),
|
|
81
|
-
generation: exports_external.number().int().nonnegative()
|
|
82
|
-
}).strict().optional(),
|
|
83
|
-
previousChainHash: exports_external.string().regex(/^[a-f0-9]{64}$/).nullable(),
|
|
84
|
-
chainHash: exports_external.string().regex(/^[a-f0-9]{64}$/)
|
|
85
|
-
}).strict();
|
|
86
|
-
function samePath(left, right) {
|
|
87
|
-
const normalize = (value) => process.platform === "win32" ? path.resolve(value).toLowerCase() : path.resolve(value);
|
|
88
|
-
return normalize(left) === normalize(right);
|
|
89
|
-
}
|
|
90
|
-
function assertSafeRequirementsParent(directory, create) {
|
|
91
|
-
assertProjectRoot(directory, undefined, "task gate requirements");
|
|
92
|
-
const rootStat = fs.lstatSync(directory);
|
|
93
|
-
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
94
|
-
throw new Error("TASK_GATE_REQUIREMENTS_UNREADABLE: project root is not a regular directory");
|
|
95
|
-
}
|
|
96
|
-
const canonicalRoot = fs.realpathSync(directory);
|
|
97
|
-
const segments = [".swarm", "evidence", "task-gate-requirements"];
|
|
98
|
-
let current = canonicalRoot;
|
|
99
|
-
for (const segment of segments) {
|
|
100
|
-
current = path.join(current, segment);
|
|
101
|
-
let info;
|
|
102
|
-
try {
|
|
103
|
-
info = fs.lstatSync(current);
|
|
104
|
-
} catch (error) {
|
|
105
|
-
if (error.code === "ENOENT") {
|
|
106
|
-
if (!create)
|
|
107
|
-
return current;
|
|
108
|
-
try {
|
|
109
|
-
fs.mkdirSync(current);
|
|
110
|
-
} catch (mkdirError) {
|
|
111
|
-
if (mkdirError.code !== "EEXIST") {
|
|
112
|
-
throw mkdirError;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
info = fs.lstatSync(current);
|
|
116
|
-
} else {
|
|
117
|
-
throw error;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
121
|
-
throw new Error("TASK_GATE_REQUIREMENTS_UNREADABLE: receipt parent is redirected or not a directory");
|
|
122
|
-
}
|
|
123
|
-
const canonicalCurrent = fs.realpathSync(current);
|
|
124
|
-
if (!samePath(canonicalCurrent, current)) {
|
|
125
|
-
throw new Error("TASK_GATE_REQUIREMENTS_UNREADABLE: receipt parent escaped the project root");
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return current;
|
|
129
|
-
}
|
|
130
|
-
function safeTaskGateRequirementsReceiptPath(directory, taskId, createParent) {
|
|
131
|
-
const parent = assertSafeRequirementsParent(directory, createParent);
|
|
132
|
-
return path.join(parent, `${taskId}.jsonl`);
|
|
133
|
-
}
|
|
134
|
-
function sha256(value) {
|
|
135
|
-
return createHash("sha256").update(value).digest("hex");
|
|
136
|
-
}
|
|
137
|
-
function canonicalReceiptPayload(receipt) {
|
|
138
|
-
const routeBinding = receipt.routeBinding ? {
|
|
139
|
-
role: receipt.routeBinding.role,
|
|
140
|
-
identity: receipt.routeBinding.identity,
|
|
141
|
-
sessionId: receipt.routeBinding.sessionId,
|
|
142
|
-
taskId: receipt.routeBinding.taskId,
|
|
143
|
-
slotId: receipt.routeBinding.slotId,
|
|
144
|
-
callId: receipt.routeBinding.callId,
|
|
145
|
-
childSessionId: receipt.routeBinding.childSessionId,
|
|
146
|
-
generation: receipt.routeBinding.generation
|
|
147
|
-
} : undefined;
|
|
148
|
-
return JSON.stringify({
|
|
149
|
-
schemaVersion: receipt.schemaVersion,
|
|
150
|
-
taskId: receipt.taskId,
|
|
151
|
-
generation: receipt.generation,
|
|
152
|
-
normalizedRole: receipt.normalizedRole,
|
|
153
|
-
testEngineerExempt: receipt.testEngineerExempt,
|
|
154
|
-
requiredGates: [...receipt.requiredGates],
|
|
155
|
-
sourceEvent: receipt.sourceEvent,
|
|
156
|
-
sourceTransitionId: receipt.sourceTransitionId,
|
|
157
|
-
recordedAt: receipt.recordedAt,
|
|
158
|
-
...routeBinding ? { routeBinding } : {},
|
|
159
|
-
previousChainHash: receipt.previousChainHash
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
function computeChainHash(receipt) {
|
|
163
|
-
return sha256(canonicalReceiptPayload(receipt));
|
|
164
|
-
}
|
|
165
|
-
function routeBindingFromEvent(event) {
|
|
166
|
-
if (event.type !== "stage_b_completed" || !event.routeBinding)
|
|
167
|
-
return;
|
|
168
|
-
const binding = event.routeBinding;
|
|
169
|
-
if (binding.role !== event.gate || !binding.sessionId || !binding.taskId || !binding.slotId || !binding.callId || !binding.childSessionId || typeof binding.generation !== "number") {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
return binding;
|
|
173
|
-
}
|
|
174
|
-
function readBoundedTextFile(filePath) {
|
|
175
|
-
let pathStat;
|
|
176
|
-
try {
|
|
177
|
-
pathStat = fs.lstatSync(filePath);
|
|
178
|
-
} catch (error) {
|
|
179
|
-
if (error.code === "ENOENT")
|
|
180
|
-
return null;
|
|
181
|
-
throw error;
|
|
182
|
-
}
|
|
183
|
-
if (pathStat.isSymbolicLink() || !pathStat.isFile()) {
|
|
184
|
-
throw new Error("TASK_GATE_REQUIREMENTS_UNREADABLE: receipt path is not a regular file");
|
|
185
|
-
}
|
|
186
|
-
if (pathStat.size > MAX_TASK_GATE_REQUIREMENTS_BYTES) {
|
|
187
|
-
throw new Error(`TASK_GATE_REQUIREMENTS_OVERSIZED: ${pathStat.size} bytes exceeds ${MAX_TASK_GATE_REQUIREMENTS_BYTES}`);
|
|
188
|
-
}
|
|
189
|
-
const descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
190
|
-
try {
|
|
191
|
-
const openedStat = fs.fstatSync(descriptor);
|
|
192
|
-
if (!openedStat.isFile() || openedStat.dev !== pathStat.dev || openedStat.ino !== pathStat.ino) {
|
|
193
|
-
throw new Error("TASK_GATE_REQUIREMENTS_UNREADABLE: receipt changed while being opened");
|
|
194
|
-
}
|
|
195
|
-
if (openedStat.size > MAX_TASK_GATE_REQUIREMENTS_BYTES) {
|
|
196
|
-
throw new Error(`TASK_GATE_REQUIREMENTS_OVERSIZED: ${openedStat.size} bytes exceeds ${MAX_TASK_GATE_REQUIREMENTS_BYTES}`);
|
|
197
|
-
}
|
|
198
|
-
return fs.readFileSync(descriptor, "utf-8");
|
|
199
|
-
} finally {
|
|
200
|
-
fs.closeSync(descriptor);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
async function readTaskGateRequirementsReceipts(directory, taskId) {
|
|
204
|
-
const filePath = safeTaskGateRequirementsReceiptPath(directory, taskId, false);
|
|
205
|
-
const text = readBoundedTextFile(filePath);
|
|
206
|
-
if (text === null)
|
|
207
|
-
return [];
|
|
208
|
-
const receipts = [];
|
|
209
|
-
for (const line of text.split(`
|
|
210
|
-
`)) {
|
|
211
|
-
if (!line.trim())
|
|
212
|
-
continue;
|
|
213
|
-
const parsed = TaskGateRequirementsReceiptSchema.parse(JSON.parse(line));
|
|
214
|
-
if (parsed.taskId !== taskId || parsed.routeBinding !== undefined && parsed.routeBinding.taskId !== taskId) {
|
|
215
|
-
throw new Error(`TASK_GATE_REQUIREMENTS_IDENTITY_MISMATCH: expected ${taskId}, found ${parsed.taskId}`);
|
|
216
|
-
}
|
|
217
|
-
const { chainHash, ...withoutChainHash } = parsed;
|
|
218
|
-
const expectedHash = computeChainHash(withoutChainHash);
|
|
219
|
-
if (expectedHash !== chainHash) {
|
|
220
|
-
throw new Error(`TASK_GATE_REQUIREMENTS_CHAIN_MISMATCH: receipt for ${taskId} failed hash validation`);
|
|
221
|
-
}
|
|
222
|
-
const previousChainHash = receipts.at(-1)?.chainHash ?? null;
|
|
223
|
-
if (parsed.previousChainHash !== previousChainHash) {
|
|
224
|
-
throw new Error(`TASK_GATE_REQUIREMENTS_CHAIN_MISMATCH: receipt for ${taskId} failed linkage validation`);
|
|
225
|
-
}
|
|
226
|
-
receipts.push(parsed);
|
|
227
|
-
}
|
|
228
|
-
return receipts;
|
|
229
|
-
}
|
|
230
|
-
function readTaskGateRequirementsReceiptsSync(directory, taskId) {
|
|
231
|
-
const filePath = safeTaskGateRequirementsReceiptPath(directory, taskId, false);
|
|
232
|
-
const text = readBoundedTextFile(filePath);
|
|
233
|
-
if (text === null)
|
|
234
|
-
return [];
|
|
235
|
-
const receipts = [];
|
|
236
|
-
for (const line of text.split(`
|
|
237
|
-
`)) {
|
|
238
|
-
if (!line.trim())
|
|
239
|
-
continue;
|
|
240
|
-
const parsed = TaskGateRequirementsReceiptSchema.parse(JSON.parse(line));
|
|
241
|
-
if (parsed.taskId !== taskId || parsed.routeBinding !== undefined && parsed.routeBinding.taskId !== taskId) {
|
|
242
|
-
throw new Error(`TASK_GATE_REQUIREMENTS_IDENTITY_MISMATCH: expected ${taskId}, found ${parsed.taskId}`);
|
|
243
|
-
}
|
|
244
|
-
const { chainHash, ...withoutChainHash } = parsed;
|
|
245
|
-
if (computeChainHash(withoutChainHash) !== chainHash) {
|
|
246
|
-
throw new Error(`TASK_GATE_REQUIREMENTS_CHAIN_MISMATCH: receipt for ${taskId} failed hash validation`);
|
|
247
|
-
}
|
|
248
|
-
const previousChainHash = receipts.at(-1)?.chainHash ?? null;
|
|
249
|
-
if (parsed.previousChainHash !== previousChainHash) {
|
|
250
|
-
throw new Error(`TASK_GATE_REQUIREMENTS_CHAIN_MISMATCH: receipt for ${taskId} failed linkage validation`);
|
|
251
|
-
}
|
|
252
|
-
receipts.push(parsed);
|
|
253
|
-
}
|
|
254
|
-
return receipts;
|
|
255
|
-
}
|
|
256
|
-
function routeEvidenceFromTaskGateRequirements(receipts) {
|
|
257
|
-
const latestByDispatch = new Map;
|
|
258
|
-
for (const receipt of receipts) {
|
|
259
|
-
const binding = receipt.routeBinding;
|
|
260
|
-
if (!binding)
|
|
261
|
-
continue;
|
|
262
|
-
const dispatchKey = [
|
|
263
|
-
binding.role,
|
|
264
|
-
binding.sessionId,
|
|
265
|
-
binding.taskId,
|
|
266
|
-
binding.callId,
|
|
267
|
-
binding.childSessionId,
|
|
268
|
-
String(binding.generation)
|
|
269
|
-
].join("\x00");
|
|
270
|
-
latestByDispatch.set(dispatchKey, binding);
|
|
271
|
-
}
|
|
272
|
-
return [...latestByDispatch.values()];
|
|
273
|
-
}
|
|
274
|
-
function isLegacyTaskGateRequirementsChain(receipts) {
|
|
275
|
-
return receipts.length > 0 && receipts.every((receipt) => !receipt.routeBinding);
|
|
276
|
-
}
|
|
277
|
-
function deriveReceiptRole(event) {
|
|
278
|
-
switch (event.type) {
|
|
279
|
-
case "accepted_mutation":
|
|
280
|
-
case "dispatch_attempted":
|
|
281
|
-
case "dispatch_no_mutation":
|
|
282
|
-
if (typeof event.agentType === "string" && event.agentType.trim().length > 0) {
|
|
283
|
-
return {
|
|
284
|
-
normalizedRole: event.agentType.trim(),
|
|
285
|
-
testEngineerExempt: event.agentType === "coder" ? event.context?.testEngineerExempt === true : null
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
return null;
|
|
289
|
-
case "stage_b_completed":
|
|
290
|
-
case "gate_recorded":
|
|
291
|
-
return {
|
|
292
|
-
normalizedRole: event.gate,
|
|
293
|
-
testEngineerExempt: null
|
|
294
|
-
};
|
|
295
|
-
default:
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
function shouldAppendReceipt(current, next, role) {
|
|
300
|
-
if (next.required_gates.length === 0)
|
|
301
|
-
return false;
|
|
302
|
-
const currentGeneration = current?.workflow?.generation ?? -1;
|
|
303
|
-
const nextGeneration = next.workflow?.generation ?? -1;
|
|
304
|
-
if (current === null)
|
|
305
|
-
return true;
|
|
306
|
-
if (JSON.stringify(current.required_gates) !== JSON.stringify(next.required_gates)) {
|
|
307
|
-
return true;
|
|
308
|
-
}
|
|
309
|
-
if ((current.test_engineer_exempt ?? null) !== role.testEngineerExempt) {
|
|
310
|
-
return true;
|
|
311
|
-
}
|
|
312
|
-
return currentGeneration !== nextGeneration;
|
|
313
|
-
}
|
|
314
|
-
async function appendTaskGateRequirementsReceiptIfNeeded(directory, taskId, current, next, event) {
|
|
315
|
-
const role = deriveReceiptRole(event);
|
|
316
|
-
const routeBinding = routeBindingFromEvent(event);
|
|
317
|
-
if (routeBinding && routeBinding.taskId !== taskId) {
|
|
318
|
-
throw new Error(`TASK_GATE_REQUIREMENTS_ROUTE_IDENTITY_MISMATCH: expected ${taskId}, found ${routeBinding.taskId}`);
|
|
319
|
-
}
|
|
320
|
-
if (!role || !shouldAppendReceipt(current, next, role) && !routeBinding) {
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
const filePath = safeTaskGateRequirementsReceiptPath(directory, taskId, true);
|
|
324
|
-
const receipts = await readTaskGateRequirementsReceipts(directory, taskId);
|
|
325
|
-
const latest = receipts.at(-1);
|
|
326
|
-
const baseReceipt = {
|
|
327
|
-
schemaVersion: 1,
|
|
328
|
-
taskId,
|
|
329
|
-
generation: next.workflow?.generation ?? 0,
|
|
330
|
-
normalizedRole: role.normalizedRole,
|
|
331
|
-
testEngineerExempt: role.testEngineerExempt,
|
|
332
|
-
requiredGates: [...next.required_gates],
|
|
333
|
-
sourceEvent: event.type,
|
|
334
|
-
sourceTransitionId: event.transitionId ?? null,
|
|
335
|
-
recordedAt: new Date().toISOString(),
|
|
336
|
-
previousChainHash: latest?.chainHash ?? null,
|
|
337
|
-
...routeBinding ? { routeBinding } : {}
|
|
338
|
-
};
|
|
339
|
-
const receipt = {
|
|
340
|
-
...baseReceipt,
|
|
341
|
-
chainHash: computeChainHash(baseReceipt)
|
|
342
|
-
};
|
|
343
|
-
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)) {
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
const lines = [...receipts, receipt].map((entry) => JSON.stringify(entry));
|
|
347
|
-
assertSafeRequirementsParent(directory, false);
|
|
348
|
-
await atomicWriteFile(filePath, `${lines.join(`
|
|
349
|
-
`)}
|
|
350
|
-
`);
|
|
351
|
-
assertSafeRequirementsParent(directory, false);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// src/validation/task-id.ts
|
|
355
|
-
var STRICT_TASK_ID_PATTERN = /^\d+\.\d+(\.\d+)*$/;
|
|
356
|
-
var RETRO_TASK_ID_REGEX = /^retro-\d+$/;
|
|
357
|
-
var INTERNAL_TOOL_ID_REGEX = /^(?:sast_scan|quality_budget|syntax_check|placeholder_scan|sbom_generate|build|secretscan)$/;
|
|
358
|
-
var GENERAL_TASK_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
359
|
-
function checkUnsafeChars(taskId) {
|
|
360
|
-
if (!taskId || taskId.length === 0) {
|
|
361
|
-
return "Invalid task ID: empty string";
|
|
362
|
-
}
|
|
363
|
-
if (/\0/.test(taskId)) {
|
|
364
|
-
return "Invalid task ID: contains null bytes";
|
|
365
|
-
}
|
|
366
|
-
for (let i = 0;i < taskId.length; i++) {
|
|
367
|
-
if (taskId.charCodeAt(i) < 32) {
|
|
368
|
-
return "Invalid task ID: contains control characters";
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
if (taskId.includes("..") || taskId.includes("/") || taskId.includes("\\")) {
|
|
372
|
-
return "Invalid task ID: path traversal detected";
|
|
373
|
-
}
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
function isStrictTaskId(taskId) {
|
|
377
|
-
if (!taskId)
|
|
378
|
-
return false;
|
|
379
|
-
const unsafeMsg = checkUnsafeChars(taskId);
|
|
380
|
-
if (unsafeMsg)
|
|
381
|
-
return false;
|
|
382
|
-
return STRICT_TASK_ID_PATTERN.test(taskId);
|
|
383
|
-
}
|
|
384
|
-
function assertStrictTaskId(taskId) {
|
|
385
|
-
if (!isStrictTaskId(taskId)) {
|
|
386
|
-
throw new Error(`Invalid taskId: "${taskId}". Must match N.M or N.M.P (e.g. "1.1", "1.2.3").`);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
function sanitizeTaskId(taskId) {
|
|
390
|
-
const unsafeMsg = checkUnsafeChars(taskId);
|
|
391
|
-
if (unsafeMsg) {
|
|
392
|
-
throw new Error(unsafeMsg);
|
|
393
|
-
}
|
|
394
|
-
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)) {
|
|
395
|
-
return taskId;
|
|
396
|
-
}
|
|
397
|
-
throw new Error(`Invalid task ID: must be alphanumeric (ASCII) with optional hyphens, underscores, or dots, got "${taskId}"`);
|
|
398
|
-
}
|
|
399
|
-
function validateTaskIdFormat(taskId) {
|
|
400
|
-
if (!STRICT_TASK_ID_PATTERN.test(taskId)) {
|
|
401
|
-
return `Invalid taskId "${taskId}". Must match pattern N.M or N.M.P (e.g., "1.1", "1.2.3")`;
|
|
402
|
-
}
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
33
|
|
|
406
34
|
// src/workflow/workflow-wal-file.ts
|
|
407
|
-
import * as
|
|
35
|
+
import * as fs3 from "fs";
|
|
408
36
|
import { mkdir, open } from "fs/promises";
|
|
409
37
|
import { dirname } from "path";
|
|
410
38
|
|
|
@@ -413,15 +41,15 @@ init_path_identity();
|
|
|
413
41
|
|
|
414
42
|
// src/worktree/merge.ts
|
|
415
43
|
init_warning_buffer();
|
|
416
|
-
import * as
|
|
44
|
+
import * as fs2 from "fs";
|
|
417
45
|
import * as path3 from "path";
|
|
418
46
|
|
|
419
47
|
// src/turbo/lean/recovery.ts
|
|
420
|
-
import * as
|
|
421
|
-
import * as
|
|
48
|
+
import * as fs from "fs";
|
|
49
|
+
import * as path from "path";
|
|
422
50
|
var RECOVERY_DIR_NAME = "recovery";
|
|
423
51
|
function recoveryDir(directory) {
|
|
424
|
-
return
|
|
52
|
+
return path.join(directory, ".swarm", RECOVERY_DIR_NAME);
|
|
425
53
|
}
|
|
426
54
|
function isRecoveryRecord(value) {
|
|
427
55
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
@@ -435,11 +63,11 @@ function readRecoveryRecords(directory) {
|
|
|
435
63
|
const dir = recoveryDir(directory);
|
|
436
64
|
let entries;
|
|
437
65
|
try {
|
|
438
|
-
entries =
|
|
66
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
439
67
|
} catch {
|
|
440
68
|
return {
|
|
441
69
|
records: [],
|
|
442
|
-
errored:
|
|
70
|
+
errored: fs.existsSync(dir)
|
|
443
71
|
};
|
|
444
72
|
}
|
|
445
73
|
const records = [];
|
|
@@ -447,9 +75,9 @@ function readRecoveryRecords(directory) {
|
|
|
447
75
|
for (const entry of entries) {
|
|
448
76
|
if (!entry.isFile() || !entry.name.endsWith(".json"))
|
|
449
77
|
continue;
|
|
450
|
-
const full =
|
|
78
|
+
const full = path.join(dir, entry.name);
|
|
451
79
|
try {
|
|
452
|
-
const parsed = JSON.parse(
|
|
80
|
+
const parsed = JSON.parse(fs.readFileSync(full, "utf-8"));
|
|
453
81
|
if (isRecoveryRecord(parsed)) {
|
|
454
82
|
records.push(parsed);
|
|
455
83
|
} else {
|
|
@@ -475,7 +103,169 @@ function recoveryReadErrored(directory) {
|
|
|
475
103
|
// src/worktree/merge.ts
|
|
476
104
|
init_bun_compat();
|
|
477
105
|
init_git_executable();
|
|
478
|
-
|
|
106
|
+
|
|
107
|
+
// src/worktree/settlement-source-recovery.ts
|
|
108
|
+
init_bun_compat();
|
|
109
|
+
init_git_executable();
|
|
110
|
+
import { stat } from "fs/promises";
|
|
111
|
+
import * as path2 from "path";
|
|
112
|
+
var _internals = { bunSpawn, resolveGitExecutable };
|
|
113
|
+
var OID = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/;
|
|
114
|
+
var REPO_REDIRECT_ENV = new Set([
|
|
115
|
+
"GIT_DIR",
|
|
116
|
+
"GIT_WORK_TREE",
|
|
117
|
+
"GIT_INDEX_FILE",
|
|
118
|
+
"GIT_COMMON_DIR",
|
|
119
|
+
"GIT_OBJECT_DIRECTORY",
|
|
120
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
121
|
+
"GIT_NAMESPACE"
|
|
122
|
+
]);
|
|
123
|
+
function gitEnvWithoutRepoRedirectors() {
|
|
124
|
+
const env = {};
|
|
125
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
126
|
+
if (!REPO_REDIRECT_ENV.has(key.toUpperCase()))
|
|
127
|
+
env[key] = value;
|
|
128
|
+
}
|
|
129
|
+
env.LC_ALL = "C";
|
|
130
|
+
return env;
|
|
131
|
+
}
|
|
132
|
+
async function git(directory, args) {
|
|
133
|
+
let proc;
|
|
134
|
+
try {
|
|
135
|
+
proc = _internals.bunSpawn([_internals.resolveGitExecutable(), ...args], {
|
|
136
|
+
cwd: directory,
|
|
137
|
+
stdin: "ignore",
|
|
138
|
+
stdout: "pipe",
|
|
139
|
+
stderr: "pipe",
|
|
140
|
+
timeout: 1e4,
|
|
141
|
+
maxBuffer: 65536,
|
|
142
|
+
env: gitEnvWithoutRepoRedirectors()
|
|
143
|
+
});
|
|
144
|
+
if (proc.spawnError)
|
|
145
|
+
throw proc.spawnError;
|
|
146
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
147
|
+
proc.exited,
|
|
148
|
+
proc.stdout.text(),
|
|
149
|
+
proc.stderr.text()
|
|
150
|
+
]);
|
|
151
|
+
if (proc.spawnError)
|
|
152
|
+
throw proc.spawnError;
|
|
153
|
+
return { exitCode, stdout, stderr };
|
|
154
|
+
} finally {
|
|
155
|
+
try {
|
|
156
|
+
proc?.kill();
|
|
157
|
+
} catch {}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function requireOid(oid) {
|
|
161
|
+
if (!OID.test(oid))
|
|
162
|
+
throw new Error("CODER_SETTLEMENT_SOURCE_UNCERTAIN: malformed source OID");
|
|
163
|
+
}
|
|
164
|
+
async function probeSettlementSource(directory, oid) {
|
|
165
|
+
requireOid(oid);
|
|
166
|
+
const result = await git(directory, [
|
|
167
|
+
"rev-parse",
|
|
168
|
+
"--verify",
|
|
169
|
+
"--quiet",
|
|
170
|
+
`${oid}^{commit}`
|
|
171
|
+
]);
|
|
172
|
+
if (result.exitCode === 0 && result.stdout.trim() === oid)
|
|
173
|
+
return "present";
|
|
174
|
+
if (result.exitCode === 1 && !result.stderr.trim() && !result.stdout.trim()) {
|
|
175
|
+
const objects = await git(directory, [
|
|
176
|
+
"rev-parse",
|
|
177
|
+
"--git-path",
|
|
178
|
+
"objects"
|
|
179
|
+
]);
|
|
180
|
+
const objectsPath = objects.exitCode === 0 ? objects.stdout.trim() : "";
|
|
181
|
+
const objectsUsable = objectsPath !== "" && await stat(path2.resolve(directory, objectsPath)).then((entry) => entry.isDirectory(), () => false);
|
|
182
|
+
if (!objectsUsable) {
|
|
183
|
+
throw new Error(`CODER_SETTLEMENT_SOURCE_UNCERTAIN: object database unavailable for ${oid}`);
|
|
184
|
+
}
|
|
185
|
+
const health = await git(directory, ["count-objects", "-v"]);
|
|
186
|
+
if (health.exitCode !== 0 || health.stderr.trim())
|
|
187
|
+
throw new Error(`CODER_SETTLEMENT_SOURCE_UNCERTAIN: object database unreadable for ${oid}`);
|
|
188
|
+
return "absent";
|
|
189
|
+
}
|
|
190
|
+
throw new Error(`CODER_SETTLEMENT_SOURCE_UNCERTAIN: ${oid}: ${result.stderr.trim() || "Git object probe failed"}`);
|
|
191
|
+
}
|
|
192
|
+
function settlementBackupRef(oid) {
|
|
193
|
+
requireOid(oid);
|
|
194
|
+
return `refs/swarm/coder-settlements/${oid}`;
|
|
195
|
+
}
|
|
196
|
+
async function readDirectRef(directory, ref) {
|
|
197
|
+
const symbolic = await git(directory, ["symbolic-ref", "-q", ref]);
|
|
198
|
+
if (symbolic.exitCode !== 1)
|
|
199
|
+
throw new Error(`CODER_SETTLEMENT_REF_UNCERTAIN: symbolic or unreadable ref ${ref}`);
|
|
200
|
+
const result = await git(directory, [
|
|
201
|
+
"rev-parse",
|
|
202
|
+
"--verify",
|
|
203
|
+
"--quiet",
|
|
204
|
+
ref
|
|
205
|
+
]);
|
|
206
|
+
if (result.exitCode === 0 && OID.test(result.stdout.trim()))
|
|
207
|
+
return result.stdout.trim();
|
|
208
|
+
if (result.exitCode === 1 && !result.stderr.trim() && !result.stdout.trim())
|
|
209
|
+
return null;
|
|
210
|
+
throw new Error(`CODER_SETTLEMENT_REF_UNCERTAIN: ${ref}`);
|
|
211
|
+
}
|
|
212
|
+
async function createExactRef(directory, ref, oid) {
|
|
213
|
+
requireOid(oid);
|
|
214
|
+
const current = await readDirectRef(directory, ref);
|
|
215
|
+
if (current === oid)
|
|
216
|
+
return false;
|
|
217
|
+
if (current !== null)
|
|
218
|
+
throw new Error(`RECOVERABLE_ORPHAN_COMMIT: ${oid}; occupied ref ${ref}`);
|
|
219
|
+
const result = await git(directory, [
|
|
220
|
+
"update-ref",
|
|
221
|
+
"--no-deref",
|
|
222
|
+
ref,
|
|
223
|
+
oid,
|
|
224
|
+
"0".repeat(oid.length)
|
|
225
|
+
]);
|
|
226
|
+
if (await readDirectRef(directory, ref) !== oid)
|
|
227
|
+
throw new Error(`RECOVERABLE_ORPHAN_COMMIT: ${oid}; cannot preserve ${ref}: ${result.stderr.trim()}`);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
async function preserveSettlementSource(directory, oid) {
|
|
231
|
+
const ref = settlementBackupRef(oid);
|
|
232
|
+
await createExactRef(directory, ref, oid);
|
|
233
|
+
return ref;
|
|
234
|
+
}
|
|
235
|
+
async function restoreSettlementBranch(directory, branch, oid) {
|
|
236
|
+
const ref = `refs/heads/${branch}`;
|
|
237
|
+
const valid = await git(directory, ["check-ref-format", ref]);
|
|
238
|
+
if (valid.exitCode !== 0)
|
|
239
|
+
throw new Error("CODER_SETTLEMENT_REF_UNCERTAIN: malformed lane branch");
|
|
240
|
+
return createExactRef(directory, ref, oid);
|
|
241
|
+
}
|
|
242
|
+
async function releaseSettlementSource(directory, oid) {
|
|
243
|
+
const ref = settlementBackupRef(oid);
|
|
244
|
+
const current = await readDirectRef(directory, ref);
|
|
245
|
+
if (current === null)
|
|
246
|
+
return;
|
|
247
|
+
if (current !== oid)
|
|
248
|
+
throw new Error(`CODER_SETTLEMENT_REF_UNCERTAIN: occupied backup ${ref}`);
|
|
249
|
+
await git(directory, ["update-ref", "--no-deref", "-d", ref, oid]);
|
|
250
|
+
if (await readDirectRef(directory, ref) !== null)
|
|
251
|
+
throw new Error(`CODER_SETTLEMENT_REF_UNCERTAIN: cannot release ${ref}`);
|
|
252
|
+
}
|
|
253
|
+
async function validateSettlementLane(directory, branchName, expectedHead) {
|
|
254
|
+
requireOid(expectedHead);
|
|
255
|
+
if ((await git(directory, ["check-ref-format", `refs/heads/${branchName}`])).exitCode !== 0)
|
|
256
|
+
throw new Error("CODER_SETTLEMENT_DISPATCH_ARTIFACT_CHANGED: invalid branch");
|
|
257
|
+
const [branch, head] = await Promise.all([
|
|
258
|
+
git(directory, ["symbolic-ref", "--short", "HEAD"]),
|
|
259
|
+
git(directory, ["rev-parse", "--verify", "HEAD"])
|
|
260
|
+
]);
|
|
261
|
+
if (branch.exitCode !== 0 || branch.stdout.trim() !== branchName || head.exitCode !== 0 || head.stdout.trim() !== expectedHead)
|
|
262
|
+
throw new Error("CODER_SETTLEMENT_DISPATCH_ARTIFACT_CHANGED: lane branch or HEAD changed");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/worktree/merge.ts
|
|
266
|
+
var _internals2 = {
|
|
267
|
+
withSettlementArtifactLifecycle,
|
|
268
|
+
assertSettlementArtifactsRemovable,
|
|
479
269
|
bunSpawn,
|
|
480
270
|
platform: process.platform,
|
|
481
271
|
sleep: (ms) => new Promise((resolve3) => setTimeout(resolve3, ms)),
|
|
@@ -513,7 +303,7 @@ function gitSpawnFailureResult(error, cwd, args) {
|
|
|
513
303
|
async function runGit(args, cwd, timeoutMs = MERGE_TIMEOUT_MS) {
|
|
514
304
|
let proc;
|
|
515
305
|
try {
|
|
516
|
-
proc =
|
|
306
|
+
proc = _internals2.bunSpawn([_internals2.resolveGitExecutable(), ...args], {
|
|
517
307
|
cwd,
|
|
518
308
|
timeout: timeoutMs,
|
|
519
309
|
stdin: "ignore",
|
|
@@ -587,7 +377,7 @@ async function matchesSourceHeadWorkingTree(primaryDir, sourceHead) {
|
|
|
587
377
|
return { landed: false };
|
|
588
378
|
if (status === "A") {
|
|
589
379
|
const worktreePath = path3.join(primaryDir, filePath);
|
|
590
|
-
if (!
|
|
380
|
+
if (!fs2.existsSync(worktreePath))
|
|
591
381
|
return { landed: false };
|
|
592
382
|
const expected = await runGit(["rev-parse", `${sourceHead}:${filePath}`], primaryDir);
|
|
593
383
|
const actual = await runGit(["hash-object", "--", filePath], primaryDir);
|
|
@@ -654,13 +444,13 @@ async function detectInProgressOperation(primaryDir) {
|
|
|
654
444
|
if (!rawPath)
|
|
655
445
|
continue;
|
|
656
446
|
const markerPath = path3.isAbsolute(rawPath) ? rawPath : path3.resolve(primaryDir, rawPath);
|
|
657
|
-
if (
|
|
447
|
+
if (fs2.existsSync(markerPath))
|
|
658
448
|
return { inProgress: true };
|
|
659
449
|
}
|
|
660
450
|
return { inProgress: false };
|
|
661
451
|
}
|
|
662
452
|
function currentPathCasePolicy() {
|
|
663
|
-
return
|
|
453
|
+
return _internals2.platform === "win32" ? "insensitive" : "sensitive";
|
|
664
454
|
}
|
|
665
455
|
function normalizeGitPath(rawPath, casePolicy) {
|
|
666
456
|
if (rawPath.includes("\x00")) {
|
|
@@ -890,6 +680,258 @@ function sameOverlapSnapshot(left, right) {
|
|
|
890
680
|
function getMergeStrategy(config) {
|
|
891
681
|
return config.mergeStrategy ?? config.merge_strategy ?? "merge";
|
|
892
682
|
}
|
|
683
|
+
var FINGERPRINT_BATCH = 200;
|
|
684
|
+
var FINGERPRINT_ARG_BUDGET = 24000;
|
|
685
|
+
async function hashWorktreeFiles(primaryDir, files) {
|
|
686
|
+
const hashed = await runGit(["hash-object", "--", ...files], primaryDir);
|
|
687
|
+
if (hashed.exitCode !== 0)
|
|
688
|
+
return `hash-object: ${hashed.stderr.trim() || hashed.stdout.trim() || `git exited ${hashed.exitCode}`}`;
|
|
689
|
+
const oids = hashed.stdout.split(/\r?\n/).filter(Boolean);
|
|
690
|
+
return oids.length === files.length ? oids : "hash-object: unexpected output length";
|
|
691
|
+
}
|
|
692
|
+
async function fingerprintWorktreePaths(primaryDir, paths, tolerant = false) {
|
|
693
|
+
const fingerprints = new Map;
|
|
694
|
+
const regular = [];
|
|
695
|
+
for (const p of paths) {
|
|
696
|
+
let stat2;
|
|
697
|
+
try {
|
|
698
|
+
stat2 = fs2.lstatSync(path3.join(primaryDir, p));
|
|
699
|
+
} catch (error) {
|
|
700
|
+
const code = error.code;
|
|
701
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
702
|
+
fingerprints.set(p, "absent");
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (!tolerant)
|
|
706
|
+
return `worktree stat for ${p}: ${error.message}`;
|
|
707
|
+
fingerprints.set(p, "unreadable");
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (stat2.isFile())
|
|
711
|
+
regular.push(p);
|
|
712
|
+
else
|
|
713
|
+
fingerprints.set(p, "other");
|
|
714
|
+
}
|
|
715
|
+
const batches = [];
|
|
716
|
+
let batch = [];
|
|
717
|
+
let length = 0;
|
|
718
|
+
for (const p of regular) {
|
|
719
|
+
if (batch.length > 0 && (batch.length >= FINGERPRINT_BATCH || length + p.length + 1 > FINGERPRINT_ARG_BUDGET)) {
|
|
720
|
+
batches.push(batch);
|
|
721
|
+
batch = [];
|
|
722
|
+
length = 0;
|
|
723
|
+
}
|
|
724
|
+
batch.push(p);
|
|
725
|
+
length += p.length + 1;
|
|
726
|
+
}
|
|
727
|
+
if (batch.length > 0)
|
|
728
|
+
batches.push(batch);
|
|
729
|
+
for (const current of batches) {
|
|
730
|
+
const oids = await hashWorktreeFiles(primaryDir, current);
|
|
731
|
+
if (typeof oids !== "string") {
|
|
732
|
+
for (const [index, p] of current.entries())
|
|
733
|
+
fingerprints.set(p, oids[index]);
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (!tolerant)
|
|
737
|
+
return oids;
|
|
738
|
+
for (const p of current) {
|
|
739
|
+
const single = await hashWorktreeFiles(primaryDir, [p]);
|
|
740
|
+
fingerprints.set(p, typeof single === "string" ? "unreadable" : single[0]);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return fingerprints;
|
|
744
|
+
}
|
|
745
|
+
function unmergedIndexPaths(index) {
|
|
746
|
+
const unmerged = new Set;
|
|
747
|
+
for (const [name, records] of index) {
|
|
748
|
+
if (records.split(`
|
|
749
|
+
`).some((record) => !record.endsWith(" 0")))
|
|
750
|
+
unmerged.add(name);
|
|
751
|
+
}
|
|
752
|
+
return unmerged;
|
|
753
|
+
}
|
|
754
|
+
async function takeSquashStateSnapshot(primaryDir, incomingPaths) {
|
|
755
|
+
const describe = (result, operation) => `${operation}: ${result.stderr.trim() || result.stdout.trim() || `git exited ${result.exitCode}`}`;
|
|
756
|
+
const listing = await runGit(["ls-files", "-s", "-z"], primaryDir);
|
|
757
|
+
if (listing.exitCode !== 0)
|
|
758
|
+
return describe(listing, "index listing");
|
|
759
|
+
const index = new Map;
|
|
760
|
+
for (const record of listing.stdout.split("\x00")) {
|
|
761
|
+
const tab = record.indexOf("\t");
|
|
762
|
+
if (tab < 0)
|
|
763
|
+
continue;
|
|
764
|
+
const name = record.slice(tab + 1);
|
|
765
|
+
const entry = record.slice(0, tab);
|
|
766
|
+
index.set(name, index.has(name) ? `${index.get(name)}
|
|
767
|
+
${entry}` : entry);
|
|
768
|
+
}
|
|
769
|
+
const worktree = await fingerprintWorktreePaths(primaryDir, incomingPaths);
|
|
770
|
+
if (typeof worktree === "string")
|
|
771
|
+
return worktree;
|
|
772
|
+
const differing = await runGit(["diff", "--name-only", "--no-renames", "-z", "HEAD"], primaryDir);
|
|
773
|
+
if (differing.exitCode !== 0)
|
|
774
|
+
return describe(differing, "HEAD diff listing");
|
|
775
|
+
const flagged = await runGit(["ls-files", "-v", "-z"], primaryDir);
|
|
776
|
+
if (flagged.exitCode !== 0)
|
|
777
|
+
return describe(flagged, "index tag listing");
|
|
778
|
+
const incoming = new Set(incomingPaths);
|
|
779
|
+
const outside = new Set;
|
|
780
|
+
for (const name of differing.stdout.split("\x00")) {
|
|
781
|
+
if (name !== "" && !incoming.has(name))
|
|
782
|
+
outside.add(name);
|
|
783
|
+
}
|
|
784
|
+
for (const record of flagged.stdout.split("\x00")) {
|
|
785
|
+
if (!/^[a-z] /.test(record))
|
|
786
|
+
continue;
|
|
787
|
+
const name = record.slice(2);
|
|
788
|
+
if (!incoming.has(name))
|
|
789
|
+
outside.add(name);
|
|
790
|
+
}
|
|
791
|
+
const dirty = await fingerprintWorktreePaths(primaryDir, [...outside], true);
|
|
792
|
+
if (typeof dirty === "string")
|
|
793
|
+
return dirty;
|
|
794
|
+
return { index, worktree, dirty };
|
|
795
|
+
}
|
|
796
|
+
async function restoreFailedSquashPaths(primaryDir, branchName, incomingPaths, before) {
|
|
797
|
+
const describe = (result, operation) => `${operation}: ${result.stderr.trim() || result.stdout.trim() || `git exited ${result.exitCode}`}`;
|
|
798
|
+
const after = await takeSquashStateSnapshot(primaryDir, incomingPaths);
|
|
799
|
+
if (typeof after === "string")
|
|
800
|
+
return after;
|
|
801
|
+
const incoming = new Set(incomingPaths);
|
|
802
|
+
const reported = [];
|
|
803
|
+
const indexChanged = new Set;
|
|
804
|
+
const indexNames = new Set([...before.index.keys(), ...after.index.keys()]);
|
|
805
|
+
for (const name of indexNames) {
|
|
806
|
+
if (incoming.has(name))
|
|
807
|
+
continue;
|
|
808
|
+
if (before.index.get(name) !== after.index.get(name)) {
|
|
809
|
+
reported.push(`changed outside the incoming paths: ${name}`);
|
|
810
|
+
indexChanged.add(name);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const headListing = await runGit(["--literal-pathspecs", "ls-tree", "-z", "HEAD", "--", ...incomingPaths], primaryDir);
|
|
814
|
+
if (headListing.exitCode !== 0)
|
|
815
|
+
return describe(headListing, "HEAD listing");
|
|
816
|
+
const headTypes = new Map;
|
|
817
|
+
for (const record of headListing.stdout.split("\x00")) {
|
|
818
|
+
const tab = record.indexOf("\t");
|
|
819
|
+
if (tab < 0)
|
|
820
|
+
continue;
|
|
821
|
+
const [, type] = record.slice(0, tab).split(" ");
|
|
822
|
+
headTypes.set(record.slice(tab + 1), type);
|
|
823
|
+
}
|
|
824
|
+
const laneListing = await runGit([
|
|
825
|
+
"--literal-pathspecs",
|
|
826
|
+
"ls-tree",
|
|
827
|
+
"-z",
|
|
828
|
+
branchName,
|
|
829
|
+
"--",
|
|
830
|
+
...incomingPaths
|
|
831
|
+
], primaryDir);
|
|
832
|
+
const laneBlobs = new Map;
|
|
833
|
+
if (laneListing.exitCode !== 0)
|
|
834
|
+
reported.push(describe(laneListing, "lane listing"));
|
|
835
|
+
const laneRecords = laneListing.exitCode === 0 ? laneListing.stdout.split("\x00") : [];
|
|
836
|
+
for (const record of laneRecords) {
|
|
837
|
+
const tab = record.indexOf("\t");
|
|
838
|
+
if (tab < 0)
|
|
839
|
+
continue;
|
|
840
|
+
const [mode, type, oid] = record.slice(0, tab).split(" ");
|
|
841
|
+
if (type === "blob" && mode !== "120000" && oid)
|
|
842
|
+
laneBlobs.set(record.slice(tab + 1), oid);
|
|
843
|
+
}
|
|
844
|
+
const changed = [];
|
|
845
|
+
for (const p of incomingPaths) {
|
|
846
|
+
if (before.index.get(p) !== after.index.get(p)) {
|
|
847
|
+
changed.push(p);
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
if (before.worktree.get(p) === after.worktree.get(p))
|
|
851
|
+
continue;
|
|
852
|
+
if (headTypes.get(p) !== "blob") {
|
|
853
|
+
changed.push(p);
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const state = after.worktree.get(p);
|
|
857
|
+
if (state === "absent" || state !== "other" && state === laneBlobs.get(p)) {
|
|
858
|
+
changed.push(p);
|
|
859
|
+
} else {
|
|
860
|
+
reported.push(`left in place (working tree changed without an index change): ${p}`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (changed.length > 0) {
|
|
864
|
+
const trackedBlobs = changed.filter((p) => headTypes.get(p) === "blob");
|
|
865
|
+
const absentFromHead = changed.filter((p) => !headTypes.has(p));
|
|
866
|
+
for (const p of changed) {
|
|
867
|
+
const type = headTypes.get(p);
|
|
868
|
+
if (type !== undefined && type !== "blob")
|
|
869
|
+
reported.push(`not restored (directory or submodule in HEAD): ${p}`);
|
|
870
|
+
}
|
|
871
|
+
const restorable = [...trackedBlobs, ...absentFromHead];
|
|
872
|
+
if (restorable.length > 0) {
|
|
873
|
+
const reset = await runGit(["--literal-pathspecs", "reset", "-q", "HEAD", "--", ...restorable], primaryDir);
|
|
874
|
+
if (reset.exitCode !== 0)
|
|
875
|
+
return describe(reset, "index reset");
|
|
876
|
+
}
|
|
877
|
+
if (trackedBlobs.length > 0) {
|
|
878
|
+
const checkout = await runGit([
|
|
879
|
+
"--literal-pathspecs",
|
|
880
|
+
"checkout",
|
|
881
|
+
"-q",
|
|
882
|
+
"HEAD",
|
|
883
|
+
"--",
|
|
884
|
+
...trackedBlobs
|
|
885
|
+
], primaryDir);
|
|
886
|
+
if (checkout.exitCode !== 0)
|
|
887
|
+
return describe(checkout, "checkout");
|
|
888
|
+
}
|
|
889
|
+
const candidates = [];
|
|
890
|
+
for (const p of absentFromHead) {
|
|
891
|
+
const state = after.worktree.get(p);
|
|
892
|
+
if (state === "other")
|
|
893
|
+
reported.push(`left in place (not a regular file): ${p}`);
|
|
894
|
+
else if (state !== "absent")
|
|
895
|
+
candidates.push(p);
|
|
896
|
+
}
|
|
897
|
+
for (const p of candidates) {
|
|
898
|
+
if (laneBlobs.get(p) !== after.worktree.get(p)) {
|
|
899
|
+
reported.push(`left in place (not the exact lane bytes): ${p}`);
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const current = await runGit(["hash-object", "--", p], primaryDir);
|
|
903
|
+
if (current.exitCode !== 0 || current.stdout.trim() !== laneBlobs.get(p)) {
|
|
904
|
+
reported.push(`left in place (changed since it was checked): ${p}`);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
try {
|
|
908
|
+
fs2.unlinkSync(path3.join(primaryDir, p));
|
|
909
|
+
} catch (error) {
|
|
910
|
+
if (error.code !== "ENOENT")
|
|
911
|
+
reported.push(`could not remove ${p}: ${error.message}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
const dirtyNames = new Set([...before.dirty.keys(), ...after.dirty.keys()]);
|
|
916
|
+
for (const name of dirtyNames) {
|
|
917
|
+
if (!indexChanged.has(name) && before.dirty.get(name) !== after.dirty.get(name))
|
|
918
|
+
reported.push(`working tree outside the incoming paths changed during the failed squash: ${name}`);
|
|
919
|
+
}
|
|
920
|
+
const remaining = await runGit(["ls-files", "-u", "-z"], primaryDir);
|
|
921
|
+
if (remaining.exitCode !== 0)
|
|
922
|
+
return describe(remaining, "unmerged listing after restore");
|
|
923
|
+
const unmergedBefore = unmergedIndexPaths(before.index);
|
|
924
|
+
for (const record of remaining.stdout.split("\x00")) {
|
|
925
|
+
const tab = record.indexOf("\t");
|
|
926
|
+
if (tab < 0)
|
|
927
|
+
continue;
|
|
928
|
+
const name = record.slice(tab + 1);
|
|
929
|
+
if (!unmergedBefore.has(name))
|
|
930
|
+
reported.push(`unmerged: ${name}`);
|
|
931
|
+
}
|
|
932
|
+
const unique = [...new Set(reported)];
|
|
933
|
+
return unique.length === 0 ? null : `not restored: ${unique.join("; ")}`;
|
|
934
|
+
}
|
|
893
935
|
async function mergeLaneBranch(primaryDir, branchName, strategy, commitMessage) {
|
|
894
936
|
let result;
|
|
895
937
|
switch (strategy) {
|
|
@@ -933,26 +975,33 @@ async function mergeLaneBranch(primaryDir, branchName, strategy, commitMessage)
|
|
|
933
975
|
};
|
|
934
976
|
}
|
|
935
977
|
const incomingPaths = [...parsedIncoming.paths.values()];
|
|
978
|
+
const squashSnapshot = await takeSquashStateSnapshot(primaryDir, incomingPaths);
|
|
979
|
+
if (typeof squashSnapshot === "string") {
|
|
980
|
+
return {
|
|
981
|
+
error: `could not record pre-merge state before squash of ${branchName}: ${squashSnapshot}`
|
|
982
|
+
};
|
|
983
|
+
}
|
|
936
984
|
const squashResult = await runGit(["merge", "--squash", "--no-commit", branchName], primaryDir);
|
|
937
985
|
if (squashResult.exitCode !== 0) {
|
|
938
986
|
const combined = `${squashResult.stderr}
|
|
939
987
|
${squashResult.stdout}`;
|
|
940
|
-
const
|
|
988
|
+
const restoreFailure = await restoreFailedSquashPaths(primaryDir, branchName, incomingPaths, squashSnapshot);
|
|
989
|
+
if (restoreFailure !== null) {
|
|
990
|
+
advisoryWarn(`[worktree] mergeLaneBranch: squash merge failed and targeted restore also failed for ${branchName}: ${restoreFailure}`);
|
|
991
|
+
}
|
|
992
|
+
const restoreSuffix = restoreFailure === null ? "" : `; restore of pre-merge state incomplete, partial merge state may remain: ${restoreFailure}`;
|
|
941
993
|
if (/CONFLICT/i.test(combined)) {
|
|
942
994
|
return {
|
|
943
995
|
conflict: true,
|
|
944
996
|
files: parseConflictFiles(combined),
|
|
945
|
-
message: squashResult.stderr.trim() || squashResult.stdout.trim() || "squash merge reported a conflict"
|
|
997
|
+
message: `${squashResult.stderr.trim() || squashResult.stdout.trim() || "squash merge reported a conflict"}${restoreSuffix}`
|
|
946
998
|
};
|
|
947
999
|
}
|
|
948
|
-
if (restore.exitCode !== 0) {
|
|
949
|
-
advisoryWarn(`[worktree] mergeLaneBranch: squash merge failed and targeted restore also failed for ${branchName}: ${restore.stderr.trim() || restore.stdout.trim()}`);
|
|
950
|
-
}
|
|
951
1000
|
return {
|
|
952
|
-
error: squashResult.stderr.trim() || squashResult.stdout.trim() || "squash merge failed"
|
|
1001
|
+
error: `${squashResult.stderr.trim() || squashResult.stdout.trim() || "squash merge failed"}${restoreSuffix}`
|
|
953
1002
|
};
|
|
954
1003
|
}
|
|
955
|
-
const resetResult = await runGit(["reset", "-q", "HEAD", "--", ...incomingPaths], primaryDir);
|
|
1004
|
+
const resetResult = await runGit(["--literal-pathspecs", "reset", "-q", "HEAD", "--", ...incomingPaths], primaryDir);
|
|
956
1005
|
if (resetResult.exitCode !== 0) {
|
|
957
1006
|
advisoryWarn(`[worktree] mergeLaneBranch: targeted unstage failed for ${branchName} (${resetResult.stderr.trim() || resetResult.stdout.trim()}); lane changes remain staged`);
|
|
958
1007
|
}
|
|
@@ -1008,7 +1057,7 @@ async function readOrderedCommitRange(primaryDir, fromExclusive, toInclusive, de
|
|
|
1008
1057
|
}
|
|
1009
1058
|
return { commits };
|
|
1010
1059
|
}
|
|
1011
|
-
async function recoverMergeBackFromImmutableCoordinates(primaryDir, coordinates) {
|
|
1060
|
+
async function recoverMergeBackFromImmutableCoordinates(primaryDir, coordinates, options) {
|
|
1012
1061
|
if (!GIT_OBJECT_ID_PATTERN.test(coordinates.sourceBaseOid) || !GIT_OBJECT_ID_PATTERN.test(coordinates.sourceHeadOid) || !GIT_OBJECT_ID_PATTERN.test(coordinates.targetHeadOid)) {
|
|
1013
1062
|
return {
|
|
1014
1063
|
error: "Malformed preserved lane recovery coordinates"
|
|
@@ -1045,7 +1094,10 @@ async function recoverMergeBackFromImmutableCoordinates(primaryDir, coordinates)
|
|
|
1045
1094
|
};
|
|
1046
1095
|
}
|
|
1047
1096
|
if (coordinates.strategy === "merge") {
|
|
1048
|
-
|
|
1097
|
+
if (options?.landUnstaged) {
|
|
1098
|
+
return mergeLaneBranch(primaryDir, coordinates.sourceHeadOid, "squash-unstaged");
|
|
1099
|
+
}
|
|
1100
|
+
return mergeLaneBranch(primaryDir, coordinates.sourceHeadOid, "merge", options?.landingCommitMessage);
|
|
1049
1101
|
}
|
|
1050
1102
|
if (coordinates.strategy === "rebase") {
|
|
1051
1103
|
return finalizeMergeStrategyResult(primaryDir, "rebase", await runGit([
|
|
@@ -1135,22 +1187,31 @@ async function reconcileLandedMerge(primaryDir, provenance) {
|
|
|
1135
1187
|
return trailerSearch.stdout.trim() ? { landed: true, method: "cherry-pick-trailer" } : { landed: false };
|
|
1136
1188
|
}
|
|
1137
1189
|
async function postMergeCleanup(directory, branchName) {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1190
|
+
try {
|
|
1191
|
+
return await _internals2.withSettlementArtifactLifecycle(directory, async () => {
|
|
1192
|
+
await _internals2.assertSettlementArtifactsRemovable(directory, {
|
|
1193
|
+
branch: branchName
|
|
1194
|
+
});
|
|
1195
|
+
const pruneResult = await runGit(["worktree", "prune"], directory);
|
|
1196
|
+
const pruneOk = pruneResult.exitCode === 0;
|
|
1197
|
+
const deleteResult = await runGit(["branch", "-D", branchName], directory);
|
|
1198
|
+
const deleteOk = deleteResult.exitCode === 0;
|
|
1199
|
+
if (deleteOk && pruneOk) {
|
|
1200
|
+
return { cleaned: true };
|
|
1201
|
+
}
|
|
1202
|
+
if (!deleteOk && pruneOk) {
|
|
1203
|
+
return {
|
|
1204
|
+
error: `Branch delete failed: ${deleteResult.stderr.trim() || deleteResult.stdout.trim()}`,
|
|
1205
|
+
partial: true
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
return {
|
|
1209
|
+
error: deleteOk ? `Worktree prune failed: ${pruneResult.stderr.trim() || pruneResult.stdout.trim()}` : `Branch delete failed: ${deleteResult.stderr.trim() || deleteResult.stdout.trim()}; worktree prune failed: ${pruneResult.stderr.trim() || pruneResult.stdout.trim()}`
|
|
1210
|
+
};
|
|
1211
|
+
});
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
return { error: String(error) };
|
|
1150
1214
|
}
|
|
1151
|
-
return {
|
|
1152
|
-
error: deleteOk ? `Worktree prune failed: ${pruneResult.stderr.trim() || pruneResult.stdout.trim()}` : `Branch delete failed: ${deleteResult.stderr.trim() || deleteResult.stdout.trim()}; worktree prune failed: ${pruneResult.stderr.trim() || pruneResult.stdout.trim()}`
|
|
1153
|
-
};
|
|
1154
1215
|
}
|
|
1155
1216
|
async function pruneStaleWorktreeMetadata(directory) {
|
|
1156
1217
|
const result = await runGit(["worktree", "prune"], directory);
|
|
@@ -1193,6 +1254,13 @@ async function handleSourceWorktreeGone(branchHead, worktreePath, branchName, pr
|
|
|
1193
1254
|
advisoryWarn(`STALE_LANE_WORKTREE_DETECTED: lane worktree "${worktreePath}" no longer exists. ` + `Its stale git registration was pruned, and the merge-back for branch "${branchName}" ` + `is being taken from "${primaryDir}" instead. Any uncommitted work in that worktree is ` + "unrecoverable; every commit on the branch is preserved.");
|
|
1194
1255
|
return null;
|
|
1195
1256
|
}
|
|
1257
|
+
async function releaseVerifiedSettlementSource(primaryDir, sourceHead, context) {
|
|
1258
|
+
try {
|
|
1259
|
+
await releaseSettlementSource(primaryDir, sourceHead);
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
advisoryWarn(`[worktree] attemptMergeBackFromDirty: landing verified (${context}) but releasing preserved settlement source ${sourceHead} failed; outcome unchanged, backup ref left in place:`, error);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1196
1264
|
async function attemptMergeBackFromDirty(worktreePath, branchName, primaryDir, strategy, options = {}) {
|
|
1197
1265
|
let autoCommitted = false;
|
|
1198
1266
|
let cleaned = false;
|
|
@@ -1209,6 +1277,34 @@ async function attemptMergeBackFromDirty(worktreePath, branchName, primaryDir, s
|
|
|
1209
1277
|
provenance
|
|
1210
1278
|
};
|
|
1211
1279
|
}
|
|
1280
|
+
let orphanRestored = false;
|
|
1281
|
+
try {
|
|
1282
|
+
if (await probeSettlementSource(primaryDir, provenance.sourceHead) === "absent") {
|
|
1283
|
+
return {
|
|
1284
|
+
failed: true,
|
|
1285
|
+
stage: SOURCE_WORKTREE_GONE_STAGE,
|
|
1286
|
+
message: "Recorded source commit is absent",
|
|
1287
|
+
provenance
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
return {
|
|
1292
|
+
failed: true,
|
|
1293
|
+
stage: "reconciliation",
|
|
1294
|
+
message: String(error),
|
|
1295
|
+
provenance
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
try {
|
|
1299
|
+
await preserveSettlementSource(primaryDir, provenance.sourceHead);
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
return {
|
|
1302
|
+
failed: true,
|
|
1303
|
+
stage: "reconciliation",
|
|
1304
|
+
message: String(error),
|
|
1305
|
+
provenance
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1212
1308
|
const reconciliation = await reconcileLandedMerge(primaryDir, provenance);
|
|
1213
1309
|
if ("error" in reconciliation && reconciliation.error) {
|
|
1214
1310
|
return {
|
|
@@ -1219,6 +1315,9 @@ async function attemptMergeBackFromDirty(worktreePath, branchName, primaryDir, s
|
|
|
1219
1315
|
};
|
|
1220
1316
|
}
|
|
1221
1317
|
if (reconciliation.landed) {
|
|
1318
|
+
if (reconciliation.method !== "worktree-match") {
|
|
1319
|
+
await releaseVerifiedSettlementSource(primaryDir, provenance.sourceHead, "resume reconciliation");
|
|
1320
|
+
}
|
|
1222
1321
|
return {
|
|
1223
1322
|
merged: true,
|
|
1224
1323
|
strategy: reconciliation.method === "worktree-match" ? "squash-unstaged" : strategy,
|
|
@@ -1228,6 +1327,16 @@ async function attemptMergeBackFromDirty(worktreePath, branchName, primaryDir, s
|
|
|
1228
1327
|
provenance
|
|
1229
1328
|
};
|
|
1230
1329
|
}
|
|
1330
|
+
try {
|
|
1331
|
+
orphanRestored = await restoreSettlementBranch(primaryDir, branchName, provenance.sourceHead);
|
|
1332
|
+
} catch (error) {
|
|
1333
|
+
return {
|
|
1334
|
+
failed: true,
|
|
1335
|
+
stage: "reconciliation",
|
|
1336
|
+
message: String(error),
|
|
1337
|
+
provenance
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1231
1340
|
const source = await captureLaneSourceHead(worktreePath, branchName, primaryDir, false);
|
|
1232
1341
|
if (source.sourceWorktreeMissing) {
|
|
1233
1342
|
const unrecoverable = await handleSourceWorktreeGone(source.result, worktreePath, branchName, primaryDir, provenance);
|
|
@@ -1237,7 +1346,7 @@ async function attemptMergeBackFromDirty(worktreePath, branchName, primaryDir, s
|
|
|
1237
1346
|
}
|
|
1238
1347
|
const sourceHead = source.result;
|
|
1239
1348
|
const targetHead = await runGit(["rev-parse", "HEAD"], primaryDir);
|
|
1240
|
-
if (sourceHead.exitCode !== 0 || targetHead.exitCode !== 0 || sourceHead.stdout.trim() !== provenance.sourceHead
|
|
1349
|
+
if (sourceHead.exitCode !== 0 || targetHead.exitCode !== 0 || sourceHead.stdout.trim() !== provenance.sourceHead) {
|
|
1241
1350
|
return {
|
|
1242
1351
|
failed: true,
|
|
1243
1352
|
stage: "reconciliation",
|
|
@@ -1245,6 +1354,61 @@ async function attemptMergeBackFromDirty(worktreePath, branchName, primaryDir, s
|
|
|
1245
1354
|
provenance
|
|
1246
1355
|
};
|
|
1247
1356
|
}
|
|
1357
|
+
if (strategy === "merge" && (orphanRestored || targetHead.stdout.trim() !== provenance.targetHeadBefore)) {
|
|
1358
|
+
const relation = await runGit([
|
|
1359
|
+
"merge-base",
|
|
1360
|
+
"--is-ancestor",
|
|
1361
|
+
provenance.targetHeadBefore,
|
|
1362
|
+
targetHead.stdout.trim()
|
|
1363
|
+
], primaryDir);
|
|
1364
|
+
if (relation.exitCode !== 0)
|
|
1365
|
+
return {
|
|
1366
|
+
failed: true,
|
|
1367
|
+
stage: "reconciliation",
|
|
1368
|
+
message: "Current primary HEAD no longer descends from recorded target",
|
|
1369
|
+
provenance
|
|
1370
|
+
};
|
|
1371
|
+
const base = await runGit(["merge-base", targetHead.stdout.trim(), provenance.sourceHead], primaryDir);
|
|
1372
|
+
if (base.exitCode !== 0)
|
|
1373
|
+
return {
|
|
1374
|
+
failed: true,
|
|
1375
|
+
stage: "reconciliation",
|
|
1376
|
+
message: "Unable to derive current immutable merge base",
|
|
1377
|
+
provenance
|
|
1378
|
+
};
|
|
1379
|
+
const result = await recoverMergeBackFromImmutableCoordinates(primaryDir, {
|
|
1380
|
+
sourceBaseOid: base.stdout.trim(),
|
|
1381
|
+
sourceHeadOid: provenance.sourceHead,
|
|
1382
|
+
targetHeadOid: targetHead.stdout.trim(),
|
|
1383
|
+
strategy
|
|
1384
|
+
}, options.commitLanding ? { landingCommitMessage: options.landingCommitMessage } : { landUnstaged: true });
|
|
1385
|
+
if ("merged" in result) {
|
|
1386
|
+
if (result.strategy !== "squash-unstaged") {
|
|
1387
|
+
await releaseVerifiedSettlementSource(primaryDir, provenance.sourceHead, "retarget recovery");
|
|
1388
|
+
}
|
|
1389
|
+
return {
|
|
1390
|
+
merged: true,
|
|
1391
|
+
strategy: result.strategy === "squash-unstaged" ? "squash-unstaged" : strategy,
|
|
1392
|
+
autoCommitted: false,
|
|
1393
|
+
cleaned: false,
|
|
1394
|
+
reconciled: false,
|
|
1395
|
+
provenance
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
return {
|
|
1399
|
+
failed: true,
|
|
1400
|
+
stage: "merge",
|
|
1401
|
+
message: "error" in result ? result.error : result.message,
|
|
1402
|
+
provenance
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
if (targetHead.stdout.trim() !== provenance.targetHeadBefore)
|
|
1406
|
+
return {
|
|
1407
|
+
failed: true,
|
|
1408
|
+
stage: "reconciliation",
|
|
1409
|
+
message: "Stored merge operation target HEAD has changed",
|
|
1410
|
+
provenance
|
|
1411
|
+
};
|
|
1248
1412
|
}
|
|
1249
1413
|
if (!provenance && !sourceWorktreeMissing) {
|
|
1250
1414
|
const commitResult = await autoCommitDirty(worktreePath);
|
|
@@ -1283,7 +1447,7 @@ async function attemptMergeBackFromDirty(worktreePath, branchName, primaryDir, s
|
|
|
1283
1447
|
};
|
|
1284
1448
|
}
|
|
1285
1449
|
}
|
|
1286
|
-
const initialOverlapSnapshot = await
|
|
1450
|
+
const initialOverlapSnapshot = await _internals2.captureMergeOverlapSnapshot(primaryDir, branchName);
|
|
1287
1451
|
if ("error" in initialOverlapSnapshot) {
|
|
1288
1452
|
return {
|
|
1289
1453
|
failed: true,
|
|
@@ -1320,7 +1484,7 @@ async function attemptMergeBackFromDirty(worktreePath, branchName, primaryDir, s
|
|
|
1320
1484
|
}
|
|
1321
1485
|
}
|
|
1322
1486
|
}
|
|
1323
|
-
const finalOverlapSnapshot = await
|
|
1487
|
+
const finalOverlapSnapshot = await _internals2.captureMergeOverlapSnapshot(primaryDir, branchName);
|
|
1324
1488
|
if ("error" in finalOverlapSnapshot) {
|
|
1325
1489
|
return {
|
|
1326
1490
|
failed: true,
|
|
@@ -1353,6 +1517,12 @@ async function attemptMergeBackFromDirty(worktreePath, branchName, primaryDir, s
|
|
|
1353
1517
|
const landingStrategy = strategy === "merge" && !options.commitLanding ? "squash-unstaged" : strategy;
|
|
1354
1518
|
const mergeResult = await mergeLaneBranch(primaryDir, branchName, landingStrategy, landingStrategy === "merge" && options.commitLanding ? options.landingCommitMessage : undefined);
|
|
1355
1519
|
if ("merged" in mergeResult && mergeResult.merged) {
|
|
1520
|
+
if (provenance && landingStrategy !== "squash-unstaged") {
|
|
1521
|
+
const landed = await reconcileLandedMerge(primaryDir, provenance);
|
|
1522
|
+
if (landed.landed && landed.method !== "worktree-match") {
|
|
1523
|
+
await releaseVerifiedSettlementSource(primaryDir, provenance.sourceHead, "fresh landing");
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1356
1526
|
return {
|
|
1357
1527
|
merged: true,
|
|
1358
1528
|
strategy: landingStrategy,
|
|
@@ -1440,7 +1610,19 @@ async function cleanupOrphanedBranches(directory, activeSessionIds = [], options
|
|
|
1440
1610
|
skippedRecoveryBranches.push(branch);
|
|
1441
1611
|
continue;
|
|
1442
1612
|
}
|
|
1443
|
-
|
|
1613
|
+
let deleteResult;
|
|
1614
|
+
try {
|
|
1615
|
+
deleteResult = await _internals2.withSettlementArtifactLifecycle(directory, async () => {
|
|
1616
|
+
await _internals2.assertSettlementArtifactsRemovable(directory, {
|
|
1617
|
+
branch
|
|
1618
|
+
});
|
|
1619
|
+
return runGit(["branch", options.preserveUnmerged ? "-d" : "-D", branch], directory);
|
|
1620
|
+
});
|
|
1621
|
+
} catch (error) {
|
|
1622
|
+
skipped.push(branch);
|
|
1623
|
+
errors.push({ branch, error: String(error) });
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1444
1626
|
if (deleteResult.exitCode === 0) {
|
|
1445
1627
|
removed.push(branch);
|
|
1446
1628
|
} else {
|
|
@@ -1577,11 +1759,11 @@ var UTF8_FATAL_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
|
1577
1759
|
var CODER_SETTLEMENT_MAX_BYTES = 64 * 1024 * 1024;
|
|
1578
1760
|
var SMALL_WORKFLOW_WAL_MAX_BYTES = 1024 * 1024;
|
|
1579
1761
|
var CHUNK_BYTES = 64 * 1024;
|
|
1580
|
-
var
|
|
1762
|
+
var _internals3 = {
|
|
1581
1763
|
openFile: open,
|
|
1582
|
-
openFileSync:
|
|
1583
|
-
readFileSync:
|
|
1584
|
-
closeFileSync:
|
|
1764
|
+
openFileSync: fs3.openSync,
|
|
1765
|
+
readFileSync: fs3.readSync,
|
|
1766
|
+
closeFileSync: fs3.closeSync
|
|
1585
1767
|
};
|
|
1586
1768
|
function maxBytesFor(kind) {
|
|
1587
1769
|
return kind === "coder-settlement" ? CODER_SETTLEMENT_MAX_BYTES : SMALL_WORKFLOW_WAL_MAX_BYTES;
|
|
@@ -1646,7 +1828,7 @@ function validateWal(kind, raw, filePath, expectedTaskId) {
|
|
|
1646
1828
|
async function readBoundedBytes(kind, filePath, maxBytes) {
|
|
1647
1829
|
let handle;
|
|
1648
1830
|
try {
|
|
1649
|
-
handle = await
|
|
1831
|
+
handle = await _internals3.openFile(filePath, "r");
|
|
1650
1832
|
} catch (error) {
|
|
1651
1833
|
if (error.code === "ENOENT")
|
|
1652
1834
|
return null;
|
|
@@ -1687,7 +1869,7 @@ async function readBoundedBytes(kind, filePath, maxBytes) {
|
|
|
1687
1869
|
function readBoundedBytesSync(kind, filePath, maxBytes) {
|
|
1688
1870
|
let fd;
|
|
1689
1871
|
try {
|
|
1690
|
-
fd =
|
|
1872
|
+
fd = _internals3.openFileSync(filePath, "r");
|
|
1691
1873
|
} catch (error) {
|
|
1692
1874
|
if (error.code === "ENOENT")
|
|
1693
1875
|
return null;
|
|
@@ -1704,7 +1886,7 @@ function readBoundedBytesSync(kind, filePath, maxBytes) {
|
|
|
1704
1886
|
if (remaining <= 0)
|
|
1705
1887
|
break;
|
|
1706
1888
|
const buffer = Buffer.alloc(Math.min(CHUNK_BYTES, remaining));
|
|
1707
|
-
const bytesRead =
|
|
1889
|
+
const bytesRead = _internals3.readFileSync(fd, buffer, 0, buffer.length, null);
|
|
1708
1890
|
if (bytesRead === 0)
|
|
1709
1891
|
break;
|
|
1710
1892
|
total += bytesRead;
|
|
@@ -1717,7 +1899,7 @@ function readBoundedBytesSync(kind, filePath, maxBytes) {
|
|
|
1717
1899
|
failure = walIoError(kind, filePath, "read", error);
|
|
1718
1900
|
}
|
|
1719
1901
|
try {
|
|
1720
|
-
|
|
1902
|
+
_internals3.closeFileSync(fd);
|
|
1721
1903
|
} catch (error) {
|
|
1722
1904
|
failure ??= walIoError(kind, filePath, "close", error);
|
|
1723
1905
|
}
|
|
@@ -1761,951 +1943,4 @@ async function writeWorkflowWalFile(kind, filePath, wal) {
|
|
|
1761
1943
|
await atomicWriteFile(filePath, serialized);
|
|
1762
1944
|
}
|
|
1763
1945
|
|
|
1764
|
-
|
|
1765
|
-
var _internals3 = {
|
|
1766
|
-
appendTaskGateRequirementsReceiptIfNeeded
|
|
1767
|
-
};
|
|
1768
|
-
var TASK_WORKFLOW_SCHEMA_MARKER = "exact-task-v1";
|
|
1769
|
-
var TASK_WORKFLOW_STATES = [
|
|
1770
|
-
"idle",
|
|
1771
|
-
"coder_delegated",
|
|
1772
|
-
"pre_check_passed",
|
|
1773
|
-
"reviewer_run",
|
|
1774
|
-
"tests_run",
|
|
1775
|
-
"rework_required",
|
|
1776
|
-
"complete",
|
|
1777
|
-
"blocked",
|
|
1778
|
-
"closed"
|
|
1779
|
-
];
|
|
1780
|
-
var TASK_WORKFLOW_OUTCOMES = [
|
|
1781
|
-
"none",
|
|
1782
|
-
"dispatch_attempted",
|
|
1783
|
-
"dispatch_no_mutation",
|
|
1784
|
-
"accepted_mutation",
|
|
1785
|
-
"accepted_mutation_failed",
|
|
1786
|
-
"stage_a_passed",
|
|
1787
|
-
"stage_a_failed",
|
|
1788
|
-
"stage_b_completed",
|
|
1789
|
-
"stage_b_failed",
|
|
1790
|
-
"gate_recorded",
|
|
1791
|
-
"task_completed",
|
|
1792
|
-
"task_blocked",
|
|
1793
|
-
"task_closed",
|
|
1794
|
-
"repair_idle",
|
|
1795
|
-
"external_evidence_attested",
|
|
1796
|
-
"external_evidence_revoked"
|
|
1797
|
-
];
|
|
1798
|
-
var MAX_PLAN_SCOPE_INSPECTION_BYTES = 4 * 1024 * 1024;
|
|
1799
|
-
function readCurrentTaskDeclaredFiles(directory, taskId) {
|
|
1800
|
-
try {
|
|
1801
|
-
const planPath = validateSwarmPath(directory, "plan.json");
|
|
1802
|
-
const stat = statSync2(planPath);
|
|
1803
|
-
if (!stat.isFile() || stat.size > MAX_PLAN_SCOPE_INSPECTION_BYTES) {
|
|
1804
|
-
return null;
|
|
1805
|
-
}
|
|
1806
|
-
const parsed = JSON.parse(readFileSync3(planPath, "utf8"));
|
|
1807
|
-
if (!Array.isArray(parsed.phases))
|
|
1808
|
-
return null;
|
|
1809
|
-
let declaredFiles = null;
|
|
1810
|
-
for (const phase of parsed.phases) {
|
|
1811
|
-
if (!phase || typeof phase !== "object")
|
|
1812
|
-
return null;
|
|
1813
|
-
const tasks = phase.tasks;
|
|
1814
|
-
if (!Array.isArray(tasks))
|
|
1815
|
-
return null;
|
|
1816
|
-
for (const task of tasks) {
|
|
1817
|
-
if (!task || typeof task !== "object")
|
|
1818
|
-
return null;
|
|
1819
|
-
const record = task;
|
|
1820
|
-
if (record.id !== taskId)
|
|
1821
|
-
continue;
|
|
1822
|
-
if (!Array.isArray(record.files_touched) || !record.files_touched.every((file) => typeof file === "string")) {
|
|
1823
|
-
return null;
|
|
1824
|
-
}
|
|
1825
|
-
if (declaredFiles !== null)
|
|
1826
|
-
return null;
|
|
1827
|
-
declaredFiles = [...record.files_touched];
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
return declaredFiles;
|
|
1831
|
-
} catch {
|
|
1832
|
-
return null;
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
var TASK_GATE_REQUIREMENTS_RECONSTRUCTION_SENTINEL = "requirements_reconstruction";
|
|
1836
|
-
function assertTaskEvidenceWriteAllowed(directory, taskId, event) {
|
|
1837
|
-
assertValidTaskId(taskId);
|
|
1838
|
-
const coderWalPath = validateSwarmPath(directory, `coder-settlements/${taskId}.json`);
|
|
1839
|
-
const coderWal = readWorkflowWalFileSync("coder-settlement", coderWalPath, taskId);
|
|
1840
|
-
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"))) {
|
|
1841
|
-
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.`);
|
|
1842
|
-
}
|
|
1843
|
-
const terminalWalPath = validateSwarmPath(directory, `task-terminals/${taskId}.json`);
|
|
1844
|
-
const terminalWal = readWorkflowWalFileSync("task-terminal", terminalWalPath, taskId);
|
|
1845
|
-
if (terminalWal?.state === "PREPARED") {
|
|
1846
|
-
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");
|
|
1847
|
-
if (!matchingTerminalEvent) {
|
|
1848
|
-
throw new Error(`TASK_TERMINAL_PREPARED: transition ${String(terminalWal.transitionId)} owns evidence for task ${taskId}`);
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
const repairWalPath = validateSwarmPath(directory, `task-repairs/${taskId}.json`);
|
|
1852
|
-
const repairWal = readWorkflowWalFileSync("task-repair", repairWalPath, taskId);
|
|
1853
|
-
if (repairWal?.state === "PREPARED" && !(event?.type === "repair_idle" && event.transitionId === repairWal.transitionId)) {
|
|
1854
|
-
throw new Error(`TASK_REPAIR_PREPARED: transition ${String(repairWal.transitionId)} owns evidence for task ${taskId}`);
|
|
1855
|
-
}
|
|
1856
|
-
}
|
|
1857
|
-
var GateEvidenceSchema = exports_external.object({
|
|
1858
|
-
sessionId: exports_external.string(),
|
|
1859
|
-
timestamp: exports_external.string(),
|
|
1860
|
-
agent: exports_external.string()
|
|
1861
|
-
}).passthrough();
|
|
1862
|
-
var ExternalEvidenceBundleSchema = exports_external.object({
|
|
1863
|
-
class: exports_external.literal("external_git"),
|
|
1864
|
-
transitionId: exports_external.string().min(1).max(512),
|
|
1865
|
-
declarationDigest: exports_external.string().regex(/^[a-f0-9]{64}$/),
|
|
1866
|
-
anchors: exports_external.array(exports_external.object({
|
|
1867
|
-
command: exports_external.string().min(1).max(400),
|
|
1868
|
-
expect: exports_external.string().max(4096).optional(),
|
|
1869
|
-
expect_prefix: exports_external.string().min(1).max(4096).optional(),
|
|
1870
|
-
matched: exports_external.boolean(),
|
|
1871
|
-
exit_code: exports_external.number().int(),
|
|
1872
|
-
stdout: exports_external.string().max(4096)
|
|
1873
|
-
})).min(1).max(16).superRefine((anchors, ctx) => {
|
|
1874
|
-
anchors.forEach((anchor, index) => {
|
|
1875
|
-
if (anchor.expect === undefined && anchor.expect_prefix === undefined) {
|
|
1876
|
-
ctx.addIssue({
|
|
1877
|
-
code: exports_external.ZodIssueCode.custom,
|
|
1878
|
-
message: `anchor ${index} must carry expect and/or a non-empty expect_prefix`,
|
|
1879
|
-
path: [index]
|
|
1880
|
-
});
|
|
1881
|
-
}
|
|
1882
|
-
});
|
|
1883
|
-
}),
|
|
1884
|
-
attestedAt: exports_external.string().min(1)
|
|
1885
|
-
});
|
|
1886
|
-
var TaskWorkflowMetadataSchema = exports_external.object({
|
|
1887
|
-
schema: exports_external.literal(TASK_WORKFLOW_SCHEMA_MARKER),
|
|
1888
|
-
generation: exports_external.number().int().min(0),
|
|
1889
|
-
state: exports_external.enum(TASK_WORKFLOW_STATES),
|
|
1890
|
-
retryCount: exports_external.number().int().min(0).default(0),
|
|
1891
|
-
retryHistory: exports_external.array(exports_external.enum(TASK_WORKFLOW_OUTCOMES)).max(3).default([]),
|
|
1892
|
-
retryEpoch: exports_external.number().int().min(0).default(0),
|
|
1893
|
-
lastOutcome: exports_external.enum(TASK_WORKFLOW_OUTCOMES).default("none"),
|
|
1894
|
-
lastTransitionId: exports_external.string().min(1).nullable().optional().default(null),
|
|
1895
|
-
updatedAt: exports_external.string(),
|
|
1896
|
-
forcedCompletion: exports_external.boolean().optional(),
|
|
1897
|
-
supervisedRecovery: exports_external.boolean().optional(),
|
|
1898
|
-
settlementRecovery: exports_external.boolean().optional(),
|
|
1899
|
-
noMutationSettlement: exports_external.object({
|
|
1900
|
-
generation: exports_external.literal(0),
|
|
1901
|
-
transitionId: exports_external.string().min(1).max(512),
|
|
1902
|
-
declaredFiles: exports_external.array(exports_external.string()).length(0)
|
|
1903
|
-
}).optional(),
|
|
1904
|
-
externalEvidence: ExternalEvidenceBundleSchema.optional()
|
|
1905
|
-
});
|
|
1906
|
-
var TaskEvidenceSchema = exports_external.object({
|
|
1907
|
-
taskId: exports_external.string(),
|
|
1908
|
-
required_gates: exports_external.array(exports_external.string()).default([]),
|
|
1909
|
-
gates: exports_external.record(exports_external.string(), GateEvidenceSchema),
|
|
1910
|
-
turbo: exports_external.boolean().optional(),
|
|
1911
|
-
requirements_state: exports_external.enum(["known", "unknown"]).optional(),
|
|
1912
|
-
test_engineer_exempt: exports_external.boolean().optional(),
|
|
1913
|
-
planId: exports_external.string().optional(),
|
|
1914
|
-
planIdentityHash: exports_external.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
1915
|
-
workflow: TaskWorkflowMetadataSchema.optional(),
|
|
1916
|
-
repair_provenance: exports_external.object({
|
|
1917
|
-
source_sha256: exports_external.string().regex(/^[a-f0-9]{64}$/).nullable(),
|
|
1918
|
-
source_generation: exports_external.number().int().min(0).nullable(),
|
|
1919
|
-
requirements_receipt_hash: exports_external.string().regex(/^[a-f0-9]{64}$/).nullable()
|
|
1920
|
-
}).optional(),
|
|
1921
|
-
todo_scan: exports_external.object({
|
|
1922
|
-
priority: exports_external.string(),
|
|
1923
|
-
count: exports_external.number().int().min(0),
|
|
1924
|
-
details: exports_external.array(exports_external.string()).max(200).optional(),
|
|
1925
|
-
recorded_at: exports_external.string().optional()
|
|
1926
|
-
}).optional()
|
|
1927
|
-
});
|
|
1928
|
-
function parseTaskEvidence(raw, expectedTaskId) {
|
|
1929
|
-
const parsed = TaskEvidenceSchema.parse(JSON.parse(raw));
|
|
1930
|
-
if (parsed.taskId !== expectedTaskId) {
|
|
1931
|
-
throw new Error(`TASK_EVIDENCE_IDENTITY_MISMATCH: expected ${expectedTaskId}, found ${parsed.taskId}`);
|
|
1932
|
-
}
|
|
1933
|
-
return parsed;
|
|
1934
|
-
}
|
|
1935
|
-
function resolveCurrentPlanIdentitySync(directory) {
|
|
1936
|
-
if (!directory)
|
|
1937
|
-
return null;
|
|
1938
|
-
try {
|
|
1939
|
-
const planPath = path4.join(directory, ".swarm", "plan.json");
|
|
1940
|
-
const raw = readFileSync3(planPath, "utf8");
|
|
1941
|
-
const plan = JSON.parse(raw);
|
|
1942
|
-
if (typeof plan.swarm !== "string" || typeof plan.title !== "string")
|
|
1943
|
-
return null;
|
|
1944
|
-
return {
|
|
1945
|
-
planId: derivePlanId(plan),
|
|
1946
|
-
planIdentityHash: derivePlanIdentityHash(plan)
|
|
1947
|
-
};
|
|
1948
|
-
} catch {
|
|
1949
|
-
return null;
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
1952
|
-
function evidenceStampedForDifferentPlan(evidence, currentPlanIdentityHash) {
|
|
1953
|
-
if (evidence == null || currentPlanIdentityHash == null)
|
|
1954
|
-
return false;
|
|
1955
|
-
return typeof evidence.planIdentityHash === "string" && evidence.planIdentityHash !== currentPlanIdentityHash;
|
|
1956
|
-
}
|
|
1957
|
-
function planStampForPersist(nextEvidence, previous, planIdentity, campaignRestart) {
|
|
1958
|
-
if (nextEvidence.planIdentityHash !== undefined)
|
|
1959
|
-
return {};
|
|
1960
|
-
const carriesGates = Object.keys(nextEvidence.gates ?? {}).length > 0;
|
|
1961
|
-
if (carriesGates && previous != null && !campaignRestart) {
|
|
1962
|
-
return previous.planIdentityHash !== undefined ? {
|
|
1963
|
-
...previous.planId !== undefined ? { planId: previous.planId } : {},
|
|
1964
|
-
planIdentityHash: previous.planIdentityHash
|
|
1965
|
-
} : {};
|
|
1966
|
-
}
|
|
1967
|
-
return planIdentity ? {
|
|
1968
|
-
planId: planIdentity.planId,
|
|
1969
|
-
planIdentityHash: planIdentity.planIdentityHash
|
|
1970
|
-
} : {};
|
|
1971
|
-
}
|
|
1972
|
-
var DEFAULT_REQUIRED_GATES = ["reviewer", "test_engineer"];
|
|
1973
|
-
var DEFAULT_WORKFLOW_STATE = "idle";
|
|
1974
|
-
var WORKFLOW_STATE_RANK = {
|
|
1975
|
-
idle: 0,
|
|
1976
|
-
coder_delegated: 1,
|
|
1977
|
-
pre_check_passed: 2,
|
|
1978
|
-
reviewer_run: 3,
|
|
1979
|
-
tests_run: 4,
|
|
1980
|
-
rework_required: 5,
|
|
1981
|
-
blocked: 6,
|
|
1982
|
-
closed: 7,
|
|
1983
|
-
complete: 8
|
|
1984
|
-
};
|
|
1985
|
-
function isValidTaskId(taskId) {
|
|
1986
|
-
return isStrictTaskId(taskId);
|
|
1987
|
-
}
|
|
1988
|
-
function assertValidTaskId(taskId) {
|
|
1989
|
-
assertStrictTaskId(taskId);
|
|
1990
|
-
}
|
|
1991
|
-
function createDefaultWorkflowMetadata(nowIso) {
|
|
1992
|
-
return {
|
|
1993
|
-
schema: TASK_WORKFLOW_SCHEMA_MARKER,
|
|
1994
|
-
generation: 0,
|
|
1995
|
-
state: DEFAULT_WORKFLOW_STATE,
|
|
1996
|
-
retryCount: 0,
|
|
1997
|
-
retryHistory: [],
|
|
1998
|
-
retryEpoch: 0,
|
|
1999
|
-
lastOutcome: "none",
|
|
2000
|
-
lastTransitionId: null,
|
|
2001
|
-
updatedAt: nowIso
|
|
2002
|
-
};
|
|
2003
|
-
}
|
|
2004
|
-
function getEventOutcome(event) {
|
|
2005
|
-
switch (event.type) {
|
|
2006
|
-
case "dispatch_attempted":
|
|
2007
|
-
return "dispatch_attempted";
|
|
2008
|
-
case "dispatch_no_mutation":
|
|
2009
|
-
return "dispatch_no_mutation";
|
|
2010
|
-
case "accepted_mutation":
|
|
2011
|
-
return event.context?.settlementFailed === true ? "accepted_mutation_failed" : "accepted_mutation";
|
|
2012
|
-
case "stage_a_passed":
|
|
2013
|
-
return "stage_a_passed";
|
|
2014
|
-
case "stage_a_failed":
|
|
2015
|
-
return "stage_a_failed";
|
|
2016
|
-
case "stage_b_completed":
|
|
2017
|
-
return "stage_b_completed";
|
|
2018
|
-
case "stage_b_failed":
|
|
2019
|
-
return "stage_b_failed";
|
|
2020
|
-
case "gate_recorded":
|
|
2021
|
-
return "gate_recorded";
|
|
2022
|
-
case "task_completed":
|
|
2023
|
-
return "task_completed";
|
|
2024
|
-
case "task_blocked":
|
|
2025
|
-
return "task_blocked";
|
|
2026
|
-
case "task_closed":
|
|
2027
|
-
return "task_closed";
|
|
2028
|
-
case "repair_idle":
|
|
2029
|
-
return "repair_idle";
|
|
2030
|
-
case "external_evidence_attested":
|
|
2031
|
-
return "external_evidence_attested";
|
|
2032
|
-
case "external_evidence_revoked":
|
|
2033
|
-
return "external_evidence_revoked";
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
function isAuthoritativeWorkflowMetadata(workflow) {
|
|
2037
|
-
return workflow?.schema === TASK_WORKFLOW_SCHEMA_MARKER;
|
|
2038
|
-
}
|
|
2039
|
-
function getTaskWorkflowSnapshot(evidence) {
|
|
2040
|
-
if (isAuthoritativeWorkflowMetadata(evidence?.workflow)) {
|
|
2041
|
-
return { ...evidence.workflow, authoritative: true };
|
|
2042
|
-
}
|
|
2043
|
-
return {
|
|
2044
|
-
...createDefaultWorkflowMetadata(""),
|
|
2045
|
-
authoritative: false
|
|
2046
|
-
};
|
|
2047
|
-
}
|
|
2048
|
-
function maybeExpandRequiredGates(existing, event) {
|
|
2049
|
-
if (event.type === "accepted_mutation") {
|
|
2050
|
-
const agentType = event.agentType;
|
|
2051
|
-
if (typeof agentType === "string" && agentType.trim() !== "") {
|
|
2052
|
-
return existing ? expandRequiredGates(existing.required_gates.filter((gate) => gate !== TASK_GATE_REQUIREMENTS_RECONSTRUCTION_SENTINEL), agentType, event.context ?? {}) : deriveRequiredGates(agentType, event.context ?? {});
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
if (event.type === "stage_b_completed" && event.ensureDefaultStageB === true) {
|
|
2056
|
-
return expandRequiredGates(existing?.required_gates ?? [], "coder", {
|
|
2057
|
-
testEngineerExempt: false
|
|
2058
|
-
});
|
|
2059
|
-
}
|
|
2060
|
-
if (event.type === "stage_b_completed" || event.type === "gate_recorded") {
|
|
2061
|
-
return existing ? expandRequiredGates(existing.required_gates, event.gate) : deriveRequiredGates(event.gate);
|
|
2062
|
-
}
|
|
2063
|
-
return existing?.required_gates ?? [];
|
|
2064
|
-
}
|
|
2065
|
-
function hasAllRequiredGatesPassed(requiredGates, gates) {
|
|
2066
|
-
return requiredGates.length > 0 && requiredGates.every((gate) => gates[gate] != null);
|
|
2067
|
-
}
|
|
2068
|
-
function shouldTreatGateAsStageB(gate) {
|
|
2069
|
-
return gate === "reviewer" || gate === "test_engineer";
|
|
2070
|
-
}
|
|
2071
|
-
function clearWorkflowGateProof(gates) {
|
|
2072
|
-
const retained = { ...gates };
|
|
2073
|
-
delete retained.pre_check;
|
|
2074
|
-
delete retained.reviewer;
|
|
2075
|
-
delete retained.test_engineer;
|
|
2076
|
-
delete retained.council;
|
|
2077
|
-
delete retained.critic;
|
|
2078
|
-
delete retained.critic_sounding_board;
|
|
2079
|
-
return retained;
|
|
2080
|
-
}
|
|
2081
|
-
function assertExpectedGeneration(snapshot, event) {
|
|
2082
|
-
if (typeof event.expectedGeneration === "number" && event.expectedGeneration !== snapshot.generation) {
|
|
2083
|
-
throw new Error(`TASK_WORKFLOW_GENERATION_MISMATCH: expected ${event.expectedGeneration}, current ${snapshot.generation}`);
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
function isDuplicateTransition(snapshot, event) {
|
|
2087
|
-
if (event.type === "dispatch_no_mutation" && event.context?.declaredFiles?.length === 0 && event.context.settlementTransitionId === event.transitionId && !snapshot.noMutationSettlement) {
|
|
2088
|
-
return false;
|
|
2089
|
-
}
|
|
2090
|
-
return snapshot.authoritative && typeof event.transitionId === "string" && event.transitionId.length > 0 && snapshot.lastTransitionId === event.transitionId && snapshot.lastOutcome === getEventOutcome(event);
|
|
2091
|
-
}
|
|
2092
|
-
function isNoMutationSettlementMetadata(workflow) {
|
|
2093
|
-
const settlement = workflow.noMutationSettlement;
|
|
2094
|
-
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)
|
|
2095
|
-
return false;
|
|
2096
|
-
return workflow.state === "idle" || workflow.state === "complete";
|
|
2097
|
-
}
|
|
2098
|
-
function deriveApplicableGateSet(evidence, options) {
|
|
2099
|
-
if (!evidence) {
|
|
2100
|
-
return {
|
|
2101
|
-
requiredGates: ["pre_check"],
|
|
2102
|
-
satisfiedGates: [],
|
|
2103
|
-
missingGates: ["pre_check"],
|
|
2104
|
-
readOnlyNoMutation: false
|
|
2105
|
-
};
|
|
2106
|
-
}
|
|
2107
|
-
const workflow = getTaskWorkflowSnapshot(evidence);
|
|
2108
|
-
const readOnlyNoMutation = isNoMutationSettlementMetadata(workflow) && (options?.currentDeclaredFiles === undefined ? true : options.currentDeclaredFiles !== null && options.currentDeclaredFiles.length === 0);
|
|
2109
|
-
const externalEvidenceMatched = workflow.lastOutcome === "external_evidence_attested" && (workflow.externalEvidence?.anchors.length ?? 0) > 0 && workflow.externalEvidence.anchors.every((anchor) => anchor.matched);
|
|
2110
|
-
const requiredGates = [...new Set(evidence.required_gates ?? [])];
|
|
2111
|
-
if (!readOnlyNoMutation && !externalEvidenceMatched && !requiredGates.includes("pre_check")) {
|
|
2112
|
-
requiredGates.unshift("pre_check");
|
|
2113
|
-
}
|
|
2114
|
-
const satisfiedGates = requiredGates.filter((gate) => evidence.gates?.[gate] != null);
|
|
2115
|
-
return {
|
|
2116
|
-
requiredGates,
|
|
2117
|
-
satisfiedGates,
|
|
2118
|
-
missingGates: requiredGates.filter((gate) => evidence.gates?.[gate] == null),
|
|
2119
|
-
readOnlyNoMutation
|
|
2120
|
-
};
|
|
2121
|
-
}
|
|
2122
|
-
function isReadOnlyNoMutationEligible(evidence, options) {
|
|
2123
|
-
return deriveApplicableGateSet(evidence, options).readOnlyNoMutation;
|
|
2124
|
-
}
|
|
2125
|
-
function computeExternalDeclarationDigest(input) {
|
|
2126
|
-
const canonical = {
|
|
2127
|
-
taskId: input.taskId,
|
|
2128
|
-
evidence_class: input.evidence_class,
|
|
2129
|
-
evidence_anchors: input.evidence_anchors.map((anchor) => ({
|
|
2130
|
-
command: anchor.command,
|
|
2131
|
-
expect: anchor.expect,
|
|
2132
|
-
expect_prefix: anchor.expect_prefix
|
|
2133
|
-
})),
|
|
2134
|
-
plan_title: input.plan_title,
|
|
2135
|
-
plan_swarm: input.plan_swarm,
|
|
2136
|
-
generation: input.generation
|
|
2137
|
-
};
|
|
2138
|
-
return createHash2("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
2139
|
-
}
|
|
2140
|
-
function externalProofCurrent(plan, planTask, evidence) {
|
|
2141
|
-
if (planTask.evidence_class !== "external_git")
|
|
2142
|
-
return false;
|
|
2143
|
-
const workflow = evidence?.workflow;
|
|
2144
|
-
if (!workflow)
|
|
2145
|
-
return false;
|
|
2146
|
-
const bundle = workflow.externalEvidence;
|
|
2147
|
-
if (!bundle || bundle.anchors.length === 0)
|
|
2148
|
-
return false;
|
|
2149
|
-
if (!bundle.anchors.every((anchor) => anchor.matched))
|
|
2150
|
-
return false;
|
|
2151
|
-
return bundle.declarationDigest === computeExternalDeclarationDigest({
|
|
2152
|
-
taskId: planTask.id,
|
|
2153
|
-
evidence_class: "external_git",
|
|
2154
|
-
evidence_anchors: planTask.evidence_anchors ?? [],
|
|
2155
|
-
plan_title: plan.title,
|
|
2156
|
-
plan_swarm: plan.swarm,
|
|
2157
|
-
generation: workflow.generation
|
|
2158
|
-
});
|
|
2159
|
-
}
|
|
2160
|
-
function isExternalEvidenceInitialCompletionEligible(plan, planTask, evidence) {
|
|
2161
|
-
if (!externalProofCurrent(plan, planTask, evidence))
|
|
2162
|
-
return false;
|
|
2163
|
-
const snapshot = getTaskWorkflowSnapshot(evidence);
|
|
2164
|
-
return snapshot.authoritative && snapshot.state === "idle";
|
|
2165
|
-
}
|
|
2166
|
-
function reduceTaskWorkflowSnapshot(current, event, context) {
|
|
2167
|
-
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)) {
|
|
2168
|
-
throw new Error(`TASK_WORKFLOW_TERMINAL: cannot apply ${event.type} from ${current.state}`);
|
|
2169
|
-
}
|
|
2170
|
-
const outcome = getEventOutcome(event);
|
|
2171
|
-
const base = {
|
|
2172
|
-
schema: TASK_WORKFLOW_SCHEMA_MARKER,
|
|
2173
|
-
generation: current.generation,
|
|
2174
|
-
state: current.state,
|
|
2175
|
-
retryCount: current.retryCount,
|
|
2176
|
-
retryHistory: current.retryHistory,
|
|
2177
|
-
retryEpoch: current.retryEpoch,
|
|
2178
|
-
lastOutcome: outcome,
|
|
2179
|
-
lastTransitionId: event.transitionId ?? current.lastTransitionId,
|
|
2180
|
-
updatedAt: context.nowIso,
|
|
2181
|
-
...current.forcedCompletion === true ? { forcedCompletion: true } : {},
|
|
2182
|
-
...current.supervisedRecovery === true ? { supervisedRecovery: true } : {},
|
|
2183
|
-
...current.settlementRecovery === true ? { settlementRecovery: true } : {},
|
|
2184
|
-
...current.noMutationSettlement ? { noMutationSettlement: current.noMutationSettlement } : {},
|
|
2185
|
-
...current.externalEvidence ? { externalEvidence: current.externalEvidence } : {}
|
|
2186
|
-
};
|
|
2187
|
-
switch (event.type) {
|
|
2188
|
-
case "dispatch_attempted": {
|
|
2189
|
-
const {
|
|
2190
|
-
noMutationSettlement: _clearedSettlement,
|
|
2191
|
-
externalEvidence: _clearedExternalEvidence,
|
|
2192
|
-
...withoutSettlement
|
|
2193
|
-
} = base;
|
|
2194
|
-
return withoutSettlement;
|
|
2195
|
-
}
|
|
2196
|
-
case "dispatch_no_mutation": {
|
|
2197
|
-
const {
|
|
2198
|
-
noMutationSettlement: _clearedSettlement,
|
|
2199
|
-
externalEvidence: _clearedExternalEvidence,
|
|
2200
|
-
...withoutSettlement
|
|
2201
|
-
} = base;
|
|
2202
|
-
return {
|
|
2203
|
-
...withoutSettlement,
|
|
2204
|
-
retryCount: Math.min(current.retryCount + 1, 3),
|
|
2205
|
-
retryHistory: [...current.retryHistory, outcome].slice(-3),
|
|
2206
|
-
retryEpoch: current.retryEpoch || current.generation + 1,
|
|
2207
|
-
...current.generation === 0 && event.transitionId && Array.isArray(event.context?.declaredFiles) && event.context.declaredFiles.length === 0 && event.context.settlementTransitionId === event.transitionId ? {
|
|
2208
|
-
noMutationSettlement: {
|
|
2209
|
-
generation: 0,
|
|
2210
|
-
transitionId: event.transitionId,
|
|
2211
|
-
declaredFiles: []
|
|
2212
|
-
}
|
|
2213
|
-
} : {}
|
|
2214
|
-
};
|
|
2215
|
-
}
|
|
2216
|
-
case "stage_b_failed":
|
|
2217
|
-
if (current.state !== "pre_check_passed" && current.state !== "reviewer_run" && current.state !== "tests_run" && current.state !== "rework_required") {
|
|
2218
|
-
throw new Error(`TASK_WORKFLOW_STAGE_A_REQUIRED: cannot reject ${event.gate} from ${current.state}`);
|
|
2219
|
-
}
|
|
2220
|
-
return {
|
|
2221
|
-
...base,
|
|
2222
|
-
state: "rework_required",
|
|
2223
|
-
retryCount: Math.min(current.retryCount + 1, 3),
|
|
2224
|
-
retryHistory: [...current.retryHistory, outcome].slice(-3),
|
|
2225
|
-
retryEpoch: current.retryEpoch || current.generation + 1
|
|
2226
|
-
};
|
|
2227
|
-
case "accepted_mutation": {
|
|
2228
|
-
const {
|
|
2229
|
-
noMutationSettlement: _clearedSettlement,
|
|
2230
|
-
externalEvidence: _clearedExternalEvidence,
|
|
2231
|
-
...withoutSettlement
|
|
2232
|
-
} = base;
|
|
2233
|
-
if (event.context?.settlementFailed === true) {
|
|
2234
|
-
return {
|
|
2235
|
-
...withoutSettlement,
|
|
2236
|
-
generation: current.generation + 1,
|
|
2237
|
-
state: "rework_required",
|
|
2238
|
-
retryCount: Math.min(current.retryCount + 1, 3),
|
|
2239
|
-
retryHistory: [...current.retryHistory, outcome].slice(-3),
|
|
2240
|
-
retryEpoch: current.retryEpoch || current.generation + 1,
|
|
2241
|
-
supervisedRecovery: undefined,
|
|
2242
|
-
settlementRecovery: undefined
|
|
2243
|
-
};
|
|
2244
|
-
}
|
|
2245
|
-
return {
|
|
2246
|
-
...withoutSettlement,
|
|
2247
|
-
generation: current.generation + 1,
|
|
2248
|
-
state: "coder_delegated",
|
|
2249
|
-
supervisedRecovery: undefined,
|
|
2250
|
-
settlementRecovery: undefined
|
|
2251
|
-
};
|
|
2252
|
-
}
|
|
2253
|
-
case "stage_a_passed":
|
|
2254
|
-
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"))) {
|
|
2255
|
-
throw new Error(`TASK_WORKFLOW_CODER_MUTATION_REQUIRED: cannot pass Stage A from ${current.state}`);
|
|
2256
|
-
}
|
|
2257
|
-
return {
|
|
2258
|
-
...base,
|
|
2259
|
-
...event.supervisedRecovery === true ? { supervisedRecovery: true } : {},
|
|
2260
|
-
...event.settlementRecovery === true ? { settlementRecovery: true } : {},
|
|
2261
|
-
state: "pre_check_passed"
|
|
2262
|
-
};
|
|
2263
|
-
case "stage_a_failed":
|
|
2264
|
-
if (current.state !== "coder_delegated" && current.state !== "pre_check_passed" && current.state !== "rework_required") {
|
|
2265
|
-
throw new Error(`TASK_WORKFLOW_CODER_MUTATION_REQUIRED: cannot fail Stage A from ${current.state}`);
|
|
2266
|
-
}
|
|
2267
|
-
return {
|
|
2268
|
-
...base,
|
|
2269
|
-
state: "rework_required",
|
|
2270
|
-
retryCount: Math.min(current.retryCount + 1, 3),
|
|
2271
|
-
retryHistory: [...current.retryHistory, outcome].slice(-3),
|
|
2272
|
-
retryEpoch: current.retryEpoch || current.generation + 1
|
|
2273
|
-
};
|
|
2274
|
-
case "stage_b_completed":
|
|
2275
|
-
if (current.state !== "pre_check_passed" && current.state !== "reviewer_run" && current.state !== "tests_run") {
|
|
2276
|
-
throw new Error(`TASK_WORKFLOW_STAGE_A_REQUIRED: cannot record ${event.gate} from ${current.state}`);
|
|
2277
|
-
}
|
|
2278
|
-
if (event.routeComplete === false) {
|
|
2279
|
-
return {
|
|
2280
|
-
...base,
|
|
2281
|
-
state: event.gate === "reviewer" || current.state === "reviewer_run" ? "reviewer_run" : current.state
|
|
2282
|
-
};
|
|
2283
|
-
}
|
|
2284
|
-
if (event.routeComplete === true) {
|
|
2285
|
-
return { ...base, state: "tests_run" };
|
|
2286
|
-
}
|
|
2287
|
-
return {
|
|
2288
|
-
...base,
|
|
2289
|
-
state: hasAllRequiredGatesPassed(context.requiredGates, context.gates) ? "tests_run" : "reviewer_run"
|
|
2290
|
-
};
|
|
2291
|
-
case "gate_recorded":
|
|
2292
|
-
return {
|
|
2293
|
-
...base,
|
|
2294
|
-
state: current.state
|
|
2295
|
-
};
|
|
2296
|
-
case "external_evidence_attested": {
|
|
2297
|
-
if (current.state !== "idle") {
|
|
2298
|
-
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}`);
|
|
2299
|
-
}
|
|
2300
|
-
if (current.externalEvidence?.transitionId === event.bundle.transitionId) {
|
|
2301
|
-
return { ...base };
|
|
2302
|
-
}
|
|
2303
|
-
return { ...base, externalEvidence: event.bundle };
|
|
2304
|
-
}
|
|
2305
|
-
case "external_evidence_revoked": {
|
|
2306
|
-
const {
|
|
2307
|
-
externalEvidence: _revokedExternalEvidence,
|
|
2308
|
-
...withoutExternalEvidence
|
|
2309
|
-
} = base;
|
|
2310
|
-
return withoutExternalEvidence;
|
|
2311
|
-
}
|
|
2312
|
-
case "task_completed": {
|
|
2313
|
-
const readOnlyNoMutation = event.readOnlyNoMutation === true && current.state === "idle" && current.generation === 0 && isNoMutationSettlementMetadata(current) && context.requiredGates.every((gate) => context.gates[gate] != null);
|
|
2314
|
-
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");
|
|
2315
|
-
if (event.qaExempt !== true && current.state !== "tests_run" && current.state !== "complete" && !(current.state === "pre_check_passed" && context.gates.council != null) && !readOnlyNoMutation && !externalAdmitted) {
|
|
2316
|
-
throw new Error(`TASK_WORKFLOW_QA_REQUIRED: cannot complete from ${current.state}`);
|
|
2317
|
-
}
|
|
2318
|
-
return {
|
|
2319
|
-
...base,
|
|
2320
|
-
state: "complete",
|
|
2321
|
-
...event.qaExempt === true ? { forcedCompletion: true } : {}
|
|
2322
|
-
};
|
|
2323
|
-
}
|
|
2324
|
-
case "task_blocked":
|
|
2325
|
-
return {
|
|
2326
|
-
...base,
|
|
2327
|
-
state: "blocked"
|
|
2328
|
-
};
|
|
2329
|
-
case "task_closed":
|
|
2330
|
-
return {
|
|
2331
|
-
...base,
|
|
2332
|
-
state: "closed"
|
|
2333
|
-
};
|
|
2334
|
-
case "repair_idle": {
|
|
2335
|
-
const {
|
|
2336
|
-
forcedCompletion: _cleared,
|
|
2337
|
-
supervisedRecovery: _clearedMarker,
|
|
2338
|
-
settlementRecovery: _clearedSettlementMarker,
|
|
2339
|
-
noMutationSettlement: _clearedSettlement,
|
|
2340
|
-
externalEvidence: _clearedExternalEvidence,
|
|
2341
|
-
...withoutForced
|
|
2342
|
-
} = base;
|
|
2343
|
-
return {
|
|
2344
|
-
...withoutForced,
|
|
2345
|
-
generation: current.generation + 1,
|
|
2346
|
-
state: "idle",
|
|
2347
|
-
retryCount: 0,
|
|
2348
|
-
retryHistory: [],
|
|
2349
|
-
retryEpoch: 0
|
|
2350
|
-
};
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
function getEvidenceDir(directory) {
|
|
2355
|
-
const swarmDir = path4.resolve(directory, ".swarm");
|
|
2356
|
-
const evidenceDir = path4.join(swarmDir, "evidence");
|
|
2357
|
-
mkdirSync3(evidenceDir, { recursive: true });
|
|
2358
|
-
let resolvedSwarmDir;
|
|
2359
|
-
let resolvedEvidenceDir;
|
|
2360
|
-
try {
|
|
2361
|
-
resolvedSwarmDir = path4.normalize(realpathSync2(swarmDir));
|
|
2362
|
-
resolvedEvidenceDir = path4.normalize(realpathSync2(evidenceDir));
|
|
2363
|
-
} catch (error) {
|
|
2364
|
-
throw new Error(`Unable to resolve evidence directory: ${error.message}`);
|
|
2365
|
-
}
|
|
2366
|
-
const swarmPrefix = `${resolvedSwarmDir}${path4.sep}`;
|
|
2367
|
-
const withinSwarmBoundary = process.platform === "win32" ? resolvedEvidenceDir.toLowerCase().startsWith(swarmPrefix.toLowerCase()) : resolvedEvidenceDir.startsWith(swarmPrefix);
|
|
2368
|
-
if (!withinSwarmBoundary) {
|
|
2369
|
-
throw new Error(`Evidence path escapes .swarm boundary: ${resolvedEvidenceDir}`);
|
|
2370
|
-
}
|
|
2371
|
-
return resolvedEvidenceDir;
|
|
2372
|
-
}
|
|
2373
|
-
function getEvidencePath(directory, taskId) {
|
|
2374
|
-
assertValidTaskId(taskId);
|
|
2375
|
-
return taskEvidencePath(directory, taskId);
|
|
2376
|
-
}
|
|
2377
|
-
function bindTrustedNoMutationSettlement(directory, taskId, event) {
|
|
2378
|
-
if (event.type !== "dispatch_no_mutation")
|
|
2379
|
-
return event;
|
|
2380
|
-
const context = event.context;
|
|
2381
|
-
if (!context || event.transitionId === undefined) {
|
|
2382
|
-
return { ...event, context: undefined };
|
|
2383
|
-
}
|
|
2384
|
-
try {
|
|
2385
|
-
const wal = readWorkflowWalFileSync("coder-settlement", validateSwarmPath(directory, `coder-settlements/${taskId}.json`), taskId);
|
|
2386
|
-
const declaredFiles = wal?.context?.declaredFiles;
|
|
2387
|
-
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;
|
|
2388
|
-
return {
|
|
2389
|
-
...event,
|
|
2390
|
-
context: trusted ? {
|
|
2391
|
-
...context,
|
|
2392
|
-
declaredFiles,
|
|
2393
|
-
settlementTransitionId: wal.transitionId
|
|
2394
|
-
} : { ...context, settlementTransitionId: undefined }
|
|
2395
|
-
};
|
|
2396
|
-
} catch {
|
|
2397
|
-
return {
|
|
2398
|
-
...event,
|
|
2399
|
-
context: { ...context, settlementTransitionId: undefined }
|
|
2400
|
-
};
|
|
2401
|
-
}
|
|
2402
|
-
}
|
|
2403
|
-
function readExisting(evidencePath, taskId) {
|
|
2404
|
-
try {
|
|
2405
|
-
const raw = readFileSync3(evidencePath, "utf-8");
|
|
2406
|
-
return parseTaskEvidence(raw, taskId);
|
|
2407
|
-
} catch (error) {
|
|
2408
|
-
if (error.code === "ENOENT")
|
|
2409
|
-
return null;
|
|
2410
|
-
telemetry.gateParseError(taskId, error);
|
|
2411
|
-
throw error;
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
function deriveRequiredGates(agentType, context = {}) {
|
|
2415
|
-
switch (agentType) {
|
|
2416
|
-
case "coder":
|
|
2417
|
-
return context.testEngineerExempt === true ? ["reviewer"] : ["reviewer", "test_engineer"];
|
|
2418
|
-
case "docs":
|
|
2419
|
-
return ["docs"];
|
|
2420
|
-
case "designer":
|
|
2421
|
-
return ["designer", "reviewer", "test_engineer"];
|
|
2422
|
-
case "explorer":
|
|
2423
|
-
return ["explorer"];
|
|
2424
|
-
case "sme":
|
|
2425
|
-
return ["sme"];
|
|
2426
|
-
case "reviewer":
|
|
2427
|
-
return ["reviewer"];
|
|
2428
|
-
case "test_engineer":
|
|
2429
|
-
return ["test_engineer"];
|
|
2430
|
-
case "critic":
|
|
2431
|
-
return ["critic"];
|
|
2432
|
-
case "critic_sounding_board":
|
|
2433
|
-
return ["critic_sounding_board"];
|
|
2434
|
-
case "critic_drift_verifier":
|
|
2435
|
-
return ["critic_drift_verifier"];
|
|
2436
|
-
case "critic_hallucination_verifier":
|
|
2437
|
-
return ["critic_hallucination_verifier"];
|
|
2438
|
-
case "critic_architecture_supervisor":
|
|
2439
|
-
return ["critic_architecture_supervisor"];
|
|
2440
|
-
default:
|
|
2441
|
-
return ["reviewer", "test_engineer"];
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
function expandRequiredGates(existingGates, newAgentType, context = {}) {
|
|
2445
|
-
const newGates = deriveRequiredGates(newAgentType, context);
|
|
2446
|
-
const combined = [...new Set([...existingGates ?? [], ...newGates])];
|
|
2447
|
-
return combined.sort();
|
|
2448
|
-
}
|
|
2449
|
-
function updateEvidenceForTransition(existing, event) {
|
|
2450
|
-
const nowIso = new Date().toISOString();
|
|
2451
|
-
const requiredGates = maybeExpandRequiredGates(existing, event);
|
|
2452
|
-
let gates = {
|
|
2453
|
-
...existing?.gates ?? {}
|
|
2454
|
-
};
|
|
2455
|
-
if (event.type === "accepted_mutation" || event.type === "repair_idle") {
|
|
2456
|
-
gates = clearWorkflowGateProof(gates);
|
|
2457
|
-
}
|
|
2458
|
-
if (event.type === "stage_a_passed") {
|
|
2459
|
-
gates = {
|
|
2460
|
-
...gates,
|
|
2461
|
-
pre_check: {
|
|
2462
|
-
sessionId: "system",
|
|
2463
|
-
timestamp: nowIso,
|
|
2464
|
-
agent: "pre_check"
|
|
2465
|
-
}
|
|
2466
|
-
};
|
|
2467
|
-
}
|
|
2468
|
-
if (event.type === "stage_a_failed") {
|
|
2469
|
-
delete gates.pre_check;
|
|
2470
|
-
}
|
|
2471
|
-
if (event.type === "stage_b_failed") {
|
|
2472
|
-
delete gates.reviewer;
|
|
2473
|
-
delete gates.test_engineer;
|
|
2474
|
-
}
|
|
2475
|
-
if (event.type === "stage_b_completed" || event.type === "gate_recorded") {
|
|
2476
|
-
gates = {
|
|
2477
|
-
...gates,
|
|
2478
|
-
[event.gate]: {
|
|
2479
|
-
sessionId: event.sessionId,
|
|
2480
|
-
timestamp: nowIso,
|
|
2481
|
-
agent: event.gate
|
|
2482
|
-
}
|
|
2483
|
-
};
|
|
2484
|
-
}
|
|
2485
|
-
const snapshot = getTaskWorkflowSnapshot(existing);
|
|
2486
|
-
assertExpectedGeneration(snapshot, event);
|
|
2487
|
-
if (isDuplicateTransition(snapshot, event) && existing) {
|
|
2488
|
-
return existing;
|
|
2489
|
-
}
|
|
2490
|
-
const workflow = reduceTaskWorkflowSnapshot(snapshot, event, {
|
|
2491
|
-
requiredGates,
|
|
2492
|
-
gates,
|
|
2493
|
-
nowIso
|
|
2494
|
-
});
|
|
2495
|
-
return {
|
|
2496
|
-
taskId: existing?.taskId ?? "",
|
|
2497
|
-
required_gates: requiredGates,
|
|
2498
|
-
gates,
|
|
2499
|
-
turbo: "turbo" in event && event.turbo === true ? true : ("turbo" in event) && event.turbo === false ? existing?.turbo : existing?.turbo,
|
|
2500
|
-
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,
|
|
2501
|
-
workflow,
|
|
2502
|
-
requirements_state: existing?.requirements_state,
|
|
2503
|
-
...existing?.repair_provenance ? { repair_provenance: existing.repair_provenance } : {},
|
|
2504
|
-
...existing?.todo_scan ? { todo_scan: existing.todo_scan } : {}
|
|
2505
|
-
};
|
|
2506
|
-
}
|
|
2507
|
-
async function transitionTaskWorkflowEvidence(directory, taskId, event) {
|
|
2508
|
-
const { evidence } = await transitionTaskWorkflowEvidenceWithStatus(directory, taskId, event);
|
|
2509
|
-
return evidence;
|
|
2510
|
-
}
|
|
2511
|
-
async function transitionTaskWorkflowEvidenceWithStatus(directory, taskId, event) {
|
|
2512
|
-
assertValidTaskId(taskId);
|
|
2513
|
-
let updatedEvidence = null;
|
|
2514
|
-
let duplicated = false;
|
|
2515
|
-
await withTaskEvidenceTransaction(directory, taskId, event.type, async (transaction) => {
|
|
2516
|
-
if (transaction.transitionWithStatus) {
|
|
2517
|
-
const result = await transaction.transitionWithStatus(event);
|
|
2518
|
-
updatedEvidence = result.evidence;
|
|
2519
|
-
duplicated = result.duplicated;
|
|
2520
|
-
return;
|
|
2521
|
-
}
|
|
2522
|
-
updatedEvidence = await transaction.transition(event);
|
|
2523
|
-
});
|
|
2524
|
-
return {
|
|
2525
|
-
evidence: updatedEvidence ?? {
|
|
2526
|
-
taskId,
|
|
2527
|
-
required_gates: [],
|
|
2528
|
-
gates: {},
|
|
2529
|
-
workflow: createDefaultWorkflowMetadata(new Date().toISOString())
|
|
2530
|
-
},
|
|
2531
|
-
duplicated
|
|
2532
|
-
};
|
|
2533
|
-
}
|
|
2534
|
-
async function withTaskEvidenceTransaction(directory, taskId, agent, callback) {
|
|
2535
|
-
assertValidTaskId(taskId);
|
|
2536
|
-
return withTaskEvidenceLock(directory, taskId, agent, async () => {
|
|
2537
|
-
const resolvedEvidenceDir = getEvidenceDir(directory);
|
|
2538
|
-
const evidencePath = path4.join(resolvedEvidenceDir, `${taskId}.json`);
|
|
2539
|
-
let current = null;
|
|
2540
|
-
try {
|
|
2541
|
-
current = readExisting(evidencePath, taskId);
|
|
2542
|
-
} catch (error) {
|
|
2543
|
-
telemetry.gateParseError(taskId, error);
|
|
2544
|
-
throw error;
|
|
2545
|
-
}
|
|
2546
|
-
const persist = async (nextEvidence, event) => {
|
|
2547
|
-
const planIdentity = resolveCurrentPlanIdentitySync(directory);
|
|
2548
|
-
const validated = TaskEvidenceSchema.parse({
|
|
2549
|
-
...nextEvidence,
|
|
2550
|
-
taskId,
|
|
2551
|
-
...planStampForPersist(nextEvidence, current, planIdentity, event?.type === "accepted_mutation" && event.agentType === "coder" && nextEvidence !== current)
|
|
2552
|
-
});
|
|
2553
|
-
await atomicWriteFile(evidencePath, JSON.stringify(validated, null, 2));
|
|
2554
|
-
if (event) {
|
|
2555
|
-
await _internals3.appendTaskGateRequirementsReceiptIfNeeded(directory, taskId, current, validated, event);
|
|
2556
|
-
}
|
|
2557
|
-
current = validated;
|
|
2558
|
-
return validated;
|
|
2559
|
-
};
|
|
2560
|
-
const transitionWithStatusImpl = async (event) => {
|
|
2561
|
-
const boundEvent = bindTrustedNoMutationSettlement(directory, taskId, event);
|
|
2562
|
-
assertTaskEvidenceWriteAllowed(directory, taskId, boundEvent);
|
|
2563
|
-
const nextEvidence = updateEvidenceForTransition(current, boundEvent);
|
|
2564
|
-
const duplicated = nextEvidence === current;
|
|
2565
|
-
nextEvidence.taskId = taskId;
|
|
2566
|
-
const evidence = await persist(nextEvidence, boundEvent);
|
|
2567
|
-
return { evidence, duplicated };
|
|
2568
|
-
};
|
|
2569
|
-
const saveImpl = async (nextEvidence) => {
|
|
2570
|
-
assertTaskEvidenceWriteAllowed(directory, taskId, undefined);
|
|
2571
|
-
nextEvidence.taskId = taskId;
|
|
2572
|
-
return persist(nextEvidence);
|
|
2573
|
-
};
|
|
2574
|
-
return callback({
|
|
2575
|
-
taskId,
|
|
2576
|
-
read: () => current,
|
|
2577
|
-
transition: async (event) => {
|
|
2578
|
-
const { evidence } = await transitionWithStatusImpl(event);
|
|
2579
|
-
return evidence;
|
|
2580
|
-
},
|
|
2581
|
-
transitionWithStatus: transitionWithStatusImpl,
|
|
2582
|
-
save: saveImpl
|
|
2583
|
-
});
|
|
2584
|
-
});
|
|
2585
|
-
}
|
|
2586
|
-
async function ensureTaskGateRequirementsReceiptForSettledTransition(directory, taskId, evidence, event) {
|
|
2587
|
-
assertValidTaskId(taskId);
|
|
2588
|
-
const validated = TaskEvidenceSchema.parse({ ...evidence, taskId });
|
|
2589
|
-
await _internals3.appendTaskGateRequirementsReceiptIfNeeded(directory, taskId, null, validated, event);
|
|
2590
|
-
}
|
|
2591
|
-
async function recordGateEvidence(directory, taskId, gate, sessionId, turbo, options = {}) {
|
|
2592
|
-
assertValidTaskId(taskId);
|
|
2593
|
-
if (shouldTreatGateAsStageB(gate) && typeof options.expectedGeneration !== "number") {
|
|
2594
|
-
throw new Error(`TASK_WORKFLOW_GENERATION_REQUIRED: ${gate} for task ${taskId} must be bound at dispatch`);
|
|
2595
|
-
}
|
|
2596
|
-
await transitionTaskWorkflowEvidence(directory, taskId, shouldTreatGateAsStageB(gate) ? {
|
|
2597
|
-
type: "stage_b_completed",
|
|
2598
|
-
gate,
|
|
2599
|
-
sessionId,
|
|
2600
|
-
routeBinding: options.routeBinding,
|
|
2601
|
-
routeComplete: options.routeComplete,
|
|
2602
|
-
turbo,
|
|
2603
|
-
ensureDefaultStageB: options.ensureDefaultStageB,
|
|
2604
|
-
expectedGeneration: options.expectedGeneration,
|
|
2605
|
-
transitionId: options.transitionId
|
|
2606
|
-
} : {
|
|
2607
|
-
type: "gate_recorded",
|
|
2608
|
-
gate,
|
|
2609
|
-
sessionId,
|
|
2610
|
-
turbo,
|
|
2611
|
-
expectedGeneration: options.expectedGeneration,
|
|
2612
|
-
transitionId: options.transitionId
|
|
2613
|
-
});
|
|
2614
|
-
telemetry.gatePassed(sessionId, gate, taskId);
|
|
2615
|
-
}
|
|
2616
|
-
async function recordTodoScanEvidence(directory, taskId, scan) {
|
|
2617
|
-
assertValidTaskId(taskId);
|
|
2618
|
-
await withTaskEvidenceTransaction(directory, taskId, "todo_scan_recorded", async (transaction) => {
|
|
2619
|
-
const existing = transaction.read();
|
|
2620
|
-
const base = existing ?? {
|
|
2621
|
-
taskId,
|
|
2622
|
-
required_gates: [],
|
|
2623
|
-
gates: {},
|
|
2624
|
-
workflow: createDefaultWorkflowMetadata(new Date().toISOString())
|
|
2625
|
-
};
|
|
2626
|
-
if (!transaction.save) {
|
|
2627
|
-
throw new Error(`TASK_EVIDENCE_SAVE_UNAVAILABLE: transaction for task ${taskId} does not support supplementary writes`);
|
|
2628
|
-
}
|
|
2629
|
-
await transaction.save({ ...base, todo_scan: scan });
|
|
2630
|
-
});
|
|
2631
|
-
}
|
|
2632
|
-
async function recordAgentDispatch(directory, taskId, agentType, turbo, context = {}) {
|
|
2633
|
-
assertValidTaskId(taskId);
|
|
2634
|
-
const expectedGeneration = getTaskWorkflowSnapshot(await readTaskEvidence(directory, taskId)).generation;
|
|
2635
|
-
await transitionTaskWorkflowEvidence(directory, taskId, agentType === "coder" ? {
|
|
2636
|
-
type: "accepted_mutation",
|
|
2637
|
-
agentType,
|
|
2638
|
-
turbo,
|
|
2639
|
-
context,
|
|
2640
|
-
expectedGeneration
|
|
2641
|
-
} : {
|
|
2642
|
-
type: "dispatch_attempted",
|
|
2643
|
-
agentType,
|
|
2644
|
-
turbo,
|
|
2645
|
-
context,
|
|
2646
|
-
expectedGeneration
|
|
2647
|
-
});
|
|
2648
|
-
}
|
|
2649
|
-
async function readTaskEvidence(directory, taskId) {
|
|
2650
|
-
const state = await readTaskEvidenceState(directory, taskId);
|
|
2651
|
-
return state.kind === "ok" ? state.evidence : null;
|
|
2652
|
-
}
|
|
2653
|
-
async function readTaskEvidenceState(directory, taskId) {
|
|
2654
|
-
try {
|
|
2655
|
-
assertValidTaskId(taskId);
|
|
2656
|
-
} catch {
|
|
2657
|
-
return { kind: "missing" };
|
|
2658
|
-
}
|
|
2659
|
-
try {
|
|
2660
|
-
const evidencePath = getEvidencePath(directory, taskId);
|
|
2661
|
-
const raw = readFileSync3(evidencePath, "utf-8");
|
|
2662
|
-
const evidence = parseTaskEvidence(raw, taskId);
|
|
2663
|
-
return { kind: "ok", evidence };
|
|
2664
|
-
} catch (error) {
|
|
2665
|
-
const evidencePath = getEvidencePathSafe(directory, taskId);
|
|
2666
|
-
const code = error.code;
|
|
2667
|
-
if (code === "ENOENT" || code === "ENAMETOOLONG") {
|
|
2668
|
-
return { kind: "missing" };
|
|
2669
|
-
}
|
|
2670
|
-
telemetry.gateParseError(taskId, error);
|
|
2671
|
-
return { kind: "unparseable", evidencePath, error };
|
|
2672
|
-
}
|
|
2673
|
-
}
|
|
2674
|
-
function getEvidencePathSafe(directory, taskId) {
|
|
2675
|
-
try {
|
|
2676
|
-
return getEvidencePath(directory, taskId);
|
|
2677
|
-
} catch {
|
|
2678
|
-
return `${directory}/.swarm/evidence/${taskId}.json`;
|
|
2679
|
-
}
|
|
2680
|
-
}
|
|
2681
|
-
function readTaskEvidenceRaw(directory, taskId) {
|
|
2682
|
-
assertValidTaskId(taskId);
|
|
2683
|
-
const evidencePath = getEvidencePath(directory, taskId);
|
|
2684
|
-
try {
|
|
2685
|
-
const raw = readFileSync3(evidencePath, "utf-8");
|
|
2686
|
-
return parseTaskEvidence(raw, taskId);
|
|
2687
|
-
} catch (error) {
|
|
2688
|
-
const code = error.code;
|
|
2689
|
-
if (code === "ENOENT" || code === "ENAMETOOLONG")
|
|
2690
|
-
return null;
|
|
2691
|
-
throw error;
|
|
2692
|
-
}
|
|
2693
|
-
}
|
|
2694
|
-
async function hasPassedAllGates(directory, taskId, currentDeclaredFiles) {
|
|
2695
|
-
const evidence = await readTaskEvidence(directory, taskId);
|
|
2696
|
-
if (!evidence)
|
|
2697
|
-
return false;
|
|
2698
|
-
if (!Array.isArray(evidence.required_gates))
|
|
2699
|
-
return false;
|
|
2700
|
-
if (currentDeclaredFiles === undefined) {
|
|
2701
|
-
if (deriveApplicableGateSet(evidence).readOnlyNoMutation)
|
|
2702
|
-
return false;
|
|
2703
|
-
return hasAllRequiredGatesPassed(evidence.required_gates, evidence.gates);
|
|
2704
|
-
}
|
|
2705
|
-
return deriveApplicableGateSet(evidence, { currentDeclaredFiles }).missingGates.length === 0;
|
|
2706
|
-
}
|
|
2707
|
-
function compareTaskWorkflowStateRank(left, right) {
|
|
2708
|
-
return WORKFLOW_STATE_RANK[left] - WORKFLOW_STATE_RANK[right];
|
|
2709
|
-
}
|
|
2710
|
-
|
|
2711
|
-
export { readTaskGateRequirementsReceiptsSync, routeEvidenceFromTaskGateRequirements, isLegacyTaskGateRequirementsChain, isStrictTaskId, sanitizeTaskId, validateTaskIdFormat, listRecoveryRecords, recoveryReadErrored, _internals, SOURCE_WORKTREE_GONE_STAGE, SOURCE_WORKTREE_UNCERTAIN_STAGE, getMergeStrategy, recoverMergeBackFromImmutableCoordinates, reconcileLandedMerge, postMergeCleanup, pruneStaleWorktreeMetadata, attemptMergeBackFromDirty, cleanupOrphanedBranches, startupOrphanRecovery, readWorkflowWalFile, writeWorkflowWalFile, _internals3 as _internals1, 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 };
|
|
1946
|
+
export { listRecoveryRecords, recoveryReadErrored, probeSettlementSource, preserveSettlementSource, releaseSettlementSource, validateSettlementLane, _internals2 as _internals, SOURCE_WORKTREE_GONE_STAGE, SOURCE_WORKTREE_UNCERTAIN_STAGE, getMergeStrategy, recoverMergeBackFromImmutableCoordinates, reconcileLandedMerge, postMergeCleanup, pruneStaleWorktreeMetadata, attemptMergeBackFromDirty, cleanupOrphanedBranches, startupOrphanRecovery, _internals3 as _internals1, readWorkflowWalFile, readWorkflowWalFileSync, writeWorkflowWalFile };
|