opencode-swarm 7.141.2 → 7.142.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/{config-doctor-weqs97sa.js → config-doctor-2a3qmp5p.js} +2 -2
- package/dist/cli/{core-zg7fqfsm.js → core-h9z49ant.js} +1 -1
- package/dist/cli/{curation-policy-j1fbc3ry.js → curation-policy-pkb2eeb1.js} +6 -6
- package/dist/cli/curator-drift-49p3jnte.js +1 -1
- package/dist/cli/{curator-llm-factory-8p86mgye.js → curator-llm-factory-9fr243kj.js} +29 -29
- package/dist/cli/{curator-v1tnrt93.js → curator-mtz194wy.js} +29 -29
- package/dist/cli/{evidence-summary-service-a4w0yc8h.js → evidence-summary-service-fgckng0a.js} +13 -11
- package/dist/cli/{gate-evidence-rt9034ft.js → gate-evidence-ppna7em4.js} +9 -5
- package/dist/cli/{guardrail-explain-8yrznk98.js → guardrail-explain-ejdvvnjz.js} +30 -30
- package/dist/cli/{guardrail-log-7hj4x9yt.js → guardrail-log-adjs25ye.js} +4 -4
- package/dist/cli/{hive-promoter-6zcnyh1j.js → hive-promoter-jejme31e.js} +29 -29
- package/dist/cli/{index-vts5e84g.js → index-0r4qs243.js} +31 -31
- package/dist/cli/index-2mb5y2w1.js +1372 -0
- package/dist/cli/{index-nse60anv.js → index-7c4bxgdh.js} +1 -1
- package/dist/cli/{index-qskz8j2z.js → index-82m5kwtd.js} +1 -1
- package/dist/cli/{index-6bp1jecx.js → index-9kf3mj08.js} +4 -4
- package/dist/cli/{index-szrcxvr7.js → index-9wmx9ca8.js} +6 -6
- package/dist/cli/{index-76651erq.js → index-ajfa2wmj.js} +9 -9
- package/dist/cli/{index-jpt07f2k.js → index-bb4pcn58.js} +2 -2
- package/dist/cli/{index-qa50yyeh.js → index-bkkxxwcd.js} +1 -1
- package/dist/cli/{index-c2hczjhp.js → index-erfnw81g.js} +3 -3
- package/dist/cli/{index-why8n7cr.js → index-gtkk5rx7.js} +137 -36
- package/dist/cli/{index-73sxchez.js → index-h7ye3bzp.js} +4127 -2558
- package/dist/cli/{index-hwxcdrv9.js → index-j02ez59y.js} +9 -9
- package/dist/cli/{index-h3vk67wp.js → index-k1xsjzm3.js} +3 -3
- package/dist/cli/{index-zbj3yrrd.js → index-k31en0ja.js} +3 -3
- package/dist/cli/{index-q4vaar2b.js → index-k92cagmt.js} +2 -2
- package/dist/cli/{index-p5y72gtj.js → index-kbshtwha.js} +3 -3
- package/dist/cli/{index-r6pdts5c.js → index-ktazgjqw.js} +2 -2
- package/dist/cli/{index-mxtt4x8r.js → index-myav90y6.js} +212 -10
- package/dist/cli/{index-kkn2jxda.js → index-rthq1a6w.js} +1 -1
- package/dist/cli/{index-9d4gck4e.js → index-s2rcz51a.js} +59 -35
- package/dist/cli/{index-y6k6x197.js → index-sdqtecsd.js} +1 -1
- package/dist/cli/{index-v250345n.js → index-tq5s07ta.js} +1 -1
- package/dist/cli/{index-zf5nkkdd.js → index-vefjxxmb.js} +1 -1
- package/dist/cli/{index-bxjhn3t3.js → index-vy11zjvh.js} +2 -2
- package/dist/cli/{index-kq5p3nm3.js → index-w7x7qxz2.js} +1 -1
- package/dist/cli/{index-mqqmant3.js → index-we279je2.js} +1 -1
- package/dist/cli/{index-xx0q37xb.js → index-xah70t2t.js} +1 -1
- package/dist/cli/{index-aqzvjhb7.js → index-xfhe29z3.js} +5 -5
- package/dist/cli/{index-f1hsrgw1.js → index-y9h2vqf6.js} +1 -1
- package/dist/cli/index.js +42 -29
- package/dist/cli/{knowledge-escalator-xjkbek46.js → knowledge-escalator-0b80ryka.js} +12 -12
- package/dist/cli/{knowledge-events-hsdx5prn.js → knowledge-events-8x2wt562.js} +10 -10
- package/dist/cli/{knowledge-link-x86ebng8.js → knowledge-link-fn5j3r4q.js} +5 -5
- package/dist/cli/{knowledge-store-vt059r81.js → knowledge-store-d0xj6wht.js} +6 -6
- package/dist/cli/{knowledge-validator-jr5694jb.js → knowledge-validator-ewfj679a.js} +8 -8
- package/dist/cli/{pending-delegations-7xsagjy5.js → pending-delegations-0hb97ps1.js} +4 -4
- package/dist/cli/{pr-subscriptions-r6cdsa3t.js → pr-subscriptions-b63t252j.js} +4 -4
- package/dist/cli/{runner-nm3baddf.js → runner-hrg7h2h4.js} +6 -6
- package/dist/cli/{scan-cursor-6nrkcfbr.js → scan-cursor-hfyz7zpw.js} +7 -7
- package/dist/cli/{schema-f6pt0zjy.js → schema-ney3c685.js} +1 -1
- package/dist/cli/{scope-persistence-jc0mtp7g.js → scope-persistence-dbfgs7gh.js} +6 -6
- package/dist/cli/{skill-generator-4vp33gjb.js → skill-generator-p651jex5.js} +14 -14
- package/dist/cli/{telemetry-zex3zv3v.js → telemetry-xyfb5vm8.js} +1 -1
- package/dist/cli/{worktree-collision-ownership-n41qw3n8.js → worktree-collision-ownership-ymdxetnn.js} +4 -4
- package/dist/cli/{worktree-isolation-a3hkz665.js → worktree-isolation-513vx61s.js} +29 -29
- package/dist/commands/close.d.ts +20 -2
- package/dist/commands/hive-quarantine.d.ts +23 -0
- package/dist/commands/registry.d.ts +9 -0
- package/dist/gate-evidence.d.ts +24 -1
- package/dist/hooks/hive-transaction.d.ts +41 -0
- package/dist/index.js +491 -478
- package/dist/knowledge/hive-quarantine.d.ts +156 -0
- package/dist/observability/catalog.d.ts +2 -2
- package/dist/plan/ledger.d.ts +31 -1
- package/dist/plan/manager.d.ts +1 -0
- package/dist/state.d.ts +1 -1
- package/dist/telemetry.d.ts +1 -1
- package/dist/workflow/close-terminal.d.ts +22 -0
- package/dist/workflow/coder-settlement.d.ts +0 -23
- package/dist/workflow/task-repair.d.ts +1 -0
- package/dist/workflow/task-terminal.d.ts +18 -23
- package/dist/workflow/workflow-wal-file.d.ts +20 -0
- package/dist/workflow/workflow-wal-schema.d.ts +67 -0
- package/dist/worktree/merge.d.ts +17 -0
- package/package.json +1 -1
- package/dist/cli/index-3d13jkzq.js +0 -623
|
@@ -0,0 +1,1372 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
autoCommitDirty,
|
|
4
|
+
cleanUntrackedFiles
|
|
5
|
+
} from "./index-bkkxxwcd.js";
|
|
6
|
+
import {
|
|
7
|
+
validateSwarmPath
|
|
8
|
+
} from "./index-k46me75h.js";
|
|
9
|
+
import {
|
|
10
|
+
atomicWriteFile,
|
|
11
|
+
taskEvidencePath,
|
|
12
|
+
withTaskEvidenceLock
|
|
13
|
+
} from "./index-erfnw81g.js";
|
|
14
|
+
import {
|
|
15
|
+
isPathWithinDeclaredScope,
|
|
16
|
+
unsafePathTextReason
|
|
17
|
+
} from "./index-556ja8an.js";
|
|
18
|
+
import {
|
|
19
|
+
telemetry
|
|
20
|
+
} from "./index-s2rcz51a.js";
|
|
21
|
+
import {
|
|
22
|
+
bunSpawn
|
|
23
|
+
} from "./index-4nt52yr9.js";
|
|
24
|
+
import {
|
|
25
|
+
advisoryWarn,
|
|
26
|
+
init_warning_buffer
|
|
27
|
+
} from "./index-4rhhvd1a.js";
|
|
28
|
+
import {
|
|
29
|
+
log
|
|
30
|
+
} from "./index-zgwm4ryv.js";
|
|
31
|
+
import {
|
|
32
|
+
exports_external,
|
|
33
|
+
init_zod
|
|
34
|
+
} from "./index-bpmtbmy9.js";
|
|
35
|
+
|
|
36
|
+
// src/gate-evidence.ts
|
|
37
|
+
init_zod();
|
|
38
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, realpathSync } from "fs";
|
|
39
|
+
import * as path2 from "path";
|
|
40
|
+
|
|
41
|
+
// src/validation/task-id.ts
|
|
42
|
+
var STRICT_TASK_ID_PATTERN = /^\d+\.\d+(\.\d+)*$/;
|
|
43
|
+
var RETRO_TASK_ID_REGEX = /^retro-\d+$/;
|
|
44
|
+
var INTERNAL_TOOL_ID_REGEX = /^(?:sast_scan|quality_budget|syntax_check|placeholder_scan|sbom_generate|build|secretscan)$/;
|
|
45
|
+
var GENERAL_TASK_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
46
|
+
function checkUnsafeChars(taskId) {
|
|
47
|
+
if (!taskId || taskId.length === 0) {
|
|
48
|
+
return "Invalid task ID: empty string";
|
|
49
|
+
}
|
|
50
|
+
if (/\0/.test(taskId)) {
|
|
51
|
+
return "Invalid task ID: contains null bytes";
|
|
52
|
+
}
|
|
53
|
+
for (let i = 0;i < taskId.length; i++) {
|
|
54
|
+
if (taskId.charCodeAt(i) < 32) {
|
|
55
|
+
return "Invalid task ID: contains control characters";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (taskId.includes("..") || taskId.includes("/") || taskId.includes("\\")) {
|
|
59
|
+
return "Invalid task ID: path traversal detected";
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
function isStrictTaskId(taskId) {
|
|
64
|
+
if (!taskId)
|
|
65
|
+
return false;
|
|
66
|
+
const unsafeMsg = checkUnsafeChars(taskId);
|
|
67
|
+
if (unsafeMsg)
|
|
68
|
+
return false;
|
|
69
|
+
return STRICT_TASK_ID_PATTERN.test(taskId);
|
|
70
|
+
}
|
|
71
|
+
function assertStrictTaskId(taskId) {
|
|
72
|
+
if (!isStrictTaskId(taskId)) {
|
|
73
|
+
throw new Error(`Invalid taskId: "${taskId}". Must match N.M or N.M.P (e.g. "1.1", "1.2.3").`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function sanitizeTaskId(taskId) {
|
|
77
|
+
const unsafeMsg = checkUnsafeChars(taskId);
|
|
78
|
+
if (unsafeMsg) {
|
|
79
|
+
throw new Error(unsafeMsg);
|
|
80
|
+
}
|
|
81
|
+
if (STRICT_TASK_ID_PATTERN.test(taskId) || RETRO_TASK_ID_REGEX.test(taskId) || INTERNAL_TOOL_ID_REGEX.test(taskId) || GENERAL_TASK_ID_REGEX.test(taskId)) {
|
|
82
|
+
return taskId;
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`Invalid task ID: must be alphanumeric (ASCII) with optional hyphens, underscores, or dots, got "${taskId}"`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/workflow/workflow-wal-file.ts
|
|
88
|
+
import * as fs2 from "fs";
|
|
89
|
+
import { mkdir, open } from "fs/promises";
|
|
90
|
+
import { dirname } from "path";
|
|
91
|
+
|
|
92
|
+
// src/worktree/merge.ts
|
|
93
|
+
init_warning_buffer();
|
|
94
|
+
|
|
95
|
+
// src/turbo/lean/recovery.ts
|
|
96
|
+
import * as fs from "fs";
|
|
97
|
+
import * as path from "path";
|
|
98
|
+
var RECOVERY_DIR_NAME = "recovery";
|
|
99
|
+
function recoveryDir(directory) {
|
|
100
|
+
return path.join(directory, ".swarm", RECOVERY_DIR_NAME);
|
|
101
|
+
}
|
|
102
|
+
function isRecoveryRecord(value) {
|
|
103
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const record = value;
|
|
107
|
+
const validStatus = record.status === "failed" || record.status === "partial" || record.status === "conflict";
|
|
108
|
+
return typeof record.laneId === "string" && typeof record.sessionId === "string" && typeof record.worktreePath === "string" && validStatus && typeof record.reason === "string" && typeof record.recordedAt === "number" && Number.isFinite(record.recordedAt) && typeof record.replayHint === "string" && (record.branchName === undefined || typeof record.branchName === "string") && (record.conflictFiles === undefined || Array.isArray(record.conflictFiles) && record.conflictFiles.every((file) => typeof file === "string"));
|
|
109
|
+
}
|
|
110
|
+
function readRecoveryRecords(directory) {
|
|
111
|
+
const dir = recoveryDir(directory);
|
|
112
|
+
let entries;
|
|
113
|
+
try {
|
|
114
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
115
|
+
} catch {
|
|
116
|
+
return {
|
|
117
|
+
records: [],
|
|
118
|
+
errored: fs.existsSync(dir)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const records = [];
|
|
122
|
+
let errored = false;
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
if (!entry.isFile() || !entry.name.endsWith(".json"))
|
|
125
|
+
continue;
|
|
126
|
+
const full = path.join(dir, entry.name);
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(fs.readFileSync(full, "utf-8"));
|
|
129
|
+
if (isRecoveryRecord(parsed)) {
|
|
130
|
+
records.push(parsed);
|
|
131
|
+
} else {
|
|
132
|
+
errored = true;
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
errored = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { records, errored };
|
|
139
|
+
}
|
|
140
|
+
function listRecoveryRecords(directory) {
|
|
141
|
+
return readRecoveryRecords(directory).records;
|
|
142
|
+
}
|
|
143
|
+
function hasRecoveryRecordForBranch(directory, branchName) {
|
|
144
|
+
const records = listRecoveryRecords(directory);
|
|
145
|
+
return records.some((r) => r.branchName === branchName);
|
|
146
|
+
}
|
|
147
|
+
function recoveryReadErrored(directory) {
|
|
148
|
+
return readRecoveryRecords(directory).errored;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/worktree/merge.ts
|
|
152
|
+
var _internals = {
|
|
153
|
+
bunSpawn,
|
|
154
|
+
platform: process.platform,
|
|
155
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
156
|
+
cleanupOrphanedBranches,
|
|
157
|
+
startupOrphanRecovery,
|
|
158
|
+
extractSessionId
|
|
159
|
+
};
|
|
160
|
+
var MERGE_TIMEOUT_MS = 30000;
|
|
161
|
+
async function runGit(args, cwd, timeoutMs = MERGE_TIMEOUT_MS) {
|
|
162
|
+
const proc = _internals.bunSpawn(["git", ...args], {
|
|
163
|
+
cwd,
|
|
164
|
+
timeout: timeoutMs,
|
|
165
|
+
stdin: "ignore",
|
|
166
|
+
stdout: "pipe",
|
|
167
|
+
stderr: "pipe",
|
|
168
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
169
|
+
});
|
|
170
|
+
try {
|
|
171
|
+
const exitCode = await proc.exited;
|
|
172
|
+
const stdout = await proc.stdout.text();
|
|
173
|
+
const stderr = await proc.stderr.text();
|
|
174
|
+
return { exitCode, stdout, stderr };
|
|
175
|
+
} finally {
|
|
176
|
+
try {
|
|
177
|
+
proc.kill();
|
|
178
|
+
} catch {}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function parseConflictFiles(output) {
|
|
182
|
+
const files = [];
|
|
183
|
+
const lines = output.split(`
|
|
184
|
+
`);
|
|
185
|
+
for (const line of lines) {
|
|
186
|
+
const match = line.match(/CONFLICT\b.*(?:Merge conflict in |in )(.+)/);
|
|
187
|
+
if (match?.[1]) {
|
|
188
|
+
files.push(match[1].trim());
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return files;
|
|
192
|
+
}
|
|
193
|
+
var GIT_OBJECT_ID_PATTERN = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/;
|
|
194
|
+
function getMergeStrategy(config) {
|
|
195
|
+
return config.mergeStrategy ?? config.merge_strategy ?? "merge";
|
|
196
|
+
}
|
|
197
|
+
async function mergeLaneBranch(primaryDir, branchName, strategy) {
|
|
198
|
+
let result;
|
|
199
|
+
switch (strategy) {
|
|
200
|
+
case "merge":
|
|
201
|
+
result = await runGit(["merge", "--no-edit", branchName], primaryDir);
|
|
202
|
+
break;
|
|
203
|
+
case "rebase":
|
|
204
|
+
result = await runGit(["rebase", branchName], primaryDir);
|
|
205
|
+
break;
|
|
206
|
+
case "cherry-pick": {
|
|
207
|
+
const mergeBaseResult = await runGit(["merge-base", "HEAD", branchName], primaryDir);
|
|
208
|
+
if (mergeBaseResult.exitCode === 0 && mergeBaseResult.stdout.trim()) {
|
|
209
|
+
const mergeBase = mergeBaseResult.stdout.trim();
|
|
210
|
+
result = await runGit(["cherry-pick", `${mergeBase}..${branchName}`, "-x"], primaryDir);
|
|
211
|
+
} else {
|
|
212
|
+
advisoryWarn("[worktree] mergeLaneBranch: git merge-base failed for cherry-pick; falling back to tip-only cherry-pick");
|
|
213
|
+
result = await runGit(["cherry-pick", branchName, "-x"], primaryDir);
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (result.exitCode === 0) {
|
|
219
|
+
return { merged: true, strategy };
|
|
220
|
+
}
|
|
221
|
+
const combinedOutput = `${result.stderr}
|
|
222
|
+
${result.stdout}`;
|
|
223
|
+
const hasConflict = /CONFLICT/i.test(combinedOutput) || /conflict/i.test(combinedOutput);
|
|
224
|
+
if (hasConflict) {
|
|
225
|
+
const files = parseConflictFiles(combinedOutput);
|
|
226
|
+
const abortArgs = strategy === "rebase" ? ["rebase", "--abort"] : strategy === "cherry-pick" ? ["cherry-pick", "--abort"] : ["merge", "--abort"];
|
|
227
|
+
await runGit(abortArgs, primaryDir);
|
|
228
|
+
return {
|
|
229
|
+
conflict: true,
|
|
230
|
+
files,
|
|
231
|
+
message: result.stderr.trim()
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
error: result.stderr.trim() || result.stdout.trim()
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
async function reconcileLandedMerge(primaryDir, provenance) {
|
|
239
|
+
if (!provenance.operationId || !provenance.sourceHead || !provenance.targetHeadBefore) {
|
|
240
|
+
return { landed: false, error: "Incomplete merge operation provenance" };
|
|
241
|
+
}
|
|
242
|
+
if (!GIT_OBJECT_ID_PATTERN.test(provenance.sourceHead) || !GIT_OBJECT_ID_PATTERN.test(provenance.targetHeadBefore)) {
|
|
243
|
+
return { landed: false, error: "Malformed merge operation provenance" };
|
|
244
|
+
}
|
|
245
|
+
if (provenance.strategy !== "cherry-pick") {
|
|
246
|
+
const ancestry = await runGit(["merge-base", "--is-ancestor", provenance.sourceHead, "HEAD"], primaryDir);
|
|
247
|
+
if (ancestry.exitCode === 0) {
|
|
248
|
+
return { landed: true, method: "ancestry" };
|
|
249
|
+
}
|
|
250
|
+
if (ancestry.exitCode === 1) {
|
|
251
|
+
return { landed: false };
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
landed: false,
|
|
255
|
+
error: ancestry.stderr.trim() || ancestry.stdout.trim() || `git merge-base exited ${ancestry.exitCode}`
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const exactTrailer = `(cherry picked from commit ${provenance.sourceHead})`;
|
|
259
|
+
const escapedTrailer = exactTrailer.replace(/[\\.^$|?*+()[\]{}]/g, "\\$&");
|
|
260
|
+
const trailerSearch = await runGit([
|
|
261
|
+
"log",
|
|
262
|
+
"--format=%H",
|
|
263
|
+
"--extended-regexp",
|
|
264
|
+
`--grep=^${escapedTrailer}$`,
|
|
265
|
+
"--max-count=1",
|
|
266
|
+
`${provenance.targetHeadBefore}..HEAD`,
|
|
267
|
+
"--"
|
|
268
|
+
], primaryDir);
|
|
269
|
+
if (trailerSearch.exitCode !== 0) {
|
|
270
|
+
return {
|
|
271
|
+
landed: false,
|
|
272
|
+
error: trailerSearch.stderr.trim() || trailerSearch.stdout.trim() || `git log exited ${trailerSearch.exitCode}`
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return trailerSearch.stdout.trim() ? { landed: true, method: "cherry-pick-trailer" } : { landed: false };
|
|
276
|
+
}
|
|
277
|
+
async function postMergeCleanup(directory, branchName) {
|
|
278
|
+
const deleteResult = await runGit(["branch", "-D", branchName], directory);
|
|
279
|
+
const deleteOk = deleteResult.exitCode === 0;
|
|
280
|
+
const pruneResult = await runGit(["worktree", "prune"], directory);
|
|
281
|
+
const pruneOk = pruneResult.exitCode === 0;
|
|
282
|
+
if (deleteOk && pruneOk) {
|
|
283
|
+
return { cleaned: true };
|
|
284
|
+
}
|
|
285
|
+
if (!deleteOk && pruneOk) {
|
|
286
|
+
return {
|
|
287
|
+
error: `Branch delete failed: ${deleteResult.stderr.trim() || deleteResult.stdout.trim()}`,
|
|
288
|
+
partial: true
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
error: deleteOk ? `Worktree prune failed: ${pruneResult.stderr.trim() || pruneResult.stdout.trim()}` : `Branch delete failed: ${deleteResult.stderr.trim() || deleteResult.stdout.trim()}; worktree prune failed: ${pruneResult.stderr.trim() || pruneResult.stdout.trim()}`
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
async function pruneStaleWorktreeMetadata(directory) {
|
|
296
|
+
const result = await runGit(["worktree", "prune"], directory);
|
|
297
|
+
if (result.exitCode === 0)
|
|
298
|
+
return { pruned: true };
|
|
299
|
+
return {
|
|
300
|
+
error: result.stderr.trim() || result.stdout.trim() || `git worktree prune exited ${result.exitCode}`
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async function attemptMergeBackFromDirty(worktreePath, branchName, primaryDir, strategy, options = {}) {
|
|
304
|
+
let autoCommitted = false;
|
|
305
|
+
let cleaned = false;
|
|
306
|
+
let autoCommitFailed = false;
|
|
307
|
+
let cleanFailed = false;
|
|
308
|
+
let provenance = options.resume;
|
|
309
|
+
if (provenance) {
|
|
310
|
+
if (provenance.branchName !== branchName || provenance.strategy !== strategy || options.operationId !== undefined && provenance.operationId !== options.operationId) {
|
|
311
|
+
return {
|
|
312
|
+
failed: true,
|
|
313
|
+
stage: "reconciliation",
|
|
314
|
+
message: "Stored merge operation identity does not match this dispatch",
|
|
315
|
+
provenance
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const reconciliation = await reconcileLandedMerge(primaryDir, provenance);
|
|
319
|
+
if ("error" in reconciliation && reconciliation.error) {
|
|
320
|
+
return {
|
|
321
|
+
failed: true,
|
|
322
|
+
stage: "reconciliation",
|
|
323
|
+
message: `Unable to reconcile prior merge-back: ${reconciliation.error}`,
|
|
324
|
+
provenance
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (reconciliation.landed) {
|
|
328
|
+
return {
|
|
329
|
+
merged: true,
|
|
330
|
+
strategy,
|
|
331
|
+
autoCommitted: false,
|
|
332
|
+
cleaned: false,
|
|
333
|
+
reconciled: true,
|
|
334
|
+
provenance
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const sourceHead = await runGit(["rev-parse", "HEAD"], worktreePath);
|
|
338
|
+
const targetHead = await runGit(["rev-parse", "HEAD"], primaryDir);
|
|
339
|
+
if (sourceHead.exitCode !== 0 || targetHead.exitCode !== 0 || sourceHead.stdout.trim() !== provenance.sourceHead || targetHead.stdout.trim() !== provenance.targetHeadBefore) {
|
|
340
|
+
return {
|
|
341
|
+
failed: true,
|
|
342
|
+
stage: "reconciliation",
|
|
343
|
+
message: "Stored merge operation did not land and its source or target HEAD has changed",
|
|
344
|
+
provenance
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (!provenance) {
|
|
349
|
+
const commitResult = await autoCommitDirty(worktreePath);
|
|
350
|
+
if (commitResult.committed) {
|
|
351
|
+
autoCommitted = true;
|
|
352
|
+
} else if (commitResult.reason !== "Nothing to commit") {
|
|
353
|
+
autoCommitFailed = true;
|
|
354
|
+
log(`[worktree] attemptMergeBackFromDirty: auto-commit failed for worktree "${worktreePath}" branch "${branchName}": ${commitResult.reason}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const cleanResult = await cleanUntrackedFiles(worktreePath);
|
|
358
|
+
if (cleanResult.cleaned) {
|
|
359
|
+
cleaned = true;
|
|
360
|
+
} else {
|
|
361
|
+
cleanFailed = true;
|
|
362
|
+
log(`[worktree] attemptMergeBackFromDirty: clean untracked failed for worktree "${worktreePath}" branch "${branchName}": ${cleanResult.error}`);
|
|
363
|
+
}
|
|
364
|
+
if (autoCommitFailed && cleanFailed) {
|
|
365
|
+
return {
|
|
366
|
+
failed: true,
|
|
367
|
+
stage: "cleanup",
|
|
368
|
+
message: "Auto-commit and clean both failed; abandoning worktree",
|
|
369
|
+
...provenance ? { provenance } : {}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (!provenance && (options.operationId || options.onBeforeMerge)) {
|
|
373
|
+
if (!options.operationId) {
|
|
374
|
+
return {
|
|
375
|
+
failed: true,
|
|
376
|
+
stage: "pre-merge",
|
|
377
|
+
message: "A pre-merge callback requires a stable operationId"
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const sourceHead = await runGit(["rev-parse", "HEAD"], worktreePath);
|
|
381
|
+
const targetHead = await runGit(["rev-parse", "HEAD"], primaryDir);
|
|
382
|
+
if (sourceHead.exitCode !== 0 || targetHead.exitCode !== 0 || !sourceHead.stdout.trim() || !targetHead.stdout.trim()) {
|
|
383
|
+
return {
|
|
384
|
+
failed: true,
|
|
385
|
+
stage: "pre-merge",
|
|
386
|
+
message: "Unable to capture source and target HEAD before merge-back"
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
provenance = {
|
|
390
|
+
operationId: options.operationId,
|
|
391
|
+
sourceHead: sourceHead.stdout.trim(),
|
|
392
|
+
targetHeadBefore: targetHead.stdout.trim(),
|
|
393
|
+
branchName,
|
|
394
|
+
strategy
|
|
395
|
+
};
|
|
396
|
+
if (options.onBeforeMerge) {
|
|
397
|
+
try {
|
|
398
|
+
await options.onBeforeMerge(provenance);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
return {
|
|
401
|
+
failed: true,
|
|
402
|
+
stage: "pre-merge",
|
|
403
|
+
message: `Unable to persist merge operation provenance: ${String(error)}`,
|
|
404
|
+
provenance
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const mergeResult = await mergeLaneBranch(primaryDir, branchName, strategy);
|
|
410
|
+
if ("merged" in mergeResult && mergeResult.merged) {
|
|
411
|
+
return {
|
|
412
|
+
merged: true,
|
|
413
|
+
strategy,
|
|
414
|
+
autoCommitted,
|
|
415
|
+
cleaned,
|
|
416
|
+
...provenance ? { reconciled: false, provenance } : {}
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if ("conflict" in mergeResult) {
|
|
420
|
+
return {
|
|
421
|
+
partial: true,
|
|
422
|
+
stage: "merge",
|
|
423
|
+
autoCommitted,
|
|
424
|
+
cleaned,
|
|
425
|
+
message: mergeResult.message,
|
|
426
|
+
conflictFiles: mergeResult.files,
|
|
427
|
+
...provenance ? { provenance } : {}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
if ("error" in mergeResult) {
|
|
431
|
+
return {
|
|
432
|
+
failed: true,
|
|
433
|
+
stage: "merge",
|
|
434
|
+
message: mergeResult.error,
|
|
435
|
+
...provenance ? { provenance } : {}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
failed: true,
|
|
440
|
+
stage: "merge",
|
|
441
|
+
message: "Merge failed with unexpected result",
|
|
442
|
+
...provenance ? { provenance } : {}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function extractSessionId(branchName) {
|
|
446
|
+
const segments = branchName.trim().split("/");
|
|
447
|
+
if (segments.length >= 3 && segments[0] === "swarm-lane") {
|
|
448
|
+
return segments[1];
|
|
449
|
+
}
|
|
450
|
+
if (segments.length >= 4 && segments[0] === "swarm" && segments[1] === "lane") {
|
|
451
|
+
return segments[2];
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
async function listLaneBranches(directory) {
|
|
456
|
+
const branches = new Set;
|
|
457
|
+
const result = await runGit(["branch", "--format=%(refname:short)"], directory);
|
|
458
|
+
if (result.exitCode === 0) {
|
|
459
|
+
for (const line of result.stdout.split(`
|
|
460
|
+
`)) {
|
|
461
|
+
const branch = line.trim();
|
|
462
|
+
if (branch.length > 0 && (branch.startsWith("swarm-lane/") || branch.startsWith("swarm/lane/"))) {
|
|
463
|
+
branches.add(branch);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return [...branches];
|
|
468
|
+
}
|
|
469
|
+
async function cleanupOrphanedBranches(directory, activeSessionIds = [], options = {}) {
|
|
470
|
+
const removed = [];
|
|
471
|
+
const skipped = [];
|
|
472
|
+
const skippedRecoveryBranches = [];
|
|
473
|
+
const errors = [];
|
|
474
|
+
const recoveryReadError = recoveryReadErrored(directory);
|
|
475
|
+
if (recoveryReadError) {
|
|
476
|
+
log("[worktree] cleanupOrphanedBranches: .swarm/recovery/ read error \u2014 skipping all " + "lane-branch deletions this pass (fail-safe for preserved recovery branches).");
|
|
477
|
+
const branches2 = await listLaneBranches(directory);
|
|
478
|
+
return {
|
|
479
|
+
removed,
|
|
480
|
+
skipped: branches2,
|
|
481
|
+
skippedRecoveryBranches,
|
|
482
|
+
errors,
|
|
483
|
+
recoveryReadError: true
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
const branches = await listLaneBranches(directory);
|
|
487
|
+
for (const branch of branches) {
|
|
488
|
+
const sessionId = extractSessionId(branch);
|
|
489
|
+
if (sessionId !== null && activeSessionIds.includes(sessionId)) {
|
|
490
|
+
skipped.push(branch);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (hasRecoveryRecordForBranch(directory, branch)) {
|
|
494
|
+
skippedRecoveryBranches.push(branch);
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
const deleteResult = await runGit(["branch", options.preserveUnmerged ? "-d" : "-D", branch], directory);
|
|
498
|
+
if (deleteResult.exitCode === 0) {
|
|
499
|
+
removed.push(branch);
|
|
500
|
+
} else {
|
|
501
|
+
errors.push({
|
|
502
|
+
branch,
|
|
503
|
+
error: deleteResult.stderr.trim() || deleteResult.stdout.trim()
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
await runGit(["worktree", "prune"], directory);
|
|
508
|
+
return {
|
|
509
|
+
removed,
|
|
510
|
+
skipped,
|
|
511
|
+
skippedRecoveryBranches,
|
|
512
|
+
errors
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
async function startupOrphanRecovery(directory, activeSessionIds = []) {
|
|
516
|
+
const warnings = [];
|
|
517
|
+
const pruneResult = await runGit(["worktree", "prune"], directory);
|
|
518
|
+
if (pruneResult.exitCode !== 0) {
|
|
519
|
+
warnings.push(`git worktree prune failed: ${pruneResult.stderr.trim() || pruneResult.stdout.trim()}`);
|
|
520
|
+
}
|
|
521
|
+
const allBranches = await listLaneBranches(directory);
|
|
522
|
+
const orphanBranches = [];
|
|
523
|
+
for (const branch of allBranches) {
|
|
524
|
+
const sessionId = extractSessionId(branch);
|
|
525
|
+
if (sessionId === null || !activeSessionIds.includes(sessionId)) {
|
|
526
|
+
orphanBranches.push(branch);
|
|
527
|
+
warnings.push(`Orphaned swarm-lane branch "${branch}" detected in "${directory}"`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
for (const warning of warnings) {
|
|
531
|
+
log(warning);
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
prunedWorktrees: pruneResult.exitCode === 0,
|
|
535
|
+
remainingBranches: orphanBranches,
|
|
536
|
+
warnings
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/workflow/workflow-wal-schema.ts
|
|
541
|
+
var TASK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
542
|
+
var SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
543
|
+
var BRANCH_NAME_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9._/-]{0,1023}$/;
|
|
544
|
+
function isSafeGitBranchName(value) {
|
|
545
|
+
if (typeof value !== "string" || !BRANCH_NAME_PATTERN.test(value)) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
if (value.includes("..") || value.endsWith("/") || value.endsWith(".")) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
return !value.split("/").some((segment) => segment.length === 0 || segment.startsWith(".") || segment.endsWith(".lock"));
|
|
552
|
+
}
|
|
553
|
+
function isSafeWorktreePath(value) {
|
|
554
|
+
return typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.startsWith("-") && unsafePathTextReason(value) === null;
|
|
555
|
+
}
|
|
556
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
557
|
+
var WORKFLOW_STATES = new Set([
|
|
558
|
+
"idle",
|
|
559
|
+
"coder_delegated",
|
|
560
|
+
"pre_check_passed",
|
|
561
|
+
"reviewer_run",
|
|
562
|
+
"tests_run",
|
|
563
|
+
"rework_required",
|
|
564
|
+
"complete",
|
|
565
|
+
"blocked",
|
|
566
|
+
"closed"
|
|
567
|
+
]);
|
|
568
|
+
function formatUnreadableMessage(code, filePath, detail, remediation) {
|
|
569
|
+
throw new Error(`${code}: ${filePath} ${detail}. ${remediation}`);
|
|
570
|
+
}
|
|
571
|
+
function parseWalObject(raw, filePath, code, remediation) {
|
|
572
|
+
let parsed;
|
|
573
|
+
try {
|
|
574
|
+
parsed = JSON.parse(raw);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
formatUnreadableMessage(code, filePath, `is not valid JSON (${error instanceof Error ? error.message : String(error)})`, remediation);
|
|
577
|
+
}
|
|
578
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
579
|
+
formatUnreadableMessage(code, filePath, "is not a JSON object", remediation);
|
|
580
|
+
}
|
|
581
|
+
return parsed;
|
|
582
|
+
}
|
|
583
|
+
function parseCoderSettlementWal(raw, filePath, expectedTaskId) {
|
|
584
|
+
const parsed = parseWalObject(raw, filePath, "CODER_SETTLEMENT_WAL_UNREADABLE", "Preserve this file, reconcile the task lane, and only then move it aside.");
|
|
585
|
+
const context = parsed.context;
|
|
586
|
+
const baseline = context?.baseline;
|
|
587
|
+
const worktree = parsed.worktree;
|
|
588
|
+
const provenance = parsed.mergeProvenance;
|
|
589
|
+
if (parsed.version !== 1 || !["ABORTED", "COMMITTED", "DISPATCHED", "PREPARED"].includes(String(parsed.state)) || typeof parsed.taskId !== "string" || !TASK_ID_PATTERN.test(parsed.taskId) || typeof parsed.transitionId !== "string" || parsed.transitionId.length === 0 || parsed.transitionId.length > 512 || typeof parsed.actor !== "string" || parsed.actor.length === 0 || parsed.actor.length > 512 || !Number.isInteger(parsed.processId) || (parsed.processId ?? 0) <= 0 || typeof parsed.runtimeId !== "string" || parsed.runtimeId.length === 0 || !Number.isInteger(parsed.expectedGeneration) || (parsed.expectedGeneration ?? -1) < 0 || !context || !baseline || typeof baseline.directory !== "string" || baseline.directory.length === 0 || baseline.directory.length > 4096 || typeof baseline.gitHead !== "string" && baseline.gitHead !== null || typeof baseline.dirtyHash !== "string" && baseline.dirtyHash !== null || typeof baseline.prHeadSha !== "string" && baseline.prHeadSha !== null || typeof baseline.scope !== "string" && baseline.scope !== null || !Array.isArray(baseline.changedFiles) && baseline.changedFiles !== null && baseline.changedFiles !== undefined || Array.isArray(baseline.changedFiles) && (baseline.changedFiles.length > 50000 || baseline.changedFiles.some((candidatePath) => typeof candidatePath !== "string" || candidatePath.length > 4096)) || !Array.isArray(context.declaredFiles) && context.declaredFiles !== null || Array.isArray(context.declaredFiles) && (context.declaredFiles.length > 50000 || context.declaredFiles.some((candidatePath) => typeof candidatePath !== "string" || candidatePath.length > 4096)) || parsed.observedFiles !== undefined && (!Array.isArray(parsed.observedFiles) || parsed.observedFiles.length > 50000 || parsed.observedFiles.some((candidatePath) => typeof candidatePath !== "string" || candidatePath.length > 4096 || !isPathWithinDeclaredScope(candidatePath, context.declaredFiles ?? [], baseline.directory))) || worktree !== undefined && (typeof worktree.callID !== "string" || typeof worktree.parentSessionId !== "string" || typeof worktree.taskId !== "string" || worktree.taskId !== parsed.taskId || !isSafeWorktreePath(worktree.worktreePath) || !isSafeGitBranchName(worktree.branchName) || typeof worktree.worktreeId !== "string" || typeof worktree.worktreeSessionId !== "string" || !["merge", "rebase", "cherry-pick"].includes(String(worktree.mergeStrategy)) || !Number.isInteger(worktree.laneIndex)) || provenance !== undefined && (typeof provenance.operationId !== "string" || provenance.operationId !== parsed.transitionId || typeof provenance.sourceHead !== "string" || !GIT_OBJECT_ID_PATTERN.test(provenance.sourceHead) || typeof provenance.targetHeadBefore !== "string" || !GIT_OBJECT_ID_PATTERN.test(provenance.targetHeadBefore) || !isSafeGitBranchName(provenance.branchName) || worktree !== undefined && (provenance.branchName !== worktree.branchName || provenance.strategy !== worktree.mergeStrategy) || !["merge", "rebase", "cherry-pick"].includes(String(provenance.strategy))) || typeof parsed.recordedAt !== "string" || parsed.state === "PREPARED" && typeof parsed.accepted !== "boolean" || parsed.cleanupComplete !== undefined && typeof parsed.cleanupComplete !== "boolean" || parsed.settlementFailed !== undefined && typeof parsed.settlementFailed !== "boolean") {
|
|
590
|
+
formatUnreadableMessage("CODER_SETTLEMENT_WAL_UNREADABLE", filePath, "is not a valid v1 coder-settlement WAL", "Preserve this file, reconcile the task lane, and only then move it aside.");
|
|
591
|
+
}
|
|
592
|
+
if (expectedTaskId && parsed.taskId !== expectedTaskId) {
|
|
593
|
+
formatUnreadableMessage("CODER_SETTLEMENT_WAL_TASK_MISMATCH", filePath, `records task ${parsed.taskId} but was read for task ${expectedTaskId}`, "Preserve this file, reconcile the task lane, and only then move it aside.");
|
|
594
|
+
}
|
|
595
|
+
return parsed;
|
|
596
|
+
}
|
|
597
|
+
function parseTaskRepairWal(raw, filePath, expectedTaskId) {
|
|
598
|
+
const parsed = parseWalObject(raw, filePath, "TASK_REPAIR_WAL_UNREADABLE", "Preserve this file, reconcile the repair transition, and only then move it aside.");
|
|
599
|
+
if (parsed.version !== 1 || parsed.state !== "PREPARED" && parsed.state !== "COMMITTED" && parsed.state !== "ABORTED" || typeof parsed.taskId !== "string" || !TASK_ID_PATTERN.test(parsed.taskId) || typeof parsed.transitionId !== "string" || parsed.transitionId.length === 0 || parsed.transitionId.length > 512 || typeof parsed.reason !== "string" || parsed.reason.length === 0 || parsed.reason.length > 4096 || typeof parsed.actor !== "string" || parsed.actor.length === 0 || parsed.actor.length > 512 || typeof parsed.oldPlanStatus !== "string" || parsed.oldPlanStatus.length === 0 || parsed.oldPlanStatus.length > 128 || parsed.newPlanStatus !== "in_progress" || typeof parsed.oldWorkflowState !== "string" || !WORKFLOW_STATES.has(parsed.oldWorkflowState) || parsed.newWorkflowState !== "idle" || !Number.isInteger(parsed.generation) || (parsed.generation ?? -1) < 0 || !Number.isInteger(parsed.oldGeneration) || (parsed.oldGeneration ?? -1) < 0 || parsed.generation !== (parsed.oldGeneration ?? -1) + 1 || typeof parsed.recordedAt !== "string" || !Number.isFinite(Date.parse(parsed.recordedAt))) {
|
|
600
|
+
formatUnreadableMessage("TASK_REPAIR_WAL_UNREADABLE", filePath, "is not a valid v1 repair WAL (unexpected shape or version)", "Preserve this file, reconcile the repair transition, and only then move it aside.");
|
|
601
|
+
}
|
|
602
|
+
if (expectedTaskId && parsed.taskId !== expectedTaskId) {
|
|
603
|
+
formatUnreadableMessage("TASK_REPAIR_WAL_TASK_MISMATCH", filePath, `records task ${parsed.taskId} but was read for task ${expectedTaskId}`, "Preserve this file, reconcile the repair transition, and only then move it aside.");
|
|
604
|
+
}
|
|
605
|
+
return parsed;
|
|
606
|
+
}
|
|
607
|
+
function parseTaskTerminalWal(raw, filePath, expectedTaskId) {
|
|
608
|
+
const parsed = parseWalObject(raw, filePath, "TASK_TERMINAL_WAL_UNREADABLE", "Preserve this file and reconcile the task terminal transition before moving it aside.");
|
|
609
|
+
if (parsed.version !== 1 && parsed.version !== 2 || parsed.state !== "PREPARED" && parsed.state !== "COMMITTED" && parsed.state !== "ABORTED" || typeof parsed.taskId !== "string" || !TASK_ID_PATTERN.test(parsed.taskId) || typeof parsed.transitionId !== "string" || parsed.transitionId.length === 0 || parsed.transitionId.length > 512 || typeof parsed.actor !== "string" || parsed.actor.length === 0 || parsed.actor.length > 512 || typeof parsed.oldPlanStatus !== "string" || parsed.oldPlanStatus.length === 0 || parsed.oldPlanStatus.length > 128 || parsed.newPlanStatus !== "completed" && parsed.newPlanStatus !== "blocked" && parsed.newPlanStatus !== "closed" || typeof parsed.oldWorkflowState !== "string" || !WORKFLOW_STATES.has(parsed.oldWorkflowState) || parsed.newWorkflowState !== "complete" && parsed.newWorkflowState !== "blocked" && parsed.newWorkflowState !== "closed" || !Number.isInteger(parsed.generation) || (parsed.generation ?? -1) < 0 || typeof parsed.qaExempt !== "boolean" || typeof parsed.recordedAt !== "string" || !Number.isFinite(Date.parse(parsed.recordedAt))) {
|
|
610
|
+
formatUnreadableMessage("TASK_TERMINAL_WAL_UNREADABLE", filePath, "is not a valid terminal WAL", "Preserve this file and reconcile the task terminal transition before moving it aside.");
|
|
611
|
+
}
|
|
612
|
+
if (parsed.version === 2 && (typeof parsed.planIdentityHash !== "string" || !SHA256_PATTERN.test(parsed.planIdentityHash) || typeof parsed.planEpoch !== "string" || !UUID_PATTERN.test(parsed.planEpoch))) {
|
|
613
|
+
formatUnreadableMessage("TASK_TERMINAL_WAL_UNREADABLE", filePath, "is missing the v2 plan identity fields", "Preserve this file and reconcile the task terminal transition before moving it aside.");
|
|
614
|
+
}
|
|
615
|
+
if (parsed.version === 1 && (parsed.newPlanStatus === "closed" || parsed.newWorkflowState === "closed")) {
|
|
616
|
+
formatUnreadableMessage("TASK_TERMINAL_WAL_UNREADABLE", filePath, "claims a closed transition in a legacy v1 terminal WAL", "Preserve this file and reconcile the task terminal transition before moving it aside.");
|
|
617
|
+
}
|
|
618
|
+
if (!(parsed.newPlanStatus === "completed" && parsed.newWorkflowState === "complete" || parsed.newPlanStatus === "blocked" && parsed.newWorkflowState === "blocked" || parsed.newPlanStatus === "closed" && parsed.newWorkflowState === "closed")) {
|
|
619
|
+
throw new Error(`TASK_TERMINAL_WAL_STATE_MISMATCH: ${filePath} encodes ${String(parsed.newPlanStatus)} -> ${String(parsed.newWorkflowState)}`);
|
|
620
|
+
}
|
|
621
|
+
if (expectedTaskId && parsed.taskId !== expectedTaskId) {
|
|
622
|
+
formatUnreadableMessage("TASK_TERMINAL_WAL_TASK_MISMATCH", filePath, `records task ${parsed.taskId} but was read for task ${expectedTaskId}`, "Preserve this file and reconcile the task terminal transition before moving it aside.");
|
|
623
|
+
}
|
|
624
|
+
return parsed;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/workflow/workflow-wal-file.ts
|
|
628
|
+
var UTF8_FATAL_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
629
|
+
var CODER_SETTLEMENT_MAX_BYTES = 64 * 1024 * 1024;
|
|
630
|
+
var SMALL_WORKFLOW_WAL_MAX_BYTES = 1024 * 1024;
|
|
631
|
+
var CHUNK_BYTES = 64 * 1024;
|
|
632
|
+
var _internals2 = {
|
|
633
|
+
openFile: open,
|
|
634
|
+
openFileSync: fs2.openSync,
|
|
635
|
+
readFileSync: fs2.readSync,
|
|
636
|
+
closeFileSync: fs2.closeSync
|
|
637
|
+
};
|
|
638
|
+
function maxBytesFor(kind) {
|
|
639
|
+
return kind === "coder-settlement" ? CODER_SETTLEMENT_MAX_BYTES : SMALL_WORKFLOW_WAL_MAX_BYTES;
|
|
640
|
+
}
|
|
641
|
+
function remediationFor(kind) {
|
|
642
|
+
switch (kind) {
|
|
643
|
+
case "coder-settlement":
|
|
644
|
+
return "Preserve this file, reconcile the task lane, and only then move it aside.";
|
|
645
|
+
case "task-repair":
|
|
646
|
+
return "Preserve this file, reconcile the repair transition, and only then move it aside.";
|
|
647
|
+
case "task-terminal":
|
|
648
|
+
return "Preserve this file and reconcile the task terminal transition before moving it aside.";
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function unreadableCode(kind) {
|
|
652
|
+
switch (kind) {
|
|
653
|
+
case "coder-settlement":
|
|
654
|
+
return "CODER_SETTLEMENT_WAL_UNREADABLE";
|
|
655
|
+
case "task-repair":
|
|
656
|
+
return "TASK_REPAIR_WAL_UNREADABLE";
|
|
657
|
+
case "task-terminal":
|
|
658
|
+
return "TASK_TERMINAL_WAL_UNREADABLE";
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function walIoError(kind, filePath, action, error) {
|
|
662
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
663
|
+
return new Error(`${unreadableCode(kind)}: could not ${action} ${filePath} (${detail}). ${remediationFor(kind)}`, { cause: error });
|
|
664
|
+
}
|
|
665
|
+
function oversizeCode(kind) {
|
|
666
|
+
switch (kind) {
|
|
667
|
+
case "coder-settlement":
|
|
668
|
+
return "CODER_SETTLEMENT_WAL_OVERSIZE";
|
|
669
|
+
case "task-repair":
|
|
670
|
+
return "TASK_REPAIR_WAL_OVERSIZE";
|
|
671
|
+
case "task-terminal":
|
|
672
|
+
return "TASK_TERMINAL_WAL_OVERSIZE";
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function oversizeMessage(kind, filePath, maxBytes) {
|
|
676
|
+
if (kind === "coder-settlement") {
|
|
677
|
+
throw new Error(`${oversizeCode(kind)}: ${filePath} exceeds the 64 MiB safety limit. Preserve this file, reconcile the lane, reduce or partition the declared scope, and only then move the WAL aside.`);
|
|
678
|
+
}
|
|
679
|
+
throw new Error(`${oversizeCode(kind)}: ${filePath} exceeds the ${maxBytes} byte safety limit. ${remediationFor(kind)}`);
|
|
680
|
+
}
|
|
681
|
+
function decodeUtf8(kind, filePath, bytes) {
|
|
682
|
+
try {
|
|
683
|
+
return UTF8_FATAL_DECODER.decode(bytes);
|
|
684
|
+
} catch (error) {
|
|
685
|
+
throw new Error(`${unreadableCode(kind)}: ${filePath} is not valid UTF-8 (${error instanceof Error ? error.message : String(error)}). ${remediationFor(kind)}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function validateWal(kind, raw, filePath, expectedTaskId) {
|
|
689
|
+
switch (kind) {
|
|
690
|
+
case "coder-settlement":
|
|
691
|
+
return parseCoderSettlementWal(raw, filePath, expectedTaskId);
|
|
692
|
+
case "task-repair":
|
|
693
|
+
return parseTaskRepairWal(raw, filePath, expectedTaskId);
|
|
694
|
+
case "task-terminal":
|
|
695
|
+
return parseTaskTerminalWal(raw, filePath, expectedTaskId);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
async function readBoundedBytes(kind, filePath, maxBytes) {
|
|
699
|
+
let handle;
|
|
700
|
+
try {
|
|
701
|
+
handle = await _internals2.openFile(filePath, "r");
|
|
702
|
+
} catch (error) {
|
|
703
|
+
if (error.code === "ENOENT")
|
|
704
|
+
return null;
|
|
705
|
+
throw walIoError(kind, filePath, "open", error);
|
|
706
|
+
}
|
|
707
|
+
let result = new Uint8Array;
|
|
708
|
+
let failure = null;
|
|
709
|
+
try {
|
|
710
|
+
const limit = maxBytes + 1;
|
|
711
|
+
const chunks = [];
|
|
712
|
+
let total = 0;
|
|
713
|
+
while (true) {
|
|
714
|
+
const remaining = limit - total;
|
|
715
|
+
if (remaining <= 0)
|
|
716
|
+
break;
|
|
717
|
+
const buffer = Buffer.alloc(Math.min(CHUNK_BYTES, remaining));
|
|
718
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
|
|
719
|
+
if (bytesRead === 0)
|
|
720
|
+
break;
|
|
721
|
+
total += bytesRead;
|
|
722
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
723
|
+
if (total > maxBytes)
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
result = Buffer.concat(chunks, total);
|
|
727
|
+
} catch (error) {
|
|
728
|
+
failure = walIoError(kind, filePath, "read", error);
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
await handle.close();
|
|
732
|
+
} catch (error) {
|
|
733
|
+
failure ??= walIoError(kind, filePath, "close", error);
|
|
734
|
+
}
|
|
735
|
+
if (failure)
|
|
736
|
+
throw failure;
|
|
737
|
+
return result;
|
|
738
|
+
}
|
|
739
|
+
function readBoundedBytesSync(kind, filePath, maxBytes) {
|
|
740
|
+
let fd;
|
|
741
|
+
try {
|
|
742
|
+
fd = _internals2.openFileSync(filePath, "r");
|
|
743
|
+
} catch (error) {
|
|
744
|
+
if (error.code === "ENOENT")
|
|
745
|
+
return null;
|
|
746
|
+
throw walIoError(kind, filePath, "open", error);
|
|
747
|
+
}
|
|
748
|
+
let result = new Uint8Array;
|
|
749
|
+
let failure = null;
|
|
750
|
+
try {
|
|
751
|
+
const limit = maxBytes + 1;
|
|
752
|
+
const chunks = [];
|
|
753
|
+
let total = 0;
|
|
754
|
+
while (true) {
|
|
755
|
+
const remaining = limit - total;
|
|
756
|
+
if (remaining <= 0)
|
|
757
|
+
break;
|
|
758
|
+
const buffer = Buffer.alloc(Math.min(CHUNK_BYTES, remaining));
|
|
759
|
+
const bytesRead = _internals2.readFileSync(fd, buffer, 0, buffer.length, null);
|
|
760
|
+
if (bytesRead === 0)
|
|
761
|
+
break;
|
|
762
|
+
total += bytesRead;
|
|
763
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
764
|
+
if (total > maxBytes)
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
result = Buffer.concat(chunks, total);
|
|
768
|
+
} catch (error) {
|
|
769
|
+
failure = walIoError(kind, filePath, "read", error);
|
|
770
|
+
}
|
|
771
|
+
try {
|
|
772
|
+
_internals2.closeFileSync(fd);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
failure ??= walIoError(kind, filePath, "close", error);
|
|
775
|
+
}
|
|
776
|
+
if (failure)
|
|
777
|
+
throw failure;
|
|
778
|
+
return result;
|
|
779
|
+
}
|
|
780
|
+
async function readWorkflowWalFile(kind, filePath, expectedTaskId) {
|
|
781
|
+
const maxBytes = maxBytesFor(kind);
|
|
782
|
+
const bytes = await readBoundedBytes(kind, filePath, maxBytes);
|
|
783
|
+
if (bytes === null)
|
|
784
|
+
return null;
|
|
785
|
+
if (bytes.length === 0) {
|
|
786
|
+
throw new Error(`${unreadableCode(kind)}: ${filePath} is empty. ${remediationFor(kind)}`);
|
|
787
|
+
}
|
|
788
|
+
if (bytes.length > maxBytes)
|
|
789
|
+
oversizeMessage(kind, filePath, maxBytes);
|
|
790
|
+
return validateWal(kind, decodeUtf8(kind, filePath, bytes), filePath, expectedTaskId);
|
|
791
|
+
}
|
|
792
|
+
function readWorkflowWalFileSync(kind, filePath, expectedTaskId) {
|
|
793
|
+
const maxBytes = maxBytesFor(kind);
|
|
794
|
+
const bytes = readBoundedBytesSync(kind, filePath, maxBytes);
|
|
795
|
+
if (bytes === null)
|
|
796
|
+
return null;
|
|
797
|
+
if (bytes.length === 0) {
|
|
798
|
+
throw new Error(`${unreadableCode(kind)}: ${filePath} is empty. ${remediationFor(kind)}`);
|
|
799
|
+
}
|
|
800
|
+
if (bytes.length > maxBytes)
|
|
801
|
+
oversizeMessage(kind, filePath, maxBytes);
|
|
802
|
+
return validateWal(kind, decodeUtf8(kind, filePath, bytes), filePath, expectedTaskId);
|
|
803
|
+
}
|
|
804
|
+
async function writeWorkflowWalFile(kind, filePath, wal) {
|
|
805
|
+
const serialized = `${JSON.stringify(wal, null, 2)}
|
|
806
|
+
`;
|
|
807
|
+
const maxBytes = maxBytesFor(kind);
|
|
808
|
+
if (Buffer.byteLength(serialized, "utf8") > maxBytes) {
|
|
809
|
+
oversizeMessage(kind, filePath, maxBytes);
|
|
810
|
+
}
|
|
811
|
+
validateWal(kind, serialized, filePath, wal.taskId);
|
|
812
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
813
|
+
await atomicWriteFile(filePath, serialized);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// src/gate-evidence.ts
|
|
817
|
+
var TASK_WORKFLOW_SCHEMA_MARKER = "exact-task-v1";
|
|
818
|
+
var TASK_WORKFLOW_STATES = [
|
|
819
|
+
"idle",
|
|
820
|
+
"coder_delegated",
|
|
821
|
+
"pre_check_passed",
|
|
822
|
+
"reviewer_run",
|
|
823
|
+
"tests_run",
|
|
824
|
+
"rework_required",
|
|
825
|
+
"complete",
|
|
826
|
+
"blocked",
|
|
827
|
+
"closed"
|
|
828
|
+
];
|
|
829
|
+
var TASK_WORKFLOW_OUTCOMES = [
|
|
830
|
+
"none",
|
|
831
|
+
"dispatch_attempted",
|
|
832
|
+
"dispatch_no_mutation",
|
|
833
|
+
"accepted_mutation",
|
|
834
|
+
"accepted_mutation_failed",
|
|
835
|
+
"stage_a_passed",
|
|
836
|
+
"stage_a_failed",
|
|
837
|
+
"stage_b_completed",
|
|
838
|
+
"stage_b_failed",
|
|
839
|
+
"gate_recorded",
|
|
840
|
+
"task_completed",
|
|
841
|
+
"task_blocked",
|
|
842
|
+
"task_closed",
|
|
843
|
+
"repair_idle"
|
|
844
|
+
];
|
|
845
|
+
function assertTaskEvidenceWriteAllowed(directory, taskId, event) {
|
|
846
|
+
assertValidTaskId(taskId);
|
|
847
|
+
const coderWalPath = validateSwarmPath(directory, `coder-settlements/${taskId}.json`);
|
|
848
|
+
const coderWal = readWorkflowWalFileSync("coder-settlement", coderWalPath, taskId);
|
|
849
|
+
if ((coderWal?.state === "DISPATCHED" || coderWal?.state === "PREPARED") && !(event != null && event.transitionId === coderWal.transitionId && (event.type === "dispatch_attempted" || event.type === "dispatch_no_mutation" || event.type === "accepted_mutation"))) {
|
|
850
|
+
throw new Error(`CODER_SETTLEMENT_IN_PROGRESS: transition ${String(coderWal.transitionId)} owns evidence for task ${taskId}`);
|
|
851
|
+
}
|
|
852
|
+
const terminalWalPath = validateSwarmPath(directory, `task-terminals/${taskId}.json`);
|
|
853
|
+
const terminalWal = readWorkflowWalFileSync("task-terminal", terminalWalPath, taskId);
|
|
854
|
+
if (terminalWal?.state === "PREPARED") {
|
|
855
|
+
const matchingTerminalEvent = event != null && event.transitionId === terminalWal.transitionId && (event.type === "task_completed" && terminalWal.newPlanStatus === "completed" || event.type === "task_blocked" && terminalWal.newPlanStatus === "blocked" || event.type === "task_closed" && terminalWal.newPlanStatus === "closed");
|
|
856
|
+
if (!matchingTerminalEvent) {
|
|
857
|
+
throw new Error(`TASK_TERMINAL_PREPARED: transition ${String(terminalWal.transitionId)} owns evidence for task ${taskId}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const repairWalPath = validateSwarmPath(directory, `task-repairs/${taskId}.json`);
|
|
861
|
+
const repairWal = readWorkflowWalFileSync("task-repair", repairWalPath, taskId);
|
|
862
|
+
if (repairWal?.state === "PREPARED" && !(event?.type === "repair_idle" && event.transitionId === repairWal.transitionId)) {
|
|
863
|
+
throw new Error(`TASK_REPAIR_PREPARED: transition ${String(repairWal.transitionId)} owns evidence for task ${taskId}`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
var GateEvidenceSchema = exports_external.object({
|
|
867
|
+
sessionId: exports_external.string(),
|
|
868
|
+
timestamp: exports_external.string(),
|
|
869
|
+
agent: exports_external.string()
|
|
870
|
+
}).passthrough();
|
|
871
|
+
var TaskWorkflowMetadataSchema = exports_external.object({
|
|
872
|
+
schema: exports_external.literal(TASK_WORKFLOW_SCHEMA_MARKER),
|
|
873
|
+
generation: exports_external.number().int().min(0),
|
|
874
|
+
state: exports_external.enum(TASK_WORKFLOW_STATES),
|
|
875
|
+
retryCount: exports_external.number().int().min(0).default(0),
|
|
876
|
+
retryHistory: exports_external.array(exports_external.enum(TASK_WORKFLOW_OUTCOMES)).max(3).default([]),
|
|
877
|
+
retryEpoch: exports_external.number().int().min(0).default(0),
|
|
878
|
+
lastOutcome: exports_external.enum(TASK_WORKFLOW_OUTCOMES).default("none"),
|
|
879
|
+
lastTransitionId: exports_external.string().min(1).nullable().optional().default(null),
|
|
880
|
+
updatedAt: exports_external.string(),
|
|
881
|
+
forcedCompletion: exports_external.boolean().optional()
|
|
882
|
+
});
|
|
883
|
+
var TaskEvidenceSchema = exports_external.object({
|
|
884
|
+
taskId: exports_external.string(),
|
|
885
|
+
required_gates: exports_external.array(exports_external.string()).default([]),
|
|
886
|
+
gates: exports_external.record(exports_external.string(), GateEvidenceSchema),
|
|
887
|
+
turbo: exports_external.boolean().optional(),
|
|
888
|
+
test_engineer_exempt: exports_external.boolean().optional(),
|
|
889
|
+
workflow: TaskWorkflowMetadataSchema.optional()
|
|
890
|
+
});
|
|
891
|
+
function parseTaskEvidence(raw, expectedTaskId) {
|
|
892
|
+
const parsed = TaskEvidenceSchema.parse(JSON.parse(raw));
|
|
893
|
+
if (parsed.taskId !== expectedTaskId) {
|
|
894
|
+
throw new Error(`TASK_EVIDENCE_IDENTITY_MISMATCH: expected ${expectedTaskId}, found ${parsed.taskId}`);
|
|
895
|
+
}
|
|
896
|
+
return parsed;
|
|
897
|
+
}
|
|
898
|
+
var DEFAULT_REQUIRED_GATES = ["reviewer", "test_engineer"];
|
|
899
|
+
var DEFAULT_WORKFLOW_STATE = "idle";
|
|
900
|
+
var WORKFLOW_STATE_RANK = {
|
|
901
|
+
idle: 0,
|
|
902
|
+
coder_delegated: 1,
|
|
903
|
+
pre_check_passed: 2,
|
|
904
|
+
reviewer_run: 3,
|
|
905
|
+
tests_run: 4,
|
|
906
|
+
rework_required: 5,
|
|
907
|
+
blocked: 6,
|
|
908
|
+
closed: 7,
|
|
909
|
+
complete: 8
|
|
910
|
+
};
|
|
911
|
+
function isValidTaskId(taskId) {
|
|
912
|
+
return isStrictTaskId(taskId);
|
|
913
|
+
}
|
|
914
|
+
function assertValidTaskId(taskId) {
|
|
915
|
+
assertStrictTaskId(taskId);
|
|
916
|
+
}
|
|
917
|
+
function createDefaultWorkflowMetadata(nowIso) {
|
|
918
|
+
return {
|
|
919
|
+
schema: TASK_WORKFLOW_SCHEMA_MARKER,
|
|
920
|
+
generation: 0,
|
|
921
|
+
state: DEFAULT_WORKFLOW_STATE,
|
|
922
|
+
retryCount: 0,
|
|
923
|
+
retryHistory: [],
|
|
924
|
+
retryEpoch: 0,
|
|
925
|
+
lastOutcome: "none",
|
|
926
|
+
lastTransitionId: null,
|
|
927
|
+
updatedAt: nowIso
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
function getEventOutcome(event) {
|
|
931
|
+
switch (event.type) {
|
|
932
|
+
case "dispatch_attempted":
|
|
933
|
+
return "dispatch_attempted";
|
|
934
|
+
case "dispatch_no_mutation":
|
|
935
|
+
return "dispatch_no_mutation";
|
|
936
|
+
case "accepted_mutation":
|
|
937
|
+
return event.context?.settlementFailed === true ? "accepted_mutation_failed" : "accepted_mutation";
|
|
938
|
+
case "stage_a_passed":
|
|
939
|
+
return "stage_a_passed";
|
|
940
|
+
case "stage_a_failed":
|
|
941
|
+
return "stage_a_failed";
|
|
942
|
+
case "stage_b_completed":
|
|
943
|
+
return "stage_b_completed";
|
|
944
|
+
case "stage_b_failed":
|
|
945
|
+
return "stage_b_failed";
|
|
946
|
+
case "gate_recorded":
|
|
947
|
+
return "gate_recorded";
|
|
948
|
+
case "task_completed":
|
|
949
|
+
return "task_completed";
|
|
950
|
+
case "task_blocked":
|
|
951
|
+
return "task_blocked";
|
|
952
|
+
case "task_closed":
|
|
953
|
+
return "task_closed";
|
|
954
|
+
case "repair_idle":
|
|
955
|
+
return "repair_idle";
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
function isAuthoritativeWorkflowMetadata(workflow) {
|
|
959
|
+
return workflow?.schema === TASK_WORKFLOW_SCHEMA_MARKER;
|
|
960
|
+
}
|
|
961
|
+
function getTaskWorkflowSnapshot(evidence) {
|
|
962
|
+
if (isAuthoritativeWorkflowMetadata(evidence?.workflow)) {
|
|
963
|
+
return { ...evidence.workflow, authoritative: true };
|
|
964
|
+
}
|
|
965
|
+
return {
|
|
966
|
+
...createDefaultWorkflowMetadata(""),
|
|
967
|
+
authoritative: false
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
function maybeExpandRequiredGates(existing, event) {
|
|
971
|
+
if (event.type === "accepted_mutation") {
|
|
972
|
+
const agentType = event.agentType;
|
|
973
|
+
if (typeof agentType === "string" && agentType.trim() !== "") {
|
|
974
|
+
return existing ? expandRequiredGates(existing.required_gates, agentType, event.context ?? {}) : deriveRequiredGates(agentType, event.context ?? {});
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (event.type === "stage_b_completed" && event.ensureDefaultStageB === true) {
|
|
978
|
+
return expandRequiredGates(existing?.required_gates ?? [], "coder", {
|
|
979
|
+
testEngineerExempt: false
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
if (event.type === "stage_b_completed" || event.type === "gate_recorded") {
|
|
983
|
+
return existing ? expandRequiredGates(existing.required_gates, event.gate) : deriveRequiredGates(event.gate);
|
|
984
|
+
}
|
|
985
|
+
return existing?.required_gates ?? [];
|
|
986
|
+
}
|
|
987
|
+
function hasAllRequiredGatesPassed(requiredGates, gates) {
|
|
988
|
+
return requiredGates.length > 0 && requiredGates.every((gate) => gates[gate] != null);
|
|
989
|
+
}
|
|
990
|
+
function shouldTreatGateAsStageB(gate) {
|
|
991
|
+
return gate === "reviewer" || gate === "test_engineer";
|
|
992
|
+
}
|
|
993
|
+
function clearWorkflowGateProof(gates) {
|
|
994
|
+
const retained = { ...gates };
|
|
995
|
+
delete retained.pre_check;
|
|
996
|
+
delete retained.reviewer;
|
|
997
|
+
delete retained.test_engineer;
|
|
998
|
+
delete retained.council;
|
|
999
|
+
delete retained.critic;
|
|
1000
|
+
delete retained.critic_sounding_board;
|
|
1001
|
+
return retained;
|
|
1002
|
+
}
|
|
1003
|
+
function assertExpectedGeneration(snapshot, event) {
|
|
1004
|
+
if (typeof event.expectedGeneration === "number" && event.expectedGeneration !== snapshot.generation) {
|
|
1005
|
+
throw new Error(`TASK_WORKFLOW_GENERATION_MISMATCH: expected ${event.expectedGeneration}, current ${snapshot.generation}`);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function isDuplicateTransition(snapshot, event) {
|
|
1009
|
+
return snapshot.authoritative && typeof event.transitionId === "string" && event.transitionId.length > 0 && snapshot.lastTransitionId === event.transitionId && snapshot.lastOutcome === getEventOutcome(event);
|
|
1010
|
+
}
|
|
1011
|
+
function reduceTaskWorkflowSnapshot(current, event, context) {
|
|
1012
|
+
if ((current.state === "complete" || current.state === "blocked" || current.state === "closed") && event.type !== "repair_idle" && !(current.state === "complete" && event.type === "task_completed" || current.state === "blocked" && event.type === "task_blocked" || current.state === "blocked" && event.type === "task_closed")) {
|
|
1013
|
+
throw new Error(`TASK_WORKFLOW_TERMINAL: cannot apply ${event.type} from ${current.state}`);
|
|
1014
|
+
}
|
|
1015
|
+
const outcome = getEventOutcome(event);
|
|
1016
|
+
const base = {
|
|
1017
|
+
schema: TASK_WORKFLOW_SCHEMA_MARKER,
|
|
1018
|
+
generation: current.generation,
|
|
1019
|
+
state: current.state,
|
|
1020
|
+
retryCount: current.retryCount,
|
|
1021
|
+
retryHistory: current.retryHistory,
|
|
1022
|
+
retryEpoch: current.retryEpoch,
|
|
1023
|
+
lastOutcome: outcome,
|
|
1024
|
+
lastTransitionId: event.transitionId ?? current.lastTransitionId,
|
|
1025
|
+
updatedAt: context.nowIso,
|
|
1026
|
+
...current.forcedCompletion === true ? { forcedCompletion: true } : {}
|
|
1027
|
+
};
|
|
1028
|
+
switch (event.type) {
|
|
1029
|
+
case "dispatch_attempted":
|
|
1030
|
+
return base;
|
|
1031
|
+
case "dispatch_no_mutation":
|
|
1032
|
+
return {
|
|
1033
|
+
...base,
|
|
1034
|
+
retryCount: Math.min(current.retryCount + 1, 3),
|
|
1035
|
+
retryHistory: [...current.retryHistory, outcome].slice(-3),
|
|
1036
|
+
retryEpoch: current.retryEpoch || current.generation + 1
|
|
1037
|
+
};
|
|
1038
|
+
case "stage_b_failed":
|
|
1039
|
+
if (current.state !== "pre_check_passed" && current.state !== "reviewer_run" && current.state !== "tests_run" && current.state !== "rework_required") {
|
|
1040
|
+
throw new Error(`TASK_WORKFLOW_STAGE_A_REQUIRED: cannot reject ${event.gate} from ${current.state}`);
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
...base,
|
|
1044
|
+
state: "rework_required",
|
|
1045
|
+
retryCount: Math.min(current.retryCount + 1, 3),
|
|
1046
|
+
retryHistory: [...current.retryHistory, outcome].slice(-3),
|
|
1047
|
+
retryEpoch: current.retryEpoch || current.generation + 1
|
|
1048
|
+
};
|
|
1049
|
+
case "accepted_mutation":
|
|
1050
|
+
if (event.context?.settlementFailed === true) {
|
|
1051
|
+
return {
|
|
1052
|
+
...base,
|
|
1053
|
+
generation: current.generation + 1,
|
|
1054
|
+
state: "rework_required",
|
|
1055
|
+
retryCount: Math.min(current.retryCount + 1, 3),
|
|
1056
|
+
retryHistory: [...current.retryHistory, outcome].slice(-3),
|
|
1057
|
+
retryEpoch: current.retryEpoch || current.generation + 1
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
return {
|
|
1061
|
+
...base,
|
|
1062
|
+
generation: current.generation + 1,
|
|
1063
|
+
state: "coder_delegated"
|
|
1064
|
+
};
|
|
1065
|
+
case "stage_a_passed":
|
|
1066
|
+
if (current.state !== "coder_delegated" && current.state !== "pre_check_passed") {
|
|
1067
|
+
throw new Error(`TASK_WORKFLOW_CODER_MUTATION_REQUIRED: cannot pass Stage A from ${current.state}`);
|
|
1068
|
+
}
|
|
1069
|
+
return {
|
|
1070
|
+
...base,
|
|
1071
|
+
state: "pre_check_passed"
|
|
1072
|
+
};
|
|
1073
|
+
case "stage_a_failed":
|
|
1074
|
+
if (current.state !== "coder_delegated" && current.state !== "pre_check_passed" && current.state !== "rework_required") {
|
|
1075
|
+
throw new Error(`TASK_WORKFLOW_CODER_MUTATION_REQUIRED: cannot fail Stage A from ${current.state}`);
|
|
1076
|
+
}
|
|
1077
|
+
return {
|
|
1078
|
+
...base,
|
|
1079
|
+
state: "rework_required",
|
|
1080
|
+
retryCount: Math.min(current.retryCount + 1, 3),
|
|
1081
|
+
retryHistory: [...current.retryHistory, outcome].slice(-3),
|
|
1082
|
+
retryEpoch: current.retryEpoch || current.generation + 1
|
|
1083
|
+
};
|
|
1084
|
+
case "stage_b_completed":
|
|
1085
|
+
if (current.state !== "pre_check_passed" && current.state !== "reviewer_run" && current.state !== "tests_run") {
|
|
1086
|
+
throw new Error(`TASK_WORKFLOW_STAGE_A_REQUIRED: cannot record ${event.gate} from ${current.state}`);
|
|
1087
|
+
}
|
|
1088
|
+
return {
|
|
1089
|
+
...base,
|
|
1090
|
+
state: hasAllRequiredGatesPassed(context.requiredGates, context.gates) ? "tests_run" : "reviewer_run"
|
|
1091
|
+
};
|
|
1092
|
+
case "gate_recorded":
|
|
1093
|
+
return {
|
|
1094
|
+
...base,
|
|
1095
|
+
state: current.state
|
|
1096
|
+
};
|
|
1097
|
+
case "task_completed":
|
|
1098
|
+
if (event.qaExempt !== true && current.state !== "tests_run" && current.state !== "complete" && !(current.state === "pre_check_passed" && context.gates.council != null)) {
|
|
1099
|
+
throw new Error(`TASK_WORKFLOW_QA_REQUIRED: cannot complete from ${current.state}`);
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
...base,
|
|
1103
|
+
state: "complete",
|
|
1104
|
+
...event.qaExempt === true ? { forcedCompletion: true } : {}
|
|
1105
|
+
};
|
|
1106
|
+
case "task_blocked":
|
|
1107
|
+
return {
|
|
1108
|
+
...base,
|
|
1109
|
+
state: "blocked"
|
|
1110
|
+
};
|
|
1111
|
+
case "task_closed":
|
|
1112
|
+
return {
|
|
1113
|
+
...base,
|
|
1114
|
+
state: "closed"
|
|
1115
|
+
};
|
|
1116
|
+
case "repair_idle": {
|
|
1117
|
+
const { forcedCompletion: _cleared, ...withoutForced } = base;
|
|
1118
|
+
return {
|
|
1119
|
+
...withoutForced,
|
|
1120
|
+
generation: current.generation + 1,
|
|
1121
|
+
state: "idle",
|
|
1122
|
+
retryCount: 0,
|
|
1123
|
+
retryHistory: [],
|
|
1124
|
+
retryEpoch: 0
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
function getEvidenceDir(directory) {
|
|
1130
|
+
const swarmDir = path2.resolve(directory, ".swarm");
|
|
1131
|
+
const evidenceDir = path2.join(swarmDir, "evidence");
|
|
1132
|
+
mkdirSync2(evidenceDir, { recursive: true });
|
|
1133
|
+
let resolvedSwarmDir;
|
|
1134
|
+
let resolvedEvidenceDir;
|
|
1135
|
+
try {
|
|
1136
|
+
resolvedSwarmDir = path2.normalize(realpathSync(swarmDir));
|
|
1137
|
+
resolvedEvidenceDir = path2.normalize(realpathSync(evidenceDir));
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
throw new Error(`Unable to resolve evidence directory: ${error.message}`);
|
|
1140
|
+
}
|
|
1141
|
+
const swarmPrefix = `${resolvedSwarmDir}${path2.sep}`;
|
|
1142
|
+
const withinSwarmBoundary = process.platform === "win32" ? resolvedEvidenceDir.toLowerCase().startsWith(swarmPrefix.toLowerCase()) : resolvedEvidenceDir.startsWith(swarmPrefix);
|
|
1143
|
+
if (!withinSwarmBoundary) {
|
|
1144
|
+
throw new Error(`Evidence path escapes .swarm boundary: ${resolvedEvidenceDir}`);
|
|
1145
|
+
}
|
|
1146
|
+
return resolvedEvidenceDir;
|
|
1147
|
+
}
|
|
1148
|
+
function getEvidencePath(directory, taskId) {
|
|
1149
|
+
assertValidTaskId(taskId);
|
|
1150
|
+
return taskEvidencePath(directory, taskId);
|
|
1151
|
+
}
|
|
1152
|
+
function readExisting(evidencePath, taskId) {
|
|
1153
|
+
try {
|
|
1154
|
+
const raw = readFileSync2(evidencePath, "utf-8");
|
|
1155
|
+
return parseTaskEvidence(raw, taskId);
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
if (error.code === "ENOENT")
|
|
1158
|
+
return null;
|
|
1159
|
+
telemetry.gateParseError(taskId, error);
|
|
1160
|
+
throw error;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
function deriveRequiredGates(agentType, context = {}) {
|
|
1164
|
+
switch (agentType) {
|
|
1165
|
+
case "coder":
|
|
1166
|
+
return context.testEngineerExempt === true ? ["reviewer"] : ["reviewer", "test_engineer"];
|
|
1167
|
+
case "docs":
|
|
1168
|
+
return ["docs"];
|
|
1169
|
+
case "designer":
|
|
1170
|
+
return ["designer", "reviewer", "test_engineer"];
|
|
1171
|
+
case "explorer":
|
|
1172
|
+
return ["explorer"];
|
|
1173
|
+
case "sme":
|
|
1174
|
+
return ["sme"];
|
|
1175
|
+
case "reviewer":
|
|
1176
|
+
return ["reviewer"];
|
|
1177
|
+
case "test_engineer":
|
|
1178
|
+
return ["test_engineer"];
|
|
1179
|
+
case "critic":
|
|
1180
|
+
return ["critic"];
|
|
1181
|
+
case "critic_sounding_board":
|
|
1182
|
+
return ["critic_sounding_board"];
|
|
1183
|
+
case "critic_drift_verifier":
|
|
1184
|
+
return ["critic_drift_verifier"];
|
|
1185
|
+
case "critic_hallucination_verifier":
|
|
1186
|
+
return ["critic_hallucination_verifier"];
|
|
1187
|
+
case "critic_architecture_supervisor":
|
|
1188
|
+
return ["critic_architecture_supervisor"];
|
|
1189
|
+
default:
|
|
1190
|
+
return ["reviewer", "test_engineer"];
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
function expandRequiredGates(existingGates, newAgentType, context = {}) {
|
|
1194
|
+
const newGates = deriveRequiredGates(newAgentType, context);
|
|
1195
|
+
const combined = [...new Set([...existingGates ?? [], ...newGates])];
|
|
1196
|
+
return combined.sort();
|
|
1197
|
+
}
|
|
1198
|
+
function updateEvidenceForTransition(existing, event) {
|
|
1199
|
+
const nowIso = new Date().toISOString();
|
|
1200
|
+
const requiredGates = maybeExpandRequiredGates(existing, event);
|
|
1201
|
+
let gates = {
|
|
1202
|
+
...existing?.gates ?? {}
|
|
1203
|
+
};
|
|
1204
|
+
if (event.type === "accepted_mutation" || event.type === "repair_idle") {
|
|
1205
|
+
gates = clearWorkflowGateProof(gates);
|
|
1206
|
+
}
|
|
1207
|
+
if (event.type === "stage_a_passed") {
|
|
1208
|
+
gates = {
|
|
1209
|
+
...gates,
|
|
1210
|
+
pre_check: {
|
|
1211
|
+
sessionId: "system",
|
|
1212
|
+
timestamp: nowIso,
|
|
1213
|
+
agent: "pre_check"
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
if (event.type === "stage_a_failed") {
|
|
1218
|
+
delete gates.pre_check;
|
|
1219
|
+
}
|
|
1220
|
+
if (event.type === "stage_b_failed") {
|
|
1221
|
+
delete gates.reviewer;
|
|
1222
|
+
delete gates.test_engineer;
|
|
1223
|
+
}
|
|
1224
|
+
if (event.type === "stage_b_completed" || event.type === "gate_recorded") {
|
|
1225
|
+
gates = {
|
|
1226
|
+
...gates,
|
|
1227
|
+
[event.gate]: {
|
|
1228
|
+
sessionId: event.sessionId,
|
|
1229
|
+
timestamp: nowIso,
|
|
1230
|
+
agent: event.gate
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
const snapshot = getTaskWorkflowSnapshot(existing);
|
|
1235
|
+
assertExpectedGeneration(snapshot, event);
|
|
1236
|
+
if (isDuplicateTransition(snapshot, event) && existing) {
|
|
1237
|
+
return existing;
|
|
1238
|
+
}
|
|
1239
|
+
const workflow = reduceTaskWorkflowSnapshot(snapshot, event, {
|
|
1240
|
+
requiredGates,
|
|
1241
|
+
gates,
|
|
1242
|
+
nowIso
|
|
1243
|
+
});
|
|
1244
|
+
return {
|
|
1245
|
+
taskId: existing?.taskId ?? "",
|
|
1246
|
+
required_gates: requiredGates,
|
|
1247
|
+
gates,
|
|
1248
|
+
turbo: "turbo" in event && event.turbo === true ? true : ("turbo" in event) && event.turbo === false ? existing?.turbo : existing?.turbo,
|
|
1249
|
+
test_engineer_exempt: (event.type === "dispatch_attempted" || event.type === "dispatch_no_mutation" || event.type === "accepted_mutation") && event.agentType === "coder" ? event.context?.testEngineerExempt === true && !requiredGates.includes("test_engineer") : existing?.test_engineer_exempt,
|
|
1250
|
+
workflow
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
async function transitionTaskWorkflowEvidence(directory, taskId, event) {
|
|
1254
|
+
assertValidTaskId(taskId);
|
|
1255
|
+
let updatedEvidence = null;
|
|
1256
|
+
await withTaskEvidenceTransaction(directory, taskId, event.type, async (transaction) => {
|
|
1257
|
+
updatedEvidence = await transaction.transition(event);
|
|
1258
|
+
});
|
|
1259
|
+
return updatedEvidence ?? {
|
|
1260
|
+
taskId,
|
|
1261
|
+
required_gates: [],
|
|
1262
|
+
gates: {},
|
|
1263
|
+
workflow: createDefaultWorkflowMetadata(new Date().toISOString())
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
async function withTaskEvidenceTransaction(directory, taskId, agent, callback) {
|
|
1267
|
+
assertValidTaskId(taskId);
|
|
1268
|
+
return withTaskEvidenceLock(directory, taskId, agent, async () => {
|
|
1269
|
+
const resolvedEvidenceDir = getEvidenceDir(directory);
|
|
1270
|
+
const evidencePath = path2.join(resolvedEvidenceDir, `${taskId}.json`);
|
|
1271
|
+
let current = null;
|
|
1272
|
+
try {
|
|
1273
|
+
current = readExisting(evidencePath, taskId);
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
telemetry.gateParseError(taskId, error);
|
|
1276
|
+
throw error;
|
|
1277
|
+
}
|
|
1278
|
+
const persist = async (nextEvidence) => {
|
|
1279
|
+
const validated = TaskEvidenceSchema.parse({
|
|
1280
|
+
...nextEvidence,
|
|
1281
|
+
taskId
|
|
1282
|
+
});
|
|
1283
|
+
await atomicWriteFile(evidencePath, JSON.stringify(validated, null, 2));
|
|
1284
|
+
current = validated;
|
|
1285
|
+
return validated;
|
|
1286
|
+
};
|
|
1287
|
+
return callback({
|
|
1288
|
+
taskId,
|
|
1289
|
+
read: () => current,
|
|
1290
|
+
transition: async (event) => {
|
|
1291
|
+
assertTaskEvidenceWriteAllowed(directory, taskId, event);
|
|
1292
|
+
const nextEvidence = updateEvidenceForTransition(current, event);
|
|
1293
|
+
nextEvidence.taskId = taskId;
|
|
1294
|
+
return persist(nextEvidence);
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
async function recordGateEvidence(directory, taskId, gate, sessionId, turbo, options = {}) {
|
|
1300
|
+
assertValidTaskId(taskId);
|
|
1301
|
+
if (shouldTreatGateAsStageB(gate) && typeof options.expectedGeneration !== "number") {
|
|
1302
|
+
throw new Error(`TASK_WORKFLOW_GENERATION_REQUIRED: ${gate} for task ${taskId} must be bound at dispatch`);
|
|
1303
|
+
}
|
|
1304
|
+
await transitionTaskWorkflowEvidence(directory, taskId, shouldTreatGateAsStageB(gate) ? {
|
|
1305
|
+
type: "stage_b_completed",
|
|
1306
|
+
gate,
|
|
1307
|
+
sessionId,
|
|
1308
|
+
turbo,
|
|
1309
|
+
ensureDefaultStageB: options.ensureDefaultStageB,
|
|
1310
|
+
expectedGeneration: options.expectedGeneration,
|
|
1311
|
+
transitionId: options.transitionId
|
|
1312
|
+
} : {
|
|
1313
|
+
type: "gate_recorded",
|
|
1314
|
+
gate,
|
|
1315
|
+
sessionId,
|
|
1316
|
+
turbo,
|
|
1317
|
+
expectedGeneration: options.expectedGeneration,
|
|
1318
|
+
transitionId: options.transitionId
|
|
1319
|
+
});
|
|
1320
|
+
telemetry.gatePassed(sessionId, gate, taskId);
|
|
1321
|
+
}
|
|
1322
|
+
async function recordAgentDispatch(directory, taskId, agentType, turbo, context = {}) {
|
|
1323
|
+
assertValidTaskId(taskId);
|
|
1324
|
+
const expectedGeneration = getTaskWorkflowSnapshot(await readTaskEvidence(directory, taskId)).generation;
|
|
1325
|
+
await transitionTaskWorkflowEvidence(directory, taskId, agentType === "coder" ? {
|
|
1326
|
+
type: "accepted_mutation",
|
|
1327
|
+
agentType,
|
|
1328
|
+
turbo,
|
|
1329
|
+
context,
|
|
1330
|
+
expectedGeneration
|
|
1331
|
+
} : {
|
|
1332
|
+
type: "dispatch_attempted",
|
|
1333
|
+
agentType,
|
|
1334
|
+
turbo,
|
|
1335
|
+
context,
|
|
1336
|
+
expectedGeneration
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
async function readTaskEvidence(directory, taskId) {
|
|
1340
|
+
try {
|
|
1341
|
+
assertValidTaskId(taskId);
|
|
1342
|
+
return readExisting(getEvidencePath(directory, taskId), taskId);
|
|
1343
|
+
} catch {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
function readTaskEvidenceRaw(directory, taskId) {
|
|
1348
|
+
assertValidTaskId(taskId);
|
|
1349
|
+
const evidencePath = getEvidencePath(directory, taskId);
|
|
1350
|
+
try {
|
|
1351
|
+
const raw = readFileSync2(evidencePath, "utf-8");
|
|
1352
|
+
return parseTaskEvidence(raw, taskId);
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
const code = error.code;
|
|
1355
|
+
if (code === "ENOENT" || code === "ENAMETOOLONG")
|
|
1356
|
+
return null;
|
|
1357
|
+
throw error;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
async function hasPassedAllGates(directory, taskId) {
|
|
1361
|
+
const evidence = await readTaskEvidence(directory, taskId);
|
|
1362
|
+
if (!evidence)
|
|
1363
|
+
return false;
|
|
1364
|
+
if (!Array.isArray(evidence.required_gates) || evidence.required_gates.length === 0)
|
|
1365
|
+
return false;
|
|
1366
|
+
return evidence.required_gates.every((gate) => evidence.gates[gate] != null);
|
|
1367
|
+
}
|
|
1368
|
+
function compareTaskWorkflowStateRank(left, right) {
|
|
1369
|
+
return WORKFLOW_STATE_RANK[left] - WORKFLOW_STATE_RANK[right];
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
export { sanitizeTaskId, listRecoveryRecords, _internals, getMergeStrategy, reconcileLandedMerge, postMergeCleanup, pruneStaleWorktreeMetadata, attemptMergeBackFromDirty, cleanupOrphanedBranches, startupOrphanRecovery, readWorkflowWalFile, writeWorkflowWalFile, TASK_WORKFLOW_SCHEMA_MARKER, assertTaskEvidenceWriteAllowed, parseTaskEvidence, DEFAULT_REQUIRED_GATES, isValidTaskId, getTaskWorkflowSnapshot, reduceTaskWorkflowSnapshot, deriveRequiredGates, expandRequiredGates, transitionTaskWorkflowEvidence, withTaskEvidenceTransaction, recordGateEvidence, recordAgentDispatch, readTaskEvidence, readTaskEvidenceRaw, hasPassedAllGates, compareTaskWorkflowStateRank };
|