@glrs-dev/cli 2.4.0 → 2.6.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 (49) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/{chunk-HQUCVJ4G.js → chunk-FBXSGZAA.js} +4 -0
  3. package/dist/chunk-J3FXSHMA.js +263 -0
  4. package/dist/{chunk-5ZVUFNCP.js → chunk-S6N5E2GG.js} +8 -1
  5. package/dist/{chunk-2VMFXAJH.js → chunk-UO7WHIKY.js} +18 -5
  6. package/dist/cli.js +10 -3
  7. package/dist/commands/autopilot-tui.d.ts +11 -1
  8. package/dist/commands/autopilot-tui.js +2 -1
  9. package/dist/commands/autopilot.d.ts +2 -0
  10. package/dist/commands/autopilot.js +62 -21
  11. package/dist/commands/debrief.d.ts +2 -0
  12. package/dist/commands/debrief.js +1 -1
  13. package/dist/commands/loop.d.ts +2 -0
  14. package/dist/commands/loop.js +33 -12
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/node_modules/@glrs-dev/adapter-opencode/dist/index.d.ts +270 -0
  18. package/dist/node_modules/@glrs-dev/adapter-opencode/dist/index.js +506 -0
  19. package/dist/node_modules/@glrs-dev/adapter-opencode/package.json +8 -0
  20. package/dist/node_modules/@glrs-dev/autopilot/dist/auto-ship-EVLBKHUZ.js +7 -0
  21. package/dist/node_modules/@glrs-dev/autopilot/dist/changeset-generator-HAHYSSUR.js +15 -0
  22. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-2X3CWH47.js +3288 -0
  23. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-2ZQ6SBV3.js +70 -0
  24. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-6JZQLIRP.js +781 -0
  25. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-AWRK6S6G.js +91 -0
  26. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-BLEIZHET.js +101 -0
  27. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-GXXCEGDD.js +251 -0
  28. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-S34HOCZ4.js +44 -0
  29. package/dist/node_modules/@glrs-dev/autopilot/dist/index.d.ts +1915 -0
  30. package/dist/node_modules/@glrs-dev/autopilot/dist/index.js +768 -0
  31. package/dist/node_modules/@glrs-dev/autopilot/dist/logger-3XLFMXLN.js +8 -0
  32. package/dist/node_modules/@glrs-dev/autopilot/dist/loop-session-YLCVJGPV.js +9 -0
  33. package/dist/node_modules/@glrs-dev/autopilot/dist/plan-enrichment-4SQYV5FC.js +17 -0
  34. package/dist/node_modules/@glrs-dev/autopilot/package.json +8 -0
  35. package/dist/vendor/harness-opencode/dist/agents/prompts/agents-md-writer.md +1 -1
  36. package/dist/vendor/harness-opencode/dist/agents/prompts/architecture-advisor.md +1 -1
  37. package/dist/vendor/harness-opencode/dist/agents/prompts/code-searcher.md +1 -1
  38. package/dist/vendor/harness-opencode/dist/agents/prompts/docs-maintainer.md +0 -8
  39. package/dist/vendor/harness-opencode/dist/agents/prompts/gap-analyzer.md +1 -3
  40. package/dist/vendor/harness-opencode/dist/agents/prompts/lib-reader.md +1 -1
  41. package/dist/vendor/harness-opencode/dist/agents/prompts/plan-reviewer.md +0 -2
  42. package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +1 -1
  43. package/dist/vendor/harness-opencode/dist/agents/prompts/prime.md +78 -262
  44. package/dist/vendor/harness-opencode/dist/agents/prompts/research.md +5 -14
  45. package/dist/vendor/harness-opencode/dist/agents/prompts/scoper.md +7 -2
  46. package/dist/vendor/harness-opencode/dist/autopilot/strategies/default.md +29 -0
  47. package/dist/vendor/harness-opencode/dist/index.js +112 -82
  48. package/dist/vendor/harness-opencode/package.json +1 -1
  49. package/package.json +9 -7
