@open-tomato/rafa 0.40.0 → 0.41.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.
@@ -4,6 +4,8 @@ import {
4
4
  escapeBlockerText,
5
5
  findNextTask,
6
6
  insertTrackerTask,
7
+ listOpenTasks,
8
+ listTrackerTasks,
7
9
  loadPlanPrerequisites,
8
10
  malformedPrerequisiteLines,
9
11
  parsePlan,
@@ -11,10 +13,11 @@ import {
11
13
  prerequisitesPathForPlan,
12
14
  renderInjection,
13
15
  runPreflight,
16
+ splitBlockerComment,
14
17
  trackerPathFor,
15
18
  updateTrackerLine,
16
19
  writeTrackerBlocker
17
- } from "./index-krpmxpkh.js";
20
+ } from "./index-zg0qsb5b.js";
18
21
  import {
19
22
  ACROSS_STORES_ORDER,
20
23
  CORE_ADAPTER_REGISTRY,
@@ -106,10 +109,12 @@ import {
106
109
  runClaude,
107
110
  runClaudeCaptured,
108
111
  runsDir,
112
+ samePlan,
109
113
  scanLayout,
110
114
  selectEffortStore,
111
115
  selectSync,
112
116
  selfAndAncestors,
117
+ sessionDecisions,
113
118
  sessionFilePath,
114
119
  sessionPhase,
115
120
  spawnClaude,
@@ -129,7 +134,7 @@ import {
129
134
  writeNames,
130
135
  writePositionFile,
131
136
  writeSqliteStore
132
- } from "./index-8veaj0t9.js";
137
+ } from "./index-cd0wzc12.js";
133
138
  import {
134
139
  CHANGE_LEVELS,
135
140
  CONFIG_DEFAULTS,
@@ -146,6 +151,7 @@ import {
146
151
  collisionMessage,
147
152
  configFilePath,
148
153
  findTierItem,
154
+ isLoopRetryCount,
149
155
  optionalPrerequisite,
150
156
  parseConfigText,
151
157
  parseReport,
@@ -158,7 +164,7 @@ import {
158
164
  requiredPrerequisite,
159
165
  resolveTiers,
160
166
  stampPrompt
161
- } from "./index-334y39tn.js";
167
+ } from "./index-3jpz9nr3.js";
162
168
  import {
163
169
  promotable
164
170
  } from "./index-d6d0942q.js";
@@ -13832,7 +13838,7 @@ var CHECKOUT_ROSTER_TIMEOUT_MS = 30000;
13832
13838
  var DESCRIBE_LINE = `bun ${RAFA_ENTRY} describe --output=json`;
13833
13839
  async function coreRoster() {
13834
13840
  const [{ CORE_REGISTRY }, { describeRegistry }] = await Promise.all([
13835
- import("./index-j7kjj0dd.js"),
13841
+ import("./index-snf04fpt.js"),
13836
13842
  import("./describe-pwhh0vws.js")
13837
13843
  ]);
13838
13844
  return describeRegistry(CORE_REGISTRY, version);
@@ -19475,7 +19481,7 @@ import {
19475
19481
  chmodSync,
19476
19482
  copyFileSync,
19477
19483
  existsSync as existsSync19,
19478
- mkdirSync as mkdirSync9,
19484
+ mkdirSync as mkdirSync10,
19479
19485
  readdirSync as readdirSync11,
19480
19486
  readFileSync as readFileSync26,
19481
19487
  realpathSync as realpathSync2,
@@ -19484,16 +19490,206 @@ import {
19484
19490
  statSync as statSync11,
19485
19491
  symlinkSync
19486
19492
  } from "fs";
19487
- import { basename as basename7, delimiter as delimiter3, dirname as dirname10, join as join20, relative as relative6, resolve as resolve16 } from "path";
19493
+ import { basename as basename7, delimiter as delimiter3, dirname as dirname10, join as join21, relative as relative6, resolve as resolve16 } from "path";
19488
19494
 
19489
19495
  // src/start/runtime.ts
19490
19496
  import { existsSync as existsSync18, readdirSync as readdirSync10, realpathSync, statSync as statSync10 } from "fs";
19491
19497
  import { homedir as homedir5 } from "os";
19492
- import { isAbsolute as isAbsolute4, join as join19, relative as relative5, resolve as resolve15, sep } from "path";
19498
+ import { isAbsolute as isAbsolute4, join as join20, relative as relative5, resolve as resolve15, sep } from "path";
19493
19499
 
19494
19500
  // src/start/session.ts
19495
19501
  import { randomUUID as randomUUID4 } from "crypto";
19496
19502
  import { relative as relative4 } from "path";
19503
+
19504
+ // src/loop/events-file.ts
19505
+ import { closeSync, fstatSync, openSync, readSync } from "fs";
19506
+
19507
+ // src/start/loop-events.ts
19508
+ import { appendFileSync, mkdirSync as mkdirSync9 } from "fs";
19509
+ import { join as join19 } from "path";
19510
+ var MINUTE_MS = 60000;
19511
+ var TOKENS_PER_K = 1000;
19512
+ function quoted2(text) {
19513
+ return `"${oneLine(text).replaceAll('"', "\\\"")}"`;
19514
+ }
19515
+ function taskKind(position, verb) {
19516
+ return padKind(`task ${position.index}/${position.total} ${verb}`);
19517
+ }
19518
+ function minutes(durationMs) {
19519
+ return `${Math.max(1, Math.round(durationMs / MINUTE_MS))}m`;
19520
+ }
19521
+ function summaryOf2(event) {
19522
+ switch (event.kind) {
19523
+ case "task-start":
19524
+ return `${taskKind(event.position, "start")}${quoted2(event.text)}`;
19525
+ case "task-done":
19526
+ return event.tokens === null ? `${taskKind(event.position, "done")}${minutes(event.durationMs)}` : `${taskKind(event.position, "done")}${minutes(event.durationMs)} ${Math.round(event.tokens / TOKENS_PER_K)}k tokens`;
19527
+ case "task-blocked":
19528
+ return `${taskKind(event.position, "blocked")}${oneLine(event.reason)}`;
19529
+ case "wrap-up":
19530
+ return `${padKind("wrap-up")}${event.phase}`;
19531
+ case "pr":
19532
+ return `pr #${event.number} opened`;
19533
+ case "no-pr":
19534
+ return `${padKind("no pr")}${oneLine(event.reason)}`;
19535
+ case "halt":
19536
+ return `${padKind("halt")}${oneLine(event.reason)}`;
19537
+ case "retry":
19538
+ return `${padKind(`retry ${event.attempt}/${event.of}`)}${oneLine(event.reason)}`;
19539
+ case "decision":
19540
+ return `${padKind(`decision ${event.strategy}`)}line ${event.line}: ${oneLine(event.reason)}`;
19541
+ case "decision-needed":
19542
+ return `${padKind("decision needed")}line ${event.line} ${quoted2(event.task)}`;
19543
+ case "passed-over":
19544
+ return `${padKind("passed over")}${passedOverList(event.tasks)}`;
19545
+ case "inherited":
19546
+ return `${padKind("inherited")}${oneLine(`${event.file} > ${event.name}`)}`;
19547
+ case "error":
19548
+ return `${padKind("error")}${firstLine4(event.message)}`;
19549
+ }
19550
+ }
19551
+ function passedOverList(tasks) {
19552
+ const noun = tasks.length === 1 ? "task" : "tasks";
19553
+ return `${tasks.length} ${noun}: ${tasks.map((task) => `line ${task.line} (${task.strategy})`).join(", ")}`;
19554
+ }
19555
+ function firstLine4(message) {
19556
+ return message.split(`
19557
+ `).map((line) => line.trim()).find((line) => line !== "") ?? "";
19558
+ }
19559
+ var EVENTS_EXTENSION = ".events.ndjson";
19560
+ var bound = null;
19561
+ function eventsFilePath(root, sessionId) {
19562
+ if (!isSessionId(sessionId)) {
19563
+ throw new Error(`events file: unusable session id ${JSON.stringify(sessionId)}`);
19564
+ }
19565
+ return join19(runsDir(root), `${sessionId}${EVENTS_EXTENSION}`);
19566
+ }
19567
+ function bindEventsFile(root, sessionId, warn = (line) => {
19568
+ process.stderr.write(line);
19569
+ }) {
19570
+ const file = eventsFilePath(root, sessionId);
19571
+ try {
19572
+ mkdirSync9(runsDir(root), { recursive: true });
19573
+ } catch {}
19574
+ bound = { file, warn, warned: false };
19575
+ return file;
19576
+ }
19577
+ function unbindEventsFile() {
19578
+ bound = null;
19579
+ }
19580
+ function appendEventLine(line) {
19581
+ if (bound === null)
19582
+ return;
19583
+ try {
19584
+ appendFileSync(bound.file, `${JSON.stringify(line)}
19585
+ `);
19586
+ } catch (error) {
19587
+ if (bound.warned)
19588
+ return;
19589
+ bound = { ...bound, warned: true };
19590
+ bound.warn(`rafa: cannot write the events file ${bound.file}: ${messageOf(error)}
19591
+ `);
19592
+ }
19593
+ }
19594
+ function emitLoopEvent(event, now = () => new Date) {
19595
+ const { kind, ...data } = event;
19596
+ const line = { name: kind, summary: summaryOf2(event), data, ts: now().toISOString() };
19597
+ appendEventLine(line);
19598
+ activeOutput().emit({ type: "event", ...line });
19599
+ }
19600
+ async function unlessText(read) {
19601
+ return activeOutputMode() === "text" ? null : read();
19602
+ }
19603
+ function taskPosition(trackerContent, lineNum) {
19604
+ const { tasks } = parsePlan(trackerContent);
19605
+ return { index: tasks.findIndex((task) => task.lineNum === lineNum) + 1, total: tasks.length };
19606
+ }
19607
+ async function taskTokens(checkout, sessionId, home) {
19608
+ try {
19609
+ const { usage } = await readSessionLog(join19(sessionLogDir(checkout, home), `${sessionId}.jsonl`));
19610
+ return usage.inputTokens + usage.cacheCreationInputTokens + usage.outputTokens;
19611
+ } catch {
19612
+ return null;
19613
+ }
19614
+ }
19615
+
19616
+ // src/loop/events-file.ts
19617
+ var NEWLINE = 10;
19618
+ function eventsFileOf(root, record) {
19619
+ return eventsFilePath(root, record.sessionId);
19620
+ }
19621
+ function isPlainObject(value) {
19622
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19623
+ }
19624
+ function parseEventLine(line) {
19625
+ let value;
19626
+ try {
19627
+ value = JSON.parse(line);
19628
+ } catch {
19629
+ return null;
19630
+ }
19631
+ if (!isPlainObject(value))
19632
+ return null;
19633
+ const { name: name2, summary, data, ts } = value;
19634
+ if (typeof name2 !== "string" || typeof summary !== "string" || typeof ts !== "string")
19635
+ return null;
19636
+ if (!isPlainObject(data))
19637
+ return null;
19638
+ return Object.freeze({ name: name2, summary, data: Object.freeze({ ...data }), ts });
19639
+ }
19640
+ function bytesFrom(fd, offset) {
19641
+ const length = fstatSync(fd).size - offset;
19642
+ if (length <= 0)
19643
+ return null;
19644
+ const buffer = Buffer.alloc(length);
19645
+ let filled = 0;
19646
+ while (filled < length) {
19647
+ const read = readSync(fd, buffer, filled, length - filled, offset + filled);
19648
+ if (read === 0)
19649
+ break;
19650
+ filled += read;
19651
+ }
19652
+ return buffer.subarray(0, filled);
19653
+ }
19654
+ function readTail(file, offset) {
19655
+ let fd;
19656
+ try {
19657
+ fd = openSync(file, "r");
19658
+ } catch (error) {
19659
+ if (errorCode(error) === "ENOENT")
19660
+ return "absent";
19661
+ throw error;
19662
+ }
19663
+ try {
19664
+ return bytesFrom(fd, offset);
19665
+ } finally {
19666
+ closeSync(fd);
19667
+ }
19668
+ }
19669
+ function readEventsFrom(file, offset) {
19670
+ if (!Number.isSafeInteger(offset) || offset < 0) {
19671
+ throw new RangeError(`events file: unusable offset ${String(offset)}`);
19672
+ }
19673
+ const tail = readTail(file, offset);
19674
+ if (tail === "absent")
19675
+ return Object.freeze({ kind: "absent" });
19676
+ const end = tail?.lastIndexOf(NEWLINE) ?? -1;
19677
+ if (tail === null || end < 0) {
19678
+ return Object.freeze({ kind: "read", events: Object.freeze([]), malformed: 0, offset });
19679
+ }
19680
+ const lines = tail.subarray(0, end).toString("utf8").split(`
19681
+ `);
19682
+ const parsed2 = lines.map(parseEventLine);
19683
+ const events = parsed2.filter((event) => event !== null);
19684
+ return Object.freeze({
19685
+ kind: "read",
19686
+ events: Object.freeze(events),
19687
+ malformed: parsed2.length - events.length,
19688
+ offset: offset + end + 1
19689
+ });
19690
+ }
19691
+
19692
+ // src/start/session.ts
19497
19693
  var NOTHING_DISPATCHED = " Nothing was checked and nothing was dispatched.";
19498
19694
  function recordLabel(repoRoot, sessionId) {
19499
19695
  return relative4(repoRoot, sessionFilePath(repoRoot, sessionId));
@@ -19552,6 +19748,7 @@ function sessionHandle(repoRoot, sessionId) {
19552
19748
  pullRequestStarted: () => change("the pull-request phase", { phase: "pull-request" }),
19553
19749
  ciStarted: () => change("the ci phase", { phase: "ci" }),
19554
19750
  repairStarted: () => change("the repair phase", { phase: "repair" }),
19751
+ decisionsChanged: (list) => change("the pass-over list", { decisions: list }),
19555
19752
  finished: () => {
19556
19753
  reachedEnd = true;
19557
19754
  },
@@ -19607,6 +19804,37 @@ function openRunSession(options) {
19607
19804
  }
19608
19805
  return sessionHandle(repoRoot, draft.sessionId);
19609
19806
  }
19807
+ function sameRunPlace(repoRoot, record, plan) {
19808
+ if (record.branch !== plan.branch)
19809
+ return false;
19810
+ if (record.worktree === undefined)
19811
+ return true;
19812
+ return plan.checkout !== repoRoot && record.worktree === plan.checkout;
19813
+ }
19814
+ var ENDED_STATES = new Set(["stopped", "done"]);
19815
+ function endedRunsNewestFirst(repoRoot, plan, seams) {
19816
+ const wanted = { planStub: plan.planStub, plan: relative4(repoRoot, plan.planPath) };
19817
+ return readSessions(repoRoot, seams).filter((record) => ENDED_STATES.has(record.state) && samePlan(record, wanted) && sameRunPlace(repoRoot, record, plan)).reverse();
19818
+ }
19819
+ function readPreviousPassOver(repoRoot, plan, seams = {}) {
19820
+ const newest = endedRunsNewestFirst(repoRoot, plan, seams)[0];
19821
+ return newest === undefined ? [] : sessionDecisions(newest);
19822
+ }
19823
+ var TASK_EVENTS = new Set(["task-start", "decision", "decision-needed"]);
19824
+ function neededOf(data) {
19825
+ const { task, line } = data;
19826
+ return typeof task === "string" && typeof line === "number" ? { task, line } : null;
19827
+ }
19828
+ function readPreviousDecisionNeeded(repoRoot, plan, seams = {}) {
19829
+ for (const record of endedRunsNewestFirst(repoRoot, plan, seams)) {
19830
+ const read = readEventsFrom(eventsFileOf(repoRoot, record), 0);
19831
+ const last = read.kind === "read" ? read.events.filter((event) => TASK_EVENTS.has(event.name)).at(-1) : undefined;
19832
+ if (last === undefined)
19833
+ continue;
19834
+ return last.name === "decision-needed" ? neededOf(last.data) : null;
19835
+ }
19836
+ return null;
19837
+ }
19610
19838
 
19611
19839
  // src/start/run-config.ts
19612
19840
  function refuseEffortDirRun(env) {
@@ -19684,7 +19912,7 @@ function announcePlanIssues(planContent) {
19684
19912
 
19685
19913
  // src/start/runtime.ts
19686
19914
  var RUNTIME_FLAG = "--runtime";
19687
- var RUNTIME_SUBDIR = join19(".rafa", "runtime");
19915
+ var RUNTIME_SUBDIR = join20(".rafa", "runtime");
19688
19916
  var RUNTIME_ENTRY = "cli.js";
19689
19917
  var SOURCE_DIR = "src";
19690
19918
  var RUNTIME_COMMAND = ["start"];
@@ -19711,7 +19939,7 @@ function resolveRuntime(value, places) {
19711
19939
  throw refusal2([`\u274C Refusing to start: \`--runtime\` names no runtime. Spell it ${SPELLING}.`]);
19712
19940
  }
19713
19941
  const isVersion = VERSION_SHAPE2.test(value);
19714
- const named3 = isVersion ? join19(places.home, RUNTIME_SUBDIR, value, RUNTIME_ENTRY) : resolve15(places.cwd, value);
19942
+ const named3 = isVersion ? join20(places.home, RUNTIME_SUBDIR, value, RUNTIME_ENTRY) : resolve15(places.cwd, value);
19715
19943
  refuseInsideSource(value, named3, places);
19716
19944
  const entry = entryOf2(value, named3, isVersion, places.home);
19717
19945
  const real = realpathSync(entry);
@@ -19722,7 +19950,7 @@ function entryOf2(value, named3, isVersion, home) {
19722
19950
  if (isVersion) {
19723
19951
  if (isFile2(named3))
19724
19952
  return named3;
19725
- const root = join19(home, RUNTIME_SUBDIR);
19953
+ const root = join20(home, RUNTIME_SUBDIR);
19726
19954
  const installed = installedVersions(root);
19727
19955
  throw refusal2([
19728
19956
  `\u274C Refusing to start: \`--runtime=${value}\` names no installed runtime: ${named3} is no file.`,
@@ -19731,7 +19959,7 @@ function entryOf2(value, named3, isVersion, home) {
19731
19959
  }
19732
19960
  if (isFile2(named3))
19733
19961
  return named3;
19734
- const inside = join19(named3, RUNTIME_ENTRY);
19962
+ const inside = join20(named3, RUNTIME_ENTRY);
19735
19963
  if (isDirectory(named3) && isFile2(inside))
19736
19964
  return inside;
19737
19965
  throw refusal2([
@@ -19749,7 +19977,7 @@ function refuseInsideSource(value, path3, places) {
19749
19977
  ]);
19750
19978
  }
19751
19979
  function sourceDirs({ cwd, root }) {
19752
- const dirs = [join19(cwd, SOURCE_DIR), join19(root, SOURCE_DIR)];
19980
+ const dirs = [join20(cwd, SOURCE_DIR), join20(root, SOURCE_DIR)];
19753
19981
  const real = dirs.filter((dir) => existsSync18(dir)).map((dir) => realpathSync(dir));
19754
19982
  return [...new Set([...dirs, ...real])];
19755
19983
  }
@@ -19759,7 +19987,7 @@ function isInside2(path3, dir) {
19759
19987
  }
19760
19988
  function installedVersions(root) {
19761
19989
  try {
19762
- return readdirSync10(root).filter((name2) => isFile2(join19(root, name2, RUNTIME_ENTRY))).sort((a, b) => a.localeCompare(b));
19990
+ return readdirSync10(root).filter((name2) => isFile2(join20(root, name2, RUNTIME_ENTRY))).sort((a, b) => a.localeCompare(b));
19763
19991
  } catch {
19764
19992
  return [];
19765
19993
  }
@@ -19953,19 +20181,19 @@ function runtimeRefusalLines(runtimeDir, version2) {
19953
20181
  }
19954
20182
  function listTrackers(repoRoot, planDir) {
19955
20183
  const dir = resolve16(repoRoot, planDir);
19956
- return entryNames(dir).filter((entry) => TRACKER_NAME.test(entry)).sort((a, b) => a.localeCompare(b)).map((entry) => join20(dir, entry)).filter((path3) => statSync11(path3).isFile()).map((path3) => relative6(repoRoot, path3));
20184
+ return entryNames(dir).filter((entry) => TRACKER_NAME.test(entry)).sort((a, b) => a.localeCompare(b)).map((entry) => join21(dir, entry)).filter((path3) => statSync11(path3).isFile()).map((path3) => relative6(repoRoot, path3));
19957
20185
  }
19958
20186
  function openTrackers(repoRoot, planDir) {
19959
20187
  return listTrackers(repoRoot, planDir).flatMap((path3) => {
19960
- const task = findNextTask(readFileSync26(join20(repoRoot, path3), "utf8"));
20188
+ const task = findNextTask(readFileSync26(join21(repoRoot, path3), "utf8"));
19961
20189
  return task ? [{ path: path3, task }] : [];
19962
20190
  });
19963
20191
  }
19964
20192
  function readManifestVersion2(repoRoot) {
19965
- const manifest = JSON.parse(readFileSync26(join20(repoRoot, "package.json"), "utf8"));
20193
+ const manifest = JSON.parse(readFileSync26(join21(repoRoot, "package.json"), "utf8"));
19966
20194
  const record = typeof manifest === "object" && manifest !== null ? manifest : {};
19967
20195
  if (record["name"] !== name) {
19968
- throw new Error(`${join20(repoRoot, "package.json")} names ${JSON.stringify(record["name"])}, not ${name}: this is no rafa checkout`);
20196
+ throw new Error(`${join21(repoRoot, "package.json")} names ${JSON.stringify(record["name"])}, not ${name}: this is no rafa checkout`);
19969
20197
  }
19970
20198
  const version2 = record["version"];
19971
20199
  if (typeof version2 !== "string" || !VERSION_SHAPE2.test(version2)) {
@@ -19974,12 +20202,12 @@ function readManifestVersion2(repoRoot) {
19974
20202
  return version2;
19975
20203
  }
19976
20204
  function copyTree(from, to) {
19977
- mkdirSync9(to, { recursive: true });
20205
+ mkdirSync10(to, { recursive: true });
19978
20206
  let copied = 0;
19979
20207
  const entries = readdirSync11(from, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
19980
20208
  for (const entry of entries) {
19981
- const source = join20(from, entry.name);
19982
- const target = join20(to, entry.name);
20209
+ const source = join21(from, entry.name);
20210
+ const target = join21(to, entry.name);
19983
20211
  if (entry.isDirectory()) {
19984
20212
  copied += copyTree(source, target);
19985
20213
  } else if (entry.isFile()) {
@@ -20020,7 +20248,7 @@ function swapIn(staging, to) {
20020
20248
  rmSync2(outgoing, { recursive: true, force: true });
20021
20249
  }
20022
20250
  function relink(linkPath, target) {
20023
- mkdirSync9(dirname10(linkPath), { recursive: true });
20251
+ mkdirSync10(dirname10(linkPath), { recursive: true });
20024
20252
  const temporary = temporaryName(linkPath);
20025
20253
  rmSync2(temporary, { force: true });
20026
20254
  try {
@@ -20074,8 +20302,8 @@ function installRuntime(seams, options = {}) {
20074
20302
  }
20075
20303
  function runInstall(seams, options) {
20076
20304
  const { repoRoot, home, log } = seams;
20077
- const runtimeRoot = join20(home, RUNTIME_SUBDIR);
20078
- const linkPath = join20(home, RAFA_BIN_DIR, LINK_NAME);
20305
+ const runtimeRoot = join21(home, RUNTIME_SUBDIR);
20306
+ const linkPath = join21(home, RAFA_BIN_DIR, LINK_NAME);
20079
20307
  log(`repo root: ${repoRoot}`);
20080
20308
  log(`runtime root: ${runtimeRoot}`);
20081
20309
  log(`link: ${linkPath}`);
@@ -20085,8 +20313,8 @@ function runInstall(seams, options) {
20085
20313
  const open = inStage("trackers", () => openTrackers(repoRoot, planDir));
20086
20314
  if (open.length > 0)
20087
20315
  return { kind: "refused", reason: "trackers", planDir, trackers: open };
20088
- const distDir = join20(repoRoot, "dist");
20089
- const runtimeDir = join20(runtimeRoot, version2);
20316
+ const distDir = join21(repoRoot, "dist");
20317
+ const runtimeDir = join21(runtimeRoot, version2);
20090
20318
  const installed = existsSync19(runtimeDir);
20091
20319
  if (installed && options.force !== true) {
20092
20320
  return { kind: "refused", reason: "runtime-exists", version: version2, runtimeDir };
@@ -20095,13 +20323,13 @@ function runInstall(seams, options) {
20095
20323
  const code = inStage("build", () => seams.build(repoRoot));
20096
20324
  if (code !== 0)
20097
20325
  throw new StageFailure("build", `the build exited ${code}`);
20098
- if (!existsSync19(join20(distDir, RUNTIME_ENTRY))) {
20099
- throw new StageFailure("build", `the build exited 0 and left no ${join20(distDir, RUNTIME_ENTRY)}`);
20326
+ if (!existsSync19(join21(distDir, RUNTIME_ENTRY))) {
20327
+ throw new StageFailure("build", `the build exited 0 and left no ${join21(distDir, RUNTIME_ENTRY)}`);
20100
20328
  }
20101
20329
  log(installed ? `copying ${distDir} over ${runtimeDir}, replacing it whole` : `copying ${distDir} into ${runtimeDir}`);
20102
20330
  const copied = inStage("copy", () => replaceTree(distDir, runtimeDir));
20103
20331
  log(`copied ${copied} file(s)`);
20104
- const resolved = inStage("link", () => relink(linkPath, join20(runtimeDir, RUNTIME_ENTRY)));
20332
+ const resolved = inStage("link", () => relink(linkPath, join21(runtimeDir, RUNTIME_ENTRY)));
20105
20333
  log(`${linkPath} resolves to ${resolved}`);
20106
20334
  return { kind: "done", version: version2, runtimeDir, linkPath, resolved, copied };
20107
20335
  }
@@ -20135,7 +20363,7 @@ function replaceFile(source, target) {
20135
20363
  }
20136
20364
  }
20137
20365
  function temporaryName(path3) {
20138
- return join20(dirname10(path3), `.${basename7(path3)}.snapshot-${process.pid}`);
20366
+ return join21(dirname10(path3), `.${basename7(path3)}.snapshot-${process.pid}`);
20139
20367
  }
20140
20368
 
20141
20369
  // src/commands/self-update.ts
@@ -20244,12 +20472,12 @@ var self_update_default = createSelfUpdateCommand();
20244
20472
 
20245
20473
  // src/commands/plan/list.ts
20246
20474
  import { existsSync as existsSync20, readdirSync as readdirSync12, readFileSync as readFileSync27 } from "fs";
20247
- import { join as join21 } from "path";
20475
+ import { join as join22 } from "path";
20248
20476
  var USAGE5 = "rafa plan list";
20249
20477
  var OPEN_HINT = "Type it bare: rafa plan list --open";
20250
20478
  function listing(dir, stub) {
20251
- const plan = join21(dir, planFileName(stub, false));
20252
- const trackerPath = join21(dir, planFileName(stub, true));
20479
+ const plan = join22(dir, planFileName(stub, false));
20480
+ const trackerPath = join22(dir, planFileName(stub, true));
20253
20481
  const tracker = isFile(trackerPath) ? trackerPath : null;
20254
20482
  const planModel = parsePlan(readFileSync27(plan, "utf8"));
20255
20483
  const counted4 = tracker === null ? planModel : parsePlan(readFileSync27(tracker, "utf8"));
@@ -20259,7 +20487,7 @@ function listPlans(plans) {
20259
20487
  const dir = plans.path;
20260
20488
  if (!existsSync20(dir))
20261
20489
  return { dir, plans: [] };
20262
- const stubs = readdirSync12(dir).filter((name2) => isFile(join21(dir, name2))).map((name2) => stubOfPlanFile(name2)).filter((stub) => stub !== null).sort((a, b) => a.localeCompare(b));
20490
+ const stubs = readdirSync12(dir).filter((name2) => isFile(join22(dir, name2))).map((name2) => stubOfPlanFile(name2)).filter((stub) => stub !== null).sort((a, b) => a.localeCompare(b));
20263
20491
  return { dir, plans: stubs.map((stub) => listing(dir, stub)) };
20264
20492
  }
20265
20493
  function isOpen(plan) {
@@ -20442,7 +20670,7 @@ async function openNextRelations(context, seams) {
20442
20670
  // src/commands/loop/loop-sessions.ts
20443
20671
  import { execFileSync } from "child_process";
20444
20672
  import { readFileSync as readFileSync28 } from "fs";
20445
- import { join as join22, relative as relative7 } from "path";
20673
+ import { join as join23, relative as relative7 } from "path";
20446
20674
 
20447
20675
  // src/effort/store/task-finishes.ts
20448
20676
  import { existsSync as existsSync21 } from "fs";
@@ -20458,8 +20686,8 @@ function readTaskFinishes(repoRoot, query) {
20458
20686
  const path3 = sqliteStorePath(repoRoot);
20459
20687
  if (!existsSync21(path3))
20460
20688
  return [];
20461
- const bound = [query.planStub, query.since, query.planStub, query.since];
20462
- const rows = withSqliteStore(path3, "read", false, (db) => db.query(SELECT_FINISHES).all(...bound));
20689
+ const bound2 = [query.planStub, query.since, query.planStub, query.since];
20690
+ const rows = withSqliteStore(path3, "read", false, (db) => db.query(SELECT_FINISHES).all(...bound2));
20463
20691
  return rows.map((row) => row.collected_at);
20464
20692
  }
20465
20693
 
@@ -20594,7 +20822,7 @@ function writeSession(root, record, change) {
20594
20822
  }
20595
20823
  }
20596
20824
  function readSessionChecklist(root, record) {
20597
- const plan = join22(root, record.plan);
20825
+ const plan = join23(root, record.plan);
20598
20826
  const file = [trackerPathFor(plan), plan].find((path3) => isFile(path3));
20599
20827
  if (file === undefined)
20600
20828
  return null;
@@ -20630,11 +20858,11 @@ function readSessionFinishes(root, record) {
20630
20858
  function formatDuration(seconds) {
20631
20859
  if (seconds < 60)
20632
20860
  return "under a minute";
20633
- const minutes = Math.round(seconds / 60);
20634
- const hours = Math.floor(minutes / 60);
20635
- const rest = minutes % 60;
20861
+ const minutes2 = Math.round(seconds / 60);
20862
+ const hours = Math.floor(minutes2 / 60);
20863
+ const rest = minutes2 % 60;
20636
20864
  if (hours === 0)
20637
- return `${minutes}m`;
20865
+ return `${minutes2}m`;
20638
20866
  return rest === 0 ? `${hours}h` : `${hours}h ${rest}m`;
20639
20867
  }
20640
20868
  function etaLine(eta) {
@@ -20753,7 +20981,7 @@ function readBoardBody(body) {
20753
20981
 
20754
20982
  // src/board/codeowners.ts
20755
20983
  import { readFileSync as readFileSync29, statSync as statSync12 } from "fs";
20756
- import { join as join23 } from "path";
20984
+ import { join as join24 } from "path";
20757
20985
  var CODEOWNERS_LOCATIONS = Object.freeze([
20758
20986
  ".github/CODEOWNERS",
20759
20987
  "CODEOWNERS",
@@ -20805,7 +21033,7 @@ function isFile3(path3) {
20805
21033
  }
20806
21034
  }
20807
21035
  function findCodeowners(root) {
20808
- return CODEOWNERS_LOCATIONS.find((location) => isFile3(join23(root, location))) ?? null;
21036
+ return CODEOWNERS_LOCATIONS.find((location) => isFile3(join24(root, location))) ?? null;
20809
21037
  }
20810
21038
  function readCodeowners(root) {
20811
21039
  const path3 = findCodeowners(root);
@@ -20813,7 +21041,7 @@ function readCodeowners(root) {
20813
21041
  return null;
20814
21042
  let text;
20815
21043
  try {
20816
- text = readFileSync29(join23(root, path3), "utf8");
21044
+ text = readFileSync29(join24(root, path3), "utf8");
20817
21045
  } catch (error) {
20818
21046
  const why = error instanceof Error ? error.message : String(error);
20819
21047
  throw new Error(`Cannot read ${path3}: ${why}`, { cause: error });
@@ -21588,7 +21816,7 @@ function pickerFor(followed, seams) {
21588
21816
 
21589
21817
  // src/commands/pr/merge-cleanup.ts
21590
21818
  import { existsSync as existsSync22, readFileSync as readFileSync30 } from "fs";
21591
- import { join as join24 } from "path";
21819
+ import { join as join25 } from "path";
21592
21820
 
21593
21821
  // src/release/enabled.ts
21594
21822
  import { statSync as statSync13 } from "fs";
@@ -21773,11 +22001,11 @@ function settleWaitingOn(place, git) {
21773
22001
  return { base: place.base, fragments: reading2.fragments.length, version: reading2.version };
21774
22002
  }
21775
22003
  function followUpsFor(place, git) {
21776
- const facts = readPackageFacts(textOf2(join24(place.root, "package.json")));
22004
+ const facts = readPackageFacts(textOf2(join25(place.root, "package.json")));
21777
22005
  return readFollowUps({
21778
22006
  version: facts.version,
21779
22007
  rafaCheckout: facts.rafaCheckout,
21780
- runtimeInstalled: facts.version !== null && existsSync22(join24(place.home, RUNTIME_SUBDIR, facts.version)),
22008
+ runtimeInstalled: facts.version !== null && existsSync22(join25(place.home, RUNTIME_SUBDIR, facts.version)),
21781
22009
  settle: settleWaitingOn(place, git),
21782
22010
  liveLoopBranches: liveLoopBranchesOf(place.root, place.isAlive)
21783
22011
  });
@@ -23722,19 +23950,19 @@ var ready_default = createIssueReadyCommand();
23722
23950
  import fs from "fs";
23723
23951
  import { homedir as homedir10 } from "os";
23724
23952
  import path4 from "path";
23725
- import { fileURLToPath as fileURLToPath2 } from "url";
23953
+ import { fileURLToPath as fileURLToPath5 } from "url";
23726
23954
 
23727
23955
  // src/notices/run.ts
23728
23956
  import { homedir as homedir6 } from "os";
23729
23957
 
23730
23958
  // src/notices/notices.ts
23731
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync31, writeFileSync as writeFileSync11 } from "fs";
23732
- import { dirname as dirname11, join as join25 } from "path";
23959
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync31, writeFileSync as writeFileSync11 } from "fs";
23960
+ import { dirname as dirname11, join as join26 } from "path";
23733
23961
  var NOTICE_IDS = Object.freeze(["alpha", "danger"]);
23734
23962
  var FEEDBACK_URL = "https://github.com/open-tomato/rafa/issues/new/choose";
23735
23963
  var ROADMAP_URL = "https://github.com/open-tomato/rafa/issues/31";
23736
23964
  function noticesPath(home) {
23737
- return join25(home, ".rafa", "notices.json");
23965
+ return join26(home, ".rafa", "notices.json");
23738
23966
  }
23739
23967
  function isNoticeId(value) {
23740
23968
  return typeof value === "string" && NOTICE_IDS.includes(value);
@@ -23761,7 +23989,7 @@ function readDismissed(home) {
23761
23989
  function writeDismissed(home, ids) {
23762
23990
  const all = NOTICE_IDS.filter((id2) => ids.includes(id2) || readDismissed(home).includes(id2));
23763
23991
  const path3 = noticesPath(home);
23764
- mkdirSync10(dirname11(path3), { recursive: true });
23992
+ mkdirSync11(dirname11(path3), { recursive: true });
23765
23993
  writeFileSync11(path3, `${JSON.stringify({ dismissed: all }, null, 2)}
23766
23994
  `);
23767
23995
  }
@@ -23879,7 +24107,7 @@ function realDirOf(path3) {
23879
24107
  return null;
23880
24108
  }
23881
24109
  }
23882
- function firstLine4(git, args) {
24110
+ function firstLine5(git, args) {
23883
24111
  const result = git(args);
23884
24112
  if (!result.ok)
23885
24113
  return null;
@@ -23892,13 +24120,13 @@ function readCheckout2(checkout, seams = {}) {
23892
24120
  if (realDir === null)
23893
24121
  return { kind: "missing" };
23894
24122
  const git = (seams.git ?? createGitRunner)(checkout);
23895
- const toplevel = firstLine4(git, ["rev-parse", "--show-toplevel"]);
24123
+ const toplevel = firstLine5(git, ["rev-parse", "--show-toplevel"]);
23896
24124
  if (toplevel === null || toplevel !== realDir)
23897
24125
  return { kind: "foreign", toplevel };
23898
24126
  return {
23899
24127
  kind: "read",
23900
- branch: firstLine4(git, ["symbolic-ref", "--quiet", "--short", "HEAD"]),
23901
- head: firstLine4(git, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"])
24128
+ branch: firstLine5(git, ["symbolic-ref", "--quiet", "--short", "HEAD"]),
24129
+ head: firstLine5(git, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"])
23902
24130
  };
23903
24131
  }
23904
24132
  function compareCheckout(expected2, reading2) {
@@ -23968,7 +24196,7 @@ function guardCheckout(expected2, seams = {}) {
23968
24196
  const move = compareCheckout(expected2, reading2);
23969
24197
  if (move === null)
23970
24198
  return { held: true };
23971
- const branchTip = move === "branch" ? firstLine4((seams.git ?? createGitRunner)(expected2.checkout), ["rev-parse", "--verify", "--quiet", `refs/heads/${expected2.branch}^{commit}`]) : null;
24199
+ const branchTip = move === "branch" ? firstLine5((seams.git ?? createGitRunner)(expected2.checkout), ["rev-parse", "--verify", "--quiet", `refs/heads/${expected2.branch}^{commit}`]) : null;
23972
24200
  return { held: false, move, reading: reading2, lines: checkoutMovedLines({ expected: expected2, move, reading: reading2, branchTip }) };
23973
24201
  }
23974
24202
 
@@ -24048,7 +24276,7 @@ function haltIfWrapUpMoved(options, seams = {}) {
24048
24276
  // src/project/roots.ts
24049
24277
  import { spawnSync as spawnSync2 } from "child_process";
24050
24278
  import { accessSync, constants, readFileSync as readFileSync32, statSync as statSync14 } from "fs";
24051
- import { isAbsolute as isAbsolute5, join as join26, resolve as resolve18 } from "path";
24279
+ import { isAbsolute as isAbsolute5, join as join27, resolve as resolve18 } from "path";
24052
24280
  var REFUSED_ROOTS = Object.freeze([
24053
24281
  { path: "/", why: "is the filesystem root" },
24054
24282
  { path: "/var", why: "holds system state" },
@@ -24180,8 +24408,8 @@ function readManifest(file, fs) {
24180
24408
  }
24181
24409
  }
24182
24410
  function readDir(dir, fs) {
24183
- const manifest = readManifest(join26(dir, PACKAGE_JSON), fs);
24184
- const files = MONOREPO_MARKER_FILES.filter((name2) => fs.exists(join26(dir, name2)));
24411
+ const manifest = readManifest(join27(dir, PACKAGE_JSON), fs);
24412
+ const files = MONOREPO_MARKER_FILES.filter((name2) => fs.exists(join27(dir, name2)));
24185
24413
  const markers = manifest.marks ? [PACKAGE_JSON, ...files] : files;
24186
24414
  return { ...manifest, dir, markers };
24187
24415
  }
@@ -24546,6 +24774,12 @@ var NO_REPORT_HOLD = "no report: the session wrote no rafa:report block the loop
24546
24774
  var BACKGROUND_WAIT_HOLD = "no report: the session ended its turn waiting on a command left running in the background:";
24547
24775
  var NO_COMMIT_HOLD = "no commit: the task changed no tracked file";
24548
24776
  var NOTHING_REPORTED_OR_COMMITTED = "the session wrote no report block and changed no tracked file";
24777
+ function isNothingLeftBehindHold(hold) {
24778
+ return hold === NO_REPORT_HOLD || hold === NO_COMMIT_HOLD || hold === NOTHING_REPORTED_OR_COMMITTED || hold.startsWith(`${BACKGROUND_WAIT_HOLD} `);
24779
+ }
24780
+ function heldOnNothingLeftBehind(finished) {
24781
+ return finished.attempt.outcome !== "failed" && finished.holds.length > 0 && finished.holds.every(isNothingLeftBehindHold);
24782
+ }
24549
24783
  function indentBlock(text) {
24550
24784
  return text.split(`
24551
24785
  `).map((line) => ` ${line}`).join(`
@@ -24636,6 +24870,571 @@ function finishCleanExit(options) {
24636
24870
  };
24637
24871
  }
24638
24872
 
24873
+ // src/start/decision-parse.ts
24874
+ var DECISION_KIND = "decision";
24875
+ var DECISION_STRATEGIES = ["retry", "stop", "jump", "defer"];
24876
+ var UNREADABLE_PREFIX = "the decision could not be read, so the run stops: ";
24877
+ function isMapping3(value) {
24878
+ return typeof value === "object" && value !== null && !Array.isArray(value);
24879
+ }
24880
+ function fieldOf(mapping, key) {
24881
+ return Object.entries(mapping).find(([own]) => own === key)?.[1];
24882
+ }
24883
+ function describeValue3(value) {
24884
+ if (typeof value === "string")
24885
+ return JSON.stringify(value);
24886
+ if (value === null || value === undefined)
24887
+ return "nothing";
24888
+ if (Array.isArray(value))
24889
+ return "a list";
24890
+ if (typeof value === "object")
24891
+ return "a mapping";
24892
+ return `the ${typeof value} ${String(value)}`;
24893
+ }
24894
+ function messageOf7(error) {
24895
+ return error instanceof Error ? error.message : String(error);
24896
+ }
24897
+ function unreadable(why) {
24898
+ return { strategy: "stop", reason: `${UNREADABLE_PREFIX}${why}` };
24899
+ }
24900
+ function isStrategy(value) {
24901
+ return DECISION_STRATEGIES.includes(value);
24902
+ }
24903
+ function textField(document, key, needs = "") {
24904
+ const raw = fieldOf(document, key);
24905
+ if (raw === undefined || raw === null)
24906
+ return { problem: `${key} is missing${needs}` };
24907
+ if (typeof raw !== "string")
24908
+ return { problem: `${key} is ${describeValue3(raw)}, expected text` };
24909
+ const value = raw.trim();
24910
+ return value === "" ? { problem: `${key} is blank${needs}` } : { value };
24911
+ }
24912
+ function afterField(document) {
24913
+ const raw = fieldOf(document, "after");
24914
+ if (raw === undefined || raw === null)
24915
+ return { problem: "after is missing, and defer needs one" };
24916
+ return typeof raw === "number" && Number.isSafeInteger(raw) && raw >= 1 ? { value: raw } : { problem: `after is ${describeValue3(raw)}, expected a line number from 1` };
24917
+ }
24918
+ function readDecision(document) {
24919
+ const strategy = fieldOf(document, "strategy");
24920
+ if (strategy === undefined || strategy === null)
24921
+ return unreadable("strategy is missing");
24922
+ if (!isStrategy(strategy)) {
24923
+ return unreadable(`strategy is ${describeValue3(strategy)}, expected one of: ${DECISION_STRATEGIES.join(", ")}`);
24924
+ }
24925
+ const reason = textField(document, "reason");
24926
+ if ("problem" in reason)
24927
+ return unreadable(reason.problem);
24928
+ if (strategy === "retry") {
24929
+ const approach = textField(document, "approach", ", and retry needs one");
24930
+ return "problem" in approach ? unreadable(approach.problem) : { strategy, reason: reason.value, approach: approach.value };
24931
+ }
24932
+ if (strategy === "defer") {
24933
+ const after = afterField(document);
24934
+ return "problem" in after ? unreadable(after.problem) : { strategy, reason: reason.value, after: after.value };
24935
+ }
24936
+ return { strategy, reason: reason.value };
24937
+ }
24938
+ function parseDecision(output) {
24939
+ const block = readRafaBlocks(output).filter((each) => each.kind === DECISION_KIND).at(-1);
24940
+ if (block === undefined)
24941
+ return unreadable("the output holds no rafa:decision block");
24942
+ const at = `the rafa:decision block at line ${String(block.span.first)}`;
24943
+ if (!block.closed)
24944
+ return unreadable(`${at} is never closed`);
24945
+ let document;
24946
+ try {
24947
+ document = Bun.YAML.parse(block.body);
24948
+ } catch (error) {
24949
+ return unreadable(`${at} is not valid YAML (${messageOf7(error)})`);
24950
+ }
24951
+ if (!isMapping3(document))
24952
+ return unreadable(`${at} holds ${describeValue3(document)}, not a mapping`);
24953
+ return readDecision(document);
24954
+ }
24955
+
24956
+ // src/start/decision-prompt.ts
24957
+ import { existsSync as existsSync26, readFileSync as readFileSync35 } from "fs";
24958
+ import { dirname as dirname14, join as join30, resolve as resolve19 } from "path";
24959
+ import { fileURLToPath as fileURLToPath4 } from "url";
24960
+
24961
+ // src/epic/verify-plan.ts
24962
+ import { existsSync as existsSync25, readFileSync as readFileSync34 } from "fs";
24963
+ import { dirname as dirname13, join as join29 } from "path";
24964
+ import { fileURLToPath as fileURLToPath3 } from "url";
24965
+
24966
+ // src/board/epic-template.ts
24967
+ import { existsSync as existsSync24, readFileSync as readFileSync33 } from "fs";
24968
+ import { dirname as dirname12, join as join28 } from "path";
24969
+ import { fileURLToPath as fileURLToPath2 } from "url";
24970
+ var MODULE_DIR2 = dirname12(fileURLToPath2(import.meta.url));
24971
+ var EPIC_TEMPLATE_FILE = "epic.md";
24972
+ var CRITERIA_PLACEHOLDERS = Object.freeze([
24973
+ "- Placeholder: one line per thing that makes this epic finished," + " written so a check against main can tell whether it holds."
24974
+ ]);
24975
+ var PLACEHOLDER_REASON = "the line is still the epic template's placeholder, so there is nothing to check";
24976
+ function epicTemplateSource(moduleDir = MODULE_DIR2) {
24977
+ return join28(moduleDir, TEMPLATES_DIRNAME, EPIC_TEMPLATE_FILE);
24978
+ }
24979
+ function readEpicTemplate(moduleDir = MODULE_DIR2) {
24980
+ const source = epicTemplateSource(moduleDir);
24981
+ if (!existsSync24(source)) {
24982
+ throw new Error(`epic template: the epic issue template is missing: no file at ${source}`);
24983
+ }
24984
+ return readFileSync33(source, "utf8");
24985
+ }
24986
+ function placeholderCriteria(criteria) {
24987
+ if (criteria === null)
24988
+ return Object.freeze([]);
24989
+ const held = criteria.split(/\r\n?|\n/u).map((line) => line.trim()).filter((line) => CRITERIA_PLACEHOLDERS.includes(line));
24990
+ return Object.freeze(held.map((text) => Object.freeze({ text, reason: PLACEHOLDER_REASON })));
24991
+ }
24992
+ function renderEpicBody(template = readEpicTemplate()) {
24993
+ const read = readEpicBody(template);
24994
+ if (read.criteria === null) {
24995
+ throw new Error("epic template: the template carries no acceptance criteria section with a placeholder under it");
24996
+ }
24997
+ const missing = CRITERIA_PLACEHOLDERS.length - placeholderCriteria(read.criteria).length;
24998
+ if (missing > 0) {
24999
+ throw new Error(`epic template: the acceptance criteria section has lost ${String(missing)} of its placeholder lines`);
25000
+ }
25001
+ if (read.lines.length > 0) {
25002
+ throw new Error(`epic template: the template carries ${String(read.lines.length)} checklist lines; a new epic has none`);
25003
+ }
25004
+ return template;
25005
+ }
25006
+
25007
+ // src/epic/verify-plan.ts
25008
+ var MODULE_DIR3 = dirname13(fileURLToPath3(import.meta.url));
25009
+ var VERIFY_PROMPT_FILE = "epic-verify-prompt.md";
25010
+ var VERIFY_BLOCK_FENCE = "rafa:verify";
25011
+ var VERIFY_PROMPT_SLOTS = ["EPIC_NUMBER", "EPIC_TITLE", "CRITERIA"];
25012
+ var SKIPPED_REASON = "the verification plan did not answer it";
25013
+ var FENCE4 = /^\s*(?:`{3,}|~{3,})/u;
25014
+ var LIST_ITEM2 = /^( {0,3})(?:[-*+]|\d{1,9}[.)])(?:\s|$)/u;
25015
+ var INDENTED2 = /^\s/u;
25016
+ var MIN_FENCE_LENGTH = 3;
25017
+ var VERIFY_KIND = VERIFY_BLOCK_FENCE.slice("rafa:".length);
25018
+ var SLOT_PATTERN = new RegExp(`\\{(${VERIFY_PROMPT_SLOTS.join("|")})\\}`, "gu");
25019
+ var ANSWER_KEYS = ["check", "uncheckable"];
25020
+ function opensCriterion(text, state) {
25021
+ const item = LIST_ITEM2.exec(text);
25022
+ if (item !== null) {
25023
+ const indent = item[1]?.length ?? 0;
25024
+ state.topIndent ??= indent;
25025
+ return indent <= state.topIndent;
25026
+ }
25027
+ return state.afterBlank && !INDENTED2.test(text);
25028
+ }
25029
+ function splitCriteria(criteria) {
25030
+ if (criteria === null)
25031
+ return Object.freeze([]);
25032
+ const gathered = [];
25033
+ const state = { fenced: false, afterBlank: false, topIndent: null };
25034
+ for (const text of criteria.split(/\r\n?|\n/u)) {
25035
+ const insideFence = state.fenced;
25036
+ if (FENCE4.test(text))
25037
+ state.fenced = !state.fenced;
25038
+ if (!insideFence && text.trim() === "") {
25039
+ state.afterBlank = true;
25040
+ gathered.at(-1)?.push(text);
25041
+ continue;
25042
+ }
25043
+ const current = gathered.at(-1);
25044
+ const opens = !insideFence && opensCriterion(text, state);
25045
+ if (current === undefined || opens) {
25046
+ gathered.push([text]);
25047
+ } else {
25048
+ current.push(text);
25049
+ }
25050
+ state.afterBlank = false;
25051
+ }
25052
+ const numbered2 = gathered.map((lines, index) => {
25053
+ const text = lines.join(`
25054
+ `).trim();
25055
+ return Object.freeze({ number: index + 1, text, placeholder: CRITERIA_PLACEHOLDERS.includes(text) });
25056
+ });
25057
+ return Object.freeze(numbered2);
25058
+ }
25059
+ function criteriaToAsk(criteria) {
25060
+ return Object.freeze(criteria.filter((criterion) => !criterion.placeholder));
25061
+ }
25062
+ function verifyPromptCandidates(moduleDir = MODULE_DIR3) {
25063
+ return [join29(moduleDir, VERIFY_PROMPT_FILE), join29(dirname13(moduleDir), VERIFY_PROMPT_FILE)];
25064
+ }
25065
+ function readVerifyPrompt(moduleDir = MODULE_DIR3) {
25066
+ const candidates = verifyPromptCandidates(moduleDir);
25067
+ const found = candidates.find((candidate) => existsSync25(candidate));
25068
+ if (found === undefined) {
25069
+ throw new Error(`epic verify prompt: the verification prompt is missing: no file at ${candidates.join(" or ")}`);
25070
+ }
25071
+ return readFileSync34(found, "utf8");
25072
+ }
25073
+ function fenceFor(text) {
25074
+ const longestRun = Math.max(0, ...(text.match(/`+/gu) ?? []).map((run) => run.length));
25075
+ return "`".repeat(Math.max(MIN_FENCE_LENGTH, longestRun + 1));
25076
+ }
25077
+ function renderCriterion(criterion) {
25078
+ const fence = fenceFor(criterion.text);
25079
+ return [`### Criterion ${String(criterion.number)}`, "", `${fence}text`, criterion.text, fence].join(`
25080
+ `);
25081
+ }
25082
+ function buildVerifyPrompt(template, input) {
25083
+ const missing = VERIFY_PROMPT_SLOTS.find((slot) => !template.includes(`{${slot}}`));
25084
+ if (missing !== undefined) {
25085
+ throw new Error(`epic verify prompt: the template carries no {${missing}} slot`);
25086
+ }
25087
+ const values = {
25088
+ EPIC_NUMBER: String(input.epic),
25089
+ EPIC_TITLE: input.title.replace(/\s+/gu, " ").trim(),
25090
+ CRITERIA: criteriaToAsk(input.criteria).map(renderCriterion).join(`
25091
+
25092
+ `)
25093
+ };
25094
+ return template.replace(SLOT_PATTERN, (_whole, slot) => values[slot]);
25095
+ }
25096
+ function isMapping4(value) {
25097
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25098
+ }
25099
+ function fieldOf2(mapping, key) {
25100
+ return Object.entries(mapping).find(([own]) => own === key)?.[1];
25101
+ }
25102
+ function describeValue4(value) {
25103
+ if (typeof value === "string")
25104
+ return JSON.stringify(value);
25105
+ if (value === null || value === undefined)
25106
+ return "nothing";
25107
+ if (Array.isArray(value))
25108
+ return "a list";
25109
+ if (typeof value === "object")
25110
+ return "a mapping";
25111
+ return `the ${typeof value} ${String(value)}`;
25112
+ }
25113
+ function messageOf8(error) {
25114
+ return error instanceof Error ? error.message : String(error);
25115
+ }
25116
+ function absent2(reason, block, text) {
25117
+ return Object.freeze({ present: false, reason, block, text });
25118
+ }
25119
+ function readEntry(item, field4, asked) {
25120
+ if (!isMapping4(item))
25121
+ return { problem: `${field4} is ${describeValue4(item)}, not a mapping`, number: null };
25122
+ const number = fieldOf2(item, "criterion");
25123
+ if (typeof number !== "number" || !Number.isInteger(number) || number < 1) {
25124
+ return { problem: `${field4}.criterion is ${describeValue4(number)}, not a criterion number from 1`, number: null };
25125
+ }
25126
+ if (!asked.has(number)) {
25127
+ return { problem: `${field4}.criterion ${String(number)} is no criterion the plan was asked about`, number: null };
25128
+ }
25129
+ const carried = ANSWER_KEYS.filter((key2) => fieldOf2(item, key2) !== undefined);
25130
+ const [key] = carried;
25131
+ if (carried.length !== 1 || key === undefined) {
25132
+ const count = carried.length === 0 ? "neither" : "both";
25133
+ return { problem: `${field4} carries ${count} check and uncheckable`, number };
25134
+ }
25135
+ const value = fieldOf2(item, key);
25136
+ if (typeof value !== "string" || value.trim() === "") {
25137
+ return { problem: `${field4}.${key} is ${describeValue4(value)}, not a non-blank string`, number };
25138
+ }
25139
+ return { answer: { number, key, text: value.trim() } };
25140
+ }
25141
+ function verdictFor(criterion, answer4, problem) {
25142
+ if (criterion.placeholder) {
25143
+ return { kind: "uncheckable", criterion, reason: PLACEHOLDER_REASON, source: "placeholder" };
25144
+ }
25145
+ if (answer4?.key === "check")
25146
+ return { kind: "check", criterion, check: answer4.text };
25147
+ if (answer4?.key === "uncheckable")
25148
+ return { kind: "uncheckable", criterion, reason: answer4.text, source: "answer" };
25149
+ const reason = problem === undefined ? SKIPPED_REASON : `${SKIPPED_REASON}: its entry was dropped, as ${problem}`;
25150
+ return { kind: "uncheckable", criterion, reason, source: "skipped" };
25151
+ }
25152
+ function readVerdicts(items2, criteria, block) {
25153
+ const asked = new Set(criteriaToAsk(criteria).map((criterion) => criterion.number));
25154
+ const answers = new Map;
25155
+ const problems = new Map;
25156
+ const issues = [];
25157
+ for (const [index, item] of items2.entries()) {
25158
+ const field4 = `criteria[${String(index)}]`;
25159
+ const reading2 = readEntry(item, field4, asked);
25160
+ if ("problem" in reading2) {
25161
+ issues.push(Object.freeze({ field: field4, text: `${reading2.problem}; dropped` }));
25162
+ if (reading2.number !== null && !problems.has(reading2.number))
25163
+ problems.set(reading2.number, reading2.problem);
25164
+ continue;
25165
+ }
25166
+ if (answers.has(reading2.answer.number)) {
25167
+ const text = `${field4} answers criterion ${String(reading2.answer.number)} a second time; dropped`;
25168
+ issues.push(Object.freeze({ field: field4, text }));
25169
+ continue;
25170
+ }
25171
+ answers.set(reading2.answer.number, reading2.answer);
25172
+ }
25173
+ const verdicts = criteria.map((criterion) => Object.freeze(verdictFor(criterion, answers.get(criterion.number), problems.get(criterion.number))));
25174
+ return Object.freeze({
25175
+ present: true,
25176
+ verdicts: Object.freeze(verdicts),
25177
+ block,
25178
+ issues: Object.freeze(issues)
25179
+ });
25180
+ }
25181
+ function parseVerifyPlan(output, criteria) {
25182
+ const block = readRafaBlocks(output).filter((each) => each.kind === VERIFY_KIND).at(-1);
25183
+ if (block === undefined) {
25184
+ return absent2("no-block", null, `the session output holds no ${VERIFY_BLOCK_FENCE} block`);
25185
+ }
25186
+ const at = `${VERIFY_BLOCK_FENCE} block at line ${String(block.span.first)}`;
25187
+ if (!block.closed) {
25188
+ return absent2("unclosed-block", block, `${at} is never closed, so it is not read`);
25189
+ }
25190
+ let document;
25191
+ try {
25192
+ document = Bun.YAML.parse(block.body);
25193
+ } catch (error) {
25194
+ return absent2("malformed-block", block, `${at} is not valid YAML (${messageOf8(error)})`);
25195
+ }
25196
+ if (!isMapping4(document)) {
25197
+ return absent2("malformed-block", block, `${at} holds ${describeValue4(document)}, not a mapping`);
25198
+ }
25199
+ const listed = fieldOf2(document, "criteria") ?? [];
25200
+ if (!Array.isArray(listed)) {
25201
+ return absent2("malformed-block", block, `${at} has criteria ${describeValue4(listed)}, not a list`);
25202
+ }
25203
+ return readVerdicts(listed, criteria, block);
25204
+ }
25205
+
25206
+ // src/start/decision-prompt.ts
25207
+ var MODULE_DIR4 = dirname14(fileURLToPath4(import.meta.url));
25208
+ var DECISION_PROMPT_FILE = "continue-decision-prompt.md";
25209
+ var CONTINUE_CRITERIA_FILE = "continue-criteria.md";
25210
+ var PROJECT_CRITERIA_HEADING = "### Project criteria";
25211
+ var DECISION_PROMPT_SLOTS = [
25212
+ "plan",
25213
+ "line",
25214
+ "task",
25215
+ "holds",
25216
+ "retriesLeft",
25217
+ "openTasks",
25218
+ "criteria"
25219
+ ];
25220
+ var SLOT_PATTERN2 = new RegExp(`\\{\\{(${DECISION_PROMPT_SLOTS.join("|")})\\}\\}`, "gu");
25221
+ var RETRY_SECTION = /^<!-- (retry|no-retry) -->\n([\s\S]*?)^<!-- \/\1 -->\n/gmu;
25222
+ function chooseRetrySections(template, retriesLeft) {
25223
+ const kept = retriesLeft > 0 ? "retry" : "no-retry";
25224
+ return template.replace(RETRY_SECTION, (_whole, section, body) => section === kept ? body : "");
25225
+ }
25226
+ var NO_HOLDS = "(no reason was recorded)";
25227
+ var NO_OPEN_TASKS = "(no open task is left)";
25228
+ function bundledCandidates(file, moduleDir = MODULE_DIR4) {
25229
+ return [join30(moduleDir, file), join30(dirname14(moduleDir), file)];
25230
+ }
25231
+ function readBundledFile(file, moduleDir = MODULE_DIR4) {
25232
+ const candidates = bundledCandidates(file, moduleDir);
25233
+ const found = candidates.find((candidate) => existsSync26(candidate));
25234
+ if (found === undefined) {
25235
+ throw new Error(`loop continue: ${file} is missing: no file at ${candidates.join(" or ")}`);
25236
+ }
25237
+ return readFileSync35(found, "utf8");
25238
+ }
25239
+ function readProjectFile(path3) {
25240
+ if (!existsSync26(path3))
25241
+ return { text: null };
25242
+ try {
25243
+ return { text: readFileSync35(path3, "utf8") };
25244
+ } catch (error) {
25245
+ const message = error instanceof Error ? error.message : String(error);
25246
+ return { refusal: `loop.continue.criteria cannot be read at ${path3}: ${message}` };
25247
+ }
25248
+ }
25249
+ function resolveContinueCriteria(settings, moduleDir = MODULE_DIR4) {
25250
+ const path3 = resolve19(settings.root, settings.path);
25251
+ const read = readProjectFile(path3);
25252
+ if ("refusal" in read)
25253
+ return { ok: false, refusal: read.refusal };
25254
+ const projectText = read.text?.trim() ?? "";
25255
+ const project = read.text === null ? null : path3;
25256
+ if (settings.mode === "replace") {
25257
+ if (read.text === null) {
25258
+ return {
25259
+ ok: false,
25260
+ refusal: `loop.continue.criteriaMode is replace, and loop.continue.criteria names no file at ${path3}`
25261
+ };
25262
+ }
25263
+ if (projectText === "") {
25264
+ return { ok: false, refusal: `loop.continue.criteriaMode is replace, and ${path3} holds no criteria` };
25265
+ }
25266
+ return { ok: true, criteria: projectText, project };
25267
+ }
25268
+ const base = readBundledFile(CONTINUE_CRITERIA_FILE, moduleDir).trimEnd();
25269
+ const criteria = projectText === "" ? base : `${base}
25270
+
25271
+ ${PROJECT_CRITERIA_HEADING}
25272
+
25273
+ ${projectText}`;
25274
+ return { ok: true, criteria, project };
25275
+ }
25276
+ function fenced(text) {
25277
+ const fence = fenceFor(text);
25278
+ return `${fence}text
25279
+ ${text}
25280
+ ${fence}`;
25281
+ }
25282
+ function shownLine(lineNum) {
25283
+ return String(lineNum + 1);
25284
+ }
25285
+ function renderOpenTasks(tasks) {
25286
+ if (tasks.length === 0)
25287
+ return NO_OPEN_TASKS;
25288
+ return fenced(tasks.map((task) => `line ${shownLine(task.lineNum)}: ${task.task}`).join(`
25289
+ `));
25290
+ }
25291
+ function renderDecisionPrompt(template, input) {
25292
+ const missing = DECISION_PROMPT_SLOTS.find((slot) => !template.includes(`{{${slot}}}`));
25293
+ if (missing !== undefined) {
25294
+ throw new Error(`loop continue: the decision prompt carries no {{${missing}}} slot`);
25295
+ }
25296
+ const values = {
25297
+ plan: input.plan,
25298
+ line: shownLine(input.task.lineNum),
25299
+ task: fenced(input.task.task),
25300
+ holds: input.holds.length === 0 ? NO_HOLDS : fenced(input.holds.join(`
25301
+ `)),
25302
+ retriesLeft: String(input.retriesLeft),
25303
+ openTasks: renderOpenTasks(input.openTasks),
25304
+ criteria: input.criteria
25305
+ };
25306
+ return chooseRetrySections(template, input.retriesLeft).replace(SLOT_PATTERN2, (_whole, slot) => values[slot]);
25307
+ }
25308
+ function buildDecisionPrompt(input, moduleDir = MODULE_DIR4) {
25309
+ return renderDecisionPrompt(readBundledFile(DECISION_PROMPT_FILE, moduleDir), input);
25310
+ }
25311
+
25312
+ // src/start/continue-args.ts
25313
+ var CONTINUE_FLAG = "--continue";
25314
+ var DECIDE_FLAG = "--decide";
25315
+ var APPROACH_FLAG = "--approach";
25316
+ var AFTER_FLAG = "--after";
25317
+ var FORCE_WRAP_UP_FLAG = "--force-wrap-up";
25318
+ var DIRECTIVE_REASON = `named on the line with ${DECIDE_FLAG}`;
25319
+ var CONTINUE_OFF = Object.freeze({ on: false, directive: null, forceWrapUp: false });
25320
+ var LINE_NUMBER = /^[1-9][0-9]*$/;
25321
+ function refuse10(reason, hint) {
25322
+ throw new CommandExit(1, [`\u274C Refusing ${reason}: ${hint}`, NOTHING_DISPATCHED].join(`
25323
+ `));
25324
+ }
25325
+ function flagWords(args) {
25326
+ const end = args.indexOf("--");
25327
+ return end === -1 ? args : args.slice(0, end);
25328
+ }
25329
+ function lastValue(words, flag, shape) {
25330
+ if (words.includes(flag))
25331
+ refuse10(`a bare ${flag}`, `it takes a value as ${flag}=<${shape}>.`);
25332
+ const prefix = `${flag}=`;
25333
+ return [...words].reverse().find((word) => word.startsWith(prefix))?.slice(prefix.length);
25334
+ }
25335
+ function isStrategy2(value) {
25336
+ return DECISION_STRATEGIES.includes(value);
25337
+ }
25338
+ function readStrategy(words) {
25339
+ const typed = lastValue(words, DECIDE_FLAG, "strategy");
25340
+ if (typed === undefined)
25341
+ return;
25342
+ if (!isStrategy2(typed)) {
25343
+ refuse10(`${DECIDE_FLAG}=${typed}`, `it takes retry, stop, jump or defer as ${DECIDE_FLAG}=<strategy>.`);
25344
+ }
25345
+ return typed;
25346
+ }
25347
+ function readAfter(words) {
25348
+ const typed = lastValue(words, AFTER_FLAG, "line");
25349
+ if (typed === undefined)
25350
+ return;
25351
+ if (!LINE_NUMBER.test(typed)) {
25352
+ refuse10(`${AFTER_FLAG}=${typed}`, "it takes a tracker line number from 1, as the decision prompt shows lines.");
25353
+ }
25354
+ return Number(typed);
25355
+ }
25356
+ function directiveOf(strategy, approach, after) {
25357
+ if (approach !== undefined && strategy !== "retry") {
25358
+ refuse10(APPROACH_FLAG, `${APPROACH_FLAG} is read only beside ${DECIDE_FLAG}=retry.`);
25359
+ }
25360
+ if (after !== undefined && strategy !== "defer") {
25361
+ refuse10(AFTER_FLAG, `${AFTER_FLAG} is read only beside ${DECIDE_FLAG}=defer.`);
25362
+ }
25363
+ if (strategy === undefined)
25364
+ return null;
25365
+ if (strategy === "retry") {
25366
+ const text = approach?.trim() ?? "";
25367
+ if (text === "")
25368
+ refuse10(`${DECIDE_FLAG}=retry`, `${DECIDE_FLAG}=retry needs ${APPROACH_FLAG}=<text>, the new approach.`);
25369
+ return { strategy, reason: DIRECTIVE_REASON, approach: text };
25370
+ }
25371
+ if (strategy === "defer") {
25372
+ if (after === undefined)
25373
+ refuse10(`${DECIDE_FLAG}=defer`, `${DECIDE_FLAG}=defer needs ${AFTER_FLAG}=<line>, the task to run first.`);
25374
+ return { strategy, reason: DIRECTIVE_REASON, after };
25375
+ }
25376
+ return { strategy, reason: DIRECTIVE_REASON };
25377
+ }
25378
+ function readContinueArgs(args) {
25379
+ const words = flagWords(args);
25380
+ const on = words.includes(CONTINUE_FLAG);
25381
+ const forceWrapUp = words.includes(FORCE_WRAP_UP_FLAG);
25382
+ const strategy = readStrategy(words);
25383
+ const approach = lastValue(words, APPROACH_FLAG, "text");
25384
+ const after = readAfter(words);
25385
+ if (!on) {
25386
+ if (strategy !== undefined)
25387
+ refuse10(DECIDE_FLAG, `${DECIDE_FLAG} needs ${CONTINUE_FLAG}, the run it decides for.`);
25388
+ if (forceWrapUp)
25389
+ refuse10(FORCE_WRAP_UP_FLAG, `${FORCE_WRAP_UP_FLAG} needs ${CONTINUE_FLAG}, the run that passes tasks over.`);
25390
+ }
25391
+ const directive = directiveOf(strategy, approach, after);
25392
+ return on ? { on, directive, forceWrapUp } : CONTINUE_OFF;
25393
+ }
25394
+ function configuredRetries(config, continueOn) {
25395
+ return continueOn ? config.loopRetriesOnContinue : config.loopRetries;
25396
+ }
25397
+ function refuseRetryWithoutBudget(continueArgs, retries) {
25398
+ if (continueArgs.directive?.strategy !== "retry" || retries !== false)
25399
+ return;
25400
+ refuse10(`${DECIDE_FLAG}=retry`, "the run has no retry to spend: loop.retriesOnContinue is false and --retry names no count. Name one with --retry=<n>, or decide another strategy.");
25401
+ }
25402
+ function refuseUnusableCriteria(continueArgs, root, config) {
25403
+ if (!continueArgs.on)
25404
+ return;
25405
+ const reading2 = resolveContinueCriteria({
25406
+ root,
25407
+ path: config.loopContinueCriteria,
25408
+ mode: config.loopContinueCriteriaMode
25409
+ });
25410
+ if (reading2.ok)
25411
+ return;
25412
+ refuse10(CONTINUE_FLAG, `${reading2.refusal}. Write the criteria there, or set loop.continue.criteriaMode: extend.`);
25413
+ }
25414
+
25415
+ // src/start/continue-exits.ts
25416
+ var DECISION_STOP_EXIT = 20;
25417
+ var DECISION_NEEDED_EXIT = 21;
25418
+ var PASSED_OVER_EXIT = 22;
25419
+ var CONTINUE_EXITS = Object.freeze([
25420
+ DECISION_STOP_EXIT,
25421
+ DECISION_NEEDED_EXIT,
25422
+ PASSED_OVER_EXIT
25423
+ ]);
25424
+
25425
+ class LoopEnd extends CommandExit {
25426
+ constructor(exitCode, message) {
25427
+ super(exitCode, message);
25428
+ this.name = "LoopEnd";
25429
+ }
25430
+ }
25431
+
25432
+ // src/start/continue-run.ts
25433
+ import { existsSync as existsSync33, readFileSync as readFileSync40 } from "fs";
25434
+
25435
+ // src/start/decision-session.ts
25436
+ import { randomUUID as randomUUID9 } from "crypto";
25437
+
24639
25438
  // src/start/dispatch.ts
24640
25439
  import { randomUUID as randomUUID8 } from "crypto";
24641
25440
 
@@ -24708,13 +25507,13 @@ function bodyLines2(body) {
24708
25507
  function bodySections(body) {
24709
25508
  const lines = bodyLines2(body);
24710
25509
  const headings = [];
24711
- let fenced = false;
25510
+ let fenced2 = false;
24712
25511
  lines.forEach((line, index) => {
24713
25512
  if (FENCE_LINE6.test(line)) {
24714
- fenced = !fenced;
25513
+ fenced2 = !fenced2;
24715
25514
  return;
24716
25515
  }
24717
- if (fenced)
25516
+ if (fenced2)
24718
25517
  return;
24719
25518
  const match = HEADING_LINE2.exec(line);
24720
25519
  if (match === null)
@@ -24740,7 +25539,7 @@ function bodySections(body) {
24740
25539
  }
24741
25540
  function numberedRuns(body) {
24742
25541
  const runs = [];
24743
- let fenced = false;
25542
+ let fenced2 = false;
24744
25543
  let steps = 0;
24745
25544
  let line = 0;
24746
25545
  const close = () => {
@@ -24750,10 +25549,10 @@ function numberedRuns(body) {
24750
25549
  };
24751
25550
  bodyLines2(body).forEach((text, index) => {
24752
25551
  if (FENCE_LINE6.test(text)) {
24753
- fenced = !fenced;
25552
+ fenced2 = !fenced2;
24754
25553
  return;
24755
25554
  }
24756
- if (fenced || text.trim() === "")
25555
+ if (fenced2 || text.trim() === "")
24757
25556
  return;
24758
25557
  if (NUMBERED_ITEM.test(text)) {
24759
25558
  if (steps === 0)
@@ -24835,13 +25634,13 @@ function firstSection(sections2, heading) {
24835
25634
  }
24836
25635
  function withoutFences(text) {
24837
25636
  const kept = [];
24838
- let fenced = false;
25637
+ let fenced2 = false;
24839
25638
  for (const line of bodyLines2(text)) {
24840
25639
  if (FENCE_LINE6.test(line)) {
24841
- fenced = !fenced;
25640
+ fenced2 = !fenced2;
24842
25641
  continue;
24843
25642
  }
24844
- if (!fenced)
25643
+ if (!fenced2)
24845
25644
  kept.push(line);
24846
25645
  }
24847
25646
  return kept;
@@ -24894,13 +25693,13 @@ function extractedDate(body) {
24894
25693
  return match?.[1] ?? null;
24895
25694
  }
24896
25695
  function errorString(body) {
24897
- let fenced = false;
25696
+ let fenced2 = false;
24898
25697
  for (const line of bodyLines2(body)) {
24899
25698
  if (FENCE_LINE6.test(line)) {
24900
- fenced = !fenced;
25699
+ fenced2 = !fenced2;
24901
25700
  continue;
24902
25701
  }
24903
- if (fenced) {
25702
+ if (fenced2) {
24904
25703
  if (ERROR_WORDS.test(line))
24905
25704
  return line.trim();
24906
25705
  continue;
@@ -25046,7 +25845,7 @@ function reportLessons(record, context, now) {
25046
25845
 
25047
25846
  // src/effort/store/absences.ts
25048
25847
  import { randomUUID as randomUUID5 } from "crypto";
25049
- import { existsSync as existsSync24 } from "fs";
25848
+ import { existsSync as existsSync27 } from "fs";
25050
25849
  var ABSENCE_REASONS = {
25051
25850
  "no-block": true,
25052
25851
  "unclosed-block": true,
@@ -25106,7 +25905,7 @@ function writeReportAbsence(repoRoot, write, seams = {}) {
25106
25905
  write.outcome,
25107
25906
  collectedAt
25108
25907
  ];
25109
- const appended3 = withSqliteStore(path3, "write", !existsSync24(path3), (db) => db.query(INSERT_ABSENCE).run(...values).changes);
25908
+ const appended3 = withSqliteStore(path3, "write", !existsSync27(path3), (db) => db.query(INSERT_ABSENCE).run(...values).changes);
25110
25909
  return { path: path3, appended: appended3, skipped: 1 - appended3 };
25111
25910
  }
25112
25911
 
@@ -25369,8 +26168,8 @@ function renderLessonsSection(lessons) {
25369
26168
  }
25370
26169
 
25371
26170
  // src/utils/progress.ts
25372
- import { existsSync as existsSync25, writeFileSync as writeFileSync12 } from "fs";
25373
- import { join as join27 } from "path";
26171
+ import { existsSync as existsSync28, writeFileSync as writeFileSync12 } from "fs";
26172
+ import { join as join31 } from "path";
25374
26173
  var PROGRESS_FILE_NAME = "progress.txt";
25375
26174
  var PROGRESS_CAP_BYTES = 16000;
25376
26175
  var LISTED_FIELDS = [
@@ -25412,7 +26211,7 @@ var ESCAPED_CODE_POINTS = new Set([
25412
26211
  8233
25413
26212
  ]);
25414
26213
  function progressFilePath(repoRoot) {
25415
- return join27(repoRoot, PROGRESS_FILE_NAME);
26214
+ return join31(repoRoot, PROGRESS_FILE_NAME);
25416
26215
  }
25417
26216
  function escapeValue(value) {
25418
26217
  let escaped = "";
@@ -25473,7 +26272,7 @@ function renderProgressText(findings, capBytes = PROGRESS_CAP_BYTES) {
25473
26272
  }
25474
26273
  function readProgressFindings(repoRoot, planStub2) {
25475
26274
  const path3 = sqliteStorePath(repoRoot);
25476
- if (!existsSync25(path3))
26275
+ if (!existsSync28(path3))
25477
26276
  return [];
25478
26277
  const rows = withSqliteStore(path3, "read", false, (db) => {
25479
26278
  const query = db.query(SELECT_FINDINGS2);
@@ -25499,7 +26298,7 @@ function writeProgress(repoRoot, planStub2, dir = repoRoot) {
25499
26298
  }
25500
26299
 
25501
26300
  // src/inventory/search/rank.ts
25502
- import { readFileSync as readFileSync33 } from "fs";
26301
+ import { readFileSync as readFileSync36 } from "fs";
25503
26302
  var SEARCH_LIMIT = 12;
25504
26303
  var MIN_WORD_LENGTH = 2;
25505
26304
  var MIN_PREFIX_LENGTH = 4;
@@ -25621,7 +26420,7 @@ function rankCandidates(question, candidates, limit = SEARCH_LIMIT) {
25621
26420
  return [];
25622
26421
  return candidates.map((candidate) => scoreCandidate(candidate, words)).filter((ranked) => ranked.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
25623
26422
  }
25624
- function readRankCandidate(record, read = (path3) => readFileSync33(path3, "utf8")) {
26423
+ function readRankCandidate(record, read = (path3) => readFileSync36(path3, "utf8")) {
25625
26424
  let text;
25626
26425
  try {
25627
26426
  text = read(record.path);
@@ -25700,7 +26499,7 @@ function renderSkillIndex(resolution) {
25700
26499
  // src/task/resolve-skills.ts
25701
26500
  var RANK_FLOOR = 0;
25702
26501
  var TAG_LIMIT = 3;
25703
- function summaryOf2(row) {
26502
+ function summaryOf3(row) {
25704
26503
  const { summary } = row;
25705
26504
  return typeof summary === "string" ? summary : "";
25706
26505
  }
@@ -25709,7 +26508,7 @@ function offered(item) {
25709
26508
  name: item.name,
25710
26509
  source: item.winner.source,
25711
26510
  path: item.winner.path,
25712
- description: summaryOf2(item.winner)
26511
+ description: summaryOf3(item.winner)
25713
26512
  };
25714
26513
  }
25715
26514
  function asRecord(row) {
@@ -25822,14 +26621,14 @@ function taskInputFor(taskInfo, text, declaration, planContent) {
25822
26621
  stageContext: stageContextOf(taskInfo, planContent)
25823
26622
  };
25824
26623
  }
25825
- function messageOf7(error) {
26624
+ function messageOf9(error) {
25826
26625
  return error instanceof Error ? error.message : String(error);
25827
26626
  }
25828
26627
  async function pullBundle(learning, repoRoot) {
25829
26628
  try {
25830
26629
  return await taskLearningAdapter(learning, repoRoot).pullBlessed();
25831
26630
  } catch (error) {
25832
- activeOutput().warn(` Lessons: none handed to this task: the \`${learning.kind}\` learning adapter answered no blessed set: ${messageOf7(error)}`);
26631
+ activeOutput().warn(` Lessons: none handed to this task: the \`${learning.kind}\` learning adapter answered no blessed set: ${messageOf9(error)}`);
25833
26632
  activeOutput().warn(" The task is not failed for it: it is dispatched without a lessons section.");
25834
26633
  return null;
25835
26634
  }
@@ -25870,17 +26669,17 @@ function renderInheritedSection(failures) {
25870
26669
 
25871
26670
  // src/start/preflight.ts
25872
26671
  import { randomUUID as randomUUID7 } from "crypto";
25873
- import { existsSync as existsSync28, readFileSync as readFileSync35 } from "fs";
26672
+ import { existsSync as existsSync31, readFileSync as readFileSync38 } from "fs";
25874
26673
  import { homedir as homedir7 } from "os";
25875
- import { basename as basename8, join as join28 } from "path";
26674
+ import { basename as basename8, join as join32 } from "path";
25876
26675
 
25877
26676
  // src/effort/store/schema-report.ts
25878
- import { existsSync as existsSync26 } from "fs";
26677
+ import { existsSync as existsSync29 } from "fs";
25879
26678
  import { Database } from "bun:sqlite";
25880
26679
  var NO_NEXT_STEP = "none";
25881
26680
  function readSchemaReport(path3, options = {}) {
25882
26681
  guardTestProcess(path3);
25883
- if (!existsSync26(path3))
26682
+ if (!existsSync29(path3))
25884
26683
  return absentReport(path3);
25885
26684
  const catalogue = sqliteCatalogue(options.migrations ?? SQLITE_MIGRATIONS);
25886
26685
  const store = readReadOnly(path3);
@@ -25988,11 +26787,11 @@ function developmentApplied(report2) {
25988
26787
  }
25989
26788
 
25990
26789
  // src/preflight/first-dispatch.ts
25991
- import { readFileSync as readFileSync34 } from "fs";
26790
+ import { readFileSync as readFileSync37 } from "fs";
25992
26791
  var TICKED_TASK = /^- \[[xX]\] (.+)/;
25993
26792
  function readIfReadable(path3) {
25994
26793
  try {
25995
- return readFileSync34(path3, "utf8");
26794
+ return readFileSync37(path3, "utf8");
25996
26795
  } catch {
25997
26796
  return null;
25998
26797
  }
@@ -26019,7 +26818,7 @@ function isFirstDispatch(planPath) {
26019
26818
  }
26020
26819
 
26021
26820
  // src/claims/device.ts
26022
- import { existsSync as existsSync27 } from "fs";
26821
+ import { existsSync as existsSync30 } from "fs";
26023
26822
  var NO_CLAIMANT = "no claim can name this device";
26024
26823
  function ndjsonReason() {
26025
26824
  return "this project's effort store is NDJSON (store: ndjson), which records no store id," + ` so ${NO_CLAIMANT}. Next safe step: ${MOVE_TO_SQLITE}`;
@@ -26034,7 +26833,7 @@ function readDeviceStoreId(repoRoot, config) {
26034
26833
  if (config.store === "ndjson")
26035
26834
  return { ok: false, cause: "ndjson", reason: ndjsonReason() };
26036
26835
  const path3 = sqliteStorePath(repoRoot);
26037
- if (!existsSync27(path3))
26836
+ if (!existsSync30(path3))
26038
26837
  return { ok: false, cause: "absent", reason: absentReason(path3) };
26039
26838
  const meta = withSqliteStore(path3, "read", false, (db) => readStoreMeta(db));
26040
26839
  return meta === null ? { ok: false, cause: "unminted", reason: unmintedReason(path3) } : { ok: true, storeId: meta.storeId };
@@ -26453,11 +27252,11 @@ function knownMissingNotice(lines) {
26453
27252
  }
26454
27253
  function agentSourcePath(planPath) {
26455
27254
  const tracker = trackerPathFor(planPath);
26456
- return existsSync28(tracker) ? tracker : planPath;
27255
+ return existsSync31(tracker) ? tracker : planPath;
26457
27256
  }
26458
27257
  function readIfReadable2(path3) {
26459
27258
  try {
26460
- return readFileSync35(path3, "utf8");
27259
+ return readFileSync38(path3, "utf8");
26461
27260
  } catch {
26462
27261
  return null;
26463
27262
  }
@@ -26592,7 +27391,7 @@ function announceReminders(planPath, reminders) {
26592
27391
  function announceUnknownMigrations(repoRoot) {
26593
27392
  const output = activeOutput();
26594
27393
  try {
26595
- const report2 = readSchemaReport(join28(effortStoreDir(repoRoot), SQLITE_STORE_FILE_NAME));
27394
+ const report2 = readSchemaReport(join32(effortStoreDir(repoRoot), SQLITE_STORE_FILE_NAME));
26596
27395
  for (const migration of unknownAdditive(report2))
26597
27396
  output.warn(unknownAdditiveWarning(migration));
26598
27397
  } catch (error) {
@@ -26669,6 +27468,294 @@ function withStamp(prompt) {
26669
27468
  return activePlanStub === null ? prompt : stampPrompt(activePlanStub, prompt);
26670
27469
  }
26671
27470
 
27471
+ // src/start/type-step.ts
27472
+ import { existsSync as existsSync32, mkdtempSync, realpathSync as realpathSync4, rmSync as rmSync4, symlinkSync as symlinkSync2, writeFileSync as writeFileSync13 } from "fs";
27473
+ import { tmpdir } from "os";
27474
+ import { dirname as dirname15, join as join33, relative as relative8, resolve as resolve20, sep as sep2 } from "path";
27475
+ var TEST_FILE_SUFFIX = ".test.ts";
27476
+ var TSC_FLAGS = ["--noEmit", "--pretty", "false"];
27477
+ var ERRORS_LISTED = 10;
27478
+ var NODE_MODULES = "node_modules";
27479
+ var TSC_BIN = [".bin", "tsc"];
27480
+ var SCRATCH_NAME = "tsconfig.json";
27481
+ var FILE_ERROR = /^(.+)\((\d+),(\d+)\): error (TS\d+): (.*)$/;
27482
+ var GLOBAL_ERROR = /^error (TS\d+): (.*)$/;
27483
+ var NOTHING_RAN = { ran: false, red: false, interrupted: false, blocker: null };
27484
+ var GREEN = { ran: true, red: false, interrupted: false, blocker: null };
27485
+ var INTERRUPTED = { ran: true, red: false, interrupted: true, blocker: null };
27486
+ async function runTsc(options) {
27487
+ const proc = Bun.spawn([...options.argv], {
27488
+ cwd: options.cwd,
27489
+ stdin: "ignore",
27490
+ stdout: "pipe",
27491
+ stderr: "pipe"
27492
+ });
27493
+ const [stdout, stderr, exitCode] = await Promise.all([
27494
+ new Response(proc.stdout).text(),
27495
+ new Response(proc.stderr).text(),
27496
+ proc.exited
27497
+ ]);
27498
+ return { exitCode, stdout, stderr };
27499
+ }
27500
+ function typeLabel(task) {
27501
+ return `type step after "${task}"`;
27502
+ }
27503
+ function pathsOf(status) {
27504
+ return /^[RC]/.test(status) ? 2 : 1;
27505
+ }
27506
+ function readTypeFiles(git, base) {
27507
+ const result = git(["diff", "--name-status", "-z", "--find-renames", "--diff-filter=d", base, "HEAD"]);
27508
+ if (!result.ok) {
27509
+ activeOutput().warn(`\u26A0\uFE0F git diff from ${base} did not answer (${gitSaid(result) || "nothing said"}); the type step runs nothing.`);
27510
+ return null;
27511
+ }
27512
+ const fields = result.stdout.split("\x00").filter((field4) => field4 !== "");
27513
+ const files = [];
27514
+ for (let index = 0;index < fields.length; ) {
27515
+ const status = fields[index] ?? "";
27516
+ const count = pathsOf(status);
27517
+ const paths = fields.slice(index + 1, index + 1 + count);
27518
+ index += 1 + count;
27519
+ const head = paths[paths.length - 1];
27520
+ if (head === undefined || !head.endsWith(TEST_FILE_SUFFIX))
27521
+ continue;
27522
+ files.push({ head, base: status.startsWith("A") ? null : paths[0] ?? null });
27523
+ }
27524
+ return files;
27525
+ }
27526
+ function realPath(path3) {
27527
+ try {
27528
+ return realpathSync4(path3);
27529
+ } catch {
27530
+ return resolve20(path3);
27531
+ }
27532
+ }
27533
+ function walkStop(git) {
27534
+ const result = git(["rev-parse", "--path-format=absolute", "--git-common-dir"]);
27535
+ const common = result.stdout.trim();
27536
+ return result.ok && common !== "" ? dirname15(realPath(common)) : null;
27537
+ }
27538
+ function isUnder(dir, root) {
27539
+ return dir === root || dir.startsWith(root.endsWith(sep2) ? root : `${root}${sep2}`);
27540
+ }
27541
+ function findNodeModules(checkout, git) {
27542
+ const start = realPath(checkout);
27543
+ const stop = walkStop(git);
27544
+ const top = stop !== null && isUnder(start, stop) ? stop : start;
27545
+ for (let dir = start;; dir = dirname15(dir)) {
27546
+ const modules = join33(dir, NODE_MODULES);
27547
+ if (existsSync32(join33(modules, ...TSC_BIN)))
27548
+ return modules;
27549
+ if (dir === top || dirname15(dir) === dir)
27550
+ return null;
27551
+ }
27552
+ }
27553
+ function scratchTsconfigFields(tree, files, modules) {
27554
+ const root = resolve20(tree);
27555
+ return {
27556
+ extends: join33(root, "tsconfig.json"),
27557
+ compilerOptions: { typeRoots: [join33(resolve20(modules), "@types")] },
27558
+ files: files.map((file) => join33(root, file)),
27559
+ include: []
27560
+ };
27561
+ }
27562
+ function scratchTsconfig(tree, files, modules) {
27563
+ return `${JSON.stringify(scratchTsconfigFields(tree, files, modules), null, 2)}
27564
+ `;
27565
+ }
27566
+ function tscArgv(modules, scratch) {
27567
+ return [join33(modules, ...TSC_BIN), "-p", scratch, ...TSC_FLAGS];
27568
+ }
27569
+ function modulesFor(checkout, git) {
27570
+ return findNodeModules(checkout, git) ?? join33(resolve20(checkout), NODE_MODULES);
27571
+ }
27572
+ function readTypeCheckRecipe(checkout, git) {
27573
+ if (!existsSync32(join33(checkout, "tsconfig.json")))
27574
+ return null;
27575
+ const modules = findNodeModules(checkout, git);
27576
+ return modules === null ? null : { checkout, modules };
27577
+ }
27578
+ function parseTscOutput(stdout, cwd) {
27579
+ const errors = [];
27580
+ for (const line of stdout.split(`
27581
+ `)) {
27582
+ const atFile = FILE_ERROR.exec(line);
27583
+ const global = atFile === null ? GLOBAL_ERROR.exec(line) : null;
27584
+ const last = errors[errors.length - 1];
27585
+ if (atFile !== null) {
27586
+ const [, file = "", row = "0", column = "0", code = "", message = ""] = atFile;
27587
+ errors.push({ file: relative8(cwd, resolve20(cwd, file)), line: Number(row), column: Number(column), code, message });
27588
+ } else if (global !== null) {
27589
+ errors.push({ file: null, line: 0, column: 0, code: global[1] ?? "", message: global[2] ?? "" });
27590
+ } else if (line.startsWith(" ") && last !== undefined) {
27591
+ errors[errors.length - 1] = { ...last, message: `${last.message}
27592
+ ${line.trimEnd()}` };
27593
+ }
27594
+ }
27595
+ return errors;
27596
+ }
27597
+ function identity(error) {
27598
+ return `${error.code} ${error.message}`;
27599
+ }
27600
+ function newErrors(head, base) {
27601
+ const left = new Map;
27602
+ for (const error of base) {
27603
+ const key = `${error.file ?? ""}\x00${identity(error)}`;
27604
+ left.set(key, (left.get(key) ?? 0) + 1);
27605
+ }
27606
+ return head.filter((error) => {
27607
+ const key = `${error.file ?? ""}\x00${identity(error)}`;
27608
+ const count = left.get(key) ?? 0;
27609
+ if (count === 0)
27610
+ return true;
27611
+ left.set(key, count - 1);
27612
+ return false;
27613
+ });
27614
+ }
27615
+ function errorLine(error) {
27616
+ const first = error.message.split(`
27617
+ `)[0] ?? "";
27618
+ return `${error.file ?? "(no file)"}:${error.line}:${error.column} ${error.code} ${first}`;
27619
+ }
27620
+ function listed(errors) {
27621
+ const shown2 = errors.slice(0, ERRORS_LISTED).map(errorLine);
27622
+ return errors.length > ERRORS_LISTED ? [...shown2, `...and ${errors.length - ERRORS_LISTED} more`] : shown2;
27623
+ }
27624
+ function typeBlockerText(label, base, errors) {
27625
+ const files = [...new Set(errors.map((error) => error.file ?? ""))];
27626
+ const shown2 = listed(errors).join("; ").replace(/\.$/u, "");
27627
+ return `The runner's ${label} found type errors in the task's test files that ${base} did not hold. ` + `New errors: ${shown2}. ` + `Fix them, then check ${files.join(" ")} with tsc ${TSC_FLAGS.join(" ")} -p over a tsconfig outside the checkout ` + "that extends its tsconfig.json by absolute path and lists them under files.";
27628
+ }
27629
+ function brokenBlockerText(label, run, said) {
27630
+ return `The runner's ${label} could not type-check the task's test files: tsc exited ${run.exitCode}${said === undefined ? " and printed no error." : `: ${said}`}`;
27631
+ }
27632
+ function configError(errors, scratch, cwd) {
27633
+ const config = relative8(cwd, scratch);
27634
+ return errors.find((error) => error.file === null || error.file === config);
27635
+ }
27636
+ function firstLine6(run) {
27637
+ return `${run.stdout}
27638
+ ${run.stderr}`.split(`
27639
+ `).map((line) => line.trim()).find((line) => line !== "");
27640
+ }
27641
+ async function checkTree(input, modules, tree, files) {
27642
+ const scratchDir = mkdtempSync(join33(tmpdir(), "rafa-type-step-"));
27643
+ const scratch = join33(scratchDir, SCRATCH_NAME);
27644
+ try {
27645
+ writeFileSync13(scratch, scratchTsconfig(tree, files, modules), "utf8");
27646
+ let run;
27647
+ try {
27648
+ run = await (input.runTypes ?? runTsc)({ cwd: tree, argv: tscArgv(modules, scratch) });
27649
+ } catch (error) {
27650
+ return { kind: "threw", why: messageOf(error) };
27651
+ }
27652
+ if (run.exitCode === input.stopCode || input.isInterrupted?.() === true)
27653
+ return { kind: "interrupted" };
27654
+ const errors = parseTscOutput(run.stdout, tree);
27655
+ const config = configError(errors, scratch, tree);
27656
+ if (config !== undefined)
27657
+ return { kind: "broken", run, said: `error ${config.code}: ${config.message.split(`
27658
+ `)[0] ?? ""}` };
27659
+ if (run.exitCode !== 0 && errors.length === 0)
27660
+ return { kind: "broken", run, said: firstLine6(run) };
27661
+ const wanted = new Set(files);
27662
+ return { kind: "read", errors: errors.filter((error) => error.file !== null && wanted.has(error.file)) };
27663
+ } finally {
27664
+ rmSync4(scratchDir, { recursive: true, force: true });
27665
+ }
27666
+ }
27667
+ async function inBaseTree(input, label, modules, check) {
27668
+ const parent = mkdtempSync(join33(tmpdir(), "rafa-type-base-"));
27669
+ const tree = join33(parent, "tree");
27670
+ try {
27671
+ const added = input.git(["worktree", "add", "--detach", tree, input.base]);
27672
+ if (!added.ok) {
27673
+ activeOutput().warn(`\u26A0\uFE0F The ${label} could not check out ${input.base} (${gitSaid(added) || "nothing said"}); it reads no error as new.`);
27674
+ return null;
27675
+ }
27676
+ if (existsSync32(modules) && !existsSync32(join33(tree, NODE_MODULES)))
27677
+ symlinkSync2(modules, join33(tree, NODE_MODULES));
27678
+ if (!existsSync32(join33(tree, "tsconfig.json")))
27679
+ return { kind: "read", errors: [] };
27680
+ return await check(tree);
27681
+ } finally {
27682
+ if (existsSync32(tree)) {
27683
+ const removed = input.git(["worktree", "remove", "--force", tree]);
27684
+ if (!removed.ok)
27685
+ activeOutput().warn(`\u26A0\uFE0F The ${label} could not remove its worktree ${tree} (${gitSaid(removed) || "nothing said"}).`);
27686
+ }
27687
+ rmSync4(parent, { recursive: true, force: true });
27688
+ }
27689
+ }
27690
+ async function baseErrors(input, label, modules, files) {
27691
+ const pairs = files.filter((file) => file.base !== null);
27692
+ if (pairs.length === 0)
27693
+ return { errors: [] };
27694
+ const run = await inBaseTree(input, label, modules, (tree) => checkTree(input, modules, tree, pairs.map((file) => file.base)));
27695
+ if (run === null)
27696
+ return { outcome: GREEN };
27697
+ if (run.kind === "interrupted")
27698
+ return { outcome: interrupted(label) };
27699
+ if (run.kind !== "read") {
27700
+ const why = run.kind === "threw" ? run.why : brokenBlockerText(label, run.run, run.said);
27701
+ activeOutput().warn(`\u26A0\uFE0F The ${label} could not check the files at ${input.base} (${why}); it reads no error as new.`);
27702
+ return { outcome: GREEN };
27703
+ }
27704
+ const headOf = new Map(pairs.map((file) => [file.base, file.head]));
27705
+ return { errors: run.errors.map((error) => ({ ...error, file: headOf.get(error.file ?? "") ?? error.file })) };
27706
+ }
27707
+ function interrupted(label) {
27708
+ activeOutput().info(`\u23F9 The ${label} was interrupted by SIGINT: read as a stop, not as errors.`);
27709
+ return INTERRUPTED;
27710
+ }
27711
+ function filesToCheck(input, label) {
27712
+ if (!existsSync32(join33(input.checkout, "tsconfig.json"))) {
27713
+ activeOutput().info(`\uD83D\uDD0E ${label}: no tsconfig.json at the checkout root; nothing to type-check.`);
27714
+ return null;
27715
+ }
27716
+ const files = readTypeFiles(input.git, input.base);
27717
+ if (files === null)
27718
+ return null;
27719
+ if (files.length > 0)
27720
+ return files;
27721
+ activeOutput().info(`\uD83D\uDD0E ${label}: the task changed no ${TEST_FILE_SUFFIX} file; nothing to type-check.`);
27722
+ return null;
27723
+ }
27724
+ async function runTypeStep(input) {
27725
+ const label = typeLabel(input.task);
27726
+ const files = filesToCheck(input, label);
27727
+ if (files === null)
27728
+ return NOTHING_RAN;
27729
+ const modules = modulesFor(input.checkout, input.git);
27730
+ const head = await checkTree(input, modules, input.checkout, files.map((file) => file.head));
27731
+ if (head.kind === "threw") {
27732
+ activeOutput().warn(`\u26A0\uFE0F The ${label} could not run tsc (${head.why}); the run goes on without it.`);
27733
+ return NOTHING_RAN;
27734
+ }
27735
+ if (head.kind === "interrupted")
27736
+ return interrupted(label);
27737
+ if (head.kind === "broken") {
27738
+ const blocker = brokenBlockerText(label, head.run, head.said);
27739
+ activeOutput().info(`\uD83D\uDD0E ${label}: ${blocker}`);
27740
+ return { ran: true, red: true, interrupted: false, blocker };
27741
+ }
27742
+ const ran = `\uD83D\uDD0E ${label}: tsc over ${files.length} test file(s)`;
27743
+ if (head.errors.length === 0) {
27744
+ activeOutput().info(`${ran}: no type error.`);
27745
+ return GREEN;
27746
+ }
27747
+ const base = await baseErrors(input, label, modules, files.filter((file) => head.errors.some((error) => error.file === file.head)));
27748
+ if ("outcome" in base)
27749
+ return base.outcome;
27750
+ const added = newErrors(head.errors, base.errors);
27751
+ activeOutput().info(`${ran}: ${head.errors.length} error(s), ${head.errors.length - added.length} already held at ${input.base}, ${added.length} not held.`);
27752
+ for (const line of listed(added))
27753
+ activeOutput().info(` ${line}`);
27754
+ if (added.length === 0)
27755
+ return GREEN;
27756
+ return { ran: true, red: true, interrupted: false, blocker: typeBlockerText(label, input.base, added) };
27757
+ }
27758
+
26672
27759
  // src/start/task-gate-lines.ts
26673
27760
  var BASE_PROMPT_PREFIX = "The base commit of this task is ";
26674
27761
  var COMMIT_NAME = /^[0-9a-f]{7,64}$/;
@@ -26696,9 +27783,11 @@ function readAlwaysRunFiles(git, globs) {
26696
27783
  }
26697
27784
  return alwaysRunFiles(globs, result.stdout.split("\x00").filter((path3) => path3 !== ""));
26698
27785
  }
27786
+ function shellWord(word) {
27787
+ return SHELL_SAFE_PATH.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
27788
+ }
26699
27789
  function testArgument(path3) {
26700
- const prefixed = `./${path3}`;
26701
- return SHELL_SAFE_PATH.test(prefixed) ? prefixed : `'${prefixed.replaceAll("'", "'\\''")}'`;
27790
+ return shellWord(`./${path3}`);
26702
27791
  }
26703
27792
  function alwaysRunLines(files) {
26704
27793
  if (files.length === 0)
@@ -26706,6 +27795,17 @@ function alwaysRunLines(files) {
26706
27795
  const args = files.map(testArgument).join(" ");
26707
27796
  return [`Also run \`bun test ${args}\`: these are the \`tests.alwaysRun\` files, content sweeps no changed file selects.`];
26708
27797
  }
27798
+ var TYPE_CHECK_FILE = "<test file>";
27799
+ var TYPE_CHECK_SCRATCH = "<that tsconfig.json>";
27800
+ function typeCheckLines(recipe) {
27801
+ if (recipe === null)
27802
+ return [];
27803
+ const scratch = JSON.stringify(scratchTsconfigFields(recipe.checkout, [TYPE_CHECK_FILE], recipe.modules));
27804
+ const run = tscArgv(recipe.modules, TYPE_CHECK_SCRATCH).map((word) => word === TYPE_CHECK_SCRATCH ? word : shellWord(word)).join(" ");
27805
+ return [
27806
+ `Before you report done, type-check the \`*${TEST_FILE_SUFFIX}\` files your change adds or edits, if any, as the runner's type step will:` + ` write \`${scratch}\` to \`tsconfig.json\` in a new directory outside the checkout,` + ` one \`files\` entry per such file with \`${TYPE_CHECK_FILE}\` replaced by its path relative to the checkout, such as \`src/x${TEST_FILE_SUFFIX}\`` + ` (the entry already opens with the checkout's path), then run \`${run}\` in the checkout.` + " In a test file your change adds, fix every error it reports." + ` In one your change edits, fix the errors on the lines your change touches, which \`git diff <base> -- ${TYPE_CHECK_FILE}\` names` + " with `<base>` the base commit above, and leave the others: the file held them before this task."
27807
+ ];
27808
+ }
26709
27809
 
26710
27810
  // src/start/dispatch.ts
26711
27811
  var SESSION_ID_FLAG2 = "--session-id";
@@ -26722,13 +27822,14 @@ var NO_TASK_SECTIONS = { skills: "", lessons: "" };
26722
27822
  function sectionLines(sections2) {
26723
27823
  return [sections2.skills, sections2.lessons, sections2.inherited].filter((section) => section.trim().length > 0).flatMap((section) => [section, ""]);
26724
27824
  }
26725
- function buildTaskPrompt(taskText, promptContent, planText, knownMissing = [], blocker = null, sections2 = NO_TASK_SECTIONS, base = null, inherited = [], alwaysRun = []) {
27825
+ function buildTaskPrompt(taskText, promptContent, planText, knownMissing = [], blocker = null, sections2 = NO_TASK_SECTIONS, base = null, inherited = [], alwaysRun = [], typeCheck = null) {
26726
27826
  return [
26727
27827
  `Your scoped task is: ${taskText}`,
26728
27828
  "Consider tasks listed above this one in the plan checklist as completed. Do not re-evaluate or re-do them. Focus only on the scoped task.",
26729
27829
  ...blockerLines(blocker),
26730
27830
  ...baseLines(base),
26731
27831
  ...alwaysRunLines(alwaysRun),
27832
+ ...typeCheckLines(typeCheck),
26732
27833
  "",
26733
27834
  ...sectionLines({ ...sections2, inherited: renderInheritedSection(inherited) }),
26734
27835
  promptContent,
@@ -26772,7 +27873,7 @@ async function dispatchTask(options) {
26772
27873
  const resolution = options.serving === null ? EMPTY_RESOLUTION : resolveSessionTiers(options.serving);
26773
27874
  const task = taskInputFor(taskInfo, taskText, declaration, options.planContent);
26774
27875
  const handed = await handOut(options.handout, task, resolution, options.repoRoot);
26775
- const prompt = withStamp(buildTaskPrompt(taskText, options.promptContent, injection.text, options.knownMissing, taskInfo.blocker ?? null, { skills: renderSkillsSection(handed.skills), lessons: renderLessonsSection(handed.lessons) }, options.base, options.inherited, options.alwaysRun));
27876
+ const prompt = withStamp(buildTaskPrompt(taskText, options.promptContent, injection.text, options.knownMissing, taskInfo.blocker ?? null, { skills: renderSkillsSection(handed.skills), lessons: renderLessonsSection(handed.lessons) }, options.base, options.inherited, options.alwaysRun, options.typeCheck ?? null));
26776
27877
  const served = serveForSession(options.serving, resolution);
26777
27878
  const record = {
26778
27879
  taskText,
@@ -26805,7 +27906,7 @@ function serveForSession(serving, resolution) {
26805
27906
  activeOutput().warn(` ${skipped2.message}`);
26806
27907
  return served.flags;
26807
27908
  }
26808
- function messageOf8(error) {
27909
+ function messageOf10(error) {
26809
27910
  return error instanceof Error ? error.message : String(error);
26810
27911
  }
26811
27912
  function renderProgressForDispatch(repoRoot, planStub2, checkout = repoRoot) {
@@ -26818,7 +27919,7 @@ function renderProgressForDispatch(repoRoot, planStub2, checkout = repoRoot) {
26818
27919
  return true;
26819
27920
  } catch (error) {
26820
27921
  activeOutput().error(`
26821
- \u274C progress.txt could not be rendered from the findings store: ${messageOf8(error)}`);
27922
+ \u274C progress.txt could not be rendered from the findings store: ${messageOf10(error)}`);
26822
27923
  activeOutput().error(" Nothing was dispatched. Make the store readable, then run again.");
26823
27924
  return false;
26824
27925
  }
@@ -26842,7 +27943,7 @@ async function pushTaskLessons(options, record) {
26842
27943
  const rules = result.decisions.map((decision) => decision.rule).join(", ");
26843
27944
  activeOutput().info(` Pushed ${count} lesson(s) to the \`${learning.kind}\` learning adapter: ${rules}.`);
26844
27945
  } catch (error) {
26845
- activeOutput().warn(` ${count} lesson(s) of session ${dispatch.sessionId} were not pushed to the \`${learning.kind}\` learning adapter: ${messageOf8(error)}`);
27946
+ activeOutput().warn(` ${count} lesson(s) of session ${dispatch.sessionId} were not pushed to the \`${learning.kind}\` learning adapter: ${messageOf10(error)}`);
26846
27947
  activeOutput().warn(" The task is not failed for it: its report is stored, findings and all.");
26847
27948
  }
26848
27949
  }
@@ -26876,7 +27977,7 @@ async function storeTaskReport(options) {
26876
27977
  activeOutput().warn(` ${warning}`);
26877
27978
  } catch (error) {
26878
27979
  activeOutput().error(`
26879
- \u274C The report of session ${dispatch.sessionId} was not stored: ${messageOf8(error)}`);
27980
+ \u274C The report of session ${dispatch.sessionId} was not stored: ${messageOf10(error)}`);
26880
27981
  activeOutput().error(" The session printed it above as it ran.");
26881
27982
  return false;
26882
27983
  }
@@ -26884,101 +27985,570 @@ async function storeTaskReport(options) {
26884
27985
  return true;
26885
27986
  }
26886
27987
 
26887
- // src/start/loop-events.ts
26888
- import { appendFileSync, mkdirSync as mkdirSync11 } from "fs";
26889
- import { join as join29 } from "path";
26890
- var MINUTE_MS = 60000;
26891
- var TOKENS_PER_K = 1000;
26892
- function quoted2(text) {
26893
- return `"${oneLine(text).replaceAll('"', "\\\"")}"`;
27988
+ // src/start/decision-session.ts
27989
+ var DECISION_TOOLS = Object.freeze(["Read", "Grep", "Glob"]);
27990
+ function decisionFlags(sessionId) {
27991
+ return [SESSION_ID_FLAG2, sessionId, "--tools", DECISION_TOOLS.join(",")];
26894
27992
  }
26895
- function taskKind(position, verb) {
26896
- return padKind(`task ${position.index}/${position.total} ${verb}`);
27993
+ function runDecisionSession(input) {
27994
+ const id2 = (input.sessionId ?? randomUUID9)();
27995
+ return runClaudeCaptured(withStamp(input.prompt), input.settingSources, decisionFlags(id2), input.spawn, [], { cwd: input.checkout });
26897
27996
  }
26898
- function minutes(durationMs) {
26899
- return `${Math.max(1, Math.round(durationMs / MINUTE_MS))}m`;
27997
+
27998
+ // src/start/pass-over.ts
27999
+ var EMPTY_PASS_OVER = Object.freeze([]);
28000
+ function taskIdentity(text) {
28001
+ return stripTaskDeclaration(splitBlockerComment(text.trim()).text).trim();
26900
28002
  }
26901
- function summaryOf3(event) {
26902
- switch (event.kind) {
26903
- case "task-start":
26904
- return `${taskKind(event.position, "start")}${quoted2(event.text)}`;
26905
- case "task-done":
26906
- return event.tokens === null ? `${taskKind(event.position, "done")}${minutes(event.durationMs)}` : `${taskKind(event.position, "done")}${minutes(event.durationMs)} ${Math.round(event.tokens / TOKENS_PER_K)}k tokens`;
26907
- case "task-blocked":
26908
- return `${taskKind(event.position, "blocked")}${oneLine(event.reason)}`;
26909
- case "wrap-up":
26910
- return `${padKind("wrap-up")}${event.phase}`;
26911
- case "pr":
26912
- return `pr #${event.number} opened`;
26913
- case "no-pr":
26914
- return `${padKind("no pr")}${oneLine(event.reason)}`;
26915
- case "halt":
26916
- return `${padKind("halt")}${oneLine(event.reason)}`;
26917
- case "inherited":
26918
- return `${padKind("inherited")}${oneLine(`${event.file} > ${event.name}`)}`;
26919
- case "error":
26920
- return `${padKind("error")}${firstLine5(event.message)}`;
26921
- }
28003
+ function ordinalOf(ref8) {
28004
+ return ref8.ordinal ?? 1;
26922
28005
  }
26923
- function firstLine5(message) {
26924
- return message.split(`
26925
- `).map((line) => line.trim()).find((line) => line !== "") ?? "";
28006
+ function sameTask(a, b) {
28007
+ return a.task === b.task && ordinalOf(a) === ordinalOf(b);
26926
28008
  }
26927
- var EVENTS_EXTENSION = ".events.ndjson";
26928
- var bound = null;
26929
- function eventsFilePath(root, sessionId) {
26930
- if (!isSessionId(sessionId)) {
26931
- throw new Error(`events file: unusable session id ${JSON.stringify(sessionId)}`);
28009
+ function taskRefIn(task, trackerContent) {
28010
+ const identity2 = taskIdentity(task.task);
28011
+ const copies = listTrackerTasks(trackerContent).filter((line) => line.lineNum <= task.lineNum && taskIdentity(line.task) === identity2);
28012
+ return { lineNum: task.lineNum, task: identity2, ordinal: Math.max(1, copies.length) };
28013
+ }
28014
+ function locate(ref8, tasks) {
28015
+ const copies = tasks.filter((task) => taskIdentity(task.task) === ref8.task);
28016
+ return copies[ordinalOf(ref8) - 1] ?? null;
28017
+ }
28018
+ function movedTo(ref8, task) {
28019
+ return { lineNum: task.lineNum, task: ref8.task, ordinal: ordinalOf(ref8) };
28020
+ }
28021
+ function locateOpen(ref8, tasks) {
28022
+ const found = locate(ref8, tasks);
28023
+ return found === null || found.status === "done" ? null : found;
28024
+ }
28025
+ function stillPassedOver(entry, tasks) {
28026
+ const task = locateOpen(entry.task, tasks);
28027
+ if (task === null)
28028
+ return null;
28029
+ const moved = { ...entry, task: movedTo(entry.task, task) };
28030
+ if (entry.after === undefined)
28031
+ return moved;
28032
+ const after = locateOpen(entry.after, tasks);
28033
+ return after === null ? null : { ...moved, after: movedTo(entry.after, after) };
28034
+ }
28035
+ function addDecision(list, task, decision, trackerContent) {
28036
+ const ref8 = taskRefIn(task, trackerContent);
28037
+ const others = list.filter((entry) => !sameTask(entry.task, ref8));
28038
+ const { strategy, reason } = decision;
28039
+ if (strategy === "jump")
28040
+ return [...others, { task: ref8, strategy, reason }];
28041
+ if (strategy !== "defer" || decision.after === undefined)
28042
+ return others;
28043
+ const afterLine = decision.after - 1;
28044
+ const target = listOpenTasks(trackerContent).find((open) => open.lineNum === afterLine && open.lineNum !== task.lineNum);
28045
+ return target === undefined ? others : [...others, { task: ref8, strategy, reason, after: taskRefIn(target, trackerContent) }];
28046
+ }
28047
+ function markDone(list, task, trackerContent) {
28048
+ const done = taskRefIn(task, trackerContent);
28049
+ const waitsOnDone = (entry) => entry.after !== undefined && sameTask(entry.after, done);
28050
+ return list.filter((entry) => !sameTask(entry.task, done) && !waitsOnDone(entry));
28051
+ }
28052
+ function skippedLines(list, trackerContent) {
28053
+ return new Set(remaining(list, trackerContent).map((entry) => entry.task.lineNum));
28054
+ }
28055
+ function remaining(list, trackerContent) {
28056
+ if (list.length === 0)
28057
+ return EMPTY_PASS_OVER;
28058
+ const tasks = listTrackerTasks(trackerContent);
28059
+ return list.flatMap((entry) => stillPassedOver(entry, tasks) ?? []);
28060
+ }
28061
+ function seedFrom(list, trackerContent) {
28062
+ if (list.length === 0)
28063
+ return EMPTY_PASS_OVER;
28064
+ const tasks = listTrackerTasks(trackerContent);
28065
+ const isBlocked = (entry) => locate(entry.task, tasks)?.status === "blocked";
28066
+ return list.filter(isBlocked).flatMap((entry) => stillPassedOver(entry, tasks) ?? []);
28067
+ }
28068
+
28069
+ // src/start/suite-blocker.ts
28070
+ import { readFileSync as readFileSync39, writeFileSync as writeFileSync14 } from "fs";
28071
+ function failingFiles(failures) {
28072
+ const counts = new Map;
28073
+ for (const failure3 of failures)
28074
+ counts.set(failure3.file, (counts.get(failure3.file) ?? 0) + 1);
28075
+ return [...counts.entries()];
28076
+ }
28077
+ var UNHANDLED_NAMED = 10;
28078
+ function unhandledName(error) {
28079
+ const file = error.file ?? "a file Bun did not name";
28080
+ return error.firstLine === null ? `${file} threw an error Bun printed no line for` : `${file} threw "${error.firstLine}"`;
28081
+ }
28082
+ function unhandledNames(errors) {
28083
+ if (errors.length === 0)
28084
+ return "no block of Bun's stderr named a file";
28085
+ const named3 = errors.slice(0, UNHANDLED_NAMED).map(unhandledName);
28086
+ const left = errors.length - named3.length;
28087
+ const more = left > 0 ? [`and ${left} more`] : [];
28088
+ return [...named3, ...more].join("; ");
28089
+ }
28090
+ function unhandledFiles(errors) {
28091
+ return [...new Set(errors.flatMap((error) => error.file === null ? [] : [error.file]))];
28092
+ }
28093
+ function runnablePath(path3) {
28094
+ return path3.startsWith("/") || path3.startsWith("./") ? path3 : `./${path3}`;
28095
+ }
28096
+ function errorsText(newErrors2, errors) {
28097
+ const counted4 = `${newErrors2} more error(s) outside any test than the baseline`;
28098
+ if (errors.length === 0)
28099
+ return `${counted4}: a test file that throws while it loads, which the JUnit report names no file for.`;
28100
+ const files = unhandledFiles(errors);
28101
+ const run = files.length === 0 ? "" : ` Run bun test ${files.map(runnablePath).join(" ")} and make each load.`;
28102
+ return `${counted4}; Bun's stderr named them as ${unhandledNames(errors)}.${run}`;
28103
+ }
28104
+ function blockerText(label, result, verdict) {
28105
+ const files = failingFiles(verdict.fresh);
28106
+ const parts = [`The runner's ${label} found failures the suite baseline does not hold.`];
28107
+ if (files.length > 0) {
28108
+ const named3 = files.map(([file, count]) => `${file} (${count} ${count === 1 ? "test" : "tests"})`);
28109
+ parts.push(`New failing test files: ${named3.join(", ")}. Run bun test ${files.map(([file]) => runnablePath(file)).join(" ")} and make them pass.`);
26932
28110
  }
26933
- return join29(runsDir(root), `${sessionId}${EVENTS_EXTENSION}`);
28111
+ if (verdict.newErrors > 0)
28112
+ parts.push(errorsText(verdict.newErrors, result.unhandled));
28113
+ if (verdict.unreported)
28114
+ parts.push(`bun test exited ${result.exitCode} and printed no summary line.`);
28115
+ return parts.join(" ");
26934
28116
  }
26935
- function bindEventsFile(root, sessionId, warn2 = (line) => {
26936
- process.stderr.write(line);
26937
- }) {
26938
- const file = eventsFilePath(root, sessionId);
26939
- try {
26940
- mkdirSync11(runsDir(root), { recursive: true });
26941
- } catch {}
26942
- bound = { file, warn: warn2, warned: false };
26943
- return file;
28117
+ var REPAIR_AGENT = "build-error-resolver";
28118
+ var REPAIR_TASK = /^Repair the red (?:task|stage|pre-wrap-up) step at commit \S+$/;
28119
+ var PRE_WRAP_UP_REPAIR = /^Repair the red pre-wrap-up step at commit \S+$/;
28120
+ var COMMIT_SHOWN = 12;
28121
+ var TICKED_PREFIX = "- [x] ";
28122
+ function repairTaskText(kind, commit) {
28123
+ const shown2 = commit === null ? "(unread)" : commit.slice(0, COMMIT_SHOWN);
28124
+ return `Repair the red ${kind} step at commit ${shown2}`;
26944
28125
  }
26945
- function unbindEventsFile() {
26946
- bound = null;
28126
+ function isRepairTask(text) {
28127
+ return REPAIR_TASK.test(text.trim());
26947
28128
  }
26948
- function appendEventLine(line) {
26949
- if (bound === null)
26950
- return;
26951
- try {
26952
- appendFileSync(bound.file, `${JSON.stringify(line)}
26953
- `);
26954
- } catch (error) {
26955
- if (bound.warned)
26956
- return;
26957
- bound = { ...bound, warned: true };
26958
- bound.warn(`rafa: cannot write the events file ${bound.file}: ${messageOf(error)}
28129
+ function lineOfTask(content, text) {
28130
+ const found = parsePlan(content).tasks.filter((task) => task.text === text.trim());
28131
+ return found.at(-1)?.lineNum ?? null;
28132
+ }
28133
+ function lineOfTickedPreWrapUpRepair(content) {
28134
+ const found = parsePlan(content).tasks.filter((task) => task.status === "done" && PRE_WRAP_UP_REPAIR.test(task.text.trim()));
28135
+ return found.at(-1)?.lineNum ?? null;
28136
+ }
28137
+ function existingRepairOf(content, site) {
28138
+ if (site.kind === "pre-wrap-up")
28139
+ return lineOfTickedPreWrapUpRepair(content);
28140
+ return site.task !== undefined && isRepairTask(site.task) ? lineOfTask(content, site.task) : null;
28141
+ }
28142
+ function blockRepairAgain(trackerPath, content, line, blocker) {
28143
+ const lines = content.split(`
26959
28144
  `);
28145
+ const current = lines[line] ?? "";
28146
+ if (current.startsWith(TICKED_PREFIX)) {
28147
+ const reopened = [...lines.slice(0, line), `- [ ] ${current.slice(TICKED_PREFIX.length)}`, ...lines.slice(line + 1)];
28148
+ writeFileSync14(trackerPath, reopened.join(`
28149
+ `), "utf8");
26960
28150
  }
28151
+ return writeTrackerBlocker(trackerPath, line, blocker) ? { line, inserted: false } : null;
26961
28152
  }
26962
- function emitLoopEvent(event, now = () => new Date) {
26963
- const { kind, ...data } = event;
26964
- const line = { name: kind, summary: summaryOf3(event), data, ts: now().toISOString() };
26965
- appendEventLine(line);
26966
- activeOutput().emit({ type: "event", ...line });
28153
+ function writeRepairTask(trackerPath, blocker, site) {
28154
+ const content = readFileSync39(trackerPath, "utf8");
28155
+ const repaired = existingRepairOf(content, site);
28156
+ if (repaired !== null)
28157
+ return blockRepairAgain(trackerPath, content, repaired, blocker);
28158
+ const firstOpen = parsePlan(content).tasks.find((task) => task.status !== "done")?.lineNum ?? null;
28159
+ const entry = { task: repairTaskText(site.kind, site.commit), declaration: `agent=${REPAIR_AGENT}`, blocker };
28160
+ return { line: insertTrackerTask(trackerPath, entry, firstOpen), inserted: true };
26967
28161
  }
26968
- async function unlessText(read) {
26969
- return activeOutputMode() === "text" ? null : read();
28162
+
28163
+ // src/start/retry-budget.ts
28164
+ function resolveRunRetries(flag, configured) {
28165
+ return flag ?? configured;
26970
28166
  }
26971
- function taskPosition(trackerContent, lineNum) {
26972
- const { tasks } = parsePlan(trackerContent);
26973
- return { index: tasks.findIndex((task) => task.lineNum === lineNum) + 1, total: tasks.length };
28167
+ function openRetryBudget(retries) {
28168
+ return Object.freeze({
28169
+ of: retries === false ? 0 : retries,
28170
+ used: 0
28171
+ });
26974
28172
  }
26975
- async function taskTokens(checkout, sessionId, home) {
26976
- try {
26977
- const { usage } = await readSessionLog(join29(sessionLogDir(checkout, home), `${sessionId}.jsonl`));
26978
- return usage.inputTokens + usage.cacheCreationInputTokens + usage.outputTokens;
26979
- } catch {
26980
- return null;
28173
+ function takeRetry(budget) {
28174
+ if (budget.used >= budget.of)
28175
+ return { granted: false, budget };
28176
+ const used = budget.used + 1;
28177
+ return {
28178
+ granted: true,
28179
+ attempt: used,
28180
+ of: budget.of,
28181
+ budget: Object.freeze({ of: budget.of, used })
28182
+ };
28183
+ }
28184
+ function retryTaskOf(taskInfo, trackerContent) {
28185
+ const ref8 = taskRefIn(taskInfo, trackerContent);
28186
+ return Object.freeze({ key: `${String(ref8.ordinal)}:${ref8.task}`, repair: isRepairTask(ref8.task) });
28187
+ }
28188
+ function settleOnDone(budget, stoppedOn, done) {
28189
+ if (done.repair || done.key === stoppedOn)
28190
+ return budget;
28191
+ return Object.freeze({ of: budget.of, used: 0 });
28192
+ }
28193
+ function retryLine2(grant, reason) {
28194
+ return `\uD83D\uDD01 Retrying (retry ${grant.attempt} of ${grant.of} in a row) after the stop: ${reason}.` + " The loop goes on without a new rafa loop start.";
28195
+ }
28196
+ function movedLine2(reason) {
28197
+ return `\u26A0\uFE0F No retry after the stop: ${reason}. The checkout has moved from the loop's last commit,` + " so the task keeps its own stop.";
28198
+ }
28199
+ function createRunRetries(options) {
28200
+ let budget = openRetryBudget(options.retries);
28201
+ let refusal4 = null;
28202
+ let stoppedOn = null;
28203
+ const refuse11 = (why) => {
28204
+ refusal4 = why;
28205
+ return false;
28206
+ };
28207
+ const retry = (reason, stopped2) => {
28208
+ stoppedOn = stopped2?.key ?? null;
28209
+ if (options.isInterrupted())
28210
+ return refuse11("interrupted");
28211
+ const grant = takeRetry(budget);
28212
+ if (!grant.granted)
28213
+ return refuse11("spent");
28214
+ if (!options.isCheckoutHeld()) {
28215
+ activeOutput().warn(movedLine2(reason));
28216
+ return refuse11("checkout moved");
28217
+ }
28218
+ budget = grant.budget;
28219
+ refusal4 = null;
28220
+ activeOutput().warn(retryLine2(grant, reason));
28221
+ emitLoopEvent({ kind: "retry", attempt: grant.attempt, of: grant.of, reason });
28222
+ return true;
28223
+ };
28224
+ return {
28225
+ retry,
28226
+ taskDone: (done) => {
28227
+ budget = settleOnDone(budget, stoppedOn, done);
28228
+ },
28229
+ left: () => budget.of - budget.used,
28230
+ lastRefusal: () => refusal4
28231
+ };
28232
+ }
28233
+
28234
+ // src/start/continue-run.ts
28235
+ var SUITE_STEP_RED = "suite step red";
28236
+ var INTERRUPTED2 = "interrupted";
28237
+ var NO_LINES = new Set;
28238
+ var NO_DECISIONS = Object.freeze({
28239
+ skipLines: () => NO_LINES,
28240
+ atStop: () => Promise.resolve(false),
28241
+ atFirstTask: () => Promise.resolve(false),
28242
+ taskDone: () => {
28243
+ return;
28244
+ },
28245
+ atRunEnd: () => {
28246
+ return;
28247
+ },
28248
+ atPlanEnd: () => [],
28249
+ gateForcedWrapUp: (gate) => gate
28250
+ });
28251
+ function whenOf(stop) {
28252
+ if (stop.kind !== "clean-exit")
28253
+ return "spent";
28254
+ if (stop.finished.attempt.outcome === "failed")
28255
+ return "never";
28256
+ return heldOnNothingLeftBehind(stop.finished) ? "spent" : "now";
28257
+ }
28258
+ function openingList(seed, trackerPath) {
28259
+ if (seed.length === 0)
28260
+ return seed;
28261
+ const kept = existsSync33(trackerPath) ? seedFrom(seed, readFileSync40(trackerPath, "utf8")) : [];
28262
+ const back = seed.length - kept.length;
28263
+ if (back > 0) {
28264
+ activeOutput().info(`\u21A9\uFE0F ${back} task(s) the last --continue run passed over no longer read [BLOCKED], so this run takes them again.`);
26981
28265
  }
28266
+ return kept;
28267
+ }
28268
+ function boundKey(taskInfo, trackerContent) {
28269
+ const ref8 = taskRefIn(taskInfo, trackerContent);
28270
+ return `${String(ref8.ordinal)}:${ref8.task}`;
28271
+ }
28272
+ function refusedRetry(refusal4) {
28273
+ if (refusal4 === "checkout moved")
28274
+ return "retry was chosen, but the checkout has moved from the loop's last commit";
28275
+ if (refusal4 === "interrupted")
28276
+ return "retry was chosen, but the run was interrupted";
28277
+ return "retry was chosen with no retry left in a row";
28278
+ }
28279
+ function shownLine2(taskInfo) {
28280
+ return taskInfo.lineNum + 1;
28281
+ }
28282
+ function createRunDecisions(options) {
28283
+ if (!options.continueArgs.on)
28284
+ return NO_DECISIONS;
28285
+ const { trackerPath, retries, session } = options;
28286
+ const mode = options.mode ?? activeOutputMode;
28287
+ let directive = options.continueArgs.directive;
28288
+ let firstPass = true;
28289
+ const readTracker = () => readFileSync40(trackerPath, "utf8");
28290
+ let list = openingList(options.seed, trackerPath);
28291
+ const deferred = new Set;
28292
+ const passedOver = new Set;
28293
+ if (list.length > 0) {
28294
+ session.decisionsChanged(list);
28295
+ activeOutput().info(`\u23ED Passing over ${list.length} task(s) the plan's last --continue run passed over, while they stay [BLOCKED].`);
28296
+ }
28297
+ const skipLines = (trackerContent) => skippedLines(list, trackerContent);
28298
+ const subjectOf = (stop) => {
28299
+ if (stop.kind === "clean-exit")
28300
+ return { taskInfo: stop.taskInfo, retryTask: stop.retryTask, holds: stop.finished.holds, stopEvent: stop.stopEvent };
28301
+ if (stop.kind === "session-exit") {
28302
+ return { taskInfo: stop.taskInfo, retryTask: stop.retryTask, holds: [`session exited ${stop.exitCode}`], stopEvent: stop.stopEvent };
28303
+ }
28304
+ const content = readTracker();
28305
+ const blocked3 = findNextTask(content, { skipLines: skipLines(content) });
28306
+ if (blocked3?.status !== "blocked")
28307
+ return null;
28308
+ return {
28309
+ taskInfo: blocked3,
28310
+ retryTask: retryTaskOf(blocked3, content),
28311
+ holds: [blocked3.blocker ?? SUITE_STEP_RED],
28312
+ stopEvent: { kind: "halt", reason: SUITE_STEP_RED }
28313
+ };
28314
+ };
28315
+ const stopWith = (decision, subject) => {
28316
+ const line = shownLine2(subject.taskInfo);
28317
+ emitLoopEvent({ kind: "decision", strategy: "stop", line, reason: decision.reason });
28318
+ emitLoopEvent(subject.stopEvent);
28319
+ throw new LoopEnd(DECISION_STOP_EXIT, `\u26D4 The --continue decision on line ${line} is stop: ${decision.reason}`);
28320
+ };
28321
+ const promptFor = (subject, content) => {
28322
+ const criteria = resolveContinueCriteria({
28323
+ root: options.repoRoot,
28324
+ path: options.settings.loopContinueCriteria,
28325
+ mode: options.settings.loopContinueCriteriaMode
28326
+ });
28327
+ if (!criteria.ok)
28328
+ return { strategy: "stop", reason: `the criteria cannot be read, so the run stops: ${criteria.refusal}` };
28329
+ return buildDecisionPrompt({
28330
+ plan: options.planPath,
28331
+ task: subject.taskInfo,
28332
+ holds: subject.holds,
28333
+ retriesLeft: retries.left(),
28334
+ openTasks: listOpenTasks(content),
28335
+ criteria: criteria.criteria
28336
+ });
28337
+ };
28338
+ const needDecision = (subject, content, prompt) => {
28339
+ const line = shownLine2(subject.taskInfo);
28340
+ emitLoopEvent({
28341
+ kind: "decision-needed",
28342
+ task: subject.taskInfo.task,
28343
+ line,
28344
+ holds: subject.holds,
28345
+ openTasks: listOpenTasks(content).map((task) => ({ line: shownLine2(task), text: task.task })),
28346
+ retriesLeft: retries.left(),
28347
+ prompt
28348
+ });
28349
+ emitLoopEvent(subject.stopEvent);
28350
+ throw new LoopEnd(DECISION_NEEDED_EXIT, [
28351
+ `\uD83E\uDDED A decision is needed for the stop on line ${line}; the decision-needed event holds its prompt.`,
28352
+ " Run again with --continue --decide=retry|stop|jump|defer (--approach=<text> for retry, --after=<line> for defer)."
28353
+ ].join(`
28354
+ `));
28355
+ };
28356
+ const sessionDecision = async (subject, prompt) => {
28357
+ activeOutput().info(`
28358
+ \uD83E\uDDED Deciding how the run goes on after the stop on line ${shownLine2(subject.taskInfo)}: one read-only session (--continue).`);
28359
+ const answer4 = await runDecisionSession({
28360
+ prompt,
28361
+ settingSources: options.settingSources,
28362
+ checkout: options.checkout,
28363
+ ...options.spawn === undefined ? {} : { spawn: options.spawn }
28364
+ });
28365
+ if (options.isInterrupted())
28366
+ return null;
28367
+ if (answer4.exitCode !== 0) {
28368
+ return { strategy: "stop", reason: `the decision session exited ${answer4.exitCode}, so the run stops` };
28369
+ }
28370
+ return parseDecision(answer4.stdout);
28371
+ };
28372
+ const takeDirective = (subject) => {
28373
+ const taken = directive;
28374
+ if (taken === null)
28375
+ return null;
28376
+ directive = null;
28377
+ activeOutput().info(`
28378
+ \uD83E\uDDED Applying --decide=${taken.strategy}, named on the line, to line ${shownLine2(subject.taskInfo)}.`);
28379
+ return taken;
28380
+ };
28381
+ const decisionFor2 = async (subject, content) => {
28382
+ const directed = takeDirective(subject);
28383
+ if (directed !== null)
28384
+ return directed;
28385
+ const prompt = promptFor(subject, content);
28386
+ if (typeof prompt !== "string")
28387
+ return prompt;
28388
+ if (mode() === "json")
28389
+ needDecision(subject, content, prompt);
28390
+ return sessionDecision(subject, prompt);
28391
+ };
28392
+ const bounded = (decision, identity2) => {
28393
+ const passing = decision.strategy === "jump" || decision.strategy === "defer";
28394
+ if (passing && passedOver.has(identity2)) {
28395
+ return { strategy: "stop", reason: `the task was passed over once already in this run and stopped again, so the run stops: ${decision.reason}` };
28396
+ }
28397
+ if (decision.strategy === "defer" && deferred.has(identity2)) {
28398
+ return { strategy: "jump", reason: `the task was deferred once already in this run, so it is passed over as a jump: ${decision.reason}` };
28399
+ }
28400
+ return decision;
28401
+ };
28402
+ const passOver = (decision, subject, content, identity2) => {
28403
+ const line = shownLine2(subject.taskInfo);
28404
+ list = addDecision(list, subject.taskInfo, decision, content);
28405
+ session.decisionsChanged(list);
28406
+ if (decision.strategy === "defer")
28407
+ deferred.add(identity2);
28408
+ else
28409
+ passedOver.add(identity2);
28410
+ emitLoopEvent({ kind: "decision", strategy: decision.strategy, line, reason: decision.reason });
28411
+ const how = decision.strategy === "defer" ? `deferred until line ${String(decision.after)} is done` : "passed over for the rest of this run";
28412
+ activeOutput().warn(`\u23ED Line ${line} is ${how}: ${decision.reason}`);
28413
+ return true;
28414
+ };
28415
+ const interruptedDeciding = (subject) => {
28416
+ emitLoopEvent({ ...subject.stopEvent, reason: INTERRUPTED2 });
28417
+ throw new LoopEnd(0, `
28418
+ \u26A0\uFE0F Interrupted during the --continue decision on line ${shownLine2(subject.taskInfo)}: nothing was decided, and the task stays [BLOCKED]. Run again to resume.`);
28419
+ };
28420
+ const retryWith = (decision, subject) => {
28421
+ if (!retries.retry("the decision chose retry", subject.retryTask)) {
28422
+ return stopWith({ strategy: "stop", reason: `${refusedRetry(retries.lastRefusal())}, so the run stops: ${decision.reason}` }, subject);
28423
+ }
28424
+ const line = shownLine2(subject.taskInfo);
28425
+ writeTrackerBlocker(trackerPath, subject.taskInfo.lineNum, decision.approach ?? "");
28426
+ emitLoopEvent({ kind: "decision", strategy: "retry", line, reason: decision.reason });
28427
+ activeOutput().warn(`\uD83D\uDD01 Line ${line} is retried with a new approach: ${decision.reason}`);
28428
+ return true;
28429
+ };
28430
+ const apply = (decision, subject, content, identity2) => {
28431
+ switch (decision.strategy) {
28432
+ case "retry":
28433
+ return retryWith(decision, subject);
28434
+ case "stop":
28435
+ return stopWith(decision, subject);
28436
+ case "jump":
28437
+ case "defer":
28438
+ return passOver(decision, subject, content, identity2);
28439
+ }
28440
+ };
28441
+ const decide3 = async (subject) => {
28442
+ const content = readTracker();
28443
+ const decision = await decisionFor2(subject, content);
28444
+ if (decision === null)
28445
+ return interruptedDeciding(subject);
28446
+ const identity2 = boundKey(subject.taskInfo, content);
28447
+ return apply(bounded(decision, identity2), subject, content, identity2);
28448
+ };
28449
+ const atStop = async (stop) => {
28450
+ if (options.isInterrupted())
28451
+ return false;
28452
+ const when = whenOf(stop);
28453
+ if (when === "never")
28454
+ return false;
28455
+ const refusal4 = retries.lastRefusal();
28456
+ if (when === "spent" && refusal4 !== "spent")
28457
+ return false;
28458
+ const subject = subjectOf(stop);
28459
+ return subject === null ? false : decide3(subject);
28460
+ };
28461
+ const refuseMisdirected = (taskInfo, taken) => {
28462
+ const needed2 = options.previousNeeded ?? null;
28463
+ if (needed2 === null || taskIdentity(needed2.task) === taskIdentity(taskInfo.task))
28464
+ return;
28465
+ directive = null;
28466
+ throw new CommandExit(1, [
28467
+ `\u274C Refusing --decide=${taken.strategy}: the last run needed a decision on line ${needed2.line} "${taskIdentity(needed2.task)}",` + ` but this run opens on line ${shownLine2(taskInfo)} "${taskIdentity(taskInfo.task)}".`,
28468
+ " Check the tracker, then run again with --continue alone for a decision on the task this run opens on.",
28469
+ " Nothing was decided and nothing was dispatched."
28470
+ ].join(`
28471
+ `));
28472
+ };
28473
+ const atFirstTask = async (taskInfo, trackerContent) => {
28474
+ if (!firstPass)
28475
+ return false;
28476
+ firstPass = false;
28477
+ if (directive === null || taskInfo?.status !== "blocked")
28478
+ return false;
28479
+ refuseMisdirected(taskInfo, directive);
28480
+ const stopEvent = {
28481
+ kind: "task-blocked",
28482
+ position: taskPosition(trackerContent, taskInfo.lineNum),
28483
+ reason: `--decide=${directive.strategy}`
28484
+ };
28485
+ return decide3({
28486
+ taskInfo,
28487
+ retryTask: retryTaskOf(taskInfo, trackerContent),
28488
+ holds: [taskInfo.blocker ?? "blocked by an earlier run"],
28489
+ stopEvent
28490
+ });
28491
+ };
28492
+ const taskDone = (taskInfo) => {
28493
+ const next = markDone(list, taskInfo, readTracker());
28494
+ if (next.length === list.length)
28495
+ return;
28496
+ list = next;
28497
+ session.decisionsChanged(list);
28498
+ };
28499
+ let announced = false;
28500
+ const atPlanEnd = (trackerContent) => {
28501
+ const left = remaining(list, trackerContent);
28502
+ if (left.length === 0)
28503
+ return [];
28504
+ const tasks = left.map((entry) => ({
28505
+ line: entry.task.lineNum + 1,
28506
+ text: entry.task.task,
28507
+ strategy: entry.strategy,
28508
+ reason: entry.reason
28509
+ }));
28510
+ const forcing = options.continueArgs.forceWrapUp;
28511
+ if (!announced) {
28512
+ activeOutput().warn(forcing ? `
28513
+ \u23ED Every task left is one this run passed over; --force-wrap-up wraps up anyway, as a draft pull request listing them:` : `
28514
+ \u23ED Every task left is one this run passed over; no wrap-up runs and no pull request is opened:`);
28515
+ for (const task of tasks)
28516
+ activeOutput().warn(` line ${task.line} (${task.strategy}): ${task.text}
28517
+ ${task.reason}`);
28518
+ emitLoopEvent({ kind: "passed-over", tasks });
28519
+ announced = true;
28520
+ }
28521
+ if (forcing)
28522
+ return tasks;
28523
+ emitLoopEvent({ kind: "halt", reason: `passed over ${tasks.length} task(s)` });
28524
+ throw new LoopEnd(PASSED_OVER_EXIT, [
28525
+ `\u23ED The run ended with ${tasks.length} passed-over task(s) left [BLOCKED]. A run with --continue passes them over again.`,
28526
+ " To put a task back, mark its tracker line - [ ] and run again, or run again without --continue, which resumes the first [BLOCKED] line.",
28527
+ " Or run again with --continue --force-wrap-up to open a draft pull request listing them."
28528
+ ].join(`
28529
+ `));
28530
+ };
28531
+ const gateForcedWrapUp = (gate, forced, outcome2) => {
28532
+ if (gate !== "stop" || forced.length === 0 || outcome2 === null || !outcome2.red || outcome2.interrupted)
28533
+ return gate;
28534
+ const max = options.settings.loopForceWrapUpMaxNewFailures;
28535
+ const tolerated = max === false ? 0 : max;
28536
+ const count = outcome2.step?.newFailures.length ?? 0;
28537
+ if (count > 0 && count <= tolerated) {
28538
+ activeOutput().warn(`\u26A0\uFE0F --force-wrap-up goes on past the red pre-wrap-up step: ${count} new failure(s), within loop.forceWrapUp.maxNewFailures ${tolerated}.`);
28539
+ return "go-on";
28540
+ }
28541
+ const why = count === 0 ? "the red pre-wrap-up step holds no failure it can count (errors outside any test, or no summary)" : `${count} new failure(s), over loop.forceWrapUp.maxNewFailures ${String(max)}`;
28542
+ const halt2 = count === 0 ? "forced wrap-up refused: no failure to count" : `forced wrap-up refused: ${why}`;
28543
+ emitLoopEvent({ kind: "halt", reason: halt2 });
28544
+ throw new LoopEnd(DECISION_STOP_EXIT, `\u26D4 --force-wrap-up refused the wrap-up: ${why}. Repair them, or raise loop.forceWrapUp.maxNewFailures.`);
28545
+ };
28546
+ const atRunEnd = () => {
28547
+ if (directive === null)
28548
+ return;
28549
+ activeOutput().warn(`\u26A0\uFE0F --decide=${directive.strategy} was not applied: the run met no decision point. It applies once, to the next run's first [BLOCKED] task or first stop.`);
28550
+ };
28551
+ return { skipLines, atStop, atFirstTask, taskDone, atRunEnd, atPlanEnd, gateForcedWrapUp };
26982
28552
  }
26983
28553
 
26984
28554
  // src/start/pause.ts
@@ -27030,22 +28600,22 @@ async function holdWhilePaused(options) {
27030
28600
  }
27031
28601
 
27032
28602
  // src/start/plan-path.ts
27033
- import { existsSync as existsSync29 } from "fs";
27034
- import { resolve as resolve19 } from "path";
28603
+ import { existsSync as existsSync34 } from "fs";
28604
+ import { resolve as resolve21 } from "path";
27035
28605
  var DEFAULT_PLAN_FILE = "PLAN.md";
27036
28606
  function resolvePlanPath(repoRoot, planDir, planArg) {
27037
28607
  if (planArg)
27038
- return resolve19(repoRoot, planArg);
27039
- const inPlanDir = resolve19(repoRoot, planDir, DEFAULT_PLAN_FILE);
27040
- return existsSync29(inPlanDir) ? inPlanDir : resolve19(repoRoot, DEFAULT_PLAN_FILE);
28608
+ return resolve21(repoRoot, planArg);
28609
+ const inPlanDir = resolve21(repoRoot, planDir, DEFAULT_PLAN_FILE);
28610
+ return existsSync34(inPlanDir) ? inPlanDir : resolve21(repoRoot, DEFAULT_PLAN_FILE);
27041
28611
  }
27042
28612
 
27043
28613
  // src/start/risk-total.ts
27044
- import { readFileSync as readFileSync37 } from "fs";
28614
+ import { readFileSync as readFileSync42 } from "fs";
27045
28615
 
27046
28616
  // src/plan/risk.ts
27047
- import { readFileSync as readFileSync36 } from "fs";
27048
- import { isAbsolute as isAbsolute6, join as join30, relative as relative8 } from "path";
28617
+ import { readFileSync as readFileSync41 } from "fs";
28618
+ import { isAbsolute as isAbsolute6, join as join34, relative as relative9 } from "path";
27049
28619
 
27050
28620
  // src/plan/risk/accounts.ts
27051
28621
  var PUSH_REMOTE = "origin";
@@ -27126,7 +28696,7 @@ function repoPhrase(view) {
27126
28696
  async function viewRepo(gh, target) {
27127
28697
  const result = await gh(["repo", "view", ...target, "--json", "nameWithOwner,visibility"]);
27128
28698
  if (!result.ok) {
27129
- return { ok: false, reason: `gh repo view: ${firstLine6(result.stderr) ?? "gh exited non-zero"}` };
28699
+ return { ok: false, reason: `gh repo view: ${firstLine7(result.stderr) ?? "gh exited non-zero"}` };
27130
28700
  }
27131
28701
  const parsed2 = parseJsonObject(result.stdout);
27132
28702
  if (parsed2 === null) {
@@ -27150,7 +28720,7 @@ function stringField2(object, key) {
27150
28720
  const value = object[key];
27151
28721
  return typeof value === "string" && value !== "" ? value : null;
27152
28722
  }
27153
- function firstLine6(text) {
28723
+ function firstLine7(text) {
27154
28724
  return text.split(`
27155
28725
  `).map((line) => line.trim()).find((line) => line !== "") ?? null;
27156
28726
  }
@@ -27581,15 +29151,15 @@ function toolsFinding(task, plan, seams) {
27581
29151
  return { level: "high", kind: "tools", text: `${EVERY_TOOL} \u2014 ${access.source}; ${model}; ${budget}`, ...where };
27582
29152
  }
27583
29153
  if (holdsShell(access.tools) && access.budget === null) {
27584
- const listed = access.tools.join(", ");
27585
- return { level: "note", kind: "tools", text: `${SHELL_TOOL} with no budget= \u2014 ${access.source}: ${listed}; ${model}`, ...where };
29154
+ const listed2 = access.tools.join(", ");
29155
+ return { level: "note", kind: "tools", text: `${SHELL_TOOL} with no budget= \u2014 ${access.source}: ${listed2}; ${model}`, ...where };
27586
29156
  }
27587
29157
  return null;
27588
29158
  }
27589
29159
  function displayPath(path3, seams) {
27590
- const absolute = isAbsolute6(path3) ? path3 : join30(seams.repoRoot, path3);
29160
+ const absolute = isAbsolute6(path3) ? path3 : join34(seams.repoRoot, path3);
27591
29161
  const within = (root) => {
27592
- const step = relative8(root, absolute);
29162
+ const step = relative9(root, absolute);
27593
29163
  return step === "" || step.startsWith("..") || isAbsolute6(step) ? null : step;
27594
29164
  };
27595
29165
  const inRepo = within(seams.repoRoot);
@@ -27604,7 +29174,7 @@ function located(finding2, file, task) {
27604
29174
  }
27605
29175
  function readText5(path3) {
27606
29176
  try {
27607
- return readFileSync36(path3, "utf8");
29177
+ return readFileSync41(path3, "utf8");
27608
29178
  } catch {
27609
29179
  return null;
27610
29180
  }
@@ -27612,7 +29182,7 @@ function readText5(path3) {
27612
29182
  function findSkillFile(name2, seams) {
27613
29183
  const tiers = resolveSkillTiers({ home: seams.home, projectRoot: seams.repoRoot, entry: seams.entry });
27614
29184
  for (const { dir } of tiers) {
27615
- const path3 = join30(dir, name2, "SKILL.md");
29185
+ const path3 = join34(dir, name2, "SKILL.md");
27616
29186
  if (readText5(path3) !== null)
27617
29187
  return path3;
27618
29188
  }
@@ -27706,7 +29276,7 @@ function spawningRunners(root) {
27706
29276
  }
27707
29277
  async function readRiskTotalLine(input, seams = {}) {
27708
29278
  const makeRunners = seams.runners ?? spawningRunners;
27709
- const report2 = await assessPlanRisk({ path: input.planPath, text: readFileSync37(input.planPath, "utf8") }, {
29279
+ const report2 = await assessPlanRisk({ path: input.planPath, text: readFileSync42(input.planPath, "utf8") }, {
27710
29280
  repoRoot: input.repoRoot,
27711
29281
  home: input.home,
27712
29282
  settingSources: input.config.settingSources,
@@ -28253,6 +29823,33 @@ function guardRunBranch(planStub2, branch, args) {
28253
29823
  }
28254
29824
  }
28255
29825
  var NO_CI_WAIT_FLAG = "--no-ci-wait";
29826
+ var RETRY_FLAG = "--retry";
29827
+ var NO_RETRY_FLAG = "--no-retry";
29828
+ var RETRY_COUNT = /^[1-9][0-9]*$/;
29829
+ function refuseRetry(reason) {
29830
+ throw new CommandExit(1, [
29831
+ `\u274C Refusing ${reason}: ${RETRY_FLAG} takes a whole number from 1 to 3 as ${RETRY_FLAG}=<n>,`,
29832
+ " the retries this run makes after a retry-safe stop. Leave it out to take `loop.retries`,",
29833
+ " and set `loop.retries: false` in .rafa/config.yaml for none.",
29834
+ NOTHING_DISPATCHED
29835
+ ].join(`
29836
+ `));
29837
+ }
29838
+ function readRetryFlag(args) {
29839
+ const end = args.indexOf("--");
29840
+ const words = end === -1 ? args : args.slice(0, end);
29841
+ if (words.includes(NO_RETRY_FLAG))
29842
+ refuseRetry(NO_RETRY_FLAG);
29843
+ if (words.includes(RETRY_FLAG))
29844
+ refuseRetry(`a bare ${RETRY_FLAG}`);
29845
+ const typed = argValue([...words].reverse(), RETRY_FLAG);
29846
+ if (typed === undefined)
29847
+ return;
29848
+ const count = RETRY_COUNT.test(typed) ? Number(typed) : Number.NaN;
29849
+ if (!isLoopRetryCount(count))
29850
+ refuseRetry(`${RETRY_FLAG}=${typed}`);
29851
+ return count;
29852
+ }
28256
29853
  function readRunArgs(args) {
28257
29854
  return {
28258
29855
  startAt: argValue(args, "--start-at"),
@@ -28260,13 +29857,15 @@ function readRunArgs(args) {
28260
29857
  ciWait: !args.includes(NO_CI_WAIT_FLAG),
28261
29858
  ciTimeoutMin: Number(argValue(args, "--ci-timeout") ?? DEFAULT_CI_TIMEOUT_MIN),
28262
29859
  ciAttempts: Number(argValue(args, "--ci-attempts") ?? DEFAULT_CI_ATTEMPTS),
28263
- roadmap: args.includes(ROADMAP_FLAG2)
29860
+ roadmap: args.includes(ROADMAP_FLAG2),
29861
+ retry: readRetryFlag(args),
29862
+ continueRun: readContinueArgs(args)
28264
29863
  };
28265
29864
  }
28266
29865
 
28267
29866
  // src/start/worktree.ts
28268
- import { realpathSync as realpathSync4 } from "fs";
28269
- import { join as join31, resolve as resolve21 } from "path";
29867
+ import { realpathSync as realpathSync5 } from "fs";
29868
+ import { join as join35, resolve as resolve23 } from "path";
28270
29869
 
28271
29870
  // src/start/claim-catch-up.ts
28272
29871
  var OWN_COMMIT_FORMAT = "--format=%H%x09%P%x09%s";
@@ -28299,18 +29898,18 @@ function isClaimOnly(git, commit, baseTip) {
28299
29898
  function readClaimCatchUp(git, branch, base) {
28300
29899
  const named3 = { branch, base };
28301
29900
  const remoteBase = `${REMOTE}/${base}`;
28302
- const unreadable = (reason) => ({ kind: "unreadable", ...named3, reason });
29901
+ const unreadable2 = (reason) => ({ kind: "unreadable", ...named3, reason });
28303
29902
  const baseRef = remoteTrackingRef(base);
28304
29903
  const branchRef = localRef(branch);
28305
29904
  const baseTip = commitAt(git, baseRef);
28306
29905
  if (baseTip === null)
28307
- return unreadable(`${remoteBase} is not known here`);
29906
+ return unreadable2(`${remoteBase} is not known here`);
28308
29907
  if (commitAt(git, branchRef) === null)
28309
- return unreadable(`${branch} is not known here`);
29908
+ return unreadable2(`${branch} is not known here`);
28310
29909
  const counted5 = git(["rev-list", "--count", `${branchRef}..${baseRef}`]);
28311
29910
  const behind = Number(counted5.stdout.trim());
28312
29911
  if (!counted5.ok || counted5.stdout.trim() === "" || !Number.isInteger(behind)) {
28313
- return unreadable(`could not count how far ${branch} is behind ${remoteBase}: ${gitSaid(counted5)}`);
29912
+ return unreadable2(`could not count how far ${branch} is behind ${remoteBase}: ${gitSaid(counted5)}`);
28314
29913
  }
28315
29914
  if (behind === 0)
28316
29915
  return { kind: "current", ...named3 };
@@ -28318,22 +29917,22 @@ function readClaimCatchUp(git, branch, base) {
28318
29917
  const mergeBase = shared.stdout.trim();
28319
29918
  if (!shared.ok || mergeBase === "") {
28320
29919
  const said = gitSaid(shared);
28321
- return unreadable(said === "" ? `${branch} shares no history with ${remoteBase}` : `could not read where ${branch} left ${remoteBase}: ${said}`);
29920
+ return unreadable2(said === "" ? `${branch} shares no history with ${remoteBase}` : `could not read where ${branch} left ${remoteBase}: ${said}`);
28322
29921
  }
28323
- const listed = git(["log", OWN_COMMIT_FORMAT, `${baseRef}..${branchRef}`]);
28324
- if (!listed.ok) {
28325
- return unreadable(`could not list ${branch}'s commits past ${remoteBase}: ${gitSaid(listed)}`);
29922
+ const listed2 = git(["log", OWN_COMMIT_FORMAT, `${baseRef}..${branchRef}`]);
29923
+ if (!listed2.ok) {
29924
+ return unreadable2(`could not list ${branch}'s commits past ${remoteBase}: ${gitSaid(listed2)}`);
28326
29925
  }
28327
- const own = ownCommitsOf(listed.stdout);
29926
+ const own = ownCommitsOf(listed2.stdout);
28328
29927
  const workCommits = own.filter((commit) => !isClaimOnly(git, commit, baseTip)).length;
28329
29928
  const standing = { ...named3, behind, ahead: own.length, mergeBase, baseTip };
28330
29929
  return workCommits === 0 ? { kind: "catch-up", ...standing } : { kind: "behind", ...standing, workCommits };
28331
29930
  }
28332
29931
 
28333
29932
  // src/start/worktree-dir.ts
28334
- import { resolve as resolve20 } from "path";
29933
+ import { resolve as resolve22 } from "path";
28335
29934
  function worktreeDirAt(projectRoot3, worktreeDir) {
28336
- return resolve20(projectRoot3, worktreeDir);
29935
+ return resolve22(projectRoot3, worktreeDir);
28337
29936
  }
28338
29937
 
28339
29938
  // src/start/worktree.ts
@@ -28341,7 +29940,7 @@ var INDENT7 = " ";
28341
29940
  var UNTOUCHED = `${INDENT7}The main checkout's branch and working tree were not touched.`;
28342
29941
  var DEFAULT_WORKTREE_SEAMS = Object.freeze({});
28343
29942
  function worktreePathFor(projectRoot3, worktreeDir, stub) {
28344
- return join31(worktreeDirAt(projectRoot3, worktreeDir), stub);
29943
+ return join35(worktreeDirAt(projectRoot3, worktreeDir), stub);
28345
29944
  }
28346
29945
  function addStep(route, plan, path3) {
28347
29946
  const label = `add the worktree for ${plan.branch} at ${path3}`;
@@ -28395,9 +29994,9 @@ function heldRefusal(branch, holder, detail) {
28395
29994
  }
28396
29995
  function canonicalPath(path3) {
28397
29996
  try {
28398
- return realpathSync4(path3);
29997
+ return realpathSync5(path3);
28399
29998
  } catch {
28400
- return resolve21(path3);
29999
+ return resolve23(path3);
28401
30000
  }
28402
30001
  }
28403
30002
  function readExisting(listing2, branch, path3) {
@@ -28433,8 +30032,8 @@ function refExists2(git, ref8) {
28433
30032
  return git(["show-ref", "--verify", "--quiet", ref8]).ok;
28434
30033
  }
28435
30034
  function readListing(git) {
28436
- const listed = git(["worktree", "list", "--porcelain"]);
28437
- return listed.ok ? parseWorktrees(listed.stdout) : [];
30035
+ const listed2 = git(["worktree", "list", "--porcelain"]);
30036
+ return listed2.ok ? parseWorktrees(listed2.stdout) : [];
28438
30037
  }
28439
30038
  function openingLine2(route, plan) {
28440
30039
  return route === "create" ? `
@@ -28570,13 +30169,13 @@ async function settleRunCheckout(request, seams = {}) {
28570
30169
  }
28571
30170
 
28572
30171
  // src/start/suite-step.ts
28573
- import { existsSync as existsSync36, mkdirSync as mkdirSync14, readFileSync as readFileSync44, renameSync as renameSync7, writeFileSync as writeFileSync17 } from "fs";
30172
+ import { existsSync as existsSync40, mkdirSync as mkdirSync14, readFileSync as readFileSync48, renameSync as renameSync7, writeFileSync as writeFileSync17 } from "fs";
28574
30173
  import { constants as constants2 } from "os";
28575
- import { basename as basename11, dirname as dirname15, join as join37 } from "path";
30174
+ import { basename as basename11, dirname as dirname18, join as join40 } from "path";
28576
30175
 
28577
30176
  // src/suite/baseline.ts
28578
- import { existsSync as existsSync30, mkdirSync as mkdirSync12, readFileSync as readFileSync38, renameSync as renameSync6, writeFileSync as writeFileSync13 } from "fs";
28579
- import { basename as basename9, dirname as dirname12, join as join32 } from "path";
30177
+ import { existsSync as existsSync35, mkdirSync as mkdirSync12, readFileSync as readFileSync43, renameSync as renameSync6, writeFileSync as writeFileSync15 } from "fs";
30178
+ import { basename as basename9, dirname as dirname16, join as join36 } from "path";
28580
30179
  var BASELINE_VERSION = 1;
28581
30180
  var BASELINE_FILE_PREFIX = "SUITE_BASELINE";
28582
30181
  var PLAN_FILE_NAME2 = /^PLAN(?:_TRACKER)?(-.+)?\.md$/;
@@ -28586,7 +30185,7 @@ function baselinePathFor(trackerPath) {
28586
30185
  if (match === null) {
28587
30186
  throw new RangeError(`baselinePathFor: ${trackerPath} is not a PLAN or PLAN_TRACKER file`);
28588
30187
  }
28589
- return join32(dirname12(trackerPath), `${BASELINE_FILE_PREFIX}${match[1] ?? ""}.json`);
30188
+ return join36(dirname16(trackerPath), `${BASELINE_FILE_PREFIX}${match[1] ?? ""}.json`);
28590
30189
  }
28591
30190
  function copyFailure(failure3) {
28592
30191
  return failure3.message === undefined ? { file: failure3.file, name: failure3.name } : { file: failure3.file, name: failure3.name, message: failure3.message };
@@ -28605,9 +30204,9 @@ function baselineOf(result, recordedAt, commit) {
28605
30204
  };
28606
30205
  }
28607
30206
  function writeBaseline(path3, baseline) {
28608
- mkdirSync12(dirname12(path3), { recursive: true });
30207
+ mkdirSync12(dirname16(path3), { recursive: true });
28609
30208
  const temporary = `${path3}.${process.pid}.tmp`;
28610
- writeFileSync13(temporary, `${JSON.stringify(baseline, null, 2)}
30209
+ writeFileSync15(temporary, `${JSON.stringify(baseline, null, 2)}
28611
30210
  `, "utf8");
28612
30211
  renameSync6(temporary, path3);
28613
30212
  }
@@ -28649,11 +30248,11 @@ function asBaseline(parsed2) {
28649
30248
  return { state: "read", baseline: parsed2 };
28650
30249
  }
28651
30250
  function readBaseline(path3) {
28652
- if (!existsSync30(path3))
30251
+ if (!existsSync35(path3))
28653
30252
  return { state: "missing" };
28654
30253
  let text;
28655
30254
  try {
28656
- text = readFileSync38(path3, "utf8");
30255
+ text = readFileSync43(path3, "utf8");
28657
30256
  } catch (error) {
28658
30257
  const reason = error instanceof Error ? error.message : String(error);
28659
30258
  return { state: "unreadable", reason };
@@ -28748,8 +30347,8 @@ async function readPlanOwns(options) {
28748
30347
  }
28749
30348
 
28750
30349
  // src/suite/run.ts
28751
- import { existsSync as existsSync31, mkdirSync as mkdirSync13, readFileSync as readFileSync39, rmSync as rmSync4, writeFileSync as writeFileSync14 } from "fs";
28752
- import { basename as basename10, dirname as dirname13, extname as extname2, isAbsolute as isAbsolute7, join as join33 } from "path";
30350
+ import { existsSync as existsSync36, mkdirSync as mkdirSync13, readFileSync as readFileSync44, rmSync as rmSync5, writeFileSync as writeFileSync16 } from "fs";
30351
+ import { basename as basename10, dirname as dirname17, extname as extname2, isAbsolute as isAbsolute7, join as join37 } from "path";
28753
30352
 
28754
30353
  // src/suite/unhandled.ts
28755
30354
  var UNHANDLED_HEADING = "# Unhandled error between tests";
@@ -28790,14 +30389,14 @@ function rawBlocks(lines) {
28790
30389
  return [{ header: headerAbove(lines, index), body: lines.slice(start, bodyEnd(lines, start)) }];
28791
30390
  });
28792
30391
  }
28793
- function errorLine(body) {
30392
+ function errorLine2(body) {
28794
30393
  const found = body.find((line) => line.trim() !== "" && !FRAME_LINE.test(line) && !CARET_LINE.test(line));
28795
30394
  return found?.trim() ?? null;
28796
30395
  }
28797
30396
  function parseUnhandled(stderr) {
28798
30397
  return rawBlocks(linesOf2(stderr)).map((raw) => ({
28799
30398
  file: raw.header === null ? null : raw.header.slice(0, -1),
28800
- firstLine: errorLine(raw.body)
30399
+ firstLine: errorLine2(raw.body)
28801
30400
  }));
28802
30401
  }
28803
30402
  function summaryLines(lines) {
@@ -28859,7 +30458,7 @@ var OUTPUT_SUFFIX = ".output.txt";
28859
30458
  function outputFileFor(junitFile) {
28860
30459
  const name2 = basename10(junitFile);
28861
30460
  const stem = name2.endsWith(JUNIT_SUFFIX) ? name2.slice(0, -JUNIT_SUFFIX.length) : name2.slice(0, name2.length - extname2(name2).length);
28862
- return join33(dirname13(junitFile), `${stem}${OUTPUT_SUFFIX}`);
30461
+ return join37(dirname17(junitFile), `${stem}${OUTPUT_SUFFIX}`);
28863
30462
  }
28864
30463
  function suiteEnv(base) {
28865
30464
  return Object.fromEntries(Object.entries(base).filter(([key]) => key !== CLAUDE_CODE_ENV));
@@ -28963,11 +30562,11 @@ function parseJunitFailures(xml) {
28963
30562
  return walk.failures;
28964
30563
  }
28965
30564
  function readJunit(path3) {
28966
- if (!existsSync31(path3))
30565
+ if (!existsSync36(path3))
28967
30566
  return { junit: "missing", failures: [] };
28968
30567
  let text;
28969
30568
  try {
28970
- text = readFileSync39(path3, "utf8");
30569
+ text = readFileSync44(path3, "utf8");
28971
30570
  } catch {
28972
30571
  return { junit: "unreadable", failures: [] };
28973
30572
  }
@@ -28991,13 +30590,13 @@ async function runSuite(options) {
28991
30590
  }
28992
30591
  const command = suiteCommand(options.junitFile, options.paths, options.changedSince);
28993
30592
  const outputFile = outputFileFor(options.junitFile);
28994
- mkdirSync13(dirname13(options.junitFile), { recursive: true });
28995
- rmSync4(options.junitFile, { force: true });
28996
- rmSync4(outputFile, { force: true });
30593
+ mkdirSync13(dirname17(options.junitFile), { recursive: true });
30594
+ rmSync5(options.junitFile, { force: true });
30595
+ rmSync5(outputFile, { force: true });
28997
30596
  const spawn = options.spawn ?? spawnSuite;
28998
30597
  const env = suiteEnv(options.env ?? process.env);
28999
30598
  const { exitCode, stderr } = await spawn(command, { cwd: options.cwd, env });
29000
- writeFileSync14(outputFile, unhandledText(stderr));
30599
+ writeFileSync16(outputFile, unhandledText(stderr));
29001
30600
  const { summary, errors } = readSummary2(stderr);
29002
30601
  const { junit, failures } = readJunit(options.junitFile);
29003
30602
  const unhandled = parseUnhandled(stderr);
@@ -29006,8 +30605,8 @@ async function runSuite(options) {
29006
30605
  }
29007
30606
 
29008
30607
  // src/suite/scope.ts
29009
- import { existsSync as existsSync32, readdirSync as readdirSync14, readFileSync as readFileSync40 } from "fs";
29010
- import { isAbsolute as isAbsolute8, join as join34, relative as relative9, sep as sep2 } from "path";
30608
+ import { existsSync as existsSync37, readdirSync as readdirSync14, readFileSync as readFileSync45 } from "fs";
30609
+ import { isAbsolute as isAbsolute8, join as join38, relative as relative10, sep as sep3 } from "path";
29011
30610
  var BUNFIG_FILE = "bunfig.toml";
29012
30611
  var TEST_FILE_NAME = /[._](?:test|spec)\.(?:tsx?|jsx?|[cm][jt]s)$/;
29013
30612
  var DEPENDENCY_FOLDER = "node_modules";
@@ -29067,7 +30666,7 @@ function stageStepScope(input) {
29067
30666
  return { scope: "paths", folders, integration, paths };
29068
30667
  }
29069
30668
  function preloadPath(root, entry) {
29070
- const path3 = isAbsolute8(entry) ? relative9(root, entry).split(sep2).join("/") : entry;
30669
+ const path3 = isAbsolute8(entry) ? relative10(root, entry).split(sep3).join("/") : entry;
29071
30670
  const repoRelative2 = normaliseFolder(path3);
29072
30671
  if (repoRelative2 === "" || isAbsolute8(repoRelative2))
29073
30672
  return null;
@@ -29090,11 +30689,11 @@ function preloadFilesOf(text, root) {
29090
30689
  return paths.filter((path3) => path3 !== null);
29091
30690
  }
29092
30691
  function readPreloadFiles(root) {
29093
- const path3 = join34(root, BUNFIG_FILE);
29094
- if (!existsSync32(path3))
30692
+ const path3 = join38(root, BUNFIG_FILE);
30693
+ if (!existsSync37(path3))
29095
30694
  return { state: "missing", files: [] };
29096
30695
  try {
29097
- return { state: "read", files: preloadFilesOf(readFileSync40(path3, "utf8"), root) };
30696
+ return { state: "read", files: preloadFilesOf(readFileSync45(path3, "utf8"), root) };
29098
30697
  } catch (error) {
29099
30698
  const reason = error instanceof Error ? error.message : String(error);
29100
30699
  return { state: "unreadable", files: [], reason };
@@ -29103,7 +30702,7 @@ function readPreloadFiles(root) {
29103
30702
  function listTestFiles(root) {
29104
30703
  const found = [];
29105
30704
  const walk = (folder) => {
29106
- for (const entry of readdirSync14(join34(root, folder), { withFileTypes: true })) {
30705
+ for (const entry of readdirSync14(join38(root, folder), { withFileTypes: true })) {
29107
30706
  const path3 = folder === "" ? entry.name : `${folder}/${entry.name}`;
29108
30707
  if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== DEPENDENCY_FOLDER)
29109
30708
  walk(path3);
@@ -29116,8 +30715,8 @@ function listTestFiles(root) {
29116
30715
  }
29117
30716
 
29118
30717
  // src/start/lint-step.ts
29119
- import { existsSync as existsSync33 } from "fs";
29120
- import { join as join35, relative as relative10 } from "path";
30718
+ import { existsSync as existsSync38 } from "fs";
30719
+ import { join as join39, relative as relative11 } from "path";
29121
30720
  var LINT_COMMAND = ["bunx", "eslint", "--no-warn-ignored"];
29122
30721
  var LINT_REPORT_FLAGS = ["--format", "json"];
29123
30722
  var ESLINT_CONFIG_FILES = [
@@ -29128,9 +30727,9 @@ var ESLINT_CONFIG_FILES = [
29128
30727
  "eslint.config.mts",
29129
30728
  "eslint.config.cts"
29130
30729
  ];
29131
- var ERRORS_LISTED = 10;
30730
+ var ERRORS_LISTED2 = 10;
29132
30731
  var CRASH_BANNER = /^(?:Oops!|ESLint: \d)/;
29133
- var NOTHING_RAN = { ran: false, red: false, interrupted: false, blocker: null };
30732
+ var NOTHING_RAN2 = { ran: false, red: false, interrupted: false, blocker: null };
29134
30733
  async function runEslint(options) {
29135
30734
  const proc = Bun.spawn([...options.argv], {
29136
30735
  cwd: options.cwd,
@@ -29146,7 +30745,7 @@ async function runEslint(options) {
29146
30745
  return { exitCode, stdout, stderr };
29147
30746
  }
29148
30747
  function hasEslintConfig(checkout) {
29149
- return ESLINT_CONFIG_FILES.some((name2) => existsSync33(join35(checkout, name2)));
30748
+ return ESLINT_CONFIG_FILES.some((name2) => existsSync38(join39(checkout, name2)));
29150
30749
  }
29151
30750
  function readLintFiles(git, base) {
29152
30751
  const result = git(["diff", "--name-only", "-z", "--no-renames", "--diff-filter=d", base, "HEAD"]);
@@ -29163,7 +30762,7 @@ function fileErrorsOf(entry, cwd) {
29163
30762
  const errors = entry["errorCount"];
29164
30763
  if (typeof entry["filePath"] !== "string" || typeof errors !== "number" || errors === 0)
29165
30764
  return null;
29166
- const file = relative10(cwd, entry["filePath"]);
30765
+ const file = relative11(cwd, entry["filePath"]);
29167
30766
  const messages = Array.isArray(entry["messages"]) ? entry["messages"] : [];
29168
30767
  const lines = messages.filter((message) => message["severity"] === 2).map((message) => messageLine(file, message));
29169
30768
  return { file, errors, lines };
@@ -29208,10 +30807,10 @@ function announce(label, count, run, files) {
29208
30807
  const errors = files.reduce((sum, file) => sum + file.errors, 0);
29209
30808
  activeOutput().info(`${ran}; ${errors} error(s) in ${files.length} file(s).`);
29210
30809
  const lines = files.flatMap((file) => file.lines);
29211
- for (const line of lines.slice(0, ERRORS_LISTED))
30810
+ for (const line of lines.slice(0, ERRORS_LISTED2))
29212
30811
  activeOutput().info(` ${line}`);
29213
- if (lines.length > ERRORS_LISTED)
29214
- activeOutput().info(` ...and ${lines.length - ERRORS_LISTED} more.`);
30812
+ if (lines.length > ERRORS_LISTED2)
30813
+ activeOutput().info(` ...and ${lines.length - ERRORS_LISTED2} more.`);
29215
30814
  }
29216
30815
  function filesToLint(input, label) {
29217
30816
  if (!hasEslintConfig(input.checkout)) {
@@ -29230,13 +30829,13 @@ async function runLintStep(input) {
29230
30829
  const label = lintLabel(input.task);
29231
30830
  const files = filesToLint(input, label);
29232
30831
  if (files === null)
29233
- return NOTHING_RAN;
30832
+ return NOTHING_RAN2;
29234
30833
  let run;
29235
30834
  try {
29236
30835
  run = await (input.runLint ?? runEslint)({ cwd: input.checkout, argv: [...LINT_COMMAND, ...LINT_REPORT_FLAGS, ...files] });
29237
30836
  } catch (error) {
29238
30837
  activeOutput().warn(`\u26A0\uFE0F The ${label} could not run (${messageOf(error)}); the run goes on without it.`);
29239
- return NOTHING_RAN;
30838
+ return NOTHING_RAN2;
29240
30839
  }
29241
30840
  if (run.exitCode === input.stopCode || input.isInterrupted?.() === true) {
29242
30841
  activeOutput().info(`\u23F9 The ${label} was interrupted by SIGINT: read as a stop, not as errors.`);
@@ -29249,102 +30848,8 @@ async function runLintStep(input) {
29249
30848
  return { ran: true, red: true, interrupted: false, blocker: lintBlockerText(label, run, reported) };
29250
30849
  }
29251
30850
 
29252
- // src/start/suite-blocker.ts
29253
- import { readFileSync as readFileSync41, writeFileSync as writeFileSync15 } from "fs";
29254
- function failingFiles(failures) {
29255
- const counts = new Map;
29256
- for (const failure3 of failures)
29257
- counts.set(failure3.file, (counts.get(failure3.file) ?? 0) + 1);
29258
- return [...counts.entries()];
29259
- }
29260
- var UNHANDLED_NAMED = 10;
29261
- function unhandledName(error) {
29262
- const file = error.file ?? "a file Bun did not name";
29263
- return error.firstLine === null ? `${file} threw an error Bun printed no line for` : `${file} threw "${error.firstLine}"`;
29264
- }
29265
- function unhandledNames(errors) {
29266
- if (errors.length === 0)
29267
- return "no block of Bun's stderr named a file";
29268
- const named3 = errors.slice(0, UNHANDLED_NAMED).map(unhandledName);
29269
- const left = errors.length - named3.length;
29270
- const more = left > 0 ? [`and ${left} more`] : [];
29271
- return [...named3, ...more].join("; ");
29272
- }
29273
- function unhandledFiles(errors) {
29274
- return [...new Set(errors.flatMap((error) => error.file === null ? [] : [error.file]))];
29275
- }
29276
- function runnablePath(path3) {
29277
- return path3.startsWith("/") || path3.startsWith("./") ? path3 : `./${path3}`;
29278
- }
29279
- function errorsText(newErrors, errors) {
29280
- const counted5 = `${newErrors} more error(s) outside any test than the baseline`;
29281
- if (errors.length === 0)
29282
- return `${counted5}: a test file that throws while it loads, which the JUnit report names no file for.`;
29283
- const files = unhandledFiles(errors);
29284
- const run = files.length === 0 ? "" : ` Run bun test ${files.map(runnablePath).join(" ")} and make each load.`;
29285
- return `${counted5}; Bun's stderr named them as ${unhandledNames(errors)}.${run}`;
29286
- }
29287
- function blockerText(label, result, verdict) {
29288
- const files = failingFiles(verdict.fresh);
29289
- const parts = [`The runner's ${label} found failures the suite baseline does not hold.`];
29290
- if (files.length > 0) {
29291
- const named3 = files.map(([file, count]) => `${file} (${count} ${count === 1 ? "test" : "tests"})`);
29292
- parts.push(`New failing test files: ${named3.join(", ")}. Run bun test ${files.map(([file]) => runnablePath(file)).join(" ")} and make them pass.`);
29293
- }
29294
- if (verdict.newErrors > 0)
29295
- parts.push(errorsText(verdict.newErrors, result.unhandled));
29296
- if (verdict.unreported)
29297
- parts.push(`bun test exited ${result.exitCode} and printed no summary line.`);
29298
- return parts.join(" ");
29299
- }
29300
- var REPAIR_AGENT = "build-error-resolver";
29301
- var REPAIR_TASK = /^Repair the red (?:task|stage|pre-wrap-up) step at commit \S+$/;
29302
- var PRE_WRAP_UP_REPAIR = /^Repair the red pre-wrap-up step at commit \S+$/;
29303
- var COMMIT_SHOWN = 12;
29304
- var TICKED_PREFIX = "- [x] ";
29305
- function repairTaskText(kind, commit) {
29306
- const shown2 = commit === null ? "(unread)" : commit.slice(0, COMMIT_SHOWN);
29307
- return `Repair the red ${kind} step at commit ${shown2}`;
29308
- }
29309
- function isRepairTask(text) {
29310
- return REPAIR_TASK.test(text.trim());
29311
- }
29312
- function lineOfTask(content, text) {
29313
- const found = parsePlan(content).tasks.filter((task) => task.text === text.trim());
29314
- return found.at(-1)?.lineNum ?? null;
29315
- }
29316
- function lineOfTickedPreWrapUpRepair(content) {
29317
- const found = parsePlan(content).tasks.filter((task) => task.status === "done" && PRE_WRAP_UP_REPAIR.test(task.text.trim()));
29318
- return found.at(-1)?.lineNum ?? null;
29319
- }
29320
- function existingRepairOf(content, site) {
29321
- if (site.kind === "pre-wrap-up")
29322
- return lineOfTickedPreWrapUpRepair(content);
29323
- return site.task !== undefined && isRepairTask(site.task) ? lineOfTask(content, site.task) : null;
29324
- }
29325
- function blockRepairAgain(trackerPath, content, line, blocker) {
29326
- const lines = content.split(`
29327
- `);
29328
- const current = lines[line] ?? "";
29329
- if (current.startsWith(TICKED_PREFIX)) {
29330
- const reopened = [...lines.slice(0, line), `- [ ] ${current.slice(TICKED_PREFIX.length)}`, ...lines.slice(line + 1)];
29331
- writeFileSync15(trackerPath, reopened.join(`
29332
- `), "utf8");
29333
- }
29334
- return writeTrackerBlocker(trackerPath, line, blocker) ? { line, inserted: false } : null;
29335
- }
29336
- function writeRepairTask(trackerPath, blocker, site) {
29337
- const content = readFileSync41(trackerPath, "utf8");
29338
- const repaired = existingRepairOf(content, site);
29339
- if (repaired !== null)
29340
- return blockRepairAgain(trackerPath, content, repaired, blocker);
29341
- const firstOpen = parsePlan(content).tasks.find((task) => task.status !== "done")?.lineNum ?? null;
29342
- const entry = { task: repairTaskText(site.kind, site.commit), declaration: `agent=${REPAIR_AGENT}`, blocker };
29343
- return { line: insertTrackerTask(trackerPath, entry, firstOpen), inserted: true };
29344
- }
29345
-
29346
30851
  // src/start/sweep-timing.ts
29347
- import { existsSync as existsSync34, readFileSync as readFileSync42 } from "fs";
30852
+ import { existsSync as existsSync39, readFileSync as readFileSync46 } from "fs";
29348
30853
  var SLOW_SWEEP_SECONDS = 10;
29349
30854
  var SUITE_TAG = /<(\/?)testsuite\b([^>]*?)(\/?)>/g;
29350
30855
  function fileTimeOf(attributes) {
@@ -29384,10 +30889,10 @@ function slowSweepLine(slow) {
29384
30889
  return `\uD83D\uDC22 ${slow.length} tests.alwaysRun file(s) took over ${SLOW_SWEEP_SECONDS}s in the task step: ${named3}.`;
29385
30890
  }
29386
30891
  function readFileTimes(path3) {
29387
- if (!existsSync34(path3))
30892
+ if (!existsSync39(path3))
29388
30893
  return [];
29389
30894
  try {
29390
- return parseJunitFileTimes(readFileSync42(path3, "utf8")) ?? [];
30895
+ return parseJunitFileTimes(readFileSync46(path3, "utf8")) ?? [];
29391
30896
  } catch {
29392
30897
  return [];
29393
30898
  }
@@ -29462,280 +30967,6 @@ function foldResults(first, second, stopCode) {
29462
30967
  return first.noTestFiles === true && second.noTestFiles === true ? { ...folded2, noTestFiles: true } : folded2;
29463
30968
  }
29464
30969
 
29465
- // src/start/type-step.ts
29466
- import { existsSync as existsSync35, mkdtempSync, realpathSync as realpathSync5, rmSync as rmSync5, symlinkSync as symlinkSync2, writeFileSync as writeFileSync16 } from "fs";
29467
- import { tmpdir } from "os";
29468
- import { dirname as dirname14, join as join36, relative as relative11, resolve as resolve22, sep as sep3 } from "path";
29469
- var TEST_FILE_SUFFIX = ".test.ts";
29470
- var TSC_FLAGS = ["--noEmit", "--pretty", "false"];
29471
- var ERRORS_LISTED2 = 10;
29472
- var NODE_MODULES = "node_modules";
29473
- var TSC_BIN = [".bin", "tsc"];
29474
- var SCRATCH_NAME = "tsconfig.json";
29475
- var FILE_ERROR = /^(.+)\((\d+),(\d+)\): error (TS\d+): (.*)$/;
29476
- var GLOBAL_ERROR = /^error (TS\d+): (.*)$/;
29477
- var NOTHING_RAN2 = { ran: false, red: false, interrupted: false, blocker: null };
29478
- var GREEN = { ran: true, red: false, interrupted: false, blocker: null };
29479
- var INTERRUPTED = { ran: true, red: false, interrupted: true, blocker: null };
29480
- async function runTsc(options) {
29481
- const proc = Bun.spawn([...options.argv], {
29482
- cwd: options.cwd,
29483
- stdin: "ignore",
29484
- stdout: "pipe",
29485
- stderr: "pipe"
29486
- });
29487
- const [stdout, stderr, exitCode] = await Promise.all([
29488
- new Response(proc.stdout).text(),
29489
- new Response(proc.stderr).text(),
29490
- proc.exited
29491
- ]);
29492
- return { exitCode, stdout, stderr };
29493
- }
29494
- function typeLabel(task) {
29495
- return `type step after "${task}"`;
29496
- }
29497
- function pathsOf(status) {
29498
- return /^[RC]/.test(status) ? 2 : 1;
29499
- }
29500
- function readTypeFiles(git, base) {
29501
- const result = git(["diff", "--name-status", "-z", "--find-renames", "--diff-filter=d", base, "HEAD"]);
29502
- if (!result.ok) {
29503
- activeOutput().warn(`\u26A0\uFE0F git diff from ${base} did not answer (${gitSaid(result) || "nothing said"}); the type step runs nothing.`);
29504
- return null;
29505
- }
29506
- const fields = result.stdout.split("\x00").filter((field4) => field4 !== "");
29507
- const files = [];
29508
- for (let index = 0;index < fields.length; ) {
29509
- const status = fields[index] ?? "";
29510
- const count = pathsOf(status);
29511
- const paths = fields.slice(index + 1, index + 1 + count);
29512
- index += 1 + count;
29513
- const head = paths[paths.length - 1];
29514
- if (head === undefined || !head.endsWith(TEST_FILE_SUFFIX))
29515
- continue;
29516
- files.push({ head, base: status.startsWith("A") ? null : paths[0] ?? null });
29517
- }
29518
- return files;
29519
- }
29520
- function realPath(path3) {
29521
- try {
29522
- return realpathSync5(path3);
29523
- } catch {
29524
- return resolve22(path3);
29525
- }
29526
- }
29527
- function walkStop(git) {
29528
- const result = git(["rev-parse", "--path-format=absolute", "--git-common-dir"]);
29529
- const common = result.stdout.trim();
29530
- return result.ok && common !== "" ? dirname14(realPath(common)) : null;
29531
- }
29532
- function isUnder(dir, root) {
29533
- return dir === root || dir.startsWith(root.endsWith(sep3) ? root : `${root}${sep3}`);
29534
- }
29535
- function findNodeModules(checkout, git) {
29536
- const start = realPath(checkout);
29537
- const stop = walkStop(git);
29538
- const top = stop !== null && isUnder(start, stop) ? stop : start;
29539
- for (let dir = start;; dir = dirname14(dir)) {
29540
- const modules = join36(dir, NODE_MODULES);
29541
- if (existsSync35(join36(modules, ...TSC_BIN)))
29542
- return modules;
29543
- if (dir === top || dirname14(dir) === dir)
29544
- return null;
29545
- }
29546
- }
29547
- function scratchTsconfig(tree, files, modules) {
29548
- const root = resolve22(tree);
29549
- return `${JSON.stringify({
29550
- extends: join36(root, "tsconfig.json"),
29551
- compilerOptions: { typeRoots: [join36(resolve22(modules), "@types")] },
29552
- files: files.map((file) => join36(root, file)),
29553
- include: []
29554
- }, null, 2)}
29555
- `;
29556
- }
29557
- function parseTscOutput(stdout, cwd) {
29558
- const errors = [];
29559
- for (const line of stdout.split(`
29560
- `)) {
29561
- const atFile = FILE_ERROR.exec(line);
29562
- const global = atFile === null ? GLOBAL_ERROR.exec(line) : null;
29563
- const last = errors[errors.length - 1];
29564
- if (atFile !== null) {
29565
- const [, file = "", row = "0", column = "0", code = "", message = ""] = atFile;
29566
- errors.push({ file: relative11(cwd, resolve22(cwd, file)), line: Number(row), column: Number(column), code, message });
29567
- } else if (global !== null) {
29568
- errors.push({ file: null, line: 0, column: 0, code: global[1] ?? "", message: global[2] ?? "" });
29569
- } else if (line.startsWith(" ") && last !== undefined) {
29570
- errors[errors.length - 1] = { ...last, message: `${last.message}
29571
- ${line.trimEnd()}` };
29572
- }
29573
- }
29574
- return errors;
29575
- }
29576
- function identity(error) {
29577
- return `${error.code} ${error.message}`;
29578
- }
29579
- function newErrors(head, base) {
29580
- const left = new Map;
29581
- for (const error of base) {
29582
- const key = `${error.file ?? ""}\x00${identity(error)}`;
29583
- left.set(key, (left.get(key) ?? 0) + 1);
29584
- }
29585
- return head.filter((error) => {
29586
- const key = `${error.file ?? ""}\x00${identity(error)}`;
29587
- const count = left.get(key) ?? 0;
29588
- if (count === 0)
29589
- return true;
29590
- left.set(key, count - 1);
29591
- return false;
29592
- });
29593
- }
29594
- function errorLine2(error) {
29595
- const first = error.message.split(`
29596
- `)[0] ?? "";
29597
- return `${error.file ?? "(no file)"}:${error.line}:${error.column} ${error.code} ${first}`;
29598
- }
29599
- function listed(errors) {
29600
- const shown2 = errors.slice(0, ERRORS_LISTED2).map(errorLine2);
29601
- return errors.length > ERRORS_LISTED2 ? [...shown2, `...and ${errors.length - ERRORS_LISTED2} more`] : shown2;
29602
- }
29603
- function typeBlockerText(label, base, errors) {
29604
- const files = [...new Set(errors.map((error) => error.file ?? ""))];
29605
- const shown2 = listed(errors).join("; ").replace(/\.$/u, "");
29606
- return `The runner's ${label} found type errors in the task's test files that ${base} did not hold. ` + `New errors: ${shown2}. ` + `Fix them, then check ${files.join(" ")} with tsc ${TSC_FLAGS.join(" ")} -p over a tsconfig outside the checkout ` + "that extends its tsconfig.json by absolute path and lists them under files.";
29607
- }
29608
- function brokenBlockerText(label, run, said) {
29609
- return `The runner's ${label} could not type-check the task's test files: tsc exited ${run.exitCode}${said === undefined ? " and printed no error." : `: ${said}`}`;
29610
- }
29611
- function configError(errors, scratch, cwd) {
29612
- const config = relative11(cwd, scratch);
29613
- return errors.find((error) => error.file === null || error.file === config);
29614
- }
29615
- function firstLine7(run) {
29616
- return `${run.stdout}
29617
- ${run.stderr}`.split(`
29618
- `).map((line) => line.trim()).find((line) => line !== "");
29619
- }
29620
- async function checkTree(input, modules, tree, files) {
29621
- const scratchDir = mkdtempSync(join36(tmpdir(), "rafa-type-step-"));
29622
- const scratch = join36(scratchDir, SCRATCH_NAME);
29623
- try {
29624
- writeFileSync16(scratch, scratchTsconfig(tree, files, modules), "utf8");
29625
- const tsc = join36(modules, ...TSC_BIN);
29626
- let run;
29627
- try {
29628
- run = await (input.runTypes ?? runTsc)({ cwd: tree, argv: [tsc, "-p", scratch, ...TSC_FLAGS] });
29629
- } catch (error) {
29630
- return { kind: "threw", why: messageOf(error) };
29631
- }
29632
- if (run.exitCode === input.stopCode || input.isInterrupted?.() === true)
29633
- return { kind: "interrupted" };
29634
- const errors = parseTscOutput(run.stdout, tree);
29635
- const config = configError(errors, scratch, tree);
29636
- if (config !== undefined)
29637
- return { kind: "broken", run, said: `error ${config.code}: ${config.message.split(`
29638
- `)[0] ?? ""}` };
29639
- if (run.exitCode !== 0 && errors.length === 0)
29640
- return { kind: "broken", run, said: firstLine7(run) };
29641
- const wanted = new Set(files);
29642
- return { kind: "read", errors: errors.filter((error) => error.file !== null && wanted.has(error.file)) };
29643
- } finally {
29644
- rmSync5(scratchDir, { recursive: true, force: true });
29645
- }
29646
- }
29647
- async function inBaseTree(input, label, modules, check) {
29648
- const parent = mkdtempSync(join36(tmpdir(), "rafa-type-base-"));
29649
- const tree = join36(parent, "tree");
29650
- try {
29651
- const added = input.git(["worktree", "add", "--detach", tree, input.base]);
29652
- if (!added.ok) {
29653
- activeOutput().warn(`\u26A0\uFE0F The ${label} could not check out ${input.base} (${gitSaid(added) || "nothing said"}); it reads no error as new.`);
29654
- return null;
29655
- }
29656
- if (existsSync35(modules) && !existsSync35(join36(tree, NODE_MODULES)))
29657
- symlinkSync2(modules, join36(tree, NODE_MODULES));
29658
- if (!existsSync35(join36(tree, "tsconfig.json")))
29659
- return { kind: "read", errors: [] };
29660
- return await check(tree);
29661
- } finally {
29662
- if (existsSync35(tree)) {
29663
- const removed = input.git(["worktree", "remove", "--force", tree]);
29664
- if (!removed.ok)
29665
- activeOutput().warn(`\u26A0\uFE0F The ${label} could not remove its worktree ${tree} (${gitSaid(removed) || "nothing said"}).`);
29666
- }
29667
- rmSync5(parent, { recursive: true, force: true });
29668
- }
29669
- }
29670
- async function baseErrors(input, label, modules, files) {
29671
- const pairs = files.filter((file) => file.base !== null);
29672
- if (pairs.length === 0)
29673
- return { errors: [] };
29674
- const run = await inBaseTree(input, label, modules, (tree) => checkTree(input, modules, tree, pairs.map((file) => file.base)));
29675
- if (run === null)
29676
- return { outcome: GREEN };
29677
- if (run.kind === "interrupted")
29678
- return { outcome: interrupted(label) };
29679
- if (run.kind !== "read") {
29680
- const why = run.kind === "threw" ? run.why : brokenBlockerText(label, run.run, run.said);
29681
- activeOutput().warn(`\u26A0\uFE0F The ${label} could not check the files at ${input.base} (${why}); it reads no error as new.`);
29682
- return { outcome: GREEN };
29683
- }
29684
- const headOf = new Map(pairs.map((file) => [file.base, file.head]));
29685
- return { errors: run.errors.map((error) => ({ ...error, file: headOf.get(error.file ?? "") ?? error.file })) };
29686
- }
29687
- function interrupted(label) {
29688
- activeOutput().info(`\u23F9 The ${label} was interrupted by SIGINT: read as a stop, not as errors.`);
29689
- return INTERRUPTED;
29690
- }
29691
- function filesToCheck(input, label) {
29692
- if (!existsSync35(join36(input.checkout, "tsconfig.json"))) {
29693
- activeOutput().info(`\uD83D\uDD0E ${label}: no tsconfig.json at the checkout root; nothing to type-check.`);
29694
- return null;
29695
- }
29696
- const files = readTypeFiles(input.git, input.base);
29697
- if (files === null)
29698
- return null;
29699
- if (files.length > 0)
29700
- return files;
29701
- activeOutput().info(`\uD83D\uDD0E ${label}: the task changed no ${TEST_FILE_SUFFIX} file; nothing to type-check.`);
29702
- return null;
29703
- }
29704
- async function runTypeStep(input) {
29705
- const label = typeLabel(input.task);
29706
- const files = filesToCheck(input, label);
29707
- if (files === null)
29708
- return NOTHING_RAN2;
29709
- const modules = findNodeModules(input.checkout, input.git) ?? join36(resolve22(input.checkout), NODE_MODULES);
29710
- const head = await checkTree(input, modules, input.checkout, files.map((file) => file.head));
29711
- if (head.kind === "threw") {
29712
- activeOutput().warn(`\u26A0\uFE0F The ${label} could not run tsc (${head.why}); the run goes on without it.`);
29713
- return NOTHING_RAN2;
29714
- }
29715
- if (head.kind === "interrupted")
29716
- return interrupted(label);
29717
- if (head.kind === "broken") {
29718
- const blocker = brokenBlockerText(label, head.run, head.said);
29719
- activeOutput().info(`\uD83D\uDD0E ${label}: ${blocker}`);
29720
- return { ran: true, red: true, interrupted: false, blocker };
29721
- }
29722
- const ran = `\uD83D\uDD0E ${label}: tsc over ${files.length} test file(s)`;
29723
- if (head.errors.length === 0) {
29724
- activeOutput().info(`${ran}: no type error.`);
29725
- return GREEN;
29726
- }
29727
- const base = await baseErrors(input, label, modules, files.filter((file) => head.errors.some((error) => error.file === file.head)));
29728
- if ("outcome" in base)
29729
- return base.outcome;
29730
- const added = newErrors(head.errors, base.errors);
29731
- activeOutput().info(`${ran}: ${head.errors.length} error(s), ${head.errors.length - added.length} already held at ${input.base}, ${added.length} not held.`);
29732
- for (const line of listed(added))
29733
- activeOutput().info(` ${line}`);
29734
- if (added.length === 0)
29735
- return GREEN;
29736
- return { ran: true, red: true, interrupted: false, blocker: typeBlockerText(label, input.base, added) };
29737
- }
29738
-
29739
30970
  // src/start/task-step-checks.ts
29740
30971
  function lintTask(context, seams, input) {
29741
30972
  const isInterrupted = context.isInterrupted ?? (() => false);
@@ -29754,7 +30985,7 @@ async function taskDiffChecks(context, seams, input, result) {
29754
30985
  return { lint, types: await typeCheckTask(context, seams, input) };
29755
30986
  }
29756
30987
  // src/start/suite-stage-step.ts
29757
- import { readFileSync as readFileSync43 } from "fs";
30988
+ import { readFileSync as readFileSync47 } from "fs";
29758
30989
  function stageSince(trackerPath, baseline) {
29759
30990
  const taken = readStageLedger(stageLedgerPathFor(trackerPath)).filter((entry) => entry.commit !== null);
29760
30991
  return taken.at(-1)?.commit ?? baseline?.commit ?? null;
@@ -29810,7 +31041,7 @@ async function runStageStep(context, stage, baseline) {
29810
31041
  }
29811
31042
  async function runDueStageSteps(context, baseline) {
29812
31043
  const ledger = readStageLedger(stageLedgerPathFor(context.trackerPath));
29813
- const due = dueStages(readFileSync43(context.trackerPath, "utf8"), ledger);
31044
+ const due = dueStages(readFileSync47(context.trackerPath, "utf8"), ledger);
29814
31045
  const outcomes = [];
29815
31046
  for (const stage of due) {
29816
31047
  const outcome2 = await runStageStep(context, stage, baseline);
@@ -29842,15 +31073,15 @@ function seamsOf(context) {
29842
31073
  };
29843
31074
  }
29844
31075
  function junitFileFor(repoRoot, sessionId, kind) {
29845
- return join37(runsDir(repoRoot), sessionId, "suite", `${kind}.junit.xml`);
31076
+ return join40(runsDir(repoRoot), sessionId, "suite", `${kind}.junit.xml`);
29846
31077
  }
29847
31078
  function alwaysRunJunitFileFor(repoRoot, sessionId, kind = "task") {
29848
- return join37(runsDir(repoRoot), sessionId, "suite", `${kind}-always-run.junit.xml`);
31079
+ return join40(runsDir(repoRoot), sessionId, "suite", `${kind}-always-run.junit.xml`);
29849
31080
  }
29850
31081
  function stageLedgerPathFor(trackerPath) {
29851
31082
  const baselinePath = baselinePathFor(trackerPath);
29852
31083
  const name2 = basename11(baselinePath).replace(BASELINE_FILE_PREFIX, STAGE_LEDGER_PREFIX);
29853
- return join37(dirname15(baselinePath), name2);
31084
+ return join40(dirname18(baselinePath), name2);
29854
31085
  }
29855
31086
  function isLedgerEntry(value) {
29856
31087
  if (typeof value !== "object" || value === null)
@@ -29874,11 +31105,11 @@ function parseLedger(text) {
29874
31105
  return stages;
29875
31106
  }
29876
31107
  function readStageLedger(path3) {
29877
- if (!existsSync36(path3))
31108
+ if (!existsSync40(path3))
29878
31109
  return [];
29879
31110
  let parsed2;
29880
31111
  try {
29881
- parsed2 = parseLedger(readFileSync44(path3, "utf8"));
31112
+ parsed2 = parseLedger(readFileSync48(path3, "utf8"));
29882
31113
  } catch (error) {
29883
31114
  parsed2 = messageOf(error);
29884
31115
  }
@@ -29888,7 +31119,7 @@ function readStageLedger(path3) {
29888
31119
  return [];
29889
31120
  }
29890
31121
  function writeStageLedger(path3, entries) {
29891
- mkdirSync14(dirname15(path3), { recursive: true });
31122
+ mkdirSync14(dirname18(path3), { recursive: true });
29892
31123
  const temporary = `${path3}.${process.pid}.tmp`;
29893
31124
  writeFileSync17(temporary, `${JSON.stringify({ version: STAGE_LEDGER_VERSION, stages: entries }, null, 2)}
29894
31125
  `, "utf8");
@@ -30063,7 +31294,7 @@ async function ensureBaseline(context) {
30063
31294
  activeOutput().warn(`\u26A0\uFE0F Could not write ${basename11(path3)} (${messageOf(error)}); the next run records the baseline again.`);
30064
31295
  }
30065
31296
  const outcome2 = settleStep(context, seams, settling);
30066
- const covered = completeStages(readFileSync44(context.trackerPath, "utf8"));
31297
+ const covered = completeStages(readFileSync48(context.trackerPath, "utf8"));
30067
31298
  addToLedger(context.trackerPath, covered.map((stage) => ({ ...stage, commit, via: "baseline" })));
30068
31299
  return { baseline, step: outcome2.step, interrupted: false };
30069
31300
  }
@@ -30202,10 +31433,17 @@ function createRunSuiteSteps(options) {
30202
31433
  ...options.seams === undefined ? {} : { seams: options.seams },
30203
31434
  ...options.isInterrupted === undefined ? {} : { isInterrupted: options.isInterrupted }
30204
31435
  };
31436
+ let signalled = false;
31437
+ const goesOnNoting = (outcome2) => {
31438
+ if (outcome2?.interrupted === true)
31439
+ signalled = true;
31440
+ return goesOn(outcome2);
31441
+ };
30205
31442
  let held = null;
30206
31443
  const baseline = () => {
30207
31444
  held ??= guarded("suite baseline", () => calls.ensureBaseline(context)).then((outcome2) => {
30208
31445
  if (outcome2?.interrupted === true) {
31446
+ signalled = true;
30209
31447
  announceInterrupted();
30210
31448
  return "interrupted";
30211
31449
  }
@@ -30217,8 +31455,10 @@ function createRunSuiteSteps(options) {
30217
31455
  return held;
30218
31456
  };
30219
31457
  let repairSent = false;
31458
+ let lastPreWrapUp = null;
30220
31459
  const preWrapUp = async (known) => {
30221
31460
  const outcome2 = await guarded("pre-wrap-up step", () => calls.runPreWrapUpStep(context, known));
31461
+ lastPreWrapUp = outcome2;
30222
31462
  if (insertedRepair(outcome2) && !repairSent) {
30223
31463
  repairSent = true;
30224
31464
  announceRepair();
@@ -30228,7 +31468,7 @@ function createRunSuiteSteps(options) {
30228
31468
  announceRedAgain();
30229
31469
  return "stop";
30230
31470
  }
30231
- return answerOf(goesOn(outcome2));
31471
+ return answerOf(goesOnNoting(outcome2));
30232
31472
  };
30233
31473
  const beforeSession = async (taskInfo) => {
30234
31474
  const known = await baseline();
@@ -30241,7 +31481,7 @@ function createRunSuiteSteps(options) {
30241
31481
  if (taskInfo.status === "blocked")
30242
31482
  return "go-on";
30243
31483
  const stages = await guarded("stage steps", () => calls.runDueStageSteps(context, known));
30244
- return answerOf((stages ?? []).every((outcome2) => goesOn(outcome2)));
31484
+ return answerOf((stages ?? []).every((outcome2) => goesOnNoting(outcome2)));
30245
31485
  };
30246
31486
  const afterTask = async (taskInfo, base) => {
30247
31487
  const known = await baseline();
@@ -30251,9 +31491,9 @@ function createRunSuiteSteps(options) {
30251
31491
  return true;
30252
31492
  const { text, declaration } = parseTaskDeclaration(taskInfo.task);
30253
31493
  const input = { baseline: known, base, declared: readTestScope(declaration), task: text };
30254
- return goesOn(await guarded("task step", () => calls.runTaskStep(context, input)));
31494
+ return goesOnNoting(await guarded("task step", () => calls.runTaskStep(context, input)));
30255
31495
  };
30256
- return { beforeSession, afterTask };
31496
+ return { beforeSession, afterTask, stoppedOnSignal: () => signalled, lastPreWrapUp: () => lastPreWrapUp };
30257
31497
  }
30258
31498
 
30259
31499
  // src/start/triage.ts
@@ -30400,12 +31640,12 @@ function inheritedFailureOf(bug, failures) {
30400
31640
  // src/triage/issue-text.ts
30401
31641
  var TITLE_MAX_LENGTH = 120;
30402
31642
  var TITLE_CUT = "...";
30403
- var MIN_FENCE_LENGTH = 3;
31643
+ var MIN_FENCE_LENGTH2 = 3;
30404
31644
  var ISSUE_OPENING = "An out-of-scope bug a rafa task session reported. The loop filed it and" + " dispatches no task for it: a plan that wants it fixed declares a task.";
30405
31645
  var COMMENT_OPENING = "Reported again by a rafa task session.";
30406
- function fenced(value) {
31646
+ function fenced2(value) {
30407
31647
  const runs = Array.from(value.matchAll(/`+/g), (run) => run[0].length);
30408
- const fence = "`".repeat(Math.max(MIN_FENCE_LENGTH, ...runs.map((length) => length + 1)));
31648
+ const fence = "`".repeat(Math.max(MIN_FENCE_LENGTH2, ...runs.map((length) => length + 1)));
30409
31649
  const body = value.endsWith(`
30410
31650
  `) ? value : `${value}
30411
31651
  `;
@@ -30445,8 +31685,8 @@ function supersedesSection(closed, redact) {
30445
31685
 
30446
31686
  ${redact(sentence)}`;
30447
31687
  }
30448
- function section(heading, value, absent2, redact) {
30449
- const shown2 = value === null ? absent2 : fenced(redact(value));
31688
+ function section(heading, value, absent3, redact) {
31689
+ const shown2 = value === null ? absent3 : fenced2(redact(value));
30450
31690
  return `## ${heading}
30451
31691
 
30452
31692
  ${shown2}`;
@@ -30471,7 +31711,7 @@ function issueText(opening, values, redact) {
30471
31711
 
30472
31712
  // src/triage/local-paths.ts
30473
31713
  import { realpathSync as realpathSync6 } from "fs";
30474
- import { resolve as resolve23 } from "path";
31714
+ import { resolve as resolve24 } from "path";
30475
31715
  var HOME_MARKER = "[redacted: HOME]";
30476
31716
  var SEGMENT = "[^/\\s'\"`()<>:,;]+";
30477
31717
  var HOME_PATTERN = `(?:/${SEGMENT})*?/(?:Users|home)/${SEGMENT}`;
@@ -30482,7 +31722,7 @@ function literalPattern(text) {
30482
31722
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30483
31723
  }
30484
31724
  function forms(path3) {
30485
- const given = resolve23(path3);
31725
+ const given = resolve24(path3);
30486
31726
  try {
30487
31727
  const real = realpathSync6(given);
30488
31728
  return real === given ? [given] : [given, real];
@@ -30515,11 +31755,11 @@ function localPathRedactor(repoRoot, home) {
30515
31755
 
30516
31756
  // src/triage/triage.ts
30517
31757
  import { homedir as homedir8 } from "os";
30518
- import { join as join38 } from "path";
31758
+ import { join as join41 } from "path";
30519
31759
 
30520
31760
  // src/effort/store/tracker-refs.ts
30521
- import { randomUUID as randomUUID9 } from "crypto";
30522
- import { existsSync as existsSync37 } from "fs";
31761
+ import { randomUUID as randomUUID10 } from "crypto";
31762
+ import { existsSync as existsSync41 } from "fs";
30523
31763
  var SELECT_HELD2 = `
30524
31764
  SELECT seq, tracker_ref FROM findings
30525
31765
  WHERE session_id = ? AND artifact = ?
@@ -30657,7 +31897,7 @@ function writeTrackerRef(repoRoot, write, seams = {}) {
30657
31897
  const path3 = sqliteStorePath(repoRoot);
30658
31898
  const { sessionId, planStub: planStub2, taskLine } = write.dispatch;
30659
31899
  const collectedAt = (seams.now ?? (() => new Date))().toISOString();
30660
- const newId = seams.newId ?? randomUUID9;
31900
+ const newId = seams.newId ?? randomUUID10;
30661
31901
  const insertValues = () => [
30662
31902
  newId(),
30663
31903
  sessionId,
@@ -30668,7 +31908,7 @@ function writeTrackerRef(repoRoot, write, seams = {}) {
30668
31908
  wanted.json,
30669
31909
  collectedAt
30670
31910
  ];
30671
- const [action, stored] = withSqliteStore(path3, "write", !existsSync37(path3), (db) => {
31911
+ const [action, stored] = withSqliteStore(path3, "write", !existsSync41(path3), (db) => {
30672
31912
  const place = db.transaction(() => placeRef(db, path3, write, wanted, superseded, insertValues));
30673
31913
  return place.immediate();
30674
31914
  });
@@ -30677,7 +31917,7 @@ function writeTrackerRef(repoRoot, write, seams = {}) {
30677
31917
  function readTrackerRef(repoRoot, artifact) {
30678
31918
  checkArtifact("read", artifact);
30679
31919
  const path3 = sqliteStorePath(repoRoot);
30680
- if (!existsSync37(path3))
31920
+ if (!existsSync41(path3))
30681
31921
  return null;
30682
31922
  const newest = withSqliteStore(path3, "read", false, (db) => db.query(SELECT_NEWEST).get(artifact));
30683
31923
  return newest === null ? null : heldRefOf(path3, newest.seq, newest.tracker_ref).ref;
@@ -30806,12 +32046,12 @@ function nearestOpenBugs(report2, openBugs) {
30806
32046
  }
30807
32047
 
30808
32048
  // src/triage/triage.ts
30809
- var PRIVATE_TRIAGE_DIR = join38(".rafa", "triage", "private");
32049
+ var PRIVATE_TRIAGE_DIR = join41(".rafa", "triage", "private");
30810
32050
  var SECRET_ENV_NAMES = ["GITHUB_TOKEN", "ANTHROPIC_API_KEY", "LINEAR_API_KEY"];
30811
32051
  var TRIAGE_MODULE = "unassigned";
30812
32052
  var BLOCKER_SEPARATOR2 = "; ";
30813
32053
  function privateTriageDir(repoRoot) {
30814
- return join38(repoRoot, PRIVATE_TRIAGE_DIR);
32054
+ return join41(repoRoot, PRIVATE_TRIAGE_DIR);
30815
32055
  }
30816
32056
  function createPrivateTriageTracker(repoRoot, options = {}) {
30817
32057
  return createLocalTracker({
@@ -31115,16 +32355,16 @@ function foundByText(bug) {
31115
32355
  return bug.score === undefined ? found : `${found} (score ${scoreText(bug.score)})`;
31116
32356
  }
31117
32357
  function unresolvedTracker(kind, reason) {
31118
- const refuse10 = () => Promise.reject(new Error(reason));
32358
+ const refuse11 = () => Promise.reject(new Error(reason));
31119
32359
  return Object.freeze({
31120
32360
  kind,
31121
32361
  capabilities: () => ({ projects: false, customFields: false, issueTypes: false }),
31122
32362
  preflight: () => Promise.resolve({ ok: false, reason }),
31123
- find: refuse10,
31124
- get: refuse10,
31125
- create: refuse10,
31126
- comment: refuse10,
31127
- transition: refuse10
32363
+ find: refuse11,
32364
+ get: refuse11,
32365
+ create: refuse11,
32366
+ comment: refuse11,
32367
+ transition: refuse11
31128
32368
  });
31129
32369
  }
31130
32370
  function filesPublicly(bug) {
@@ -31173,9 +32413,9 @@ function describeStartTriage(result, inherited = new Map) {
31173
32413
  };
31174
32414
  }
31175
32415
  async function resolvePublicTracker(options, redact) {
31176
- const resolve24 = options.resolve ?? resolveTracker;
32416
+ const resolve25 = options.resolve ?? resolveTracker;
31177
32417
  try {
31178
- const { tracker, attempts } = await resolve24({
32418
+ const { tracker, attempts } = await resolve25({
31179
32419
  config: options.config,
31180
32420
  context: { repoRoot: options.repoRoot }
31181
32421
  });
@@ -31690,8 +32930,8 @@ function firstLine8(text) {
31690
32930
  }
31691
32931
 
31692
32932
  // src/cleanup/runs.ts
31693
- import { existsSync as existsSync38, readdirSync as readdirSync15, readFileSync as readFileSync45 } from "fs";
31694
- import { join as join39 } from "path";
32933
+ import { existsSync as existsSync42, readdirSync as readdirSync15, readFileSync as readFileSync49 } from "fs";
32934
+ import { join as join42 } from "path";
31695
32935
  var LIVE_STATES = new Set(["running", "paused"]);
31696
32936
  function readRunRecords(root, seams = {}) {
31697
32937
  const dir = runsDir(root);
@@ -31707,7 +32947,7 @@ function readRunRecords(root, seams = {}) {
31707
32947
  const notes = [];
31708
32948
  const finished = [];
31709
32949
  for (const name2 of names2.filter((entry) => entry.endsWith(RECORD_EXTENSION)).sort()) {
31710
- const file = join39(dir, name2);
32950
+ const file = join42(dir, name2);
31711
32951
  const read = readRecord(file);
31712
32952
  if (typeof read === "string") {
31713
32953
  notes.push(read);
@@ -31726,7 +32966,7 @@ function readRunRecords(root, seams = {}) {
31726
32966
  }
31727
32967
  function readRecord(file) {
31728
32968
  try {
31729
- return parseSessionRecord(readFileSync45(file, "utf8"), file);
32969
+ return parseSessionRecord(readFileSync49(file, "utf8"), file);
31730
32970
  } catch (error) {
31731
32971
  return `run records: skipped ${file}: ${messageOf(error)}`;
31732
32972
  }
@@ -31739,11 +32979,11 @@ function byStart(a, b) {
31739
32979
  }
31740
32980
  function runRow(dir, entry) {
31741
32981
  const { record } = entry;
31742
- const events = join39(dir, `${record.sessionId}${EVENTS_EXTENSION}`);
32982
+ const events = join42(dir, `${record.sessionId}${EVENTS_EXTENSION}`);
31743
32983
  return {
31744
32984
  sessionId: record.sessionId,
31745
32985
  path: entry.file,
31746
- eventsPath: existsSync38(events) ? events : null,
32986
+ eventsPath: existsSync42(events) ? events : null,
31747
32987
  plan: record.planStub ?? record.plan,
31748
32988
  startedAt: record.startedAt,
31749
32989
  ticked: true
@@ -31752,19 +32992,19 @@ function runRow(dir, entry) {
31752
32992
 
31753
32993
  // src/cleanup/worktrees.ts
31754
32994
  import { realpathSync as realpathSync8, statSync as statSync15 } from "fs";
31755
- import { join as join41, sep as sep4 } from "path";
32995
+ import { join as join44, sep as sep4 } from "path";
31756
32996
 
31757
32997
  // src/pr/worktree.ts
31758
- import { existsSync as existsSync39, realpathSync as realpathSync7 } from "fs";
31759
- import { join as join40 } from "path";
31760
- var WORKTREES_SUBDIR = join40(".rafa", "worktrees");
32998
+ import { existsSync as existsSync43, realpathSync as realpathSync7 } from "fs";
32999
+ import { join as join43 } from "path";
33000
+ var WORKTREES_SUBDIR = join43(".rafa", "worktrees");
31761
33001
  var WORKTREE_PREFIX = "pr-";
31762
33002
  var DEFAULT_REMOTE3 = "origin";
31763
33003
  var REFUSAL_PREFIX2 = "rafa pr triage --resolve refuses";
31764
33004
  var FORCING_FLAG = /^(?:-f|--force(?:-with-lease|-if-includes)?(?:=.*)?|--mirror)$/;
31765
33005
  var FORCING_REFSPEC_MARK = "+";
31766
33006
  function resolveWorktreePath(home, number) {
31767
- return join40(home, WORKTREES_SUBDIR, `${WORKTREE_PREFIX}${number}`);
33007
+ return join43(home, WORKTREES_SUBDIR, `${WORKTREE_PREFIX}${number}`);
31768
33008
  }
31769
33009
  function crossRepositoryRefusal(pr) {
31770
33010
  if (!pr.isCrossRepository)
@@ -31831,7 +33071,7 @@ function pushResolved(git, branch, remote = DEFAULT_REMOTE3) {
31831
33071
  return { id: step2.id, ok: false, command: worktreeCommandLine(step2), said: refusal4 };
31832
33072
  }
31833
33073
  function pathForms(path3) {
31834
- if (!existsSync39(path3))
33074
+ if (!existsSync43(path3))
31835
33075
  return [path3];
31836
33076
  return [path3, realpathSync7(path3)];
31837
33077
  }
@@ -31844,7 +33084,7 @@ function worktreeAt(git, path3) {
31844
33084
  }
31845
33085
 
31846
33086
  // src/cleanup/worktrees.ts
31847
- var CLAUDE_WORKTREES_SUBDIR = join41(".claude", "worktrees");
33087
+ var CLAUDE_WORKTREES_SUBDIR = join44(".claude", "worktrees");
31848
33088
  var WORKTREE_LIST = Object.freeze(["worktree", "list", "--porcelain"]);
31849
33089
  var GIT_DIR = Object.freeze(["rev-parse", "--absolute-git-dir"]);
31850
33090
  var WORKTREE_STATUS = Object.freeze([
@@ -31853,7 +33093,7 @@ var WORKTREE_STATUS = Object.freeze([
31853
33093
  "--porcelain=v1",
31854
33094
  "--untracked-files=all"
31855
33095
  ]);
31856
- var ADMIN_FILES = Object.freeze(["HEAD", join41("logs", "HEAD"), "index"]);
33096
+ var ADMIN_FILES = Object.freeze(["HEAD", join44("logs", "HEAD"), "index"]);
31857
33097
  var MS_PER_DAY3 = 24 * 60 * 60 * 1000;
31858
33098
  var UNTRACKED_MARK = "?? ";
31859
33099
  var LOCKED_LINE = "locked";
@@ -31890,8 +33130,8 @@ function readWorktrees(seams, read) {
31890
33130
  return { ok: true, worktrees: [] };
31891
33131
  }
31892
33132
  const roots = [
31893
- ...pathForms2(seams, join41(main, CLAUDE_WORKTREES_SUBDIR)),
31894
- ...pathForms2(seams, join41(read.home, WORKTREES_SUBDIR)),
33133
+ ...pathForms2(seams, join44(main, CLAUDE_WORKTREES_SUBDIR)),
33134
+ ...pathForms2(seams, join44(read.home, WORKTREES_SUBDIR)),
31895
33135
  ...pathForms2(seams, worktreeDirAt(read.projectRoot, read.worktreeDir))
31896
33136
  ];
31897
33137
  const locks = parseLocks(listed2.stdout);
@@ -31962,7 +33202,7 @@ function lastModified(seams, git, path3) {
31962
33202
  if (!gitDir.ok || adminDir === "") {
31963
33203
  return failure4("git rev-parse --absolute-git-dir", gitDir);
31964
33204
  }
31965
- const times = [path3, ...ADMIN_FILES.map((file) => join41(adminDir, file))].map((file) => seams.modifiedAt(file)).filter((time) => time !== null).map((time) => time.getTime());
33205
+ const times = [path3, ...ADMIN_FILES.map((file) => join44(adminDir, file))].map((file) => seams.modifiedAt(file)).filter((time) => time !== null).map((time) => time.getTime());
31966
33206
  if (times.length === 0) {
31967
33207
  return `no modification time could be read under ${path3} or ${adminDir}`;
31968
33208
  }
@@ -32291,6 +33531,66 @@ function firstLine9(text) {
32291
33531
  `).map((line) => line.trim()).find((line) => line !== "") ?? "";
32292
33532
  }
32293
33533
 
33534
+ // src/start/forced-draft.ts
33535
+ var PASSED_OVER_HEADING = "## Passed-over tasks";
33536
+ function forcedDraftSeamsIn(dir) {
33537
+ return { gh: createGhRunner({ cwd: dir }), pulls: ghPullRequestsIn(dir) };
33538
+ }
33539
+ function passedOverSection(tasks) {
33540
+ return [
33541
+ PASSED_OVER_HEADING,
33542
+ "",
33543
+ "This pull request is a draft: `rafa loop start --continue --force-wrap-up` wrapped up with these tasks still open.",
33544
+ "",
33545
+ ...tasks.map((task) => `- line ${task.line} (${task.strategy}): ${task.text}. ${task.reason}`)
33546
+ ].join(`
33547
+ `);
33548
+ }
33549
+ function bodyWithPassedOver(body, tasks) {
33550
+ if (body.includes(PASSED_OVER_HEADING))
33551
+ return body;
33552
+ const kept = body.trimEnd();
33553
+ return kept === "" ? passedOverSection(tasks) : `${kept}
33554
+
33555
+ ${passedOverSection(tasks)}`;
33556
+ }
33557
+ async function convertToDraft(number, gh) {
33558
+ const command = `gh pr ready ${number} --undo`;
33559
+ const result = await gh(["pr", "ready", String(number), "--undo"]);
33560
+ if (result.ok)
33561
+ return true;
33562
+ const said = result.stderr.trim() || result.stdout.trim() || "nothing said";
33563
+ activeOutput().warn(`\u26A0\uFE0F The forced wrap-up could not mark pull request #${number} as a draft (${said}); run ${command} by hand.`);
33564
+ return false;
33565
+ }
33566
+ async function writeSection(number, tasks, pulls) {
33567
+ try {
33568
+ const pull = await pulls.get(number);
33569
+ if (pull === null)
33570
+ throw new Error(`no pull request #${number}`);
33571
+ const body = bodyWithPassedOver(pull.body, tasks);
33572
+ if (body !== pull.body)
33573
+ await pulls.editBody(number, body);
33574
+ } catch (error) {
33575
+ activeOutput().warn(`\u26A0\uFE0F The forced wrap-up could not write the passed-over tasks into pull request #${number} (${messageOf(error)}).`);
33576
+ }
33577
+ }
33578
+ async function markForcedDraft(number, tasks, seams) {
33579
+ const drafted = await convertToDraft(number, seams.gh);
33580
+ await writeSection(number, tasks, seams.pulls);
33581
+ if (drafted)
33582
+ activeOutput().info(`\uD83D\uDCDD Pull request #${number} is a draft, listing the ${tasks.length} passed-over task(s) in its body.`);
33583
+ return drafted;
33584
+ }
33585
+ function refuseUndraftedPullRequest(number) {
33586
+ emitLoopEvent({ kind: "halt", reason: `forced draft refused: pull request #${number} is ready for review` });
33587
+ throw new LoopEnd(DECISION_STOP_EXIT, [
33588
+ `\u26D4 The forced wrap-up's pull request #${number} could not be made a draft, so it is open ready for review with passed-over tasks left.`,
33589
+ ` Run gh pr ready ${number} --undo by hand. The run is recorded stopped, not done, and the CI wait was skipped.`
33590
+ ].join(`
33591
+ `));
33592
+ }
33593
+
32294
33594
  // src/start/pr-retarget.ts
32295
33595
  function retargetedLine(number, from, to) {
32296
33596
  return `\u21AA Retargeted pull request #${String(number)} from ${from} to ${to} (pr.base).`;
@@ -32381,8 +33681,8 @@ async function carryReleaseIntoPullRequest(finish, target, seams = {}) {
32381
33681
  }
32382
33682
 
32383
33683
  // src/release/prepare.ts
32384
- import { mkdirSync as mkdirSync15, readFileSync as readFileSync46, writeFileSync as writeFileSync18 } from "fs";
32385
- import { dirname as dirname16, join as join42, posix as posix5 } from "path";
33684
+ import { mkdirSync as mkdirSync15, readFileSync as readFileSync50, writeFileSync as writeFileSync18 } from "fs";
33685
+ import { dirname as dirname19, join as join45, posix as posix5 } from "path";
32386
33686
  var NO_FRAGMENT = "no release fragment";
32387
33687
  var WHITESPACE3 = /\s+/g;
32388
33688
  function levelFields(reading2) {
@@ -32440,7 +33740,7 @@ function readBase(input) {
32440
33740
  }
32441
33741
  function textOf3(path3) {
32442
33742
  try {
32443
- return readFileSync46(path3, "utf8");
33743
+ return readFileSync50(path3, "utf8");
32444
33744
  } catch {
32445
33745
  return null;
32446
33746
  }
@@ -32450,7 +33750,7 @@ function nameOf2(path3) {
32450
33750
  }
32451
33751
  function writeFragment(path3, resolved, text) {
32452
33752
  try {
32453
- mkdirSync15(dirname16(resolved), { recursive: true });
33753
+ mkdirSync15(dirname19(resolved), { recursive: true });
32454
33754
  writeFileSync18(resolved, text);
32455
33755
  return null;
32456
33756
  } catch (error) {
@@ -32475,7 +33775,7 @@ function prepareRelease(input) {
32475
33775
  const fragment = { plan: input.plan, title, level, notes: lines2.notes };
32476
33776
  const name2 = allocateFragmentName(input.plan, read.base.waiting.map((waiting) => nameOf2(waiting.path)));
32477
33777
  const path3 = posix5.join(input.settings.releaseFragments.replace(/\\/g, "/"), name2);
32478
- const resolved = join42(input.repoRoot, path3);
33778
+ const resolved = join45(input.repoRoot, path3);
32479
33779
  const before = textOf3(resolved);
32480
33780
  const after = serializeFragment(fragment);
32481
33781
  const problems = lines2.problem === null ? read.problems : [...read.problems, lines2.problem];
@@ -32495,12 +33795,12 @@ function prepareRelease(input) {
32495
33795
  }
32496
33796
 
32497
33797
  // src/release/verify.ts
32498
- import { readFileSync as readFileSync47, writeFileSync as writeFileSync19 } from "fs";
33798
+ import { readFileSync as readFileSync51, writeFileSync as writeFileSync19 } from "fs";
32499
33799
  import { posix as posix6 } from "path";
32500
33800
  var STATUS_PREFIX_LENGTH = 3;
32501
33801
  function textOf4(path3) {
32502
33802
  try {
32503
- return readFileSync47(path3, "utf8");
33803
+ return readFileSync51(path3, "utf8");
32504
33804
  } catch {
32505
33805
  return null;
32506
33806
  }
@@ -32526,7 +33826,7 @@ function restoreFragment(file) {
32526
33826
  };
32527
33827
  }
32528
33828
  }
32529
- function refuse10(prepared, refusal4) {
33829
+ function refuse11(prepared, refusal4) {
32530
33830
  const restore = restoreFragment(prepared.file);
32531
33831
  const clause = restore.problem ?? `${restore.path} was restored to the text the loop wrote`;
32532
33832
  return {
@@ -32594,20 +33894,20 @@ function checkFragment(prepared, text) {
32594
33894
  function verifyRelease(prepared, context) {
32595
33895
  const changed = changedBesideFragment(prepared, context);
32596
33896
  if (!changed.ok)
32597
- return refuse10(prepared, changed.refusal);
33897
+ return refuse11(prepared, changed.refusal);
32598
33898
  if (changed.paths.length > 0)
32599
- return refuse10(prepared, otherFilesRefusal(prepared, changed.paths));
33899
+ return refuse11(prepared, otherFilesRefusal(prepared, changed.paths));
32600
33900
  const path3 = prepared.file.path;
32601
33901
  const text = textOf4(prepared.file.resolved);
32602
33902
  if (text === null) {
32603
- return refuse10(prepared, {
33903
+ return refuse11(prepared, {
32604
33904
  reason: "fragment-unreadable",
32605
33905
  because: `${path3} could not be read back after the wrap-up session`
32606
33906
  });
32607
33907
  }
32608
33908
  const fragment = checkFragment(prepared, text);
32609
33909
  if ("reason" in fragment)
32610
- return refuse10(prepared, fragment);
33910
+ return refuse11(prepared, fragment);
32611
33911
  return { kind: "verified", path: path3, fragment, text };
32612
33912
  }
32613
33913
 
@@ -32794,7 +34094,7 @@ async function finishRelease(input, seams = {}) {
32794
34094
  }
32795
34095
 
32796
34096
  // src/start/runner-pr.ts
32797
- import { readFileSync as readFileSync48 } from "fs";
34097
+ import { readFileSync as readFileSync52 } from "fs";
32798
34098
  import path3 from "path";
32799
34099
  var RELEASE_NOTES_HEADING = "## Release notes";
32800
34100
  var UNFINISHED_WRAP_UP_LINE = "> \u26A0\uFE0F The wrap-up did not finish: no wrap-up session opened this pull request, so the loop opened it. Check that the base is merged in, the release notes were rewritten and the lessons were promoted before merging.";
@@ -32827,7 +34127,7 @@ function fragmentNotesIn(checkout, fragment) {
32827
34127
  return [];
32828
34128
  let text;
32829
34129
  try {
32830
- text = readFileSync48(path3.resolve(checkout, fragment), "utf8");
34130
+ text = readFileSync52(path3.resolve(checkout, fragment), "utf8");
32831
34131
  } catch {
32832
34132
  return [];
32833
34133
  }
@@ -32942,7 +34242,7 @@ function parsePromoted(output) {
32942
34242
  if (!block.closed)
32943
34243
  return { status: "unclosed", answers: [], unreadable: [] };
32944
34244
  const answers = [];
32945
- const unreadable = [];
34245
+ const unreadable2 = [];
32946
34246
  const firstLine10 = new Map;
32947
34247
  block.body.split(`
32948
34248
  `).forEach((text, index) => {
@@ -32951,20 +34251,20 @@ function parsePromoted(output) {
32951
34251
  const line = block.span.first + 1 + index;
32952
34252
  const reading2 = readLine2(text, line);
32953
34253
  if ("reason" in reading2) {
32954
- unreadable.push({ line, text, reason: reading2.reason });
34254
+ unreadable2.push({ line, text, reason: reading2.reason });
32955
34255
  return;
32956
34256
  }
32957
34257
  const { id: id2 } = reading2.answer;
32958
34258
  const earlier = firstLine10.get(id2);
32959
34259
  if (earlier !== undefined) {
32960
34260
  const reason = `\`${id2}\` was already answered at line ${String(earlier)}`;
32961
- unreadable.push({ line, text, reason });
34261
+ unreadable2.push({ line, text, reason });
32962
34262
  return;
32963
34263
  }
32964
34264
  firstLine10.set(id2, line);
32965
34265
  answers.push(reading2.answer);
32966
34266
  });
32967
- return { status: "read", answers, unreadable };
34267
+ return { status: "read", answers, unreadable: unreadable2 };
32968
34268
  }
32969
34269
 
32970
34270
  // src/start/promoted-check.ts
@@ -33212,7 +34512,7 @@ async function openPullRequestNumber(checkout, branch) {
33212
34512
  return null;
33213
34513
  }
33214
34514
  }
33215
- function messageOf9(error) {
34515
+ function messageOf11(error) {
33216
34516
  return error instanceof Error ? error.message : String(error);
33217
34517
  }
33218
34518
  function wrapUpAdapter(learning) {
@@ -33233,7 +34533,7 @@ async function lessonsToPromote(learning) {
33233
34533
  minConfidence: learning.promoteMinConfidence
33234
34534
  });
33235
34535
  } catch (error) {
33236
- activeOutput().warn(` The wrap-up lists no lesson to promote: the \`${learning.kind}\` learning adapter answered no blessed set: ${messageOf9(error)}`);
34536
+ activeOutput().warn(` The wrap-up lists no lesson to promote: the \`${learning.kind}\` learning adapter answered no blessed set: ${messageOf11(error)}`);
33237
34537
  return [];
33238
34538
  }
33239
34539
  }
@@ -33343,9 +34643,11 @@ async function runWrapUp(input) {
33343
34643
  ciTimeoutMin,
33344
34644
  ciAttempts
33345
34645
  } = input;
34646
+ const passedOver = input.passedOver ?? [];
33346
34647
  session.wrapUpStarted();
33347
- activeOutput().info(`
33348
- \u2705 All tasks completed!`);
34648
+ activeOutput().info(passedOver.length === 0 ? `
34649
+ \u2705 All tasks completed!` : `
34650
+ \u26A0\uFE0F Wrapping up with ${passedOver.length} passed-over task(s) left open (--force-wrap-up): the pull request becomes a draft listing them.`);
33349
34651
  activeOutput().info("\uD83E\uDDF9 Wrap-up session starting: promote the listed lessons, sync with main, then commit, push and open the PR.");
33350
34652
  activeOutput().info(" This is one full Claude session with no intermediate output \u2014 expect several quiet minutes. Interrupting it skips the push and PR; if that happens, run again to retry just this stage.");
33351
34653
  emitLoopEvent({ kind: "wrap-up", phase: "fragment" });
@@ -33371,15 +34673,23 @@ async function runWrapUp(input) {
33371
34673
  }
33372
34674
  emitLoopEvent({ kind: "wrap-up", phase: "release" });
33373
34675
  const finish = await finishRelease({ repoRoot: checkout, branch: expected2.branch, settings, preparation: release }, { readProvider });
34676
+ if (passedOver.length > 0 && readProvider().provider === "none") {
34677
+ activeOutput().warn("\u26A0\uFE0F pr.provider is none: the forced wrap-up has no pull request to mark as a draft.");
34678
+ }
33374
34679
  if (readProvider().provider !== "none") {
33375
34680
  const delivery = await deliverPullRequest({ branch: expected2.branch, retries: settings.loopWrapUpRetries, previousMessage: finalMessage }, deliverySeamsIn({ ...input, base, fragment: finish.fragment }));
33376
- await emitPullRequestEvent(expected2.branch, delivery.kind === "delivered" ? delivery.pull.number : null, lookup);
34681
+ const drafted = delivery.kind !== "delivered" || passedOver.length === 0 || await markForcedDraft(delivery.pull.number, passedOver, forcedDraftSeamsIn(checkout));
34682
+ if (drafted) {
34683
+ await emitPullRequestEvent(expected2.branch, delivery.kind === "delivered" ? delivery.pull.number : null, lookup);
34684
+ }
33377
34685
  if (delivery.kind === "interrupted")
33378
34686
  return;
33379
34687
  if (delivery.kind === "blocked")
33380
34688
  throw new CommandExit(1, delivery.message);
33381
34689
  await carryReleaseIntoPullRequest(finish, { repoRoot: checkout, branch: expected2.branch }, { readProvider });
33382
34690
  await retargetPullRequest(delivery.pull, base, { pulls: ghPullRequestsIn(checkout), output: activeOutput() });
34691
+ if (!drafted)
34692
+ refuseUndraftedPullRequest(delivery.pull.number);
33383
34693
  } else {
33384
34694
  await emitPullRequestEvent(expected2.branch, null, lookup);
33385
34695
  }
@@ -33532,8 +34842,8 @@ function msUntil(startAt, now = new Date) {
33532
34842
  return target.getTime() - now.getTime();
33533
34843
  }
33534
34844
  function sleepFor(ms) {
33535
- return new Promise((resolve24) => {
33536
- setTimeout(resolve24, ms);
34845
+ return new Promise((resolve25) => {
34846
+ setTimeout(resolve25, ms);
33537
34847
  });
33538
34848
  }
33539
34849
  async function deferUntil(startAt, now = new Date, sleep = sleepFor) {
@@ -33543,7 +34853,7 @@ async function deferUntil(startAt, now = new Date, sleep = sleepFor) {
33543
34853
  }
33544
34854
 
33545
34855
  // src/start.ts
33546
- var __dirname2 = path4.dirname(fileURLToPath2(import.meta.url));
34856
+ var __dirname2 = path4.dirname(fileURLToPath5(import.meta.url));
33547
34857
  var interrupted2 = false;
33548
34858
  async function start(args, repoRoot) {
33549
34859
  refuseDetachedRun(args);
@@ -33563,7 +34873,10 @@ async function start(args, repoRoot) {
33563
34873
  }
33564
34874
  refuseWorktreeWhileTracking(args, runConfig.config);
33565
34875
  const { inject: injectMode, settingSources } = runConfig.config;
33566
- const { startAt, plan, ciWait, ciTimeoutMin, ciAttempts, roadmap } = readRunArgs(args);
34876
+ const { startAt, plan, ciWait, ciTimeoutMin, ciAttempts, roadmap, retry, continueRun } = readRunArgs(args);
34877
+ refuseUnusableCriteria(continueRun, repoRoot, runConfig.config);
34878
+ const runRetries = resolveRunRetries(retry, configuredRetries(runConfig.config, continueRun.on));
34879
+ refuseRetryWithoutBudget(continueRun, runRetries);
33567
34880
  if (startAt)
33568
34881
  await deferUntil(startAt);
33569
34882
  const planPath = resolvePlanPath(repoRoot, runConfig.config.planDir, plan);
@@ -33593,6 +34906,9 @@ async function start(args, repoRoot) {
33593
34906
  setActivePlanStub(planStub2);
33594
34907
  const session = openRunSession({ repoRoot, planPath, planStub: planStub2, branch, roadmap, checkout });
33595
34908
  bindEventsFile(repoRoot, session.id);
34909
+ let decisionsAtEnd = () => {
34910
+ return;
34911
+ };
33596
34912
  try {
33597
34913
  const planContent = fs.readFileSync(planPath, "utf8");
33598
34914
  const promptContent = fs.readFileSync(promptPath, "utf8");
@@ -33650,6 +34966,27 @@ async function start(args, repoRoot) {
33650
34966
  planContent,
33651
34967
  isInterrupted: () => interrupted2
33652
34968
  });
34969
+ const retries = createRunRetries({
34970
+ retries: runRetries,
34971
+ isInterrupted: () => interrupted2,
34972
+ isCheckoutHeld: () => guardCheckout(expected2).held
34973
+ });
34974
+ const runPlace = { planPath, planStub: planStub2, branch, checkout };
34975
+ const decisions = createRunDecisions({
34976
+ continueArgs: continueRun,
34977
+ repoRoot,
34978
+ checkout,
34979
+ trackerPath,
34980
+ planPath,
34981
+ settings: runConfig.config,
34982
+ settingSources,
34983
+ session,
34984
+ retries,
34985
+ isInterrupted: () => interrupted2,
34986
+ seed: continueRun.on ? readPreviousPassOver(repoRoot, runPlace) : [],
34987
+ previousNeeded: continueRun.directive === null ? null : readPreviousDecisionNeeded(repoRoot, runPlace)
34988
+ });
34989
+ decisionsAtEnd = decisions.atRunEnd;
33653
34990
  if (!fs.existsSync(trackerPath)) {
33654
34991
  activeOutput().info(`\uD83D\uDCCB Creating new plan tracker at ${path4.basename(trackerPath)}...`);
33655
34992
  fs.copyFileSync(planPath, trackerPath);
@@ -33667,7 +35004,10 @@ async function start(args, repoRoot) {
33667
35004
  if (interrupted2)
33668
35005
  break;
33669
35006
  const trackerContent = fs.readFileSync(trackerPath, "utf8");
33670
- const taskInfo = findNextTask(trackerContent);
35007
+ const taskInfo = findNextTask(trackerContent, { skipLines: decisions.skipLines(trackerContent) });
35008
+ const passedOver = taskInfo ? [] : decisions.atPlanEnd(trackerContent);
35009
+ if (await decisions.atFirstTask(taskInfo, trackerContent))
35010
+ continue;
33671
35011
  const moved = taskInfo ? haltIfCheckoutMoved({ expected: expected2, trackerPath, taskInfo }) : haltIfWrapUpMoved({ expected: expected2, before: "dispatch" });
33672
35012
  if (moved) {
33673
35013
  emitLoopEvent({ kind: "halt", reason: "checkout moved" });
@@ -33675,8 +35015,12 @@ async function start(args, repoRoot) {
33675
35015
  }
33676
35016
  if (!taskInfo)
33677
35017
  emitLoopEvent({ kind: "wrap-up", phase: "tests" });
33678
- const suiteGate = await suiteSteps.beforeSession(taskInfo);
35018
+ const suiteGate = decisions.gateForcedWrapUp(await suiteSteps.beforeSession(taskInfo), passedOver, suiteSteps.lastPreWrapUp());
33679
35019
  if (suiteGate === "stop") {
35020
+ if (!suiteSteps.stoppedOnSignal() && retries.retry("suite step red", null))
35021
+ continue;
35022
+ if (!suiteSteps.stoppedOnSignal() && await decisions.atStop({ kind: "suite-red" }))
35023
+ continue;
33680
35024
  emitLoopEvent({ kind: "halt", reason: "suite step red" });
33681
35025
  return;
33682
35026
  }
@@ -33701,12 +35045,14 @@ async function start(args, repoRoot) {
33701
35045
  ciWait,
33702
35046
  ciTimeoutMin,
33703
35047
  ciAttempts,
33704
- isInterrupted: () => interrupted2
35048
+ isInterrupted: () => interrupted2,
35049
+ passedOver
33705
35050
  });
33706
35051
  break;
33707
35052
  }
33708
35053
  const base = expected2.head;
33709
35054
  const position = taskPosition(trackerContent, taskInfo.lineNum);
35055
+ const retryTask = retryTaskOf(taskInfo, trackerContent);
33710
35056
  const startedAt = Date.now();
33711
35057
  emitLoopEvent({ kind: "task-start", position, text: parseTaskDeclaration(taskInfo.task).text });
33712
35058
  session.taskStarted(taskInfo);
@@ -33722,6 +35068,7 @@ async function start(args, repoRoot) {
33722
35068
  knownMissing,
33723
35069
  inherited: runStartFailures(trackerPath),
33724
35070
  alwaysRun: readAlwaysRunFiles(createGitRunner(checkout), runConfig.config.testsAlwaysRun),
35071
+ typeCheck: readTypeCheckRecipe(checkout, createGitRunner(checkout)),
33725
35072
  serving,
33726
35073
  handout,
33727
35074
  base,
@@ -33755,10 +35102,15 @@ async function start(args, repoRoot) {
33755
35102
  }
33756
35103
  if (exitCode !== 0) {
33757
35104
  updateTrackerLine(trackerPath, taskInfo.lineNum, "blocked");
35105
+ await storeReport("failed");
35106
+ const failed3 = { kind: "task-blocked", position, reason: `session exited ${exitCode}` };
35107
+ if (retries.retry(`session exited ${exitCode}`, retryTask))
35108
+ continue;
35109
+ if (await decisions.atStop({ kind: "session-exit", taskInfo, retryTask, exitCode, stopEvent: failed3 }))
35110
+ continue;
33758
35111
  activeOutput().error(`
33759
35112
  \u274C Task failed (exit ${exitCode}). Marked as blocked. Run again to retry.`);
33760
- emitLoopEvent({ kind: "task-blocked", position, reason: `session exited ${exitCode}` });
33761
- await storeReport("failed");
35113
+ emitLoopEvent(failed3);
33762
35114
  return;
33763
35115
  }
33764
35116
  if (haltIfCheckoutMoved({ expected: expected2, trackerPath, taskInfo, before: "commit" })) {
@@ -33776,9 +35128,16 @@ async function start(args, repoRoot) {
33776
35128
  expected2 = advanceExpectation(expected2, finished.attempt);
33777
35129
  const stored = await storeReport(finished.outcome);
33778
35130
  if (finished.outcome !== "done") {
33779
- emitLoopEvent({ kind: "task-blocked", position, reason: finished.holds[0] ?? "held by its report" });
35131
+ const held = { kind: "task-blocked", position, reason: finished.holds[0] ?? "held by its report" };
35132
+ if (heldOnNothingLeftBehind(finished) && retries.retry("left neither a report nor a commit", retryTask))
35133
+ continue;
35134
+ if (await decisions.atStop({ kind: "clean-exit", taskInfo, retryTask, finished, stopEvent: held }))
35135
+ continue;
35136
+ emitLoopEvent(held);
33780
35137
  return;
33781
35138
  }
35139
+ decisions.taskDone(taskInfo);
35140
+ retries.taskDone(retryTask);
33782
35141
  const tokens = await unlessText(async () => taskTokens(checkout, dispatch.sessionId));
33783
35142
  emitLoopEvent({ kind: "task-done", position, durationMs: Date.now() - startedAt, tokens });
33784
35143
  if (!stored) {
@@ -33787,14 +35146,20 @@ async function start(args, repoRoot) {
33787
35146
  return;
33788
35147
  }
33789
35148
  if (!await suiteSteps.afterTask(taskInfo, base)) {
35149
+ if (!suiteSteps.stoppedOnSignal() && retries.retry("suite step red", retryTask))
35150
+ continue;
35151
+ if (!suiteSteps.stoppedOnSignal() && await decisions.atStop({ kind: "suite-red" }))
35152
+ continue;
33790
35153
  emitLoopEvent({ kind: "halt", reason: "suite step red" });
33791
35154
  return;
33792
35155
  }
33793
35156
  }
33794
35157
  } catch (error) {
33795
- emitLoopEvent({ kind: "error", message: messageOf(error) });
35158
+ if (!(error instanceof LoopEnd))
35159
+ emitLoopEvent({ kind: "error", message: messageOf(error) });
33796
35160
  throw error;
33797
35161
  } finally {
35162
+ decisionsAtEnd();
33798
35163
  unbindEventsFile();
33799
35164
  session.end();
33800
35165
  }
@@ -33804,7 +35169,7 @@ async function start(args, repoRoot) {
33804
35169
  import fs4 from "fs";
33805
35170
  import { homedir as homedir11 } from "os";
33806
35171
  import path8 from "path";
33807
- import { fileURLToPath as fileURLToPath3 } from "url";
35172
+ import { fileURLToPath as fileURLToPath6 } from "url";
33808
35173
 
33809
35174
  // src/claims/plan-claim.ts
33810
35175
  import path5 from "path";
@@ -33944,7 +35309,7 @@ function otherBranchRefusal(attempt3, reading2, context) {
33944
35309
  function claimOwnBranch(attempt3, reading2, context) {
33945
35310
  if (reading2.state === "absent")
33946
35311
  return { kind: "fresh" };
33947
- const unreadable = () => ({
35312
+ const unreadable2 = () => ({
33948
35313
  kind: "answer",
33949
35314
  answer: refused2(attempt3, attempt3.branch, null, `the claim on ${attempt3.branch} could not be read`)
33950
35315
  });
@@ -33952,12 +35317,12 @@ function claimOwnBranch(attempt3, reading2, context) {
33952
35317
  if (unnamed !== null)
33953
35318
  return { kind: "answer", answer: unnamed };
33954
35319
  if (reading2.state !== "found")
33955
- return unreadable();
35320
+ return unreadable2();
33956
35321
  const weighed = weighBranchClaim(reading2, { labels: attempt3.labels ?? [], staleAfter: context.staleAfter, now: context.now });
33957
35322
  if (weighed.state === "released" || weighed.state === "stale-claimed")
33958
35323
  return { kind: "take", tip: reading2.tip };
33959
35324
  if (weighed.state === "none" || weighed.state === "unreadable")
33960
- return unreadable();
35325
+ return unreadable2();
33961
35326
  if (weighed.owner === attempt3.storeId)
33962
35327
  return { kind: "held" };
33963
35328
  const note = weighed.state === "stale-in-development" ? ", stale but in development, which is never taken over automatically" : "";
@@ -34414,11 +35779,11 @@ async function resolveCreateSpec(options, seams = DEFAULT_ROUTE_SEAMS) {
34414
35779
  const source = readSpecSourceFlags(options.args);
34415
35780
  if (source.request === null)
34416
35781
  throw new CommandExit(SOURCE_REFUSAL_EXIT, usageRefusal(specsDir));
34417
- const resolve24 = seams.resolve ?? resolvePlanSpec;
35782
+ const resolve25 = seams.resolve ?? resolvePlanSpec;
34418
35783
  const makeReadyOffer = seams.makeReadyOffer ?? createPlanReadyOffer;
34419
35784
  const makeAlternativeOffer = seams.makeAlternativeOffer ?? createPlanBlockedOffer;
34420
35785
  const makeRefreshOffer = seams.makeRefreshOffer ?? createPlanRefreshOffer;
34421
- return await resolve24({
35786
+ return await resolve25({
34422
35787
  request: source.request,
34423
35788
  refresh: source.refresh,
34424
35789
  dryRun: source.dryRun,
@@ -34437,7 +35802,7 @@ async function resolveCreateSpec(options, seams = DEFAULT_ROUTE_SEAMS) {
34437
35802
  }
34438
35803
 
34439
35804
  // src/plan.ts
34440
- var __dirname3 = path8.dirname(fileURLToPath3(import.meta.url));
35805
+ var __dirname3 = path8.dirname(fileURLToPath6(import.meta.url));
34441
35806
  function argValue2(args, flag) {
34442
35807
  const hit = args.find((a) => a.startsWith(`${flag}=`));
34443
35808
  return hit?.slice(flag.length + 1);
@@ -34581,7 +35946,7 @@ var PLAN_PROMPT_SLOTS = [
34581
35946
  "EPIC_CONTEXT",
34582
35947
  "SPEC_CONTENT"
34583
35948
  ];
34584
- var SLOT_PATTERN = new RegExp(`\\{(${PLAN_PROMPT_SLOTS.join("|")})\\}`, "g");
35949
+ var SLOT_PATTERN3 = new RegExp(`\\{(${PLAN_PROMPT_SLOTS.join("|")})\\}`, "g");
34585
35950
  function buildPlanPrompt(template, planFormat, specContent, stub, planDir, progressContent, routing = DEFAULT_ROUTING, skillIndex = "", epic = null) {
34586
35951
  const values = {
34587
35952
  PLAN_FILE: planFilePath(planDir, `PLAN-${stub}.md`),
@@ -34593,7 +35958,7 @@ function buildPlanPrompt(template, planFormat, specContent, stub, planDir, progr
34593
35958
  EPIC_CONTEXT: formatEpicContextSection(epic),
34594
35959
  SPEC_CONTENT: specContent
34595
35960
  };
34596
- return template.replace(SLOT_PATTERN, (_slot, name2) => values[name2]);
35961
+ return template.replace(SLOT_PATTERN3, (_slot, name2) => values[name2]);
34597
35962
  }
34598
35963
  function stubFromSpecPath(specPath2) {
34599
35964
  return path8.basename(specPath2).replace(/\.md$/, "");
@@ -34666,7 +36031,7 @@ async function plan(args, repoRoot, registry = CORE_ADAPTER_REGISTRY) {
34666
36031
  const epic = await readPlanEpicContext(resolved.spec, createGhRunner({ cwd: repoRoot }));
34667
36032
  const template = fs4.readFileSync(path8.join(__dirname3, "plan-prompt.md"), "utf8");
34668
36033
  const planFormat = readPlanFormat(__dirname3);
34669
- const skillIndex = readPlanSkillIndex(repoRoot, home, config, fileURLToPath3(import.meta.url));
36034
+ const skillIndex = readPlanSkillIndex(repoRoot, home, config, fileURLToPath6(import.meta.url));
34670
36035
  const planner = registry.resolve("planner", "claude").create({
34671
36036
  repoRoot,
34672
36037
  planDir,
@@ -34701,4 +36066,4 @@ async function plan(args, repoRoot, registry = CORE_ADAPTER_REGISTRY) {
34701
36066
  }
34702
36067
  }
34703
36068
 
34704
- export { SERVE_CLI_VERSION, PROVENANCE_FIELD, SKILL_FILE2 as SKILL_FILE, provenanceBlock, serveVerdict, SKILL_USE_CLI_VERSION, sessionLogDir, MANIFEST_VERSION, FEATURE_TYPES, OUTPUT_CHANNELS, RUNNING_MANIFEST_SEAMS, validateManifest, loadModules, moduleSettings, pullBeforeRead, collectSessionRow, collect, AGENT_DEFINITION_DIR, VENDOR_COMMAND, readAgentDirectory, resolveAgentRoster, missingPlanAgents, isStackList, stackPaths, stacksFromEccSkillName, DESCRIPTION_LIMIT, LISTING_LIMIT, SKILL_SIGNALS, countCharacters, toolOf, pathDirectories, checkReferences, checkFile, checkDirectory, bundledAgentsDirectory, sourceItem, byName, skillItem, readTrees, parseReportArgs, formatCount, alignRows, verdictOf, failingRows, formatRows, waitForChecks, recordPlanCi, formatTokens, formatTrendSegment, formatLoopsSegment, readTrendInputs, trendReportOf, buildReport, readSkillsReport, report, SPEC_EXTENSION, boardId, notesFileName, branchName, shellQuote, ghMissingMessage, ghUnauthenticatedMessage, ghHostOf, ghPreflightItems, parseWorkingTree, parseTreePaths, untrackedLeftLine, parseWorktrees, worktreesHolding, readMergeRefusal, resolvePrProvider, requireGhProvider, createGhPullRequests, ghPullRequestsIn, REMOTE, BRANCH_PREFIX2 as BRANCH_PREFIX, parseBaseStanding, hasDiverged, parseClaimMessage, claimBranchIssue, makeOwnershipCommit, fetchClaimBranches, readClaimBranch, pushOwnershipCommit, CLAIMED_LABEL, IN_DEVELOPMENT_LABEL, readClaimState, idleText, ROADMAP_TITLE, ROADMAP_SETTING, parseRoadmapBody, createGhRoadmapSearch, severalRoadmapsMessage, createGhOpenPullRequests, closedIssuesIn, branchClaims, scanClaimBranches, readEpicBody, SPEC_BLOCKED_LABEL, BLOCKED_BY_FIELD, hasSpecBlockedLabel, readBlockedBy, EPIC_LABEL_PREFIX, epicSlugsOf, isNotPlanned, groupByEpicLabel, localDay, readEpics, BOARD_LISTING_LIMIT, createGhBoardListing, SPEC_READY_LABEL, findReadinessGaps, requireCompleteSpec, validate_default, RefsBlockError, fingerprintText, PREVIOUS_COPY_WARN_ABOVE, previousDir, countPreviousCopies, createGhSpecIssueReader, hasSpecLabel, readRoadmapSetting, ROADMAP_LABEL, BOARD_LABELS, LABEL_LIST_LIMIT, SPEC_TEMPLATE_PATH, TEMPLATES_DIRNAME, boardChanged, listBoardLabels, missingBoardLabels, setUpLabels, anythingAt, writeShippedFile, setUpBoard, resolveDefaultBoard, unlabelledRoadmapMessage, createGhRoadmapBody, tickSentence, appendLine, tickLine, editChecklist, noBoardListsLine, epicTickSentence, tickRoadmapAfterMerge, createGhIssueBoard, commandRetrySeams, openProjectRunner, matchProjectFields, PROJECT_SCOPE_FIX, BOARD_SYNC_FIX, isMissingProjectScope, scopeWarning, rateLimitWarning, notFoundWarning, skippedFieldWarning, notRefreshedWarning, isWaiting, blockerArg, createNativeRelations, selectBoardRelations, resolveTracker, DEFAULT_ISSUE_SEAMS, lineRefusal, expectTwoArguments, readTextFlag, readNonBlankFlag, readRequiredFlag, readChoice, readChoiceFlag, issueProject, issueSubjectConfig, resolveIssueTracker, issueRef, issueName, urlLines, onTracker, NATIVE_MODE, NATIVE_RETRY_HINT, nativeEpicLeft, nativeAlreadyMessage, nativeParentLine, readBoardRepository, configuredMoveRelations, FRAGMENT_PLAN_ID_PATTERN, serializeFragment, allocateFragmentName, directoryPrefix, HORIZON_LABEL_PREFIX, readEpicProblems, epicProblemMessage, readEpicDependents, REASON_FLAG, normaliseReason, renderHorizonComment, renderParkedPullRequestComment, renderMoveComment, renderCloseComment, renderCancelComment, cancelMoveReason, renderDependentComment, renderUnblockNote, readReason, unaskedReasonMessage, blankReasonMessage, readRefsText, readCopyRefs, restampCopyRefs, RefVerifyError, readConfiguredRelations, requireNoLeak, blockedLineSentence, boardRepoLabel, memoiseVerifier, LEARNED_DIRECTORY, ORIGIN_FIELD, DEMOTION_VERDICTS, WHEN_TO_USE_HEADING, bodySections, classifySkill, firstSentence, convertToInstinct, rankCandidates, readRankCandidate, NO_NEXT_STEP, readSchemaReport, gateMeaning, unknownAdditive, unknownAdditiveWarning, developmentApplied, isFirstDispatch, STAGE_LABELS, unlabelReleased, createStartPreflightClaim, invalidateRows, checkDrift, driftLines, readDoctorEffortSync, renderDoctorEffortSync, effortSyncRefusal, SESSION_ID_FLAG2 as SESSION_ID_FLAG, readBoardBody, openBoards, boardOfEpic, createOwnerResolver, runIssueList, ISSUE_LIST_FLAGS, list_default, firstNowEpic, show_default, samePlace, resolvePlace, openPhase, commandProgressFeed, createGhProjectPort, resolveLoopSeams, sessionIdFlag, lineRefusal2 as lineRefusal1, refusal3 as refusal, NoSessionRefusal, isLive, planLabel, sessionLine, projectRoot2 as projectRoot, readRecords, pickSession, writeSession, readSessionChecklist, phaseNote, phasedCounts, checkboxAt, estimateEta, readSessionFinishes, formatDuration, etaLine, RELEASE_REMOTE, RELEASE_BASE_BRANCH, parseSemanticVersion, readManifestVersion, replaceManifestVersion, gitPathOf, receiptProblem, readReceiptVerdict, CHANGELOG_TITLE, releaseCommitSubject, readSettle, buildSettle, waitingSettingsOf, readWaiting, forecastPhrase, DEFAULT_RELEASE_SEAMS, compareVersions, changelogVersions, readTags, status_default, epicEndLines, watchDryEpic, nextOwnerGate, resolveReleaseEnabled, refuseMisplacedRuntime, readBinPath, self_update_default, versionTag, INDENT4 as INDENT, cleanUpAfterMerge, reportFollowUps, RELEASE_BLOCK_OPEN, RELEASE_BLOCK_CLOSE, localPlanNotes, readGuard, guardLines, mergeGuardSettings, readMergeGuard, guardReaction, followsMerge, readSettleAfterMerge, DEFAULT_BASE_BRANCH, ghNextBoard, openNextSources, readRemoteHead, eventsFilePath, WORKTREES_SUBDIR, resolveWorktreePath, crossRepositoryRefusal, addResolveWorktree, removeResolveWorktree, pushResolved, worktreeAt, worktreeDirAt, WORKTREE_STATUS, countChanges, cleanupSteps, dryRunLines, runCleanupSteps, defaultCleanupSeams, readCleanup, cleanupCounts, createLinePrompter, openNextRelations, list_default2 as list_default1, readBranch2 as readBranch, readPlans, readOpenPull, readBranchPlan, readNextState, readHomeAfterLoop, commandWords, nextQuestion, NEXT_COMMAND_ACTIONS, LABELS_ONLY_ACTIONS, actionInvocation, runAction, HINT_FLAG_SPEC, actionOutput, endWithNextStep, endingWith, lazyPrompter2 as lazyPrompter, ready_default, DEFAULT_PLAN_FILE, resolvePlanPath, assessPlanRisk, renderRiskText, announceRiskTotal, readBlockedIssues, renderBlockedIssues, blockerToken, addBlockerToBody, resolveCreateSpec, TRIAGE_MODULE, namedSecrets, triageReport, unresolvedTracker, DISK_ROOTS_FILE_SYSTEM, gitToplevel, rootRefusal, rootCandidates, DEFAULT_CI_TIMEOUT_MIN, CI_POLL_INTERVAL_MS, DEFAULT_CI_ATTEMPTS, retargetedLine, MECHANICAL_CONFLICT_SENTENCE, start, createPlanRefsVerifier, readDoctorRefs, renderDoctorRefs, createPlanDirNames, hasPlanFor, HORIZONS, horizonOf, readListedEpics, stageOf, rankOf, readIssueFacts, readRefreshBoard, refreshProjectItems, refreshIssueItems, refreshFailedWarning, unblock_default, unblockAfterMerge, labelsLineBlockersOf, LABELS_READS, createLabelsRelations, EPIC_TYPE_LABEL, plan };
36069
+ export { SERVE_CLI_VERSION, PROVENANCE_FIELD, SKILL_FILE2 as SKILL_FILE, provenanceBlock, serveVerdict, SKILL_USE_CLI_VERSION, sessionLogDir, MANIFEST_VERSION, FEATURE_TYPES, OUTPUT_CHANNELS, RUNNING_MANIFEST_SEAMS, validateManifest, loadModules, moduleSettings, pullBeforeRead, collectSessionRow, collect, AGENT_DEFINITION_DIR, VENDOR_COMMAND, readAgentDirectory, resolveAgentRoster, missingPlanAgents, isStackList, stackPaths, stacksFromEccSkillName, DESCRIPTION_LIMIT, LISTING_LIMIT, SKILL_SIGNALS, countCharacters, toolOf, pathDirectories, checkReferences, checkFile, checkDirectory, bundledAgentsDirectory, sourceItem, byName, skillItem, readTrees, parseReportArgs, formatCount, alignRows, verdictOf, failingRows, formatRows, waitForChecks, recordPlanCi, formatTokens, formatTrendSegment, formatLoopsSegment, readTrendInputs, trendReportOf, buildReport, readSkillsReport, report, SPEC_EXTENSION, boardId, notesFileName, branchName, shellQuote, ghMissingMessage, ghUnauthenticatedMessage, ghHostOf, ghPreflightItems, parseWorkingTree, parseTreePaths, untrackedLeftLine, parseWorktrees, worktreesHolding, readMergeRefusal, resolvePrProvider, requireGhProvider, createGhPullRequests, ghPullRequestsIn, REMOTE, BRANCH_PREFIX2 as BRANCH_PREFIX, parseBaseStanding, hasDiverged, parseClaimMessage, claimBranchIssue, makeOwnershipCommit, fetchClaimBranches, readClaimBranch, pushOwnershipCommit, CLAIMED_LABEL, IN_DEVELOPMENT_LABEL, readClaimState, idleText, ROADMAP_TITLE, ROADMAP_SETTING, parseRoadmapBody, createGhRoadmapSearch, severalRoadmapsMessage, createGhOpenPullRequests, closedIssuesIn, branchClaims, scanClaimBranches, readEpicBody, SPEC_BLOCKED_LABEL, BLOCKED_BY_FIELD, hasSpecBlockedLabel, readBlockedBy, EPIC_LABEL_PREFIX, epicSlugsOf, isNotPlanned, groupByEpicLabel, localDay, readEpics, BOARD_LISTING_LIMIT, createGhBoardListing, SPEC_READY_LABEL, findReadinessGaps, requireCompleteSpec, validate_default, RefsBlockError, fingerprintText, PREVIOUS_COPY_WARN_ABOVE, previousDir, countPreviousCopies, createGhSpecIssueReader, hasSpecLabel, readRoadmapSetting, ROADMAP_LABEL, BOARD_LABELS, LABEL_LIST_LIMIT, SPEC_TEMPLATE_PATH, TEMPLATES_DIRNAME, boardChanged, listBoardLabels, missingBoardLabels, setUpLabels, anythingAt, writeShippedFile, setUpBoard, resolveDefaultBoard, unlabelledRoadmapMessage, createGhRoadmapBody, tickSentence, appendLine, tickLine, editChecklist, noBoardListsLine, epicTickSentence, tickRoadmapAfterMerge, createGhIssueBoard, commandRetrySeams, openProjectRunner, matchProjectFields, PROJECT_SCOPE_FIX, BOARD_SYNC_FIX, isMissingProjectScope, scopeWarning, rateLimitWarning, notFoundWarning, skippedFieldWarning, notRefreshedWarning, isWaiting, blockerArg, createNativeRelations, selectBoardRelations, resolveTracker, DEFAULT_ISSUE_SEAMS, lineRefusal, expectTwoArguments, readTextFlag, readNonBlankFlag, readRequiredFlag, readChoice, readChoiceFlag, issueProject, issueSubjectConfig, resolveIssueTracker, issueRef, issueName, urlLines, onTracker, NATIVE_MODE, NATIVE_RETRY_HINT, nativeEpicLeft, nativeAlreadyMessage, nativeParentLine, readBoardRepository, configuredMoveRelations, FRAGMENT_PLAN_ID_PATTERN, serializeFragment, allocateFragmentName, directoryPrefix, HORIZON_LABEL_PREFIX, readEpicProblems, epicProblemMessage, readEpicDependents, REASON_FLAG, normaliseReason, renderHorizonComment, renderParkedPullRequestComment, renderMoveComment, renderCloseComment, renderCancelComment, cancelMoveReason, renderDependentComment, renderUnblockNote, readReason, unaskedReasonMessage, blankReasonMessage, readRefsText, readCopyRefs, restampCopyRefs, RefVerifyError, readConfiguredRelations, requireNoLeak, blockedLineSentence, boardRepoLabel, memoiseVerifier, LEARNED_DIRECTORY, ORIGIN_FIELD, DEMOTION_VERDICTS, WHEN_TO_USE_HEADING, bodySections, classifySkill, firstSentence, convertToInstinct, rankCandidates, readRankCandidate, NO_NEXT_STEP, readSchemaReport, gateMeaning, unknownAdditive, unknownAdditiveWarning, developmentApplied, isFirstDispatch, STAGE_LABELS, unlabelReleased, createStartPreflightClaim, invalidateRows, checkDrift, driftLines, readDoctorEffortSync, renderDoctorEffortSync, effortSyncRefusal, SESSION_ID_FLAG2 as SESSION_ID_FLAG, readBoardBody, openBoards, boardOfEpic, createOwnerResolver, runIssueList, ISSUE_LIST_FLAGS, list_default, firstNowEpic, show_default, samePlace, resolvePlace, openPhase, commandProgressFeed, createGhProjectPort, resolveLoopSeams, sessionIdFlag, lineRefusal2 as lineRefusal1, refusal3 as refusal, NoSessionRefusal, isLive, planLabel, sessionLine, projectRoot2 as projectRoot, readRecords, pickSession, writeSession, readSessionChecklist, phaseNote, phasedCounts, checkboxAt, estimateEta, readSessionFinishes, formatDuration, etaLine, RELEASE_REMOTE, RELEASE_BASE_BRANCH, parseSemanticVersion, readManifestVersion, replaceManifestVersion, gitPathOf, receiptProblem, readReceiptVerdict, CHANGELOG_TITLE, releaseCommitSubject, readSettle, buildSettle, waitingSettingsOf, readWaiting, forecastPhrase, DEFAULT_RELEASE_SEAMS, compareVersions, changelogVersions, readTags, status_default, epicEndLines, watchDryEpic, nextOwnerGate, resolveReleaseEnabled, eventsFileOf, readEventsFrom, refuseMisplacedRuntime, readBinPath, self_update_default, versionTag, INDENT4 as INDENT, cleanUpAfterMerge, reportFollowUps, RELEASE_BLOCK_OPEN, RELEASE_BLOCK_CLOSE, localPlanNotes, readGuard, guardLines, mergeGuardSettings, readMergeGuard, guardReaction, followsMerge, readSettleAfterMerge, DEFAULT_BASE_BRANCH, ghNextBoard, openNextSources, readRemoteHead, WORKTREES_SUBDIR, resolveWorktreePath, crossRepositoryRefusal, addResolveWorktree, removeResolveWorktree, pushResolved, worktreeAt, worktreeDirAt, WORKTREE_STATUS, countChanges, cleanupSteps, dryRunLines, runCleanupSteps, defaultCleanupSeams, readCleanup, cleanupCounts, createLinePrompter, openNextRelations, list_default2 as list_default1, readBranch2 as readBranch, readPlans, readOpenPull, readBranchPlan, readNextState, readHomeAfterLoop, commandWords, nextQuestion, NEXT_COMMAND_ACTIONS, LABELS_ONLY_ACTIONS, actionInvocation, runAction, HINT_FLAG_SPEC, actionOutput, endWithNextStep, endingWith, lazyPrompter2 as lazyPrompter, ready_default, DEFAULT_PLAN_FILE, resolvePlanPath, assessPlanRisk, renderRiskText, announceRiskTotal, readBlockedIssues, renderBlockedIssues, blockerToken, addBlockerToBody, resolveCreateSpec, PLACEHOLDER_REASON, renderEpicBody, splitCriteria, criteriaToAsk, readVerifyPrompt, fenceFor, buildVerifyPrompt, parseVerifyPlan, TRIAGE_MODULE, namedSecrets, triageReport, unresolvedTracker, DISK_ROOTS_FILE_SYSTEM, gitToplevel, rootRefusal, rootCandidates, DEFAULT_CI_TIMEOUT_MIN, CI_POLL_INTERVAL_MS, DEFAULT_CI_ATTEMPTS, retargetedLine, MECHANICAL_CONFLICT_SENTENCE, start, createPlanRefsVerifier, readDoctorRefs, renderDoctorRefs, createPlanDirNames, hasPlanFor, HORIZONS, horizonOf, readListedEpics, stageOf, rankOf, readIssueFacts, readRefreshBoard, refreshProjectItems, refreshIssueItems, refreshFailedWarning, unblock_default, unblockAfterMerge, labelsLineBlockersOf, LABELS_READS, createLabelsRelations, EPIC_TYPE_LABEL, plan };