@glrs-dev/cli 2.4.1 → 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 (53) hide show
  1. package/CHANGELOG.md +34 -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 +9 -0
  18. package/dist/node_modules/@glrs-dev/adapter-opencode/dist/index.js +33 -15
  19. package/dist/node_modules/@glrs-dev/adapter-opencode/package.json +1 -1
  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-DG3MVWVV.js → changeset-generator-HAHYSSUR.js} +2 -2
  22. package/dist/node_modules/@glrs-dev/autopilot/dist/{chunk-VITL2Z45.js → chunk-2X3CWH47.js} +578 -62
  23. package/dist/node_modules/@glrs-dev/autopilot/dist/{chunk-Q4ULU6ER.js → chunk-2ZQ6SBV3.js} +4 -2
  24. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-6JZQLIRP.js +781 -0
  25. package/dist/node_modules/@glrs-dev/autopilot/dist/{chunk-E7PWTRFO.js → chunk-AWRK6S6G.js} +2 -2
  26. package/dist/node_modules/@glrs-dev/autopilot/dist/{chunk-M2ZVBPWL.js → chunk-BLEIZHET.js} +1 -1
  27. package/dist/node_modules/@glrs-dev/autopilot/dist/{chunk-7OSEI5TF.js → chunk-GXXCEGDD.js} +3 -1
  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 +159 -9
  30. package/dist/node_modules/@glrs-dev/autopilot/dist/index.js +115 -35
  31. package/dist/node_modules/@glrs-dev/autopilot/dist/{logger-UITJGIZE.js → logger-3XLFMXLN.js} +1 -1
  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 +1 -1
  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 +1 -1
  50. package/dist/node_modules/@glrs-dev/autopilot/dist/auto-ship-LCT6LIH7.js +0 -7
  51. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-ZNJWARTM.js +0 -449
  52. package/dist/node_modules/@glrs-dev/autopilot/dist/loop-session-XKL3NHUA.js +0 -8
  53. package/dist/node_modules/@glrs-dev/autopilot/dist/plan-enrichment-D3RPJR2J.js +0 -14
@@ -1,7 +1,10 @@
1
+ import {
2
+ resolveModel
3
+ } from "./chunk-S34HOCZ4.js";
1
4
  import {
2
5
  childLogger,
3
6
  createAutopilotLogger
4
- } from "./chunk-Q4ULU6ER.js";
7
+ } from "./chunk-2ZQ6SBV3.js";
5
8
  import {
6
9
  detectSpecPhases,
7
10
  filterUncheckedSpecPhases,
@@ -12,7 +15,7 @@ import {
12
15
  readSpecGoal,
13
16
  validateMainSpec,
14
17
  validatePhaseSpec
15
- } from "./chunk-7OSEI5TF.js";
18
+ } from "./chunk-GXXCEGDD.js";
16
19
 
17
20
  // src/loop-session.ts
18
21
  import * as fs7 from "fs";
@@ -348,6 +351,7 @@ var MAX_ITERATIONS_PER_PHASE_BY_TIER = {
348
351
  "autopilot-execute": 10,
349
352
  fast: 10
350
353
  };
354
+ var MAX_ITERATIONS_PER_ITEM = 5;
351
355
  var KILL_SWITCH_PATH = ".agent/autopilot-disable";
352
356
  var SENTINEL_TAG = "<autopilot-done>";
