ralphctl 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -22,6 +22,8 @@ import {
22
22
  } from "./chunk-HIU74KTO.mjs";
23
23
 
24
24
  // src/application/cli/entrypoint.ts
25
+ import { realpathSync } from "fs";
26
+ import { pathToFileURL } from "url";
25
27
  import { Command } from "commander";
26
28
 
27
29
  // src/kernel/algorithms/rate-limit-coordinator.ts
@@ -210,8 +212,8 @@ async function directoryHasEntries(p) {
210
212
  try {
211
213
  const s = await stat(p);
212
214
  if (!s.isDirectory()) return false;
213
- const { readdir: readdir6 } = await import("fs/promises");
214
- const entries = await readdir6(p);
215
+ const { readdir: readdir5 } = await import("fs/promises");
216
+ const entries = await readdir5(p);
215
217
  return entries.length > 0;
216
218
  } catch {
217
219
  return false;
@@ -408,10 +410,9 @@ function renderEvaluateWorkspaceSection(workspaceDir) {
408
410
  "",
409
411
  "- `task.md` \u2014 the current task being evaluated (description, steps, verification criteria, status)",
410
412
  "- `requirements/<ticket-id>.md` \u2014 the refined requirements + raw ticket text that motivated this task",
411
- "- `tasks.md` / `tasks.json` \u2014 the full task plan, for cross-task consistency checks",
413
+ "- `tasks.md` \u2014 the full task plan, including any sibling tasks' evaluator output rendered inline where present (cross-task consistency + quality bar so far)",
412
414
  "- `project-context.md` \u2014 the project's CLAUDE.md / .github/copilot-instructions.md (when present in the target repo)",
413
- "- `dimensions.md` \u2014 the four floor dimensions plus any extra dimensions the planner emitted on this task",
414
- "- `evaluations/<task-id>.md` \u2014 prior task evaluations from earlier in this sprint (the quality bar so far)"
415
+ "- `dimensions.md` \u2014 the four floor dimensions plus any extra dimensions the planner emitted on this task"
415
416
  ].join("\n");
416
417
  }
417
418
  function renderDoneCriteriaSection(doneCriteriaBullet) {
@@ -570,8 +571,8 @@ ${input.task.steps.map((s, i) => `${String(i + 1)}. ${s}`).join("\n")}` : "";
570
571
  ${input.task.verificationCriteria.map((c34) => `- ${c34}`).join("\n")}` : "";
571
572
  const branchLine = input.sprint.branch !== null ? `**Branch:** \`${input.sprint.branch}\`` : "";
572
573
  const checkScriptSection = renderCheckScriptSection(input.checkScript);
573
- const checkRanAtForRepo = input.sprint.checkRanAt.get(input.task.projectPath);
574
- const environmentStatus = checkRanAtForRepo !== void 0 ? `Pre-task environment check passed at ${String(checkRanAtForRepo)}.` : "Not run.";
574
+ const setupRanAtForRepo = input.sprint.setupRanAt.get(input.task.projectPath);
575
+ const environmentStatus = setupRanAtForRepo !== void 0 ? `Setup script ran at ${String(setupRanAtForRepo)}.` : "Not run.";
575
576
  const rendered = substitute(tpl.value, {
576
577
  TASK_NAME: input.task.name,
577
578
  TASK_ID: String(input.task.id),
@@ -913,8 +914,7 @@ function getAdapter(name) {
913
914
 
914
915
  // src/integration/persistence/session-md-writer.ts
915
916
  import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
916
- import { dirname as dirname2, join as join5 } from "path";
917
- import { readdir as readdir2 } from "fs/promises";
917
+ import { dirname as dirname2 } from "path";
918
918
  var FRONTMATTER_DELIM = "---";
919
919
  function renderFrontmatter(fields) {
920
920
  const lines = [FRONTMATTER_DELIM];
@@ -994,7 +994,8 @@ async function writeSessionFinish(args) {
994
994
  const fm2 = renderFrontmatter({
995
995
  finished: args.finished,
996
996
  exitCode: args.exitCode,
997
- sessionId: args.sessionId
997
+ sessionId: args.sessionId,
998
+ model: args.model
998
999
  });
999
1000
  return writeFileSafe(args.path, `${fm2}
1000
1001
 
@@ -1008,6 +1009,7 @@ _(no prompt recorded \u2014 session finish without start)_
1008
1009
  merged["finished"] = args.finished;
1009
1010
  merged["exitCode"] = args.exitCode;
1010
1011
  if (args.sessionId !== void 0) merged["sessionId"] = args.sessionId;
1012
+ if (args.model !== void 0) merged["model"] = args.model;
1011
1013
  const fm = renderFrontmatter(merged);
1012
1014
  const content = `${fm}
1013
1015
 
@@ -1054,22 +1056,6 @@ function unquote(s) {
1054
1056
  }
1055
1057
  return s;
1056
1058
  }
1057
- async function nextSessionPath(unitDir) {
1058
- let entries;
1059
- try {
1060
- entries = await readdir2(unitDir);
1061
- } catch {
1062
- return join5(unitDir, "session-1.md");
1063
- }
1064
- let max = 0;
1065
- for (const entry of entries) {
1066
- const m = /^session-(\d+)\.md$/.exec(entry);
1067
- if (!m) continue;
1068
- const n = Number(m[1]);
1069
- if (Number.isFinite(n) && n > max) max = n;
1070
- }
1071
- return join5(unitDir, `session-${String(max + 1)}.md`);
1072
- }
1073
1059
 
1074
1060
  // src/integration/ai/session/session-runner.ts
1075
1061
  import { spawn } from "child_process";
@@ -1365,26 +1351,28 @@ var ProviderAiSessionAdapter = class {
1365
1351
  * The actual provider exit code isn't surfaced through the runner's
1366
1352
  * typed Result — `1` is a faithful "spawn failed" stand-in for audit
1367
1353
  * purposes; the underlying error's message is already in the logs.
1354
+ *
1355
+ * `model` is the first audit hint we have for headless spawns where
1356
+ * the runner picks the model — `writeSessionFinish` merges it into
1357
+ * the frontmatter alongside the other finish fields in a single write.
1368
1358
  */
1369
1359
  async writeSessionMdFinishHeadless(options, result) {
1370
1360
  const path = options.sessionMdPath;
1371
1361
  if (path === void 0) return;
1372
1362
  const sessionId = result.ok ? result.value.sessionId : void 0;
1363
+ const model = result.ok ? result.value.model : void 0;
1373
1364
  const written = await writeSessionFinish({
1374
1365
  path: String(path),
1375
1366
  finished: (/* @__PURE__ */ new Date()).toISOString(),
1376
1367
  exitCode: result.ok ? 0 : 1,
1377
- ...sessionId !== void 0 ? { sessionId } : {}
1368
+ ...sessionId !== void 0 ? { sessionId } : {},
1369
+ ...model !== void 0 ? { model } : {}
1378
1370
  });
1379
1371
  if (!written.ok) {
1380
1372
  this.opts.logger?.warn("failed to write session.md (finish) \u2014 spawn already settled", {
1381
1373
  path: String(path),
1382
1374
  error: written.error.message
1383
1375
  });
1384
- return;
1385
- }
1386
- if (result.ok && result.value.model !== void 0) {
1387
- await this.patchSessionMdModel(String(path), result.value.model);
1388
1376
  }
1389
1377
  }
1390
1378
  /**
@@ -1407,33 +1395,6 @@ var ProviderAiSessionAdapter = class {
1407
1395
  });
1408
1396
  }
1409
1397
  }
1410
- /**
1411
- * Patch the resolved `model` into an existing `session.md`. Reuses
1412
- * `writeSessionFinish` to round-trip frontmatter without touching the
1413
- * prompt body. Best-effort — write failures log a warn.
1414
- *
1415
- * Implementation note: `writeSessionFinish` was scoped to the
1416
- * standard finish fields only. The narrow `model` patch here goes
1417
- * through `writeSessionStart` would clobber the body, so we re-read,
1418
- * splice the field, and re-emit via the same writer surface. This
1419
- * keeps the YAML-handling logic in one place (`session-md-writer`)
1420
- * even at the cost of a second IO round-trip — the audit pack is
1421
- * cold-path and we'd rather centralise format knowledge.
1422
- */
1423
- async patchSessionMdModel(path, model) {
1424
- try {
1425
- const fs = await import("fs/promises");
1426
- const existing = await fs.readFile(path, "utf-8");
1427
- const patched = upsertFrontmatterField(existing, "model", model);
1428
- if (patched === existing) return;
1429
- await fs.writeFile(path, patched, { encoding: "utf-8", mode: 384 });
1430
- } catch (err) {
1431
- this.opts.logger?.warn("failed to patch session.md model field", {
1432
- path,
1433
- error: err instanceof Error ? err.message : String(err)
1434
- });
1435
- }
1436
- }
1437
1398
  /**
1438
1399
  * Build the flag list audited into session.md. Headless: the runner
1439
1400
  * appends `--resume <id>` when `resumeSessionId` is set, so we mirror
@@ -1480,28 +1441,6 @@ function stripTrailingPromptSlot(args) {
1480
1441
  if (end > 0 && args[end - 1] === "--") end -= 1;
1481
1442
  return args.slice(0, end);
1482
1443
  }
1483
- function upsertFrontmatterField(content, key, value) {
1484
- const lines = content.split("\n");
1485
- if (lines[0]?.trim() !== "---") return content;
1486
- let closeIdx = -1;
1487
- for (let i = 1; i < lines.length; i += 1) {
1488
- if (lines[i]?.trim() === "---") {
1489
- closeIdx = i;
1490
- break;
1491
- }
1492
- }
1493
- if (closeIdx < 0) return content;
1494
- const keyRegex = new RegExp(`^${key}\\s*:`);
1495
- for (let i = 1; i < closeIdx; i += 1) {
1496
- const line = lines[i] ?? "";
1497
- if (keyRegex.test(line.trim())) {
1498
- lines[i] = `${key}: ${value}`;
1499
- return lines.join("\n");
1500
- }
1501
- }
1502
- lines.splice(closeIdx, 0, `${key}: ${value}`);
1503
- return lines.join("\n");
1504
- }
1505
1444
 
1506
1445
  // src/integration/ai/session/process-runner.ts
1507
1446
  import { spawn as spawn2 } from "child_process";
@@ -1603,20 +1542,20 @@ var NodeProcessRunner = class {
1603
1542
 
1604
1543
  // src/integration/ai/skills/bundled-skills-copier.ts
1605
1544
  import { existsSync as existsSync3 } from "fs";
1606
- import { readdir as readdir4, rm, rmdir } from "fs/promises";
1607
- import { dirname as dirname3, join as join8 } from "path";
1545
+ import { readdir as readdir3, rm, rmdir } from "fs/promises";
1546
+ import { dirname as dirname3, join as join7 } from "path";
1608
1547
  import { fileURLToPath as fileURLToPath2 } from "url";
1609
1548
 
1610
1549
  // src/integration/ai/skills/copy-tree.ts
1611
- import { copyFile, mkdir as mkdir2, readdir as readdir3, stat as stat2 } from "fs/promises";
1612
- import { join as join6 } from "path";
1550
+ import { copyFile, mkdir as mkdir2, readdir as readdir2, stat as stat2 } from "fs/promises";
1551
+ import { join as join5 } from "path";
1613
1552
  async function copyTree(src, dst) {
1614
1553
  try {
1615
1554
  await mkdir2(dst, { recursive: true });
1616
- const dirents = await readdir3(src, { withFileTypes: true });
1555
+ const dirents = await readdir2(src, { withFileTypes: true });
1617
1556
  for (const d of dirents) {
1618
- const s = join6(src, d.name);
1619
- const t = join6(dst, d.name);
1557
+ const s = join5(src, d.name);
1558
+ const t = join5(dst, d.name);
1620
1559
  if (d.isDirectory()) {
1621
1560
  const r = await copyTree(s, t);
1622
1561
  if (!r.ok) return r;
@@ -1652,12 +1591,12 @@ async function copyTree(src, dst) {
1652
1591
  // src/integration/ai/skills/skill-git-exclude.ts
1653
1592
  import { existsSync as existsSync2 } from "fs";
1654
1593
  import { mkdir as mkdir3, readFile as readFile3, stat as stat3, writeFile as writeFile2 } from "fs/promises";
1655
- import { join as join7 } from "path";
1594
+ import { join as join6 } from "path";
1656
1595
  var BEGIN_MARKER = "# >>> ralphctl-managed-skills (do not edit) >>>";
1657
1596
  var END_MARKER = "# <<< ralphctl-managed-skills <<<";
1658
1597
  var EXCLUDE_PATTERNS = [".claude/skills/"];
1659
1598
  async function gitInfoDir(cwd) {
1660
- const dotGit = join7(cwd, ".git");
1599
+ const dotGit = join6(cwd, ".git");
1661
1600
  if (!existsSync2(dotGit)) return null;
1662
1601
  try {
1663
1602
  const s = await stat3(dotGit);
@@ -1665,12 +1604,12 @@ async function gitInfoDir(cwd) {
1665
1604
  } catch {
1666
1605
  return null;
1667
1606
  }
1668
- return join7(dotGit, "info");
1607
+ return join6(dotGit, "info");
1669
1608
  }
1670
1609
  async function addRalphctlSkillsExclude(cwd) {
1671
1610
  const infoDir = await gitInfoDir(cwd);
1672
1611
  if (infoDir === null) return Result.ok();
1673
- const excludeFile = join7(infoDir, "exclude");
1612
+ const excludeFile = join6(infoDir, "exclude");
1674
1613
  try {
1675
1614
  await mkdir3(infoDir, { recursive: true });
1676
1615
  const existing = existsSync2(excludeFile) ? await readFile3(excludeFile, "utf8") : "";
@@ -1697,7 +1636,7 @@ ${block}`;
1697
1636
  async function removeRalphctlSkillsExclude(cwd) {
1698
1637
  const infoDir = await gitInfoDir(cwd);
1699
1638
  if (infoDir === null) return Result.ok();
1700
- const excludeFile = join7(infoDir, "exclude");
1639
+ const excludeFile = join6(infoDir, "exclude");
1701
1640
  if (!existsSync2(excludeFile)) return Result.ok();
1702
1641
  try {
1703
1642
  const body = await readFile3(excludeFile, "utf8");
@@ -1735,10 +1674,10 @@ function stripMarkerBlock(body) {
1735
1674
  }
1736
1675
 
1737
1676
  // src/integration/ai/skills/bundled-skills-copier.ts
1738
- var SKILLS_SUBDIR = join8(".claude", "skills");
1677
+ var SKILLS_SUBDIR = join7(".claude", "skills");
1739
1678
  var HERE2 = dirname3(fileURLToPath2(import.meta.url));
1740
1679
  function bundledSkillsRootDir() {
1741
- const distRoot = join8(HERE2, "skills");
1680
+ const distRoot = join7(HERE2, "skills");
1742
1681
  if (existsSync3(distRoot)) return AbsolutePath.trustString(distRoot);
1743
1682
  return AbsolutePath.trustString(HERE2);
1744
1683
  }
@@ -1754,12 +1693,12 @@ var FileBundledSkillsCopier = class {
1754
1693
  this.bundledRootDir = opts.bundledRootDir ?? bundledSkillsRootDir();
1755
1694
  }
1756
1695
  async install(sessionDir, phase) {
1757
- const skillsDir = join8(sessionDir, SKILLS_SUBDIR);
1696
+ const skillsDir = join7(sessionDir, SKILLS_SUBDIR);
1758
1697
  const sources = await this.collectSourceSkills(phase);
1759
1698
  if (!sources.ok) return Result.error(sources.error);
1760
1699
  const tracked = this.installed.get(String(sessionDir)) ?? /* @__PURE__ */ new Set();
1761
1700
  for (const [name, srcDir] of sources.value) {
1762
- const dst = join8(skillsDir, name);
1701
+ const dst = join7(skillsDir, name);
1763
1702
  if (existsSync3(dst)) {
1764
1703
  continue;
1765
1704
  }
@@ -1786,10 +1725,10 @@ var FileBundledSkillsCopier = class {
1786
1725
  await removeRalphctlSkillsExclude(sessionDir);
1787
1726
  return Result.ok();
1788
1727
  }
1789
- const skillsDir = join8(sessionDir, SKILLS_SUBDIR);
1728
+ const skillsDir = join7(sessionDir, SKILLS_SUBDIR);
1790
1729
  try {
1791
1730
  for (const name of tracked) {
1792
- await rm(join8(skillsDir, name), { recursive: true, force: true });
1731
+ await rm(join7(skillsDir, name), { recursive: true, force: true });
1793
1732
  }
1794
1733
  this.installed.delete(key);
1795
1734
  } catch (err) {
@@ -1803,7 +1742,7 @@ var FileBundledSkillsCopier = class {
1803
1742
  );
1804
1743
  }
1805
1744
  await tryRmdirIfEmpty(skillsDir);
1806
- await tryRmdirIfEmpty(join8(sessionDir, ".claude"));
1745
+ await tryRmdirIfEmpty(join7(sessionDir, ".claude"));
1807
1746
  await removeRalphctlSkillsExclude(sessionDir);
1808
1747
  return Result.ok();
1809
1748
  }
@@ -1817,7 +1756,7 @@ var FileBundledSkillsCopier = class {
1817
1756
  async collectSourceSkills(phase) {
1818
1757
  const sources = /* @__PURE__ */ new Map();
1819
1758
  for (const sub of ["default", phase]) {
1820
- const dir = join8(this.bundledRootDir, sub);
1759
+ const dir = join7(this.bundledRootDir, sub);
1821
1760
  const r = await listSkillDirs(dir);
1822
1761
  if (!r.ok) return Result.error(r.error);
1823
1762
  for (const [name, abs] of r.value) {
@@ -1830,10 +1769,10 @@ var FileBundledSkillsCopier = class {
1830
1769
  async function listSkillDirs(dir) {
1831
1770
  if (!existsSync3(dir)) return Result.ok([]);
1832
1771
  try {
1833
- const dirents = await readdir4(dir, { withFileTypes: true });
1772
+ const dirents = await readdir3(dir, { withFileTypes: true });
1834
1773
  const out = [];
1835
1774
  for (const d of dirents) {
1836
- if (d.isDirectory()) out.push([d.name, join8(dir, d.name)]);
1775
+ if (d.isDirectory()) out.push([d.name, join7(dir, d.name)]);
1837
1776
  }
1838
1777
  return Result.ok(out);
1839
1778
  } catch (err) {
@@ -1850,7 +1789,7 @@ async function listSkillDirs(dir) {
1850
1789
  async function tryRmdirIfEmpty(dir) {
1851
1790
  if (!existsSync3(dir)) return;
1852
1791
  try {
1853
- const entries = await readdir4(dir);
1792
+ const entries = await readdir3(dir);
1854
1793
  if (entries.length === 0) await rmdir(dir);
1855
1794
  } catch {
1856
1795
  }
@@ -2154,7 +2093,12 @@ var DefaultExternalAdapter = class {
2154
2093
  formatIssueContext(issue) {
2155
2094
  return this.issues.format(issue);
2156
2095
  }
2157
- // --- Check script execution -----------------------------------------
2096
+ // --- Setup / check script execution ---------------------------------
2097
+ async runSetupScript(projectPath, script, timeout) {
2098
+ const r = await this.checkScripts.run(projectPath, script, "setup", timeout);
2099
+ if (r.ok) return r.value;
2100
+ return { passed: false, output: `[setup-script error: ${r.error.message}]` };
2101
+ }
2158
2102
  async runCheckScript(projectPath, script, phase, timeout) {
2159
2103
  const r = await this.checkScripts.run(projectPath, script, phase, timeout);
2160
2104
  if (r.ok) return r.value;
@@ -2930,9 +2874,9 @@ var JsonLogger = class _JsonLogger {
2930
2874
 
2931
2875
  // src/integration/logging/jsonl-file-writer.ts
2932
2876
  import { appendFile, mkdir as mkdir4 } from "fs/promises";
2933
- import { dirname as dirname4, join as join9 } from "path";
2877
+ import { dirname as dirname4, join as join8 } from "path";
2934
2878
  function fileFor(opts) {
2935
- return AbsolutePath.trustString(join9(opts.logsDir, `${opts.sessionId}.jsonl`));
2879
+ return AbsolutePath.trustString(join8(opts.logsDir, `${opts.sessionId}.jsonl`));
2936
2880
  }
2937
2881
  var JsonlFileWriter = class {
2938
2882
  constructor(opts) {
@@ -3302,7 +3246,7 @@ var NotFoundError = class extends Error {
3302
3246
 
3303
3247
  // src/integration/persistence/json-io.ts
3304
3248
  import { mkdir as mkdir6, readFile as readFile5, rename, writeFile as writeFile4 } from "fs/promises";
3305
- import { dirname as dirname6, join as join10 } from "path";
3249
+ import { dirname as dirname6, join as join9 } from "path";
3306
3250
  async function readJsonFile(path, schema2) {
3307
3251
  let raw;
3308
3252
  try {
@@ -3360,7 +3304,7 @@ ${issues}`,
3360
3304
  );
3361
3305
  }
3362
3306
  const dir = dirname6(path);
3363
- const tmp = join10(dir, `.${pathBasename(path)}.${String(process.pid)}.${String(Date.now())}.${randomTail()}.tmp`);
3307
+ const tmp = join9(dir, `.${pathBasename(path)}.${String(process.pid)}.${String(Date.now())}.${randomTail()}.tmp`);
3364
3308
  try {
3365
3309
  await mkdir6(dir, { recursive: true });
3366
3310
  await writeFile4(tmp, JSON.stringify(validated.data, null, 2) + "\n", {
@@ -4036,7 +3980,7 @@ function errnoCode3(err) {
4036
3980
  }
4037
3981
 
4038
3982
  // src/integration/persistence/file-sprint-repository.ts
4039
- import { mkdir as mkdir7, readdir as readdir5, rm as rm2 } from "fs/promises";
3983
+ import { mkdir as mkdir7, readdir as readdir4, rm as rm2 } from "fs/promises";
4040
3984
 
4041
3985
  // src/integration/persistence/schemas/sprint-schema.ts
4042
3986
  import { z as z3 } from "zod";
@@ -4137,7 +4081,12 @@ var Sprint = class _Sprint {
4137
4081
  activatedAt;
4138
4082
  closedAt;
4139
4083
  tickets;
4140
- checkRanAt;
4084
+ /**
4085
+ * Audit trail of sprint-start setup-script runs, keyed by repo path.
4086
+ * Filled in by the `setup-scripts-sprint-start` chain leaf. Cleared on
4087
+ * sprint close so a re-opened sprint starts a fresh setup record.
4088
+ */
4089
+ setupRanAt;
4141
4090
  branch;
4142
4091
  /**
4143
4092
  * Pull / merge request URL recorded after `sprint create-pr` runs.
@@ -4165,7 +4114,7 @@ var Sprint = class _Sprint {
4165
4114
  this.activatedAt = props.activatedAt;
4166
4115
  this.closedAt = props.closedAt;
4167
4116
  this.tickets = props.tickets;
4168
- this.checkRanAt = props.checkRanAt;
4117
+ this.setupRanAt = props.setupRanAt;
4169
4118
  this.branch = props.branch;
4170
4119
  this.pullRequestUrl = props.pullRequestUrl;
4171
4120
  this.projectName = props.projectName;
@@ -4192,7 +4141,7 @@ var Sprint = class _Sprint {
4192
4141
  activatedAt: null,
4193
4142
  closedAt: null,
4194
4143
  tickets: [],
4195
- checkRanAt: /* @__PURE__ */ new Map(),
4144
+ setupRanAt: /* @__PURE__ */ new Map(),
4196
4145
  branch: null,
4197
4146
  pullRequestUrl: null,
4198
4147
  projectName: input.projectName,
@@ -4229,7 +4178,7 @@ var Sprint = class _Sprint {
4229
4178
  this.with({
4230
4179
  status: "closed",
4231
4180
  closedAt: now,
4232
- checkRanAt: /* @__PURE__ */ new Map()
4181
+ setupRanAt: /* @__PURE__ */ new Map()
4233
4182
  })
4234
4183
  );
4235
4184
  }
@@ -4268,7 +4217,7 @@ var Sprint = class _Sprint {
4268
4217
  activatedAt: this.activatedAt,
4269
4218
  closedAt: this.closedAt,
4270
4219
  tickets: this.tickets,
4271
- checkRanAt: this.checkRanAt,
4220
+ setupRanAt: this.setupRanAt,
4272
4221
  branch: this.branch,
4273
4222
  pullRequestUrl: this.pullRequestUrl,
4274
4223
  projectName: this.projectName,
@@ -4364,13 +4313,13 @@ var Sprint = class _Sprint {
4364
4313
  return Result8.ok(this.with({ branch }));
4365
4314
  }
4366
4315
  /**
4367
- * Stamp a check-script run for one repo. Never fails — the harness owns
4316
+ * Stamp a setup-script run for one repo. Never fails — the harness owns
4368
4317
  * this audit trail and the entity should not gate it.
4369
4318
  */
4370
- recordCheckRun(repo, at) {
4371
- const next = new Map(this.checkRanAt);
4319
+ recordSetupRun(repo, at) {
4320
+ const next = new Map(this.setupRanAt);
4372
4321
  next.set(repo, at);
4373
- return this.with({ checkRanAt: next });
4322
+ return this.with({ setupRanAt: next });
4374
4323
  }
4375
4324
  /**
4376
4325
  * Record the pull / merge request URL published for the sprint branch.
@@ -4456,7 +4405,7 @@ var Sprint = class _Sprint {
4456
4405
  activatedAt: "activatedAt" in partial ? partial.activatedAt ?? null : this.activatedAt,
4457
4406
  closedAt: "closedAt" in partial ? partial.closedAt ?? null : this.closedAt,
4458
4407
  tickets: partial.tickets ?? this.tickets,
4459
- checkRanAt: partial.checkRanAt ?? this.checkRanAt,
4408
+ setupRanAt: partial.setupRanAt ?? this.setupRanAt,
4460
4409
  branch: "branch" in partial ? partial.branch ?? null : this.branch,
4461
4410
  pullRequestUrl: "pullRequestUrl" in partial ? partial.pullRequestUrl ?? null : this.pullRequestUrl,
4462
4411
  projectName: this.projectName,
@@ -4640,7 +4589,13 @@ var sprintJsonSchema = z3.object({
4640
4589
  // to `[]` on a fresh draft sprint, populated post-plan).
4641
4590
  affectedRepositories: z3.array(z3.string()),
4642
4591
  // `Map<AbsolutePath, IsoTimestamp>` — serialised as a plain object map.
4643
- checkRanAt: z3.record(z3.string(), z3.string()),
4592
+ // Backwards-compat: v0.6.2 wrote this audit map under the legacy key
4593
+ // `checkRanAt` and may not have emitted any key at all on older drafts.
4594
+ // `.optional().default({})` accepts both shapes; Zod silently strips the
4595
+ // legacy `checkRanAt` payload on the next save (a stale audit trail under
4596
+ // the wrong key has no migration value — the worst case is one extra
4597
+ // setup run on first resume).
4598
+ setupRanAt: z3.record(z3.string(), z3.string()).optional().default({}),
4644
4599
  tickets: z3.array(ticketJsonSchema)
4645
4600
  });
4646
4601
  var SYNTHETIC_SLUG_R = Slug.parse("rehydrate");
@@ -4653,9 +4608,9 @@ function toSprint(parsed) {
4653
4608
  if (!r.ok) return Result.error(r.error);
4654
4609
  tickets.push(r.value);
4655
4610
  }
4656
- const checkRanAt = /* @__PURE__ */ new Map();
4657
- for (const [k, v] of Object.entries(parsed.checkRanAt)) {
4658
- checkRanAt.set(AbsolutePath.trustString(k), IsoTimestamp.trustString(v));
4611
+ const setupRanAt = /* @__PURE__ */ new Map();
4612
+ for (const [k, v] of Object.entries(parsed.setupRanAt)) {
4613
+ setupRanAt.set(AbsolutePath.trustString(k), IsoTimestamp.trustString(v));
4659
4614
  }
4660
4615
  const created = Sprint.create({
4661
4616
  id: SprintId.trustString(parsed.id),
@@ -4690,8 +4645,8 @@ function toSprint(parsed) {
4690
4645
  const r = s.close(IsoTimestamp.trustString(at));
4691
4646
  if (r.ok) s = r.value;
4692
4647
  }
4693
- for (const [path, at] of checkRanAt) {
4694
- s = s.recordCheckRun(path, at);
4648
+ for (const [path, at] of setupRanAt) {
4649
+ s = s.recordSetupRun(path, at);
4695
4650
  }
4696
4651
  if (parsed.branch !== null) {
4697
4652
  const r = s.setBranch(parsed.branch);
@@ -4704,9 +4659,9 @@ function toSprint(parsed) {
4704
4659
  return Result.ok(s);
4705
4660
  }
4706
4661
  function fromSprint(sprint) {
4707
- const checkRanAt = {};
4708
- for (const [k, v] of sprint.checkRanAt) {
4709
- checkRanAt[k] = v;
4662
+ const setupRanAt = {};
4663
+ for (const [k, v] of sprint.setupRanAt) {
4664
+ setupRanAt[k] = v;
4710
4665
  }
4711
4666
  return {
4712
4667
  id: sprint.id,
@@ -4719,7 +4674,7 @@ function fromSprint(sprint) {
4719
4674
  pullRequestUrl: sprint.pullRequestUrl,
4720
4675
  projectName: sprint.projectName,
4721
4676
  affectedRepositories: [...sprint.affectedRepositories],
4722
- checkRanAt,
4677
+ setupRanAt,
4723
4678
  tickets: sprint.tickets.map(fromTicket)
4724
4679
  };
4725
4680
  }
@@ -4807,7 +4762,7 @@ var FileSprintRepository = class {
4807
4762
  async list() {
4808
4763
  let entries;
4809
4764
  try {
4810
- const dirents = await readdir5(this.paths.sprintsDir, {
4765
+ const dirents = await readdir4(this.paths.sprintsDir, {
4811
4766
  withFileTypes: true
4812
4767
  });
4813
4768
  entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
@@ -5496,8 +5451,8 @@ var FileWriteContextFileAdapter = class {
5496
5451
  };
5497
5452
 
5498
5453
  // src/integration/persistence/execution-unit-builder.ts
5499
- import { readFile as readFile6, rm as rm4 } from "fs/promises";
5500
- import { join as join12 } from "path";
5454
+ import { readFile as readFile6 } from "fs/promises";
5455
+ import { join as join11 } from "path";
5501
5456
 
5502
5457
  // src/integration/persistence/unit-slug.ts
5503
5458
  function unitSlug(id, name) {
@@ -5514,10 +5469,10 @@ function toSlug(name) {
5514
5469
 
5515
5470
  // src/integration/persistence/session-folder-helpers.ts
5516
5471
  import { cp, mkdir as mkdir10, rm as rm3, writeFile as writeFile6 } from "fs/promises";
5517
- import { basename as basename2, dirname as dirname8, join as join11 } from "path";
5472
+ import { basename as basename2, dirname as dirname8, join as join10 } from "path";
5518
5473
  function contextFileFor(provider) {
5519
5474
  if (provider === "claude") return { path: "CLAUDE.md", needsGithubDir: false };
5520
- return { path: join11(".github", "copilot-instructions.md"), needsGithubDir: true };
5475
+ return { path: join10(".github", "copilot-instructions.md"), needsGithubDir: true };
5521
5476
  }
5522
5477
  function renderContextFile(args) {
5523
5478
  const { sprint, phase, affectedRepos, copilot } = args;
@@ -5539,7 +5494,7 @@ function renderInputsLine(phase) {
5539
5494
  return "- Input: `./ticket.md` \u2014 the seed idea. Write the proposed sprint output to `./output.json`.\n";
5540
5495
  if (phase === "plan")
5541
5496
  return "- Inputs: per-ticket refined requirements live alongside this folder under `../refinement/<unit>/requirements.json`. Write your generated `tasks.json` to `./tasks.json` in this folder.\n";
5542
- return "- Inputs are in `./task.md` (the task under review), `./tasks.md` / `./tasks.json` (full task plan), `./requirements/` (per-ticket requirements), `./project-context.md` (target repo context), `./dimensions.md` (grading rubric), and `./evaluations/` (prior evaluations from this sprint).\n";
5497
+ return "- Inputs are in `./task.md` (the task under review), `./tasks.md` (full task plan including any sibling evaluator output), `./requirements/` (per-ticket requirements), `./project-context.md` (target repo context), and `./dimensions.md` (grading rubric).\n";
5543
5498
  }
5544
5499
  function renderRepoLine(phase, affectedRepos, copilot) {
5545
5500
  if (phase === "refine" || phase === "ideate")
@@ -5649,7 +5604,7 @@ async function mirrorRepo(src, dst) {
5649
5604
  async function writeContextFile(args) {
5650
5605
  const { path, needsGithubDir } = contextFileFor(args.provider);
5651
5606
  if (needsGithubDir) {
5652
- const ensure = await ensureDirSafe(join11(args.root, ".github"));
5607
+ const ensure = await ensureDirSafe(join10(args.root, ".github"));
5653
5608
  if (!ensure.ok) return Result.error(ensure.error);
5654
5609
  }
5655
5610
  const body = renderContextFile({
@@ -5658,7 +5613,7 @@ async function writeContextFile(args) {
5658
5613
  affectedRepos: args.affectedRepos,
5659
5614
  copilot: args.provider === "copilot"
5660
5615
  });
5661
- return writeFileSafe2(join11(args.root, path), body);
5616
+ return writeFileSafe2(join10(args.root, path), body);
5662
5617
  }
5663
5618
 
5664
5619
  // src/integration/persistence/execution-unit-builder.ts
@@ -5721,7 +5676,7 @@ function renderTaskInput(task) {
5721
5676
  }
5722
5677
  return lines.join("\n");
5723
5678
  }
5724
- function renderTasksList(tasks) {
5679
+ function renderTasksList(tasks, priorEvaluations) {
5725
5680
  const ordered = [...tasks].sort((a, b) => a.order - b.order);
5726
5681
  const lines = ["# Task plan", ""];
5727
5682
  for (const t of ordered) {
@@ -5745,6 +5700,10 @@ function renderTasksList(tasks) {
5745
5700
  for (const c34 of t.verificationCriteria) lines.push(`- ${c34}`);
5746
5701
  lines.push("");
5747
5702
  }
5703
+ const priorEval = priorEvaluations.get(t.id);
5704
+ if (priorEval !== void 0 && priorEval.length > 0) {
5705
+ lines.push("### Evaluator output", "", "```text", priorEval, "```", "");
5706
+ }
5748
5707
  }
5749
5708
  return lines.join("\n");
5750
5709
  }
@@ -5775,7 +5734,7 @@ function renderDimensions(task) {
5775
5734
  return lines.join("\n");
5776
5735
  }
5777
5736
  async function readProjectContext(repoPath, provider) {
5778
- const target = provider === "claude" ? join12(repoPath, "CLAUDE.md") : join12(repoPath, ".github", "copilot-instructions.md");
5737
+ const target = provider === "claude" ? join11(repoPath, "CLAUDE.md") : join11(repoPath, ".github", "copilot-instructions.md");
5779
5738
  try {
5780
5739
  const body = await readFile6(target, "utf-8");
5781
5740
  return [`<!-- copied from ${target} -->`, "", body].join("\n");
@@ -5790,59 +5749,20 @@ async function readProjectContext(repoPath, provider) {
5790
5749
  ].join("\n");
5791
5750
  }
5792
5751
  }
5793
- function serialiseTasks(tasks) {
5794
- return [...tasks].sort((a, b) => a.order - b.order).map((t) => ({
5795
- id: t.id,
5796
- name: t.name,
5797
- description: t.description,
5798
- steps: t.steps,
5799
- verificationCriteria: t.verificationCriteria,
5800
- status: t.status,
5801
- order: t.order,
5802
- ticketId: t.ticketId,
5803
- blockedBy: t.blockedBy,
5804
- projectPath: t.projectPath,
5805
- extraDimensions: t.extraDimensions
5806
- }));
5807
- }
5808
5752
  async function writeExecutionVolatile(args) {
5809
- const taskMd = await writeFileSafe2(join12(args.root, "task.md"), renderTaskInput(args.task));
5753
+ const taskMd = await writeFileSafe2(join11(args.root, "task.md"), renderTaskInput(args.task));
5810
5754
  if (!taskMd.ok) return Result.error(taskMd.error);
5811
- const tasksMd = await writeFileSafe2(join12(args.root, "tasks.md"), renderTasksList(args.tasks));
5755
+ const tasksMd = await writeFileSafe2(join11(args.root, "tasks.md"), renderTasksList(args.tasks, args.priorEvaluations));
5812
5756
  if (!tasksMd.ok) return Result.error(tasksMd.error);
5813
- const tasksJson = await writeFileSafe2(
5814
- join12(args.root, "tasks.json"),
5815
- JSON.stringify(serialiseTasks(args.tasks), null, 2)
5816
- );
5817
- if (!tasksJson.ok) return Result.error(tasksJson.error);
5818
5757
  const projectContext = await readProjectContext(args.task.projectPath, args.aiProvider);
5819
- const projectCtxFile = await writeFileSafe2(join12(args.root, "project-context.md"), projectContext);
5758
+ const projectCtxFile = await writeFileSafe2(join11(args.root, "project-context.md"), projectContext);
5820
5759
  if (!projectCtxFile.ok) return Result.error(projectCtxFile.error);
5821
- const evaluationsDir = join12(args.root, "evaluations");
5822
- try {
5823
- await rm4(evaluationsDir, { recursive: true, force: true });
5824
- } catch (err) {
5825
- return Result.error(
5826
- new StorageError({
5827
- subCode: "io",
5828
- message: `failed to clear ${evaluationsDir}: ${err instanceof Error ? err.message : String(err)}`,
5829
- path: evaluationsDir,
5830
- cause: err
5831
- })
5832
- );
5833
- }
5834
- const ensureEvals = await ensureDirSafe(evaluationsDir);
5835
- if (!ensureEvals.ok) return Result.error(ensureEvals.error);
5836
- for (const [taskId, body] of args.priorEvaluations) {
5837
- const w = await writeFileSafe2(join12(evaluationsDir, `${taskId}.md`), body);
5838
- if (!w.ok) return Result.error(w.error);
5839
- }
5840
5760
  return Result.ok();
5841
5761
  }
5842
5762
  async function buildExecutionUnit(storage2, input) {
5843
5763
  const slug = unitSlug(String(input.task.id), input.task.name);
5844
5764
  const root = storage2.executionUnitDir(input.sprint.id, slug);
5845
- const requirementsDir = AbsolutePath.trustString(join12(root, "requirements"));
5765
+ const requirementsDir = AbsolutePath.trustString(join11(root, "requirements"));
5846
5766
  const ensure = await ensureDirSafe(requirementsDir);
5847
5767
  if (!ensure.ok) return Result.error(ensure.error);
5848
5768
  const ctx = await writeContextFile({
@@ -5854,14 +5774,14 @@ async function buildExecutionUnit(storage2, input) {
5854
5774
  });
5855
5775
  if (!ctx.ok) return Result.error(ctx.error);
5856
5776
  for (const ticket of input.sprint.tickets) {
5857
- const filePath = join12(requirementsDir, `${ticket.id}.md`);
5777
+ const filePath = join11(requirementsDir, `${ticket.id}.md`);
5858
5778
  const w = await writeFileSafe2(filePath, renderRequirementsInput(ticket));
5859
5779
  if (!w.ok) return Result.error(w.error);
5860
5780
  }
5861
- const dims = await writeFileSafe2(join12(root, "dimensions.md"), renderDimensions(input.task));
5781
+ const dims = await writeFileSafe2(join11(root, "dimensions.md"), renderDimensions(input.task));
5862
5782
  if (!dims.ok) return Result.error(dims.error);
5863
5783
  const sprintCriteriaPath = String(storage2.doneCriteriaFile(input.sprint.id));
5864
- const unitCriteriaPath = join12(root, "done-criteria.md");
5784
+ const unitCriteriaPath = join11(root, "done-criteria.md");
5865
5785
  const copied = await copyFileSafe(sprintCriteriaPath, unitCriteriaPath);
5866
5786
  if (!copied.ok) {
5867
5787
  const msg = `build-execution-unit: done-criteria.md not found for sprint ${String(input.sprint.id)} \u2014 evaluator will grade without per-task criteria reference`;
@@ -5881,7 +5801,7 @@ async function buildExecutionUnit(storage2, input) {
5881
5801
  let addDirs;
5882
5802
  let sessionCwd;
5883
5803
  if (input.aiProvider === "copilot") {
5884
- const repoMirror = join12(root, "repo");
5804
+ const repoMirror = join11(root, "repo");
5885
5805
  const m = await mirrorRepo(input.task.projectPath, repoMirror);
5886
5806
  if (!m.ok) return Result.error(m.error);
5887
5807
  addDirs = [];
@@ -5893,8 +5813,7 @@ async function buildExecutionUnit(storage2, input) {
5893
5813
  return Result.ok({
5894
5814
  root,
5895
5815
  addDirs,
5896
- sessionCwd,
5897
- evaluationMdPath: AbsolutePath.trustString(join12(root, "evaluation.md"))
5816
+ sessionCwd
5898
5817
  });
5899
5818
  }
5900
5819
  async function refreshExecutionUnit(storage2, input) {
@@ -5911,7 +5830,7 @@ async function refreshExecutionUnit(storage2, input) {
5911
5830
  }
5912
5831
 
5913
5832
  // src/integration/persistence/ideate-unit-builder.ts
5914
- import { join as join13 } from "path";
5833
+ import { join as join12 } from "path";
5915
5834
  async function buildIdeationUnit(storage2, input) {
5916
5835
  const slug = unitSlug(String(input.ticket.id), input.ticket.title);
5917
5836
  const root = storage2.ideationUnitDir(input.sprint.id, slug);
@@ -5925,19 +5844,19 @@ async function buildIdeationUnit(storage2, input) {
5925
5844
  affectedRepos: []
5926
5845
  });
5927
5846
  if (!ctx.ok) return Result.error(ctx.error);
5928
- const ticketMdPath = AbsolutePath.trustString(join13(root, "ticket.md"));
5847
+ const ticketMdPath = AbsolutePath.trustString(join12(root, "ticket.md"));
5929
5848
  const wrote = await writeFileSafe2(ticketMdPath, renderTicketInput(input.ticket));
5930
5849
  if (!wrote.ok) return Result.error(wrote.error);
5931
5850
  return Result.ok({
5932
5851
  root,
5933
- sessionMdPath: AbsolutePath.trustString(join13(root, "session.md")),
5852
+ sessionMdPath: AbsolutePath.trustString(join12(root, "session.md")),
5934
5853
  ticketMdPath,
5935
- outputJsonPath: AbsolutePath.trustString(join13(root, "output.json"))
5854
+ outputJsonPath: AbsolutePath.trustString(join12(root, "output.json"))
5936
5855
  });
5937
5856
  }
5938
5857
 
5939
5858
  // src/integration/persistence/planning-folder-builder.ts
5940
- import { basename as basename3, join as join14 } from "path";
5859
+ import { basename as basename3, join as join13 } from "path";
5941
5860
 
5942
5861
  // src/business/usecases/sprint/sprint-requirements-aggregate.ts
5943
5862
  function buildSprintRequirementsAggregate(sprint, now = /* @__PURE__ */ new Date()) {
@@ -6013,7 +5932,7 @@ async function buildPlanningFolder(storage2, input) {
6013
5932
  });
6014
5933
  if (!ctx.ok) return Result.error(ctx.error);
6015
5934
  const reqSrc = String(storage2.requirementsAggregateFile(input.sprint.id));
6016
- const reqDst = join14(root, "requirements.json");
5935
+ const reqDst = join13(root, "requirements.json");
6017
5936
  const copied = await copyFileSafe(reqSrc, reqDst);
6018
5937
  if (!copied.ok) {
6019
5938
  const inlineBody = serialiseSprintRequirementsAggregate(buildSprintRequirementsAggregate(input.sprint));
@@ -6022,11 +5941,11 @@ async function buildPlanningFolder(storage2, input) {
6022
5941
  }
6023
5942
  let addDirs;
6024
5943
  if (isCopilot) {
6025
- const reposDir = join14(root, "repos");
5944
+ const reposDir = join13(root, "repos");
6026
5945
  const ensureRepos = await ensureDirSafe(reposDir);
6027
5946
  if (!ensureRepos.ok) return Result.error(ensureRepos.error);
6028
5947
  for (const repoPath of input.sprint.affectedRepositories) {
6029
- const dst = join14(reposDir, basename3(repoPath));
5948
+ const dst = join13(reposDir, basename3(repoPath));
6030
5949
  const m = await mirrorRepo(repoPath, dst);
6031
5950
  if (!m.ok) return Result.error(m.error);
6032
5951
  }
@@ -6036,14 +5955,14 @@ async function buildPlanningFolder(storage2, input) {
6036
5955
  }
6037
5956
  return Result.ok({
6038
5957
  root,
6039
- sessionMdPath: AbsolutePath.trustString(join14(root, "session.md")),
6040
- rawTasksJsonPath: AbsolutePath.trustString(join14(root, "tasks.json")),
5958
+ sessionMdPath: AbsolutePath.trustString(join13(root, "session.md")),
5959
+ rawTasksJsonPath: AbsolutePath.trustString(join13(root, "tasks.json")),
6041
5960
  addDirs
6042
5961
  });
6043
5962
  }
6044
5963
 
6045
5964
  // src/integration/persistence/refine-unit-builder.ts
6046
- import { join as join15 } from "path";
5965
+ import { join as join14 } from "path";
6047
5966
  async function buildRefinementUnit(storage2, input) {
6048
5967
  const slug = unitSlug(String(input.ticket.id), input.ticket.title);
6049
5968
  const root = storage2.refinementUnitDir(input.sprint.id, slug);
@@ -6057,14 +5976,14 @@ async function buildRefinementUnit(storage2, input) {
6057
5976
  affectedRepos: []
6058
5977
  });
6059
5978
  if (!ctx.ok) return Result.error(ctx.error);
6060
- const ticketMdPath = AbsolutePath.trustString(join15(root, "ticket.md"));
5979
+ const ticketMdPath = AbsolutePath.trustString(join14(root, "ticket.md"));
6061
5980
  const wrote = await writeFileSafe2(ticketMdPath, renderTicketInput(input.ticket));
6062
5981
  if (!wrote.ok) return Result.error(wrote.error);
6063
5982
  return Result.ok({
6064
5983
  root,
6065
- sessionMdPath: AbsolutePath.trustString(join15(root, "session.md")),
5984
+ sessionMdPath: AbsolutePath.trustString(join14(root, "session.md")),
6066
5985
  ticketMdPath,
6067
- requirementsJsonPath: AbsolutePath.trustString(join15(root, "requirements.json"))
5986
+ requirementsJsonPath: AbsolutePath.trustString(join14(root, "requirements.json"))
6068
5987
  });
6069
5988
  }
6070
5989
 
@@ -6196,15 +6115,11 @@ var InMemorySignalBus = class {
6196
6115
  };
6197
6116
 
6198
6117
  // src/integration/signals/file-system-handler.ts
6199
- import { appendFile as appendFile2, mkdir as mkdir11, writeFile as writeFile7 } from "fs/promises";
6200
- import { dirname as dirname9, join as join16 } from "path";
6118
+ import { appendFile as appendFile2, mkdir as mkdir11 } from "fs/promises";
6119
+ import { dirname as dirname9 } from "path";
6201
6120
  function progressPath(paths, sprintId) {
6202
6121
  return paths.progressFile(sprintId);
6203
6122
  }
6204
- function evaluationPath(paths, sprintId, taskId, taskName) {
6205
- const slug = unitSlug(taskId, taskName);
6206
- return AbsolutePath.trustString(join16(paths.executionUnitDir(sprintId, slug), "evaluation.md"));
6207
- }
6208
6123
  async function appendLine(path, line) {
6209
6124
  try {
6210
6125
  await mkdir11(dirname9(path), { recursive: true });
@@ -6222,26 +6137,6 @@ async function appendLine(path, line) {
6222
6137
  );
6223
6138
  }
6224
6139
  }
6225
- async function writeText(path, body) {
6226
- try {
6227
- await mkdir11(dirname9(path), { recursive: true });
6228
- await writeFile7(path, body.endsWith("\n") ? body : `${body}
6229
- `, {
6230
- encoding: "utf-8",
6231
- mode: 384
6232
- });
6233
- return Result.ok();
6234
- } catch (err) {
6235
- return Result.error(
6236
- new StorageError({
6237
- subCode: "io",
6238
- message: `failed to write ${path}: ${err instanceof Error ? err.message : String(err)}`,
6239
- path,
6240
- cause: err
6241
- })
6242
- );
6243
- }
6244
- }
6245
6140
  var FileSystemSignalHandler = class {
6246
6141
  constructor(paths, fileLocker = new FileLocker()) {
6247
6142
  this.paths = paths;
@@ -6273,14 +6168,10 @@ var FileSystemSignalHandler = class {
6273
6168
  return Result.error(
6274
6169
  new StorageError({
6275
6170
  subCode: "io",
6276
- message: "evaluation signal requires taskName in meta to derive the execution unit slug"
6171
+ message: "evaluation signal requires taskName in meta"
6277
6172
  })
6278
6173
  );
6279
6174
  }
6280
- const file = evaluationPath(this.paths, meta.sprintId, String(meta.taskId), meta.taskName);
6281
- const body = renderEvaluationBody(signal);
6282
- const w = await writeText(file, body);
6283
- if (!w.ok) return w;
6284
6175
  const scoreSuffix = signal.overallScore !== void 0 ? `, score ${String(signal.overallScore)}/5` : "";
6285
6176
  return this.appendProgress(
6286
6177
  meta.sprintId,
@@ -6319,33 +6210,6 @@ function formatProgress(timestamp, message, files) {
6319
6210
  const filesSuffix = files !== void 0 && files.length > 0 ? ` (files: ${files.join(", ")})` : "";
6320
6211
  return `- ${timestamp} \u2014 ${message}${filesSuffix}`;
6321
6212
  }
6322
- function renderEvaluationBody(signal) {
6323
- const lines = [];
6324
- lines.push(`# Evaluation \u2014 ${signal.status}`);
6325
- lines.push("");
6326
- lines.push(`Recorded: ${signal.timestamp}`);
6327
- if (signal.overallScore !== void 0) {
6328
- lines.push(`Overall score: ${String(signal.overallScore)}/5`);
6329
- }
6330
- lines.push("");
6331
- if (signal.dimensions.length > 0) {
6332
- lines.push("## Dimensions");
6333
- lines.push("");
6334
- for (const d of signal.dimensions) {
6335
- const verdict = d.passed ? "PASS" : "FAIL";
6336
- const scoreLabel = d.score !== void 0 ? ` (score ${String(d.score)}/5)` : "";
6337
- lines.push(`- **${d.dimension}**${scoreLabel}: ${verdict} \u2014 ${d.finding}`);
6338
- }
6339
- lines.push("");
6340
- }
6341
- if (signal.critique !== void 0 && signal.critique.length > 0) {
6342
- lines.push("## Critique");
6343
- lines.push("");
6344
- lines.push(signal.critique);
6345
- lines.push("");
6346
- }
6347
- return lines.join("\n");
6348
- }
6349
6213
 
6350
6214
  // src/integration/signals/parser.ts
6351
6215
  function buildPatterns() {
@@ -7355,18 +7219,18 @@ async function isFirstLaunch(deps) {
7355
7219
  // src/application/runtime/legacy-detector.ts
7356
7220
  import { access } from "fs/promises";
7357
7221
  import { homedir } from "os";
7358
- import { join as join17 } from "path";
7222
+ import { join as join15 } from "path";
7359
7223
  var NOT_LEGACY = { isLegacy: false, legacyConfigPath: null, hint: "" };
7360
7224
  function defaultRoot() {
7361
7225
  const fromEnv = process.env["RALPHCTL_ROOT"];
7362
7226
  if (fromEnv !== void 0 && fromEnv.length > 0) {
7363
7227
  return AbsolutePath.trustString(fromEnv);
7364
7228
  }
7365
- return AbsolutePath.trustString(join17(homedir(), ".ralphctl"));
7229
+ return AbsolutePath.trustString(join15(homedir(), ".ralphctl"));
7366
7230
  }
7367
7231
  async function detectLegacyLayout(deps = {}) {
7368
7232
  const root = deps.root ?? defaultRoot();
7369
- const legacyConfigPath = AbsolutePath.trustString(join17(root, "config.json"));
7233
+ const legacyConfigPath = AbsolutePath.trustString(join15(root, "config.json"));
7370
7234
  try {
7371
7235
  await access(legacyConfigPath);
7372
7236
  } catch {
@@ -7387,7 +7251,7 @@ Then re-run ralphctl to start fresh, and use 'ralphctl project add' to register
7387
7251
  // src/integration/ai/dist-asset-manifest.ts
7388
7252
  import { existsSync as existsSync4 } from "fs";
7389
7253
  import { readFile as readFile7, stat as stat4 } from "fs/promises";
7390
- import { dirname as dirname10, join as join18 } from "path";
7254
+ import { dirname as dirname10, join as join16 } from "path";
7391
7255
  import { fileURLToPath as fileURLToPath3 } from "url";
7392
7256
  var HERE3 = dirname10(fileURLToPath3(import.meta.url));
7393
7257
  var cached2 = { state: "unverified" };
@@ -7395,7 +7259,7 @@ async function verifyDistAssets(distRootOverride) {
7395
7259
  if (cached2.state === "pass") return Result.ok();
7396
7260
  if (cached2.state === "fail") return Result.error(cached2.error);
7397
7261
  const distRoot = distRootOverride ?? HERE3;
7398
- const manifestPath = join18(distRoot, "manifest.json");
7262
+ const manifestPath = join16(distRoot, "manifest.json");
7399
7263
  if (!existsSync4(manifestPath)) {
7400
7264
  cached2 = { state: "pass" };
7401
7265
  return Result.ok();
@@ -7407,7 +7271,7 @@ async function verifyDistAssets(distRootOverride) {
7407
7271
  }
7408
7272
  const missing = [];
7409
7273
  for (const rel of parsed.value.assets) {
7410
- const abs = join18(distRoot, rel);
7274
+ const abs = join16(distRoot, rel);
7411
7275
  try {
7412
7276
  const s = await stat4(abs);
7413
7277
  if (!s.isFile()) missing.push(rel);
@@ -9681,7 +9545,22 @@ function unlinkSkillsLeaf(deps, opts = {}) {
9681
9545
  }
9682
9546
 
9683
9547
  // src/application/chains/execute/per-task-flow.ts
9684
- import { dirname as dirname11, join as join21 } from "path";
9548
+ import { join as join20 } from "path";
9549
+
9550
+ // src/kernel/algorithms/execution-round-paths.ts
9551
+ import { join as join17 } from "path";
9552
+ function roundDir(unitRoot, round) {
9553
+ return join17(unitRoot, "rounds", String(round));
9554
+ }
9555
+ function generatorRoundDir(unitRoot, round) {
9556
+ return join17(roundDir(unitRoot, round), "generator");
9557
+ }
9558
+ function evaluatorRoundDir(unitRoot, round) {
9559
+ return join17(roundDir(unitRoot, round), "evaluator");
9560
+ }
9561
+ function evaluatorVerdictSprintRelative(slug, round) {
9562
+ return join17("execution", slug, "rounds", String(round), "evaluator", "evaluation.md");
9563
+ }
9685
9564
 
9686
9565
  // src/integration/persistence/done-criteria-reader.ts
9687
9566
  import { readFile as readFile8 } from "fs/promises";
@@ -9735,7 +9614,7 @@ var BranchPreflightUseCase = class {
9735
9614
  };
9736
9615
 
9737
9616
  // src/business/usecases/evaluate/evaluate-and-fix-loop.ts
9738
- import { join as join19 } from "path";
9617
+ import { join as join18 } from "path";
9739
9618
 
9740
9619
  // src/business/usecases/_shared/add-dir-args.ts
9741
9620
  function buildAdditionalCwdArgs(paths) {
@@ -9756,6 +9635,24 @@ function renderFileHandoffWrapper(promptFilePath) {
9756
9635
  "Follow the protocol in that file exactly."
9757
9636
  ].join("\n");
9758
9637
  }
9638
+ function renderFixHandoffWrapper(promptFilePath, critique) {
9639
+ const safeCritique = critique.replace(/<\/evaluator-critique>/g, "<\\/evaluator-critique>");
9640
+ return [
9641
+ "You are an agent under the ralphctl harness \u2014 resuming on a fix round.",
9642
+ "",
9643
+ "The evaluator from the previous round flagged the work as not yet complete.",
9644
+ "Its critique follows verbatim \u2014 read it FIRST, before doing anything else:",
9645
+ "",
9646
+ "<evaluator-critique>",
9647
+ safeCritique,
9648
+ "</evaluator-critique>",
9649
+ "",
9650
+ `Then re-read the task spec at \`${promptFilePath}\` to refresh the success criteria.`,
9651
+ "",
9652
+ "Address every dimension flagged failed. Do not regress the dimensions that already passed.",
9653
+ "When the work is complete, emit `<task-complete>` per the spec."
9654
+ ].join("\n");
9655
+ }
9759
9656
 
9760
9657
  // src/business/usecases/evaluate/evaluate-task.ts
9761
9658
  var MAX_MALFORMED_CRITIQUE_CHARS = 500;
@@ -9895,9 +9792,6 @@ var EvaluateAndFixLoopUseCase = class {
9895
9792
  history: []
9896
9793
  });
9897
9794
  }
9898
- const evaluatorPromptPath = AbsolutePath.trustString(
9899
- input.evaluateWorkspaceDir !== void 0 ? join19(input.evaluateWorkspaceDir, "evaluator-prompt.md") : join19(String(input.contextsDir), `evaluate-${String(input.task.id)}.md`)
9900
- );
9901
9795
  const history = [];
9902
9796
  let previousSignal;
9903
9797
  let previousCritique;
@@ -9921,18 +9815,21 @@ var EvaluateAndFixLoopUseCase = class {
9921
9815
  });
9922
9816
  }
9923
9817
  }
9818
+ const evaluatorPromptPath = AbsolutePath.trustString(
9819
+ join18(evaluatorRoundDir(input.evaluateWorkspaceDir, round), "prompt.md")
9820
+ );
9924
9821
  const evalPromptResult = await this.prompts.buildEvaluatePrompt({
9925
9822
  task: input.task,
9926
9823
  sprint: input.sprint,
9824
+ evaluateWorkspaceDir: input.evaluateWorkspaceDir,
9927
9825
  ...previousCritique !== void 0 ? { previousCritique } : {},
9928
- ...input.evaluateWorkspaceDir !== void 0 ? { evaluateWorkspaceDir: input.evaluateWorkspaceDir } : {},
9929
9826
  ...input.doneCriteriaBullet !== void 0 ? { doneCriteriaBullet: input.doneCriteriaBullet } : {}
9930
9827
  });
9931
9828
  if (!evalPromptResult.ok) return Result.error(evalPromptResult.error);
9932
9829
  const written = await this.writeContextFile.write(evaluatorPromptPath, evalPromptResult.value);
9933
9830
  if (!written.ok) return Result.error(written.error);
9934
9831
  const evaluatorCwd = input.evaluateSessionCwd ?? input.cwd;
9935
- const evaluatorSessionMdPath = input.nextSessionMdPath ? await input.nextSessionMdPath("evaluator") : void 0;
9832
+ const evaluatorSessionMdPath = input.nextSessionMdPath ? await input.nextSessionMdPath("evaluator", round) : void 0;
9936
9833
  const evalResult = await this.evaluator.execute({
9937
9834
  task: input.task,
9938
9835
  sprint: input.sprint,
@@ -9945,6 +9842,16 @@ var EvaluateAndFixLoopUseCase = class {
9945
9842
  if (!evalResult.ok) return Result.error(evalResult.error);
9946
9843
  const { outcome, signal, fullCritique } = evalResult.value;
9947
9844
  history.push({ round, outcome, signal, critique: fullCritique });
9845
+ const verdictPath = AbsolutePath.trustString(
9846
+ join18(evaluatorRoundDir(input.evaluateWorkspaceDir, round), "evaluation.md")
9847
+ );
9848
+ const verdictWritten = await this.writeContextFile.write(verdictPath, fullCritique);
9849
+ if (!verdictWritten.ok) {
9850
+ log.warn("failed to persist per-round evaluator verdict", {
9851
+ round,
9852
+ error: verdictWritten.error.message
9853
+ });
9854
+ }
9948
9855
  log.info(`evaluator round complete for task ${String(input.task.id)}`, { round, outcome });
9949
9856
  if (outcome === "passed") break;
9950
9857
  if (outcome === "malformed") {
@@ -9970,7 +9877,8 @@ var EvaluateAndFixLoopUseCase = class {
9970
9877
  break;
9971
9878
  }
9972
9879
  log.info("resuming generator with critique", { round });
9973
- const generatorSessionMdPath = input.nextSessionMdPath ? await input.nextSessionMdPath("generator") : void 0;
9880
+ const fixRound = round + 1;
9881
+ const generatorSessionMdPath = input.nextSessionMdPath ? await input.nextSessionMdPath("generator", fixRound) : void 0;
9974
9882
  const fixResult = await this.generator.execute({
9975
9883
  task: input.task,
9976
9884
  sprint: input.sprint,
@@ -9978,6 +9886,7 @@ var EvaluateAndFixLoopUseCase = class {
9978
9886
  promptFilePath: input.executePromptFilePath,
9979
9887
  ...resumeSessionId !== void 0 ? { resumeSessionId } : {},
9980
9888
  ...generatorSessionMdPath !== void 0 ? { sessionMdPath: generatorSessionMdPath } : {},
9889
+ fixContext: { critique: fullCritique },
9981
9890
  ...input.abortSignal !== void 0 ? { abortSignal: input.abortSignal } : {}
9982
9891
  });
9983
9892
  if (!fixResult.ok) return Result.error(fixResult.error);
@@ -9989,8 +9898,8 @@ var EvaluateAndFixLoopUseCase = class {
9989
9898
  projectPath: input.cwd,
9990
9899
  checkScript: input.checkScript
9991
9900
  });
9992
- if (!checkResult.ok) return Result.error(checkResult.error);
9993
- if (!checkResult.value.passed) {
9901
+ if (!checkResult.ok) {
9902
+ if (checkResult.error.code !== "check-failed") return Result.error(checkResult.error);
9994
9903
  log.warn("post-task check failed after fix attempt \u2014 re-evaluating anyway", { round });
9995
9904
  }
9996
9905
  }
@@ -10037,7 +9946,7 @@ var ExecuteSingleTaskUseCase = class {
10037
9946
  taskId: input.task.id,
10038
9947
  projectPath: input.cwd
10039
9948
  });
10040
- const wrapper = renderFileHandoffWrapper(input.promptFilePath);
9949
+ const wrapper = input.fixContext !== void 0 ? renderFixHandoffWrapper(input.promptFilePath, input.fixContext.critique) : renderFileHandoffWrapper(input.promptFilePath);
10041
9950
  log.info(`executing task ${String(input.task.id)}${formatNameSuffix2(input.task.name)}`);
10042
9951
  const sessionOptions = {
10043
9952
  cwd: input.cwd,
@@ -10107,6 +10016,22 @@ function formatNameSuffix2(name) {
10107
10016
  return ` \u2014 "${slice}"`;
10108
10017
  }
10109
10018
 
10019
+ // src/domain/errors/check-failed-error.ts
10020
+ var CheckFailedError = class extends Error {
10021
+ /** Discriminator. `as const` keeps it narrow at the type level. */
10022
+ code = "check-failed";
10023
+ /** Captured output of the failing script — surfaced to logs / progress. */
10024
+ output;
10025
+ constructor(opts) {
10026
+ super(opts.message ?? "post-task check script failed");
10027
+ this.name = "CheckFailedError";
10028
+ this.output = opts.output;
10029
+ if (opts.cause !== void 0) {
10030
+ this.cause = opts.cause;
10031
+ }
10032
+ }
10033
+ };
10034
+
10110
10035
  // src/business/usecases/execute/post-task-check.ts
10111
10036
  var PostTaskCheckUseCase = class {
10112
10037
  constructor(external, logger) {
@@ -10129,10 +10054,11 @@ var PostTaskCheckUseCase = class {
10129
10054
  input.timeoutMs
10130
10055
  );
10131
10056
  if (!result.passed) {
10132
- log.warn("post-task check failed");
10057
+ log.warn("post-task check failed", { output: result.output });
10058
+ return Result.error(new CheckFailedError({ output: result.output }));
10133
10059
  }
10134
10060
  return Result.ok({
10135
- passed: result.passed,
10061
+ passed: true,
10136
10062
  output: result.output,
10137
10063
  skipped: false
10138
10064
  });
@@ -10290,8 +10216,7 @@ function buildExecutionUnitLeaf(deps, opts = {}) {
10290
10216
  ...ctx,
10291
10217
  executionUnitRoot: out.root,
10292
10218
  executionAddDirs: out.addDirs,
10293
- executionSessionCwd: out.sessionCwd,
10294
- executionEvaluationMdPath: out.evaluationMdPath
10219
+ executionSessionCwd: out.sessionCwd
10295
10220
  })
10296
10221
  });
10297
10222
  }
@@ -10305,8 +10230,21 @@ function collectPriorEvaluations(tasks) {
10305
10230
  return map;
10306
10231
  }
10307
10232
 
10233
+ // src/application/chains/leaves/noop-leaf.ts
10234
+ function noopLeaf(name) {
10235
+ return new Leaf(name, {
10236
+ useCase: {
10237
+ execute(input) {
10238
+ return Promise.resolve(Result.ok(input));
10239
+ }
10240
+ },
10241
+ input: (ctx) => ctx,
10242
+ output: (_, ctx) => ctx
10243
+ });
10244
+ }
10245
+
10308
10246
  // src/application/chains/leaves/render-prompt-to-file.ts
10309
- import { join as join20 } from "path";
10247
+ import { join as join19 } from "path";
10310
10248
  function renderPromptToFileLeaf(deps, opts) {
10311
10249
  return new Leaf("render-prompt-to-file", {
10312
10250
  useCase: {
@@ -10336,7 +10274,7 @@ function defaultPromptPath(ctx, opts) {
10336
10274
  const sprintDir = storagePaths.sprintDir(ctx.sprintId);
10337
10275
  const id = opts.identifier(ctx);
10338
10276
  const basename4 = id.length > 0 ? `${opts.flowName}-${id}.md` : `${opts.flowName}.md`;
10339
- return AbsolutePath.trustString(join20(sprintDir, "contexts", basename4));
10277
+ return AbsolutePath.trustString(join19(sprintDir, "contexts", basename4));
10340
10278
  }
10341
10279
 
10342
10280
  // src/application/chains/execute/per-task-flow.ts
@@ -10383,18 +10321,30 @@ function createPerTaskFlow(deps, opts) {
10383
10321
  catchIf: (err) => err.code === "aborted",
10384
10322
  fallback: markCancelledFallbackLeaf(deps)
10385
10323
  });
10386
- const buildEvalWorkspaceStep = buildExecutionUnitLeaf({
10387
- sessionFolderBuilder: deps.sessionFolderBuilder,
10388
- aiSession: deps.aiSession
10389
- });
10390
- const evaluatorAsLeaf = new OnError(
10391
- new Sequential("evaluate", [buildEvalWorkspaceStep, evaluateLoopLeaf(deps, evaluateLoop)]),
10324
+ const buildEvalWorkspaceStep = new OnError(
10325
+ buildExecutionUnitLeaf({
10326
+ sessionFolderBuilder: deps.sessionFolderBuilder,
10327
+ aiSession: deps.aiSession
10328
+ }),
10392
10329
  {
10393
10330
  catchIf: (err) => err.code !== "aborted",
10394
- fallback: noopLeaf("evaluate-task-noop")
10331
+ fallback: noopLeaf("build-execution-unit-noop")
10332
+ }
10333
+ );
10334
+ const evaluateLoopStep = new OnError(evaluateLoopLeaf(deps, evaluateLoop), {
10335
+ catchIf: (err) => err.code !== "aborted",
10336
+ fallback: noopLeaf("evaluate-task-noop")
10337
+ });
10338
+ const postTaskStep = new OnError(
10339
+ new OnError(postTaskCheckLeaf(postCheck), {
10340
+ catchIf: (err) => err.code === "check-failed",
10341
+ fallback: postTaskCheckBlockedFallbackLeaf(deps)
10342
+ }),
10343
+ {
10344
+ catchIf: (err) => err.code !== "aborted" && err.code !== "check-failed",
10345
+ fallback: noopLeaf("post-task-check-noop")
10395
10346
  }
10396
10347
  );
10397
- const postTaskStep = postTaskCheckLeaf(postCheck);
10398
10348
  const renderPromptStep = renderPromptToFileLeaf(
10399
10349
  { writeContextFile: deps.writeContextFile },
10400
10350
  {
@@ -10403,7 +10353,7 @@ function createPerTaskFlow(deps, opts) {
10403
10353
  path: (ctx) => {
10404
10354
  const slug = unitSlug(String(ctx.task.id), ctx.task.name);
10405
10355
  const root = resolveStoragePaths().executionUnitDir(ctx.sprintId, slug);
10406
- return AbsolutePath.trustString(join21(String(root), "prompt.md"));
10356
+ return AbsolutePath.trustString(join20(String(root), "prompt.md"));
10407
10357
  },
10408
10358
  buildPrompt: (ctx) => deps.prompts.buildExecutePrompt({
10409
10359
  task: ctx.task,
@@ -10416,9 +10366,10 @@ function createPerTaskFlow(deps, opts) {
10416
10366
  branchPreflightStep,
10417
10367
  markInProgressLeaf(deps),
10418
10368
  renderPromptStep,
10369
+ buildEvalWorkspaceStep,
10419
10370
  executeTaskStep,
10420
10371
  postTaskStep,
10421
- evaluatorAsLeaf,
10372
+ evaluateLoopStep,
10422
10373
  commitTaskLeaf(deps),
10423
10374
  markDoneLeaf(deps)
10424
10375
  ]);
@@ -10461,6 +10412,22 @@ function markBlockedFallbackLeaf(deps) {
10461
10412
  output: (ctx, task) => ({ ...ctx, task, taskBlocked: true })
10462
10413
  });
10463
10414
  }
10415
+ function postTaskCheckBlockedFallbackLeaf(deps) {
10416
+ return new Leaf("mark-blocked-check", {
10417
+ useCase: {
10418
+ async execute(input) {
10419
+ const transitioned = input.task.markBlocked("post-task check failed");
10420
+ if (!transitioned.ok) return Result.error(transitioned.error);
10421
+ const saved = await deps.taskRepo.update(input.sprintId, transitioned.value);
10422
+ if (!saved.ok) return Result.error(saved.error);
10423
+ deps.signalBus.emit({ type: "task-finished", taskId: transitioned.value.id, status: "blocked" });
10424
+ return Result.ok(transitioned.value);
10425
+ }
10426
+ },
10427
+ input: (ctx) => ({ sprintId: ctx.sprintId, task: ctx.task }),
10428
+ output: (ctx, task) => ({ ...ctx, task, taskBlocked: true })
10429
+ });
10430
+ }
10464
10431
  function markCancelledFallbackLeaf(deps) {
10465
10432
  return new Leaf("mark-cancelled", {
10466
10433
  useCase: {
@@ -10496,6 +10463,7 @@ function markInProgressLeaf(deps) {
10496
10463
  });
10497
10464
  }
10498
10465
  function executeTaskLeaf(useCase) {
10466
+ let attempt = 0;
10499
10467
  return new Leaf("execute-task", {
10500
10468
  useCase: {
10501
10469
  async execute(input) {
@@ -10508,7 +10476,9 @@ function executeTaskLeaf(useCase) {
10508
10476
  message: "execute-task: promptFilePath is missing \u2014 render-prompt-to-file must run first"
10509
10477
  });
10510
10478
  }
10511
- const sessionMdPath = input.executionUnitRoot !== void 0 ? AbsolutePath.trustString(await nextSessionPath(String(input.executionUnitRoot))) : void 0;
10479
+ attempt += 1;
10480
+ const filename = attempt === 1 ? "session.md" : `session-attempt-${String(attempt)}.md`;
10481
+ const sessionMdPath = input.executionUnitRoot !== void 0 ? AbsolutePath.trustString(join20(generatorRoundDir(String(input.executionUnitRoot), 1), filename)) : void 0;
10512
10482
  const result = await useCase.execute({
10513
10483
  sprint: input.sprint,
10514
10484
  task: input.task,
@@ -10616,19 +10586,21 @@ function evaluateLoopLeaf(deps, loop) {
10616
10586
  if (input.taskBlocked) {
10617
10587
  return Result.ok({ task: input.task });
10618
10588
  }
10619
- const doneCriteriaBullet = input.executionUnitRoot !== void 0 ? await readDoneCriteriaBullet(
10620
- join21(String(input.executionUnitRoot), "done-criteria.md"),
10621
- String(input.task.id)
10622
- ) : "";
10589
+ if (input.executionUnitRoot === void 0) {
10590
+ return Result.ok({ task: input.task });
10591
+ }
10623
10592
  if (input.promptFilePath === void 0) {
10624
10593
  return Result.error({
10625
10594
  code: "invalid-state",
10626
10595
  message: "evaluate-task: promptFilePath is missing \u2014 render-prompt-to-file must run first"
10627
10596
  });
10628
10597
  }
10629
- const contextsDir = AbsolutePath.trustString(dirname11(String(input.promptFilePath)));
10630
- const unitMounted = input.executionUnitRoot !== void 0;
10631
- const refreshWorkspace = unitMounted ? async () => {
10598
+ const unitRoot = input.executionUnitRoot;
10599
+ const doneCriteriaBullet = await readDoneCriteriaBullet(
10600
+ join20(String(unitRoot), "done-criteria.md"),
10601
+ String(input.task.id)
10602
+ );
10603
+ const refreshWorkspace = async () => {
10632
10604
  await deps.aiSession.ensureReady();
10633
10605
  const aiProvider = deps.aiSession.getProviderName();
10634
10606
  const priorEvaluations = collectPriorEvaluations2(input.tasks);
@@ -10639,30 +10611,34 @@ function evaluateLoopLeaf(deps, loop) {
10639
10611
  aiProvider,
10640
10612
  priorEvaluations
10641
10613
  });
10642
- } : void 0;
10643
- const unitRoot = input.executionUnitRoot;
10644
- const nextSessionMdPath = unitRoot ? async () => AbsolutePath.trustString(await nextSessionPath(String(unitRoot))) : void 0;
10614
+ };
10615
+ const nextSessionMdPath = (kind, round) => {
10616
+ const dir = kind === "generator" ? generatorRoundDir : evaluatorRoundDir;
10617
+ return Promise.resolve(AbsolutePath.trustString(join20(dir(String(unitRoot), round), "session.md")));
10618
+ };
10645
10619
  const result = await loop.execute({
10646
10620
  task: input.task,
10647
10621
  sprint: input.sprint,
10648
10622
  cwd: input.cwd,
10649
10623
  executePromptFilePath: String(input.promptFilePath),
10650
- contextsDir,
10624
+ evaluateWorkspaceDir: String(unitRoot),
10625
+ refreshWorkspace,
10626
+ nextSessionMdPath,
10651
10627
  ...input.checkScript !== void 0 ? { checkScript: input.checkScript } : {},
10652
10628
  ...input.resumeSessionId !== void 0 ? { resumeSessionId: input.resumeSessionId } : {},
10653
10629
  ...input.executionAddDirs !== void 0 ? { addDirs: input.executionAddDirs } : {},
10654
10630
  ...input.executionSessionCwd !== void 0 ? { evaluateSessionCwd: input.executionSessionCwd } : {},
10655
- ...input.executionUnitRoot !== void 0 ? { evaluateWorkspaceDir: String(input.executionUnitRoot) } : {},
10656
- ...refreshWorkspace !== void 0 ? { refreshWorkspace } : {},
10657
- ...nextSessionMdPath !== void 0 ? { nextSessionMdPath } : {},
10658
10631
  ...doneCriteriaBullet.length > 0 ? { doneCriteriaBullet } : {}
10659
10632
  });
10660
10633
  if (!result.ok) return Result.error(result.error);
10661
10634
  if (result.value.rounds > 0 && result.value.finalSignal !== null) {
10635
+ const finalRound = result.value.rounds;
10636
+ const slug = unitSlug(String(input.task.id), input.task.name);
10637
+ const file = evaluatorVerdictSprintRelative(slug, finalRound);
10662
10638
  const recorded = input.task.recordEvaluation({
10663
10639
  status: result.value.finalSignal.status,
10664
10640
  output: result.value.finalCritique.slice(0, MAX_PREVIEW_CHARS),
10665
- file: input.executionEvaluationMdPath !== void 0 ? String(input.executionEvaluationMdPath) : `execution/${String(input.task.id)}/evaluation.md`
10641
+ file
10666
10642
  });
10667
10643
  const saved = await deps.taskRepo.update(input.sprintId, recorded);
10668
10644
  if (!saved.ok) return Result.error(saved.error);
@@ -10683,8 +10659,7 @@ function evaluateLoopLeaf(deps, loop) {
10683
10659
  taskBlocked: ctx.taskBlocked === true,
10684
10660
  ...ctx.executionUnitRoot !== void 0 ? { executionUnitRoot: ctx.executionUnitRoot } : {},
10685
10661
  ...ctx.executionAddDirs !== void 0 ? { executionAddDirs: ctx.executionAddDirs } : {},
10686
- ...ctx.executionSessionCwd !== void 0 ? { executionSessionCwd: ctx.executionSessionCwd } : {},
10687
- ...ctx.executionEvaluationMdPath !== void 0 ? { executionEvaluationMdPath: ctx.executionEvaluationMdPath } : {}
10662
+ ...ctx.executionSessionCwd !== void 0 ? { executionSessionCwd: ctx.executionSessionCwd } : {}
10688
10663
  }),
10689
10664
  // Push the recorded task back onto the context so the downstream
10690
10665
  // `mark-done` leaf carries the evaluation forward when it flips
@@ -10749,17 +10724,6 @@ function commitTaskLeaf(deps) {
10749
10724
  output: (ctx, task) => ({ ...ctx, task })
10750
10725
  });
10751
10726
  }
10752
- function noopLeaf(name) {
10753
- return new Leaf(name, {
10754
- useCase: {
10755
- execute(input) {
10756
- return Promise.resolve(Result.ok(input));
10757
- }
10758
- },
10759
- input: (ctx) => ctx,
10760
- output: (_, ctx) => ctx
10761
- });
10762
- }
10763
10727
 
10764
10728
  // src/application/chains/execute/execute-flow.ts
10765
10729
  function createExecuteFlow(deps, opts) {
@@ -10782,7 +10746,8 @@ function createExecuteFlow(deps, opts) {
10782
10746
  const initializeStep = new Sequential("initialize", [
10783
10747
  resolveBranchLeaf(deps),
10784
10748
  dirtyTreePreflightLeaf(deps, opts),
10785
- checkScriptsSprintStartLeaf(deps)
10749
+ resolveCheckScriptsLeaf(deps),
10750
+ setupScriptsSprintStartLeaf(deps)
10786
10751
  ]);
10787
10752
  return new Sequential("execute", [
10788
10753
  loadSprintLeaf({ sprintRepo: deps.sprintRepo }),
@@ -10972,14 +10937,16 @@ function bridgePerTaskChain(task, inner, opts, taskRepo) {
10972
10937
  async execute(input) {
10973
10938
  const fresh = await taskRepo.findBySprintId(input.sprintId);
10974
10939
  const liveTasks = fresh.ok ? fresh.value : input.tasks ?? opts.tasks;
10940
+ const liveSprint = input.sprint ?? opts.sprint;
10941
+ const resolvedCheckScript = input.checkScript ?? input.checkScripts?.get(task.projectPath);
10975
10942
  const innerCtx = {
10976
10943
  sprintId: input.sprintId,
10977
- sprint: opts.sprint,
10944
+ sprint: liveSprint,
10978
10945
  task,
10979
10946
  tasks: liveTasks,
10980
10947
  cwd: task.projectPath,
10981
10948
  expectedBranch: input.expectedBranch,
10982
- ...input.checkScript !== void 0 ? { checkScript: input.checkScript } : {},
10949
+ ...resolvedCheckScript !== void 0 ? { checkScript: resolvedCheckScript } : {},
10983
10950
  ...input.noCommit === true ? { noCommit: true } : {}
10984
10951
  };
10985
10952
  const innerResult = await inner.execute(innerCtx);
@@ -11081,33 +11048,118 @@ function assertTasksAcyclicLeaf(sortResult) {
11081
11048
  output: (ctx) => ctx
11082
11049
  });
11083
11050
  }
11084
- function checkScriptsSprintStartLeaf(deps) {
11051
+ function resolveCheckScriptsLeaf(deps) {
11052
+ return new Leaf("resolve-check-scripts", {
11053
+ useCase: {
11054
+ async execute(input) {
11055
+ const map = /* @__PURE__ */ new Map();
11056
+ const repoPaths = input.sprint.affectedRepositories;
11057
+ if (repoPaths.length === 0) {
11058
+ return Result.ok(map);
11059
+ }
11060
+ const project = await deps.projectRepo.findByName(input.sprint.projectName);
11061
+ if (!project.ok) {
11062
+ deps.logger.warn(
11063
+ `resolve-check-scripts: project ${String(input.sprint.projectName)} not found \u2014 per-task gate will skip`,
11064
+ { error: project.error.message }
11065
+ );
11066
+ return Result.ok(map);
11067
+ }
11068
+ for (const repoPath of repoPaths) {
11069
+ const repo = project.value.repositories.find((r) => r.path === repoPath);
11070
+ if (repo === void 0) continue;
11071
+ const script = repo.checkScript;
11072
+ if (script === void 0 || script.length === 0) continue;
11073
+ map.set(repoPath, script);
11074
+ }
11075
+ return Result.ok(map);
11076
+ }
11077
+ },
11078
+ input: (ctx) => {
11079
+ if (!ctx.sprint) throw new Error("resolve-check-scripts: ctx.sprint must be loaded first");
11080
+ return { sprint: ctx.sprint };
11081
+ },
11082
+ output: (ctx, checkScripts) => ({ ...ctx, checkScripts })
11083
+ });
11084
+ }
11085
+ function setupScriptsSprintStartLeaf(deps) {
11085
11086
  return new Leaf(
11086
- "check-scripts-sprint-start",
11087
+ "setup-scripts-sprint-start",
11087
11088
  {
11088
11089
  useCase: {
11089
11090
  async execute(input) {
11090
- if (input.checkScript === void 0 || input.checkScript.length === 0) {
11091
- return Promise.resolve(Result.ok(void 0));
11091
+ const repoPaths = input.sprint.affectedRepositories;
11092
+ if (repoPaths.length === 0) {
11093
+ return Result.ok(void 0);
11092
11094
  }
11093
- const r = await deps.external.runCheckScript(input.cwd, input.checkScript, "sprint-start");
11094
- if (!r.passed) {
11095
- return Result.error(
11096
- new InvalidStateError({
11097
- entity: "sprint",
11098
- currentState: "check-failed",
11099
- attemptedAction: "execute",
11100
- message: "sprint-start check script failed"
11101
- })
11095
+ const project = await deps.projectRepo.findByName(input.sprint.projectName);
11096
+ if (!project.ok) {
11097
+ deps.logger.warn(
11098
+ `setup-scripts-sprint-start: project ${String(input.sprint.projectName)} not found \u2014 skipping setup`,
11099
+ { error: project.error.message }
11102
11100
  );
11101
+ return Result.ok(void 0);
11102
+ }
11103
+ let sprint = input.sprint;
11104
+ const now = IsoTimestamp.trustString((/* @__PURE__ */ new Date()).toISOString());
11105
+ for (const repoPath of repoPaths) {
11106
+ if (input.sprint.setupRanAt.has(repoPath)) {
11107
+ const stampedAt = input.sprint.setupRanAt.get(repoPath);
11108
+ deps.logger.debug(
11109
+ `setup-scripts-sprint-start: skipping ${String(repoPath)} \u2014 already stamped at ${String(stampedAt)}`
11110
+ );
11111
+ continue;
11112
+ }
11113
+ const repo = project.value.repositories.find((r) => r.path === repoPath);
11114
+ if (repo === void 0) {
11115
+ continue;
11116
+ }
11117
+ const script = repo.setupScript;
11118
+ if (script === void 0 || script.length === 0) {
11119
+ continue;
11120
+ }
11121
+ let outcome;
11122
+ try {
11123
+ outcome = await deps.external.runSetupScript(repoPath, script, repo.checkTimeout);
11124
+ } catch (err) {
11125
+ return Result.error(
11126
+ new InvalidStateError({
11127
+ entity: "sprint",
11128
+ currentState: "setup-failed",
11129
+ attemptedAction: "execute",
11130
+ message: `sprint-start setup script failed in ${String(repoPath)}: ${err instanceof Error ? err.message : String(err)}`,
11131
+ hint: "Fix the setup script (configured via `project onboard` / `project repo add`) and retry. Setup runs once at sprint start; the per-task `checkScript` is a separate gate."
11132
+ })
11133
+ );
11134
+ }
11135
+ if (!outcome.passed) {
11136
+ return Result.error(
11137
+ new InvalidStateError({
11138
+ entity: "sprint",
11139
+ currentState: "setup-failed",
11140
+ attemptedAction: "execute",
11141
+ message: `sprint-start setup script failed in ${String(repoPath)}`,
11142
+ hint: "Fix the setup script (configured via `project onboard` / `project repo add`) and retry. Setup runs once at sprint start; the per-task `checkScript` is a separate gate."
11143
+ })
11144
+ );
11145
+ }
11146
+ sprint = sprint.recordSetupRun(repoPath, now);
11147
+ }
11148
+ if (sprint !== input.sprint) {
11149
+ const saved = await deps.sprintRepo.save(sprint);
11150
+ if (!saved.ok) {
11151
+ deps.logger.warn("setup-scripts-sprint-start: failed to persist setup audit stamps", {
11152
+ error: saved.error.message
11153
+ });
11154
+ }
11103
11155
  }
11104
11156
  return Result.ok(void 0);
11105
11157
  }
11106
11158
  },
11107
- input: (ctx) => ({
11108
- cwd: ctx.cwd,
11109
- ...ctx.checkScript !== void 0 ? { checkScript: ctx.checkScript } : {}
11110
- }),
11159
+ input: (ctx) => {
11160
+ if (!ctx.sprint) throw new Error("setup-scripts-sprint-start: ctx.sprint must be loaded first");
11161
+ return { sprintId: ctx.sprintId, sprint: ctx.sprint };
11162
+ },
11111
11163
  output: (ctx) => ctx
11112
11164
  }
11113
11165
  );
@@ -11204,10 +11256,10 @@ function applyFeedbackLeaf(useCase) {
11204
11256
  });
11205
11257
  }
11206
11258
  const { resolveStoragePaths: resolveStoragePaths2 } = await import("./storage-paths-IPNZZM5D.mjs");
11207
- const { join: join35 } = await import("path");
11259
+ const { join: join34 } = await import("path");
11208
11260
  const sprintDir = resolveStoragePaths2().sprintDir(input.sprintId);
11209
11261
  const { AbsolutePath: APV } = await import("./absolute-path-WUTZQ37D.mjs");
11210
- const sessionMdPath = APV.trustString(join35(sprintDir, "feedback", `session-${String(input.iteration)}.md`));
11262
+ const sessionMdPath = APV.trustString(join34(sprintDir, "feedback", `session-${String(input.iteration)}.md`));
11211
11263
  const result = await useCase.execute({
11212
11264
  sprint: input.sprint,
11213
11265
  promptFilePath: String(input.promptFilePath),
@@ -11244,7 +11296,7 @@ function recordFeedbackIterationLeaf(logger) {
11244
11296
  try {
11245
11297
  const { resolveStoragePaths: resolveStoragePaths2 } = await import("./storage-paths-IPNZZM5D.mjs");
11246
11298
  const { mkdir: mkdir16, appendFile: appendFile4 } = await import("fs/promises");
11247
- const { dirname: dirname17 } = await import("path");
11299
+ const { dirname: dirname16 } = await import("path");
11248
11300
  const path = resolveStoragePaths2().feedbackFile(input.sprintId);
11249
11301
  const stamp = (/* @__PURE__ */ new Date()).toISOString();
11250
11302
  const block = `
@@ -11254,7 +11306,7 @@ function recordFeedbackIterationLeaf(logger) {
11254
11306
 
11255
11307
  ${input.feedbackText.trim()}
11256
11308
  `;
11257
- await mkdir16(dirname17(path), { recursive: true });
11309
+ await mkdir16(dirname16(path), { recursive: true });
11258
11310
  await appendFile4(path, block, "utf-8");
11259
11311
  } catch (err) {
11260
11312
  logger.warn("feedback: failed to append to feedback.md", {
@@ -11716,8 +11768,8 @@ function saveSprintLeaf(deps, name = "save-sprint") {
11716
11768
  }
11717
11769
 
11718
11770
  // src/application/chains/leaves/save-tasks.ts
11719
- import { writeFile as writeFile8 } from "fs/promises";
11720
- import { dirname as dirname12 } from "path";
11771
+ import { writeFile as writeFile7 } from "fs/promises";
11772
+ import { dirname as dirname11 } from "path";
11721
11773
  import { mkdir as mkdir12 } from "fs/promises";
11722
11774
  var FALLBACK_CRITERION = "(no explicit criteria \u2014 use task description as proxy)";
11723
11775
  function renderDoneCriteria(tasks) {
@@ -11743,9 +11795,9 @@ function saveTasksLeaf(deps, name = "save-tasks") {
11743
11795
  const storage2 = resolveStoragePaths();
11744
11796
  const criteriaPath = String(storage2.doneCriteriaFile(input.sprintId));
11745
11797
  try {
11746
- await mkdir12(dirname12(criteriaPath), { recursive: true });
11798
+ await mkdir12(dirname11(criteriaPath), { recursive: true });
11747
11799
  const body = renderDoneCriteria(input.tasks);
11748
- await writeFile8(criteriaPath, body, { encoding: "utf-8", mode: 384 });
11800
+ await writeFile7(criteriaPath, body, { encoding: "utf-8", mode: 384 });
11749
11801
  } catch (err) {
11750
11802
  return Result.error(
11751
11803
  new StorageError({
@@ -11848,11 +11900,11 @@ function ideateAndPlanLeaf(useCase) {
11848
11900
 
11849
11901
  // src/application/chains/onboard/onboard-load-leaves.ts
11850
11902
  import { readFile as readFile9 } from "fs/promises";
11851
- import { join as join23 } from "path";
11903
+ import { join as join22 } from "path";
11852
11904
 
11853
11905
  // src/application/chains/onboard/onboard-persist-leaves.ts
11854
- import { mkdir as mkdir13, writeFile as writeFile9 } from "fs/promises";
11855
- import { dirname as dirname13, join as join22 } from "path";
11906
+ import { mkdir as mkdir13, writeFile as writeFile8 } from "fs/promises";
11907
+ import { dirname as dirname12, join as join21 } from "path";
11856
11908
  var HARNESS_MARKER_PREFIX = "<!-- ralphctl onboard:";
11857
11909
  function makeMarker(now = () => /* @__PURE__ */ new Date()) {
11858
11910
  return `${HARNESS_MARKER_PREFIX} ${now().toISOString()} -->`;
@@ -11865,14 +11917,14 @@ function writeContextFileLeaf(now = () => /* @__PURE__ */ new Date()) {
11865
11917
  if (proposals === void 0 || accepted === null || accepted === void 0 || accepted.length === 0) {
11866
11918
  return Result.ok(void 0);
11867
11919
  }
11868
- const targetPath = join22(repo.path, proposals.contextFilePath);
11920
+ const targetPath = join21(repo.path, proposals.contextFilePath);
11869
11921
  try {
11870
- await mkdir13(dirname13(targetPath), { recursive: true });
11922
+ await mkdir13(dirname12(targetPath), { recursive: true });
11871
11923
  const marker = makeMarker(now);
11872
11924
  const firstLine = accepted.split("\n", 1)[0] ?? "";
11873
11925
  const body = firstLine.startsWith(HARNESS_MARKER_PREFIX) ? accepted : `${marker}
11874
11926
  ${accepted}`;
11875
- await writeFile9(targetPath, body, "utf-8");
11927
+ await writeFile8(targetPath, body, "utf-8");
11876
11928
  return Result.ok(void 0);
11877
11929
  } catch (err) {
11878
11930
  return Result.error(
@@ -11944,7 +11996,7 @@ var PRE_EXISTING_CONTEXT_FILES = ["CLAUDE.md", ".github/copilot-instructions.md"
11944
11996
  async function findExistingContextFiles(repoPath) {
11945
11997
  const found = [];
11946
11998
  for (const relPath of PRE_EXISTING_CONTEXT_FILES) {
11947
- const fullPath = join23(repoPath, relPath);
11999
+ const fullPath = join22(repoPath, relPath);
11948
12000
  let body;
11949
12001
  try {
11950
12002
  body = await readFile9(fullPath, "utf-8");
@@ -12064,7 +12116,7 @@ function detectExistingFilesLeaf(deps) {
12064
12116
 
12065
12117
  // src/application/chains/onboard/onboard-ai-leaves.ts
12066
12118
  import { readFile as readFile10 } from "fs/promises";
12067
- import { join as join24 } from "path";
12119
+ import { join as join23 } from "path";
12068
12120
 
12069
12121
  // src/business/usecases/onboard/onboard-repo.ts
12070
12122
  function contextFilePathFor(provider) {
@@ -12221,7 +12273,7 @@ function runOnboardAiLeaf(deps) {
12221
12273
  await deps.aiSession.ensureReady();
12222
12274
  const provider = deps.aiSession.getProviderName();
12223
12275
  const fileName = contextFilePathFor(provider);
12224
- const targetPath = join24(input.repo.path, fileName);
12276
+ const targetPath = join23(input.repo.path, fileName);
12225
12277
  const detected = await detectModeAndBody(targetPath);
12226
12278
  const result = await useCase.execute({
12227
12279
  project: input.project,
@@ -12378,11 +12430,11 @@ function createOnboardFlow(deps, opts) {
12378
12430
 
12379
12431
  // src/application/chains/plan/plan-flow.ts
12380
12432
  import { mkdir as mkdir14 } from "fs/promises";
12381
- import { dirname as dirname15, join as join25 } from "path";
12433
+ import { dirname as dirname14, join as join24 } from "path";
12382
12434
 
12383
12435
  // src/business/usecases/plan/plan-sprint-tasks.ts
12384
12436
  import { readFile as readFile11 } from "fs/promises";
12385
- import { dirname as dirname14 } from "path";
12437
+ import { dirname as dirname13 } from "path";
12386
12438
  var PlanSprintTasksUseCase = class {
12387
12439
  constructor(ai, logger) {
12388
12440
  this.ai = ai;
@@ -12454,8 +12506,8 @@ var PlanSprintTasksUseCase = class {
12454
12506
  // the whole spec before the user sees any response.
12455
12507
  async runInteractive(input, wrapper, log) {
12456
12508
  const handover = input.runInTerminal ?? (async (fn) => fn());
12457
- const promptDir = dirname14(input.promptFilePath);
12458
- const outputDir = input.outputFilePath !== void 0 ? dirname14(input.outputFilePath) : promptDir;
12509
+ const promptDir = dirname13(input.promptFilePath);
12510
+ const outputDir = input.outputFilePath !== void 0 ? dirname13(input.outputFilePath) : promptDir;
12459
12511
  const repoArgs = buildAdditionalCwdArgs(input.additionalRepoPaths);
12460
12512
  const extraArgs = [...repoArgs, "--add-dir", promptDir];
12461
12513
  if (outputDir !== promptDir) {
@@ -12626,7 +12678,7 @@ function createPlanFlow(deps, opts) {
12626
12678
  if (!ctx.planningFolderRoot) {
12627
12679
  throw new Error("render-prompt-to-file: ctx.planningFolderRoot must be set by build-planning-folder");
12628
12680
  }
12629
- return AbsolutePath.trustString(join25(String(ctx.planningFolderRoot), "prompt.md"));
12681
+ return AbsolutePath.trustString(join24(String(ctx.planningFolderRoot), "prompt.md"));
12630
12682
  },
12631
12683
  buildPrompt: (ctx) => {
12632
12684
  if (!ctx.sprint) {
@@ -12748,7 +12800,7 @@ function planTasksLeaf(useCase, opts) {
12748
12800
  useCase: {
12749
12801
  async execute(input) {
12750
12802
  if (opts.outputFilePath !== void 0 && opts.outputFilePath !== "") {
12751
- await mkdir14(dirname15(opts.outputFilePath), { recursive: true });
12803
+ await mkdir14(dirname14(opts.outputFilePath), { recursive: true });
12752
12804
  }
12753
12805
  const result = await useCase.execute({
12754
12806
  sprint: input.sprint,
@@ -12912,11 +12964,11 @@ function assertAllTicketsApprovedLeaf() {
12912
12964
  }
12913
12965
 
12914
12966
  // src/application/chains/refine/refine-flow.ts
12915
- import { join as join26 } from "path";
12967
+ import { join as join25 } from "path";
12916
12968
 
12917
12969
  // src/business/usecases/refine/refine-single-ticket.ts
12918
12970
  import { readFile as readFile12 } from "fs/promises";
12919
- import { dirname as dirname16 } from "path";
12971
+ import { dirname as dirname15 } from "path";
12920
12972
  var RefineSingleTicketUseCase = class {
12921
12973
  constructor(ai, logger) {
12922
12974
  this.ai = ai;
@@ -12979,8 +13031,8 @@ var RefineSingleTicketUseCase = class {
12979
13031
  // scroll past before it responds.
12980
13032
  async runInteractive(input, wrapper, log) {
12981
13033
  const handover = input.runInTerminal ?? (async (fn) => fn());
12982
- const promptDir = dirname16(input.promptFilePath);
12983
- const outputDir = input.outputFilePath !== void 0 ? dirname16(input.outputFilePath) : promptDir;
13034
+ const promptDir = dirname15(input.promptFilePath);
13035
+ const outputDir = input.outputFilePath !== void 0 ? dirname15(input.outputFilePath) : promptDir;
12984
13036
  const addDirArgs = ["--add-dir", promptDir];
12985
13037
  if (outputDir !== promptDir) {
12986
13038
  addDirArgs.push("--add-dir", outputDir);
@@ -13193,7 +13245,7 @@ function buildPerTicketChain(deps, refineUseCase, ticket, opts) {
13193
13245
  if (!ctx.refinementUnitRoot) {
13194
13246
  throw new Error(`refine-${String(ticket.id)}: ctx.refinementUnitRoot must be set by build-refinement-unit`);
13195
13247
  }
13196
- return AbsolutePath.trustString(join26(String(ctx.refinementUnitRoot), "prompt.md"));
13248
+ return AbsolutePath.trustString(join25(String(ctx.refinementUnitRoot), "prompt.md"));
13197
13249
  },
13198
13250
  buildPrompt: (ctx) => {
13199
13251
  const outputFilePath = ctx.refinementRequirementsJsonPath !== void 0 ? String(ctx.refinementRequirementsJsonPath) : void 0;
@@ -14483,18 +14535,24 @@ function stepGlyph(status, spinnerFrame) {
14483
14535
  return /* @__PURE__ */ jsx19(Text17, { color: inkColors.muted, bold: true, children: glyphs.emDash });
14484
14536
  return /* @__PURE__ */ jsx19(Text17, { color: inkColors.muted, bold: true, children: glyphs.phasePending });
14485
14537
  }
14538
+ var MAX_RENDERED_STEPS = 50;
14486
14539
  function StepTrace({ steps, isRunning }) {
14487
14540
  const spinnerFrame = useSpinnerFrame();
14488
14541
  if (steps.length === 0) {
14489
14542
  if (isRunning) return /* @__PURE__ */ jsx19(Spinner, { label: "Starting\u2026" });
14490
14543
  return /* @__PURE__ */ jsx19(Text17, { dimColor: true, children: "No steps recorded." });
14491
14544
  }
14492
- return /* @__PURE__ */ jsx19(Box18, { flexDirection: "column", children: steps.map((step, i) => /* @__PURE__ */ jsxs18(Box18, { children: [
14493
- stepGlyph(step.status, spinnerFrame),
14494
- /* @__PURE__ */ jsx19(Text17, { bold: step.status === void 0, children: ` ${step.name}` }),
14495
- step.durationMs !== void 0 ? /* @__PURE__ */ jsx19(Text17, { dimColor: true, children: ` ${glyphs.inlineDot} ${durationLabel(step.durationMs)}` }) : null,
14496
- step.errorMessage ? /* @__PURE__ */ jsx19(Text17, { color: inkColors.error, children: ` ${glyphs.emDash} ${step.errorMessage}` }) : null
14497
- ] }, i)) });
14545
+ const visible = steps.length > MAX_RENDERED_STEPS ? steps.slice(-MAX_RENDERED_STEPS) : steps;
14546
+ const elided = steps.length - visible.length;
14547
+ return /* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", children: [
14548
+ elided > 0 ? /* @__PURE__ */ jsx19(Text17, { dimColor: true, children: `\u2026 ${String(elided)} earlier steps` }) : null,
14549
+ visible.map((step, i) => /* @__PURE__ */ jsxs18(Box18, { children: [
14550
+ stepGlyph(step.status, spinnerFrame),
14551
+ /* @__PURE__ */ jsx19(Text17, { bold: step.status === void 0, children: ` ${step.name}` }),
14552
+ step.durationMs !== void 0 ? /* @__PURE__ */ jsx19(Text17, { dimColor: true, children: ` ${glyphs.inlineDot} ${durationLabel(step.durationMs)}` }) : null,
14553
+ step.errorMessage ? /* @__PURE__ */ jsx19(Text17, { color: inkColors.error, children: ` ${glyphs.emDash} ${step.errorMessage}` }) : null
14554
+ ] }, i))
14555
+ ] });
14498
14556
  }
14499
14557
  function CompactStepSummary({ steps }) {
14500
14558
  if (steps.length === 0) return /* @__PURE__ */ jsx19(Text17, { dimColor: true, children: "No steps recorded." });
@@ -14974,6 +15032,7 @@ var EXECUTE_HINTS_TERMINAL = [
14974
15032
  function isTaskStep2(name) {
14975
15033
  return /^task-[a-zA-Z0-9_-]+$/.test(name);
14976
15034
  }
15035
+ var MAX_LIVE_STEPS = 200;
14977
15036
  function ExecuteView({ sessionId, sessionManager, signalBus }) {
14978
15037
  const router = useRouterOptional();
14979
15038
  const cancelInFlight = useRef2(false);
@@ -15043,12 +15102,14 @@ function ExecuteView({ sessionId, sessionManager, signalBus }) {
15043
15102
  durationMs: entry.durationMs,
15044
15103
  errorMessage: entry.error?.message
15045
15104
  };
15105
+ let next;
15046
15106
  if (idx >= 0) {
15047
- const next = [...prev];
15107
+ next = [...prev];
15048
15108
  next[idx] = settled;
15049
- return next;
15109
+ } else {
15110
+ next = [...prev, settled];
15050
15111
  }
15051
- return [...prev, settled];
15112
+ return next.length > MAX_LIVE_STEPS ? next.slice(-MAX_LIVE_STEPS) : next;
15052
15113
  });
15053
15114
  if (signalBus === void 0 || signalBus === null) {
15054
15115
  if (entry.stepName === "rate-limit-paused") setRateLimitVisible(true);
@@ -18542,13 +18603,13 @@ async function currentSprintReadableCheck(deps) {
18542
18603
  }
18543
18604
 
18544
18605
  // src/application/doctor/checks/data-dir-writable.ts
18545
- import { mkdir as mkdir15, unlink as unlink3, writeFile as writeFile10 } from "fs/promises";
18546
- import { join as join27 } from "path";
18606
+ import { mkdir as mkdir15, unlink as unlink3, writeFile as writeFile9 } from "fs/promises";
18607
+ import { join as join26 } from "path";
18547
18608
  async function dataDirWritableCheck(deps) {
18548
- const probe = join27(deps.storage.dataDir, `.doctor-write-${String(process.pid)}-${String(Date.now())}.tmp`);
18609
+ const probe = join26(deps.storage.dataDir, `.doctor-write-${String(process.pid)}-${String(Date.now())}.tmp`);
18549
18610
  try {
18550
18611
  await mkdir15(deps.storage.dataDir, { recursive: true });
18551
- await writeFile10(probe, "doctor", { encoding: "utf-8", mode: 384 });
18612
+ await writeFile9(probe, "doctor", { encoding: "utf-8", mode: 384 });
18552
18613
  } catch (err) {
18553
18614
  return {
18554
18615
  name: "Data directory",
@@ -18655,7 +18716,7 @@ function nodeVersionCheck() {
18655
18716
 
18656
18717
  // src/application/doctor/checks/onboarding-status.ts
18657
18718
  import { readFile as readFile13 } from "fs/promises";
18658
- import { join as join28 } from "path";
18719
+ import { join as join27 } from "path";
18659
18720
  var HARNESS_MARKER_PREFIX2 = "<!-- ralphctl onboard:";
18660
18721
  var MIN_HYBRID_PROSE_CHARS = 200;
18661
18722
  var CONTEXT_FILE_BY_PROVIDER = {
@@ -18676,7 +18737,7 @@ function extractPreamble(body) {
18676
18737
  return body.slice(0, idx);
18677
18738
  }
18678
18739
  async function classifyContextFile(repoPath, relPath) {
18679
- const fullPath = join28(repoPath, relPath);
18740
+ const fullPath = join27(repoPath, relPath);
18680
18741
  let body;
18681
18742
  try {
18682
18743
  body = await readFile13(fullPath, "utf-8");
@@ -18759,10 +18820,10 @@ async function onboardingStatusCheck(deps) {
18759
18820
 
18760
18821
  // src/application/doctor/checks/project-paths-exist.ts
18761
18822
  import { stat as stat5 } from "fs/promises";
18762
- import { join as join29 } from "path";
18823
+ import { join as join28 } from "path";
18763
18824
  async function isGitDir(path) {
18764
18825
  try {
18765
- const s = await stat5(join29(path, ".git"));
18826
+ const s = await stat5(join28(path, ".git"));
18766
18827
  return s.isDirectory() || s.isFile();
18767
18828
  } catch {
18768
18829
  return false;
@@ -18823,9 +18884,9 @@ async function projectPathsExistCheck(deps) {
18823
18884
  }
18824
18885
 
18825
18886
  // src/application/doctor/checks/session-log-path.ts
18826
- import { join as join30 } from "path";
18887
+ import { join as join29 } from "path";
18827
18888
  function sessionLogPathCheck(deps) {
18828
- const file = join30(deps.storage.logsDir, `${deps.sessionId}.jsonl`);
18889
+ const file = join29(deps.storage.logsDir, `${deps.sessionId}.jsonl`);
18829
18890
  return Promise.resolve({
18830
18891
  name: "Session log path",
18831
18892
  status: "pass",
@@ -18965,12 +19026,12 @@ function DoctorView() {
18965
19026
 
18966
19027
  // src/application/tui/views/browse/progress-view.tsx
18967
19028
  import { useEffect as useEffect39, useState as useState25 } from "react";
18968
- import { join as join32 } from "path";
19029
+ import { join as join31 } from "path";
18969
19030
  import { Box as Box34, Text as Text29 } from "ink";
18970
19031
 
18971
19032
  // src/business/usecases/sprint/show-progress.ts
18972
19033
  import { readFile as readFile14 } from "fs/promises";
18973
- import { join as join31 } from "path";
19034
+ import { join as join30 } from "path";
18974
19035
  var DEFAULT_STALE_THRESHOLD_HOURS = 24;
18975
19036
  var STALE_THRESHOLD_HOURS = DEFAULT_STALE_THRESHOLD_HOURS;
18976
19037
  var ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/;
@@ -19056,7 +19117,7 @@ function detectBranchInconsistency(sprint, projects, external) {
19056
19117
  return out;
19057
19118
  }
19058
19119
  var ShowProgressUseCase = class {
19059
- constructor(sprints, tasks, projects, external, readFileImpl = (path) => readFile14(path, "utf-8"), progressPathForSprint = (sprintId) => join31(process.env["RALPHCTL_ROOT"] ?? "", "data", "sprints", String(sprintId), "progress.md")) {
19120
+ constructor(sprints, tasks, projects, external, readFileImpl = (path) => readFile14(path, "utf-8"), progressPathForSprint = (sprintId) => join30(process.env["RALPHCTL_ROOT"] ?? "", "data", "sprints", String(sprintId), "progress.md")) {
19060
19121
  this.sprints = sprints;
19061
19122
  this.tasks = tasks;
19062
19123
  this.projects = projects;
@@ -19152,7 +19213,7 @@ function ProgressView() {
19152
19213
  deps.projectRepo,
19153
19214
  deps.external,
19154
19215
  void 0,
19155
- (id) => join32(String(deps.storage.sprintsDir), String(id), "progress.md")
19216
+ (id) => join31(String(deps.storage.sprintsDir), String(id), "progress.md")
19156
19217
  );
19157
19218
  const result = await uc.execute({ sprintId, now: IsoTimestamp.now() });
19158
19219
  if (cancel.current) return;
@@ -19255,14 +19316,14 @@ function Timeline({ report }) {
19255
19316
 
19256
19317
  // src/application/tui/views/crud/sprint-export-requirements-view.tsx
19257
19318
  import { useEffect as useEffect40 } from "react";
19258
- import { writeFile as writeFile11 } from "fs/promises";
19319
+ import { writeFile as writeFile10 } from "fs/promises";
19259
19320
  import { isAbsolute, resolve } from "path";
19260
19321
 
19261
19322
  // src/business/usecases/sprint/export-requirements.ts
19262
19323
  import { readFile as readFile15 } from "fs/promises";
19263
19324
  var ExportRequirementsUseCase = class {
19264
- constructor(writeFile15, readJsonFile2 = (p) => readFile15(p, "utf-8")) {
19265
- this.writeFile = writeFile15;
19325
+ constructor(writeFile14, readJsonFile2 = (p) => readFile15(p, "utf-8")) {
19326
+ this.writeFile = writeFile14;
19266
19327
  this.readJsonFile = readJsonFile2;
19267
19328
  }
19268
19329
  writeFile;
@@ -19335,7 +19396,7 @@ function SprintExportRequirementsView() {
19335
19396
  const finalPath = trimmed.length === 0 ? defaultPath : isAbsolute(trimmed) ? trimmed : resolve(process.cwd(), trimmed);
19336
19397
  setStep("Writing file\u2026");
19337
19398
  const aggregatePath = resolveStoragePaths().requirementsAggregateFile(sprintId);
19338
- const uc = new ExportRequirementsUseCase((p, b) => writeFile11(p, b, "utf-8"));
19399
+ const uc = new ExportRequirementsUseCase((p, b) => writeFile10(p, b, "utf-8"));
19339
19400
  const result = await uc.execute({
19340
19401
  aggregatePath,
19341
19402
  outputPath: AbsolutePath.trustString(finalPath)
@@ -19372,16 +19433,16 @@ function SprintExportRequirementsView() {
19372
19433
 
19373
19434
  // src/application/tui/views/crud/sprint-export-context-view.tsx
19374
19435
  import { useEffect as useEffect41 } from "react";
19375
- import { writeFile as writeFile12 } from "fs/promises";
19436
+ import { writeFile as writeFile11 } from "fs/promises";
19376
19437
  import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
19377
19438
 
19378
19439
  // src/business/usecases/sprint/export-context.ts
19379
19440
  var ExportContextUseCase = class {
19380
- constructor(sprints, tasks, projects, writeFile15) {
19441
+ constructor(sprints, tasks, projects, writeFile14) {
19381
19442
  this.sprints = sprints;
19382
19443
  this.tasks = tasks;
19383
19444
  this.projects = projects;
19384
- this.writeFile = writeFile15;
19445
+ this.writeFile = writeFile14;
19385
19446
  }
19386
19447
  sprints;
19387
19448
  tasks;
@@ -19543,7 +19604,7 @@ function SprintExportContextView() {
19543
19604
  deps.sprintRepo,
19544
19605
  deps.taskRepo,
19545
19606
  deps.projectRepo,
19546
- (p, b) => writeFile12(p, b, "utf-8")
19607
+ (p, b) => writeFile11(p, b, "utf-8")
19547
19608
  );
19548
19609
  const result = await uc.execute({
19549
19610
  sprintId,
@@ -20126,7 +20187,7 @@ async function handleCompletionRequest(program, deps) {
20126
20187
  // src/application/cli/commands/completion-install.ts
20127
20188
  import { appendFile as appendFile3, readFile as readFile16 } from "fs/promises";
20128
20189
  import { homedir as homedir2 } from "os";
20129
- import { join as join33 } from "path";
20190
+ import { join as join32 } from "path";
20130
20191
  import * as c from "colorette";
20131
20192
 
20132
20193
  // src/application/cli/exit-codes.ts
@@ -20236,11 +20297,11 @@ function rcFileForShell(shell) {
20236
20297
  const home = homedir2();
20237
20298
  switch (shell) {
20238
20299
  case "bash":
20239
- return join33(home, ".bashrc");
20300
+ return join32(home, ".bashrc");
20240
20301
  case "zsh":
20241
- return join33(home, ".zshrc");
20302
+ return join32(home, ".zshrc");
20242
20303
  case "fish":
20243
- return join33(home, ".config", "fish", "config.fish");
20304
+ return join32(home, ".config", "fish", "config.fish");
20244
20305
  }
20245
20306
  }
20246
20307
  function runCompletionShow(requestedShell) {
@@ -21131,12 +21192,12 @@ async function runSprintClose(deps, id) {
21131
21192
  }
21132
21193
 
21133
21194
  // src/application/cli/commands/sprint-context.ts
21134
- import { writeFile as writeFile14 } from "fs/promises";
21195
+ import { writeFile as writeFile13 } from "fs/promises";
21135
21196
  import { isAbsolute as isAbsolute4, resolve as resolve4 } from "path";
21136
21197
  import * as c16 from "colorette";
21137
21198
 
21138
21199
  // src/application/cli/commands/sprint-requirements.ts
21139
- import { writeFile as writeFile13 } from "fs/promises";
21200
+ import { writeFile as writeFile12 } from "fs/promises";
21140
21201
  import { isAbsolute as isAbsolute3, resolve as resolve3 } from "path";
21141
21202
  import * as c15 from "colorette";
21142
21203
  function attachSprintRequirements(group, deps) {
@@ -21153,7 +21214,7 @@ async function runSprintRequirements(deps, id, opts) {
21153
21214
  if (!sprintR.ok) return sprintR;
21154
21215
  const outPath = resolveOutputPath(opts.output, `${String(sprintR.value)}-requirements.md`);
21155
21216
  const aggregatePath = resolveStoragePaths().requirementsAggregateFile(sprintR.value);
21156
- const uc = new ExportRequirementsUseCase((path, body) => writeFile13(path, body, "utf-8"));
21217
+ const uc = new ExportRequirementsUseCase((path, body) => writeFile12(path, body, "utf-8"));
21157
21218
  return uc.execute({ aggregatePath, outputPath: outPath });
21158
21219
  },
21159
21220
  format: (_d, out) => `${c15.green("wrote")} ${String(out.path)} (${String(out.byteCount)} bytes)`
@@ -21220,7 +21281,7 @@ async function runSprintContext(deps, id, opts) {
21220
21281
  deps.sprintRepo,
21221
21282
  deps.taskRepo,
21222
21283
  deps.projectRepo,
21223
- (path, body) => writeFile14(path, body, "utf-8")
21284
+ (path, body) => writeFile13(path, body, "utf-8")
21224
21285
  );
21225
21286
  return uc.execute({ sprintId: sprintR.value, outputPath: outPath });
21226
21287
  },
@@ -21488,7 +21549,7 @@ async function runSprintPlan(deps, opts) {
21488
21549
  }
21489
21550
 
21490
21551
  // src/application/cli/commands/sprint-progress.ts
21491
- import { join as join34 } from "path";
21552
+ import { join as join33 } from "path";
21492
21553
  import * as c19 from "colorette";
21493
21554
  function attachSprintProgress(group, deps) {
21494
21555
  group.command("progress [id]").description("show sprint progress, blockers, stale tasks, and dependency cycles").option("--log", "print the full timeline only (no diagnostics summary)").option("--lines <n>", "cap the number of timeline entries shown", "50").action(async (id, opts) => {
@@ -21532,7 +21593,7 @@ async function runSprintProgress(deps, id, opts) {
21532
21593
  deps.projectRepo,
21533
21594
  deps.external,
21534
21595
  void 0,
21535
- (sprintId) => join34(String(deps.storage.sprintsDir), String(sprintId), "progress.md")
21596
+ (sprintId) => join33(String(deps.storage.sprintsDir), String(sprintId), "progress.md")
21536
21597
  );
21537
21598
  return uc.execute({ sprintId: idR.value, now: IsoTimestamp.now() });
21538
21599
  },
@@ -21749,7 +21810,10 @@ ${formatTicketsTable(sprint)}`
21749
21810
  // src/application/cli/commands/sprint-start.ts
21750
21811
  import * as c23 from "colorette";
21751
21812
  function attachSprintStart(group, deps) {
21752
- group.command("start").description("execute the active sprint").requiredOption("--sprint <id>", "sprint id").option("--cwd <abs>", "working directory for AI sessions", process.cwd()).option("--branch", "auto-generate sprint branch name (`ralphctl/<sprint-id>`)").option("--branch-name <name>", "use a custom branch name").option("--check-script <cmd>", "sprint-start check script").option("--no-commit", "do not auto-commit each task after the evaluator round").action(async (opts) => {
21813
+ group.command("start").description("execute the active sprint").requiredOption("--sprint <id>", "sprint id").option("--cwd <abs>", "working directory for AI sessions", process.cwd()).option("--branch", "auto-generate sprint branch name (`ralphctl/<sprint-id>`)").option("--branch-name <name>", "use a custom branch name").option(
21814
+ "--check-script <cmd>",
21815
+ "override the post-task check script for every task (otherwise auto-sourced from each repo)"
21816
+ ).option("--no-commit", "do not auto-commit each task after the evaluator round").action(async (opts) => {
21753
21817
  const code = await runSprintStart(deps, opts);
21754
21818
  if (code !== EXIT_SUCCESS) process.exitCode = code;
21755
21819
  });
@@ -22267,7 +22331,7 @@ async function runTicketRemove(deps, opts) {
22267
22331
  // package.json
22268
22332
  var package_default = {
22269
22333
  name: "ralphctl",
22270
- version: "0.6.1",
22334
+ version: "0.6.3",
22271
22335
  description: "Agent harness for long-running AI coding tasks \u2014 orchestrates Claude Code & GitHub Copilot across repositories",
22272
22336
  homepage: "https://github.com/lukas-grigis/ralphctl",
22273
22337
  type: "module",
@@ -22482,9 +22546,11 @@ function shouldAutoInvoke() {
22482
22546
  if (process.env["VITEST"] !== void 0) return false;
22483
22547
  const entry = process.argv[1];
22484
22548
  if (entry === void 0) return false;
22485
- const base = entry.split("/").pop() ?? "";
22486
- if (!/^(entrypoint|cli)\.(ts|mjs|js)$/.test(base)) return false;
22487
- return true;
22549
+ try {
22550
+ return pathToFileURL(realpathSync(entry)).href === import.meta.url;
22551
+ } catch {
22552
+ return false;
22553
+ }
22488
22554
  }
22489
22555
  if (shouldAutoInvoke()) {
22490
22556
  void main(process.argv).then((code) => {