opencode-swarm 7.199.5 → 7.200.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/binaries/win32-arm64/swarm-sandbox-runner.exe +0 -0
  2. package/binaries/win32-x64/swarm-sandbox-runner.exe +0 -0
  3. package/dist/cli/attest-task-8ekwny8v.js +2209 -0
  4. package/dist/cli/{auto-checkpoint-tje4xvtw.js → auto-checkpoint-dqjk82s7.js} +9 -9
  5. package/dist/cli/{checkpoint-wyh8nkhb.js → checkpoint-2ahps2sp.js} +17 -17
  6. package/dist/cli/{coder-settlement-d0ca5fb3.js → coder-settlement-1dk37pa9.js} +12 -12
  7. package/dist/cli/{config-doctor-swg02esj.js → config-doctor-73rctg85.js} +16 -16
  8. package/dist/cli/{core-yh8ae69m.js → core-y5mfws8v.js} +3 -3
  9. package/dist/cli/{curation-policy-w9y0db1n.js → curation-policy-43gq6t0y.js} +6 -6
  10. package/dist/cli/{curator-drift-85qect05.js → curator-drift-k7j9rcgp.js} +8 -8
  11. package/dist/cli/curator-llm-factory-rke0964p.js +82 -0
  12. package/dist/cli/{curator-9vzqhesg.js → curator-m278hyrq.js} +43 -43
  13. package/dist/cli/{doctor-knowledge-receipt-phase-id-eqzsh3b7.js → doctor-knowledge-receipt-phase-id-1m7pkzvt.js} +9 -9
  14. package/dist/cli/{evidence-summary-service-hc513kyq.js → evidence-summary-service-19k8x1g9.js} +20 -20
  15. package/dist/cli/{gate-evidence-a10epye0.js → gate-evidence-hche0hf8.js} +15 -7
  16. package/dist/cli/guardrail-explain-jg5png4a.js +83 -0
  17. package/dist/cli/{guardrail-log-caspzygc.js → guardrail-log-tvbgnxns.js} +7 -7
  18. package/dist/cli/{guardrail-reset-48r2n8mz.js → guardrail-reset-v8kvh9cn.js} +43 -43
  19. package/dist/cli/{hive-promoter-sd3a61mq.js → hive-promoter-bqfa763z.js} +43 -43
  20. package/dist/cli/{index-hx2f3j9w.js → index-0wg5q595.js} +2 -2
  21. package/dist/cli/{index-6xv2hz0e.js → index-1rfgzsaw.js} +4 -4
  22. package/dist/cli/{index-zbygz217.js → index-1tpz99db.js} +2 -2
  23. package/dist/cli/{index-xtda4xsn.js → index-22atsy06.js} +6 -6
  24. package/dist/cli/{index-gyz59cbh.js → index-35tf03js.js} +21 -14
  25. package/dist/cli/{index-gx2vp8gx.js → index-3t46mez1.js} +27 -1
  26. package/dist/cli/{index-b0kr8720.js → index-511hw14d.js} +47 -45
  27. package/dist/cli/{index-yg5xd4am.js → index-58cr1xg0.js} +172 -14
  28. package/dist/cli/{index-6xyt6d5m.js → index-71bqn1nn.js} +7 -7
  29. package/dist/cli/{index-ybvnf7rt.js → index-7v74n9a1.js} +12 -8
  30. package/dist/cli/{index-4cyc18bc.js → index-8a3aasag.js} +7 -7
  31. package/dist/cli/{index-my753e0g.js → index-8rrs1nc0.js} +3 -3
  32. package/dist/cli/{index-j3rfbs45.js → index-99zt998b.js} +2 -2
  33. package/dist/cli/{index-76skvj8z.js → index-9nmrw7ek.js} +3 -3
  34. package/dist/cli/{index-jmhn6ab5.js → index-ac8vdmgv.js} +2 -2
  35. package/dist/cli/{index-eyq1q1xn.js → index-bp476gwc.js} +2 -2
  36. package/dist/cli/{index-dtwj4hsk.js → index-c2k7q723.js} +2 -2
  37. package/dist/cli/{index-snx2vb1x.js → index-d083d1q7.js} +1 -1
  38. package/dist/cli/{index-n1ddn12j.js → index-dng8z8me.js} +1 -1
  39. package/dist/cli/{index-85vd6sxy.js → index-e8m8bb1r.js} +4 -4
  40. package/dist/cli/{index-3ckvw3j2.js → index-gwc7jahh.js} +1 -1
  41. package/dist/cli/{index-26dmsdx3.js → index-hg4qa0aq.js} +2704 -2208
  42. package/dist/cli/{index-zte3djpf.js → index-hxp5jgd8.js} +2 -2
  43. package/dist/cli/{index-0aj9tvms.js → index-jbm301bq.js} +43 -43
  44. package/dist/cli/{index-0veqvgny.js → index-jy0mgzjq.js} +1 -1
  45. package/dist/cli/{index-esakwcr3.js → index-kwfhr3mz.js} +1 -1
  46. package/dist/cli/{index-0ra0vs2p.js → index-m9j9varx.js} +3 -3
  47. package/dist/cli/{index-3fz5w2wb.js → index-md2180vx.js} +3 -3
  48. package/dist/cli/{index-bgydmx8k.js → index-n5k81wym.js} +7 -7
  49. package/dist/cli/{index-0je0mees.js → index-nq3zcrhm.js} +1 -1
  50. package/dist/cli/{index-xyg7des8.js → index-p5m71yph.js} +4 -4
  51. package/dist/cli/{index-p5d32v0x.js → index-pyv8vzkx.js} +6 -6
  52. package/dist/cli/{index-270zfq1f.js → index-q1x73nnd.js} +6 -6
  53. package/dist/cli/{index-dz15f98h.js → index-q8vfw61p.js} +1 -1
  54. package/dist/cli/{index-10wwrgj2.js → index-qnabg1wp.js} +6 -6
  55. package/dist/cli/{index-9n40q82y.js → index-qnst0jwc.js} +3 -3
  56. package/dist/cli/{index-tsg61z0r.js → index-r02aqt0a.js} +2 -2
  57. package/dist/cli/{index-4pbzepgw.js → index-rfvnq6wd.js} +10 -10
  58. package/dist/cli/{index-9c0vp5yj.js → index-sk1rva12.js} +2 -2
  59. package/dist/cli/{index-v4mremxw.js → index-tv9c3b3q.js} +4 -4
  60. package/dist/cli/{index-9y6yk1n0.js → index-vmevv6my.js} +1 -1
  61. package/dist/cli/{index-zdfzrwbk.js → index-vqtca3xv.js} +2 -2
  62. package/dist/cli/{index-34jat4f2.js → index-wfb6vrc2.js} +2 -2
  63. package/dist/cli/{index-3mzc5rqc.js → index-wr0n2w3z.js} +3 -3
  64. package/dist/cli/{index-b3ws03kp.js → index-x1z5d5kj.js} +1 -1
  65. package/dist/cli/{index-d7y3cafq.js → index-x25rhkmk.js} +6 -6
  66. package/dist/cli/{index-6ssraqaa.js → index-yss701b3.js} +5 -5
  67. package/dist/cli/{index-dengtwpp.js → index-yvv0my1a.js} +1 -1
  68. package/dist/cli/index.js +44 -44
  69. package/dist/cli/{knowledge-escalator-9wqe0m8j.js → knowledge-escalator-qcra58bx.js} +15 -15
  70. package/dist/cli/{knowledge-events-m5vdchaa.js → knowledge-events-9mnx1a3f.js} +13 -13
  71. package/dist/cli/{knowledge-link-t0ce3mw7.js → knowledge-link-2jsn9f6a.js} +5 -5
  72. package/dist/cli/{knowledge-store-ff4hy057.js → knowledge-store-fdneqf7z.js} +6 -6
  73. package/dist/cli/{knowledge-validator-erj5thtq.js → knowledge-validator-7nj987ga.js} +8 -8
  74. package/dist/cli/{mcp-d0hz5gfp.js → mcp-0g34wmz4.js} +1 -1
  75. package/dist/cli/{model-preflight-p34x0k25.js → model-preflight-g11ykgc0.js} +43 -43
  76. package/dist/cli/{pending-delegations-1j6dx1mf.js → pending-delegations-5vme18wx.js} +13 -13
  77. package/dist/cli/{pr-subscriptions-b1g41jce.js → pr-subscriptions-cbx5p838.js} +12 -12
  78. package/dist/cli/{pr-workflow-gate-zjwt050h.js → pr-workflow-gate-376gw6yj.js} +43 -43
  79. package/dist/cli/{runner-wq954cda.js → runner-vvd26z2z.js} +6 -6
  80. package/dist/cli/{scan-cursor-enz1rp21.js → scan-cursor-x3j5wvfm.js} +7 -7
  81. package/dist/cli/{schema-9r8sggpb.js → schema-d98zzrpn.js} +1 -1
  82. package/dist/cli/{scope-persistence-c5n4g9f0.js → scope-persistence-vkb8z4hc.js} +16 -16
  83. package/dist/cli/{server-8z7pd66d.js → server-25svy6cb.js} +45 -45
  84. package/dist/cli/{skill-generator-nezzfa0t.js → skill-generator-ktz33dt6.js} +17 -17
  85. package/dist/cli/{snapshot-coordination-init-2j06c7ck.js → snapshot-coordination-init-74x3psxc.js} +43 -43
  86. package/dist/cli/{speckit-checkoff-805cqmrf.js → speckit-checkoff-9t6cdn4a.js} +4 -4
  87. package/dist/cli/{telemetry-gena5typ.js → telemetry-cr8q16wk.js} +1 -1
  88. package/dist/cli/{workspace-snapshot-hzhz0rxw.js → workspace-snapshot-afqvh9bq.js} +3 -3
  89. package/dist/cli/{worktree-collision-ownership-xpr0yv7h.js → worktree-collision-ownership-7a1n1f9j.js} +14 -14
  90. package/dist/cli/{worktree-isolation-z6ytes0j.js → worktree-isolation-8snw7w34.js} +43 -43
  91. package/dist/commands/attest.d.ts +10 -0
  92. package/dist/commands/index.d.ts +1 -0
  93. package/dist/commands/registry.d.ts +8 -0
  94. package/dist/config/plan-schema.d.ts +44 -0
  95. package/dist/gate-evidence.d.ts +138 -1
  96. package/dist/host/v2/guidance.d.ts +7 -0
  97. package/dist/index.js +561 -557
  98. package/dist/tools/attest-task.d.ts +67 -0
  99. package/dist/tools/phase-complete.d.ts +12 -0
  100. package/dist/tools/save-plan.d.ts +16 -0
  101. package/dist/tools/update-task-status.d.ts +1 -1
  102. package/dist/workflow/task-terminal.d.ts +1 -0
  103. package/dist/workflow/workflow-wal-schema.d.ts +2 -0
  104. package/package.json +1 -1
  105. package/dist/cli/curator-llm-factory-dwvftpvm.js +0 -82
  106. package/dist/cli/guardrail-explain-rqqhmb44.js +0 -83