353
357
  var STATUS_INTERVAL_MS = (() => {
@@ -462,7 +466,10 @@ function formatSlackMessage(event) {
462
466
  function isSlackWebhookUrl(url) {
463
467
  return url.includes("hooks.slack.com/");
464
468
  }
465
- async function notifyWebhook(url, event) {
469
+ async function notifyWebhook(url, event, allowedEvents) {
470
+ if (allowedEvents && !allowedEvents.includes(event.event)) {
471
+ return;
472
+ }
466
473
  try {
467
474
  const body = isSlackWebhookUrl(url) ? JSON.stringify(formatSlackMessage(event)) : JSON.stringify(event);
468
475
  const response = await fetch(url, {
@@ -712,13 +719,36 @@ async function getHeadSha(cwd) {
712
719
  return "HEAD";
713
720
  }
714
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
+ }
715
736
  async function runRalphLoop(opts) {
716
737
  process.env["GLRS_AUTOPILOT_HEADLESS"] = "1";
717
738
  const maxIterations = opts.maxIterations ?? MAX_ITERATIONS;
718
739
  const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
719
740
  const tier = opts.agentName === "autopilot-fast" ? "autopilot-execute" : "deep";
720
- const stallMs = opts.stallMs ?? STALL_MS_BY_TIER[tier];
741
+ const cfgObj = opts.config;
742
+ const cfgStallMs = cfgObj?.stall_timeout;
743
+ const stallMs = opts.stallMs ?? cfgStallMs ?? STALL_MS_BY_TIER[tier];
721
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;
722
752
  if (!opts.adapter) {
723
753
  throw new Error("runRalphLoop: adapter is required");
724
754
  }
@@ -727,8 +757,8 @@ async function runRalphLoop(opts) {
727
757
  const struggle = new StruggleDetector(struggleThreshold);
728
758
  const startTime = Date.now();
729
759
  const notify = (event) => {
730
- if (opts.notifyUrl) {
731
- notifyWebhook(opts.notifyUrl, event).catch(() => {
760
+ if (resolvedNotifyUrl) {
761
+ notifyWebhook(resolvedNotifyUrl, event, finalNotifyEvents).catch(() => {
732
762
  });
733
763
  }
734
764
  };
@@ -773,7 +803,7 @@ async function runRalphLoop(opts) {
773
803
  log.info({ file: autopilotLog.logFilePath }, `Logging to ${autopilotLog.logFilePath}`);
774
804
  }
775
805
  log.info({ cwd: opts.cwd, maxIterations, timeoutMs }, `Starting agent (${adapter.name})`);
776
- const handle = await adapter.start({ cwd: opts.cwd });
806
+ const handle = await adapter.start({ cwd: opts.cwd, agents: opts.agentOverrides });
777
807
  log.info({ agentId: handle.id }, "Agent ready");
778
808
  const abort = new AbortController();
779
809
  const timeoutHandle = setTimeout(() => {
@@ -808,13 +838,14 @@ async function runRalphLoop(opts) {
808
838
  try {
809
839
  const agentName = opts.agentName ?? "autopilot-prime";
810
840
  const tierLabel = agentName === "autopilot-fast" ? "autopilot-execute tier" : "deep tier";
811
- sessionId = await adapter.createSession(handle, { agentName });
841
+ sessionId = await adapter.createSession(handle, { agentName, model: opts.model });
812
842
  log.info({ sessionId, agentName, tier: tierLabel }, `Session created with ${agentName} (${tierLabel})`);
843
+ const statusFileEnabled = opts.config?.status_file !== false;
813
844
  heartbeat = createStatusHeartbeat({
814
845
  logger: statusLog,
815
846
  intervalMs: STATUS_INTERVAL_MS,
816
847
  pollCost: async () => adapter.getSessionCost(handle, sessionId),
817
- statusFilePath: join4(opts.cwd, ".agent", "autopilot-status.json")
848
+ statusFilePath: statusFileEnabled ? join4(opts.cwd, ".agent", "autopilot-status.json") : void 0
818
849
  });
819
850
  heartbeat.start();
820
851
  for (let iteration = 1; iteration <= maxIterations; iteration++) {
@@ -832,6 +863,7 @@ async function runRalphLoop(opts) {
832
863
  iterations: iteration - 1,
833
864
  message: `Kill switch active (.agent/autopilot-disable exists). Stopping after ${iteration - 1} iteration(s).`,
834
865
  sessionId,
866
+ cumulativeCostUsd: heartbeat?.getState().cumulativeCostUsd,
835
867
  agentHandle: transferHandle()
836
868
  };
837
869
  }
@@ -849,6 +881,7 @@ async function runRalphLoop(opts) {
849
881
  iterations: iteration - 1,
850
882
  message: `Total timeout (${timeoutMs}ms) exceeded after ${iteration - 1} iteration(s).`,
851
883
  sessionId,
884
+ cumulativeCostUsd: heartbeat?.getState().cumulativeCostUsd,
852
885
  agentHandle: transferHandle()
853
886
  };
854
887
  }
@@ -1011,6 +1044,7 @@ async function runRalphLoop(opts) {
1011
1044
  iterations: iteration,
1012
1045
  message: `Aborted after ${iteration} iteration(s) (total timeout exceeded).`,
1013
1046
  sessionId,
1047
+ cumulativeCostUsd: heartbeat?.getState().cumulativeCostUsd,
1014
1048
  agentHandle: transferHandle()
1015
1049
  };
1016
1050
  }
@@ -1028,6 +1062,7 @@ async function runRalphLoop(opts) {
1028
1062
  iterations: iteration,
1029
1063
  message: `Iteration ${iteration} stalled for ${result.stallMs}ms with no idle signal.`,
1030
1064
  sessionId,
1065
+ cumulativeCostUsd: heartbeat?.getState().cumulativeCostUsd,
1031
1066
  agentHandle: transferHandle()
1032
1067
  };
1033
1068
  }
@@ -1093,6 +1128,7 @@ async function runRalphLoop(opts) {
1093
1128
  iterations: iteration,
1094
1129
  message: `Error in iteration ${iteration}: ${result.message}`,
1095
1130
  sessionId,
1131
+ cumulativeCostUsd: heartbeat?.getState().cumulativeCostUsd,
1096
1132
  agentHandle: transferHandle()
1097
1133
  };
1098
1134
  }
@@ -1118,6 +1154,7 @@ async function runRalphLoop(opts) {
1118
1154
  iterations: iteration,
1119
1155
  message: `Agent emitted <autopilot-done> at iteration ${iteration}.`,
1120
1156
  sessionId,
1157
+ cumulativeCostUsd: heartbeat?.getState().cumulativeCostUsd,
1121
1158
  agentHandle: transferHandle()
1122
1159
  };
1123
1160
  }
@@ -1196,6 +1233,10 @@ async function runRalphLoop(opts) {
1196
1233
  commitSubject = logOut.trim().replace(/^[0-9a-f]+ /, "");
1197
1234
  } catch {
1198
1235
  }
1236
+ if (commitSubject && commitPrefix) {
1237
+ amendCommitWithPrefix(opts.cwd, commitPrefix).catch(() => {
1238
+ });
1239
+ }
1199
1240
  if (commitSubject) {
1200
1241
  log.info(`${lanePrefix}commit: ${commitSubject}`);
1201
1242
  }
@@ -1289,6 +1330,7 @@ async function runRalphLoop(opts) {
1289
1330
  iterations: iteration,
1290
1331
  message: `Agent made no filesystem progress for ${struggleThreshold} consecutive iteration(s). Stopping at iteration ${iteration}.`,
1291
1332
  sessionId,
1333
+ cumulativeCostUsd: heartbeat?.getState().cumulativeCostUsd,
1292
1334
  agentHandle: transferHandle()
1293
1335
  };
1294
1336
  }
@@ -1306,6 +1348,7 @@ async function runRalphLoop(opts) {
1306
1348
  iterations: maxIterations,
1307
1349
  message: `Reached maximum iterations (${maxIterations}). Stopping.`,
1308
1350
  sessionId,
1351
+ cumulativeCostUsd: heartbeat?.getState().cumulativeCostUsd,
1309
1352
  agentHandle: transferHandle()
1310
1353
  };
1311
1354
  } finally {
@@ -1318,12 +1361,20 @@ async function runRalphLoop(opts) {
1318
1361
  try {
1319
1362
  const { stdout: porcelain } = await execFile2("git", ["status", "--porcelain"], { cwd: opts.cwd });
1320
1363
  if (porcelain.trim().length > 0) {
1321
- log.info({ signal: interruptedSignal }, "Committing WIP before exit");
1322
- try {
1323
- await execFile2("git", ["add", "-A"], { cwd: opts.cwd });
1324
- await execFile2("git", ["commit", "-m", "[WIP] autopilot interrupted"], { cwd: opts.cwd });
1325
- } catch (err) {
1326
- log.warn({ err: err instanceof Error ? err.message : String(err) }, "WIP commit failed (hooks may have rejected)");
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
+ );
1327
1378
  }
1328
1379
  }
1329
1380
  } catch (err) {
@@ -1387,6 +1438,28 @@ function atomicWriteFileSync(target, content) {
1387
1438
  fs5.writeFileSync(tmp, content, "utf-8");
1388
1439
  fs5.renameSync(tmp, target);
1389
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
+ }
1390
1463
  function markPhaseCompleted(planDir, phaseFile) {
1391
1464
  const mainPath = path4.join(planDir, "spec", "main.yaml");
1392
1465
  try {
@@ -1652,6 +1725,22 @@ import { execFile as execFileCb6 } from "child_process";
1652
1725
  import { promisify as promisify6 } from "util";
1653
1726
  var execFileDefault3 = promisify6(execFileCb6);
1654
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
+ }
1655
1744
  async function runVerifyCommands(items, cwd, opts = {}) {
1656
1745
  const execFile4 = opts._deps?.execFile ?? execFileDefault3;
1657
1746
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@@ -1659,11 +1748,12 @@ async function runVerifyCommands(items, cwd, opts = {}) {
1659
1748
  for (const item of items) {
1660
1749
  const command = item.verify?.trim();
1661
1750
  if (!command) continue;
1751
+ const itemTimeoutMs = getTimeoutForProofType(item.proof_type, timeoutMs);
1662
1752
  const start = Date.now();
1663
1753
  try {
1664
1754
  const { stdout, stderr } = await execFile4("/bin/sh", ["-c", command], {
1665
1755
  cwd,
1666
- signal: AbortSignal.timeout(timeoutMs),
1756
+ signal: AbortSignal.timeout(itemTimeoutMs),
1667
1757
  // Capture as much output as the command produces — verify
1668
1758
  // commands are typically test runs; truncating their output
1669
1759
  // would hide the failure detail we want in the next prompt.
@@ -1683,7 +1773,7 @@ async function runVerifyCommands(items, cwd, opts = {}) {
1683
1773
  const stdout = e.stdout !== void 0 ? typeof e.stdout === "string" ? e.stdout : e.stdout.toString() : "";
1684
1774
  let stderr = e.stderr !== void 0 ? typeof e.stderr === "string" ? e.stderr : e.stderr.toString() : "";
1685
1775
  if (isTimeout) {
1686
- stderr = (stderr ? stderr + "\n" : "") + `[verify-runner] command timed out after ${Math.round(timeoutMs / 1e3)}s`;
1776
+ stderr = (stderr ? stderr + "\n" : "") + `[verify-runner] command timed out after ${Math.round(itemTimeoutMs / 1e3)}s`;
1687
1777
  } else if (!stderr && e.message) {
1688
1778
  stderr = e.message;
1689
1779
  }
@@ -1700,6 +1790,37 @@ async function runVerifyCommands(items, cwd, opts = {}) {
1700
1790
  return results;
1701
1791
  }
1702
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
+
1703
1824
  // src/plan-validator.ts
1704
1825
  import * as fs6 from "fs";
1705
1826
  import * as path6 from "path";
@@ -1900,7 +2021,76 @@ function checkItemsSoft(content, file, warnings) {
1900
2021
  }
1901
2022
  }
1902
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
+
1903
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
+ }
1904
2094
  function extractSection(content, sectionName) {
1905
2095
  const re = new RegExp(
1906
2096
  `^## ${sectionName}\\s*\\n([\\s\\S]*?)(?=^## |$)`,
@@ -2029,14 +2219,40 @@ async function runLoopSession(opts) {
2029
2219
  })();
2030
2220
  if (!isDirectory) {
2031
2221
  const prompt = `Work the plan at ${opts.planPath}. Complete all items in ## Acceptance criteria. Mark items done as they complete.`;
2032
- return _runRalphLoop({ prompt, cwd: opts.cwd, agentName: opts.fast ? "autopilot-fast" : void 0, logger: opts.logger, emitter, adapter: opts.adapter });
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
+ });
2033
2243
  }
2034
2244
  const log = opts.logger ? opts.logger.root.child({ component: "autopilot.orchestrator" }) : pino(
2035
2245
  { level: "info", timestamp: pino.stdTimeFunctions.isoTime },
2036
2246
  pino.destination({ fd: 2, sync: false })
2037
2247
  ).child({ component: "autopilot.orchestrator" });
2038
2248
  const tier = opts.fast ? "autopilot-execute" : "deep";
2039
- const maxIterationsPerPhase = opts.maxIterationsPerPhase ?? MAX_ITERATIONS_PER_PHASE_BY_TIER[tier];
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);
2040
2256
  const mainMdPath = path7.join(opts.planPath, "main.md");
2041
2257
  const useYamlSpec = hasSpec(opts.planPath);
2042
2258
  let goal;
@@ -2153,11 +2369,17 @@ async function runLoopSession(opts) {
2153
2369
  iterations: 0,
2154
2370
  message: "No phases to execute."
2155
2371
  };
2156
- const MAX_ITERATIONS_PER_ITEM = 5;
2372
+ const cfgMaxIterPerItem = opts.config?.max_iterations_per_item;
2373
+ const maxIterationsPerItem = cfgMaxIterPerItem ?? MAX_ITERATIONS_PER_ITEM;
2157
2374
  const runItemsForPhase = async (args) => {
2158
2375
  const { phaseFile, phasePath, laneId, runCwd, useParallel: useParallel2 } = args;
2159
2376
  const phaseContent = args.readFileSync(phasePath);
2160
- const items = (useYamlSpec ? parseSpecItems(phasePath) : parseItems(phaseContent)).filter((it) => !it.checked);
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
+ );
2161
2383
  if (items.length === 0) {
2162
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.
2163
2385
 
@@ -2171,11 +2393,20 @@ ${constraints}
2171
2393
  ${phaseContent}
2172
2394
 
2173
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;
2174
2401
  return args.runRalphLoop({
2175
2402
  prompt,
2176
2403
  cwd: runCwd,
2177
2404
  agentName: "autopilot-fast",
2405
+ model: executionModel,
2178
2406
  maxIterations: maxIterationsPerPhase,
2407
+ ...args.stallMs ? { stallMs: args.stallMs } : {},
2408
+ config: args.config,
2409
+ agentOverrides: args.agentOverrides,
2179
2410
  laneId: useParallel2 ? laneId : void 0,
2180
2411
  logger: args.logger,
2181
2412
  emitter: args.emitter,
@@ -2185,7 +2416,7 @@ Do not work on items from other phases. Do not ask questions.`;
2185
2416
  const perItemCap = Math.max(
2186
2417
  1,
2187
2418
  Math.min(
2188
- MAX_ITERATIONS_PER_ITEM,
2419
+ maxIterationsPerItem,
2189
2420
  Math.ceil(maxIterationsPerPhase / items.length)
2190
2421
  )
2191
2422
  );
@@ -2199,7 +2430,48 @@ Do not work on items from other phases. Do not ask questions.`;
2199
2430
  for (const item of items) {
2200
2431
  const filesList = item.files.map((f) => `${f.path}${f.isNew ? " (CREATE)" : " (EDIT)"}`).join(", ");
2201
2432
  const verify = item.verify?.trim() || "(no verify command declared)";
2202
- const 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.
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.
2203
2475
 
2204
2476
  ## Overall goal
2205
2477
  ${goal}
@@ -2213,7 +2485,7 @@ ${constraints}
2213
2485
  files: ${filesList || "(none declared)"}
2214
2486
  verify: ${verify}
2215
2487
 
2216
- ## Structured context
2488
+ ` + enrichmentBlock + `## Structured context
2217
2489
 
2218
2490
  Files you may touch (ONLY these):
2219
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)") + `
@@ -2221,16 +2493,61 @@ Files you may touch (ONLY these):
2221
2493
  Verify command (must exit 0):
2222
2494
  - ${verify}
2223
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
+
2224
2502
  Non-goals:
2503
+ - Do NOT skip the RED phase \u2014 always write the test first.
2225
2504
  - Do NOT modify files outside the list above.
2226
2505
  - Do NOT work on items other than ${item.id}.
2227
2506
 
2228
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;
2229
2542
  const itemResult = await args.runRalphLoop({
2230
2543
  prompt: itemPrompt,
2231
2544
  cwd: runCwd,
2232
2545
  agentName: "autopilot-fast",
2546
+ model: executionModel,
2233
2547
  maxIterations: perItemCap,
2548
+ ...args.stallMs ? { stallMs: args.stallMs } : {},
2549
+ config: args.config,
2550
+ agentOverrides: args.agentOverrides,
2234
2551
  laneId: useParallel2 ? laneId : void 0,
2235
2552
  logger: args.logger,
2236
2553
  emitter: args.emitter,
@@ -2261,6 +2578,48 @@ When done: mark the checkbox for item ${item.id} in ${phaseFile} as [x], commit,
2261
2578
  message: `Item ${item.id} failed: ${itemResult.message}`
2262
2579
  };
2263
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
+ }
2264
2623
  }
2265
2624
  return {
2266
2625
  ...lastItemResult,
@@ -2286,7 +2645,17 @@ When done: mark the checkbox for item ${item.id} in ${phaseFile} as [x], commit,
2286
2645
  current: phasesCompleted + 1,
2287
2646
  total: uncheckedPhases.length
2288
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;
2289
2657
  let result;
2658
+ const phaseVerifyConfig = extractVerifyConfig(phaseConfig);
2290
2659
  if (opts.fast) {
2291
2660
  result = await runItemsForPhase({
2292
2661
  phaseFile,
@@ -2295,10 +2664,16 @@ When done: mark the checkbox for item ${item.id} in ${phaseFile} as [x], commit,
2295
2664
  runCwd,
2296
2665
  runRalphLoop: _runRalphLoop,
2297
2666
  readFileSync: _readFileSync,
2667
+ writeFileSync: _writeFileSync,
2668
+ planPath: opts.planPath,
2298
2669
  useParallel,
2670
+ stallMs,
2299
2671
  logger: opts.logger,
2300
2672
  emitter,
2301
- adapter: opts.adapter
2673
+ adapter: opts.adapter,
2674
+ config: opts.config,
2675
+ agentOverrides: phaseAgentOverrides,
2676
+ verifyConfig: phaseVerifyConfig
2302
2677
  });
2303
2678
  } else {
2304
2679
  const retrySection = retryContext ? `
@@ -2321,11 +2696,20 @@ ${constraints}
2321
2696
  ${phaseContent}
2322
2697
  ` + retrySection + `
2323
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;
2324
2704
  result = await _runRalphLoop({
2325
2705
  prompt,
2326
2706
  cwd: runCwd,
2327
2707
  agentName: void 0,
2708
+ model: executionModel,
2328
2709
  maxIterations: maxIterationsPerPhase,
2710
+ stallMs,
2711
+ config: opts.config,
2712
+ agentOverrides: phaseAgentOverrides,
2329
2713
  laneId: useParallel ? laneId : void 0,
2330
2714
  logger: opts.logger,
2331
2715
  emitter,
@@ -2340,9 +2724,17 @@ Do not work on items from other phases. Do not ask questions \u2014 pick sensibl
2340
2724
  const updatedPhaseContent = _readFileSync(phasePath);
2341
2725
  let phaseComplete = useYamlSpec ? (() => {
2342
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
+ );
2343
2732
  return yamlItems.length > 0 && yamlItems.every((i) => i.checked);
2344
2733
  })() : isPhaseComplete(updatedPhaseContent);
2345
- if (phaseComplete) {
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) {
2346
2738
  const items = useYamlSpec ? parseSpecItems(phasePath) : parseItems(updatedPhaseContent);
2347
2739
  const itemsWithVerify = items.filter((it) => it.verify?.trim());
2348
2740
  if (itemsWithVerify.length > 0) {
@@ -2356,7 +2748,9 @@ Do not work on items from other phases. Do not ask questions \u2014 pick sensibl
2356
2748
  phase: phaseFile,
2357
2749
  itemCount: itemsWithVerify.length
2358
2750
  });
2359
- const phaseVerify = await _runVerifyCommands(itemsWithVerify, runCwd);
2751
+ const phaseVerify = await _runVerifyCommands(itemsWithVerify, runCwd, {
2752
+ timeoutMs: verifyConfig2.timeoutMs
2753
+ });
2360
2754
  verifyResults.push({ phaseFile, results: phaseVerify });
2361
2755
  const failed = phaseVerify.filter((r) => !r.passed);
2362
2756
  for (const r of phaseVerify) {
@@ -2371,6 +2765,7 @@ Do not work on items from other phases. Do not ask questions \u2014 pick sensibl
2371
2765
  });
2372
2766
  }
2373
2767
  if (failed.length > 0) {
2768
+ const failedItemIds = failed.map((f) => f.itemId);
2374
2769
  for (const f of failed) {
2375
2770
  log.warn(
2376
2771
  {
@@ -2382,6 +2777,20 @@ Do not work on items from other phases. Do not ask questions \u2014 pick sensibl
2382
2777
  "verify command failed"
2383
2778
  );
2384
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
+ }
2385
2794
  phaseComplete = false;
2386
2795
  verifyFailureSummary = failed.map((f) => `- \`${f.command}\` failed (item ${f.itemId}): ${f.stderr.split("\n").slice(-3).join(" ").slice(0, 200)}`).join("\n");
2387
2796
  } else {
@@ -2401,13 +2810,16 @@ Do not work on items from other phases. Do not ask questions \u2014 pick sensibl
2401
2810
  }
2402
2811
  if (!phaseComplete && !SUCCESS_REASONS.has(result.exitReason)) {
2403
2812
  log.warn({ phase: phaseFile, exitReason: result.exitReason }, "phase failed");
2404
- if (opts.fast && preHeadSha && preHeadSha !== "HEAD") {
2813
+ const rollbackConfig = cfgObj?.rollback_on_failure ?? "soft";
2814
+ if (rollbackConfig !== "off" && opts.fast && preHeadSha && preHeadSha !== "HEAD") {
2405
2815
  const ok = await resetSoft(runCwd, preHeadSha, {
2406
2816
  onWarn: (m) => log.warn(m)
2407
2817
  });
2408
2818
  if (ok) {
2409
2819
  log.info({ ref: preHeadSha.slice(0, 8) }, "soft-reset to pre-phase state");
2410
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");
2411
2823
  }
2412
2824
  emitter?.emitEvent({
2413
2825
  type: "phase:done",
@@ -2418,6 +2830,12 @@ Do not work on items from other phases. Do not ask questions \u2014 pick sensibl
2418
2830
  iterations: result.iterations,
2419
2831
  costUsd: costThisPhase
2420
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
+ }
2421
2839
  return {
2422
2840
  phaseFile,
2423
2841
  laneId,
@@ -2434,13 +2852,16 @@ Do not work on items from other phases. Do not ask questions \u2014 pick sensibl
2434
2852
  { phase: phaseFile, max: maxIterationsPerPhase },
2435
2853
  "phase budget exhausted \u2014 moving on"
2436
2854
  );
2437
- writeCheckpoint(opts.cwd, {
2438
- planPath: opts.planPath,
2439
- completedPhases: [...completedPhasesAcc],
2440
- totalCostUsd,
2441
- totalIterations,
2442
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2443
- });
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
+ }
2444
2865
  }
2445
2866
  emitter?.emitEvent({
2446
2867
  type: "phase:done",
@@ -2463,7 +2884,7 @@ Do not work on items from other phases. Do not ask questions \u2014 pick sensibl
2463
2884
  verifyFailures: verifyFailureSummary
2464
2885
  };
2465
2886
  };
2466
- const recordPhaseCompletion = (phaseFile, result) => {
2887
+ const recordPhaseCompletion = async (phaseFile, result, phaseHooksConfig) => {
2467
2888
  phasesCompleted++;
2468
2889
  completedPhasesAcc.push(phaseFile);
2469
2890
  if (useYamlSpec) {
@@ -2472,13 +2893,16 @@ Do not work on items from other phases. Do not ask questions \u2014 pick sensibl
2472
2893
  const updatedMain = markPhaseChecked(_readFileSync(mainMdPath), phaseFile);
2473
2894
  _writeFileSync(mainMdPath, updatedMain);
2474
2895
  }
2475
- writeCheckpoint(opts.cwd, {
2476
- planPath: opts.planPath,
2477
- completedPhases: [...completedPhasesAcc],
2478
- totalCostUsd,
2479
- totalIterations,
2480
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2481
- });
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
+ }
2482
2906
  log.info(
2483
2907
  {
2484
2908
  phase: phaseFile,
@@ -2489,18 +2913,28 @@ Do not work on items from other phases. Do not ask questions \u2014 pick sensibl
2489
2913
  },
2490
2914
  "phase complete"
2491
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
+ }
2492
2923
  };
2493
2924
  if (!useParallel) {
2494
2925
  for (const phaseFile of uncheckedPhases) {
2495
2926
  if (opts.signal?.aborted) {
2496
2927
  log.info({ completed: phasesCompleted, remaining: uncheckedPhases.length }, "abort signal received \u2014 writing checkpoint and stopping");
2497
- writeCheckpoint(opts.cwd, {
2498
- planPath: opts.planPath,
2499
- completedPhases: [...completedPhasesAcc],
2500
- totalCostUsd,
2501
- totalIterations,
2502
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2503
- });
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
+ }
2504
2938
  return {
2505
2939
  exitReason: "aborted",
2506
2940
  iterations: totalIterations,
@@ -2508,13 +2942,20 @@ Do not work on items from other phases. Do not ask questions \u2014 pick sensibl
2508
2942
  message: `Aborted after ${phasesCompleted}/${uncheckedPhases.length} phases completed`
2509
2943
  };
2510
2944
  }
2511
- const MAX_PHASE_RETRIES = opts.maxPhaseRetries ?? (opts._deps ? 1 : 3);
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;
2512
2953
  let attempt = 0;
2513
2954
  let phaseSuccess = false;
2514
2955
  let lastVerifyFailures;
2515
- while (attempt < MAX_PHASE_RETRIES && !phaseSuccess) {
2956
+ while (attempt < effectiveMaxRetries && !phaseSuccess) {
2516
2957
  attempt++;
2517
- const isEscalation = attempt === MAX_PHASE_RETRIES && attempt > 1 && opts.fast;
2958
+ const isEscalation = attempt === effectiveMaxRetries && attempt > 1 && opts.fast;
2518
2959
  if (attempt > 1) {
2519
2960
  log.info(
2520
2961
  { phase: phaseFile, attempt, escalate: isEscalation },
@@ -2533,13 +2974,25 @@ Do not work on items from other phases. Do not ask questions \u2014 pick sensibl
2533
2974
  if (isEscalation) {
2534
2975
  opts.fast = false;
2535
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
+ }
2536
2989
  const r = await runPhaseInner(phaseFile, "lane-1", opts.cwd, lastVerifyFailures);
2537
2990
  if (isEscalation) {
2538
2991
  opts.fast = originalFast;
2539
2992
  }
2540
2993
  lastVerifyFailures = r.verifyFailures;
2541
2994
  if (r.phaseComplete) {
2542
- recordPhaseCompletion(phaseFile, r.phaseLoopResult);
2995
+ await recordPhaseCompletion(phaseFile, r.phaseLoopResult, phaseHooksConfig);
2543
2996
  phaseSuccess = true;
2544
2997
  break;
2545
2998
  }
@@ -2551,7 +3004,7 @@ Do not work on items from other phases. Do not ask questions \u2014 pick sensibl
2551
3004
  message: `${r.phaseLoopResult.message} (phase ${phaseFile}, ${phasesCompleted}/${uncheckedPhases.length} phases completed, total $${totalCostUsd.toFixed(2)})`
2552
3005
  };
2553
3006
  }
2554
- if (attempt >= MAX_PHASE_RETRIES) {
3007
+ if (attempt >= effectiveMaxRetries) {
2555
3008
  log.warn({ phase: phaseFile, attempts: attempt }, "phase exhausted retries \u2014 moving on");
2556
3009
  }
2557
3010
  }
@@ -2601,6 +3054,33 @@ Do not work on items from other phases. Do not ask questions \u2014 pick sensibl
2601
3054
  log.warn({ phase: phaseFile, err: msg }, "worktree create failed \u2014 falling back to main cwd");
2602
3055
  }
2603
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
+ }
2604
3084
  const result = await runPhaseInner(phaseFile, laneId, runCwd);
2605
3085
  if (handle) {
2606
3086
  handles.set(phaseFile, handle);
@@ -2629,12 +3109,18 @@ Do not work on items from other phases. Do not ask questions \u2014 pick sensibl
2629
3109
  });
2630
3110
  for (const r of lanesResult.results) {
2631
3111
  if (r.ok) {
2632
- recordPhaseCompletion(r.phaseFile, {
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, {
2633
3119
  exitReason: "sentinel",
2634
3120
  iterations: r.iterations,
2635
3121
  message: "completed via parallel lane",
2636
3122
  cumulativeCostUsd: r.costUsd
2637
- });
3123
+ }, phaseHooksConfig);
2638
3124
  }
2639
3125
  }
2640
3126
  const fatalResult = lanesResult.results.find((r) => r.fatal);
@@ -2678,19 +3164,49 @@ ${constraints}
2678
3164
  ${finalMainContent}
2679
3165
 
2680
3166
  Only work on the unchecked items in main.md's acceptance criteria. Phase items are already done. Do not ask questions.`;
2681
- const crossResult = await _runRalphLoop({ prompt: crossCuttingPrompt, cwd: opts.cwd, agentName: opts.fast ? "autopilot-fast" : void 0, logger: opts.logger, emitter, adapter: opts.adapter });
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
+ });
2682
3187
  totalIterations += crossResult.iterations;
2683
3188
  totalCostUsd += crossResult.cumulativeCostUsd ?? 0;
2684
3189
  lastResult = crossResult;
2685
3190
  }
2686
3191
  }
2687
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
+ }
2688
3199
  deleteCheckpoint(opts.cwd);
2689
3200
  let changesetPath;
2690
- if (!opts._deps && phasesCompleted === uncheckedPhases.length && uncheckedPhases.length > 0) {
3201
+ const changesetEnabled = cfgObj?.changeset !== false;
3202
+ if (changesetEnabled && !opts._deps && phasesCompleted === uncheckedPhases.length && uncheckedPhases.length > 0) {
2691
3203
  try {
2692
- const { generateChangeset } = await import("./changeset-generator-DG3MVWVV.js");
2693
- const cs = await generateChangeset(opts.planPath, opts.cwd);
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);
2694
3210
  changesetPath = cs.path;
2695
3211
  log.info(
2696
3212
  { path: cs.path, bumpLevel: cs.bumpLevel },
@@ -2704,7 +3220,7 @@ Only work on the unchecked items in main.md's acceptance criteria. Phase items a
2704
3220
  let prUrl;
2705
3221
  if (!opts._deps && opts.ship && phasesCompleted === uncheckedPhases.length && uncheckedPhases.length > 0) {
2706
3222
  try {
2707
- const { autoShip } = await import("./auto-ship-LCT6LIH7.js");
3223
+ const { autoShip } = await import("./auto-ship-EVLBKHUZ.js");
2708
3224
  const shipResult = await autoShip({
2709
3225
  planPath: opts.planPath,
2710
3226
  repoRoot: opts.cwd