@@ -0,0 +1,3288 @@
1
+ import {
2
+ resolveModel
3
+ } from "./chunk-S34HOCZ4.js";
4
+ import {
5
+ childLogger,
6
+ createAutopilotLogger
7
+ } from "./chunk-2ZQ6SBV3.js";
8
+ import {
9
+ detectSpecPhases,
10
+ filterUncheckedSpecPhases,
11
+ hasSpec,
12
+ parseSpecItems,
13
+ parseSpecState,
14
+ readSpecConstraints,
15
+ readSpecGoal,
16
+ validateMainSpec,
17
+ validatePhaseSpec
18
+ } from "./chunk-GXXCEGDD.js";
19
+
20
+ // src/loop-session.ts
21
+ import * as fs7 from "fs";
22
+ import * as path7 from "path";
23
+ import pino from "pino";
24
+
25
+ // src/loop.ts
26
+ import { execFile as execFileCb3 } from "child_process";
27
+ import { promisify as promisify3 } from "util";
28
+ import { readFileSync as readFileSync3 } from "fs";
29
+ import { join as join4 } from "path";
30
+
31
+ // src/status.ts
32
+ import * as fs from "fs/promises";
33
+ import { mkdirSync, existsSync } from "fs";
34
+ import { dirname } from "path";
35
+ function formatElapsed(ms) {
36
+ const totalSeconds = Math.floor(ms / 1e3);
37
+ const hours = Math.floor(totalSeconds / 3600);
38
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
39
+ const seconds = totalSeconds % 60;
40
+ if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
41
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
42
+ return `${seconds}s`;
43
+ }
44
+ function formatCost(usd, estimated) {
45
+ if (usd === 0) return "pending";
46
+ const base = `$${usd.toFixed(3)}`;
47
+ return estimated ? `~${base} est` : base;
48
+ }
49
+ function composeStatusMessage(state, now) {
50
+ const elapsed = formatElapsed(now - state.startedAt);
51
+ const cost = formatCost(state.cumulativeCostUsd, state.costIsEstimated);
52
+ const iterNote = state.iterationsCompleted === 0 ? "iteration 1 in flight" : `${state.iterationsCompleted} iteration${state.iterationsCompleted === 1 ? "" : "s"} complete`;
53
+ let planNote = "";
54
+ if (state.phaseCount !== void 0 && state.phasesCompleted !== void 0 && state.mainCheckboxesTotal !== void 0 && state.mainCheckboxesCompleted !== void 0) {
55
+ planNote = `, phase ${state.phasesCompleted}/${state.phaseCount}, ${state.mainCheckboxesCompleted}/${state.mainCheckboxesTotal} boxes`;
56
+ }
57
+ let laneNote = "";
58
+ if (state.lanes && Object.keys(state.lanes).length > 0) {
59
+ const laneIds = Object.keys(state.lanes).sort();
60
+ const parts = laneIds.map((id) => {
61
+ const l = state.lanes[id];
62
+ const tool = l.lastTool ? ` ${l.lastTool}` : "";
63
+ return `${id}: ${l.phaseFile} iter ${l.iteration}${tool}`;
64
+ });
65
+ laneNote = `, lanes: [${parts.join(", ")}]`;
66
+ }
67
+ if (state.lastIterationErrored) {
68
+ return `working (${iterNote}, ${elapsed} elapsed, ${cost} used${planNote}${laneNote}) \u2014 last iteration errored`;
69
+ }
70
+ return `working (${iterNote}, ${elapsed} elapsed, ${cost} used${planNote}${laneNote})`;
71
+ }
72
+ async function writeStatusFile(filePath, state, nowMs) {
73
+ const snapshot = {
74
+ ...state,
75
+ elapsedMs: nowMs - state.startedAt,
76
+ writtenAt: new Date(nowMs).toISOString()
77
+ };
78
+ const json = JSON.stringify(snapshot, null, 2) + "\n";
79
+ const tmp = `${filePath}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 10)}`;
80
+ const parent = dirname(filePath);
81
+ if (!existsSync(parent)) {
82
+ mkdirSync(parent, { recursive: true });
83
+ }
84
+ await fs.writeFile(tmp, json, "utf8");
85
+ await fs.rename(tmp, filePath);
86
+ }
87
+ function createStatusHeartbeat(opts) {
88
+ const now = opts._deps?.now ?? (() => Date.now());
89
+ const setIntervalFn = opts._deps?.setInterval ?? setInterval;
90
+ const clearIntervalFn = opts._deps?.clearInterval ?? clearInterval;
91
+ const state = {
92
+ startedAt: now(),
93
+ iterationsCompleted: 0,
94
+ cumulativeCostUsd: 0,
95
+ lastIterationProgress: false,
96
+ lastIterationErrored: false
97
+ };
98
+ let timerId = null;
99
+ const tick = () => {
100
+ if (opts.pollCost) {
101
+ opts.pollCost().then((cost) => {
102
+ if (cost > 0) state.cumulativeCostUsd = cost;
103
+ }).catch(() => {
104
+ });
105
+ }
106
+ const message = composeStatusMessage(state, now());
107
+ opts.logger.info(
108
+ {
109
+ elapsedMs: now() - state.startedAt,
110
+ iterationsCompleted: state.iterationsCompleted,
111
+ cumulativeCostUsd: state.cumulativeCostUsd,
112
+ lastIterationProgress: state.lastIterationProgress,
113
+ lastIterationErrored: state.lastIterationErrored
114
+ },
115
+ message
116
+ );
117
+ if (opts.statusFilePath) {
118
+ writeStatusFile(opts.statusFilePath, state, now()).catch(() => {
119
+ });
120
+ }
121
+ };
122
+ return {
123
+ start() {
124
+ if (timerId !== null) return;
125
+ timerId = setIntervalFn(tick, opts.intervalMs);
126
+ },
127
+ stop() {
128
+ if (timerId === null) return;
129
+ clearIntervalFn(timerId);
130
+ timerId = null;
131
+ },
132
+ update(patch) {
133
+ Object.assign(state, patch);
134
+ },
135
+ getState() {
136
+ return { ...state };
137
+ }
138
+ };
139
+ }
140
+
141
+ // src/plan-parser.ts
142
+ import * as fs2 from "fs";
143
+ import * as path from "path";
144
+ var DEGRADED = {
145
+ type: "single",
146
+ totalItems: 0,
147
+ checkedItems: 0,
148
+ phaseCount: 0,
149
+ phasesCompleted: 0,
150
+ phases: []
151
+ };
152
+ function countCheckboxes(content) {
153
+ let total = 0;
154
+ let checked = 0;
155
+ const checkboxRe = /^[ \t]*-\s+\[([ xX])\]/gm;
156
+ let match;
157
+ while ((match = checkboxRe.exec(content)) !== null) {
158
+ total++;
159
+ if (match[1] !== " ") {
160
+ checked++;
161
+ }
162
+ }
163
+ return { total, checked };
164
+ }
165
+ function parseSingleFile(filePath) {
166
+ const content = fs2.readFileSync(filePath, "utf8");
167
+ return countCheckboxes(content);
168
+ }
169
+ function detectPhaseFiles(dir) {
170
+ const entries = fs2.readdirSync(dir);
171
+ return entries.filter((f) => f.endsWith(".md") && f !== "main.md" && f !== "scope.md" && f !== "scope-seed.md").sort((a, b) => {
172
+ const na = parseInt(a.replace(/[^0-9]/g, ""), 10);
173
+ const nb = parseInt(b.replace(/[^0-9]/g, ""), 10);
174
+ return na - nb;
175
+ });
176
+ }
177
+ function parseMultiFile(dir) {
178
+ const mainPath = path.join(dir, "main.md");
179
+ const mainContent = fs2.readFileSync(mainPath, "utf8");
180
+ const mainCounts = countCheckboxes(mainContent);
181
+ const phaseFiles = detectPhaseFiles(dir);
182
+ const phases = [];
183
+ let phasesCompleted = 0;
184
+ for (const phaseFile of phaseFiles) {
185
+ const phasePath = path.join(dir, phaseFile);
186
+ const { total, checked } = parseSingleFile(phasePath);
187
+ phases.push({ file: phaseFile, totalItems: total, checkedItems: checked });
188
+ if (total > 0 && checked === total) {
189
+ phasesCompleted++;
190
+ }
191
+ }
192
+ return {
193
+ type: "multi",
194
+ totalItems: mainCounts.total,
195
+ checkedItems: mainCounts.checked,
196
+ phaseCount: phaseFiles.length,
197
+ phasesCompleted,
198
+ phases
199
+ };
200
+ }
201
+ function parsePlanState(planPath) {
202
+ try {
203
+ const stat = fs2.statSync(planPath);
204
+ if (stat.isDirectory() && hasSpec(planPath)) {
205
+ return parseSpecState(planPath);
206
+ }
207
+ if (stat.isDirectory()) {
208
+ const mainPath = path.join(planPath, "main.md");
209
+ if (fs2.existsSync(mainPath)) {
210
+ return parseMultiFile(planPath);
211
+ }
212
+ return { ...DEGRADED, type: "multi" };
213
+ }
214
+ const { total, checked } = parseSingleFile(planPath);
215
+ return {
216
+ type: "single",
217
+ totalItems: total,
218
+ checkedItems: checked,
219
+ phaseCount: 0,
220
+ phasesCompleted: 0,
221
+ phases: []
222
+ };
223
+ } catch {
224
+ return { ...DEGRADED };
225
+ }
226
+ }
227
+ function extractPlanStateFence(content) {
228
+ const fenceRe = /^```plan-state\r?\n([\s\S]*?)^```/m;
229
+ const match = fenceRe.exec(content);
230
+ return match ? match[1] : null;
231
+ }
232
+ function parseItems(content) {
233
+ try {
234
+ const fence = extractPlanStateFence(content);
235
+ if (!fence) return [];
236
+ const items = [];
237
+ const itemBlocks = fence.split(/(?=^- \[[ xX]\])/m).filter((b) => b.trim());
238
+ for (const block of itemBlocks) {
239
+ const lines = block.split("\n");
240
+ const firstLine = lines[0];
241
+ const headerMatch = /^- \[([ xX])\]\s+id:\s*(.+)$/.exec(firstLine.trim());
242
+ if (!headerMatch) continue;
243
+ const checked = headerMatch[1] !== " ";
244
+ const id = headerMatch[2].trim();
245
+ let intent = "";
246
+ let verify = "";
247
+ const files = [];
248
+ const tests = [];
249
+ let i = 1;
250
+ while (i < lines.length) {
251
+ const line = lines[i];
252
+ const trimmed = line.trim();
253
+ if (trimmed.startsWith("intent:")) {
254
+ intent = trimmed.slice("intent:".length).trim();
255
+ i++;
256
+ } else if (trimmed === "files:") {
257
+ i++;
258
+ while (i < lines.length) {
259
+ const fileLine = lines[i];
260
+ const fileMatch = /^\s{4}-\s+(.+)$/.exec(fileLine);
261
+ if (!fileMatch) break;
262
+ const rawPath = fileMatch[1].trim();
263
+ const isNew = rawPath.includes("(NEW)");
264
+ const cleanPath = rawPath.replace(/\s*\(NEW\)\s*/, "").trim();
265
+ i++;
266
+ let change = "";
267
+ if (i < lines.length) {
268
+ const changeLine = lines[i];
269
+ const changeMatch = /^\s{6}Change:\s*(.+)$/.exec(changeLine);
270
+ if (changeMatch) {
271
+ change = changeMatch[1].trim();
272
+ i++;
273
+ }
274
+ }
275
+ files.push({ path: cleanPath, isNew, change });
276
+ }
277
+ } else if (trimmed === "tests:") {
278
+ i++;
279
+ while (i < lines.length) {
280
+ const testLine = lines[i];
281
+ const testMatch = /^\s{4}-\s+(.+)$/.exec(testLine);
282
+ if (!testMatch) break;
283
+ tests.push(testMatch[1].trim());
284
+ i++;
285
+ }
286
+ } else if (trimmed.startsWith("verify:")) {
287
+ verify = trimmed.slice("verify:".length).trim();
288
+ i++;
289
+ } else {
290
+ i++;
291
+ }
292
+ }
293
+ items.push({ id, intent, files, tests, verify, checked });
294
+ }
295
+ return items;
296
+ } catch {
297
+ return [];
298
+ }
299
+ }
300
+
301
+ // src/scope-validator.ts
302
+ import { execFile as execFileCb } from "child_process";
303
+ import { promisify } from "util";
304
+ var execFileDefault = promisify(execFileCb);
305
+ function validateScope(expected, actual) {
306
+ const expectedSet = new Set(expected.filter((p) => p && p.trim()));
307
+ const actualSet = new Set(actual.filter((p) => p && p.trim()));
308
+ const extra = [];
309
+ for (const f of actualSet) {
310
+ if (!expectedSet.has(f)) extra.push(f);
311
+ }
312
+ const missing = [];
313
+ for (const f of expectedSet) {
314
+ if (!actualSet.has(f)) missing.push(f);
315
+ }
316
+ extra.sort();
317
+ missing.sort();
318
+ return { extra, missing };
319
+ }
320
+ async function getChangedFiles(cwd, baseRef, opts = {}) {
321
+ const execFile4 = opts._deps?.execFile ?? execFileDefault;
322
+ try {
323
+ const { stdout } = await execFile4(
324
+ "git",
325
+ ["diff", "--name-only", baseRef],
326
+ { cwd }
327
+ );
328
+ const text = typeof stdout === "string" ? stdout : String(stdout ?? "");
329
+ return text.trim().split("\n").map((s) => s.trim()).filter(Boolean);
330
+ } catch {
331
+ return [];
332
+ }
333
+ }
334
+
335
+ // src/config.ts
336
+ var MAX_ITERATIONS = 50;
337
+ var STRUGGLE_THRESHOLD = 3;
338
+ var TIMEOUT_MS = 4 * 60 * 60 * 1e3;
339
+ var STALL_MS_BY_TIER = {
340
+ deep: 30 * 60 * 1e3,
341
+ mid: 15 * 60 * 1e3,
342
+ "mid-execute": 10 * 60 * 1e3,
343
+ "autopilot-execute": 10 * 60 * 1e3,
344
+ fast: 5 * 60 * 1e3
345
+ };
346
+ var STALL_MS = STALL_MS_BY_TIER.deep;
347
+ var MAX_ITERATIONS_PER_PHASE_BY_TIER = {
348
+ deep: 5,
349
+ mid: 8,
350
+ "mid-execute": 10,
351
+ "autopilot-execute": 10,
352
+ fast: 10
353
+ };
354
+ var MAX_ITERATIONS_PER_ITEM = 5;
355
+ var KILL_SWITCH_PATH = ".agent/autopilot-disable";
356
+ var SENTINEL_TAG = "<autopilot-done>";
357
+ var STATUS_INTERVAL_MS = (() => {
358
+ const env = process.env["GLRS_AUTOPILOT_STATUS_INTERVAL_MS"];
359
+ if (!env) return 5 * 60 * 1e3;
360
+ const n = Number.parseInt(env, 10);
361
+ if (Number.isNaN(n) || n < 1e3 || n > 60 * 60 * 1e3) return 5 * 60 * 1e3;
362
+ return n;
363
+ })();
364
+
365
+ // src/sentinel.ts
366
+ function detectSentinel(text) {
367
+ if (!text.includes(SENTINEL_TAG)) {
368
+ return false;
369
+ }
370
+ const withoutFences = text.replace(/```[\s\S]*?```/g, "");
371
+ const withoutInline = withoutFences.replace(/`[^`\n]*`/g, "");
372
+ return withoutInline.includes(SENTINEL_TAG);
373
+ }
374
+
375
+ // src/struggle.ts
376
+ import * as fs3 from "fs";
377
+ import * as path2 from "path";
378
+ var StruggleDetector = class {
379
+ _consecutiveStalls = 0;
380
+ _threshold;
381
+ constructor(threshold) {
382
+ this._threshold = threshold;
383
+ }
384
+ /** Number of consecutive stall iterations recorded so far. */
385
+ get consecutiveStalls() {
386
+ return this._consecutiveStalls;
387
+ }
388
+ /**
389
+ * Record the result of one iteration.
390
+ * @param madeProgress - true if the agent made filesystem changes this iteration.
391
+ */
392
+ record(madeProgress) {
393
+ if (madeProgress) {
394
+ this._consecutiveStalls = 0;
395
+ } else {
396
+ this._consecutiveStalls++;
397
+ }
398
+ }
399
+ /**
400
+ * Returns true if the agent has stalled for `threshold` consecutive
401
+ * iterations without making progress.
402
+ */
403
+ isStruggling() {
404
+ return this._consecutiveStalls >= this._threshold;
405
+ }
406
+ };
407
+ function checkKillSwitch(cwd) {
408
+ const killSwitchFile = path2.join(cwd, KILL_SWITCH_PATH);
409
+ return fs3.existsSync(killSwitchFile);
410
+ }
411
+
412
+ // src/lib/slack-formatter.ts
413
+ var EVENT_LABELS = {
414
+ iteration_complete: "\u2705 Iteration Complete",
415
+ phase_complete: "\u{1F3C1} Phase Complete",
416
+ run_complete: "\u{1F389} Run Complete",
417
+ error: "\u274C Error",
418
+ struggle: "\u26A0\uFE0F Struggle Detected",
419
+ stall: "\u23F8\uFE0F Stall Detected"
420
+ };
421
+ function formatSlackMessage(event) {
422
+ const label = EVENT_LABELS[event.event] ?? event.event;
423
+ const fields = [
424
+ { type: "mrkdwn", text: `*Iteration:* ${event.iteration}` }
425
+ ];
426
+ if (event.costUsd !== void 0 && event.costUsd > 0) {
427
+ fields.push({ type: "mrkdwn", text: `*Cost so far:* $${event.costUsd.toFixed(3)}` });
428
+ }
429
+ if (event.filesChanged !== void 0 && event.filesChanged > 0) {
430
+ fields.push({ type: "mrkdwn", text: `*Files changed:* ${event.filesChanged}` });
431
+ }
432
+ if (event.phaseFile) {
433
+ fields.push({ type: "mrkdwn", text: `*Phase:* ${event.phaseFile}` });
434
+ }
435
+ if (event.commitSubject) {
436
+ fields.push({ type: "mrkdwn", text: `*Last commit:* ${event.commitSubject}` });
437
+ }
438
+ if (event.errorMessage) {
439
+ fields.push({ type: "mrkdwn", text: `*Error:* ${event.errorMessage}` });
440
+ }
441
+ const blocks = [
442
+ {
443
+ type: "header",
444
+ text: { type: "plain_text", text: label, emoji: true }
445
+ }
446
+ ];
447
+ if (fields.length > 0) {
448
+ blocks.push({
449
+ type: "section",
450
+ fields
451
+ });
452
+ }
453
+ blocks.push({
454
+ type: "context",
455
+ elements: [
456
+ {
457
+ type: "mrkdwn",
458
+ text: `${event.message} \xB7 ${event.timestamp}`
459
+ }
460
+ ]
461
+ });
462
+ return { blocks };
463
+ }
464
+
465
+ // src/lib/webhook-notifier.ts
466
+ function isSlackWebhookUrl(url) {
467
+ return url.includes("hooks.slack.com/");
468
+ }
469
+ async function notifyWebhook(url, event, allowedEvents) {
470
+ if (allowedEvents && !allowedEvents.includes(event.event)) {
471
+ return;
472
+ }
473
+ try {
474
+ const body = isSlackWebhookUrl(url) ? JSON.stringify(formatSlackMessage(event)) : JSON.stringify(event);
475
+ const response = await fetch(url, {
476
+ method: "POST",
477
+ headers: { "Content-Type": "application/json" },
478
+ body
479
+ });
480
+ if (!response.ok) {
481
+ console.warn(
482
+ `\u26A0 Webhook POST to ${url} returned ${response.status} ${response.statusText}`
483
+ );
484
+ }
485
+ } catch (err) {
486
+ const msg = err instanceof Error ? err.message : String(err);
487
+ console.warn(`\u26A0 Webhook notification failed (non-fatal): ${msg}`);
488
+ }
489
+ }
490
+
491
+ // src/lib/model-pricing.ts
492
+ var MODEL_PRICING = {
493
+ // Claude 3 Opus
494
+ "claude-3-opus": { input: 15, output: 75 },
495
+ "claude-opus": { input: 15, output: 75 },
496
+ // Claude 3.5 Sonnet (more specific than "sonnet" — check first)
497
+ "claude-3-5-sonnet": { input: 3, output: 15 },
498
+ "claude-3.5-sonnet": { input: 3, output: 15 },
499
+ // Claude 3.7 Sonnet
500
+ "claude-3-7-sonnet": { input: 3, output: 15 },
501
+ "claude-3.7-sonnet": { input: 3, output: 15 },
502
+ // Claude 3 Sonnet (generic)
503
+ "claude-3-sonnet": { input: 3, output: 15 },
504
+ "claude-sonnet": { input: 3, output: 15 },
505
+ // Claude 3.5 Haiku (more specific than "haiku" — check first)
506
+ "claude-3-5-haiku": { input: 0.8, output: 4 },
507
+ "claude-3.5-haiku": { input: 0.8, output: 4 },
508
+ // Claude 3 Haiku (generic)
509
+ "claude-3-haiku": { input: 0.25, output: 1.25 },
510
+ "claude-haiku": { input: 0.25, output: 1.25 },
511
+ // Amazon Nova models (approximate)
512
+ "amazon.nova-pro": { input: 0.8, output: 3.2 },
513
+ "amazon.nova-lite": { input: 0.06, output: 0.24 },
514
+ "amazon.nova-micro": { input: 0.035, output: 0.14 }
515
+ // GLM-5 and Kimi: TBD — return 0 via unknown-model fallback
516
+ };
517
+ function estimateCost(modelId, tokens) {
518
+ const pricing = findPricing(modelId);
519
+ if (!pricing) return 0;
520
+ return (tokens.input * pricing.input + tokens.output * pricing.output) / 1e6;
521
+ }
522
+ function findPricing(modelId) {
523
+ const lower = modelId.toLowerCase();
524
+ for (const [key, pricing] of Object.entries(MODEL_PRICING)) {
525
+ if (lower.includes(key.toLowerCase())) {
526
+ return pricing;
527
+ }
528
+ }
529
+ return void 0;
530
+ }
531
+
532
+ // src/lib/error-classifier.ts
533
+ var TRANSIENT_PATTERNS = [
534
+ // Network
535
+ "etimedout",
536
+ "econnreset",
537
+ "eai_again",
538
+ "socket hang up",
539
+ "network",
540
+ "fetch failed",
541
+ // Rate limits
542
+ "429",
543
+ "too many requests",
544
+ "rate limit",
545
+ "throttling",
546
+ "throttled",
547
+ // Server-side
548
+ "500",
549
+ "502",
550
+ "503",
551
+ "504",
552
+ "internal server error",
553
+ "service unavailable",
554
+ "bad gateway",
555
+ "gateway timeout"
556
+ ];
557
+ var CREDENTIAL_EXPIRED_PATTERNS = [
558
+ /expiredtoken/i,
559
+ /invalididentitytoken/i,
560
+ /tokenrefreshrequired/i,
561
+ /expired.*token/i,
562
+ /token.*expired/i,
563
+ /credentials?.*expired/i,
564
+ /expired.*credentials?/i,
565
+ /sso.*expired/i,
566
+ /session.*expired/i
567
+ ];
568
+ function classifyError(message) {
569
+ if (typeof message !== "string" || message.length === 0) {
570
+ return "permanent";
571
+ }
572
+ const lower = message.toLowerCase();
573
+ for (const re of CREDENTIAL_EXPIRED_PATTERNS) {
574
+ if (re.test(message)) {
575
+ return "credential-expired";
576
+ }
577
+ }
578
+ for (const pattern of TRANSIENT_PATTERNS) {
579
+ if (lower.includes(pattern)) {
580
+ return "transient";
581
+ }
582
+ }
583
+ return "permanent";
584
+ }
585
+
586
+ // src/lib/credential-refresh.ts
587
+ import { execFile as execFileCb2 } from "child_process";
588
+ import { promisify as promisify2 } from "util";
589
+ var execFile = promisify2(execFileCb2);
590
+ function detectProvider(modelName) {
591
+ if (typeof modelName !== "string" || modelName.length === 0) {
592
+ return "unknown";
593
+ }
594
+ const lower = modelName.toLowerCase();
595
+ if (lower.startsWith("bedrock/") || lower.startsWith("amazon-bedrock/") || lower.startsWith("aws/")) {
596
+ return "aws";
597
+ }
598
+ if (lower.startsWith("azure/") || lower.includes(".azure.")) {
599
+ return "azure";
600
+ }
601
+ return "unknown";
602
+ }
603
+
604
+ // src/checkpoint.ts
605
+ import * as fs4 from "fs";
606
+ import * as path3 from "path";
607
+ var CHECKPOINT_REL_PATH = path3.join(".agent", "autopilot-checkpoint.json");
608
+ function checkpointPath(cwd) {
609
+ return path3.join(cwd, CHECKPOINT_REL_PATH);
610
+ }
611
+ function writeCheckpoint(cwd, state, deps) {
612
+ const _writeFileSync = deps?.writeFileSync ?? ((p, content) => fs4.writeFileSync(p, content, "utf8"));
613
+ const _renameSync = deps?.renameSync ?? fs4.renameSync;
614
+ const _mkdirSync = deps?.mkdirSync ?? ((p, opts) => fs4.mkdirSync(p, opts));
615
+ const target = checkpointPath(cwd);
616
+ const dir = path3.dirname(target);
617
+ try {
618
+ _mkdirSync(dir, { recursive: true });
619
+ } catch {
620
+ return;
621
+ }
622
+ const tmp = `${target}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 10)}`;
623
+ try {
624
+ _writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
625
+ _renameSync(tmp, target);
626
+ } catch {
627
+ try {
628
+ fs4.unlinkSync(tmp);
629
+ } catch {
630
+ }
631
+ }
632
+ }
633
+ function readCheckpoint(cwd, deps) {
634
+ const _readFileSync = deps?.readFileSync ?? ((p) => fs4.readFileSync(p, "utf8"));
635
+ const target = checkpointPath(cwd);
636
+ let raw;
637
+ try {
638
+ raw = _readFileSync(target);
639
+ } catch {
640
+ return null;
641
+ }
642
+ let parsed;
643
+ try {
644
+ parsed = JSON.parse(raw);
645
+ } catch {
646
+ return null;
647
+ }
648
+ if (!parsed || typeof parsed !== "object") return null;
649
+ const obj = parsed;
650
+ if (typeof obj["planPath"] !== "string") return null;
651
+ if (!Array.isArray(obj["completedPhases"])) return null;
652
+ if (!obj["completedPhases"].every((p) => typeof p === "string")) return null;
653
+ if (typeof obj["totalCostUsd"] !== "number") return null;
654
+ if (typeof obj["totalIterations"] !== "number") return null;
655
+ if (typeof obj["timestamp"] !== "string") return null;
656
+ return {
657
+ planPath: obj["planPath"],
658
+ completedPhases: obj["completedPhases"],
659
+ totalCostUsd: obj["totalCostUsd"],
660
+ totalIterations: obj["totalIterations"],
661
+ timestamp: obj["timestamp"]
662
+ };
663
+ }
664
+ function deleteCheckpoint(cwd, deps) {
665
+ const _unlinkSync = deps?.unlinkSync ?? fs4.unlinkSync;
666
+ const _existsSync = deps?.existsSync ?? fs4.existsSync;
667
+ const target = checkpointPath(cwd);
668
+ try {
669
+ if (_existsSync(target)) {
670
+ _unlinkSync(target);
671
+ }
672
+ } catch {
673
+ }
674
+ }
675
+
676
+ // src/loop.ts
677
+ var TRANSIENT_RETRY_MAX_ATTEMPTS = 3;
678
+ var TRANSIENT_RETRY_BASE_MS = 1e3;
679
+ var TRANSIENT_RETRY_MAX_MS = 3e4;
680
+ var execFile2 = promisify3(execFileCb3);
681
+ function buildFullPrompt(userPrompt) {
682
+ const candidates = [
683
+ join4(import.meta.dir, "prompt-template.md"),
684
+ join4(import.meta.dir, "..", "..", "src", "autopilot", "prompt-template.md")
685
+ ];
686
+ let template = "";
687
+ for (const candidate of candidates) {
688
+ try {
689
+ const raw = readFileSync3(candidate, "utf8");
690
+ template = raw.replace(/^---\n[\s\S]*?\n---\n/, "");
691
+ break;
692
+ } catch {
693
+ }
694
+ }
695
+ const withArgs = template.replace("$ARGUMENTS", userPrompt);
696
+ return withArgs || userPrompt;
697
+ }
698
+ async function checkProgress(cwd, baseRef) {
699
+ try {
700
+ const { stdout } = await execFile2("git", ["diff", "--stat", baseRef], { cwd });
701
+ return stdout.trim().length > 0;
702
+ } catch {
703
+ return true;
704
+ }
705
+ }
706
+ async function captureWorkingTreeSnapshot(cwd) {
707
+ try {
708
+ const { stdout } = await execFile2("git", ["status", "--porcelain"], { cwd });
709
+ return stdout.trim();
710
+ } catch {
711
+ return "";
712
+ }
713
+ }
714
+ async function getHeadSha(cwd) {
715
+ try {
716
+ const { stdout } = await execFile2("git", ["rev-parse", "HEAD"], { cwd });
717
+ return stdout.trim();
718
+ } catch {
719
+ return "HEAD";
720
+ }
721
+ }
722
+ async function amendCommitWithPrefix(cwd, prefix) {
723
+ try {
724
+ const { stdout: subject } = await execFile2("git", ["log", "-1", "--pretty=%s"], { cwd });
725
+ const currentSubject = subject.trim();
726
+ if (!currentSubject || currentSubject.startsWith(prefix)) {
727
+ return false;
728
+ }
729
+ const newSubject = `${prefix} ${currentSubject}`;
730
+ await execFile2("git", ["commit", "--amend", "-m", newSubject], { cwd });
731
+ return true;
732
+ } catch {
733
+ return false;
734
+ }
735
+ }
736
+ async function runRalphLoop(opts) {
737
+ process.env["GLRS_AUTOPILOT_HEADLESS"] = "1";
738
+ const maxIterations = opts.maxIterations ?? MAX_ITERATIONS;
739
+ const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
740
+ const tier = opts.agentName === "autopilot-fast" ? "autopilot-execute" : "deep";
741
+ const cfgObj = opts.config;
742
+ const cfgStallMs = cfgObj?.stall_timeout;
743
+ const stallMs = opts.stallMs ?? cfgStallMs ?? STALL_MS_BY_TIER[tier];
744
+ const struggleThreshold = opts.struggleThreshold ?? STRUGGLE_THRESHOLD;
745
+ const autoCommit = cfgObj?.auto_commit ?? true;
746
+ const commitPrefix = cfgObj?.commit_prefix;
747
+ const cfgNotifyUrl = cfgObj?.notify_url;
748
+ const cfgNotifyEvents = cfgObj?.notify_events;
749
+ const resolvedNotifyUrl = opts.notifyUrl ?? cfgNotifyUrl;
750
+ const resolvedNotifyEvents = cfgNotifyEvents;
751
+ const finalNotifyEvents = opts.notifyEvents ?? resolvedNotifyEvents;
752
+ if (!opts.adapter) {
753
+ throw new Error("runRalphLoop: adapter is required");
754
+ }
755
+ const adapter = opts.adapter;
756
+ const fullPrompt = buildFullPrompt(opts.prompt);
757
+ const struggle = new StruggleDetector(struggleThreshold);
758
+ const startTime = Date.now();
759
+ const notify = (event) => {
760
+ if (resolvedNotifyUrl) {
761
+ notifyWebhook(resolvedNotifyUrl, event, finalNotifyEvents).catch(() => {
762
+ });
763
+ }
764
+ };
765
+ const autopilotLog = opts.logger ?? createAutopilotLogger({ cwd: opts.cwd });
766
+ const laneSuffix = opts.laneId ? `.lane.${opts.laneId}` : "";
767
+ const log = childLogger(autopilotLog.root, `autopilot.loop${laneSuffix}`);
768
+ const toolLog = childLogger(autopilotLog.root, `autopilot.tool${laneSuffix}`);
769
+ const streamLog = childLogger(autopilotLog.root, `autopilot.stream${laneSuffix}`);
770
+ const statusLog = childLogger(autopilotLog.root, `autopilot.status${laneSuffix}`);
771
+ const lanePrefix = opts.laneId ? `[${opts.laneId}] ` : "";
772
+ const emitter = opts.emitter;
773
+ let thinkingChars = 0;
774
+ let thinkingStartTime = 0;
775
+ let thinkingToolCalls = 0;
776
+ let modelName = "unknown";
777
+ try {
778
+ const configHome = process.env["XDG_CONFIG_HOME"] ?? join4(process.env["HOME"] ?? "", ".config");
779
+ const configPath = join4(configHome, "opencode", "opencode.json");
780
+ const raw = readFileSync3(configPath, "utf8");
781
+ const config = JSON.parse(raw);
782
+ const plugins = Array.isArray(config.plugin) ? config.plugin : [];
783
+ for (const entry of plugins) {
784
+ if (Array.isArray(entry) && entry.length >= 2) {
785
+ const opts2 = entry[1];
786
+ const models = opts2?.models;
787
+ if (models) {
788
+ const modelArr = models[tier] ?? (tier === "autopilot-execute" ? models["mid-execute"] ?? models["mid"] : models["deep"]);
789
+ if (Array.isArray(modelArr) && modelArr[0]) {
790
+ modelName = modelArr[0];
791
+ }
792
+ }
793
+ }
794
+ }
795
+ } catch {
796
+ }
797
+ const agentLabel = opts.agentName === "autopilot-fast" ? "fast executor" : "deep";
798
+ log.info(`${lanePrefix}Autopilot loop \u2014 ${agentLabel} (${modelName})`);
799
+ log.info(`${lanePrefix}Prompt: ${opts.prompt.length > 80 ? opts.prompt.slice(0, 80) + "\u2026" : opts.prompt}`);
800
+ log.info(`${lanePrefix}Max iterations: ${maxIterations}, timeout: ${(timeoutMs / 36e5).toFixed(1)}h`);
801
+ let heartbeat = null;
802
+ if (autopilotLog.logFilePath) {
803
+ log.info({ file: autopilotLog.logFilePath }, `Logging to ${autopilotLog.logFilePath}`);
804
+ }
805
+ log.info({ cwd: opts.cwd, maxIterations, timeoutMs }, `Starting agent (${adapter.name})`);
806
+ const handle = await adapter.start({ cwd: opts.cwd, agents: opts.agentOverrides });
807
+ log.info({ agentId: handle.id }, "Agent ready");
808
+ const abort = new AbortController();
809
+ const timeoutHandle = setTimeout(() => {
810
+ abort.abort();
811
+ }, timeoutMs);
812
+ let signalCount = 0;
813
+ let interruptedSignal;
814
+ const onSignal = (signal) => {
815
+ signalCount++;
816
+ if (signalCount === 1) {
817
+ interruptedSignal = signal;
818
+ log.warn({ signal }, `Signal ${signal} received \u2014 graceful shutdown (commit WIP + write checkpoint)`);
819
+ abort.abort();
820
+ } else {
821
+ log.error({ signal, signalCount }, "Second signal \u2014 force exit");
822
+ process.exit(130);
823
+ }
824
+ };
825
+ const sigintHandler = () => onSignal("SIGINT");
826
+ const sigtermHandler = () => onSignal("SIGTERM");
827
+ process.on("SIGINT", sigintHandler);
828
+ process.on("SIGTERM", sigtermHandler);
829
+ let sessionId;
830
+ let handleTransferred = false;
831
+ const transferHandle = () => {
832
+ if (opts.keepAlive) {
833
+ handleTransferred = true;
834
+ return { adapter, handle };
835
+ }
836
+ return void 0;
837
+ };
838
+ try {
839
+ const agentName = opts.agentName ?? "autopilot-prime";
840
+ const tierLabel = agentName === "autopilot-fast" ? "autopilot-execute tier" : "deep tier";
841
+ sessionId = await adapter.createSession(handle, { agentName, model: opts.model });
842
+ log.info({ sessionId, agentName, tier: tierLabel }, `Session created with ${agentName} (${tierLabel})`);
843
+ const statusFileEnabled = opts.config?.status_file !== false;
844
+ heartbeat = createStatusHeartbeat({
845
+ logger: statusLog,
846
+ intervalMs: STATUS_INTERVAL_MS,
847
+ pollCost: async () => adapter.getSessionCost(handle, sessionId),
848
+ statusFilePath: statusFileEnabled ? join4(opts.cwd, ".agent", "autopilot-status.json") : void 0
849
+ });
850
+ heartbeat.start();
851
+ for (let iteration = 1; iteration <= maxIterations; iteration++) {
852
+ if (checkKillSwitch(opts.cwd)) {
853
+ log.warn({ iteration: iteration - 1 }, "Kill switch active \u2014 stopping");
854
+ notify({
855
+ event: "run_complete",
856
+ iteration: iteration - 1,
857
+ costUsd: heartbeat?.getState().cumulativeCostUsd,
858
+ message: `Kill switch active (.agent/autopilot-disable exists). Stopping after ${iteration - 1} iteration(s).`,
859
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
860
+ });
861
+ return {
862
+ exitReason: "kill-switch",
863
+ iterations: iteration - 1,
864
+ message: `Kill switch active (.agent/autopilot-disable exists). Stopping after ${iteration - 1} iteration(s).`,
865
+ sessionId,
866
+ cumulativeCostUsd: heartbeat?.getState().cumulativeCostUsd,
867
+ agentHandle: transferHandle()
868
+ };
869
+ }
870
+ if (Date.now() - startTime >= timeoutMs) {
871
+ log.warn({ iteration: iteration - 1, timeoutMs }, "Total timeout exceeded");
872
+ notify({
873
+ event: "run_complete",
874
+ iteration: iteration - 1,
875
+ costUsd: heartbeat?.getState().cumulativeCostUsd,
876
+ message: `Total timeout (${timeoutMs}ms) exceeded after ${iteration - 1} iteration(s).`,
877
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
878
+ });
879
+ return {
880
+ exitReason: "timeout",
881
+ iterations: iteration - 1,
882
+ message: `Total timeout (${timeoutMs}ms) exceeded after ${iteration - 1} iteration(s).`,
883
+ sessionId,
884
+ cumulativeCostUsd: heartbeat?.getState().cumulativeCostUsd,
885
+ agentHandle: transferHandle()
886
+ };
887
+ }
888
+ const headBefore = await getHeadSha(opts.cwd);
889
+ const snapshotBefore = await captureWorkingTreeSnapshot(opts.cwd);
890
+ const iterationBaseCost = heartbeat.getState().cumulativeCostUsd;
891
+ const iterStart = Date.now();
892
+ log.debug({ iteration, maxIterations }, `Iteration ${iteration}/${maxIterations} \u2014 sending prompt`);
893
+ log.info(`${lanePrefix}Iteration ${iteration}/${maxIterations}`);
894
+ emitter?.emitEvent({
895
+ type: "iteration:start",
896
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
897
+ iteration,
898
+ maxIterations,
899
+ ...opts.laneId ? { laneId: opts.laneId } : {}
900
+ });
901
+ thinkingChars = 0;
902
+ thinkingStartTime = 0;
903
+ thinkingToolCalls = 0;
904
+ let streamDeltaCount = 0;
905
+ let streamCharCount = 0;
906
+ let lastStreamLogAt = 0;
907
+ let lastToolOrStreamLogAt = Date.now();
908
+ let lastThinkingEventAt = 0;
909
+ const THINKING_EVENT_INTERVAL_MS = 5e3;
910
+ const DEBUG_STREAM_INTERVAL_MS = 15e3;
911
+ const INFO_STREAM_INTERVAL_MS = 6e4;
912
+ const sendOnce = () => adapter.sendAndWait(handle, {
913
+ sessionId,
914
+ message: fullPrompt,
915
+ stallMs,
916
+ abortSignal: abort.signal,
917
+ onToolCall: (toolName, firstArg) => {
918
+ log.info(`${lanePrefix}tool: ${toolName}${firstArg ? " " + firstArg : ""}`);
919
+ thinkingToolCalls++;
920
+ thinkingChars = 0;
921
+ thinkingStartTime = 0;
922
+ lastToolOrStreamLogAt = Date.now();
923
+ lastThinkingEventAt = 0;
924
+ streamDeltaCount = 0;
925
+ streamCharCount = 0;
926
+ lastStreamLogAt = Date.now();
927
+ emitter?.emitEvent({
928
+ type: "tool:call",
929
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
930
+ toolName,
931
+ ...firstArg ? { firstArg } : {},
932
+ iteration,
933
+ ...opts.laneId ? { laneId: opts.laneId } : {}
934
+ });
935
+ },
936
+ onCostUpdate: (cost, tokens) => {
937
+ const effectiveCost = cost > 0 ? cost : tokens.input > 0 || tokens.output > 0 ? estimateCost(modelName, tokens) : 0;
938
+ const isEstimated = cost === 0 && effectiveCost > 0;
939
+ heartbeat.update({
940
+ cumulativeCostUsd: iterationBaseCost + effectiveCost,
941
+ ...isEstimated ? { costIsEstimated: true } : {}
942
+ });
943
+ emitter?.emitEvent({
944
+ type: "cost:update",
945
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
946
+ cumulativeCostUsd: iterationBaseCost + effectiveCost,
947
+ isEstimated,
948
+ iteration,
949
+ tokensIn: tokens.input,
950
+ tokensOut: tokens.output
951
+ });
952
+ },
953
+ onTextDelta: (charCount) => {
954
+ streamDeltaCount += 1;
955
+ streamCharCount += charCount;
956
+ thinkingChars += charCount;
957
+ const now = Date.now();
958
+ if (thinkingStartTime === 0) thinkingStartTime = now;
959
+ if (now - lastThinkingEventAt >= THINKING_EVENT_INTERVAL_MS) {
960
+ const elapsedSec = Math.round((now - thinkingStartTime) / 1e3);
961
+ emitter?.emitEvent({
962
+ type: "thinking",
963
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
964
+ iteration,
965
+ chars: thinkingChars,
966
+ elapsedSec,
967
+ ...opts.laneId ? { laneId: opts.laneId } : {}
968
+ });
969
+ lastThinkingEventAt = now;
970
+ }
971
+ if (now - lastStreamLogAt >= DEBUG_STREAM_INTERVAL_MS) {
972
+ streamLog.debug(
973
+ { iteration, deltas: streamDeltaCount, chars: streamCharCount },
974
+ `streaming (${streamDeltaCount} deltas, ${streamCharCount} chars)`
975
+ );
976
+ lastStreamLogAt = now;
977
+ }
978
+ const silenceSinceLastTool = now - lastToolOrStreamLogAt;
979
+ if (silenceSinceLastTool >= INFO_STREAM_INTERVAL_MS) {
980
+ const elapsedS = Math.round((now - thinkingStartTime) / 1e3);
981
+ const fmtElapsed = elapsedS < 60 ? `${elapsedS}s` : `${Math.floor(elapsedS / 60)}m ${elapsedS % 60}s`;
982
+ log.info({ thinking: fmtElapsed }, `thinking (${fmtElapsed})`);
983
+ streamLog.info(
984
+ {
985
+ iteration,
986
+ deltas: streamDeltaCount,
987
+ chars: streamCharCount,
988
+ silenceMs: silenceSinceLastTool
989
+ },
990
+ `still streaming (${streamDeltaCount} deltas, ${streamCharCount} chars, ${Math.round(silenceSinceLastTool / 1e3)}s since last tool)`
991
+ );
992
+ lastToolOrStreamLogAt = now;
993
+ }
994
+ }
995
+ });
996
+ let result = await sendOnce();
997
+ if (result.kind === "error") {
998
+ const initialClass = classifyError(result.message);
999
+ if (initialClass === "transient") {
1000
+ for (let attempt = 1; attempt < TRANSIENT_RETRY_MAX_ATTEMPTS; attempt++) {
1001
+ const delay = Math.min(
1002
+ TRANSIENT_RETRY_BASE_MS * Math.pow(2, attempt - 1),
1003
+ TRANSIENT_RETRY_MAX_MS
1004
+ );
1005
+ log.warn(
1006
+ { iteration, attempt, delayMs: delay, err: result.message },
1007
+ `Transient error \u2014 retrying in ${Math.round(delay / 1e3)}s (attempt ${attempt + 1}/${TRANSIENT_RETRY_MAX_ATTEMPTS})`
1008
+ );
1009
+ const aborted = await new Promise((resolve) => {
1010
+ if (abort.signal.aborted) {
1011
+ resolve(true);
1012
+ return;
1013
+ }
1014
+ const handle2 = setTimeout(() => {
1015
+ abort.signal.removeEventListener("abort", onAbort);
1016
+ resolve(false);
1017
+ }, delay);
1018
+ const onAbort = () => {
1019
+ clearTimeout(handle2);
1020
+ resolve(true);
1021
+ };
1022
+ abort.signal.addEventListener("abort", onAbort, { once: true });
1023
+ });
1024
+ if (aborted) break;
1025
+ result = await sendOnce();
1026
+ if (result.kind !== "error") break;
1027
+ const cls = classifyError(result.message);
1028
+ if (cls !== "transient") break;
1029
+ }
1030
+ }
1031
+ }
1032
+ const iterDurationMs = Date.now() - iterStart;
1033
+ if (result.kind === "abort") {
1034
+ log.warn({ iteration, iterDurationMs }, "Iteration aborted (total timeout)");
1035
+ notify({
1036
+ event: "run_complete",
1037
+ iteration,
1038
+ costUsd: heartbeat?.getState().cumulativeCostUsd,
1039
+ message: `Aborted after ${iteration} iteration(s) (total timeout exceeded).`,
1040
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1041
+ });
1042
+ return {
1043
+ exitReason: "timeout",
1044
+ iterations: iteration,
1045
+ message: `Aborted after ${iteration} iteration(s) (total timeout exceeded).`,
1046
+ sessionId,
1047
+ cumulativeCostUsd: heartbeat?.getState().cumulativeCostUsd,
1048
+ agentHandle: transferHandle()
1049
+ };
1050
+ }
1051
+ if (result.kind === "stall") {
1052
+ log.warn({ iteration, stallMs: result.stallMs }, "Iteration stalled");
1053
+ notify({
1054
+ event: "stall",
1055
+ iteration,
1056
+ costUsd: heartbeat?.getState().cumulativeCostUsd,
1057
+ message: `Iteration ${iteration} stalled for ${result.stallMs}ms with no idle signal.`,
1058
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1059
+ });
1060
+ return {
1061
+ exitReason: "stall",
1062
+ iterations: iteration,
1063
+ message: `Iteration ${iteration} stalled for ${result.stallMs}ms with no idle signal.`,
1064
+ sessionId,
1065
+ cumulativeCostUsd: heartbeat?.getState().cumulativeCostUsd,
1066
+ agentHandle: transferHandle()
1067
+ };
1068
+ }
1069
+ if (result.kind === "error") {
1070
+ const errorClass = classifyError(result.message);
1071
+ if (errorClass === "credential-expired") {
1072
+ const provider = detectProvider(modelName);
1073
+ log.error(
1074
+ { iteration, provider, err: result.message },
1075
+ "Credentials expired \u2014 autopilot cannot continue"
1076
+ );
1077
+ emitter?.emitEvent({
1078
+ type: "credential:expired",
1079
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1080
+ provider,
1081
+ message: `Credentials expired (${provider}). Run \`gs-assume\` and then \`glrs oc autopilot --resume\`.`,
1082
+ iteration
1083
+ });
1084
+ try {
1085
+ writeCheckpoint(opts.cwd, {
1086
+ planPath: opts.cwd,
1087
+ completedPhases: [],
1088
+ totalCostUsd: heartbeat?.getState().cumulativeCostUsd ?? 0,
1089
+ totalIterations: iteration - 1,
1090
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1091
+ });
1092
+ } catch {
1093
+ }
1094
+ notify({
1095
+ event: "error",
1096
+ iteration,
1097
+ costUsd: heartbeat?.getState().cumulativeCostUsd,
1098
+ errorMessage: result.message,
1099
+ message: `Credentials expired (${provider}). Run \`gs-assume\` and then \`glrs oc autopilot --resume\`.`,
1100
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1101
+ });
1102
+ log.error(
1103
+ "Credentials expired. Run `gs-assume` and then `glrs oc autopilot --resume`."
1104
+ );
1105
+ process.exit(2);
1106
+ }
1107
+ log.error({ iteration, err: result.message }, "Iteration errored");
1108
+ emitter?.emitEvent({
1109
+ type: "error",
1110
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1111
+ message: result.message ?? "unknown error",
1112
+ iteration
1113
+ });
1114
+ heartbeat.update({
1115
+ iterationsCompleted: iteration,
1116
+ lastIterationErrored: true
1117
+ });
1118
+ notify({
1119
+ event: "error",
1120
+ iteration,
1121
+ costUsd: heartbeat?.getState().cumulativeCostUsd,
1122
+ errorMessage: result.message,
1123
+ message: `Error in iteration ${iteration}: ${result.message}`,
1124
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1125
+ });
1126
+ return {
1127
+ exitReason: "error",
1128
+ iterations: iteration,
1129
+ message: `Error in iteration ${iteration}: ${result.message}`,
1130
+ sessionId,
1131
+ cumulativeCostUsd: heartbeat?.getState().cumulativeCostUsd,
1132
+ agentHandle: transferHandle()
1133
+ };
1134
+ }
1135
+ if (result.kind === "question_rejected") {
1136
+ log.warn(
1137
+ { iteration, questionTitle: result.title },
1138
+ `Question rejected \u2014 skipping to next iteration (iteration ${iteration})`
1139
+ );
1140
+ log.warn(`question rejected \u2014 skipping to next iteration`);
1141
+ }
1142
+ const lastMessage = await adapter.getLastResponse(handle, sessionId);
1143
+ if (detectSentinel(lastMessage)) {
1144
+ log.info({ iteration, iterDurationMs }, "Sentinel detected \u2014 autopilot done");
1145
+ notify({
1146
+ event: "run_complete",
1147
+ iteration,
1148
+ costUsd: heartbeat?.getState().cumulativeCostUsd,
1149
+ message: `Agent emitted <autopilot-done> at iteration ${iteration}.`,
1150
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1151
+ });
1152
+ return {
1153
+ exitReason: "sentinel",
1154
+ iterations: iteration,
1155
+ message: `Agent emitted <autopilot-done> at iteration ${iteration}.`,
1156
+ sessionId,
1157
+ cumulativeCostUsd: heartbeat?.getState().cumulativeCostUsd,
1158
+ agentHandle: transferHandle()
1159
+ };
1160
+ }
1161
+ const madeProgress = await checkProgress(opts.cwd, headBefore);
1162
+ struggle.record(madeProgress);
1163
+ try {
1164
+ const phaseMatch = opts.prompt.match(
1165
+ /## Your phase \([^)]+\)\n([\s\S]*?)(?=\n##\s|$)/
1166
+ );
1167
+ if (phaseMatch) {
1168
+ const phaseItems = parseItems(phaseMatch[1]);
1169
+ const expectedFiles = /* @__PURE__ */ new Set();
1170
+ for (const it of phaseItems) {
1171
+ if (it.checked) continue;
1172
+ for (const f of it.files) {
1173
+ if (f.path) expectedFiles.add(f.path);
1174
+ }
1175
+ }
1176
+ if (expectedFiles.size > 0) {
1177
+ const changedFiles = await getChangedFiles(opts.cwd, headBefore);
1178
+ const { extra, missing } = validateScope(
1179
+ [...expectedFiles],
1180
+ changedFiles
1181
+ );
1182
+ for (const f of extra) {
1183
+ log.warn(
1184
+ { scopeDrift: f, iteration },
1185
+ `Scope drift: agent edited ${f} which is not in the plan.`
1186
+ );
1187
+ }
1188
+ for (const f of missing) {
1189
+ log.warn(
1190
+ { incomplete: f, iteration },
1191
+ `Incomplete: plan expects changes to ${f} but none were made.`
1192
+ );
1193
+ }
1194
+ }
1195
+ }
1196
+ } catch (err) {
1197
+ log.debug({ err }, "scope validation skipped");
1198
+ }
1199
+ let cumulativeCostUsd = 0;
1200
+ let iterTokensIn = 0;
1201
+ let iterTokensOut = 0;
1202
+ if (adapter.getSessionStats) {
1203
+ const stats = await adapter.getSessionStats(handle, sessionId);
1204
+ cumulativeCostUsd = stats.cost;
1205
+ iterTokensIn = stats.tokensIn;
1206
+ iterTokensOut = stats.tokensOut;
1207
+ } else {
1208
+ cumulativeCostUsd = await adapter.getSessionCost(handle, sessionId);
1209
+ }
1210
+ if (cumulativeCostUsd > 0 || iterTokensIn > 0 || iterTokensOut > 0) {
1211
+ emitter?.emitEvent({
1212
+ type: "cost:update",
1213
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1214
+ cumulativeCostUsd,
1215
+ isEstimated: false,
1216
+ iteration,
1217
+ tokensIn: iterTokensIn,
1218
+ tokensOut: iterTokensOut
1219
+ });
1220
+ }
1221
+ let filesChanged = 0;
1222
+ let commitSubject = "";
1223
+ try {
1224
+ const { stdout: diffStat } = await execFile2("git", ["diff", "--stat", "HEAD~1", "HEAD"], { cwd: opts.cwd });
1225
+ const match = diffStat.match(/(\d+) files? changed/);
1226
+ if (match) filesChanged = parseInt(match[1], 10);
1227
+ } catch {
1228
+ }
1229
+ const headAfter = await getHeadSha(opts.cwd);
1230
+ if (headAfter !== headBefore) {
1231
+ try {
1232
+ const { stdout: logOut } = await execFile2("git", ["log", "--oneline", "-1"], { cwd: opts.cwd });
1233
+ commitSubject = logOut.trim().replace(/^[0-9a-f]+ /, "");
1234
+ } catch {
1235
+ }
1236
+ if (commitSubject && commitPrefix) {
1237
+ amendCommitWithPrefix(opts.cwd, commitPrefix).catch(() => {
1238
+ });
1239
+ }
1240
+ if (commitSubject) {
1241
+ log.info(`${lanePrefix}commit: ${commitSubject}`);
1242
+ }
1243
+ }
1244
+ const costDelta = cumulativeCostUsd - iterationBaseCost;
1245
+ const durationMin = iterDurationMs / 6e4;
1246
+ const fmtDur = iterDurationMs < 6e4 ? `${Math.round(iterDurationMs / 1e3)}s` : `${Math.floor(durationMin)}m ${Math.round(iterDurationMs / 1e3 % 60)}s`;
1247
+ const reqPerMin = durationMin > 0 ? Math.round(thinkingToolCalls / durationMin) : 0;
1248
+ log.info(
1249
+ {
1250
+ elapsed: fmtDur,
1251
+ ...costDelta > 0 ? { cost: `$${costDelta.toFixed(2)}` } : {},
1252
+ ...filesChanged > 0 ? { filesChanged } : {},
1253
+ ...reqPerMin > 0 ? { reqPerMin } : {}
1254
+ },
1255
+ `Iteration ${iteration} done`
1256
+ );
1257
+ emitter?.emitEvent({
1258
+ type: "iteration:done",
1259
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1260
+ iteration,
1261
+ durationMs: iterDurationMs,
1262
+ madeProgress,
1263
+ ...filesChanged > 0 ? { filesChanged } : {},
1264
+ ...commitSubject ? { commitSubject } : {},
1265
+ ...cumulativeCostUsd > 0 ? { costUsd: cumulativeCostUsd } : {},
1266
+ ...opts.laneId ? { laneId: opts.laneId } : {}
1267
+ });
1268
+ const planPathMatch = opts.prompt.match(/plans\/([^/\s]+(?:\/[^/\s]+)?)/);
1269
+ let planProgressPatch = {};
1270
+ if (planPathMatch) {
1271
+ try {
1272
+ const planPath = planPathMatch[0];
1273
+ const planState = parsePlanState(planPath);
1274
+ if (planState.type === "multi") {
1275
+ planProgressPatch = {
1276
+ phaseCount: planState.phaseCount,
1277
+ phasesCompleted: planState.phasesCompleted,
1278
+ mainCheckboxesTotal: planState.totalItems,
1279
+ mainCheckboxesCompleted: planState.checkedItems
1280
+ };
1281
+ }
1282
+ } catch (err) {
1283
+ log.debug({ err }, "plan-parser error \u2014 falling back to plan-blind heartbeat");
1284
+ }
1285
+ }
1286
+ heartbeat.update({
1287
+ iterationsCompleted: iteration,
1288
+ cumulativeCostUsd,
1289
+ lastIterationProgress: madeProgress,
1290
+ lastIterationErrored: false,
1291
+ ...planProgressPatch
1292
+ });
1293
+ notify({
1294
+ event: "iteration_complete",
1295
+ iteration,
1296
+ costUsd: cumulativeCostUsd,
1297
+ filesChanged: filesChanged > 0 ? filesChanged : void 0,
1298
+ commitSubject: commitSubject || void 0,
1299
+ message: `Iteration ${iteration} complete`,
1300
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1301
+ });
1302
+ log.debug(
1303
+ { iteration, iterDurationMs, madeProgress, cumulativeCostUsd },
1304
+ `Iteration ${iteration} idle (${(iterDurationMs / 1e3).toFixed(1)}s, ${madeProgress ? "progress" : "no progress"})`
1305
+ );
1306
+ if (!madeProgress && iteration > 1 && heartbeat?.getState().lastIterationProgress) {
1307
+ const currentSnapshot = await captureWorkingTreeSnapshot(opts.cwd);
1308
+ if (currentSnapshot === snapshotBefore) {
1309
+ log.info("No progress and no pending changes \u2014 treating as complete");
1310
+ return {
1311
+ exitReason: "sentinel",
1312
+ iterations: iteration,
1313
+ message: `No further progress possible at iteration ${iteration}. Treating as complete.`,
1314
+ sessionId,
1315
+ cumulativeCostUsd
1316
+ };
1317
+ }
1318
+ }
1319
+ if (struggle.isStruggling()) {
1320
+ log.warn({ iteration, struggleThreshold }, "Struggle detected \u2014 stopping");
1321
+ notify({
1322
+ event: "struggle",
1323
+ iteration,
1324
+ costUsd: heartbeat?.getState().cumulativeCostUsd,
1325
+ message: `Agent made no filesystem progress for ${struggleThreshold} consecutive iteration(s). Stopping at iteration ${iteration}.`,
1326
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1327
+ });
1328
+ return {
1329
+ exitReason: "struggle",
1330
+ iterations: iteration,
1331
+ message: `Agent made no filesystem progress for ${struggleThreshold} consecutive iteration(s). Stopping at iteration ${iteration}.`,
1332
+ sessionId,
1333
+ cumulativeCostUsd: heartbeat?.getState().cumulativeCostUsd,
1334
+ agentHandle: transferHandle()
1335
+ };
1336
+ }
1337
+ }
1338
+ log.warn({ maxIterations }, "Reached max iterations");
1339
+ notify({
1340
+ event: "run_complete",
1341
+ iteration: maxIterations,
1342
+ costUsd: heartbeat?.getState().cumulativeCostUsd,
1343
+ message: `Reached maximum iterations (${maxIterations}). Stopping.`,
1344
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1345
+ });
1346
+ return {
1347
+ exitReason: "max-iterations",
1348
+ iterations: maxIterations,
1349
+ message: `Reached maximum iterations (${maxIterations}). Stopping.`,
1350
+ sessionId,
1351
+ cumulativeCostUsd: heartbeat?.getState().cumulativeCostUsd,
1352
+ agentHandle: transferHandle()
1353
+ };
1354
+ } finally {
1355
+ delete process.env["GLRS_AUTOPILOT_HEADLESS"];
1356
+ clearTimeout(timeoutHandle);
1357
+ heartbeat?.stop();
1358
+ process.removeListener("SIGINT", sigintHandler);
1359
+ process.removeListener("SIGTERM", sigtermHandler);
1360
+ if (signalCount > 0) {
1361
+ try {
1362
+ const { stdout: porcelain } = await execFile2("git", ["status", "--porcelain"], { cwd: opts.cwd });
1363
+ if (porcelain.trim().length > 0) {
1364
+ if (autoCommit) {
1365
+ log.info({ signal: interruptedSignal }, "Committing WIP before exit");
1366
+ try {
1367
+ await execFile2("git", ["add", "-A"], { cwd: opts.cwd });
1368
+ const commitMsg = commitPrefix ? `${commitPrefix} [WIP] autopilot interrupted` : "[WIP] autopilot interrupted";
1369
+ await execFile2("git", ["commit", "-m", commitMsg], { cwd: opts.cwd });
1370
+ } catch (err) {
1371
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, "WIP commit failed (hooks may have rejected)");
1372
+ }
1373
+ } else {
1374
+ log.warn(
1375
+ { signal: interruptedSignal },
1376
+ "Pending changes left unstaged (auto_commit: false)"
1377
+ );
1378
+ }
1379
+ }
1380
+ } catch (err) {
1381
+ log.debug({ err }, "WIP-status check failed (non-fatal)");
1382
+ }
1383
+ try {
1384
+ writeCheckpoint(opts.cwd, {
1385
+ planPath: opts.cwd,
1386
+ completedPhases: [],
1387
+ totalCostUsd: heartbeat?.getState().cumulativeCostUsd ?? 0,
1388
+ totalIterations: heartbeat?.getState().iterationsCompleted ?? 0,
1389
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1390
+ });
1391
+ } catch {
1392
+ }
1393
+ }
1394
+ if (!handleTransferred) {
1395
+ log.info({}, "Shutting down agent");
1396
+ await adapter.shutdown(handle);
1397
+ }
1398
+ await autopilotLog.flush();
1399
+ }
1400
+ }
1401
+
1402
+ // src/git-safety.ts
1403
+ import { execFile as execFileCb4 } from "child_process";
1404
+ import { promisify as promisify4 } from "util";
1405
+ var execFile3 = promisify4(execFileCb4);
1406
+ async function recordHead(cwd, deps) {
1407
+ const exec = deps?.execGit ?? ((args, c) => execFile3("git", args, { cwd: c }));
1408
+ try {
1409
+ const { stdout } = await exec(["rev-parse", "HEAD"], cwd);
1410
+ return stdout.trim();
1411
+ } catch {
1412
+ return "HEAD";
1413
+ }
1414
+ }
1415
+ async function resetSoft(cwd, sha, opts) {
1416
+ const exec = opts?.execGit ?? ((args, c) => execFile3("git", args, { cwd: c }));
1417
+ if (!sha || sha === "HEAD") {
1418
+ opts?.onWarn?.(`resetSoft: invalid sha "${sha}" \u2014 skipping`);
1419
+ return false;
1420
+ }
1421
+ try {
1422
+ await exec(["reset", "--soft", sha], cwd);
1423
+ return true;
1424
+ } catch (err) {
1425
+ const msg = err instanceof Error ? err.message : String(err);
1426
+ opts?.onWarn?.(`resetSoft failed: ${msg}`);
1427
+ return false;
1428
+ }
1429
+ }
1430
+
1431
+ // src/spec-writer.ts
1432
+ import * as fs5 from "fs";
1433
+ import * as path4 from "path";
1434
+ import { parse as yamlParse, stringify as yamlStringify } from "yaml";
1435
+ function atomicWriteFileSync(target, content) {
1436
+ const dir = path4.dirname(target);
1437
+ const tmp = path4.join(dir, `.tmp-${process.pid}-${Date.now()}`);
1438
+ fs5.writeFileSync(tmp, content, "utf-8");
1439
+ fs5.renameSync(tmp, target);
1440
+ }
1441
+ function markItemUnchecked(planDir, phaseFile, itemId) {
1442
+ const phasePath = path4.join(planDir, "spec", phaseFile);
1443
+ try {
1444
+ const content = fs5.readFileSync(phasePath, "utf-8");
1445
+ const raw = yamlParse(content);
1446
+ if (typeof raw !== "object" || raw === null) return;
1447
+ const obj = raw;
1448
+ if (!Array.isArray(obj["items"])) return;
1449
+ const items = obj["items"];
1450
+ let found = false;
1451
+ for (const item of items) {
1452
+ if (item["id"] === itemId) {
1453
+ item["checked"] = false;
1454
+ found = true;
1455
+ break;
1456
+ }
1457
+ }
1458
+ if (!found) return;
1459
+ atomicWriteFileSync(phasePath, yamlStringify(raw));
1460
+ } catch {
1461
+ }
1462
+ }
1463
+ function markPhaseCompleted(planDir, phaseFile) {
1464
+ const mainPath = path4.join(planDir, "spec", "main.yaml");
1465
+ try {
1466
+ const content = fs5.readFileSync(mainPath, "utf-8");
1467
+ const raw = yamlParse(content);
1468
+ if (typeof raw !== "object" || raw === null) return;
1469
+ const obj = raw;
1470
+ if (!Array.isArray(obj["phases"])) return;
1471
+ const phases = obj["phases"];
1472
+ let found = false;
1473
+ for (const phase of phases) {
1474
+ if (phase["file"] === phaseFile) {
1475
+ phase["completed"] = true;
1476
+ found = true;
1477
+ break;
1478
+ }
1479
+ }
1480
+ if (!found) return;
1481
+ atomicWriteFileSync(mainPath, yamlStringify(raw));
1482
+ } catch {
1483
+ }
1484
+ }
1485
+
1486
+ // src/conflict-graph.ts
1487
+ function collectPhaseFiles(items) {
1488
+ if (items.length === 0) return null;
1489
+ const paths = /* @__PURE__ */ new Set();
1490
+ for (const item of items) {
1491
+ for (const file of item.files) {
1492
+ if (file.path) paths.add(file.path);
1493
+ }
1494
+ }
1495
+ if (paths.size === 0) return null;
1496
+ return paths;
1497
+ }
1498
+ function buildConflictGraph(phases) {
1499
+ const phaseNames = phases.map((p) => p.file);
1500
+ const fileSets = /* @__PURE__ */ new Map();
1501
+ for (const p of phases) {
1502
+ fileSets.set(p.file, collectPhaseFiles(p.items));
1503
+ }
1504
+ const conflicts = /* @__PURE__ */ new Map();
1505
+ for (const name of phaseNames) {
1506
+ conflicts.set(name, /* @__PURE__ */ new Set());
1507
+ }
1508
+ for (let i = 0; i < phaseNames.length; i++) {
1509
+ for (let j = i + 1; j < phaseNames.length; j++) {
1510
+ const a = phaseNames[i];
1511
+ const b = phaseNames[j];
1512
+ const setA = fileSets.get(a) ?? null;
1513
+ const setB = fileSets.get(b) ?? null;
1514
+ let conflict = false;
1515
+ if (setA === null || setB === null) {
1516
+ conflict = true;
1517
+ } else {
1518
+ for (const p of setA) {
1519
+ if (setB.has(p)) {
1520
+ conflict = true;
1521
+ break;
1522
+ }
1523
+ }
1524
+ }
1525
+ if (conflict) {
1526
+ conflicts.get(a).add(b);
1527
+ conflicts.get(b).add(a);
1528
+ }
1529
+ }
1530
+ }
1531
+ return { phases: phaseNames, conflicts };
1532
+ }
1533
+ function findIndependentPhases(graph) {
1534
+ const groups = [];
1535
+ for (const phase of graph.phases) {
1536
+ const conflictsWith = graph.conflicts.get(phase) ?? /* @__PURE__ */ new Set();
1537
+ let placed = false;
1538
+ for (const group of groups) {
1539
+ let ok = true;
1540
+ for (const member of group) {
1541
+ if (conflictsWith.has(member)) {
1542
+ ok = false;
1543
+ break;
1544
+ }
1545
+ }
1546
+ if (ok) {
1547
+ group.push(phase);
1548
+ placed = true;
1549
+ break;
1550
+ }
1551
+ }
1552
+ if (!placed) {
1553
+ groups.push([phase]);
1554
+ }
1555
+ }
1556
+ return groups;
1557
+ }
1558
+ function hasParallelism(graph) {
1559
+ return findIndependentPhases(graph).some((g) => g.length > 1);
1560
+ }
1561
+
1562
+ // src/lane-orchestrator.ts
1563
+ function conflictsWithRunning(phase, running, graph) {
1564
+ const conflicts = graph.conflicts.get(phase);
1565
+ if (!conflicts) return false;
1566
+ for (const r of running) {
1567
+ if (r === phase) continue;
1568
+ if (conflicts.has(r)) return true;
1569
+ }
1570
+ return false;
1571
+ }
1572
+ async function runLanes(opts) {
1573
+ const laneCount = Math.max(1, opts.laneCount);
1574
+ const queue = [...opts.phases];
1575
+ const running = /* @__PURE__ */ new Set();
1576
+ const results = [];
1577
+ const skipped = [];
1578
+ let stopScheduling = false;
1579
+ const inFlight = [];
1580
+ const freeLanes = [];
1581
+ for (let i = 1; i <= laneCount; i++) {
1582
+ freeLanes.push(`lane-${i}`);
1583
+ }
1584
+ const pickNext = () => {
1585
+ for (let i = 0; i < queue.length; i++) {
1586
+ const candidate = queue[i];
1587
+ if (!conflictsWithRunning(candidate, running, opts.conflictGraph)) {
1588
+ queue.splice(i, 1);
1589
+ return candidate;
1590
+ }
1591
+ }
1592
+ return null;
1593
+ };
1594
+ const fillLanes = () => {
1595
+ let dispatched = 0;
1596
+ while (!stopScheduling && freeLanes.length > 0 && queue.length > 0) {
1597
+ const phase = pickNext();
1598
+ if (!phase) break;
1599
+ const laneId = freeLanes.shift();
1600
+ running.add(phase);
1601
+ opts.logger?.info?.(
1602
+ { laneId, phase, lanesActive: running.size, queued: queue.length },
1603
+ `dispatch phase ${phase} on ${laneId}`
1604
+ );
1605
+ const promise = opts.runPhase(phase, laneId);
1606
+ inFlight.push({ phase, laneId, promise });
1607
+ dispatched++;
1608
+ }
1609
+ return dispatched;
1610
+ };
1611
+ if (opts.abortSignal?.aborted) {
1612
+ return { results: [], skipped: [...queue] };
1613
+ }
1614
+ const abortListener = () => {
1615
+ stopScheduling = true;
1616
+ opts.logger?.info?.({ remaining: queue.length }, "abort received \u2014 draining in-flight lanes");
1617
+ };
1618
+ opts.abortSignal?.addEventListener("abort", abortListener, { once: true });
1619
+ try {
1620
+ fillLanes();
1621
+ while (inFlight.length > 0) {
1622
+ const indexed = inFlight.map(
1623
+ (f, idx2) => f.promise.then((r) => ({ idx: idx2, result: r }))
1624
+ );
1625
+ const winner = await Promise.race(indexed);
1626
+ const { idx, result } = winner;
1627
+ const finished = inFlight[idx];
1628
+ inFlight.splice(idx, 1);
1629
+ running.delete(finished.phase);
1630
+ freeLanes.push(finished.laneId);
1631
+ results.push(result);
1632
+ opts.logger?.info?.(
1633
+ {
1634
+ laneId: finished.laneId,
1635
+ phase: finished.phase,
1636
+ ok: result.ok,
1637
+ iterations: result.iterations,
1638
+ remaining: queue.length
1639
+ },
1640
+ `completed phase ${finished.phase} on ${finished.laneId} (${result.ok ? "ok" : "failed"})`
1641
+ );
1642
+ if (result.fatal) {
1643
+ stopScheduling = true;
1644
+ opts.logger?.info?.(
1645
+ { phase: finished.phase },
1646
+ `fatal failure \u2014 stopping new dispatches`
1647
+ );
1648
+ }
1649
+ if (!stopScheduling) {
1650
+ fillLanes();
1651
+ }
1652
+ }
1653
+ skipped.push(...queue);
1654
+ } finally {
1655
+ opts.abortSignal?.removeEventListener("abort", abortListener);
1656
+ }
1657
+ return { results, skipped };
1658
+ }
1659
+
1660
+ // src/worktree.ts
1661
+ import { execFile as execFileCb5 } from "child_process";
1662
+ import { promisify as promisify5 } from "util";
1663
+ import * as path5 from "path";
1664
+ var execFileDefault2 = promisify5(execFileCb5);
1665
+ async function createWorktree(repoRoot, opts) {
1666
+ const exec = opts._deps?.execFile ?? execFileDefault2;
1667
+ const logger = opts.logger;
1668
+ const stamp = Date.now();
1669
+ const wtPath = path5.join(repoRoot, ".agent", "worktrees", `${opts.laneSlug}-${stamp}`);
1670
+ const branch = `autopilot/${opts.laneSlug}`;
1671
+ await exec("git", ["worktree", "add", wtPath, "-b", branch], { cwd: repoRoot });
1672
+ let cleanedUp = false;
1673
+ const cleanup = async () => {
1674
+ if (cleanedUp) return;
1675
+ cleanedUp = true;
1676
+ try {
1677
+ await exec("git", ["worktree", "remove", wtPath], { cwd: repoRoot });
1678
+ } catch (err) {
1679
+ logger?.warn?.(
1680
+ { err: err instanceof Error ? err.message : String(err), path: wtPath },
1681
+ `worktree cleanup failed for ${wtPath}`
1682
+ );
1683
+ return;
1684
+ }
1685
+ try {
1686
+ await exec("git", ["branch", "-D", branch], { cwd: repoRoot });
1687
+ } catch (err) {
1688
+ logger?.warn?.(
1689
+ { err: err instanceof Error ? err.message : String(err), branch },
1690
+ `branch cleanup failed for ${branch}`
1691
+ );
1692
+ }
1693
+ };
1694
+ return { path: wtPath, branch, cleanup };
1695
+ }
1696
+ function parseConflictPaths(output) {
1697
+ const paths = /* @__PURE__ */ new Set();
1698
+ const re = /^CONFLICT\s*\([^)]*\):\s*Merge conflict in\s+(.+)$/gm;
1699
+ let m;
1700
+ while ((m = re.exec(output)) !== null) {
1701
+ paths.add(m[1].trim());
1702
+ }
1703
+ return [...paths];
1704
+ }
1705
+ async function mergeWorktree(repoRoot, opts) {
1706
+ const exec = opts._deps?.execFile ?? execFileDefault2;
1707
+ try {
1708
+ await exec("git", ["merge", "--no-ff", opts.branch], { cwd: repoRoot });
1709
+ return { ok: true };
1710
+ } catch (err) {
1711
+ const e = err;
1712
+ const combined = `${e.stdout ?? ""}
1713
+ ${e.stderr ?? ""}`;
1714
+ const conflicts = parseConflictPaths(combined);
1715
+ try {
1716
+ await exec("git", ["merge", "--abort"], { cwd: repoRoot });
1717
+ } catch {
1718
+ }
1719
+ return { ok: false, conflicts };
1720
+ }
1721
+ }
1722
+
1723
+ // src/verify-runner.ts
1724
+ import { execFile as execFileCb6 } from "child_process";
1725
+ import { promisify as promisify6 } from "util";
1726
+ var execFileDefault3 = promisify6(execFileCb6);
1727
+ var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
1728
+ function getTimeoutForProofType(proofType, customTimeoutMs) {
1729
+ if (!proofType) return customTimeoutMs;
1730
+ switch (proofType) {
1731
+ case "unit_test":
1732
+ return 30 * 1e3;
1733
+ case "api_check":
1734
+ return 10 * 1e3;
1735
+ case "structural":
1736
+ case "typecheck":
1737
+ return 60 * 1e3;
1738
+ case "e2e":
1739
+ return 120 * 1e3;
1740
+ default:
1741
+ return customTimeoutMs;
1742
+ }
1743
+ }
1744
+ async function runVerifyCommands(items, cwd, opts = {}) {
1745
+ const execFile4 = opts._deps?.execFile ?? execFileDefault3;
1746
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1747
+ const results = [];
1748
+ for (const item of items) {
1749
+ const command = item.verify?.trim();
1750
+ if (!command) continue;
1751
+ const itemTimeoutMs = getTimeoutForProofType(item.proof_type, timeoutMs);
1752
+ const start = Date.now();
1753
+ try {
1754
+ const { stdout, stderr } = await execFile4("/bin/sh", ["-c", command], {
1755
+ cwd,
1756
+ signal: AbortSignal.timeout(itemTimeoutMs),
1757
+ // Capture as much output as the command produces — verify
1758
+ // commands are typically test runs; truncating their output
1759
+ // would hide the failure detail we want in the next prompt.
1760
+ maxBuffer: 10 * 1024 * 1024
1761
+ });
1762
+ results.push({
1763
+ itemId: item.id,
1764
+ command,
1765
+ passed: true,
1766
+ stdout: typeof stdout === "string" ? stdout : String(stdout ?? ""),
1767
+ stderr: typeof stderr === "string" ? stderr : String(stderr ?? ""),
1768
+ durationMs: Date.now() - start
1769
+ });
1770
+ } catch (err) {
1771
+ const e = err;
1772
+ const isTimeout = e.name === "AbortError" || e.code === "ABORT_ERR";
1773
+ const stdout = e.stdout !== void 0 ? typeof e.stdout === "string" ? e.stdout : e.stdout.toString() : "";
1774
+ let stderr = e.stderr !== void 0 ? typeof e.stderr === "string" ? e.stderr : e.stderr.toString() : "";
1775
+ if (isTimeout) {
1776
+ stderr = (stderr ? stderr + "\n" : "") + `[verify-runner] command timed out after ${Math.round(itemTimeoutMs / 1e3)}s`;
1777
+ } else if (!stderr && e.message) {
1778
+ stderr = e.message;
1779
+ }
1780
+ results.push({
1781
+ itemId: item.id,
1782
+ command,
1783
+ passed: false,
1784
+ stdout,
1785
+ stderr,
1786
+ durationMs: Date.now() - start
1787
+ });
1788
+ }
1789
+ }
1790
+ return results;
1791
+ }
1792
+
1793
+ // src/hook-runner.ts
1794
+ import { execFile as execFileCb7 } from "child_process";
1795
+ import { promisify as promisify7 } from "util";
1796
+ var execFileDefault4 = promisify7(execFileCb7);
1797
+ async function runHook(cmd, cwd, timeoutMs, opts = {}) {
1798
+ if (!cmd?.trim()) {
1799
+ return { ok: true, output: "" };
1800
+ }
1801
+ const execFile4 = opts._deps?.execFile ?? execFileDefault4;
1802
+ const timeout = opts.timeoutMs ?? timeoutMs;
1803
+ try {
1804
+ const { stdout, stderr } = await execFile4("/bin/sh", ["-c", cmd], {
1805
+ cwd,
1806
+ signal: AbortSignal.timeout(timeout),
1807
+ maxBuffer: 10 * 1024 * 1024
1808
+ });
1809
+ const combinedOutput = (typeof stdout === "string" ? stdout : String(stdout ?? "")) + (typeof stderr === "string" ? stderr : String(stderr ?? ""));
1810
+ return { ok: true, output: combinedOutput };
1811
+ } catch (err) {
1812
+ const e = err;
1813
+ const isTimeout = e.name === "AbortError" || e.code === "ABORT_ERR";
1814
+ let stderr = e.stderr !== void 0 ? typeof e.stderr === "string" ? e.stderr : e.stderr.toString() : "";
1815
+ if (isTimeout) {
1816
+ stderr = (stderr ? stderr + "\n" : "") + `[hook-runner] command timed out after ${Math.round(timeout / 1e3)}s`;
1817
+ } else if (!stderr && e.message) {
1818
+ stderr = e.message;
1819
+ }
1820
+ return { ok: false, output: stderr };
1821
+ }
1822
+ }
1823
+
1824
+ // src/plan-validator.ts
1825
+ import * as fs6 from "fs";
1826
+ import * as path6 from "path";
1827
+ import { parse as yamlParse2 } from "yaml";
1828
+ function detectReferencedPhaseFiles(mainContent) {
1829
+ const found = /* @__PURE__ */ new Set();
1830
+ const checkboxRe = /^- \[[ xX]\]\s+(?:\[)?([a-zA-Z0-9_-]+\.md)(?:\]\([^)]*\))?/gm;
1831
+ let match;
1832
+ while ((match = checkboxRe.exec(mainContent)) !== null) {
1833
+ found.add(match[1]);
1834
+ }
1835
+ const linkRe = /\[([a-zA-Z0-9_-]+\.md)\]\(\.\//g;
1836
+ while ((match = linkRe.exec(mainContent)) !== null) {
1837
+ found.add(match[1]);
1838
+ }
1839
+ return found;
1840
+ }
1841
+ function validatePlan(planPath) {
1842
+ const errors = [];
1843
+ const warnings = [];
1844
+ let stat;
1845
+ try {
1846
+ stat = fs6.statSync(planPath);
1847
+ } catch {
1848
+ errors.push({
1849
+ code: "missing-plan",
1850
+ message: `Plan path does not exist: ${planPath}`
1851
+ });
1852
+ return { errors, warnings };
1853
+ }
1854
+ if (!stat.isDirectory()) {
1855
+ try {
1856
+ const content = fs6.readFileSync(planPath, "utf-8");
1857
+ checkItemsSoft(content, planPath, warnings);
1858
+ } catch (err) {
1859
+ const msg = err instanceof Error ? err.message : String(err);
1860
+ warnings.push({
1861
+ code: "plan-read-failed",
1862
+ message: `Plan file unreadable: ${msg}`,
1863
+ file: planPath
1864
+ });
1865
+ }
1866
+ return { errors, warnings };
1867
+ }
1868
+ if (hasSpec(planPath)) {
1869
+ const mainSpecPath = path6.join(planPath, "spec", "main.yaml");
1870
+ try {
1871
+ const mainContent2 = fs6.readFileSync(mainSpecPath, "utf-8");
1872
+ const rawMain = yamlParse2(mainContent2);
1873
+ const mainValidation = validateMainSpec(rawMain);
1874
+ if (!mainValidation.valid) {
1875
+ for (const msg of mainValidation.errors) {
1876
+ errors.push({
1877
+ code: "invalid-spec-main",
1878
+ message: `spec/main.yaml: ${msg}`,
1879
+ file: mainSpecPath
1880
+ });
1881
+ }
1882
+ return { errors, warnings };
1883
+ }
1884
+ } catch (err) {
1885
+ const msg = err instanceof Error ? err.message : String(err);
1886
+ errors.push({
1887
+ code: "invalid-spec-main",
1888
+ message: `spec/main.yaml unreadable: ${msg}`,
1889
+ file: mainSpecPath
1890
+ });
1891
+ return { errors, warnings };
1892
+ }
1893
+ const phaseFiles = detectSpecPhases(planPath);
1894
+ for (const phaseFile of phaseFiles) {
1895
+ const phasePath = path6.join(planPath, "spec", phaseFile);
1896
+ if (!fs6.existsSync(phasePath)) {
1897
+ errors.push({
1898
+ code: "missing-spec-phase-file",
1899
+ message: `Phase file referenced in spec/main.yaml does not exist: ${phaseFile}`,
1900
+ file: phaseFile
1901
+ });
1902
+ continue;
1903
+ }
1904
+ try {
1905
+ const phaseContent = fs6.readFileSync(phasePath, "utf-8");
1906
+ const rawPhase = yamlParse2(phaseContent);
1907
+ const phaseValidation = validatePhaseSpec(rawPhase);
1908
+ if (!phaseValidation.valid) {
1909
+ for (const msg of phaseValidation.errors) {
1910
+ errors.push({
1911
+ code: "invalid-spec-phase",
1912
+ message: `spec/${phaseFile}: ${msg}`,
1913
+ file: phaseFile
1914
+ });
1915
+ }
1916
+ }
1917
+ } catch (err) {
1918
+ const msg = err instanceof Error ? err.message : String(err);
1919
+ warnings.push({
1920
+ code: "spec-phase-read-failed",
1921
+ message: `spec/${phaseFile} unreadable: ${msg}`,
1922
+ file: phaseFile
1923
+ });
1924
+ }
1925
+ }
1926
+ return { errors, warnings };
1927
+ }
1928
+ const mainPath = path6.join(planPath, "main.md");
1929
+ if (!fs6.existsSync(mainPath)) {
1930
+ errors.push({
1931
+ code: "missing-main",
1932
+ message: "Directory plan must contain main.md",
1933
+ file: mainPath
1934
+ });
1935
+ return { errors, warnings };
1936
+ }
1937
+ let mainContent;
1938
+ try {
1939
+ mainContent = fs6.readFileSync(mainPath, "utf-8");
1940
+ } catch (err) {
1941
+ const msg = err instanceof Error ? err.message : String(err);
1942
+ errors.push({
1943
+ code: "main-read-failed",
1944
+ message: `main.md unreadable: ${msg}`,
1945
+ file: mainPath
1946
+ });
1947
+ return { errors, warnings };
1948
+ }
1949
+ const referencedPhases = detectReferencedPhaseFiles(mainContent);
1950
+ for (const phaseFile of referencedPhases) {
1951
+ const phasePath = path6.join(planPath, phaseFile);
1952
+ if (!fs6.existsSync(phasePath)) {
1953
+ errors.push({
1954
+ code: "missing-phase-file",
1955
+ message: `Phase file referenced in main.md does not exist: ${phaseFile}`,
1956
+ file: phaseFile
1957
+ });
1958
+ }
1959
+ }
1960
+ let phaseEntries = [];
1961
+ try {
1962
+ phaseEntries = fs6.readdirSync(planPath).filter(
1963
+ (f) => f.endsWith(".md") && f !== "main.md" && f !== "scope.md" && f !== "scope-seed.md"
1964
+ );
1965
+ } catch {
1966
+ }
1967
+ for (const phaseFile of phaseEntries) {
1968
+ const phasePath = path6.join(planPath, phaseFile);
1969
+ let content;
1970
+ try {
1971
+ content = fs6.readFileSync(phasePath, "utf-8");
1972
+ } catch (err) {
1973
+ const msg = err instanceof Error ? err.message : String(err);
1974
+ warnings.push({
1975
+ code: "phase-read-failed",
1976
+ message: `Phase file unreadable: ${msg}`,
1977
+ file: phaseFile
1978
+ });
1979
+ continue;
1980
+ }
1981
+ const items = parseItems(content);
1982
+ const withIntent = items.filter((it) => it.intent && it.intent.trim());
1983
+ if (items.length > 0 && withIntent.length === 0) {
1984
+ errors.push({
1985
+ code: "no-intent-in-phase",
1986
+ message: `Phase file has items but none declare an intent: ${phaseFile}`,
1987
+ file: phaseFile
1988
+ });
1989
+ }
1990
+ checkItemsSoft(content, phaseFile, warnings);
1991
+ }
1992
+ return { errors, warnings };
1993
+ }
1994
+ function checkItemsSoft(content, file, warnings) {
1995
+ const items = parseItems(content);
1996
+ for (const item of items) {
1997
+ if (item.files.length === 0) {
1998
+ warnings.push({
1999
+ code: "missing-files",
2000
+ message: `Item ${item.id} has no files: list`,
2001
+ file,
2002
+ itemId: item.id
2003
+ });
2004
+ }
2005
+ if (item.tests.length === 0) {
2006
+ warnings.push({
2007
+ code: "missing-tests",
2008
+ message: `Item ${item.id} has no tests: list`,
2009
+ file,
2010
+ itemId: item.id
2011
+ });
2012
+ }
2013
+ if (!item.verify || !item.verify.trim()) {
2014
+ warnings.push({
2015
+ code: "missing-verify",
2016
+ message: `Item ${item.id} has no verify: command`,
2017
+ file,
2018
+ itemId: item.id
2019
+ });
2020
+ }
2021
+ }
2022
+ }
2023
+
2024
+ // src/phase-config.ts
2025
+ function deepMerge(...objects) {
2026
+ const result = {};
2027
+ for (const obj of objects) {
2028
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
2029
+ continue;
2030
+ }
2031
+ for (const key in obj) {
2032
+ const srcValue = obj[key];
2033
+ if (srcValue && typeof srcValue === "object" && !Array.isArray(srcValue) && !(srcValue instanceof Date)) {
2034
+ const targetValue = result[key];
2035
+ if (targetValue && typeof targetValue === "object" && !Array.isArray(targetValue) && !(targetValue instanceof Date)) {
2036
+ result[key] = deepMerge(
2037
+ targetValue,
2038
+ srcValue
2039
+ );
2040
+ } else {
2041
+ result[key] = srcValue;
2042
+ }
2043
+ } else {
2044
+ result[key] = srcValue;
2045
+ }
2046
+ }
2047
+ }
2048
+ return result;
2049
+ }
2050
+ function resolvePhaseConfig(baseConfig, phaseName) {
2051
+ if (!baseConfig || typeof baseConfig !== "object" || Array.isArray(baseConfig)) {
2052
+ return {};
2053
+ }
2054
+ const base = baseConfig;
2055
+ const phases = base.phases;
2056
+ if (!phases || typeof phases !== "object") {
2057
+ return base;
2058
+ }
2059
+ const phaseOverride = phases[phaseName];
2060
+ if (!phaseOverride || typeof phaseOverride !== "object" || Array.isArray(phaseOverride)) {
2061
+ return base;
2062
+ }
2063
+ return deepMerge(base, phaseOverride);
2064
+ }
2065
+
2066
+ // src/loop-session.ts
2067
+ function extractVerifyConfig(config) {
2068
+ const cfgObj = config;
2069
+ const strategy = cfgObj?.verify ?? "after_phase";
2070
+ const timeoutMs = cfgObj?.verify_timeout ?? 5 * 60 * 1e3;
2071
+ const retryOnFailure = cfgObj?.verify_retry ?? true;
2072
+ return { strategy, timeoutMs, retryOnFailure };
2073
+ }
2074
+ function extractHooksConfig(config) {
2075
+ const cfgObj = config;
2076
+ const hooks = cfgObj?.hooks;
2077
+ return {
2078
+ pre_phase: hooks?.pre_phase,
2079
+ post_phase: hooks?.post_phase,
2080
+ post_run: hooks?.post_run,
2081
+ on_error: hooks?.on_error
2082
+ };
2083
+ }
2084
+ function uncheckItemsInMarkdown(content, itemIds) {
2085
+ if (itemIds.length === 0) return content;
2086
+ let result = content;
2087
+ for (const itemId of itemIds) {
2088
+ const escaped = itemId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2089
+ const re = new RegExp(`^(- )\\[[xX]\\](\\s+id:\\s+${escaped}\\b)`, "m");
2090
+ result = result.replace(re, "$1[ ]$2");
2091
+ }
2092
+ return result;
2093
+ }
2094
+ function extractSection(content, sectionName) {
2095
+ const re = new RegExp(
2096
+ `^## ${sectionName}\\s*\\n([\\s\\S]*?)(?=^## |$)`,
2097
+ "m"
2098
+ );
2099
+ const match = re.exec(content);
2100
+ return match ? match[1].trim() : "";
2101
+ }
2102
+ function detectPhaseFiles2(mainContent, planDir) {
2103
+ const found = /* @__PURE__ */ new Set();
2104
+ const checkboxRe = /^- \[[ xX]\]\s+(?:\[)?([a-zA-Z0-9_-]+\.md)(?:\]\([^)]*\))?/gm;
2105
+ let match;
2106
+ while ((match = checkboxRe.exec(mainContent)) !== null) {
2107
+ found.add(match[1]);
2108
+ }
2109
+ const linkRe = /\[([a-zA-Z0-9_-]+\.md)\]\(\.\//g;
2110
+ while ((match = linkRe.exec(mainContent)) !== null) {
2111
+ found.add(match[1]);
2112
+ }
2113
+ if (found.size === 0) {
2114
+ try {
2115
+ const entries = fs7.readdirSync(planDir);
2116
+ for (const f of entries) {
2117
+ if (f.endsWith(".md") && f !== "main.md" && f !== "scope.md" && f !== "scope-seed.md") {
2118
+ found.add(f);
2119
+ }
2120
+ }
2121
+ } catch {
2122
+ }
2123
+ }
2124
+ return [...found].sort((a, b) => {
2125
+ const numA = parseInt(a.replace(/[^0-9]/g, ""), 10) || 0;
2126
+ const numB = parseInt(b.replace(/[^0-9]/g, ""), 10) || 0;
2127
+ return numA - numB;
2128
+ });
2129
+ }
2130
+ function filterUncheckedPhases(phaseFiles, mainContent, planDir, readFile) {
2131
+ const checkedRe = /^- \[x\]\s+(?:\[)?([a-zA-Z0-9_-]+\.md)/gm;
2132
+ const checked = /* @__PURE__ */ new Set();
2133
+ let match;
2134
+ while ((match = checkedRe.exec(mainContent)) !== null) {
2135
+ checked.add(match[1]);
2136
+ }
2137
+ const hasCheckboxRefs = /^- \[[ xX]\]\s+(?:\[)?[a-zA-Z0-9_-]+\.md/m.test(mainContent);
2138
+ if (hasCheckboxRefs) {
2139
+ return phaseFiles.filter((f) => !checked.has(f));
2140
+ }
2141
+ return phaseFiles.filter((f) => {
2142
+ try {
2143
+ const content = readFile(path7.join(planDir, f));
2144
+ return !isPhaseComplete(content);
2145
+ } catch {
2146
+ return true;
2147
+ }
2148
+ });
2149
+ }
2150
+ function isPhaseComplete(phaseContent) {
2151
+ const checkboxRe = /^[ \t]*-\s+\[([ xX])\]/gm;
2152
+ let total = 0;
2153
+ let checked = 0;
2154
+ let match;
2155
+ while ((match = checkboxRe.exec(phaseContent)) !== null) {
2156
+ total++;
2157
+ if (match[1] !== " ") checked++;
2158
+ }
2159
+ return total === 0 || checked === total;
2160
+ }
2161
+ function markPhaseChecked(mainContent, phaseFile) {
2162
+ const escaped = phaseFile.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2163
+ const re = new RegExp(`^(- )\\[ \\](\\s+(?:\\[)?${escaped})`, "m");
2164
+ return mainContent.replace(re, "$1[x]$2");
2165
+ }
2166
+ var SUCCESS_REASONS = /* @__PURE__ */ new Set(["sentinel", "idle", "max-iterations"]);
2167
+ function filterUncheckedPhasesYaml(phaseFiles, planDir) {
2168
+ return filterUncheckedSpecPhases(phaseFiles, planDir);
2169
+ }
2170
+ async function runLoopSession(opts) {
2171
+ const _runRalphLoop = opts._deps?.runRalphLoop ?? runRalphLoop;
2172
+ const _readFileSync = opts._deps?.readFileSync ?? ((p) => fs7.readFileSync(p, "utf8"));
2173
+ const _writeFileSync = opts._deps?.writeFileSync ?? ((p, content) => fs7.writeFileSync(p, content, "utf8"));
2174
+ const _runVerifyCommands = opts._deps?.runVerifyCommands ?? runVerifyCommands;
2175
+ const emitter = opts.emitter;
2176
+ if (!opts._deps) {
2177
+ const report = validatePlan(opts.planPath);
2178
+ if (report.errors.length > 0 || report.warnings.length > 0) {
2179
+ const earlyLog = opts.logger ? opts.logger.root.child({ component: "autopilot.plan-validator" }) : null;
2180
+ for (const w of report.warnings) {
2181
+ if (earlyLog) {
2182
+ earlyLog.warn(
2183
+ {
2184
+ code: w.code,
2185
+ ...w.file ? { file: w.file } : {},
2186
+ ...w.itemId ? { itemId: w.itemId } : {}
2187
+ },
2188
+ w.message
2189
+ );
2190
+ }
2191
+ }
2192
+ if (report.errors.length > 0) {
2193
+ for (const e of report.errors) {
2194
+ if (earlyLog) {
2195
+ earlyLog.error(
2196
+ {
2197
+ code: e.code,
2198
+ ...e.file ? { file: e.file } : {},
2199
+ ...e.itemId ? { itemId: e.itemId } : {}
2200
+ },
2201
+ e.message
2202
+ );
2203
+ }
2204
+ }
2205
+ return {
2206
+ exitReason: "error",
2207
+ iterations: 0,
2208
+ message: `Plan validation failed: ${report.errors.map((e) => e.message).join("; ")}`
2209
+ };
2210
+ }
2211
+ }
2212
+ }
2213
+ const isDirectory = opts._deps?.isDirectory ? opts._deps.isDirectory(opts.planPath) : (() => {
2214
+ try {
2215
+ return fs7.statSync(opts.planPath).isDirectory();
2216
+ } catch {
2217
+ return false;
2218
+ }
2219
+ })();
2220
+ if (!isDirectory) {
2221
+ const prompt = `Work the plan at ${opts.planPath}. Complete all items in ## Acceptance criteria. Mark items done as they complete.`;
2222
+ const adapterName = opts.adapter?.name;
2223
+ const singleFileCfgObj = opts.config;
2224
+ const models = singleFileCfgObj?.models;
2225
+ const executionSpecifier = models?.execution;
2226
+ const executionModel = executionSpecifier ? resolveModel(executionSpecifier, adapterName ?? "opencode") : void 0;
2227
+ const singleFileStallMs = singleFileCfgObj?.stall_timeout ?? STALL_MS_BY_TIER[opts.fast ? "autopilot-execute" : "deep"];
2228
+ const singleFileAdapters = opts.config?.adapters;
2229
+ const singleFileOcAdapter = singleFileAdapters?.opencode;
2230
+ const singleFileAgentOverrides = singleFileOcAdapter?.agents;
2231
+ return _runRalphLoop({
2232
+ prompt,
2233
+ cwd: opts.cwd,
2234
+ agentName: opts.fast ? "autopilot-fast" : void 0,
2235
+ model: executionModel,
2236
+ stallMs: singleFileStallMs,
2237
+ config: opts.config,
2238
+ agentOverrides: singleFileAgentOverrides,
2239
+ logger: opts.logger,
2240
+ emitter,
2241
+ adapter: opts.adapter
2242
+ });
2243
+ }
2244
+ const log = opts.logger ? opts.logger.root.child({ component: "autopilot.orchestrator" }) : pino(
2245
+ { level: "info", timestamp: pino.stdTimeFunctions.isoTime },
2246
+ pino.destination({ fd: 2, sync: false })
2247
+ ).child({ component: "autopilot.orchestrator" });
2248
+ const tier = opts.fast ? "autopilot-execute" : "deep";
2249
+ const cfgObj = opts.config;
2250
+ const cfgMaxIterPerPhase = cfgObj?.max_iterations_per_phase;
2251
+ const maxIterationsPerPhase = opts.maxIterationsPerPhase ?? cfgMaxIterPerPhase ?? MAX_ITERATIONS_PER_PHASE_BY_TIER[tier];
2252
+ const cfgStallMs = cfgObj?.stall_timeout;
2253
+ const stallMs = cfgStallMs ?? STALL_MS_BY_TIER[tier];
2254
+ const verifyConfig = extractVerifyConfig(opts.config);
2255
+ const hooksConfig = extractHooksConfig(opts.config);
2256
+ const mainMdPath = path7.join(opts.planPath, "main.md");
2257
+ const useYamlSpec = hasSpec(opts.planPath);
2258
+ let goal;
2259
+ let constraints;
2260
+ let allPhases;
2261
+ if (useYamlSpec) {
2262
+ goal = readSpecGoal(opts.planPath);
2263
+ constraints = readSpecConstraints(opts.planPath);
2264
+ allPhases = detectSpecPhases(opts.planPath);
2265
+ } else {
2266
+ const mainContent = _readFileSync(mainMdPath);
2267
+ goal = extractSection(mainContent, "Goal");
2268
+ constraints = extractSection(mainContent, "Constraints");
2269
+ allPhases = detectPhaseFiles2(mainContent, opts.planPath);
2270
+ }
2271
+ const mainContentForFilter = useYamlSpec ? "" : _readFileSync(mainMdPath);
2272
+ let uncheckedPhases = useYamlSpec ? filterUncheckedPhasesYaml(allPhases, opts.planPath) : filterUncheckedPhases(allPhases, mainContentForFilter, opts.planPath, _readFileSync);
2273
+ let resumedFromCheckpoint = false;
2274
+ if (opts.resume) {
2275
+ const cp = readCheckpoint(opts.cwd);
2276
+ if (cp) {
2277
+ if (cp.planPath !== opts.planPath) {
2278
+ log.warn({ expected: opts.planPath, found: cp.planPath }, "checkpoint planPath mismatch, starting fresh");
2279
+ } else {
2280
+ const skip = new Set(cp.completedPhases);
2281
+ const before = uncheckedPhases.length;
2282
+ uncheckedPhases = uncheckedPhases.filter((p) => !skip.has(p));
2283
+ const skipped = before - uncheckedPhases.length;
2284
+ if (skipped > 0) {
2285
+ log.info({ skipped, remaining: uncheckedPhases.length }, "resuming from checkpoint");
2286
+ resumedFromCheckpoint = true;
2287
+ }
2288
+ }
2289
+ } else {
2290
+ log.info("no checkpoint found, starting fresh");
2291
+ }
2292
+ }
2293
+ log.info({ total: allPhases.length, remaining: uncheckedPhases.length }, "plan loaded");
2294
+ emitter?.emitEvent({
2295
+ type: "phase:start",
2296
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2297
+ phase: `plan loaded: ${uncheckedPhases.length}/${allPhases.length} phases remaining`,
2298
+ laneId: "lane-1",
2299
+ current: 0,
2300
+ total: allPhases.length
2301
+ });
2302
+ if (uncheckedPhases.length === 0) {
2303
+ log.info("all phases already complete \u2014 nothing to do");
2304
+ emitter?.emitEvent({
2305
+ type: "phase:done",
2306
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2307
+ phase: "all phases already complete",
2308
+ laneId: "lane-1",
2309
+ completed: true,
2310
+ iterations: 0,
2311
+ costUsd: 0
2312
+ });
2313
+ return {
2314
+ exitReason: "sentinel",
2315
+ iterations: 0,
2316
+ message: "All phases already complete \u2014 nothing to do",
2317
+ cumulativeCostUsd: 0
2318
+ };
2319
+ }
2320
+ for (const p of uncheckedPhases) {
2321
+ log.info({ file: p }, "phase file");
2322
+ }
2323
+ const phaseInputs = uncheckedPhases.map((f) => ({
2324
+ file: f,
2325
+ items: (() => {
2326
+ try {
2327
+ return parseItems(_readFileSync(path7.join(opts.planPath, f)));
2328
+ } catch {
2329
+ return [];
2330
+ }
2331
+ })()
2332
+ }));
2333
+ const conflictGraph = buildConflictGraph(phaseInputs);
2334
+ const planParallel = hasParallelism(conflictGraph);
2335
+ const requestedLanes = Math.max(1, opts.parallel ?? 1);
2336
+ const useParallel = requestedLanes > 1 && planParallel;
2337
+ if (uncheckedPhases.length > 1) {
2338
+ if (useParallel) {
2339
+ log.info(
2340
+ { lanes: requestedLanes, phases: uncheckedPhases.length },
2341
+ "parallelization plan: independent phases will run in worktrees"
2342
+ );
2343
+ } else if (requestedLanes > 1 && !planParallel) {
2344
+ log.info(
2345
+ { lanes: requestedLanes },
2346
+ "parallelization plan: every phase shares a file \u2014 falling back to sequential"
2347
+ );
2348
+ } else {
2349
+ log.info({ lanes: 1 }, "parallelization plan: sequential");
2350
+ }
2351
+ }
2352
+ let totalCostUsd = 0;
2353
+ let totalIterations = 0;
2354
+ let phasesCompleted = 0;
2355
+ const laneCosts = /* @__PURE__ */ new Map();
2356
+ const orphanedWorktrees = [];
2357
+ const verifyResults = [];
2358
+ const completedPhasesAcc = [];
2359
+ if (resumedFromCheckpoint) {
2360
+ const cp = readCheckpoint(opts.cwd);
2361
+ if (cp) {
2362
+ totalCostUsd = cp.totalCostUsd;
2363
+ totalIterations = cp.totalIterations;
2364
+ completedPhasesAcc.push(...cp.completedPhases);
2365
+ }
2366
+ }
2367
+ let lastResult = {
2368
+ exitReason: "sentinel",
2369
+ iterations: 0,
2370
+ message: "No phases to execute."
2371
+ };
2372
+ const cfgMaxIterPerItem = opts.config?.max_iterations_per_item;
2373
+ const maxIterationsPerItem = cfgMaxIterPerItem ?? MAX_ITERATIONS_PER_ITEM;
2374
+ const runItemsForPhase = async (args) => {
2375
+ const { phaseFile, phasePath, laneId, runCwd, useParallel: useParallel2 } = args;
2376
+ const phaseContent = args.readFileSync(phasePath);
2377
+ const allItems = useYamlSpec ? parseSpecItems(phasePath) : parseItems(phaseContent);
2378
+ const items = allItems.filter((it) => !it.checked);
2379
+ log.info(
2380
+ { phase: phaseFile, total: allItems.length, unchecked: items.length, checked: allItems.length - items.length },
2381
+ `phase items: ${items.length} unchecked of ${allItems.length} total`
2382
+ );
2383
+ if (items.length === 0) {
2384
+ const prompt = `You are executing one phase of a multi-file plan. Work through every unchecked item in order. Check each box as you complete it. Commit when the phase is done.
2385
+
2386
+ ## Overall goal
2387
+ ${goal}
2388
+
2389
+ ## Constraints
2390
+ ${constraints}
2391
+
2392
+ ## Your phase (${phaseFile})
2393
+ ${phaseContent}
2394
+
2395
+ Do not work on items from other phases. Do not ask questions.`;
2396
+ const adapterName = args.adapter?.name;
2397
+ const cfgObj2 = args.config;
2398
+ const models = cfgObj2?.models;
2399
+ const executionSpecifier = models?.execution;
2400
+ const executionModel = executionSpecifier ? resolveModel(executionSpecifier, adapterName ?? "opencode") : void 0;
2401
+ return args.runRalphLoop({
2402
+ prompt,
2403
+ cwd: runCwd,
2404
+ agentName: "autopilot-fast",
2405
+ model: executionModel,
2406
+ maxIterations: maxIterationsPerPhase,
2407
+ ...args.stallMs ? { stallMs: args.stallMs } : {},
2408
+ config: args.config,
2409
+ agentOverrides: args.agentOverrides,
2410
+ laneId: useParallel2 ? laneId : void 0,
2411
+ logger: args.logger,
2412
+ emitter: args.emitter,
2413
+ adapter: args.adapter
2414
+ });
2415
+ }
2416
+ const perItemCap = Math.max(
2417
+ 1,
2418
+ Math.min(
2419
+ maxIterationsPerItem,
2420
+ Math.ceil(maxIterationsPerPhase / items.length)
2421
+ )
2422
+ );
2423
+ let cumulativeIterations = 0;
2424
+ let cumulativeCost = 0;
2425
+ let lastItemResult = {
2426
+ exitReason: "sentinel",
2427
+ iterations: 0,
2428
+ message: "No items to execute."
2429
+ };
2430
+ for (const item of items) {
2431
+ const filesList = item.files.map((f) => `${f.path}${f.isNew ? " (CREATE)" : " (EDIT)"}`).join(", ");
2432
+ const verify = item.verify?.trim() || "(no verify command declared)";
2433
+ const enriched = item;
2434
+ let enrichmentBlock = "";
2435
+ if (enriched.mirror || enriched.context || enriched.conventions || enriched.proof) {
2436
+ const parts = [];
2437
+ if (typeof enriched.mirror === "string" && enriched.mirror.trim()) {
2438
+ parts.push(`Pattern reference (read this file for the pattern to follow):
2439
+ ${enriched.mirror}`);
2440
+ }
2441
+ if (typeof enriched.context === "string" && enriched.context.trim()) {
2442
+ parts.push(`Code context:
2443
+ ${enriched.context}`);
2444
+ }
2445
+ if (typeof enriched.conventions === "string" && enriched.conventions.trim()) {
2446
+ parts.push(`Conventions: ${enriched.conventions}`);
2447
+ }
2448
+ if (typeof enriched.proof === "string" && enriched.proof.trim()) {
2449
+ const proofType = typeof enriched.proof_type === "string" ? ` (${enriched.proof_type})` : "";
2450
+ parts.push(`Acceptance proof${proofType}: ${enriched.proof}`);
2451
+ }
2452
+ enrichmentBlock = `
2453
+ ## Enrichment context
2454
+
2455
+ ${parts.join("\n\n")}
2456
+
2457
+ `;
2458
+ }
2459
+ const phaseConfigForItem = resolvePhaseConfig(
2460
+ args.config,
2461
+ phaseFile.replace(/\.(md|ya?ml)$/, "")
2462
+ );
2463
+ const executionStyle = phaseConfigForItem?.execution_style;
2464
+ const isTddMode = executionStyle !== "direct";
2465
+ let itemPrompt;
2466
+ if (isTddMode) {
2467
+ itemPrompt = `You are executing ONE item of a multi-item phase using TDD (test-driven development). Follow the red-green-refactor cycle strictly:
2468
+
2469
+ 1. RED: Write a failing test/proof first. Run verify to confirm it fails.
2470
+ 2. GREEN: Implement the minimal code to make the test pass. Run verify to confirm it passes.
2471
+ 3. REFACTOR: Clean up the code if needed, re-run verify to ensure it still passes.
2472
+ 4. MARK: Mark the item checkbox and commit only after the verify command passes.
2473
+
2474
+ Complete only this item, mark its checkbox in ${phaseFile}, commit, and stop. Do not work on other items.
2475
+
2476
+ ## Overall goal
2477
+ ${goal}
2478
+
2479
+ ## Constraints
2480
+ ${constraints}
2481
+
2482
+ ## Your item
2483
+ - [ ] id: ${item.id}
2484
+ intent: ${item.intent}
2485
+ files: ${filesList || "(none declared)"}
2486
+ verify: ${verify}
2487
+
2488
+ ` + enrichmentBlock + `## Structured context
2489
+
2490
+ Files you may touch (ONLY these):
2491
+ ` + (item.files.length > 0 ? item.files.map((f) => ` - ${f.path} (${f.isNew ? "CREATE" : "EDIT"})`).join("\n") : " (none declared \u2014 confine edits to the phase's natural scope)") + `
2492
+
2493
+ Verify command (must exit 0):
2494
+ - ${verify}
2495
+
2496
+ TDD workflow:
2497
+ 1. Write a test that fails (RED phase)
2498
+ 2. Implement to make it pass (GREEN phase)
2499
+ 3. Refactor if needed, re-verify (REFACTOR phase)
2500
+ 4. Mark checkbox when verify passes
2501
+
2502
+ Non-goals:
2503
+ - Do NOT skip the RED phase \u2014 always write the test first.
2504
+ - Do NOT modify files outside the list above.
2505
+ - Do NOT work on items other than ${item.id}.
2506
+
2507
+ When done: mark the checkbox for item ${item.id} in ${phaseFile} as [x], commit, and emit the autopilot-done sentinel.`;
2508
+ } else {
2509
+ itemPrompt = `You are executing ONE item of a multi-item phase. Complete only this item, mark its checkbox in ${phaseFile}, commit, and stop. Do not work on other items.
2510
+
2511
+ ## Overall goal
2512
+ ${goal}
2513
+
2514
+ ## Constraints
2515
+ ${constraints}
2516
+
2517
+ ## Your item
2518
+ - [ ] id: ${item.id}
2519
+ intent: ${item.intent}
2520
+ files: ${filesList || "(none declared)"}
2521
+ verify: ${verify}
2522
+
2523
+ ` + enrichmentBlock + `## Structured context
2524
+
2525
+ Files you may touch (ONLY these):
2526
+ ` + (item.files.length > 0 ? item.files.map((f) => ` - ${f.path} (${f.isNew ? "CREATE" : "EDIT"})`).join("\n") : " (none declared \u2014 confine edits to the phase's natural scope)") + `
2527
+
2528
+ Verify command (must exit 0):
2529
+ - ${verify}
2530
+
2531
+ Non-goals:
2532
+ - Do NOT modify files outside the list above.
2533
+ - Do NOT work on items other than ${item.id}.
2534
+
2535
+ When done: mark the checkbox for item ${item.id} in ${phaseFile} as [x], commit, and emit the autopilot-done sentinel.`;
2536
+ }
2537
+ const adapterName = args.adapter?.name;
2538
+ const cfgObj2 = args.config;
2539
+ const models = cfgObj2?.models;
2540
+ const executionSpecifier = models?.execution;
2541
+ const executionModel = executionSpecifier ? resolveModel(executionSpecifier, adapterName ?? "opencode") : void 0;
2542
+ const itemResult = await args.runRalphLoop({
2543
+ prompt: itemPrompt,
2544
+ cwd: runCwd,
2545
+ agentName: "autopilot-fast",
2546
+ model: executionModel,
2547
+ maxIterations: perItemCap,
2548
+ ...args.stallMs ? { stallMs: args.stallMs } : {},
2549
+ config: args.config,
2550
+ agentOverrides: args.agentOverrides,
2551
+ laneId: useParallel2 ? laneId : void 0,
2552
+ logger: args.logger,
2553
+ emitter: args.emitter,
2554
+ adapter: args.adapter
2555
+ });
2556
+ cumulativeIterations += itemResult.iterations;
2557
+ cumulativeCost += itemResult.cumulativeCostUsd ?? 0;
2558
+ lastItemResult = itemResult;
2559
+ let updatedContent;
2560
+ try {
2561
+ updatedContent = args.readFileSync(phasePath);
2562
+ } catch {
2563
+ updatedContent = phaseContent;
2564
+ }
2565
+ const updatedItems = parseItems(updatedContent);
2566
+ const matched = updatedItems.find((u) => u.id === item.id);
2567
+ if (matched && !matched.checked) {
2568
+ log.warn(
2569
+ { phase: phaseFile, itemId: item.id },
2570
+ "item completed iteration without marking its checkbox"
2571
+ );
2572
+ }
2573
+ if (!SUCCESS_REASONS.has(itemResult.exitReason)) {
2574
+ return {
2575
+ ...itemResult,
2576
+ iterations: cumulativeIterations,
2577
+ cumulativeCostUsd: cumulativeCost,
2578
+ message: `Item ${item.id} failed: ${itemResult.message}`
2579
+ };
2580
+ }
2581
+ if (args.verifyConfig?.strategy === "after_item" && item.verify?.trim()) {
2582
+ const itemVerifyResult = await _runVerifyCommands([item], runCwd, {
2583
+ timeoutMs: args.verifyConfig.timeoutMs
2584
+ });
2585
+ if (itemVerifyResult.length > 0 && !itemVerifyResult[0].passed) {
2586
+ const failed = itemVerifyResult[0];
2587
+ log.warn(
2588
+ {
2589
+ phase: phaseFile,
2590
+ itemId: failed.itemId,
2591
+ command: failed.command,
2592
+ stderr: failed.stderr.slice(0, 500)
2593
+ },
2594
+ "per-item verify failed"
2595
+ );
2596
+ if (useYamlSpec && args.planPath) {
2597
+ markItemUnchecked(args.planPath, phaseFile, item.id);
2598
+ args.logger?.root.child({ component: "autopilot.per-item-verify" }).warn(
2599
+ { phase: phaseFile, itemId: item.id },
2600
+ "unchecked item that failed verify \u2014 will retry next iteration"
2601
+ );
2602
+ } else if (args.writeFileSync) {
2603
+ const currentPhaseContent = args.readFileSync(phasePath);
2604
+ const uncheckContent = uncheckItemsInMarkdown(currentPhaseContent, [item.id]);
2605
+ if (uncheckContent !== currentPhaseContent) {
2606
+ args.writeFileSync(phasePath, uncheckContent);
2607
+ args.logger?.root.child({ component: "autopilot.per-item-verify" }).warn(
2608
+ { phase: phaseFile, itemId: item.id },
2609
+ "unchecked item that failed verify \u2014 will retry next iteration"
2610
+ );
2611
+ }
2612
+ }
2613
+ if (args.verifyConfig.retryOnFailure) {
2614
+ return {
2615
+ exitReason: "sentinel",
2616
+ iterations: cumulativeIterations,
2617
+ cumulativeCostUsd: cumulativeCost,
2618
+ message: `Item ${item.id} verify failed: ${failed.stderr.split("\n")[0]}`
2619
+ };
2620
+ }
2621
+ }
2622
+ }
2623
+ }
2624
+ return {
2625
+ ...lastItemResult,
2626
+ iterations: cumulativeIterations,
2627
+ cumulativeCostUsd: cumulativeCost,
2628
+ message: `${items.length} items completed in ${cumulativeIterations} iterations`
2629
+ };
2630
+ };
2631
+ const runPhaseInner = async (phaseFile, laneId, runCwd, retryContext) => {
2632
+ let verifyFailureSummary;
2633
+ const phasePath = useYamlSpec ? path7.join(opts.planPath, "spec", phaseFile) : path7.join(opts.planPath, phaseFile);
2634
+ const phaseContent = _readFileSync(phasePath);
2635
+ const preHeadSha = await recordHead(runCwd);
2636
+ log.info(
2637
+ { phase: phaseFile, lane: laneId, current: phasesCompleted + 1, total: uncheckedPhases.length },
2638
+ "starting phase"
2639
+ );
2640
+ emitter?.emitEvent({
2641
+ type: "phase:start",
2642
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2643
+ phase: phaseFile,
2644
+ laneId,
2645
+ current: phasesCompleted + 1,
2646
+ total: uncheckedPhases.length
2647
+ });
2648
+ const phaseName = phaseFile.replace(/\.(md|ya?ml)$/, "");
2649
+ const phaseConfig = resolvePhaseConfig(
2650
+ opts.config,
2651
+ phaseName
2652
+ );
2653
+ const phaseOcAdapter = phaseConfig.adapters?.opencode;
2654
+ const baseAdapters = opts.config?.adapters;
2655
+ const baseOcAdapter = baseAdapters?.opencode;
2656
+ const phaseAgentOverrides = phaseOcAdapter?.agents ?? baseOcAdapter?.agents;
2657
+ let result;
2658
+ const phaseVerifyConfig = extractVerifyConfig(phaseConfig);
2659
+ if (opts.fast) {
2660
+ result = await runItemsForPhase({
2661
+ phaseFile,
2662
+ phasePath,
2663
+ laneId,
2664
+ runCwd,
2665
+ runRalphLoop: _runRalphLoop,
2666
+ readFileSync: _readFileSync,
2667
+ writeFileSync: _writeFileSync,
2668
+ planPath: opts.planPath,
2669
+ useParallel,
2670
+ stallMs,
2671
+ logger: opts.logger,
2672
+ emitter,
2673
+ adapter: opts.adapter,
2674
+ config: opts.config,
2675
+ agentOverrides: phaseAgentOverrides,
2676
+ verifyConfig: phaseVerifyConfig
2677
+ });
2678
+ } else {
2679
+ const retrySection = retryContext ? `
2680
+
2681
+ ## Previous attempt failed
2682
+ The previous attempt at this phase failed verification. Here's what went wrong:
2683
+ ${retryContext}
2684
+
2685
+ Fix these failures before marking items as complete.
2686
+ ` : "";
2687
+ const prompt = `You are executing one phase of a multi-file plan. Work through every unchecked item in order. Check each box as you complete it. Commit when the phase is done.
2688
+
2689
+ ## Overall goal
2690
+ ${goal}
2691
+
2692
+ ## Constraints
2693
+ ${constraints}
2694
+
2695
+ ## Your phase (${phaseFile})
2696
+ ${phaseContent}
2697
+ ` + retrySection + `
2698
+ Do not work on items from other phases. Do not ask questions \u2014 pick sensible defaults and note decisions in ## Open questions.`;
2699
+ const adapterName = opts.adapter?.name;
2700
+ const cfgObj2 = opts.config;
2701
+ const models = cfgObj2?.models;
2702
+ const executionSpecifier = models?.execution;
2703
+ const executionModel = executionSpecifier ? resolveModel(executionSpecifier, adapterName ?? "opencode") : void 0;
2704
+ result = await _runRalphLoop({
2705
+ prompt,
2706
+ cwd: runCwd,
2707
+ agentName: void 0,
2708
+ model: executionModel,
2709
+ maxIterations: maxIterationsPerPhase,
2710
+ stallMs,
2711
+ config: opts.config,
2712
+ agentOverrides: phaseAgentOverrides,
2713
+ laneId: useParallel ? laneId : void 0,
2714
+ logger: opts.logger,
2715
+ emitter,
2716
+ adapter: opts.adapter
2717
+ });
2718
+ }
2719
+ totalIterations += result.iterations;
2720
+ const costThisPhase = result.cumulativeCostUsd ?? 0;
2721
+ totalCostUsd += costThisPhase;
2722
+ laneCosts.set(laneId, (laneCosts.get(laneId) ?? 0) + costThisPhase);
2723
+ lastResult = result;
2724
+ const updatedPhaseContent = _readFileSync(phasePath);
2725
+ let phaseComplete = useYamlSpec ? (() => {
2726
+ const yamlItems = parseSpecItems(phasePath);
2727
+ const checkedCount = yamlItems.filter((i) => i.checked).length;
2728
+ log.info(
2729
+ { phase: phaseFile, total: yamlItems.length, checked: checkedCount },
2730
+ `phase completion check: ${checkedCount}/${yamlItems.length} items checked`
2731
+ );
2732
+ return yamlItems.length > 0 && yamlItems.every((i) => i.checked);
2733
+ })() : isPhaseComplete(updatedPhaseContent);
2734
+ const verifyConfig2 = extractVerifyConfig(opts.config);
2735
+ const shouldSkipVerify = verifyConfig2.strategy === "skip";
2736
+ const isAfterItemMode = verifyConfig2.strategy === "after_item" && opts.fast;
2737
+ if (phaseComplete && !shouldSkipVerify && !isAfterItemMode) {
2738
+ const items = useYamlSpec ? parseSpecItems(phasePath) : parseItems(updatedPhaseContent);
2739
+ const itemsWithVerify = items.filter((it) => it.verify?.trim());
2740
+ if (itemsWithVerify.length > 0) {
2741
+ log.info(
2742
+ { phase: phaseFile, count: itemsWithVerify.length },
2743
+ "running verify commands"
2744
+ );
2745
+ emitter?.emitEvent({
2746
+ type: "verify:start",
2747
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2748
+ phase: phaseFile,
2749
+ itemCount: itemsWithVerify.length
2750
+ });
2751
+ const phaseVerify = await _runVerifyCommands(itemsWithVerify, runCwd, {
2752
+ timeoutMs: verifyConfig2.timeoutMs
2753
+ });
2754
+ verifyResults.push({ phaseFile, results: phaseVerify });
2755
+ const failed = phaseVerify.filter((r) => !r.passed);
2756
+ for (const r of phaseVerify) {
2757
+ emitter?.emitEvent({
2758
+ type: "verify:result",
2759
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2760
+ phase: phaseFile,
2761
+ itemId: r.itemId,
2762
+ command: r.command,
2763
+ passed: r.passed,
2764
+ ...r.stderr ? { stderr: r.stderr.slice(0, 500) } : {}
2765
+ });
2766
+ }
2767
+ if (failed.length > 0) {
2768
+ const failedItemIds = failed.map((f) => f.itemId);
2769
+ for (const f of failed) {
2770
+ log.warn(
2771
+ {
2772
+ phase: phaseFile,
2773
+ itemId: f.itemId,
2774
+ command: f.command,
2775
+ stderr: f.stderr.slice(0, 500)
2776
+ },
2777
+ "verify command failed"
2778
+ );
2779
+ }
2780
+ for (const itemId of failedItemIds) {
2781
+ if (useYamlSpec) {
2782
+ markItemUnchecked(opts.planPath, phaseFile, itemId);
2783
+ } else {
2784
+ const uncheckContent = uncheckItemsInMarkdown(updatedPhaseContent, [itemId]);
2785
+ if (uncheckContent !== updatedPhaseContent) {
2786
+ _writeFileSync(phasePath, uncheckContent);
2787
+ log.warn(
2788
+ { phase: phaseFile, itemId },
2789
+ "unchecked item that failed verify \u2014 will retry next iteration"
2790
+ );
2791
+ }
2792
+ }
2793
+ }
2794
+ phaseComplete = false;
2795
+ verifyFailureSummary = failed.map((f) => `- \`${f.command}\` failed (item ${f.itemId}): ${f.stderr.split("\n").slice(-3).join(" ").slice(0, 200)}`).join("\n");
2796
+ } else {
2797
+ log.info(
2798
+ { phase: phaseFile, passed: phaseVerify.length },
2799
+ "all verify commands passed"
2800
+ );
2801
+ }
2802
+ emitter?.emitEvent({
2803
+ type: "verify:done",
2804
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2805
+ phase: phaseFile,
2806
+ passed: phaseVerify.filter((r) => r.passed).length,
2807
+ failed: failed.length
2808
+ });
2809
+ }
2810
+ }
2811
+ if (!phaseComplete && !SUCCESS_REASONS.has(result.exitReason)) {
2812
+ log.warn({ phase: phaseFile, exitReason: result.exitReason }, "phase failed");
2813
+ const rollbackConfig = cfgObj?.rollback_on_failure ?? "soft";
2814
+ if (rollbackConfig !== "off" && opts.fast && preHeadSha && preHeadSha !== "HEAD") {
2815
+ const ok = await resetSoft(runCwd, preHeadSha, {
2816
+ onWarn: (m) => log.warn(m)
2817
+ });
2818
+ if (ok) {
2819
+ log.info({ ref: preHeadSha.slice(0, 8) }, "soft-reset to pre-phase state");
2820
+ }
2821
+ } else if (rollbackConfig === "off" && opts.fast && preHeadSha && preHeadSha !== "HEAD") {
2822
+ log.info({ ref: preHeadSha.slice(0, 8) }, "rollback disabled by config \u2014 keeping phase changes");
2823
+ }
2824
+ emitter?.emitEvent({
2825
+ type: "phase:done",
2826
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2827
+ phase: phaseFile,
2828
+ laneId,
2829
+ completed: false,
2830
+ iterations: result.iterations,
2831
+ costUsd: costThisPhase
2832
+ });
2833
+ const phaseHooksConfig = extractHooksConfig(phaseConfig);
2834
+ const effectiveOnErrorHook = phaseHooksConfig.on_error ?? hooksConfig.on_error;
2835
+ if (effectiveOnErrorHook) {
2836
+ runHook(effectiveOnErrorHook, runCwd, verifyConfig2.timeoutMs).catch(() => {
2837
+ });
2838
+ }
2839
+ return {
2840
+ phaseFile,
2841
+ laneId,
2842
+ ok: false,
2843
+ fatal: true,
2844
+ iterations: result.iterations,
2845
+ costUsd: costThisPhase,
2846
+ phaseLoopResult: result,
2847
+ phaseComplete: false
2848
+ };
2849
+ }
2850
+ if (!phaseComplete && result.exitReason === "max-iterations") {
2851
+ log.warn(
2852
+ { phase: phaseFile, max: maxIterationsPerPhase },
2853
+ "phase budget exhausted \u2014 moving on"
2854
+ );
2855
+ const checkpointEnabled = cfgObj?.checkpoint !== false;
2856
+ if (checkpointEnabled) {
2857
+ writeCheckpoint(opts.cwd, {
2858
+ planPath: opts.planPath,
2859
+ completedPhases: [...completedPhasesAcc],
2860
+ totalCostUsd,
2861
+ totalIterations,
2862
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2863
+ });
2864
+ }
2865
+ }
2866
+ emitter?.emitEvent({
2867
+ type: "phase:done",
2868
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2869
+ phase: phaseFile,
2870
+ laneId,
2871
+ completed: phaseComplete,
2872
+ iterations: result.iterations,
2873
+ costUsd: costThisPhase
2874
+ });
2875
+ return {
2876
+ phaseFile,
2877
+ laneId,
2878
+ ok: phaseComplete,
2879
+ fatal: false,
2880
+ iterations: result.iterations,
2881
+ costUsd: costThisPhase,
2882
+ phaseLoopResult: result,
2883
+ phaseComplete,
2884
+ verifyFailures: verifyFailureSummary
2885
+ };
2886
+ };
2887
+ const recordPhaseCompletion = async (phaseFile, result, phaseHooksConfig) => {
2888
+ phasesCompleted++;
2889
+ completedPhasesAcc.push(phaseFile);
2890
+ if (useYamlSpec) {
2891
+ markPhaseCompleted(opts.planPath, phaseFile);
2892
+ } else {
2893
+ const updatedMain = markPhaseChecked(_readFileSync(mainMdPath), phaseFile);
2894
+ _writeFileSync(mainMdPath, updatedMain);
2895
+ }
2896
+ const checkpointEnabled = cfgObj?.checkpoint !== false;
2897
+ if (checkpointEnabled) {
2898
+ writeCheckpoint(opts.cwd, {
2899
+ planPath: opts.planPath,
2900
+ completedPhases: [...completedPhasesAcc],
2901
+ totalCostUsd,
2902
+ totalIterations,
2903
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2904
+ });
2905
+ }
2906
+ log.info(
2907
+ {
2908
+ phase: phaseFile,
2909
+ completed: phasesCompleted,
2910
+ total: uncheckedPhases.length,
2911
+ iterations: result.iterations,
2912
+ cost: (result.cumulativeCostUsd ?? 0).toFixed(2)
2913
+ },
2914
+ "phase complete"
2915
+ );
2916
+ const effectivePostPhaseHook = phaseHooksConfig?.post_phase ?? hooksConfig.post_phase;
2917
+ if (effectivePostPhaseHook) {
2918
+ const hookResult = await runHook(effectivePostPhaseHook, opts.cwd, verifyConfig.timeoutMs);
2919
+ if (!hookResult.ok) {
2920
+ log.warn({ phase: phaseFile, output: hookResult.output }, "post_phase hook failed");
2921
+ }
2922
+ }
2923
+ };
2924
+ if (!useParallel) {
2925
+ for (const phaseFile of uncheckedPhases) {
2926
+ if (opts.signal?.aborted) {
2927
+ log.info({ completed: phasesCompleted, remaining: uncheckedPhases.length }, "abort signal received \u2014 writing checkpoint and stopping");
2928
+ const checkpointEnabled = cfgObj?.checkpoint !== false;
2929
+ if (checkpointEnabled) {
2930
+ writeCheckpoint(opts.cwd, {
2931
+ planPath: opts.planPath,
2932
+ completedPhases: [...completedPhasesAcc],
2933
+ totalCostUsd,
2934
+ totalIterations,
2935
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2936
+ });
2937
+ }
2938
+ return {
2939
+ exitReason: "aborted",
2940
+ iterations: totalIterations,
2941
+ cumulativeCostUsd: totalCostUsd,
2942
+ message: `Aborted after ${phasesCompleted}/${uncheckedPhases.length} phases completed`
2943
+ };
2944
+ }
2945
+ const phaseName = phaseFile.replace(/\.(md|ya?ml)$/, "");
2946
+ const phaseConfig = resolvePhaseConfig(
2947
+ opts.config,
2948
+ phaseName
2949
+ );
2950
+ const phaseHooksConfig = extractHooksConfig(phaseConfig);
2951
+ const verifyRetryConfig = extractVerifyConfig(opts.config);
2952
+ const effectiveMaxRetries = verifyRetryConfig.retryOnFailure ? opts.maxPhaseRetries ?? (opts._deps ? 1 : 3) : 1;
2953
+ let attempt = 0;
2954
+ let phaseSuccess = false;
2955
+ let lastVerifyFailures;
2956
+ while (attempt < effectiveMaxRetries && !phaseSuccess) {
2957
+ attempt++;
2958
+ const isEscalation = attempt === effectiveMaxRetries && attempt > 1 && opts.fast;
2959
+ if (attempt > 1) {
2960
+ log.info(
2961
+ { phase: phaseFile, attempt, escalate: isEscalation },
2962
+ isEscalation ? "escalating to deep model for retry" : "retrying phase after verify failure"
2963
+ );
2964
+ emitter?.emitEvent({
2965
+ type: "phase:start",
2966
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2967
+ phase: `${phaseFile} (${isEscalation ? "escalation" : `retry ${attempt - 1}`})`,
2968
+ laneId: "lane-1",
2969
+ current: phasesCompleted + 1,
2970
+ total: uncheckedPhases.length
2971
+ });
2972
+ }
2973
+ const originalFast = opts.fast;
2974
+ if (isEscalation) {
2975
+ opts.fast = false;
2976
+ }
2977
+ const effectivePrePhaseHook = phaseHooksConfig.pre_phase ?? hooksConfig.pre_phase;
2978
+ if (effectivePrePhaseHook) {
2979
+ const hookResult = await runHook(effectivePrePhaseHook, opts.cwd, verifyConfig.timeoutMs);
2980
+ if (!hookResult.ok) {
2981
+ log.warn({ phase: phaseFile, output: hookResult.output }, "pre_phase hook failed \u2014 skipping phase");
2982
+ if (isEscalation) {
2983
+ opts.fast = originalFast;
2984
+ }
2985
+ phaseSuccess = false;
2986
+ continue;
2987
+ }
2988
+ }
2989
+ const r = await runPhaseInner(phaseFile, "lane-1", opts.cwd, lastVerifyFailures);
2990
+ if (isEscalation) {
2991
+ opts.fast = originalFast;
2992
+ }
2993
+ lastVerifyFailures = r.verifyFailures;
2994
+ if (r.phaseComplete) {
2995
+ await recordPhaseCompletion(phaseFile, r.phaseLoopResult, phaseHooksConfig);
2996
+ phaseSuccess = true;
2997
+ break;
2998
+ }
2999
+ if (r.fatal) {
3000
+ return {
3001
+ ...r.phaseLoopResult,
3002
+ iterations: totalIterations,
3003
+ cumulativeCostUsd: totalCostUsd,
3004
+ message: `${r.phaseLoopResult.message} (phase ${phaseFile}, ${phasesCompleted}/${uncheckedPhases.length} phases completed, total $${totalCostUsd.toFixed(2)})`
3005
+ };
3006
+ }
3007
+ if (attempt >= effectiveMaxRetries) {
3008
+ log.warn({ phase: phaseFile, attempts: attempt }, "phase exhausted retries \u2014 moving on");
3009
+ }
3010
+ }
3011
+ }
3012
+ } else {
3013
+ const handles = /* @__PURE__ */ new Map();
3014
+ const wtLogger = {
3015
+ warn: (obj, msg) => log.warn(obj, msg),
3016
+ info: (obj, msg) => log.info(obj, msg)
3017
+ };
3018
+ const { execFileSync } = await import("child_process");
3019
+ const exitCleanup = () => {
3020
+ for (const handle of handles.values()) {
3021
+ try {
3022
+ execFileSync("git", ["worktree", "remove", "--force", handle.path], {
3023
+ cwd: opts.cwd,
3024
+ stdio: "ignore"
3025
+ });
3026
+ } catch {
3027
+ }
3028
+ try {
3029
+ execFileSync("git", ["branch", "-D", handle.branch], {
3030
+ cwd: opts.cwd,
3031
+ stdio: "ignore"
3032
+ });
3033
+ } catch {
3034
+ }
3035
+ }
3036
+ };
3037
+ process.on("exit", exitCleanup);
3038
+ try {
3039
+ const lanesResult = await runLanes({
3040
+ phases: uncheckedPhases,
3041
+ conflictGraph,
3042
+ laneCount: requestedLanes,
3043
+ logger: wtLogger,
3044
+ runPhase: async (phaseFile, laneId) => {
3045
+ const slug = phaseFile.replace(/\.md$/, "").replace(/[^a-zA-Z0-9_-]/g, "_");
3046
+ let handle = null;
3047
+ try {
3048
+ handle = await createWorktree(opts.cwd, {
3049
+ laneSlug: `${slug}-${laneId}`,
3050
+ logger: wtLogger
3051
+ });
3052
+ } catch (err) {
3053
+ const msg = err instanceof Error ? err.message : String(err);
3054
+ log.warn({ phase: phaseFile, err: msg }, "worktree create failed \u2014 falling back to main cwd");
3055
+ }
3056
+ const runCwd = handle?.path ?? opts.cwd;
3057
+ const phaseName = phaseFile.replace(/\.(md|ya?ml)$/, "");
3058
+ const phaseConfig = resolvePhaseConfig(
3059
+ opts.config,
3060
+ phaseName
3061
+ );
3062
+ const phaseHooksConfig = extractHooksConfig(phaseConfig);
3063
+ const effectivePrePhaseHook = phaseHooksConfig.pre_phase ?? hooksConfig.pre_phase;
3064
+ if (effectivePrePhaseHook) {
3065
+ const hookResult = await runHook(effectivePrePhaseHook, runCwd, verifyConfig.timeoutMs);
3066
+ if (!hookResult.ok) {
3067
+ log.warn({ phase: phaseFile, output: hookResult.output }, "pre_phase hook failed \u2014 skipping phase");
3068
+ return {
3069
+ phaseFile,
3070
+ laneId,
3071
+ ok: false,
3072
+ fatal: false,
3073
+ iterations: 0,
3074
+ costUsd: 0,
3075
+ phaseLoopResult: {
3076
+ exitReason: "error",
3077
+ iterations: 0,
3078
+ message: "pre_phase hook failed"
3079
+ },
3080
+ phaseComplete: false
3081
+ };
3082
+ }
3083
+ }
3084
+ const result = await runPhaseInner(phaseFile, laneId, runCwd);
3085
+ if (handle) {
3086
+ handles.set(phaseFile, handle);
3087
+ if (result.phaseComplete) {
3088
+ const merge = await mergeWorktree(opts.cwd, { branch: handle.branch });
3089
+ if (merge.ok) {
3090
+ await handle.cleanup().catch(() => {
3091
+ });
3092
+ handles.delete(phaseFile);
3093
+ } else {
3094
+ log.warn(
3095
+ { phase: phaseFile, conflicts: merge.conflicts, path: handle.path },
3096
+ "merge failed \u2014 worktree left on disk for manual resolution"
3097
+ );
3098
+ orphanedWorktrees.push(handle.path);
3099
+ return { ...result, ok: false, fatal: true };
3100
+ }
3101
+ } else {
3102
+ await handle.cleanup().catch(() => {
3103
+ });
3104
+ handles.delete(phaseFile);
3105
+ }
3106
+ }
3107
+ return result;
3108
+ }
3109
+ });
3110
+ for (const r of lanesResult.results) {
3111
+ if (r.ok) {
3112
+ const phaseName = r.phaseFile.replace(/\.(md|ya?ml)$/, "");
3113
+ const phaseConfig = resolvePhaseConfig(
3114
+ opts.config,
3115
+ phaseName
3116
+ );
3117
+ const phaseHooksConfig = extractHooksConfig(phaseConfig);
3118
+ await recordPhaseCompletion(r.phaseFile, {
3119
+ exitReason: "sentinel",
3120
+ iterations: r.iterations,
3121
+ message: "completed via parallel lane",
3122
+ cumulativeCostUsd: r.costUsd
3123
+ }, phaseHooksConfig);
3124
+ }
3125
+ }
3126
+ const fatalResult = lanesResult.results.find((r) => r.fatal);
3127
+ if (fatalResult) {
3128
+ return {
3129
+ ...lastResult,
3130
+ iterations: totalIterations,
3131
+ cumulativeCostUsd: totalCostUsd,
3132
+ laneCosts,
3133
+ ...orphanedWorktrees.length > 0 ? { orphanedWorktrees } : {},
3134
+ ...verifyResults.length > 0 ? { verifyResults } : {},
3135
+ message: `${lastResult.message} (phase ${fatalResult.phaseFile}, ${phasesCompleted}/${uncheckedPhases.length} phases completed, total $${totalCostUsd.toFixed(2)})`
3136
+ };
3137
+ }
3138
+ } finally {
3139
+ for (const [phaseFile, handle] of handles) {
3140
+ await handle.cleanup().catch((err) => {
3141
+ log.warn(
3142
+ { phase: phaseFile, err: err instanceof Error ? err.message : String(err) },
3143
+ "worktree cleanup failed"
3144
+ );
3145
+ });
3146
+ }
3147
+ process.removeListener("exit", exitCleanup);
3148
+ }
3149
+ }
3150
+ if (!useYamlSpec) {
3151
+ const finalMainContent = _readFileSync(mainMdPath);
3152
+ const mainHasUnchecked = /^- \[ \]\s+id:/m.test(finalMainContent);
3153
+ if (mainHasUnchecked) {
3154
+ log.info("starting cross-cutting items");
3155
+ const crossCuttingPrompt = `You are executing the cross-cutting acceptance criteria from a multi-file plan's main.md. All phase files are complete. Work through every unchecked item (id: x1, x2, etc.) in main.md. Check each box as you complete it. Commit when done.
3156
+
3157
+ ## Overall goal
3158
+ ${goal}
3159
+
3160
+ ## Constraints
3161
+ ${constraints}
3162
+
3163
+ ## main.md
3164
+ ${finalMainContent}
3165
+
3166
+ Only work on the unchecked items in main.md's acceptance criteria. Phase items are already done. Do not ask questions.`;
3167
+ const adapterName = opts.adapter?.name;
3168
+ const cfgObj2 = opts.config;
3169
+ const models = cfgObj2?.models;
3170
+ const executionSpecifier = models?.execution;
3171
+ const executionModel = executionSpecifier ? resolveModel(executionSpecifier, adapterName ?? "opencode") : void 0;
3172
+ const crossAdapters = opts.config?.adapters;
3173
+ const crossOcAdapter = crossAdapters?.opencode;
3174
+ const crossCuttingAgentOverrides = crossOcAdapter?.agents;
3175
+ const crossResult = await _runRalphLoop({
3176
+ prompt: crossCuttingPrompt,
3177
+ cwd: opts.cwd,
3178
+ agentName: opts.fast ? "autopilot-fast" : void 0,
3179
+ model: executionModel,
3180
+ stallMs,
3181
+ config: opts.config,
3182
+ agentOverrides: crossCuttingAgentOverrides,
3183
+ logger: opts.logger,
3184
+ emitter,
3185
+ adapter: opts.adapter
3186
+ });
3187
+ totalIterations += crossResult.iterations;
3188
+ totalCostUsd += crossResult.cumulativeCostUsd ?? 0;
3189
+ lastResult = crossResult;
3190
+ }
3191
+ }
3192
+ log.info({ completed: phasesCompleted, total: uncheckedPhases.length, iterations: totalIterations, cost: totalCostUsd.toFixed(2) }, "all phases done");
3193
+ if (hooksConfig.post_run) {
3194
+ const hookResult = await runHook(hooksConfig.post_run, opts.cwd, verifyConfig.timeoutMs);
3195
+ if (!hookResult.ok) {
3196
+ log.warn({ output: hookResult.output }, "post_run hook failed");
3197
+ }
3198
+ }
3199
+ deleteCheckpoint(opts.cwd);
3200
+ let changesetPath;
3201
+ const changesetEnabled = cfgObj?.changeset !== false;
3202
+ if (changesetEnabled && !opts._deps && phasesCompleted === uncheckedPhases.length && uncheckedPhases.length > 0) {
3203
+ try {
3204
+ const { generateChangeset } = await import("./changeset-generator-HAHYSSUR.js");
3205
+ const changesetOpts = {
3206
+ packageName: cfgObj?.changeset_package,
3207
+ bumpLevel: cfgObj?.changeset_bump
3208
+ };
3209
+ const cs = await generateChangeset(opts.planPath, opts.cwd, changesetOpts);
3210
+ changesetPath = cs.path;
3211
+ log.info(
3212
+ { path: cs.path, bumpLevel: cs.bumpLevel },
3213
+ "changeset generated"
3214
+ );
3215
+ } catch (err) {
3216
+ const msg = err instanceof Error ? err.message : String(err);
3217
+ log.warn({ err: msg }, "changeset generation failed");
3218
+ }
3219
+ }
3220
+ let prUrl;
3221
+ if (!opts._deps && opts.ship && phasesCompleted === uncheckedPhases.length && uncheckedPhases.length > 0) {
3222
+ try {
3223
+ const { autoShip } = await import("./auto-ship-EVLBKHUZ.js");
3224
+ const shipResult = await autoShip({
3225
+ planPath: opts.planPath,
3226
+ repoRoot: opts.cwd
3227
+ });
3228
+ prUrl = shipResult.prUrl;
3229
+ log.info({ prUrl }, "PR opened");
3230
+ } catch (err) {
3231
+ const msg = err instanceof Error ? err.message : String(err);
3232
+ log.warn({ err: msg }, "auto-ship failed \u2014 run /ship to finalize manually");
3233
+ }
3234
+ } else if (!opts._deps && !opts.ship && phasesCompleted === uncheckedPhases.length && uncheckedPhases.length > 0) {
3235
+ log.info("all phases complete, run `/ship` to finalize");
3236
+ }
3237
+ return {
3238
+ ...lastResult,
3239
+ iterations: totalIterations,
3240
+ cumulativeCostUsd: totalCostUsd,
3241
+ // Per-lane breakdown (item 3.5) — only included when more than one
3242
+ // lane was used so sequential runs don't see noise in the debrief.
3243
+ ...laneCosts.size > 1 ? { laneCosts } : {},
3244
+ // Surviving worktrees (item 3.6) — surfaced for manual cleanup.
3245
+ ...orphanedWorktrees.length > 0 ? { orphanedWorktrees } : {},
3246
+ // Per-phase verify results (item 4.1) — surfaced for the debrief.
3247
+ ...verifyResults.length > 0 ? { verifyResults } : {},
3248
+ // Changeset path (item 4.6) and PR url (item 4.7).
3249
+ ...changesetPath ? { changesetPath } : {},
3250
+ ...prUrl ? { prUrl } : {},
3251
+ message: prUrl ? `${phasesCompleted}/${uncheckedPhases.length} phases completed in ${totalIterations} iterations, total $${totalCostUsd.toFixed(2)}, PR: ${prUrl}` : `${phasesCompleted}/${uncheckedPhases.length} phases completed in ${totalIterations} iterations, total $${totalCostUsd.toFixed(2)}`
3252
+ };
3253
+ }
3254
+
3255
+ export {
3256
+ formatElapsed,
3257
+ formatCost,
3258
+ createStatusHeartbeat,
3259
+ parsePlanState,
3260
+ parseItems,
3261
+ validateScope,
3262
+ getChangedFiles,
3263
+ MAX_ITERATIONS,
3264
+ STRUGGLE_THRESHOLD,
3265
+ TIMEOUT_MS,
3266
+ STALL_MS_BY_TIER,
3267
+ STALL_MS,
3268
+ MAX_ITERATIONS_PER_PHASE_BY_TIER,
3269
+ STATUS_INTERVAL_MS,
3270
+ detectSentinel,
3271
+ StruggleDetector,
3272
+ checkKillSwitch,
3273
+ writeCheckpoint,
3274
+ readCheckpoint,
3275
+ deleteCheckpoint,
3276
+ runRalphLoop,
3277
+ recordHead,
3278
+ resetSoft,
3279
+ markPhaseCompleted,
3280
+ buildConflictGraph,
3281
+ hasParallelism,
3282
+ runLanes,
3283
+ createWorktree,
3284
+ mergeWorktree,
3285
+ runVerifyCommands,
3286
+ validatePlan,
3287
+ runLoopSession
3288
+ };