@@ -0,0 +1,2209 @@
1
+ // @bun
2
+ import {
3
+ advanceTaskState,
4
+ applySessionWorkflowView,
5
+ commitTaskTerminalUnderPlanLock,
6
+ enforcePersistedReviewRouteReceipt,
7
+ ensureAgentSession,
8
+ getAgentSession,
9
+ getModifiedFilesForTask,
10
+ getStageBRouteEvidence,
11
+ getTaskState,
12
+ hasActiveLeanTurbo,
13
+ hasActiveTurboMode,
14
+ hasBothStageBCompletions,
15
+ hasModifiedFilesForTask,
16
+ isStageBRouteRequired,
17
+ matchesTier3,
18
+ normalizePath,
19
+ readReviewRouteReceiptSync,
20
+ recordStageBCompletion,
21
+ recoverPreparedTaskRepair,
22
+ recoverPreparedTaskRepairUnderPlanLock,
23
+ recoverPreparedTaskTerminal,
24
+ recoverPreparedTaskTerminalUnderPlanLock,
25
+ repairTaskWorkflowUnderPlanLock,
26
+ startAgentSession,
27
+ swarmState,
28
+ updateTaskWorkflowCache
29
+ } from "./index-hg4qa0aq.js";
30
+ import"./index-nq3zcrhm.js";
31
+ import"./index-8rrs1nc0.js";
32
+ import"./index-md2180vx.js";
33
+ import"./index-yss701b3.js";
34
+ import"./index-0wg5q595.js";
35
+ import"./index-axqyjreb.js";
36
+ import"./index-txdagbh7.js";
37
+ import"./index-qnabg1wp.js";
38
+ import"./index-vqtca3xv.js";
39
+ import"./index-rdec0y3z.js";
40
+ import"./index-qnst0jwc.js";
41
+ import {
42
+ recoverCoderSettlement
43
+ } from "./index-71bqn1nn.js";
44
+ import"./index-e8m8bb1r.js";
45
+ import"./index-bp476gwc.js";
46
+ import"./index-8a3aasag.js";
47
+ import"./index-x5jwegqw.js";
48
+ import"./index-r02aqt0a.js";
49
+ import"./index-rfvnq6wd.js";
50
+ import"./index-8mrentmw.js";
51
+ import"./index-pt7h91zc.js";
52
+ import"./index-q7sjgr1y.js";
53
+ import {
54
+ createSwarmTool,
55
+ resolveWorkingDirectory
56
+ } from "./index-22atsy06.js";
57
+ import {
58
+ loadPlan,
59
+ recordTaskAttempt,
60
+ updateTaskStatus
61
+ } from "./index-35tf03js.js";
62
+ import"./index-6b295cas.js";
63
+ import"./index-9nyqt3kb.js";
64
+ import"./index-9nmrw7ek.js";
65
+ import"./index-v3bmwmqw.js";
66
+ import"./index-sk1rva12.js";
67
+ import {
68
+ TASK_GATE_REQUIREMENTS_RECONSTRUCTION_SENTINEL,
69
+ computeExternalDeclarationDigest,
70
+ deriveApplicableGateSet,
71
+ externalProofCurrent,
72
+ getTaskWorkflowSnapshot,
73
+ isExternalEvidenceInitialCompletionEligible,
74
+ isLegacyTaskGateRequirementsChain,
75
+ isReadOnlyNoMutationEligible,
76
+ readCurrentTaskDeclaredFiles,
77
+ readTaskEvidenceRaw,
78
+ readTaskGateRequirementsReceiptsSync,
79
+ readWorkflowWalFile,
80
+ routeEvidenceFromTaskGateRequirements,
81
+ transitionTaskWorkflowEvidence,
82
+ validateTaskIdFormat
83
+ } from "./index-58cr1xg0.js";
84
+ import"./index-m9j9varx.js";
85
+ import"./index-spggmr30.js";
86
+ import"./index-1rfgzsaw.js";
87
+ import"./index-x25rhkmk.js";
88
+ import"./index-m9zzj4ee.js";
89
+ import {
90
+ validateSwarmPath
91
+ } from "./index-x1z5d5kj.js";
92
+ import"./index-p5m71yph.js";
93
+ import"./index-jy0mgzjq.js";
94
+ import"./index-rtry5xyf.js";
95
+ import"./index-hxp5jgd8.js";
96
+ import"./index-g22dps7x.js";
97
+ import"./index-0725jpp7.js";
98
+ import"./index-pyv8vzkx.js";
99
+ import {
100
+ formatLegacyQaBindingRecovery,
101
+ getProfileLookupForIdentity
102
+ } from "./index-99zt998b.js";
103
+ import"./index-7v74n9a1.js";
104
+ import"./index-vmevv6my.js";
105
+ import"./index-d083d1q7.js";
106
+ import"./index-ry16d01s.js";
107
+ import"./index-rdm3px0g.js";
108
+ import"./index-wr0n2w3z.js";
109
+ import"./index-7pmbnjvj.js";
110
+ import"./index-9rwhas09.js";
111
+ import"./index-1tpz99db.js";
112
+ import"./index-n5k81wym.js";
113
+ import"./index-vzm6zre7.js";
114
+ import"./index-ae75rja9.js";
115
+ import"./index-dng8z8me.js";
116
+ import"./index-ac8vdmgv.js";
117
+ import"./index-q8vfw61p.js";
118
+ import {
119
+ assertProjectRoot,
120
+ hasExplicitProjectBoundary,
121
+ isStrictPathDescendant
122
+ } from "./index-t7hmqgjy.js";
123
+ import {
124
+ telemetry
125
+ } from "./index-c2k7q723.js";
126
+ import"./index-te9hq2f4.js";
127
+ import {
128
+ listActiveLocks,
129
+ tryAcquireLock
130
+ } from "./index-ejhxgyvm.js";
131
+ import"./index-7g4c7s5r.js";
132
+ import"./index-q1x73nnd.js";
133
+ import {
134
+ PlanSchema
135
+ } from "./index-3t46mez1.js";
136
+ import {
137
+ init_loader,
138
+ loadPluginConfig
139
+ } from "./index-gwc7jahh.js";
140
+ import {
141
+ init_git_executable,
142
+ init_warning_buffer,
143
+ resolveGitExecutable
144
+ } from "./index-sp2cxpaq.js";
145
+ import {
146
+ bunSpawn,
147
+ init_bun_compat
148
+ } from "./index-qbxk2rn6.js";
149
+ import {
150
+ criticalWarn,
151
+ init_logger,
152
+ log
153
+ } from "./index-zgwm4ryv.js";
154
+ import {
155
+ init_schema,
156
+ stripKnownSwarmPrefix
157
+ } from "./index-yvv0my1a.js";
158
+ import"./index-xfzkq1k4.js";
159
+ import"./index-bk5tah7q.js";
160
+ import {
161
+ exports_external,
162
+ init_zod
163
+ } from "./index-gknz3bv1.js";
164
+ import"./index-zpsr8rpc.js";
165
+ import"./index-a76rekgs.js";
166
+
167
+ // src/tools/attest-task.ts
168
+ import * as fs4 from "fs";
169
+ import * as path4 from "path";
170
+ init_bun_compat();
171
+ init_git_executable();
172
+
173
+ // src/tools/update-task-status.ts
174
+ init_zod();
175
+ init_loader();
176
+ init_schema();
177
+ import * as fs3 from "fs";
178
+ import * as path3 from "path";
179
+
180
+ // src/hooks/diff-scope.ts
181
+ init_bun_compat();
182
+ init_git_executable();
183
+ import * as fs from "fs";
184
+ import * as path from "path";
185
+
186
+ // src/utils/gitignore-warning.ts
187
+ init_warning_buffer();
188
+ init_bun_compat();
189
+ init_git_executable();
190
+ var ENSURE_SWARM_GIT_EXCLUDED_PER_CALL_TIMEOUT_MS = 1500;
191
+
192
+ // src/hooks/diff-scope.ts
193
+ var _internals = { bunSpawn, resolveGitExecutable };
194
+ function getDeclaredScope(taskId, directory) {
195
+ try {
196
+ const planPath = path.join(directory, ".swarm", "plan.json");
197
+ if (!fs.existsSync(planPath))
198
+ return null;
199
+ const raw = fs.readFileSync(planPath, "utf-8");
200
+ const plan = JSON.parse(raw);
201
+ for (const phase of plan.phases ?? []) {
202
+ for (const task of phase.tasks ?? []) {
203
+ if (task.id !== taskId)
204
+ continue;
205
+ const ft = task.files_touched;
206
+ if (Array.isArray(ft) && ft.length > 0) {
207
+ return ft;
208
+ }
209
+ if (typeof ft === "string" && ft.length > 0) {
210
+ return [ft];
211
+ }
212
+ return null;
213
+ }
214
+ }
215
+ return null;
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
220
+ var GIT_DIFF_SPAWN_OPTIONS = {
221
+ timeout: ENSURE_SWARM_GIT_EXCLUDED_PER_CALL_TIMEOUT_MS,
222
+ stdin: "ignore",
223
+ stdout: "pipe",
224
+ stderr: "pipe"
225
+ };
226
+ async function getChangedFiles(directory) {
227
+ try {
228
+ const gitExecutable = _internals.resolveGitExecutable();
229
+ const proc = _internals.bunSpawn([gitExecutable, "diff", "--name-only", "HEAD~1"], {
230
+ cwd: directory,
231
+ ...GIT_DIFF_SPAWN_OPTIONS
232
+ });
233
+ let exitCode;
234
+ let stdout;
235
+ try {
236
+ [exitCode, stdout] = await Promise.all([proc.exited, proc.stdout.text()]);
237
+ } finally {
238
+ try {
239
+ proc.kill();
240
+ } catch {}
241
+ }
242
+ if (exitCode === 0) {
243
+ return {
244
+ files: stdout.trim().split(`
245
+ `).map((f) => f.trim()).filter((f) => f.length > 0),
246
+ basis: "HEAD~1"
247
+ };
248
+ }
249
+ const proc2 = _internals.bunSpawn([gitExecutable, "diff", "--name-only", "HEAD"], {
250
+ cwd: directory,
251
+ ...GIT_DIFF_SPAWN_OPTIONS
252
+ });
253
+ let exitCode2;
254
+ let stdout2;
255
+ try {
256
+ [exitCode2, stdout2] = await Promise.all([
257
+ proc2.exited,
258
+ proc2.stdout.text()
259
+ ]);
260
+ } finally {
261
+ try {
262
+ proc2.kill();
263
+ } catch {}
264
+ }
265
+ if (exitCode2 === 0) {
266
+ return {
267
+ files: stdout2.trim().split(`
268
+ `).map((f) => f.trim()).filter((f) => f.length > 0),
269
+ basis: "HEAD"
270
+ };
271
+ }
272
+ return { files: null, basis: "HEAD~1" };
273
+ } catch {
274
+ return { files: null, basis: "HEAD~1" };
275
+ }
276
+ }
277
+ async function getLatestCommitShortSha(directory) {
278
+ try {
279
+ const gitExecutable = _internals.resolveGitExecutable();
280
+ const proc = _internals.bunSpawn([gitExecutable, "rev-parse", "--short", "HEAD"], {
281
+ cwd: directory,
282
+ ...GIT_DIFF_SPAWN_OPTIONS
283
+ });
284
+ let exitCode;
285
+ let stdout;
286
+ try {
287
+ [exitCode, stdout] = await Promise.all([proc.exited, proc.stdout.text()]);
288
+ } finally {
289
+ try {
290
+ proc.kill();
291
+ } catch {}
292
+ }
293
+ if (exitCode === 0) {
294
+ const sha = stdout.trim();
295
+ if (sha.length > 0 && !/\s/.test(sha))
296
+ return sha;
297
+ }
298
+ return "";
299
+ } catch {
300
+ return "";
301
+ }
302
+ }
303
+ function hasDeclaredDiffScope(taskId, directory) {
304
+ try {
305
+ return getDeclaredScope(taskId, directory) !== null;
306
+ } catch {
307
+ return false;
308
+ }
309
+ }
310
+ async function validateDiffScope(taskId, directory, attribution) {
311
+ try {
312
+ const declaredScope = getDeclaredScope(taskId, directory);
313
+ if (!declaredScope)
314
+ return null;
315
+ const normalise = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "");
316
+ const isSwarmPath = (p) => normalise(p).startsWith(".swarm/");
317
+ const normScope = new Set(declaredScope.map(normalise));
318
+ const canonicaliseAttributionEntry = (raw) => {
319
+ const resolved = path.isAbsolute(raw) ? path.normalize(raw) : path.resolve(directory, raw);
320
+ const relative2 = path.relative(directory, resolved);
321
+ if (!relative2)
322
+ return null;
323
+ if (relative2.startsWith("..") || path.isAbsolute(relative2)) {
324
+ return null;
325
+ }
326
+ return relative2;
327
+ };
328
+ const attributed = attribution?.attributedFiles;
329
+ let undeclared;
330
+ let evidence;
331
+ if (Array.isArray(attributed) && attributed.length > 0) {
332
+ const normScopeAttribution = new Set(declaredScope.map(normalizePath));
333
+ undeclared = attributed.filter((f) => typeof f === "string" && f.length > 0).map(canonicaliseAttributionEntry).filter((entry) => entry !== null).map(normalise).filter((f) => !isSwarmPath(f)).filter((f) => !normScopeAttribution.has(normalizePath(f)));
334
+ evidence = "task attribution record";
335
+ } else {
336
+ const { files, basis } = await getChangedFiles(directory);
337
+ if (!files)
338
+ return null;
339
+ undeclared = files.filter((f) => !isSwarmPath(f)).map(normalise).filter((f) => !normScope.has(f));
340
+ if (undeclared.length === 0)
341
+ return null;
342
+ const sha = await getLatestCommitShortSha(directory);
343
+ const shaSuffix = sha ? ` ${sha}` : "";
344
+ evidence = basis === "HEAD" ? `repository-wide uncommitted diff vs HEAD${shaSuffix}` : `repository-wide diff vs latest commit${shaSuffix}`;
345
+ }
346
+ if (undeclared.length === 0)
347
+ return null;
348
+ const scopeStr = declaredScope.join(", ");
349
+ const undeclaredStr = undeclared.slice(0, 5).join(", ");
350
+ const extra = undeclared.length > 5 ? ` (+${undeclared.length - 5} more)` : "";
351
+ return `SCOPE WARNING: Task ${taskId} declared scope [${scopeStr}] but also modified [${undeclaredStr}${extra}] (evidence: ${evidence}). Reviewer should verify these changes are intentional.`;
352
+ } catch {
353
+ return null;
354
+ }
355
+ }
356
+
357
+ // src/turbo/lean/task-completion.ts
358
+ import * as fs2 from "fs";
359
+ import * as path2 from "path";
360
+ var _internals2 = {
361
+ listActiveLocks,
362
+ verifyLeanTurboTaskCompletion
363
+ };
364
+ function verifyLeanTurboTaskCompletion(directory, taskId, sessionID) {
365
+ let persisted = null;
366
+ try {
367
+ const statePath = path2.join(directory, ".swarm", "turbo-state.json");
368
+ if (!fs2.existsSync(statePath)) {
369
+ return {
370
+ ok: false,
371
+ reason: "Lean Turbo state file not found"
372
+ };
373
+ }
374
+ const raw = fs2.readFileSync(statePath, "utf-8");
375
+ persisted = JSON.parse(raw);
376
+ } catch {
377
+ return {
378
+ ok: false,
379
+ reason: "Lean Turbo state file unreadable or malformed"
380
+ };
381
+ }
382
+ if (typeof persisted !== "object" || persisted === null || typeof persisted.sessions !== "object" || persisted.sessions === null || Array.isArray(persisted.sessions)) {
383
+ return {
384
+ ok: false,
385
+ reason: "Lean Turbo state malformed: missing expected fields"
386
+ };
387
+ }
388
+ let runState = null;
389
+ for (const sessionState of Object.values(persisted.sessions)) {
390
+ if (typeof sessionState === "object" && sessionState !== null && sessionState.strategy === "lean" && sessionState.status === "running") {
391
+ if (sessionID !== undefined && sessionState.sessionID !== sessionID) {
392
+ continue;
393
+ }
394
+ runState = sessionState;
395
+ break;
396
+ }
397
+ }
398
+ if (!runState) {
399
+ return {
400
+ ok: false,
401
+ reason: "No active Lean Turbo run state found"
402
+ };
403
+ }
404
+ if (!Array.isArray(runState.lanes) || !Array.isArray(runState.degradedTasks)) {
405
+ return {
406
+ ok: false,
407
+ reason: "Lean Turbo state malformed: missing expected fields"
408
+ };
409
+ }
410
+ const lane = runState.lanes.find((l) => typeof l === "object" && l !== null && Array.isArray(l.taskIds) && typeof l.laneId === "string" && l.taskIds.includes(taskId));
411
+ if (!lane) {
412
+ return {
413
+ ok: false,
414
+ reason: `Task ${taskId} not found in any Lean Turbo lane`,
415
+ laneFound: false
416
+ };
417
+ }
418
+ const degradedTask = runState.degradedTasks.find((dt) => typeof dt === "object" && dt !== null && dt.taskId === taskId);
419
+ if (degradedTask) {
420
+ return {
421
+ ok: false,
422
+ reason: `Task ${taskId} is degraded: ${degradedTask.reason}`,
423
+ laneFound: true
424
+ };
425
+ }
426
+ if (lane.status !== "completed") {
427
+ return {
428
+ ok: false,
429
+ reason: `Lane ${lane.laneId} is not completed (status: ${lane.status})`,
430
+ laneFound: true,
431
+ evidence: {
432
+ laneId: lane.laneId,
433
+ laneStatus: lane.status,
434
+ taskIds: lane.taskIds
435
+ }
436
+ };
437
+ }
438
+ if (lane.laneId.length === 0 || lane.laneId.includes("/") || lane.laneId.includes("\\") || lane.laneId.includes("..")) {
439
+ return {
440
+ ok: false,
441
+ reason: `Lane ID contains invalid characters: ${lane.laneId}`,
442
+ laneFound: true,
443
+ evidence: {
444
+ laneId: lane.laneId,
445
+ laneStatus: lane.status,
446
+ taskIds: lane.taskIds
447
+ }
448
+ };
449
+ }
450
+ const phase = runState.phase ?? 0;
451
+ const evidencePath = path2.join(directory, ".swarm", "evidence", String(phase), "lean-turbo", `${lane.laneId}.json`);
452
+ const expectedDir = path2.join(directory, ".swarm", "evidence", String(phase), "lean-turbo");
453
+ const resolvedPath = path2.resolve(evidencePath);
454
+ const resolvedDir = path2.resolve(expectedDir);
455
+ if (!resolvedPath.startsWith(resolvedDir + path2.sep) && resolvedPath !== resolvedDir) {
456
+ return {
457
+ ok: false,
458
+ reason: `Lane ID causes path traversal: ${lane.laneId}`,
459
+ laneFound: true,
460
+ evidence: {
461
+ laneId: lane.laneId,
462
+ laneStatus: lane.status,
463
+ taskIds: lane.taskIds
464
+ }
465
+ };
466
+ }
467
+ if (!fs2.existsSync(evidencePath)) {
468
+ return {
469
+ ok: false,
470
+ reason: `Lane ${lane.laneId} evidence file not found: ${evidencePath}`,
471
+ laneFound: true,
472
+ evidence: {
473
+ laneId: lane.laneId,
474
+ laneStatus: lane.status,
475
+ taskIds: lane.taskIds
476
+ }
477
+ };
478
+ }
479
+ const activeLocks = _internals2.listActiveLocks(directory);
480
+ const laneLocks = activeLocks.filter((lock) => lock.laneId === lane.laneId);
481
+ if (laneLocks.length > 0) {
482
+ return {
483
+ ok: false,
484
+ reason: `Active file locks exist for lane ${lane.laneId}: ${laneLocks.map((l) => l.filePath).join(", ")}`,
485
+ laneFound: true,
486
+ evidence: {
487
+ laneId: lane.laneId,
488
+ laneStatus: lane.status,
489
+ taskIds: lane.taskIds
490
+ }
491
+ };
492
+ }
493
+ let filesTouched = [];
494
+ try {
495
+ const planPath = path2.join(directory, ".swarm", "plan.json");
496
+ const planRaw = fs2.readFileSync(planPath, "utf-8");
497
+ const plan = JSON.parse(planRaw);
498
+ for (const planPhase of plan.phases ?? []) {
499
+ for (const task of planPhase.tasks ?? []) {
500
+ if (task.id === taskId && task.files_touched) {
501
+ filesTouched = task.files_touched;
502
+ break;
503
+ }
504
+ }
505
+ }
506
+ } catch {
507
+ return {
508
+ ok: false,
509
+ reason: `Cannot verify Tier-3 patterns for task ${taskId}: plan.json unreadable`,
510
+ laneFound: true,
511
+ evidence: {
512
+ laneId: lane.laneId,
513
+ laneStatus: lane.status,
514
+ taskIds: lane.taskIds
515
+ }
516
+ };
517
+ }
518
+ if (filesTouched.length > 0 && matchesTier3(filesTouched)) {
519
+ return {
520
+ ok: false,
521
+ reason: `Task ${taskId} touches Tier-3 security-sensitive files and cannot bypass Stage B`,
522
+ laneFound: true,
523
+ evidence: {
524
+ laneId: lane.laneId,
525
+ laneStatus: lane.status,
526
+ taskIds: lane.taskIds
527
+ }
528
+ };
529
+ }
530
+ return {
531
+ ok: true,
532
+ reason: `Task ${taskId} in completed lane ${lane.laneId}`,
533
+ laneFound: true,
534
+ evidence: {
535
+ laneId: lane.laneId,
536
+ laneStatus: lane.status,
537
+ taskIds: lane.taskIds
538
+ }
539
+ };
540
+ }
541
+
542
+ // src/tools/update-task-status.ts
543
+ init_logger();
544
+ var _internals3 = {
545
+ tryAcquireLock,
546
+ updateTaskStatus,
547
+ resolveWorkingDirectory,
548
+ loadPlan,
549
+ readTaskEvidenceRaw,
550
+ hasActiveLeanTurbo,
551
+ hasActiveTurboMode,
552
+ verifyLeanTurboTaskCompletion,
553
+ hasPassedDurableGateEvidence,
554
+ emitReviewerGateDecision: telemetry.reviewerGateDecision,
555
+ recordTaskAttempt
556
+ };
557
+ async function recordRunMemoryOutcome(directory, input) {
558
+ try {
559
+ await _internals3.recordTaskAttempt(directory, input);
560
+ } catch (_error) {}
561
+ }
562
+ function syncCallerWorkflowFromEvidence(sessionId, taskId, evidence) {
563
+ if (!sessionId)
564
+ return;
565
+ const callerSession = ensureAgentSession(sessionId);
566
+ const workflow = getTaskWorkflowSnapshot(evidence);
567
+ applySessionWorkflowView(callerSession, taskId, workflow);
568
+ }
569
+ async function hasPreparedWorkflowWal(directory, taskId, kind) {
570
+ return await readPreparedWorkflowWal(directory, taskId, kind) !== null;
571
+ }
572
+ async function readPreparedWorkflowWal(directory, taskId, kind) {
573
+ const walPath = validateSwarmPath(directory, kind === "task-repair" ? `task-repairs/${taskId}.json` : `task-terminals/${taskId}.json`);
574
+ const wal = await readWorkflowWalFile(kind, walPath, taskId);
575
+ return wal?.state === "PREPARED" ? wal : null;
576
+ }
577
+ function resolveRunMemoryFileTargets(plan, taskId) {
578
+ return plan?.phases.flatMap((phase) => phase.tasks).find((candidate) => candidate.id === taskId)?.files_touched ?? [];
579
+ }
580
+ var VALID_STATUSES = [
581
+ "pending",
582
+ "in_progress",
583
+ "completed",
584
+ "blocked"
585
+ ];
586
+ function validateStatus(status) {
587
+ if (!VALID_STATUSES.includes(status)) {
588
+ return `Invalid status "${status}". Must be one of: ${VALID_STATUSES.join(", ")}`;
589
+ }
590
+ return;
591
+ }
592
+ function validateTaskId(taskId) {
593
+ const result = validateTaskIdFormat(taskId);
594
+ if (result) {
595
+ return `Invalid task_id "${taskId}". Must match pattern N.M or N.M.P (e.g., "1.1", "1.2.3")`;
596
+ }
597
+ return;
598
+ }
599
+ function getSettledTransitionRejection(taskId, currentStatus, nextStatus, force) {
600
+ if (currentStatus !== "pending" && currentStatus !== "in_progress" && nextStatus !== currentStatus) {
601
+ if (nextStatus !== "in_progress" || force !== true) {
602
+ return {
603
+ success: false,
604
+ message: `Task ${taskId} is settled (${currentStatus}); backward transitions require the audited in_progress repair path.`,
605
+ errors: [
606
+ `Task ${taskId} is settled (${currentStatus}); use force:true with exact CAS and audit fields to repair it to in_progress.`
607
+ ]
608
+ };
609
+ }
610
+ }
611
+ return null;
612
+ }
613
+ function isRepairRetryShape(args) {
614
+ return args.status === "in_progress" && args.force === true && typeof args.transition_id === "string";
615
+ }
616
+ function reviewerGateDecision(taskId, sessionID, result, reasonCode, evidenceKind) {
617
+ try {
618
+ _internals3.emitReviewerGateDecision(sessionID ?? "", taskId, result.blocked, reasonCode, evidenceKind);
619
+ } catch (_error) {}
620
+ return result;
621
+ }
622
+ function hasPassedDurableGateEvidence(workingDirectory, taskId) {
623
+ const evidence = readTaskEvidenceRaw(workingDirectory, taskId);
624
+ if (!evidence || !Array.isArray(evidence.required_gates) || evidence.required_gates.length === 0) {
625
+ return false;
626
+ }
627
+ return evidence.required_gates.every((gate) => evidence.gates?.[gate] != null);
628
+ }
629
+ function routeGateAllowsTask(directory, taskId, sessionID) {
630
+ if (!directory)
631
+ return true;
632
+ const enforcementEnabled = (() => {
633
+ try {
634
+ return loadPluginConfig(directory).review_routing?.enforce_receipts ?? true;
635
+ } catch {
636
+ return true;
637
+ }
638
+ })();
639
+ if (!enforcementEnabled)
640
+ return true;
641
+ const requirementReceipts = (() => {
642
+ try {
643
+ return readTaskGateRequirementsReceiptsSync(directory, taskId);
644
+ } catch {
645
+ return null;
646
+ }
647
+ })();
648
+ if (!requirementReceipts)
649
+ return false;
650
+ const session = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
651
+ const routeRequired = sessionID ? session ? isStageBRouteRequired(session, taskId) : false : [...swarmState.agentSessions.values()].some((candidate) => isStageBRouteRequired(candidate, taskId));
652
+ const legacyUnrouted = isLegacyTaskGateRequirementsChain(requirementReceipts) && !routeRequired;
653
+ if (!routeRequired && requirementReceipts.length === 0)
654
+ return true;
655
+ const durableEvidence = routeEvidenceFromTaskGateRequirements(requirementReceipts);
656
+ const routeAllowsSession = (candidateSessionID, candidateSession) => {
657
+ try {
658
+ readReviewRouteReceiptSync({
659
+ projectRoot: directory,
660
+ sessionId: candidateSessionID,
661
+ taskId
662
+ });
663
+ } catch {
664
+ return false;
665
+ }
666
+ const liveEvidence = candidateSession ? getStageBRouteEvidence(candidateSession, taskId) : [];
667
+ return enforcePersistedReviewRouteReceipt({
668
+ projectRoot: directory,
669
+ sessionId: candidateSessionID,
670
+ taskId,
671
+ receipts: liveEvidence.length > 0 ? liveEvidence : durableEvidence,
672
+ enforcementEnabled: true,
673
+ legacyUnrouted: false,
674
+ requireEvidenceBindings: true
675
+ }).canAdvance;
676
+ };
677
+ if (sessionID)
678
+ return routeAllowsSession(sessionID, session);
679
+ if (!routeRequired)
680
+ return legacyUnrouted;
681
+ const evidenceSessionIDs = new Set(durableEvidence.map((evidence) => evidence.sessionId).filter((value) => Boolean(value)));
682
+ for (const [
683
+ candidateSessionID,
684
+ candidateSession
685
+ ] of swarmState.agentSessions) {
686
+ if (!isStageBRouteRequired(candidateSession, taskId) && !evidenceSessionIDs.has(candidateSessionID))
687
+ continue;
688
+ if (routeAllowsSession(candidateSessionID, candidateSession))
689
+ return true;
690
+ }
691
+ return false;
692
+ }
693
+ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = false, sessionID, fallbackDir, currentDeclaredFiles) {
694
+ try {
695
+ let skipStandardTurboBypass = false;
696
+ if (!fallbackDir && !sessionID && _internals3.hasActiveLeanTurbo(sessionID)) {
697
+ const resolvedDir2 = workingDirectory;
698
+ try {
699
+ const leanCheck = _internals3.verifyLeanTurboTaskCompletion(resolvedDir2, taskId, sessionID);
700
+ if (leanCheck.ok) {
701
+ return reviewerGateDecision(taskId, sessionID, {
702
+ blocked: false,
703
+ reason: `Lean Turbo bypass: ${leanCheck.reason}`
704
+ }, "lean_turbo_completed_lane", "fallback");
705
+ }
706
+ if (leanCheck.laneFound !== false) {
707
+ skipStandardTurboBypass = true;
708
+ }
709
+ } catch {
710
+ skipStandardTurboBypass = true;
711
+ }
712
+ }
713
+ if (!fallbackDir && !sessionID && !skipStandardTurboBypass && _internals3.hasActiveTurboMode(sessionID)) {
714
+ const resolvedDir2 = workingDirectory;
715
+ try {
716
+ const planPath = path3.join(resolvedDir2, ".swarm", "plan.json");
717
+ const planRaw = fs3.readFileSync(planPath, "utf-8");
718
+ const plan = JSON.parse(planRaw);
719
+ for (const planPhase of plan.phases ?? []) {
720
+ for (const task of planPhase.tasks ?? []) {
721
+ if (task.id === taskId && Array.isArray(task.files_touched) && task.files_touched.length > 0) {
722
+ if (!matchesTier3(task.files_touched)) {
723
+ return reviewerGateDecision(taskId, sessionID, {
724
+ blocked: false,
725
+ reason: "Turbo Mode bypass"
726
+ }, "standard_turbo_non_tier3", "fallback");
727
+ }
728
+ break;
729
+ }
730
+ }
731
+ }
732
+ } catch {}
733
+ }
734
+ let authoritativeDir;
735
+ if (fallbackDir) {
736
+ const resolved = _internals3.resolveWorkingDirectory(workingDirectory, fallbackDir);
737
+ authoritativeDir = resolved.success ? resolved.directory : fallbackDir;
738
+ } else if (workingDirectory) {
739
+ authoritativeDir = workingDirectory;
740
+ }
741
+ if (authoritativeDir) {
742
+ const legacyDirectCaller = Symbol("legacy-direct-reviewer-gate");
743
+ try {
744
+ const evidence = _internals3.readTaskEvidenceRaw(authoritativeDir, taskId);
745
+ if (!fallbackDir && !sessionID && !evidence?.workflow)
746
+ throw legacyDirectCaller;
747
+ try {
748
+ const planPath = path3.join(authoritativeDir, ".swarm", "plan.json");
749
+ const planRaw = fs3.readFileSync(planPath, "utf-8");
750
+ const plan = JSON.parse(planRaw);
751
+ const declaredTask = (plan.phases ?? []).flatMap((phase) => phase.tasks ?? []).find((task) => task.id === taskId);
752
+ if (declaredTask && declaredTask.evidence_class === "external_git" && typeof plan.title === "string" && typeof plan.swarm === "string") {
753
+ const planIdentity = {
754
+ title: plan.title,
755
+ swarm: plan.swarm
756
+ };
757
+ const workflow2 = getTaskWorkflowSnapshot(evidence);
758
+ if (isExternalEvidenceInitialCompletionEligible(planIdentity, declaredTask, evidence) || workflow2.state === "complete" && externalProofCurrent(planIdentity, declaredTask, evidence)) {
759
+ return reviewerGateDecision(taskId, sessionID, {
760
+ blocked: false,
761
+ reason: `External evidence attested: task ${taskId} completed via the pinned external_git anchors (issue #3422)`,
762
+ requiredGates: [],
763
+ satisfiedGates: [],
764
+ missingGates: [],
765
+ source: "external_attest",
766
+ generation: workflow2.generation
767
+ }, "durable_evidence_complete", "fallback");
768
+ }
769
+ return reviewerGateDecision(taskId, sessionID, {
770
+ blocked: true,
771
+ reason: `Task ${taskId} declares external_git evidence but no matched attestation exists yet. Run /swarm attest ${taskId} (or re-plan it as a thin coder dispatch).`,
772
+ requiredGates: [],
773
+ satisfiedGates: [],
774
+ missingGates: ["external_attest"],
775
+ source: "durable_exact_task",
776
+ generation: workflow2.generation,
777
+ nextAction: `Run /swarm attest ${taskId} (or re-plan it as a thin coder dispatch).`
778
+ }, "required_gates_missing", "block");
779
+ }
780
+ } catch {}
781
+ if (!evidence) {
782
+ return reviewerGateDecision(taskId, sessionID, {
783
+ blocked: true,
784
+ reason: `Task ${taskId} has no exact-task QA evidence. Delegate the required Stage B agents, then retry completion.`,
785
+ requiredGates: [],
786
+ satisfiedGates: [],
787
+ missingGates: ["reviewer", "test_engineer"],
788
+ source: "durable_exact_task",
789
+ generation: 0,
790
+ nextAction: "Delegate reviewer and test_engineer for this exact task generation."
791
+ }, "required_gates_missing", "block");
792
+ }
793
+ if (!evidence.workflow) {
794
+ const derivedGates2 = deriveApplicableGateSet(evidence);
795
+ return reviewerGateDecision(taskId, sessionID, {
796
+ blocked: true,
797
+ reason: `Task ${taskId} has legacy QA evidence without an authoritative workflow generation. Run a fresh exact-task workflow transition before completion.`,
798
+ requiredGates: derivedGates2.requiredGates,
799
+ satisfiedGates: derivedGates2.satisfiedGates,
800
+ missingGates: derivedGates2.missingGates,
801
+ source: "durable_exact_task",
802
+ generation: 0,
803
+ nextAction: "Re-run the exact-task coder/Stage A/Stage B workflow to migrate evidence."
804
+ }, "required_gates_missing", "block");
805
+ }
806
+ const workflow = getTaskWorkflowSnapshot(evidence);
807
+ const derivedGates = deriveApplicableGateSet(evidence, {
808
+ currentDeclaredFiles: currentDeclaredFiles === undefined ? readCurrentTaskDeclaredFiles(authoritativeDir, taskId) : currentDeclaredFiles
809
+ });
810
+ const requiredGates = derivedGates.requiredGates;
811
+ const satisfiedGates = derivedGates.satisfiedGates;
812
+ const missingGates = derivedGates.missingGates;
813
+ if (requiredGates.includes(TASK_GATE_REQUIREMENTS_RECONSTRUCTION_SENTINEL)) {
814
+ const recovery = `Task ${taskId} contains a legacy receipt-less gate-repair marker that no agent can satisfy. ` + `Call repair_gate_evidence for task ${taskId}, then delegate coder for a fresh accepted mutation before rerunning Stage A and Stage B.`;
815
+ return reviewerGateDecision(taskId, sessionID, {
816
+ blocked: true,
817
+ reason: recovery,
818
+ requiredGates,
819
+ satisfiedGates,
820
+ missingGates,
821
+ source: "durable_exact_task",
822
+ generation: workflow.generation,
823
+ nextAction: recovery
824
+ }, "required_gates_missing", "data_quality");
825
+ }
826
+ const contradictorySignals = [];
827
+ if ((workflow.state === "tests_run" || workflow.state === "complete") && missingGates.length > 0) {
828
+ contradictorySignals.push(`workflow state ${workflow.state} claims completion readiness while required proof is missing`);
829
+ }
830
+ if (workflow.state !== "tests_run" && workflow.state !== "complete" && evidence.gates.pre_check != null && requiredGates.length > 0 && missingGates.length === 0) {
831
+ contradictorySignals.push(`all required proof exists but workflow state is ${workflow.state}`);
832
+ }
833
+ const workflowComplete = workflow.state === "tests_run" || workflow.state === "complete";
834
+ if (missingGates.length === 0 && contradictorySignals.length === 0 && (workflowComplete || derivedGates.readOnlyNoMutation)) {
835
+ if (!derivedGates.readOnlyNoMutation && !routeGateAllowsTask(authoritativeDir, taskId, sessionID)) {
836
+ return reviewerGateDecision(taskId, sessionID, {
837
+ blocked: true,
838
+ reason: `Task ${taskId} has not satisfied its exact v1 review route receipt. Missing or mismatched route evidence fails closed.`,
839
+ requiredGates,
840
+ satisfiedGates,
841
+ missingGates: ["review_route_receipt"],
842
+ source: "durable_exact_task",
843
+ generation: workflow.generation,
844
+ nextAction: "Recover the exact route receipt and independent Stage-B bindings before completing this task."
845
+ }, "required_gates_missing", "block");
846
+ }
847
+ return reviewerGateDecision(taskId, sessionID, {
848
+ blocked: false,
849
+ reason: "",
850
+ requiredGates,
851
+ satisfiedGates,
852
+ missingGates,
853
+ contradictorySignals,
854
+ source: "durable_exact_task",
855
+ generation: workflow.generation
856
+ }, "durable_evidence_complete", "genuine");
857
+ }
858
+ return reviewerGateDecision(taskId, sessionID, {
859
+ blocked: true,
860
+ reason: `Task ${taskId} generation ${workflow.generation} has not passed exact-task QA. State: ${workflow.state}. Missing gates: [${missingGates.join(", ")}]. Contradictions: [${contradictorySignals.join("; ")}].`,
861
+ requiredGates,
862
+ satisfiedGates,
863
+ missingGates,
864
+ contradictorySignals,
865
+ source: "durable_exact_task",
866
+ generation: workflow.generation,
867
+ nextAction: workflow.state === "rework_required" ? "Delegate the same task to coder for repair, then rerun Stage A and Stage B." : "Complete the missing exact-task Stage A/Stage B transitions."
868
+ }, "required_gates_missing", "block");
869
+ } catch (error) {
870
+ if (error === legacyDirectCaller) {} else {
871
+ return reviewerGateDecision(taskId, sessionID, {
872
+ blocked: true,
873
+ reason: `Evidence file for task ${taskId} is corrupt or unreadable: ${error instanceof Error ? error.message : String(error)}. ` + `Call repair_gate_evidence for task ${taskId}; corrupt evidence fails closed until the repair completes.`,
874
+ requiredGates: [],
875
+ satisfiedGates: [],
876
+ missingGates: ["reviewer", "test_engineer"],
877
+ source: "durable_exact_task",
878
+ corrupt: true,
879
+ nextAction: `Call repair_gate_evidence for task ${taskId}; corrupt evidence fails closed until the repair completes.`
880
+ }, "corrupt_evidence", "data_quality");
881
+ }
882
+ }
883
+ }
884
+ let resolvedDir;
885
+ if (fallbackDir) {
886
+ const resolveResult = _internals3.resolveWorkingDirectory(workingDirectory, fallbackDir);
887
+ if (resolveResult.success) {
888
+ resolvedDir = resolveResult.directory;
889
+ } else {
890
+ resolvedDir = fallbackDir;
891
+ }
892
+ } else if (workingDirectory) {
893
+ resolvedDir = workingDirectory;
894
+ }
895
+ const routeAllowsStageB = (sessionId, _session) => routeGateAllowsTask(resolvedDir, taskId, sessionId);
896
+ let evidenceIncompleteReason = null;
897
+ try {
898
+ if (!resolvedDir) {} else {
899
+ const evidence = _internals3.readTaskEvidenceRaw(resolvedDir, taskId);
900
+ if (evidence === null) {} else if (evidence.required_gates && Array.isArray(evidence.required_gates) && evidence.gates) {
901
+ if (evidence.required_gates.length > 0 && evidence.required_gates.every((gate) => evidence.gates[gate] != null)) {
902
+ const routeSession = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
903
+ if (!resolvedDir || routeAllowsStageB(sessionID, routeSession)) {
904
+ return reviewerGateDecision(taskId, sessionID, { blocked: false, reason: "" }, "durable_evidence_complete", "genuine");
905
+ }
906
+ }
907
+ const missingGates = evidence.required_gates.filter((gate) => evidence.gates[gate] == null);
908
+ evidenceIncompleteReason = evidence.required_gates.length === 0 ? `Task ${taskId} has an evidence file with no required gates. Delegate reviewer and test_engineer before marking task as completed.` : `Task ${taskId} is missing required gates: [${missingGates.join(", ")}]. ` + `Required: [${evidence.required_gates.join(", ")}]. ` + `Completed: [${Object.keys(evidence.gates).join(", ")}]. ` + `Delegate the missing gate agents before marking task as completed.`;
909
+ }
910
+ }
911
+ } catch (error) {
912
+ log(`[gate-evidence] Evidence file for task ${taskId} is corrupt or unreadable:`, error instanceof Error ? error.message : String(error));
913
+ telemetry.gateFailed("", "qa_gate", taskId, `Evidence file corrupt or unreadable`);
914
+ return reviewerGateDecision(taskId, sessionID, {
915
+ blocked: true,
916
+ reason: `Evidence file for task ${taskId} is corrupt or unreadable. ` + `Call repair_gate_evidence for task ${taskId}; corrupt evidence fails closed until the repair completes.`
917
+ }, "corrupt_evidence", "data_quality");
918
+ }
919
+ if (swarmState.agentSessions.size === 0 && !evidenceIncompleteReason) {
920
+ return reviewerGateDecision(taskId, sessionID, { blocked: false, reason: "" }, "no_active_sessions", "fallback");
921
+ }
922
+ let validSessionCount = 0;
923
+ for (const [_sessionId, session] of swarmState.agentSessions) {
924
+ if (!(session.taskWorkflowStates instanceof Map)) {
925
+ continue;
926
+ }
927
+ validSessionCount++;
928
+ const state = getTaskState(session, taskId);
929
+ if (state === "tests_run" || state === "complete") {
930
+ if (resolvedDir && !routeAllowsStageB(_sessionId, session)) {
931
+ continue;
932
+ }
933
+ return reviewerGateDecision(taskId, sessionID, { blocked: false, reason: "" }, "workflow_state_complete", "genuine");
934
+ }
935
+ if (stageBParallelEnabled && hasBothStageBCompletions(session, taskId) && (!resolvedDir || routeAllowsStageB(_sessionId, session))) {
936
+ return reviewerGateDecision(taskId, sessionID, { blocked: false, reason: "" }, "stage_b_parallel_complete", "genuine");
937
+ }
938
+ }
939
+ if (validSessionCount === 0 && !evidenceIncompleteReason) {
940
+ return reviewerGateDecision(taskId, sessionID, { blocked: false, reason: "" }, "zero_valid_sessions", "data_quality");
941
+ }
942
+ const stateEntries = [];
943
+ for (const [sessionId, session] of swarmState.agentSessions) {
944
+ if (!(session.taskWorkflowStates instanceof Map))
945
+ continue;
946
+ const state = getTaskState(session, taskId);
947
+ stateEntries.push(`${sessionId}: ${state}`);
948
+ }
949
+ if (resolvedDir) {
950
+ try {
951
+ const planPath = path3.join(resolvedDir, ".swarm", "plan.json");
952
+ const planRaw = fs3.readFileSync(planPath, "utf-8");
953
+ const plan = JSON.parse(planRaw);
954
+ for (const planPhase of plan.phases ?? []) {
955
+ for (const task of planPhase.tasks ?? []) {
956
+ if (task.id === taskId && task.status === "completed" && _internals3.hasPassedDurableGateEvidence(resolvedDir, taskId)) {
957
+ return reviewerGateDecision(taskId, sessionID, { blocked: false, reason: "" }, "restart_recovery_complete", "genuine");
958
+ }
959
+ }
960
+ }
961
+ } catch {}
962
+ }
963
+ {
964
+ let hasReviewer = false;
965
+ let hasTestEngineer = false;
966
+ for (const [sessionId, chain] of swarmState.delegationChains) {
967
+ const session = swarmState.agentSessions.get(sessionId);
968
+ if (session && (session.currentTaskId === taskId || session.lastCoderDelegationTaskId === taskId)) {
969
+ for (const delegation of chain) {
970
+ const target = stripKnownSwarmPrefix(delegation.to);
971
+ if (target === "reviewer")
972
+ hasReviewer = true;
973
+ if (target === "test_engineer")
974
+ hasTestEngineer = true;
975
+ }
976
+ }
977
+ }
978
+ if (hasReviewer && hasTestEngineer) {
979
+ return reviewerGateDecision(taskId, sessionID, { blocked: false, reason: "" }, "scoped_delegation_complete", "genuine");
980
+ }
981
+ if (!evidenceIncompleteReason && (!hasReviewer || !hasTestEngineer)) {
982
+ for (const [sessionId, chain] of swarmState.delegationChains) {
983
+ const hasCoder = chain.some((d) => stripKnownSwarmPrefix(d.to) === "coder");
984
+ if (hasCoder)
985
+ continue;
986
+ const chainSession = swarmState.agentSessions.get(sessionId);
987
+ if (chainSession) {
988
+ const chainTaskId = chainSession.currentTaskId || chainSession.lastCoderDelegationTaskId;
989
+ if (chainTaskId && chainTaskId !== taskId)
990
+ continue;
991
+ }
992
+ for (const delegation of chain) {
993
+ const target = stripKnownSwarmPrefix(delegation.to);
994
+ if (target === "reviewer")
995
+ hasReviewer = true;
996
+ if (target === "test_engineer")
997
+ hasTestEngineer = true;
998
+ }
999
+ }
1000
+ if (hasReviewer && hasTestEngineer) {
1001
+ return reviewerGateDecision(taskId, sessionID, { blocked: false, reason: "" }, "unscoped_delegation_complete", "genuine");
1002
+ }
1003
+ }
1004
+ }
1005
+ const currentStateStr = stateEntries.length > 0 ? stateEntries.join(", ") : "no active sessions";
1006
+ const chainEntries = [];
1007
+ for (const [sessionId, chain] of swarmState.delegationChains) {
1008
+ const session = swarmState.agentSessions.get(sessionId);
1009
+ if (session && (session.currentTaskId === taskId || session.lastCoderDelegationTaskId === taskId)) {
1010
+ const targets = chain.map((d) => stripKnownSwarmPrefix(d.to));
1011
+ chainEntries.push(`${sessionId}: [${targets.join(", ")}]`);
1012
+ }
1013
+ }
1014
+ const chainSummary = chainEntries.length > 0 ? chainEntries.join("; ") : "no chains for this task";
1015
+ const rehydratedSessionCount = [
1016
+ ...swarmState.agentSessions.values()
1017
+ ].filter((s) => s.sessionRehydratedAt > 0).length;
1018
+ const finalReason = [
1019
+ `Task ${taskId} has not passed QA gates.`,
1020
+ ` Session states: [${currentStateStr}].`,
1021
+ ` Delegation chains: [${chainSummary}].`,
1022
+ ` Evidence: [${evidenceIncompleteReason ?? "no evidence file found"}].`,
1023
+ ` Rehydrated sessions: ${rehydratedSessionCount}.`,
1024
+ ` Missing required state: tests_run or complete.`
1025
+ ].join(`
1026
+ `);
1027
+ telemetry.gateFailed("", "qa_gate", taskId, evidenceIncompleteReason ? `Missing gates: evidence incomplete` : `Missing state: tests_run or complete`);
1028
+ return reviewerGateDecision(taskId, sessionID, {
1029
+ blocked: true,
1030
+ reason: finalReason
1031
+ }, "required_gates_missing", "block");
1032
+ } catch {
1033
+ return reviewerGateDecision(taskId, sessionID, { blocked: false, reason: "" }, "inspection_error", "fallback");
1034
+ }
1035
+ }
1036
+ function hasForeignAttributionRecord(taskId, checkingSessionId) {
1037
+ try {
1038
+ const checker = swarmState.agentSessions.get(checkingSessionId);
1039
+ const checkerProject = checker?.owningProjectKey ?? null;
1040
+ for (const [sessionId, session] of swarmState.agentSessions) {
1041
+ if (sessionId === checkingSessionId)
1042
+ continue;
1043
+ if (!(session.modifiedFilesByTask instanceof Map))
1044
+ continue;
1045
+ if ((session.owningProjectKey ?? null) !== checkerProject)
1046
+ continue;
1047
+ const entry = session.modifiedFilesByTask.get(taskId);
1048
+ if (Array.isArray(entry) && entry.length > 0)
1049
+ return true;
1050
+ }
1051
+ return false;
1052
+ } catch {
1053
+ return false;
1054
+ }
1055
+ }
1056
+ async function checkReviewerGateWithScope(taskId, workingDirectory, sessionID, fallbackDir) {
1057
+ const stageBParallelEnabled = true;
1058
+ const result = checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled, sessionID, fallbackDir);
1059
+ const session = sessionID ? getAgentSession(sessionID) : undefined;
1060
+ const attributedFiles = session ? getModifiedFilesForTask(session, taskId) : [];
1061
+ const scopeAdvisoryLine = !!sessionID && !hasModifiedFilesForTask(session, taskId) && !!workingDirectory && hasDeclaredDiffScope(taskId, workingDirectory) ? hasForeignAttributionRecord(taskId, sessionID) ? `SCOPE ADVISORY: attribution record for task ${taskId} exists under another session \u2014 not used for scope verification` : "SCOPE ADVISORY: no attribution record in this session \u2014 not used for scope verification" : null;
1062
+ const scopeWarning = await validateDiffScope(taskId, workingDirectory, sessionID && attributedFiles.length > 0 ? { attributedFiles } : undefined).catch(() => null);
1063
+ if (!scopeWarning && !scopeAdvisoryLine)
1064
+ return result;
1065
+ return {
1066
+ ...result,
1067
+ reason: [result.reason, scopeWarning, scopeAdvisoryLine].filter((part) => typeof part === "string" && part.length > 0).join(`
1068
+ `)
1069
+ };
1070
+ }
1071
+ function recoverTaskStateFromDelegations(taskId, directory) {
1072
+ let hasReviewer = false;
1073
+ let hasTestEngineer = false;
1074
+ for (const [sessionId, chain] of swarmState.delegationChains) {
1075
+ const session = swarmState.agentSessions.get(sessionId);
1076
+ if (session && (session.currentTaskId === taskId || session.lastCoderDelegationTaskId === taskId)) {
1077
+ for (const delegation of chain) {
1078
+ const target = stripKnownSwarmPrefix(delegation.to);
1079
+ if (target === "reviewer")
1080
+ hasReviewer = true;
1081
+ if (target === "test_engineer")
1082
+ hasTestEngineer = true;
1083
+ }
1084
+ }
1085
+ }
1086
+ let hasDurableIncompleteGates = false;
1087
+ if (directory) {
1088
+ try {
1089
+ const taskEvidence = readTaskEvidenceRaw(directory, taskId);
1090
+ if (taskEvidence?.gates && Array.isArray(taskEvidence.required_gates) && taskEvidence.required_gates.length > 0) {
1091
+ const gates = taskEvidence.gates;
1092
+ hasDurableIncompleteGates = taskEvidence.required_gates.some((g) => gates[g] == null);
1093
+ }
1094
+ } catch {
1095
+ hasDurableIncompleteGates = true;
1096
+ }
1097
+ }
1098
+ if (!hasDurableIncompleteGates && (!hasReviewer || !hasTestEngineer)) {
1099
+ for (const [sessionId, chain] of swarmState.delegationChains) {
1100
+ const hasCoder = chain.some((d) => stripKnownSwarmPrefix(d.to) === "coder");
1101
+ if (hasCoder)
1102
+ continue;
1103
+ const chainSession = swarmState.agentSessions.get(sessionId);
1104
+ if (chainSession) {
1105
+ const chainTaskId = chainSession.currentTaskId || chainSession.lastCoderDelegationTaskId;
1106
+ if (chainTaskId && chainTaskId !== taskId)
1107
+ continue;
1108
+ }
1109
+ for (const delegation of chain) {
1110
+ const target = stripKnownSwarmPrefix(delegation.to);
1111
+ if (target === "reviewer")
1112
+ hasReviewer = true;
1113
+ if (target === "test_engineer")
1114
+ hasTestEngineer = true;
1115
+ }
1116
+ }
1117
+ }
1118
+ if ((!hasReviewer || !hasTestEngineer) && directory) {
1119
+ try {
1120
+ const evidence = readTaskEvidenceRaw(directory, taskId);
1121
+ if (evidence?.gates && Array.isArray(evidence.required_gates)) {
1122
+ if (evidence.gates.reviewer != null)
1123
+ hasReviewer = true;
1124
+ if (evidence.gates.test_engineer != null)
1125
+ hasTestEngineer = true;
1126
+ }
1127
+ } catch {}
1128
+ }
1129
+ if (!hasReviewer && !hasTestEngineer)
1130
+ return;
1131
+ if (swarmState.agentSessions.size === 0) {
1132
+ try {
1133
+ startAgentSession("recovery-session", "architect");
1134
+ } catch {}
1135
+ }
1136
+ const recoveryRouteEnforcementEnabled = directory ? (() => {
1137
+ try {
1138
+ return loadPluginConfig(directory).review_routing?.enforce_receipts ?? true;
1139
+ } catch {
1140
+ return true;
1141
+ }
1142
+ })() : false;
1143
+ for (const [sessionId, session] of swarmState.agentSessions) {
1144
+ if (!(session.taskWorkflowStates instanceof Map))
1145
+ continue;
1146
+ const routeEvidence = (() => {
1147
+ const live = getStageBRouteEvidence(session, taskId);
1148
+ if (live.length > 0)
1149
+ return live;
1150
+ try {
1151
+ return routeEvidenceFromTaskGateRequirements(readTaskGateRequirementsReceiptsSync(directory, taskId));
1152
+ } catch {
1153
+ return [];
1154
+ }
1155
+ })();
1156
+ const routeRequired = isStageBRouteRequired(session, taskId) || routeEvidence.length > 0;
1157
+ if (directory && recoveryRouteEnforcementEnabled && routeRequired && !enforcePersistedReviewRouteReceipt({
1158
+ projectRoot: directory,
1159
+ sessionId,
1160
+ taskId,
1161
+ receipts: routeEvidence,
1162
+ enforcementEnabled: true,
1163
+ legacyUnrouted: (() => {
1164
+ try {
1165
+ return !isStageBRouteRequired(session, taskId) && isLegacyTaskGateRequirementsChain(readTaskGateRequirementsReceiptsSync(directory, taskId));
1166
+ } catch {
1167
+ return false;
1168
+ }
1169
+ })(),
1170
+ requireEvidenceBindings: true
1171
+ }).canAdvance) {
1172
+ continue;
1173
+ }
1174
+ const currentState = getTaskState(session, taskId);
1175
+ if (currentState === "tests_run" || currentState === "complete")
1176
+ continue;
1177
+ if (hasReviewer && currentState === "idle") {
1178
+ try {
1179
+ advanceTaskState(session, taskId, "coder_delegated");
1180
+ } catch {}
1181
+ }
1182
+ if (hasReviewer) {
1183
+ recordStageBCompletion(session, taskId, "reviewer");
1184
+ }
1185
+ if (hasTestEngineer) {
1186
+ recordStageBCompletion(session, taskId, "test_engineer");
1187
+ }
1188
+ if (hasReviewer) {
1189
+ const stateNow = getTaskState(session, taskId);
1190
+ if (stateNow === "coder_delegated" || stateNow === "pre_check_passed") {
1191
+ try {
1192
+ advanceTaskState(session, taskId, "reviewer_run");
1193
+ } catch {}
1194
+ }
1195
+ }
1196
+ if (hasTestEngineer) {
1197
+ const stateNow = getTaskState(session, taskId);
1198
+ if (stateNow === "reviewer_run") {
1199
+ try {
1200
+ advanceTaskState(session, taskId, "tests_run");
1201
+ } catch {}
1202
+ }
1203
+ }
1204
+ }
1205
+ }
1206
+ function checkCouncilGate(workingDirectory, taskId) {
1207
+ let councilEnabled = false;
1208
+ let effectiveMinimum = 3;
1209
+ try {
1210
+ const config = loadPluginConfig(workingDirectory);
1211
+ councilEnabled = config.council?.enabled === true;
1212
+ effectiveMinimum = config.council?.requireAllMembers ? 5 : config.council?.minimumMembers ?? 3;
1213
+ } catch {
1214
+ return { blocked: false, reason: "", active: false };
1215
+ }
1216
+ if (!councilEnabled) {
1217
+ return { blocked: false, reason: "", active: false };
1218
+ }
1219
+ try {
1220
+ const planPath = path3.join(workingDirectory, ".swarm", "plan.json");
1221
+ const planRaw = fs3.readFileSync(planPath, "utf-8");
1222
+ const planObj = JSON.parse(planRaw);
1223
+ if (planObj.swarm && planObj.title) {
1224
+ const lookup = getProfileLookupForIdentity(workingDirectory, planObj);
1225
+ if (lookup.kind === "bound") {
1226
+ if (!lookup.profile.gates.council_mode) {
1227
+ return { blocked: false, reason: "", active: false };
1228
+ }
1229
+ } else if (lookup.kind === "unbound_legacy") {
1230
+ if (lookup.profile.gates.council_mode) {
1231
+ return {
1232
+ blocked: true,
1233
+ reason: `council gate required but the QA gate profile is not exact-bound to the current raw swarm_id/plan_title. ${formatLegacyQaBindingRecovery({ swarm: planObj.swarm, title: planObj.title }, "retry advancing this task")}`,
1234
+ active: true
1235
+ };
1236
+ }
1237
+ return { blocked: false, reason: "", active: false };
1238
+ } else {
1239
+ return { blocked: false, reason: "", active: false };
1240
+ }
1241
+ }
1242
+ } catch {
1243
+ return { blocked: false, reason: "", active: false };
1244
+ }
1245
+ let evidence;
1246
+ try {
1247
+ evidence = readTaskEvidenceRaw(workingDirectory, taskId);
1248
+ } catch {
1249
+ return {
1250
+ blocked: true,
1251
+ reason: "council gate required but not yet run \u2014 architect must call submit_council_verdicts before advancing this task",
1252
+ active: true
1253
+ };
1254
+ }
1255
+ const councilGate = evidence?.gates?.council;
1256
+ if (!councilGate) {
1257
+ return {
1258
+ blocked: true,
1259
+ reason: "council gate required but not yet run \u2014 architect must call submit_council_verdicts before advancing this task",
1260
+ active: true
1261
+ };
1262
+ }
1263
+ const councilWorkflow = getTaskWorkflowSnapshot(evidence);
1264
+ if (!councilWorkflow.authoritative || councilWorkflow.state !== "pre_check_passed" || councilGate.workflowGeneration !== councilWorkflow.generation) {
1265
+ return {
1266
+ blocked: true,
1267
+ reason: `council gate evidence is stale or unbound: expected pre_check_passed@${councilWorkflow.generation}, recorded generation ${String(councilGate.workflowGeneration)}`,
1268
+ active: true
1269
+ };
1270
+ }
1271
+ if (councilGate.verdict === "REJECT") {
1272
+ return {
1273
+ blocked: true,
1274
+ reason: "council gate blocked advancement \u2014 resolve requiredFixes and re-run submit_council_verdicts",
1275
+ active: true
1276
+ };
1277
+ }
1278
+ const rawQuorumSize = councilGate.quorumSize;
1279
+ const quorumSize = typeof rawQuorumSize === "number" && Number.isFinite(rawQuorumSize) && rawQuorumSize >= 1 ? rawQuorumSize : 1;
1280
+ if (quorumSize < effectiveMinimum) {
1281
+ return {
1282
+ blocked: true,
1283
+ reason: `council gate blocked advancement \u2014 recorded verdict has insufficient quorum (${quorumSize} of ${effectiveMinimum} required members). Re-run submit_council_verdicts with the missing council members.`,
1284
+ active: true
1285
+ };
1286
+ }
1287
+ return { blocked: false, reason: "", active: true };
1288
+ }
1289
+ async function executeUpdateTaskStatus(args, fallbackDir, ctx) {
1290
+ const statusError = validateStatus(args.status);
1291
+ if (statusError) {
1292
+ return {
1293
+ success: false,
1294
+ message: "Validation failed",
1295
+ errors: [statusError]
1296
+ };
1297
+ }
1298
+ const taskIdError = validateTaskId(args.task_id);
1299
+ if (taskIdError) {
1300
+ return {
1301
+ success: false,
1302
+ message: "Validation failed",
1303
+ errors: [taskIdError]
1304
+ };
1305
+ }
1306
+ let directory;
1307
+ const resolveResult = _internals3.resolveWorkingDirectory(args.working_directory, fallbackDir);
1308
+ if (!resolveResult.success) {
1309
+ return {
1310
+ success: false,
1311
+ message: resolveResult.message,
1312
+ errors: [resolveResult.message]
1313
+ };
1314
+ }
1315
+ directory = resolveResult.directory;
1316
+ try {
1317
+ assertProjectRoot(directory);
1318
+ } catch (error) {
1319
+ const message = error instanceof Error ? error.message : String(error);
1320
+ return {
1321
+ success: false,
1322
+ message,
1323
+ errors: [message]
1324
+ };
1325
+ }
1326
+ const planPath = path3.join(directory, ".swarm", "plan.json");
1327
+ if (!fs3.existsSync(planPath)) {
1328
+ return {
1329
+ success: false,
1330
+ message: `Invalid working_directory: plan not found in "${directory}"`,
1331
+ errors: [`Invalid working_directory: plan not found in "${directory}"`]
1332
+ };
1333
+ }
1334
+ if (fallbackDir && directory !== fallbackDir) {
1335
+ const canonicalDir = fs3.realpathSync(path3.resolve(directory));
1336
+ const canonicalRoot = fs3.realpathSync(path3.resolve(fallbackDir));
1337
+ if (isStrictPathDescendant(canonicalDir, canonicalRoot) && !hasExplicitProjectBoundary(canonicalDir)) {
1338
+ return {
1339
+ success: false,
1340
+ message: `Invalid working_directory: "${directory}" is a subdirectory of ` + `the project root "${fallbackDir}". Pass the project root path or ` + `omit working_directory entirely.`,
1341
+ errors: [
1342
+ `Subdirectory rejected: use project root "${fallbackDir}" instead`
1343
+ ]
1344
+ };
1345
+ }
1346
+ }
1347
+ let exactRepairRetry = false;
1348
+ if (isRepairRetryShape(args)) {
1349
+ try {
1350
+ const repairWal = await readWorkflowWalFile("task-repair", validateSwarmPath(directory, `task-repairs/${args.task_id}.json`), args.task_id);
1351
+ exactRepairRetry = repairWal?.state === "PREPARED" && repairWal.transitionId === args.transition_id;
1352
+ } catch (error) {
1353
+ return {
1354
+ success: false,
1355
+ message: "Task operation paused while recovering an interrupted task repair",
1356
+ errors: [error instanceof Error ? error.message : String(error)]
1357
+ };
1358
+ }
1359
+ }
1360
+ try {
1361
+ const recoveredCoder = await recoverCoderSettlement(directory, args.task_id);
1362
+ if (recoveredCoder && ctx?.sessionID) {
1363
+ const callerSession = ensureAgentSession(ctx.sessionID);
1364
+ const workflow = getTaskWorkflowSnapshot(recoveredCoder.evidence);
1365
+ callerSession.taskWorkflowStates.set(args.task_id, workflow.state);
1366
+ callerSession.stageBCompletion?.delete(args.task_id);
1367
+ updateTaskWorkflowCache(callerSession, args.task_id, workflow);
1368
+ }
1369
+ } catch (error) {
1370
+ return {
1371
+ success: false,
1372
+ message: "Task operation paused while recovering an interrupted coder settlement",
1373
+ errors: [error instanceof Error ? error.message : String(error)]
1374
+ };
1375
+ }
1376
+ try {
1377
+ const recoveredTerminal = await recoverPreparedTaskTerminal(directory, args.task_id, ctx?.sessionID ?? "update-task-status");
1378
+ if (recoveredTerminal) {
1379
+ syncCallerWorkflowFromEvidence(ctx?.sessionID, args.task_id, recoveredTerminal.evidence);
1380
+ }
1381
+ } catch (error) {
1382
+ return {
1383
+ success: false,
1384
+ message: "Task operation paused while recovering an interrupted terminal status transition",
1385
+ errors: [error instanceof Error ? error.message : String(error)]
1386
+ };
1387
+ }
1388
+ if (!exactRepairRetry) {
1389
+ try {
1390
+ const recoveredRepair = await recoverPreparedTaskRepair(directory, args.task_id, ctx?.sessionID ?? "update-task-status");
1391
+ if (recoveredRepair) {
1392
+ syncCallerWorkflowFromEvidence(ctx?.sessionID, args.task_id, readTaskEvidenceRaw(directory, args.task_id));
1393
+ }
1394
+ } catch (error) {
1395
+ if (error instanceof Error && error.message.startsWith("TASK_REPAIR_AUDIT_LOCKED")) {
1396
+ criticalWarn(`[update-task-status] task-repair audit recovery deferred for ${args.task_id}: ${error.message}`);
1397
+ } else {
1398
+ return {
1399
+ success: false,
1400
+ message: "Task operation paused while recovering an interrupted task repair",
1401
+ errors: [error instanceof Error ? error.message : String(error)]
1402
+ };
1403
+ }
1404
+ }
1405
+ }
1406
+ let loadedPlan = null;
1407
+ try {
1408
+ loadedPlan = await _internals3.loadPlan(directory);
1409
+ } catch (error) {
1410
+ return {
1411
+ success: false,
1412
+ message: "Failed to load plan for task validation",
1413
+ errors: [error instanceof Error ? error.message : String(error)]
1414
+ };
1415
+ }
1416
+ if (!loadedPlan) {
1417
+ return {
1418
+ success: false,
1419
+ message: "Failed to load plan for task validation",
1420
+ errors: ["Plan could not be loaded"]
1421
+ };
1422
+ }
1423
+ if (loadedPlan._ledgerReplayStale === true) {
1424
+ const staleReason = loadedPlan._ledgerReplayStaleReason ?? "plan.json is stale relative to the active plan-ledger authority";
1425
+ return {
1426
+ success: false,
1427
+ message: `Task status update refused: plan.json is stale relative to the ledger (ledger replay failed). ${staleReason}`,
1428
+ errors: [staleReason],
1429
+ recovery_guidance: "Plan state could not be reconciled with the active plan-ledger authority. " + "Retry save_plan with the unchanged loaded plan so it can reconverge the projections and authority metadata, then retry update_task_status."
1430
+ };
1431
+ }
1432
+ const currentTask = loadedPlan.phases.flatMap((phase) => phase.tasks).find((task) => task.id === args.task_id);
1433
+ if (!currentTask) {
1434
+ return {
1435
+ success: false,
1436
+ message: "Failed to update task status",
1437
+ errors: [`Task not found: ${args.task_id}`]
1438
+ };
1439
+ }
1440
+ {
1441
+ const settledRejection = getSettledTransitionRejection(args.task_id, currentTask.status, args.status, args.force);
1442
+ if (settledRejection) {
1443
+ return settledRejection;
1444
+ }
1445
+ }
1446
+ if (args.status === "in_progress" && args.force === true) {
1447
+ const repairFieldsValid = typeof args.expected_state === "string" && args.expected_state.length > 0 && Number.isInteger(args.expected_generation) && (args.expected_generation ?? -1) >= 0 && args.target_state === "idle" && typeof args.reason === "string" && args.reason.trim().length > 0 && args.reason.length <= 2000 && typeof args.transition_id === "string" && args.transition_id.trim().length > 0 && args.transition_id.length <= 200;
1448
+ if (!repairFieldsValid) {
1449
+ return {
1450
+ success: false,
1451
+ message: "TASK_REPAIR_INVALID: force repair requires an exact CAS and audit identity",
1452
+ errors: [
1453
+ 'Provide expected_state, expected_generation, target_state="idle", a non-empty reason (max 2000 chars), and a non-empty transition_id (max 200 chars).'
1454
+ ]
1455
+ };
1456
+ }
1457
+ log(`[update-task-status] Force-override: attempting audited repair for task ${args.task_id} (settled task, or in_progress at the retry-escalation rung per issue #3389; admission is verified under the plan lock) to in_progress`);
1458
+ }
1459
+ const agentName = (ctx?.sessionID ? swarmState.activeAgent.get(ctx.sessionID) : undefined) ?? "update-task-status";
1460
+ let scopeAdvisories;
1461
+ if (args.status === "completed") {
1462
+ if (!fallbackDir && !ctx?.sessionID) {
1463
+ recoverTaskStateFromDelegations(args.task_id, directory);
1464
+ }
1465
+ let phaseRequiresReviewer = true;
1466
+ let taskDeclaresExternalEvidence = false;
1467
+ try {
1468
+ const planPath2 = path3.join(directory, ".swarm", "plan.json");
1469
+ const planRaw = fs3.readFileSync(planPath2, "utf-8");
1470
+ const plan = JSON.parse(planRaw);
1471
+ const taskPhase = plan.phases.find((p) => p.tasks.some((t) => t.id === args.task_id));
1472
+ if (taskPhase?.required_agents && !taskPhase.required_agents.includes("reviewer")) {
1473
+ phaseRequiresReviewer = false;
1474
+ }
1475
+ taskDeclaresExternalEvidence = taskPhase?.tasks.find((t) => t.id === args.task_id)?.evidence_class === "external_git";
1476
+ } catch {}
1477
+ const councilCheck = taskDeclaresExternalEvidence ? { blocked: false, active: false } : checkCouncilGate(directory, args.task_id);
1478
+ if (councilCheck.blocked) {
1479
+ await recordRunMemoryOutcome(directory, {
1480
+ taskId: args.task_id,
1481
+ agent: agentName,
1482
+ outcome: "fail",
1483
+ failureReason: `council gate: ${councilCheck.reason}`,
1484
+ fileTargets: resolveRunMemoryFileTargets(loadedPlan, args.task_id)
1485
+ });
1486
+ return {
1487
+ success: false,
1488
+ message: "Gate check failed: council gate not yet satisfied for task " + args.task_id,
1489
+ errors: [councilCheck.reason]
1490
+ };
1491
+ }
1492
+ if (phaseRequiresReviewer && !councilCheck.active && !taskDeclaresExternalEvidence) {
1493
+ const reviewerCheck = await checkReviewerGateWithScope(args.task_id, directory, ctx?.sessionID, ctx?.sessionID ? fallbackDir : undefined);
1494
+ if (reviewerCheck.reason) {
1495
+ const advisoryLines = reviewerCheck.reason.split(`
1496
+ `).filter((line) => line.length > 0);
1497
+ if (advisoryLines.length > 0)
1498
+ scopeAdvisories = advisoryLines;
1499
+ }
1500
+ if (reviewerCheck.blocked) {
1501
+ await recordRunMemoryOutcome(directory, {
1502
+ taskId: args.task_id,
1503
+ agent: agentName,
1504
+ outcome: "fail",
1505
+ failureReason: `QA gate: ${reviewerCheck.reason}`,
1506
+ fileTargets: resolveRunMemoryFileTargets(loadedPlan, args.task_id)
1507
+ });
1508
+ return {
1509
+ success: false,
1510
+ message: "Gate check failed: required QA gates not yet satisfied for task " + args.task_id,
1511
+ errors: [reviewerCheck.reason]
1512
+ };
1513
+ }
1514
+ }
1515
+ }
1516
+ const lockTaskId = `update-task-status-${args.task_id}-${Date.now()}`;
1517
+ const planFilePath = "plan.json";
1518
+ let lockResult;
1519
+ try {
1520
+ lockResult = await _internals3.tryAcquireLock(directory, planFilePath, agentName, lockTaskId);
1521
+ } catch (error) {
1522
+ return {
1523
+ success: false,
1524
+ message: "Failed to acquire lock for task status update",
1525
+ errors: [error instanceof Error ? error.message : String(error)]
1526
+ };
1527
+ }
1528
+ if (!lockResult.acquired) {
1529
+ return {
1530
+ success: false,
1531
+ message: `Task status write blocked: plan.json is locked by ${lockResult.existing?.agent ?? "another agent"} (task: ${lockResult.existing?.taskId ?? "unknown"})`,
1532
+ errors: [
1533
+ "Concurrent plan write detected \u2014 retry after the current write completes"
1534
+ ],
1535
+ recovery_guidance: "Wait a moment and retry update_task_status. The lock will expire automatically if the holding agent fails."
1536
+ };
1537
+ }
1538
+ try {
1539
+ const lockedPlan = await _internals3.loadPlan(directory);
1540
+ if (lockedPlan?._ledgerReplayStale === true) {
1541
+ return {
1542
+ success: false,
1543
+ message: "Task status update refused: plan became ledger-stale while waiting for the lock",
1544
+ errors: [
1545
+ lockedPlan._ledgerReplayStaleReason ?? "plan.json is stale relative to the authoritative ledger"
1546
+ ],
1547
+ recovery_guidance: "Retry save_plan with the unchanged loaded plan to reconverge the projection/hash, then retry update_task_status."
1548
+ };
1549
+ }
1550
+ if (!lockedPlan) {
1551
+ return {
1552
+ success: false,
1553
+ message: "Failed to update task status",
1554
+ errors: ["No approved plan is available under the plan lock"]
1555
+ };
1556
+ }
1557
+ let authoritativePlan = lockedPlan;
1558
+ const lockedPreparedRepairWal = await readPreparedWorkflowWal(directory, args.task_id, "task-repair");
1559
+ const lockedExactRepairRetry = isRepairRetryShape(args) && lockedPreparedRepairWal?.transitionId === args.transition_id;
1560
+ if (lockedPreparedRepairWal && !lockedExactRepairRetry) {
1561
+ try {
1562
+ const recoveredRepair = await recoverPreparedTaskRepairUnderPlanLock(directory, args.task_id, ctx?.sessionID ?? "update-task-status", authoritativePlan);
1563
+ if (recoveredRepair) {
1564
+ authoritativePlan = recoveredRepair.plan;
1565
+ syncCallerWorkflowFromEvidence(ctx?.sessionID, args.task_id, readTaskEvidenceRaw(directory, args.task_id));
1566
+ }
1567
+ } catch (error) {
1568
+ if (error instanceof Error && error.message.startsWith("TASK_REPAIR_AUDIT_LOCKED")) {
1569
+ criticalWarn(`[update-task-status] task-repair audit recovery deferred for ${args.task_id}: ${error.message}`);
1570
+ } else {
1571
+ return {
1572
+ success: false,
1573
+ message: "Task operation paused while recovering an interrupted task repair",
1574
+ errors: [error instanceof Error ? error.message : String(error)]
1575
+ };
1576
+ }
1577
+ }
1578
+ }
1579
+ if (await hasPreparedWorkflowWal(directory, args.task_id, "task-terminal")) {
1580
+ try {
1581
+ const recoveredTerminal = await recoverPreparedTaskTerminalUnderPlanLock(directory, args.task_id, ctx?.sessionID ?? "update-task-status", authoritativePlan);
1582
+ if (recoveredTerminal) {
1583
+ authoritativePlan = recoveredTerminal.plan;
1584
+ syncCallerWorkflowFromEvidence(ctx?.sessionID, args.task_id, recoveredTerminal.evidence);
1585
+ }
1586
+ } catch (error) {
1587
+ return {
1588
+ success: false,
1589
+ message: "Task operation paused while recovering an interrupted terminal status transition",
1590
+ errors: [error instanceof Error ? error.message : String(error)]
1591
+ };
1592
+ }
1593
+ }
1594
+ const lockedTask = authoritativePlan.phases.flatMap((phase) => phase.tasks).find((task) => task.id === args.task_id);
1595
+ if (!lockedTask) {
1596
+ return {
1597
+ success: false,
1598
+ message: "Failed to update task status",
1599
+ errors: [`Task not found: ${args.task_id}`]
1600
+ };
1601
+ }
1602
+ const lockedSettledRejection = getSettledTransitionRejection(args.task_id, lockedTask.status, args.status, args.force);
1603
+ if (lockedSettledRejection) {
1604
+ return lockedSettledRejection;
1605
+ }
1606
+ const forceRepair = args.status === "in_progress" && args.force === true;
1607
+ const repairResult = forceRepair ? await repairTaskWorkflowUnderPlanLock({
1608
+ directory,
1609
+ taskId: args.task_id,
1610
+ actor: agentName,
1611
+ reason: args.reason,
1612
+ transitionId: args.transition_id,
1613
+ expectedState: args.expected_state,
1614
+ expectedGeneration: args.expected_generation,
1615
+ currentPlanStatus: lockedTask.status,
1616
+ currentPlan: authoritativePlan,
1617
+ updatePlan: () => _internals3.updateTaskStatus(directory, args.task_id, "in_progress", { force: true, planLockAlreadyHeld: true })
1618
+ }) : null;
1619
+ let updatedPlan;
1620
+ let terminalEvidence = null;
1621
+ if (args.status === "completed") {
1622
+ const lockedTaskPhase = authoritativePlan.phases.find((phase) => phase.tasks.some((task) => task.id === args.task_id));
1623
+ const lockedPhaseRequiresReviewer = !lockedTaskPhase?.required_agents || lockedTaskPhase.required_agents.includes("reviewer");
1624
+ const lockedTaskDeclaresExternal = lockedTask.evidence_class === "external_git";
1625
+ const terminalResult = await commitTaskTerminalUnderPlanLock({
1626
+ directory,
1627
+ taskId: args.task_id,
1628
+ actor: agentName,
1629
+ transitionId: `plan-status:${args.task_id}:completed:${Date.now()}`,
1630
+ currentPlanStatus: lockedTask.status,
1631
+ targetStatus: "completed",
1632
+ qaExempt: !lockedPhaseRequiresReviewer && !lockedTaskDeclaresExternal,
1633
+ resolveTerminal: (evidence) => ({
1634
+ targetStatus: "completed",
1635
+ qaExempt: !lockedPhaseRequiresReviewer && !lockedTaskDeclaresExternal,
1636
+ readOnlyNoMutation: !lockedPhaseRequiresReviewer ? false : isReadOnlyNoMutationEligible(evidence, {
1637
+ currentDeclaredFiles: lockedTask.files_touched
1638
+ }),
1639
+ externalEvidence: lockedTaskDeclaresExternal
1640
+ }),
1641
+ currentPlan: authoritativePlan,
1642
+ validateEvidence: async () => {
1643
+ if (lockedTaskDeclaresExternal) {
1644
+ const externalTaskEvidence = _internals3.readTaskEvidenceRaw(directory, args.task_id);
1645
+ const planIdentity = {
1646
+ title: authoritativePlan.title,
1647
+ swarm: authoritativePlan.swarm
1648
+ };
1649
+ const externalWorkflow = getTaskWorkflowSnapshot(externalTaskEvidence);
1650
+ if (!isExternalEvidenceInitialCompletionEligible(planIdentity, lockedTask, externalTaskEvidence) && !(externalWorkflow.state === "complete" && externalProofCurrent(planIdentity, lockedTask, externalTaskEvidence))) {
1651
+ throw new Error(`TASK_COMPLETION_CAS_MISMATCH: Task ${args.task_id} declares external_git evidence but has no current matched attestation; run /swarm attest ${args.task_id} first`);
1652
+ }
1653
+ return;
1654
+ }
1655
+ const lockedGate = checkReviewerGate(args.task_id, directory, false, ctx?.sessionID, fallbackDir ?? directory, lockedTask.files_touched ?? null);
1656
+ const lockedCouncil = checkCouncilGate(directory, args.task_id);
1657
+ if (lockedCouncil.blocked || lockedPhaseRequiresReviewer && !lockedCouncil.active && lockedGate.blocked) {
1658
+ throw new Error(`TASK_COMPLETION_CAS_MISMATCH: ${lockedCouncil.blocked ? lockedCouncil.reason : lockedGate.reason}`);
1659
+ }
1660
+ },
1661
+ updatePlan: async () => {
1662
+ const next = await _internals3.updateTaskStatus(directory, args.task_id, "completed", { force: false, planLockAlreadyHeld: true });
1663
+ const persisted = next.phases.flatMap((phase) => phase.tasks).find((task) => task.id === args.task_id);
1664
+ if (persisted?.status !== "completed") {
1665
+ throw new Error("TASK_STATUS_WRITE_NOT_APPLIED");
1666
+ }
1667
+ return next;
1668
+ }
1669
+ });
1670
+ updatedPlan = terminalResult.plan;
1671
+ terminalEvidence = terminalResult.evidence;
1672
+ } else if (args.status === "blocked") {
1673
+ const terminalResult = await commitTaskTerminalUnderPlanLock({
1674
+ directory,
1675
+ taskId: args.task_id,
1676
+ actor: agentName,
1677
+ transitionId: `plan-status:${args.task_id}:blocked:${Date.now()}`,
1678
+ currentPlanStatus: lockedTask.status,
1679
+ targetStatus: "blocked",
1680
+ qaExempt: false,
1681
+ currentPlan: authoritativePlan,
1682
+ updatePlan: async () => {
1683
+ const next = await _internals3.updateTaskStatus(directory, args.task_id, "blocked", { force: false, planLockAlreadyHeld: true });
1684
+ const persisted = next.phases.flatMap((phase) => phase.tasks).find((task) => task.id === args.task_id);
1685
+ if (persisted?.status !== "blocked") {
1686
+ throw new Error("TASK_STATUS_WRITE_NOT_APPLIED");
1687
+ }
1688
+ return next;
1689
+ }
1690
+ });
1691
+ updatedPlan = terminalResult.plan;
1692
+ terminalEvidence = terminalResult.evidence;
1693
+ } else {
1694
+ updatedPlan = repairResult?.plan ?? await _internals3.updateTaskStatus(directory, args.task_id, args.status, { force: false, planLockAlreadyHeld: true });
1695
+ const persisted = updatedPlan.phases.flatMap((phase) => phase.tasks).find((task) => task.id === args.task_id);
1696
+ if (persisted?.status !== args.status) {
1697
+ throw new Error("TASK_STATUS_WRITE_NOT_APPLIED");
1698
+ }
1699
+ }
1700
+ if (terminalEvidence) {
1701
+ const terminal = getTaskWorkflowSnapshot(terminalEvidence);
1702
+ const callerSession = ctx?.sessionID ? swarmState.agentSessions.get(ctx.sessionID) : undefined;
1703
+ if (callerSession) {
1704
+ const session = callerSession;
1705
+ session.taskWorkflowStates.set(args.task_id, terminal.state);
1706
+ session.stageBCompletion?.delete(args.task_id);
1707
+ updateTaskWorkflowCache(session, args.task_id, terminal);
1708
+ }
1709
+ }
1710
+ if (args.status === "in_progress") {
1711
+ if (ctx?.sessionID)
1712
+ ensureAgentSession(ctx.sessionID).currentTaskId = args.task_id;
1713
+ if (repairResult) {
1714
+ const callerSession = ctx?.sessionID ? swarmState.agentSessions.get(ctx.sessionID) : undefined;
1715
+ if (callerSession) {
1716
+ const session = callerSession;
1717
+ session.taskWorkflowStates.set(args.task_id, "idle");
1718
+ session.stageBCompletion?.delete(args.task_id);
1719
+ session.taskCouncilApproved?.delete(args.task_id);
1720
+ session.taskCouncilWorkflowGeneration?.delete(args.task_id);
1721
+ updateTaskWorkflowCache(session, args.task_id, {
1722
+ generation: repairResult.generation
1723
+ });
1724
+ }
1725
+ }
1726
+ }
1727
+ return {
1728
+ success: true,
1729
+ message: "Task status updated successfully",
1730
+ task_id: args.task_id,
1731
+ new_status: args.status,
1732
+ current_phase: updatedPlan.current_phase,
1733
+ ...scopeAdvisories && scopeAdvisories.length > 0 ? { warnings: scopeAdvisories } : {}
1734
+ };
1735
+ } catch (error) {
1736
+ return {
1737
+ success: false,
1738
+ message: "Failed to update task status",
1739
+ errors: [error instanceof Error ? error.message : String(error)]
1740
+ };
1741
+ } finally {
1742
+ if (lockResult?.acquired && lockResult.lock._release) {
1743
+ try {
1744
+ await lockResult.lock._release();
1745
+ } catch (releaseError) {
1746
+ log("[update-task-status] Lock release failed:", releaseError);
1747
+ }
1748
+ }
1749
+ }
1750
+ }
1751
+ var update_task_status = createSwarmTool({
1752
+ description: "Update the status of a specific task in the implementation plan. " + "Task status can be one of: pending, in_progress, completed, blocked.",
1753
+ args: {
1754
+ task_id: exports_external.string().min(1).regex(/^\d+\.\d+(\.\d+)*$/, "Task ID must be in N.M or N.M.P format").describe('Task ID in N.M format, e.g. "1.1", "1.2.3"'),
1755
+ status: exports_external.enum(["pending", "in_progress", "completed", "blocked"]).describe("New status for the task: pending, in_progress, completed, or blocked"),
1756
+ working_directory: exports_external.string().optional().describe("Working directory where the plan is located"),
1757
+ force: exports_external.boolean().optional().default(false).describe('Enable the audited repair contract: re-open a settled task, or an in_progress task that reached the retry ladder user_escalation rung (issue #3389). Requires expected_state, expected_generation, target_state="idle", reason, and transition_id.'),
1758
+ expected_state: exports_external.string().optional().describe("CAS workflow state expected by a force repair."),
1759
+ expected_generation: exports_external.number().int().min(0).optional().describe("ABA-safe workflow generation expected by a force repair."),
1760
+ target_state: exports_external.literal("idle").optional().describe("Repair target state. The only valid target is idle (re-opens the task for new work)."),
1761
+ reason: exports_external.string().min(1).max(2000).optional().describe("Required human-readable audit reason for a force repair."),
1762
+ transition_id: exports_external.string().min(1).max(200).optional().describe("Caller-generated idempotency key for an audited force repair.")
1763
+ },
1764
+ execute: async (args, _directory, _ctx) => {
1765
+ return JSON.stringify(await executeUpdateTaskStatus(args, _directory, _ctx), null, 2);
1766
+ }
1767
+ });
1768
+
1769
+ // src/tools/attest-task.ts
1770
+ var ANCHOR_STDOUT_MAX_CHARS = 4096;
1771
+ var ANCHOR_SPAWN_TIMEOUT_MS = 1e4;
1772
+ var ANCHOR_SPAWN_MAX_BUFFER_BYTES = 256 * 1024;
1773
+ var ATTEST_TOTAL_DEADLINE_MS = 30000;
1774
+ var READ_ONLY_GIT_FLAGS = {
1775
+ "rev-list": { "--count": 0, "--max-count": 1, "--format": 1, "--skip": 1 },
1776
+ log: {
1777
+ "--format": 1,
1778
+ "--max-count": 1,
1779
+ "-n": 1,
1780
+ "--oneline": 0,
1781
+ "--no-patch": 0,
1782
+ "--stat": 0,
1783
+ "--name-only": 0,
1784
+ "-1": 0,
1785
+ "-2": 0,
1786
+ "-3": 0,
1787
+ "-4": 0,
1788
+ "-5": 0,
1789
+ "-9": 0
1790
+ },
1791
+ show: {
1792
+ "--format": 1,
1793
+ "--stat": 0,
1794
+ "--name-only": 0,
1795
+ "--no-patch": 0,
1796
+ "--quiet": 0
1797
+ },
1798
+ diff: { "--stat": 0, "--name-only": 0, "--quiet": 0 },
1799
+ "rev-parse": { "--verify": 0, "--short": 0 }
1800
+ };
1801
+ var REMOTE_HELPER_PATTERN = /^(ext::|[a-z+.-]+::)/;
1802
+ function anchorEnv() {
1803
+ const env = {
1804
+ PATH: process.env.PATH,
1805
+ HOME: process.env.HOME,
1806
+ LANG: process.env.LANG,
1807
+ LC_ALL: process.env.LC_ALL,
1808
+ GIT_CONFIG_NOSYSTEM: "1"
1809
+ };
1810
+ if (process.platform === "win32") {
1811
+ env.SYSTEMROOT = process.env.SYSTEMROOT;
1812
+ env.WINDIR = process.env.WINDIR;
1813
+ env.COMSPEC = process.env.COMSPEC;
1814
+ }
1815
+ const envOverrides = {
1816
+ GIT_EXTERNAL_DIFF: null,
1817
+ GIT_CONFIG_PARAMETERS: null,
1818
+ GIT_CONFIG_COUNT: null,
1819
+ GIT_DIR: null,
1820
+ GIT_WORK_TREE: null,
1821
+ GIT_INDEX_FILE: null,
1822
+ GIT_SSH_COMMAND: null,
1823
+ GIT_ASKPASS: null,
1824
+ GIT_EXEC_PATH: null,
1825
+ GIT_PAGER: null
1826
+ };
1827
+ return { env, envOverrides };
1828
+ }
1829
+ function buildAnchorArgv(command) {
1830
+ const tokens = command.trim().split(/\s+/).filter(Boolean);
1831
+ if (tokens[0] !== "git" || tokens.length < 2) {
1832
+ return {
1833
+ argv: null,
1834
+ refusal: `anchor command is not an allowed read-only git invocation: "${command}"`
1835
+ };
1836
+ }
1837
+ const subcommand = tokens[1];
1838
+ if (subcommand.startsWith("-")) {
1839
+ return {
1840
+ argv: null,
1841
+ refusal: `anchor command smuggles a git global option ("${subcommand}"); only bare read-only git subcommands are allowed`
1842
+ };
1843
+ }
1844
+ const allowed = READ_ONLY_GIT_FLAGS[subcommand];
1845
+ if (!allowed) {
1846
+ return {
1847
+ argv: null,
1848
+ refusal: `git "${subcommand}" is not in the read-only git subcommand allowlist (rev-list, log, rev-parse, show, diff)`
1849
+ };
1850
+ }
1851
+ const finalTokens = [subcommand];
1852
+ for (let i = 2;i < tokens.length; i++) {
1853
+ const token = tokens[i];
1854
+ if (token.startsWith("-")) {
1855
+ const eq = token.indexOf("=");
1856
+ const flag = eq >= 0 ? token.slice(0, eq) : token;
1857
+ const inlineValue = eq >= 0 ? token.slice(eq + 1) : undefined;
1858
+ const arity = allowed[flag];
1859
+ if (arity === undefined) {
1860
+ return {
1861
+ argv: null,
1862
+ refusal: `flag "${flag}" is not in the read-only git allowlist for git ${subcommand}`
1863
+ };
1864
+ }
1865
+ if (arity === 0) {
1866
+ if (inlineValue !== undefined) {
1867
+ return {
1868
+ argv: null,
1869
+ refusal: `flag "${flag}" takes no value; =-joined forms are not valid for it`
1870
+ };
1871
+ }
1872
+ finalTokens.push(flag);
1873
+ } else {
1874
+ let value = inlineValue;
1875
+ if (value === undefined) {
1876
+ const next = tokens[i + 1];
1877
+ if (next === undefined || next.startsWith("--")) {
1878
+ return {
1879
+ argv: null,
1880
+ refusal: `flag "${flag}" requires a value token`
1881
+ };
1882
+ }
1883
+ value = next;
1884
+ i++;
1885
+ }
1886
+ finalTokens.push(flag.startsWith("--") ? `${flag}=${value}` : `${flag}${value}`);
1887
+ }
1888
+ continue;
1889
+ }
1890
+ const hasControlByte = Array.from(token).some((ch) => ch.charCodeAt(0) < 32 || ch.charCodeAt(0) === 127);
1891
+ if (token.length > 400 || hasControlByte) {
1892
+ return {
1893
+ argv: null,
1894
+ refusal: "anchor argument is too long or contains control bytes"
1895
+ };
1896
+ }
1897
+ if (REMOTE_HELPER_PATTERN.test(token)) {
1898
+ return {
1899
+ argv: null,
1900
+ refusal: `anchor argument "${token.slice(0, 32)}" looks like a remote-helper URL; remote helpers can execute commands and are not read-only git invocations`
1901
+ };
1902
+ }
1903
+ finalTokens.push(token);
1904
+ }
1905
+ if (subcommand === "diff" || subcommand === "show" || subcommand === "log") {
1906
+ finalTokens.push("--no-ext-diff", "--no-textconv");
1907
+ }
1908
+ const git = resolveGitExecutable();
1909
+ const argv = [
1910
+ git,
1911
+ "--no-pager",
1912
+ "-c",
1913
+ "diff.external=",
1914
+ "-c",
1915
+ "core.pager=cat",
1916
+ "-c",
1917
+ "core.fsmonitor=",
1918
+ "-c",
1919
+ "gpg.program=",
1920
+ "-c",
1921
+ "credential.helper=",
1922
+ ...finalTokens
1923
+ ];
1924
+ return { argv, refusal: null };
1925
+ }
1926
+ async function runAnchor(argv, cwd, timeoutMs) {
1927
+ const { env, envOverrides } = anchorEnv();
1928
+ const proc = bunSpawn(argv, {
1929
+ cwd,
1930
+ env,
1931
+ envOverrides,
1932
+ stdin: "ignore",
1933
+ stdout: "pipe",
1934
+ stderr: "pipe",
1935
+ timeout: timeoutMs,
1936
+ maxBuffer: ANCHOR_SPAWN_MAX_BUFFER_BYTES
1937
+ });
1938
+ try {
1939
+ const exited = await proc.exited;
1940
+ let stdout;
1941
+ try {
1942
+ stdout = await proc.stdout.text();
1943
+ } catch {
1944
+ return {
1945
+ exitCode: exited,
1946
+ stdout: "",
1947
+ refusal: `anchor output exceeded the ${Math.floor(ANCHOR_SPAWN_MAX_BUFFER_BYTES / 1024)}KB spawn capture buffer; refusing rather than truncating`
1948
+ };
1949
+ }
1950
+ if (exited !== 0) {
1951
+ const stderr = await proc.stderr.text();
1952
+ const detail = stderr.trim().slice(0, 200);
1953
+ return {
1954
+ exitCode: exited,
1955
+ stdout,
1956
+ refusal: `anchor command exited with code ${exited}${detail ? `: ${detail}` : ""}`
1957
+ };
1958
+ }
1959
+ return { exitCode: exited, stdout, refusal: null };
1960
+ } finally {
1961
+ try {
1962
+ proc.kill();
1963
+ } catch {}
1964
+ }
1965
+ }
1966
+ var _internals4 = {
1967
+ runAnchor,
1968
+ buildAnchorArgv,
1969
+ loadPlanTask: (directory, taskId) => {
1970
+ const planPath = path4.join(directory, ".swarm", "plan.json");
1971
+ if (!fs4.existsSync(planPath))
1972
+ return null;
1973
+ const plan = PlanSchema.parse(JSON.parse(fs4.readFileSync(planPath, "utf-8")));
1974
+ for (const phase of plan.phases) {
1975
+ for (const task of phase.tasks) {
1976
+ if (task.id === taskId) {
1977
+ return { title: plan.title, swarm: plan.swarm, task };
1978
+ }
1979
+ }
1980
+ }
1981
+ return null;
1982
+ }
1983
+ };
1984
+ function validateAnchorExpectations(anchors) {
1985
+ for (const anchor of anchors) {
1986
+ if (anchor.expect === undefined && anchor.expect_prefix === undefined) {
1987
+ return `anchor "${anchor.command.slice(0, 60)}" declares neither expect nor expect_prefix`;
1988
+ }
1989
+ if (anchor.expect_prefix !== undefined && anchor.expect_prefix.length === 0) {
1990
+ return `anchor "${anchor.command.slice(0, 60)}" declares an empty expect_prefix, which would match any output`;
1991
+ }
1992
+ }
1993
+ return null;
1994
+ }
1995
+ async function executeAttestTask(args, fallbackDir) {
1996
+ const resolveResult = _internals3.resolveWorkingDirectory(args.working_directory, fallbackDir);
1997
+ if (!resolveResult.success) {
1998
+ return {
1999
+ success: false,
2000
+ message: resolveResult.message,
2001
+ errors: [resolveResult.message]
2002
+ };
2003
+ }
2004
+ const directory = resolveResult.directory;
2005
+ try {
2006
+ assertProjectRoot(directory);
2007
+ } catch (error) {
2008
+ const message = error instanceof Error ? error.message : String(error);
2009
+ return { success: false, message, errors: [message] };
2010
+ }
2011
+ let found;
2012
+ try {
2013
+ found = _internals4.loadPlanTask(directory, args.task_id);
2014
+ } catch (error) {
2015
+ const message = error instanceof Error ? error.message : String(error);
2016
+ if (message.includes("expect_prefix") || message.includes("evidence_anchors")) {
2017
+ const refusal2 = "the plan declares an invalid external-evidence anchor: every anchor must carry expect and/or a non-empty expect_prefix (an empty prefix would match any output)";
2018
+ return {
2019
+ success: false,
2020
+ message: refusal2,
2021
+ errors: [refusal2],
2022
+ task_id: args.task_id
2023
+ };
2024
+ }
2025
+ const refusal = `the plan at ${directory} is invalid or unreadable; fix or re-save the plan before attesting`;
2026
+ return {
2027
+ success: false,
2028
+ message: refusal,
2029
+ errors: [`${refusal} (${message.slice(0, 160)})`],
2030
+ task_id: args.task_id
2031
+ };
2032
+ }
2033
+ if (!found) {
2034
+ const message = `Task ${args.task_id} not found in the plan at ${directory}`;
2035
+ return {
2036
+ success: false,
2037
+ message,
2038
+ errors: [message],
2039
+ task_id: args.task_id
2040
+ };
2041
+ }
2042
+ if (found.task.evidence_class !== "external_git") {
2043
+ const message = `Task ${args.task_id} does not declare evidence_class external_git; the attest path is ` + `fail-closed to explicitly declared external-evidence tasks (issue #3422)`;
2044
+ return {
2045
+ success: false,
2046
+ message,
2047
+ errors: [message],
2048
+ task_id: args.task_id
2049
+ };
2050
+ }
2051
+ const anchors = found.task.evidence_anchors ?? [];
2052
+ if (anchors.length === 0) {
2053
+ const message = `Task ${args.task_id} declares external_git evidence but pins no evidence_anchors`;
2054
+ return {
2055
+ success: false,
2056
+ message,
2057
+ errors: [message],
2058
+ task_id: args.task_id
2059
+ };
2060
+ }
2061
+ const malformed = validateAnchorExpectations(anchors);
2062
+ if (malformed) {
2063
+ return {
2064
+ success: false,
2065
+ message: malformed,
2066
+ errors: [malformed],
2067
+ task_id: args.task_id
2068
+ };
2069
+ }
2070
+ const startedAt = Date.now();
2071
+ const outcomes = [];
2072
+ for (const anchor of anchors) {
2073
+ const { argv, refusal } = _internals4.buildAnchorArgv(anchor.command);
2074
+ if (refusal !== null || argv === null) {
2075
+ const message = `anchor not admitted: ${refusal ?? "not an allowed read-only git invocation"}`;
2076
+ return {
2077
+ success: false,
2078
+ message,
2079
+ errors: [message],
2080
+ task_id: args.task_id
2081
+ };
2082
+ }
2083
+ const remaining = ATTEST_TOTAL_DEADLINE_MS - (Date.now() - startedAt);
2084
+ if (remaining <= 0) {
2085
+ const message = "attest total deadline (30s) exhausted before all anchors ran";
2086
+ return {
2087
+ success: false,
2088
+ message,
2089
+ errors: [message],
2090
+ task_id: args.task_id
2091
+ };
2092
+ }
2093
+ const run = await _internals4.runAnchor(argv, directory, Math.min(ANCHOR_SPAWN_TIMEOUT_MS, remaining));
2094
+ if (run.refusal !== null) {
2095
+ const message = `anchor "${anchor.command.slice(0, 60)}" failed: ${run.refusal}`;
2096
+ return {
2097
+ success: false,
2098
+ message,
2099
+ errors: [message],
2100
+ task_id: args.task_id
2101
+ };
2102
+ }
2103
+ if (run.stdout.length > ANCHOR_STDOUT_MAX_CHARS) {
2104
+ const message = `anchor "${anchor.command.slice(0, 60)}" produced ${run.stdout.length} chars of stdout, ` + `exceeding the ${ANCHOR_STDOUT_MAX_CHARS}-char evidence storage bound; refusing rather than truncating`;
2105
+ return {
2106
+ success: false,
2107
+ message,
2108
+ errors: [message],
2109
+ task_id: args.task_id
2110
+ };
2111
+ }
2112
+ const trimmed = run.stdout.trim();
2113
+ const matched = anchor.expect !== undefined ? trimmed === anchor.expect && (anchor.expect_prefix === undefined || trimmed.startsWith(anchor.expect_prefix)) : trimmed.startsWith(anchor.expect_prefix);
2114
+ outcomes.push({
2115
+ command: anchor.command,
2116
+ expect: anchor.expect,
2117
+ expect_prefix: anchor.expect_prefix,
2118
+ matched,
2119
+ exit_code: run.exitCode,
2120
+ stdout: run.stdout
2121
+ });
2122
+ }
2123
+ if (!outcomes.every((outcome) => outcome.matched)) {
2124
+ const failed = outcomes.find((outcome) => !outcome.matched);
2125
+ const message = `anchor "${failed?.command.slice(0, 60) ?? "?"}" did not match its expectation ` + `(stdout: ${JSON.stringify(failed?.stdout.slice(0, 120) ?? "")})`;
2126
+ return {
2127
+ success: false,
2128
+ message,
2129
+ errors: [message],
2130
+ task_id: args.task_id
2131
+ };
2132
+ }
2133
+ let current;
2134
+ try {
2135
+ current = _internals4.loadPlanTask(directory, args.task_id);
2136
+ } catch (error) {
2137
+ const message = "the plan became unreadable while anchors were running; re-run attest against the current declaration";
2138
+ return {
2139
+ success: false,
2140
+ message,
2141
+ errors: [message],
2142
+ task_id: args.task_id
2143
+ };
2144
+ }
2145
+ const anchorsUnchanged = JSON.stringify(current?.task.evidence_anchors ?? null) === JSON.stringify(anchors);
2146
+ if (!current || current.task.evidence_class !== "external_git" || current.title !== found.title || current.swarm !== found.swarm || !anchorsUnchanged) {
2147
+ const message = "the plan changed while anchors were running; re-run attest against the current declaration";
2148
+ return {
2149
+ success: false,
2150
+ message,
2151
+ errors: [message],
2152
+ task_id: args.task_id
2153
+ };
2154
+ }
2155
+ let existingEvidence = null;
2156
+ try {
2157
+ existingEvidence = readTaskEvidenceRaw(directory, args.task_id);
2158
+ } catch (error) {
2159
+ const message = `the existing evidence file for task ${args.task_id} is unreadable (corrupt): ` + `${error instanceof Error ? error.message.slice(0, 160) : String(error).slice(0, 160)}; ` + "inspect the file before attesting";
2160
+ return {
2161
+ success: false,
2162
+ message,
2163
+ errors: [message],
2164
+ task_id: args.task_id
2165
+ };
2166
+ }
2167
+ const generation = existingEvidence?.workflow?.generation ?? 0;
2168
+ const transitionId = `attest:${args.task_id}:${startedAt}:${Math.random().toString(36).slice(2, 10)}`;
2169
+ const bundle = {
2170
+ class: "external_git",
2171
+ transitionId,
2172
+ declarationDigest: computeExternalDeclarationDigest({
2173
+ taskId: args.task_id,
2174
+ evidence_class: "external_git",
2175
+ evidence_anchors: anchors,
2176
+ plan_title: current.title,
2177
+ plan_swarm: current.swarm,
2178
+ generation
2179
+ }),
2180
+ anchors: outcomes,
2181
+ attestedAt: new Date().toISOString()
2182
+ };
2183
+ try {
2184
+ await transitionTaskWorkflowEvidence(directory, args.task_id, {
2185
+ type: "external_evidence_attested",
2186
+ bundle,
2187
+ expectedGeneration: generation,
2188
+ transitionId
2189
+ });
2190
+ } catch (error) {
2191
+ const message = error instanceof Error ? error.message : String(error);
2192
+ return {
2193
+ success: false,
2194
+ message,
2195
+ errors: [message],
2196
+ task_id: args.task_id
2197
+ };
2198
+ }
2199
+ return {
2200
+ success: true,
2201
+ message: `Task ${args.task_id}: ${outcomes.length} external evidence anchor(s) attested and recorded; completion is now admissible for this declared task`,
2202
+ task_id: args.task_id
2203
+ };
2204
+ }
2205
+ export {
2206
+ executeAttestTask,
2207
+ buildAnchorArgv,
2208
+ _internals4 as _internals
2209
+ };