ralphctl 0.6.2 → 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
@@ -212,8 +212,8 @@ async function directoryHasEntries(p) {
212
212
  try {
213
213
  const s = await stat(p);
214
214
  if (!s.isDirectory()) return false;
215
- const { readdir: readdir6 } = await import("fs/promises");
216
- const entries = await readdir6(p);
215
+ const { readdir: readdir5 } = await import("fs/promises");
216
+ const entries = await readdir5(p);
217
217
  return entries.length > 0;
218
218
  } catch {
219
219
  return false;
@@ -410,10 +410,9 @@ function renderEvaluateWorkspaceSection(workspaceDir) {
410
410
  "",
411
411
  "- `task.md` \u2014 the current task being evaluated (description, steps, verification criteria, status)",
412
412
  "- `requirements/<ticket-id>.md` \u2014 the refined requirements + raw ticket text that motivated this task",
413
- "- `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)",
414
414
  "- `project-context.md` \u2014 the project's CLAUDE.md / .github/copilot-instructions.md (when present in the target repo)",
415
- "- `dimensions.md` \u2014 the four floor dimensions plus any extra dimensions the planner emitted on this task",
416
- "- `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"
417
416
  ].join("\n");
418
417
  }
419
418
  function renderDoneCriteriaSection(doneCriteriaBullet) {
@@ -572,8 +571,8 @@ ${input.task.steps.map((s, i) => `${String(i + 1)}. ${s}`).join("\n")}` : "";
572
571
  ${input.task.verificationCriteria.map((c34) => `- ${c34}`).join("\n")}` : "";
573
572
  const branchLine = input.sprint.branch !== null ? `**Branch:** \`${input.sprint.branch}\`` : "";
574
573
  const checkScriptSection = renderCheckScriptSection(input.checkScript);
575
- const checkRanAtForRepo = input.sprint.checkRanAt.get(input.task.projectPath);
576
- 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.";
577
576
  const rendered = substitute(tpl.value, {
578
577
  TASK_NAME: input.task.name,
579
578
  TASK_ID: String(input.task.id),
@@ -915,8 +914,7 @@ function getAdapter(name) {
915
914
 
916
915
  // src/integration/persistence/session-md-writer.ts
917
916
  import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
918
- import { dirname as dirname2, join as join5 } from "path";
919
- import { readdir as readdir2 } from "fs/promises";
917
+ import { dirname as dirname2 } from "path";
920
918
  var FRONTMATTER_DELIM = "---";
921
919
  function renderFrontmatter(fields) {
922
920
  const lines = [FRONTMATTER_DELIM];
@@ -996,7 +994,8 @@ async function writeSessionFinish(args) {
996
994
  const fm2 = renderFrontmatter({
997
995
  finished: args.finished,
998
996
  exitCode: args.exitCode,
999
- sessionId: args.sessionId
997
+ sessionId: args.sessionId,
998
+ model: args.model
1000
999
  });
1001
1000
  return writeFileSafe(args.path, `${fm2}
1002
1001
 
@@ -1010,6 +1009,7 @@ _(no prompt recorded \u2014 session finish without start)_
1010
1009
  merged["finished"] = args.finished;
1011
1010
  merged["exitCode"] = args.exitCode;
1012
1011
  if (args.sessionId !== void 0) merged["sessionId"] = args.sessionId;
1012
+ if (args.model !== void 0) merged["model"] = args.model;
1013
1013
  const fm = renderFrontmatter(merged);
1014
1014
  const content = `${fm}
1015
1015
 
@@ -1056,22 +1056,6 @@ function unquote(s) {
1056
1056
  }
1057
1057
  return s;
1058
1058
  }
1059
- async function nextSessionPath(unitDir) {
1060
- let entries;
1061
- try {
1062
- entries = await readdir2(unitDir);
1063
- } catch {
1064
- return join5(unitDir, "session-1.md");
1065
- }
1066
- let max = 0;
1067
- for (const entry of entries) {
1068
- const m = /^session-(\d+)\.md$/.exec(entry);
1069
- if (!m) continue;
1070
- const n = Number(m[1]);
1071
- if (Number.isFinite(n) && n > max) max = n;
1072
- }
1073
- return join5(unitDir, `session-${String(max + 1)}.md`);
1074
- }
1075
1059
 
1076
1060
  // src/integration/ai/session/session-runner.ts
1077
1061
  import { spawn } from "child_process";
@@ -1367,26 +1351,28 @@ var ProviderAiSessionAdapter = class {
1367
1351
  * The actual provider exit code isn't surfaced through the runner's
1368
1352
  * typed Result — `1` is a faithful "spawn failed" stand-in for audit
1369
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.
1370
1358
  */
1371
1359
  async writeSessionMdFinishHeadless(options, result) {
1372
1360
  const path = options.sessionMdPath;
1373
1361
  if (path === void 0) return;
1374
1362
  const sessionId = result.ok ? result.value.sessionId : void 0;
1363
+ const model = result.ok ? result.value.model : void 0;
1375
1364
  const written = await writeSessionFinish({
1376
1365
  path: String(path),
1377
1366
  finished: (/* @__PURE__ */ new Date()).toISOString(),
1378
1367
  exitCode: result.ok ? 0 : 1,
1379
- ...sessionId !== void 0 ? { sessionId } : {}
1368
+ ...sessionId !== void 0 ? { sessionId } : {},
1369
+ ...model !== void 0 ? { model } : {}
1380
1370
  });
1381
1371
  if (!written.ok) {
1382
1372
  this.opts.logger?.warn("failed to write session.md (finish) \u2014 spawn already settled", {
1383
1373
  path: String(path),
1384
1374
  error: written.error.message
1385
1375
  });
1386
- return;
1387
- }
1388
- if (result.ok && result.value.model !== void 0) {
1389
- await this.patchSessionMdModel(String(path), result.value.model);
1390
1376
  }
1391
1377
  }
1392
1378
  /**
@@ -1409,33 +1395,6 @@ var ProviderAiSessionAdapter = class {
1409
1395
  });
1410
1396
  }
1411
1397
  }
1412
- /**
1413
- * Patch the resolved `model` into an existing `session.md`. Reuses
1414
- * `writeSessionFinish` to round-trip frontmatter without touching the
1415
- * prompt body. Best-effort — write failures log a warn.
1416
- *
1417
- * Implementation note: `writeSessionFinish` was scoped to the
1418
- * standard finish fields only. The narrow `model` patch here goes
1419
- * through `writeSessionStart` would clobber the body, so we re-read,
1420
- * splice the field, and re-emit via the same writer surface. This
1421
- * keeps the YAML-handling logic in one place (`session-md-writer`)
1422
- * even at the cost of a second IO round-trip — the audit pack is
1423
- * cold-path and we'd rather centralise format knowledge.
1424
- */
1425
- async patchSessionMdModel(path, model) {
1426
- try {
1427
- const fs = await import("fs/promises");
1428
- const existing = await fs.readFile(path, "utf-8");
1429
- const patched = upsertFrontmatterField(existing, "model", model);
1430
- if (patched === existing) return;
1431
- await fs.writeFile(path, patched, { encoding: "utf-8", mode: 384 });
1432
- } catch (err) {
1433
- this.opts.logger?.warn("failed to patch session.md model field", {
1434
- path,
1435
- error: err instanceof Error ? err.message : String(err)
1436
- });
1437
- }
1438
- }
1439
1398
  /**
1440
1399
  * Build the flag list audited into session.md. Headless: the runner
1441
1400
  * appends `--resume <id>` when `resumeSessionId` is set, so we mirror
@@ -1482,28 +1441,6 @@ function stripTrailingPromptSlot(args) {
1482
1441
  if (end > 0 && args[end - 1] === "--") end -= 1;
1483
1442
  return args.slice(0, end);
1484
1443
  }
1485
- function upsertFrontmatterField(content, key, value) {
1486
- const lines = content.split("\n");
1487
- if (lines[0]?.trim() !== "---") return content;
1488
- let closeIdx = -1;
1489
- for (let i = 1; i < lines.length; i += 1) {
1490
- if (lines[i]?.trim() === "---") {
1491
- closeIdx = i;
1492
- break;
1493
- }
1494
- }
1495
- if (closeIdx < 0) return content;
1496
- const keyRegex = new RegExp(`^${key}\\s*:`);
1497
- for (let i = 1; i < closeIdx; i += 1) {
1498
- const line = lines[i] ?? "";
1499
- if (keyRegex.test(line.trim())) {
1500
- lines[i] = `${key}: ${value}`;
1501
- return lines.join("\n");
1502
- }
1503
- }
1504
- lines.splice(closeIdx, 0, `${key}: ${value}`);
1505
- return lines.join("\n");
1506
- }
1507
1444
 
1508
1445
  // src/integration/ai/session/process-runner.ts
1509
1446
  import { spawn as spawn2 } from "child_process";
@@ -1605,20 +1542,20 @@ var NodeProcessRunner = class {
1605
1542
 
1606
1543
  // src/integration/ai/skills/bundled-skills-copier.ts
1607
1544
  import { existsSync as existsSync3 } from "fs";
1608
- import { readdir as readdir4, rm, rmdir } from "fs/promises";
1609
- 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";
1610
1547
  import { fileURLToPath as fileURLToPath2 } from "url";
1611
1548
 
1612
1549
  // src/integration/ai/skills/copy-tree.ts
1613
- import { copyFile, mkdir as mkdir2, readdir as readdir3, stat as stat2 } from "fs/promises";
1614
- 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";
1615
1552
  async function copyTree(src, dst) {
1616
1553
  try {
1617
1554
  await mkdir2(dst, { recursive: true });
1618
- const dirents = await readdir3(src, { withFileTypes: true });
1555
+ const dirents = await readdir2(src, { withFileTypes: true });
1619
1556
  for (const d of dirents) {
1620
- const s = join6(src, d.name);
1621
- const t = join6(dst, d.name);
1557
+ const s = join5(src, d.name);
1558
+ const t = join5(dst, d.name);
1622
1559
  if (d.isDirectory()) {
1623
1560
  const r = await copyTree(s, t);
1624
1561
  if (!r.ok) return r;
@@ -1654,12 +1591,12 @@ async function copyTree(src, dst) {
1654
1591
  // src/integration/ai/skills/skill-git-exclude.ts
1655
1592
  import { existsSync as existsSync2 } from "fs";
1656
1593
  import { mkdir as mkdir3, readFile as readFile3, stat as stat3, writeFile as writeFile2 } from "fs/promises";
1657
- import { join as join7 } from "path";
1594
+ import { join as join6 } from "path";
1658
1595
  var BEGIN_MARKER = "# >>> ralphctl-managed-skills (do not edit) >>>";
1659
1596
  var END_MARKER = "# <<< ralphctl-managed-skills <<<";
1660
1597
  var EXCLUDE_PATTERNS = [".claude/skills/"];
1661
1598
  async function gitInfoDir(cwd) {
1662
- const dotGit = join7(cwd, ".git");
1599
+ const dotGit = join6(cwd, ".git");
1663
1600
  if (!existsSync2(dotGit)) return null;
1664
1601
  try {
1665
1602
  const s = await stat3(dotGit);
@@ -1667,12 +1604,12 @@ async function gitInfoDir(cwd) {
1667
1604
  } catch {
1668
1605
  return null;
1669
1606
  }
1670
- return join7(dotGit, "info");
1607
+ return join6(dotGit, "info");
1671
1608
  }
1672
1609
  async function addRalphctlSkillsExclude(cwd) {
1673
1610
  const infoDir = await gitInfoDir(cwd);
1674
1611
  if (infoDir === null) return Result.ok();
1675
- const excludeFile = join7(infoDir, "exclude");
1612
+ const excludeFile = join6(infoDir, "exclude");
1676
1613
  try {
1677
1614
  await mkdir3(infoDir, { recursive: true });
1678
1615
  const existing = existsSync2(excludeFile) ? await readFile3(excludeFile, "utf8") : "";
@@ -1699,7 +1636,7 @@ ${block}`;
1699
1636
  async function removeRalphctlSkillsExclude(cwd) {
1700
1637
  const infoDir = await gitInfoDir(cwd);
1701
1638
  if (infoDir === null) return Result.ok();
1702
- const excludeFile = join7(infoDir, "exclude");
1639
+ const excludeFile = join6(infoDir, "exclude");
1703
1640
  if (!existsSync2(excludeFile)) return Result.ok();
1704
1641
  try {
1705
1642
  const body = await readFile3(excludeFile, "utf8");
@@ -1737,10 +1674,10 @@ function stripMarkerBlock(body) {
1737
1674
  }
1738
1675
 
1739
1676
  // src/integration/ai/skills/bundled-skills-copier.ts
1740
- var SKILLS_SUBDIR = join8(".claude", "skills");
1677
+ var SKILLS_SUBDIR = join7(".claude", "skills");
1741
1678
  var HERE2 = dirname3(fileURLToPath2(import.meta.url));
1742
1679
  function bundledSkillsRootDir() {
1743
- const distRoot = join8(HERE2, "skills");
1680
+ const distRoot = join7(HERE2, "skills");
1744
1681
  if (existsSync3(distRoot)) return AbsolutePath.trustString(distRoot);
1745
1682
  return AbsolutePath.trustString(HERE2);
1746
1683
  }
@@ -1756,12 +1693,12 @@ var FileBundledSkillsCopier = class {
1756
1693
  this.bundledRootDir = opts.bundledRootDir ?? bundledSkillsRootDir();
1757
1694
  }
1758
1695
  async install(sessionDir, phase) {
1759
- const skillsDir = join8(sessionDir, SKILLS_SUBDIR);
1696
+ const skillsDir = join7(sessionDir, SKILLS_SUBDIR);
1760
1697
  const sources = await this.collectSourceSkills(phase);
1761
1698
  if (!sources.ok) return Result.error(sources.error);
1762
1699
  const tracked = this.installed.get(String(sessionDir)) ?? /* @__PURE__ */ new Set();
1763
1700
  for (const [name, srcDir] of sources.value) {
1764
- const dst = join8(skillsDir, name);
1701
+ const dst = join7(skillsDir, name);
1765
1702
  if (existsSync3(dst)) {
1766
1703
  continue;
1767
1704
  }
@@ -1788,10 +1725,10 @@ var FileBundledSkillsCopier = class {
1788
1725
  await removeRalphctlSkillsExclude(sessionDir);
1789
1726
  return Result.ok();
1790
1727
  }
1791
- const skillsDir = join8(sessionDir, SKILLS_SUBDIR);
1728
+ const skillsDir = join7(sessionDir, SKILLS_SUBDIR);
1792
1729
  try {
1793
1730
  for (const name of tracked) {
1794
- await rm(join8(skillsDir, name), { recursive: true, force: true });
1731
+ await rm(join7(skillsDir, name), { recursive: true, force: true });
1795
1732
  }
1796
1733
  this.installed.delete(key);
1797
1734
  } catch (err) {
@@ -1805,7 +1742,7 @@ var FileBundledSkillsCopier = class {
1805
1742
  );
1806
1743
  }
1807
1744
  await tryRmdirIfEmpty(skillsDir);
1808
- await tryRmdirIfEmpty(join8(sessionDir, ".claude"));
1745
+ await tryRmdirIfEmpty(join7(sessionDir, ".claude"));
1809
1746
  await removeRalphctlSkillsExclude(sessionDir);
1810
1747
  return Result.ok();
1811
1748
  }
@@ -1819,7 +1756,7 @@ var FileBundledSkillsCopier = class {
1819
1756
  async collectSourceSkills(phase) {
1820
1757
  const sources = /* @__PURE__ */ new Map();
1821
1758
  for (const sub of ["default", phase]) {
1822
- const dir = join8(this.bundledRootDir, sub);
1759
+ const dir = join7(this.bundledRootDir, sub);
1823
1760
  const r = await listSkillDirs(dir);
1824
1761
  if (!r.ok) return Result.error(r.error);
1825
1762
  for (const [name, abs] of r.value) {
@@ -1832,10 +1769,10 @@ var FileBundledSkillsCopier = class {
1832
1769
  async function listSkillDirs(dir) {
1833
1770
  if (!existsSync3(dir)) return Result.ok([]);
1834
1771
  try {
1835
- const dirents = await readdir4(dir, { withFileTypes: true });
1772
+ const dirents = await readdir3(dir, { withFileTypes: true });
1836
1773
  const out = [];
1837
1774
  for (const d of dirents) {
1838
- if (d.isDirectory()) out.push([d.name, join8(dir, d.name)]);
1775
+ if (d.isDirectory()) out.push([d.name, join7(dir, d.name)]);
1839
1776
  }
1840
1777
  return Result.ok(out);
1841
1778
  } catch (err) {
@@ -1852,7 +1789,7 @@ async function listSkillDirs(dir) {
1852
1789
  async function tryRmdirIfEmpty(dir) {
1853
1790
  if (!existsSync3(dir)) return;
1854
1791
  try {
1855
- const entries = await readdir4(dir);
1792
+ const entries = await readdir3(dir);
1856
1793
  if (entries.length === 0) await rmdir(dir);
1857
1794
  } catch {
1858
1795
  }
@@ -2156,7 +2093,12 @@ var DefaultExternalAdapter = class {
2156
2093
  formatIssueContext(issue) {
2157
2094
  return this.issues.format(issue);
2158
2095
  }
2159
- // --- 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
+ }
2160
2102
  async runCheckScript(projectPath, script, phase, timeout) {
2161
2103
  const r = await this.checkScripts.run(projectPath, script, phase, timeout);
2162
2104
  if (r.ok) return r.value;
@@ -2932,9 +2874,9 @@ var JsonLogger = class _JsonLogger {
2932
2874
 
2933
2875
  // src/integration/logging/jsonl-file-writer.ts
2934
2876
  import { appendFile, mkdir as mkdir4 } from "fs/promises";
2935
- import { dirname as dirname4, join as join9 } from "path";
2877
+ import { dirname as dirname4, join as join8 } from "path";
2936
2878
  function fileFor(opts) {
2937
- return AbsolutePath.trustString(join9(opts.logsDir, `${opts.sessionId}.jsonl`));
2879
+ return AbsolutePath.trustString(join8(opts.logsDir, `${opts.sessionId}.jsonl`));
2938
2880
  }
2939
2881
  var JsonlFileWriter = class {
2940
2882
  constructor(opts) {
@@ -3304,7 +3246,7 @@ var NotFoundError = class extends Error {
3304
3246
 
3305
3247
  // src/integration/persistence/json-io.ts
3306
3248
  import { mkdir as mkdir6, readFile as readFile5, rename, writeFile as writeFile4 } from "fs/promises";
3307
- import { dirname as dirname6, join as join10 } from "path";
3249
+ import { dirname as dirname6, join as join9 } from "path";
3308
3250
  async function readJsonFile(path, schema2) {
3309
3251
  let raw;
3310
3252
  try {
@@ -3362,7 +3304,7 @@ ${issues}`,
3362
3304
  );
3363
3305
  }
3364
3306
  const dir = dirname6(path);
3365
- 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`);
3366
3308
  try {
3367
3309
  await mkdir6(dir, { recursive: true });
3368
3310
  await writeFile4(tmp, JSON.stringify(validated.data, null, 2) + "\n", {
@@ -4038,7 +3980,7 @@ function errnoCode3(err) {
4038
3980
  }
4039
3981
 
4040
3982
  // src/integration/persistence/file-sprint-repository.ts
4041
- 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";
4042
3984
 
4043
3985
  // src/integration/persistence/schemas/sprint-schema.ts
4044
3986
  import { z as z3 } from "zod";
@@ -4139,7 +4081,12 @@ var Sprint = class _Sprint {
4139
4081
  activatedAt;
4140
4082
  closedAt;
4141
4083
  tickets;
4142
- 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;
4143
4090
  branch;
4144
4091
  /**
4145
4092
  * Pull / merge request URL recorded after `sprint create-pr` runs.
@@ -4167,7 +4114,7 @@ var Sprint = class _Sprint {
4167
4114
  this.activatedAt = props.activatedAt;
4168
4115
  this.closedAt = props.closedAt;
4169
4116
  this.tickets = props.tickets;
4170
- this.checkRanAt = props.checkRanAt;
4117
+ this.setupRanAt = props.setupRanAt;
4171
4118
  this.branch = props.branch;
4172
4119
  this.pullRequestUrl = props.pullRequestUrl;
4173
4120
  this.projectName = props.projectName;
@@ -4194,7 +4141,7 @@ var Sprint = class _Sprint {
4194
4141
  activatedAt: null,
4195
4142
  closedAt: null,
4196
4143
  tickets: [],
4197
- checkRanAt: /* @__PURE__ */ new Map(),
4144
+ setupRanAt: /* @__PURE__ */ new Map(),
4198
4145
  branch: null,
4199
4146
  pullRequestUrl: null,
4200
4147
  projectName: input.projectName,
@@ -4231,7 +4178,7 @@ var Sprint = class _Sprint {
4231
4178
  this.with({
4232
4179
  status: "closed",
4233
4180
  closedAt: now,
4234
- checkRanAt: /* @__PURE__ */ new Map()
4181
+ setupRanAt: /* @__PURE__ */ new Map()
4235
4182
  })
4236
4183
  );
4237
4184
  }
@@ -4270,7 +4217,7 @@ var Sprint = class _Sprint {
4270
4217
  activatedAt: this.activatedAt,
4271
4218
  closedAt: this.closedAt,
4272
4219
  tickets: this.tickets,
4273
- checkRanAt: this.checkRanAt,
4220
+ setupRanAt: this.setupRanAt,
4274
4221
  branch: this.branch,
4275
4222
  pullRequestUrl: this.pullRequestUrl,
4276
4223
  projectName: this.projectName,
@@ -4366,13 +4313,13 @@ var Sprint = class _Sprint {
4366
4313
  return Result8.ok(this.with({ branch }));
4367
4314
  }
4368
4315
  /**
4369
- * 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
4370
4317
  * this audit trail and the entity should not gate it.
4371
4318
  */
4372
- recordCheckRun(repo, at) {
4373
- const next = new Map(this.checkRanAt);
4319
+ recordSetupRun(repo, at) {
4320
+ const next = new Map(this.setupRanAt);
4374
4321
  next.set(repo, at);
4375
- return this.with({ checkRanAt: next });
4322
+ return this.with({ setupRanAt: next });
4376
4323
  }
4377
4324
  /**
4378
4325
  * Record the pull / merge request URL published for the sprint branch.
@@ -4458,7 +4405,7 @@ var Sprint = class _Sprint {
4458
4405
  activatedAt: "activatedAt" in partial ? partial.activatedAt ?? null : this.activatedAt,
4459
4406
  closedAt: "closedAt" in partial ? partial.closedAt ?? null : this.closedAt,
4460
4407
  tickets: partial.tickets ?? this.tickets,
4461
- checkRanAt: partial.checkRanAt ?? this.checkRanAt,
4408
+ setupRanAt: partial.setupRanAt ?? this.setupRanAt,
4462
4409
  branch: "branch" in partial ? partial.branch ?? null : this.branch,
4463
4410
  pullRequestUrl: "pullRequestUrl" in partial ? partial.pullRequestUrl ?? null : this.pullRequestUrl,
4464
4411
  projectName: this.projectName,
@@ -4642,7 +4589,13 @@ var sprintJsonSchema = z3.object({
4642
4589
  // to `[]` on a fresh draft sprint, populated post-plan).
4643
4590
  affectedRepositories: z3.array(z3.string()),
4644
4591
  // `Map<AbsolutePath, IsoTimestamp>` — serialised as a plain object map.
4645
- 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({}),
4646
4599
  tickets: z3.array(ticketJsonSchema)
4647
4600
  });
4648
4601
  var SYNTHETIC_SLUG_R = Slug.parse("rehydrate");
@@ -4655,9 +4608,9 @@ function toSprint(parsed) {
4655
4608
  if (!r.ok) return Result.error(r.error);
4656
4609
  tickets.push(r.value);
4657
4610
  }
4658
- const checkRanAt = /* @__PURE__ */ new Map();
4659
- for (const [k, v] of Object.entries(parsed.checkRanAt)) {
4660
- 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));
4661
4614
  }
4662
4615
  const created = Sprint.create({
4663
4616
  id: SprintId.trustString(parsed.id),
@@ -4692,8 +4645,8 @@ function toSprint(parsed) {
4692
4645
  const r = s.close(IsoTimestamp.trustString(at));
4693
4646
  if (r.ok) s = r.value;
4694
4647
  }
4695
- for (const [path, at] of checkRanAt) {
4696
- s = s.recordCheckRun(path, at);
4648
+ for (const [path, at] of setupRanAt) {
4649
+ s = s.recordSetupRun(path, at);
4697
4650
  }
4698
4651
  if (parsed.branch !== null) {
4699
4652
  const r = s.setBranch(parsed.branch);
@@ -4706,9 +4659,9 @@ function toSprint(parsed) {
4706
4659
  return Result.ok(s);
4707
4660
  }
4708
4661
  function fromSprint(sprint) {
4709
- const checkRanAt = {};
4710
- for (const [k, v] of sprint.checkRanAt) {
4711
- checkRanAt[k] = v;
4662
+ const setupRanAt = {};
4663
+ for (const [k, v] of sprint.setupRanAt) {
4664
+ setupRanAt[k] = v;
4712
4665
  }
4713
4666
  return {
4714
4667
  id: sprint.id,
@@ -4721,7 +4674,7 @@ function fromSprint(sprint) {
4721
4674
  pullRequestUrl: sprint.pullRequestUrl,
4722
4675
  projectName: sprint.projectName,
4723
4676
  affectedRepositories: [...sprint.affectedRepositories],
4724
- checkRanAt,
4677
+ setupRanAt,
4725
4678
  tickets: sprint.tickets.map(fromTicket)
4726
4679
  };
4727
4680
  }
@@ -4809,7 +4762,7 @@ var FileSprintRepository = class {
4809
4762
  async list() {
4810
4763
  let entries;
4811
4764
  try {
4812
- const dirents = await readdir5(this.paths.sprintsDir, {
4765
+ const dirents = await readdir4(this.paths.sprintsDir, {
4813
4766
  withFileTypes: true
4814
4767
  });
4815
4768
  entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
@@ -5498,8 +5451,8 @@ var FileWriteContextFileAdapter = class {
5498
5451
  };
5499
5452
 
5500
5453
  // src/integration/persistence/execution-unit-builder.ts
5501
- import { readFile as readFile6, rm as rm4 } from "fs/promises";
5502
- import { join as join12 } from "path";
5454
+ import { readFile as readFile6 } from "fs/promises";
5455
+ import { join as join11 } from "path";
5503
5456
 
5504
5457
  // src/integration/persistence/unit-slug.ts
5505
5458
  function unitSlug(id, name) {
@@ -5516,10 +5469,10 @@ function toSlug(name) {
5516
5469
 
5517
5470
  // src/integration/persistence/session-folder-helpers.ts
5518
5471
  import { cp, mkdir as mkdir10, rm as rm3, writeFile as writeFile6 } from "fs/promises";
5519
- 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";
5520
5473
  function contextFileFor(provider) {
5521
5474
  if (provider === "claude") return { path: "CLAUDE.md", needsGithubDir: false };
5522
- return { path: join11(".github", "copilot-instructions.md"), needsGithubDir: true };
5475
+ return { path: join10(".github", "copilot-instructions.md"), needsGithubDir: true };
5523
5476
  }
5524
5477
  function renderContextFile(args) {
5525
5478
  const { sprint, phase, affectedRepos, copilot } = args;
@@ -5541,7 +5494,7 @@ function renderInputsLine(phase) {
5541
5494
  return "- Input: `./ticket.md` \u2014 the seed idea. Write the proposed sprint output to `./output.json`.\n";
5542
5495
  if (phase === "plan")
5543
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";
5544
- 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";
5545
5498
  }
5546
5499
  function renderRepoLine(phase, affectedRepos, copilot) {
5547
5500
  if (phase === "refine" || phase === "ideate")
@@ -5651,7 +5604,7 @@ async function mirrorRepo(src, dst) {
5651
5604
  async function writeContextFile(args) {
5652
5605
  const { path, needsGithubDir } = contextFileFor(args.provider);
5653
5606
  if (needsGithubDir) {
5654
- const ensure = await ensureDirSafe(join11(args.root, ".github"));
5607
+ const ensure = await ensureDirSafe(join10(args.root, ".github"));
5655
5608
  if (!ensure.ok) return Result.error(ensure.error);
5656
5609
  }
5657
5610
  const body = renderContextFile({
@@ -5660,7 +5613,7 @@ async function writeContextFile(args) {
5660
5613
  affectedRepos: args.affectedRepos,
5661
5614
  copilot: args.provider === "copilot"
5662
5615
  });
5663
- return writeFileSafe2(join11(args.root, path), body);
5616
+ return writeFileSafe2(join10(args.root, path), body);
5664
5617
  }
5665
5618
 
5666
5619
  // src/integration/persistence/execution-unit-builder.ts
@@ -5723,7 +5676,7 @@ function renderTaskInput(task) {
5723
5676
  }
5724
5677
  return lines.join("\n");
5725
5678
  }
5726
- function renderTasksList(tasks) {
5679
+ function renderTasksList(tasks, priorEvaluations) {
5727
5680
  const ordered = [...tasks].sort((a, b) => a.order - b.order);
5728
5681
  const lines = ["# Task plan", ""];
5729
5682
  for (const t of ordered) {
@@ -5747,6 +5700,10 @@ function renderTasksList(tasks) {
5747
5700
  for (const c34 of t.verificationCriteria) lines.push(`- ${c34}`);
5748
5701
  lines.push("");
5749
5702
  }
5703
+ const priorEval = priorEvaluations.get(t.id);
5704
+ if (priorEval !== void 0 && priorEval.length > 0) {
5705
+ lines.push("### Evaluator output", "", "```text", priorEval, "```", "");
5706
+ }
5750
5707
  }
5751
5708
  return lines.join("\n");
5752
5709
  }
@@ -5777,7 +5734,7 @@ function renderDimensions(task) {
5777
5734
  return lines.join("\n");
5778
5735
  }
5779
5736
  async function readProjectContext(repoPath, provider) {
5780
- 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");
5781
5738
  try {
5782
5739
  const body = await readFile6(target, "utf-8");
5783
5740
  return [`<!-- copied from ${target} -->`, "", body].join("\n");
@@ -5792,59 +5749,20 @@ async function readProjectContext(repoPath, provider) {
5792
5749
  ].join("\n");
5793
5750
  }
5794
5751
  }
5795
- function serialiseTasks(tasks) {
5796
- return [...tasks].sort((a, b) => a.order - b.order).map((t) => ({
5797
- id: t.id,
5798
- name: t.name,
5799
- description: t.description,
5800
- steps: t.steps,
5801
- verificationCriteria: t.verificationCriteria,
5802
- status: t.status,
5803
- order: t.order,
5804
- ticketId: t.ticketId,
5805
- blockedBy: t.blockedBy,
5806
- projectPath: t.projectPath,
5807
- extraDimensions: t.extraDimensions
5808
- }));
5809
- }
5810
5752
  async function writeExecutionVolatile(args) {
5811
- 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));
5812
5754
  if (!taskMd.ok) return Result.error(taskMd.error);
5813
- 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));
5814
5756
  if (!tasksMd.ok) return Result.error(tasksMd.error);
5815
- const tasksJson = await writeFileSafe2(
5816
- join12(args.root, "tasks.json"),
5817
- JSON.stringify(serialiseTasks(args.tasks), null, 2)
5818
- );
5819
- if (!tasksJson.ok) return Result.error(tasksJson.error);
5820
5757
  const projectContext = await readProjectContext(args.task.projectPath, args.aiProvider);
5821
- const projectCtxFile = await writeFileSafe2(join12(args.root, "project-context.md"), projectContext);
5758
+ const projectCtxFile = await writeFileSafe2(join11(args.root, "project-context.md"), projectContext);
5822
5759
  if (!projectCtxFile.ok) return Result.error(projectCtxFile.error);
5823
- const evaluationsDir = join12(args.root, "evaluations");
5824
- try {
5825
- await rm4(evaluationsDir, { recursive: true, force: true });
5826
- } catch (err) {
5827
- return Result.error(
5828
- new StorageError({
5829
- subCode: "io",
5830
- message: `failed to clear ${evaluationsDir}: ${err instanceof Error ? err.message : String(err)}`,
5831
- path: evaluationsDir,
5832
- cause: err
5833
- })
5834
- );
5835
- }
5836
- const ensureEvals = await ensureDirSafe(evaluationsDir);
5837
- if (!ensureEvals.ok) return Result.error(ensureEvals.error);
5838
- for (const [taskId, body] of args.priorEvaluations) {
5839
- const w = await writeFileSafe2(join12(evaluationsDir, `${taskId}.md`), body);
5840
- if (!w.ok) return Result.error(w.error);
5841
- }
5842
5760
  return Result.ok();
5843
5761
  }
5844
5762
  async function buildExecutionUnit(storage2, input) {
5845
5763
  const slug = unitSlug(String(input.task.id), input.task.name);
5846
5764
  const root = storage2.executionUnitDir(input.sprint.id, slug);
5847
- const requirementsDir = AbsolutePath.trustString(join12(root, "requirements"));
5765
+ const requirementsDir = AbsolutePath.trustString(join11(root, "requirements"));
5848
5766
  const ensure = await ensureDirSafe(requirementsDir);
5849
5767
  if (!ensure.ok) return Result.error(ensure.error);
5850
5768
  const ctx = await writeContextFile({
@@ -5856,14 +5774,14 @@ async function buildExecutionUnit(storage2, input) {
5856
5774
  });
5857
5775
  if (!ctx.ok) return Result.error(ctx.error);
5858
5776
  for (const ticket of input.sprint.tickets) {
5859
- const filePath = join12(requirementsDir, `${ticket.id}.md`);
5777
+ const filePath = join11(requirementsDir, `${ticket.id}.md`);
5860
5778
  const w = await writeFileSafe2(filePath, renderRequirementsInput(ticket));
5861
5779
  if (!w.ok) return Result.error(w.error);
5862
5780
  }
5863
- const dims = await writeFileSafe2(join12(root, "dimensions.md"), renderDimensions(input.task));
5781
+ const dims = await writeFileSafe2(join11(root, "dimensions.md"), renderDimensions(input.task));
5864
5782
  if (!dims.ok) return Result.error(dims.error);
5865
5783
  const sprintCriteriaPath = String(storage2.doneCriteriaFile(input.sprint.id));
5866
- const unitCriteriaPath = join12(root, "done-criteria.md");
5784
+ const unitCriteriaPath = join11(root, "done-criteria.md");
5867
5785
  const copied = await copyFileSafe(sprintCriteriaPath, unitCriteriaPath);
5868
5786
  if (!copied.ok) {
5869
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`;
@@ -5883,7 +5801,7 @@ async function buildExecutionUnit(storage2, input) {
5883
5801
  let addDirs;
5884
5802
  let sessionCwd;
5885
5803
  if (input.aiProvider === "copilot") {
5886
- const repoMirror = join12(root, "repo");
5804
+ const repoMirror = join11(root, "repo");
5887
5805
  const m = await mirrorRepo(input.task.projectPath, repoMirror);
5888
5806
  if (!m.ok) return Result.error(m.error);
5889
5807
  addDirs = [];
@@ -5895,8 +5813,7 @@ async function buildExecutionUnit(storage2, input) {
5895
5813
  return Result.ok({
5896
5814
  root,
5897
5815
  addDirs,
5898
- sessionCwd,
5899
- evaluationMdPath: AbsolutePath.trustString(join12(root, "evaluation.md"))
5816
+ sessionCwd
5900
5817
  });
5901
5818
  }
5902
5819
  async function refreshExecutionUnit(storage2, input) {
@@ -5913,7 +5830,7 @@ async function refreshExecutionUnit(storage2, input) {
5913
5830
  }
5914
5831
 
5915
5832
  // src/integration/persistence/ideate-unit-builder.ts
5916
- import { join as join13 } from "path";
5833
+ import { join as join12 } from "path";
5917
5834
  async function buildIdeationUnit(storage2, input) {
5918
5835
  const slug = unitSlug(String(input.ticket.id), input.ticket.title);
5919
5836
  const root = storage2.ideationUnitDir(input.sprint.id, slug);
@@ -5927,19 +5844,19 @@ async function buildIdeationUnit(storage2, input) {
5927
5844
  affectedRepos: []
5928
5845
  });
5929
5846
  if (!ctx.ok) return Result.error(ctx.error);
5930
- const ticketMdPath = AbsolutePath.trustString(join13(root, "ticket.md"));
5847
+ const ticketMdPath = AbsolutePath.trustString(join12(root, "ticket.md"));
5931
5848
  const wrote = await writeFileSafe2(ticketMdPath, renderTicketInput(input.ticket));
5932
5849
  if (!wrote.ok) return Result.error(wrote.error);
5933
5850
  return Result.ok({
5934
5851
  root,
5935
- sessionMdPath: AbsolutePath.trustString(join13(root, "session.md")),
5852
+ sessionMdPath: AbsolutePath.trustString(join12(root, "session.md")),
5936
5853
  ticketMdPath,
5937
- outputJsonPath: AbsolutePath.trustString(join13(root, "output.json"))
5854
+ outputJsonPath: AbsolutePath.trustString(join12(root, "output.json"))
5938
5855
  });
5939
5856
  }
5940
5857
 
5941
5858
  // src/integration/persistence/planning-folder-builder.ts
5942
- import { basename as basename3, join as join14 } from "path";
5859
+ import { basename as basename3, join as join13 } from "path";
5943
5860
 
5944
5861
  // src/business/usecases/sprint/sprint-requirements-aggregate.ts
5945
5862
  function buildSprintRequirementsAggregate(sprint, now = /* @__PURE__ */ new Date()) {
@@ -6015,7 +5932,7 @@ async function buildPlanningFolder(storage2, input) {
6015
5932
  });
6016
5933
  if (!ctx.ok) return Result.error(ctx.error);
6017
5934
  const reqSrc = String(storage2.requirementsAggregateFile(input.sprint.id));
6018
- const reqDst = join14(root, "requirements.json");
5935
+ const reqDst = join13(root, "requirements.json");
6019
5936
  const copied = await copyFileSafe(reqSrc, reqDst);
6020
5937
  if (!copied.ok) {
6021
5938
  const inlineBody = serialiseSprintRequirementsAggregate(buildSprintRequirementsAggregate(input.sprint));
@@ -6024,11 +5941,11 @@ async function buildPlanningFolder(storage2, input) {
6024
5941
  }
6025
5942
  let addDirs;
6026
5943
  if (isCopilot) {
6027
- const reposDir = join14(root, "repos");
5944
+ const reposDir = join13(root, "repos");
6028
5945
  const ensureRepos = await ensureDirSafe(reposDir);
6029
5946
  if (!ensureRepos.ok) return Result.error(ensureRepos.error);
6030
5947
  for (const repoPath of input.sprint.affectedRepositories) {
6031
- const dst = join14(reposDir, basename3(repoPath));
5948
+ const dst = join13(reposDir, basename3(repoPath));
6032
5949
  const m = await mirrorRepo(repoPath, dst);
6033
5950
  if (!m.ok) return Result.error(m.error);
6034
5951
  }
@@ -6038,14 +5955,14 @@ async function buildPlanningFolder(storage2, input) {
6038
5955
  }
6039
5956
  return Result.ok({
6040
5957
  root,
6041
- sessionMdPath: AbsolutePath.trustString(join14(root, "session.md")),
6042
- rawTasksJsonPath: AbsolutePath.trustString(join14(root, "tasks.json")),
5958
+ sessionMdPath: AbsolutePath.trustString(join13(root, "session.md")),
5959
+ rawTasksJsonPath: AbsolutePath.trustString(join13(root, "tasks.json")),
6043
5960
  addDirs
6044
5961
  });
6045
5962
  }
6046
5963
 
6047
5964
  // src/integration/persistence/refine-unit-builder.ts
6048
- import { join as join15 } from "path";
5965
+ import { join as join14 } from "path";
6049
5966
  async function buildRefinementUnit(storage2, input) {
6050
5967
  const slug = unitSlug(String(input.ticket.id), input.ticket.title);
6051
5968
  const root = storage2.refinementUnitDir(input.sprint.id, slug);
@@ -6059,14 +5976,14 @@ async function buildRefinementUnit(storage2, input) {
6059
5976
  affectedRepos: []
6060
5977
  });
6061
5978
  if (!ctx.ok) return Result.error(ctx.error);
6062
- const ticketMdPath = AbsolutePath.trustString(join15(root, "ticket.md"));
5979
+ const ticketMdPath = AbsolutePath.trustString(join14(root, "ticket.md"));
6063
5980
  const wrote = await writeFileSafe2(ticketMdPath, renderTicketInput(input.ticket));
6064
5981
  if (!wrote.ok) return Result.error(wrote.error);
6065
5982
  return Result.ok({
6066
5983
  root,
6067
- sessionMdPath: AbsolutePath.trustString(join15(root, "session.md")),
5984
+ sessionMdPath: AbsolutePath.trustString(join14(root, "session.md")),
6068
5985
  ticketMdPath,
6069
- requirementsJsonPath: AbsolutePath.trustString(join15(root, "requirements.json"))
5986
+ requirementsJsonPath: AbsolutePath.trustString(join14(root, "requirements.json"))
6070
5987
  });
6071
5988
  }
6072
5989
 
@@ -6198,15 +6115,11 @@ var InMemorySignalBus = class {
6198
6115
  };
6199
6116
 
6200
6117
  // src/integration/signals/file-system-handler.ts
6201
- import { appendFile as appendFile2, mkdir as mkdir11, writeFile as writeFile7 } from "fs/promises";
6202
- 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";
6203
6120
  function progressPath(paths, sprintId) {
6204
6121
  return paths.progressFile(sprintId);
6205
6122
  }
6206
- function evaluationPath(paths, sprintId, taskId, taskName) {
6207
- const slug = unitSlug(taskId, taskName);
6208
- return AbsolutePath.trustString(join16(paths.executionUnitDir(sprintId, slug), "evaluation.md"));
6209
- }
6210
6123
  async function appendLine(path, line) {
6211
6124
  try {
6212
6125
  await mkdir11(dirname9(path), { recursive: true });
@@ -6224,26 +6137,6 @@ async function appendLine(path, line) {
6224
6137
  );
6225
6138
  }
6226
6139
  }
6227
- async function writeText(path, body) {
6228
- try {
6229
- await mkdir11(dirname9(path), { recursive: true });
6230
- await writeFile7(path, body.endsWith("\n") ? body : `${body}
6231
- `, {
6232
- encoding: "utf-8",
6233
- mode: 384
6234
- });
6235
- return Result.ok();
6236
- } catch (err) {
6237
- return Result.error(
6238
- new StorageError({
6239
- subCode: "io",
6240
- message: `failed to write ${path}: ${err instanceof Error ? err.message : String(err)}`,
6241
- path,
6242
- cause: err
6243
- })
6244
- );
6245
- }
6246
- }
6247
6140
  var FileSystemSignalHandler = class {
6248
6141
  constructor(paths, fileLocker = new FileLocker()) {
6249
6142
  this.paths = paths;
@@ -6275,14 +6168,10 @@ var FileSystemSignalHandler = class {
6275
6168
  return Result.error(
6276
6169
  new StorageError({
6277
6170
  subCode: "io",
6278
- message: "evaluation signal requires taskName in meta to derive the execution unit slug"
6171
+ message: "evaluation signal requires taskName in meta"
6279
6172
  })
6280
6173
  );
6281
6174
  }
6282
- const file = evaluationPath(this.paths, meta.sprintId, String(meta.taskId), meta.taskName);
6283
- const body = renderEvaluationBody(signal);
6284
- const w = await writeText(file, body);
6285
- if (!w.ok) return w;
6286
6175
  const scoreSuffix = signal.overallScore !== void 0 ? `, score ${String(signal.overallScore)}/5` : "";
6287
6176
  return this.appendProgress(
6288
6177
  meta.sprintId,
@@ -6321,33 +6210,6 @@ function formatProgress(timestamp, message, files) {
6321
6210
  const filesSuffix = files !== void 0 && files.length > 0 ? ` (files: ${files.join(", ")})` : "";
6322
6211
  return `- ${timestamp} \u2014 ${message}${filesSuffix}`;
6323
6212
  }
6324
- function renderEvaluationBody(signal) {
6325
- const lines = [];
6326
- lines.push(`# Evaluation \u2014 ${signal.status}`);
6327
- lines.push("");
6328
- lines.push(`Recorded: ${signal.timestamp}`);
6329
- if (signal.overallScore !== void 0) {
6330
- lines.push(`Overall score: ${String(signal.overallScore)}/5`);
6331
- }
6332
- lines.push("");
6333
- if (signal.dimensions.length > 0) {
6334
- lines.push("## Dimensions");
6335
- lines.push("");
6336
- for (const d of signal.dimensions) {
6337
- const verdict = d.passed ? "PASS" : "FAIL";
6338
- const scoreLabel = d.score !== void 0 ? ` (score ${String(d.score)}/5)` : "";
6339
- lines.push(`- **${d.dimension}**${scoreLabel}: ${verdict} \u2014 ${d.finding}`);
6340
- }
6341
- lines.push("");
6342
- }
6343
- if (signal.critique !== void 0 && signal.critique.length > 0) {
6344
- lines.push("## Critique");
6345
- lines.push("");
6346
- lines.push(signal.critique);
6347
- lines.push("");
6348
- }
6349
- return lines.join("\n");
6350
- }
6351
6213
 
6352
6214
  // src/integration/signals/parser.ts
6353
6215
  function buildPatterns() {
@@ -7357,18 +7219,18 @@ async function isFirstLaunch(deps) {
7357
7219
  // src/application/runtime/legacy-detector.ts
7358
7220
  import { access } from "fs/promises";
7359
7221
  import { homedir } from "os";
7360
- import { join as join17 } from "path";
7222
+ import { join as join15 } from "path";
7361
7223
  var NOT_LEGACY = { isLegacy: false, legacyConfigPath: null, hint: "" };
7362
7224
  function defaultRoot() {
7363
7225
  const fromEnv = process.env["RALPHCTL_ROOT"];
7364
7226
  if (fromEnv !== void 0 && fromEnv.length > 0) {
7365
7227
  return AbsolutePath.trustString(fromEnv);
7366
7228
  }
7367
- return AbsolutePath.trustString(join17(homedir(), ".ralphctl"));
7229
+ return AbsolutePath.trustString(join15(homedir(), ".ralphctl"));
7368
7230
  }
7369
7231
  async function detectLegacyLayout(deps = {}) {
7370
7232
  const root = deps.root ?? defaultRoot();
7371
- const legacyConfigPath = AbsolutePath.trustString(join17(root, "config.json"));
7233
+ const legacyConfigPath = AbsolutePath.trustString(join15(root, "config.json"));
7372
7234
  try {
7373
7235
  await access(legacyConfigPath);
7374
7236
  } catch {
@@ -7389,7 +7251,7 @@ Then re-run ralphctl to start fresh, and use 'ralphctl project add' to register
7389
7251
  // src/integration/ai/dist-asset-manifest.ts
7390
7252
  import { existsSync as existsSync4 } from "fs";
7391
7253
  import { readFile as readFile7, stat as stat4 } from "fs/promises";
7392
- import { dirname as dirname10, join as join18 } from "path";
7254
+ import { dirname as dirname10, join as join16 } from "path";
7393
7255
  import { fileURLToPath as fileURLToPath3 } from "url";
7394
7256
  var HERE3 = dirname10(fileURLToPath3(import.meta.url));
7395
7257
  var cached2 = { state: "unverified" };
@@ -7397,7 +7259,7 @@ async function verifyDistAssets(distRootOverride) {
7397
7259
  if (cached2.state === "pass") return Result.ok();
7398
7260
  if (cached2.state === "fail") return Result.error(cached2.error);
7399
7261
  const distRoot = distRootOverride ?? HERE3;
7400
- const manifestPath = join18(distRoot, "manifest.json");
7262
+ const manifestPath = join16(distRoot, "manifest.json");
7401
7263
  if (!existsSync4(manifestPath)) {
7402
7264
  cached2 = { state: "pass" };
7403
7265
  return Result.ok();
@@ -7409,7 +7271,7 @@ async function verifyDistAssets(distRootOverride) {
7409
7271
  }
7410
7272
  const missing = [];
7411
7273
  for (const rel of parsed.value.assets) {
7412
- const abs = join18(distRoot, rel);
7274
+ const abs = join16(distRoot, rel);
7413
7275
  try {
7414
7276
  const s = await stat4(abs);
7415
7277
  if (!s.isFile()) missing.push(rel);
@@ -9683,7 +9545,22 @@ function unlinkSkillsLeaf(deps, opts = {}) {
9683
9545
  }
9684
9546
 
9685
9547
  // src/application/chains/execute/per-task-flow.ts
9686
- 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
+ }
9687
9564
 
9688
9565
  // src/integration/persistence/done-criteria-reader.ts
9689
9566
  import { readFile as readFile8 } from "fs/promises";
@@ -9737,7 +9614,7 @@ var BranchPreflightUseCase = class {
9737
9614
  };
9738
9615
 
9739
9616
  // src/business/usecases/evaluate/evaluate-and-fix-loop.ts
9740
- import { join as join19 } from "path";
9617
+ import { join as join18 } from "path";
9741
9618
 
9742
9619
  // src/business/usecases/_shared/add-dir-args.ts
9743
9620
  function buildAdditionalCwdArgs(paths) {
@@ -9758,6 +9635,24 @@ function renderFileHandoffWrapper(promptFilePath) {
9758
9635
  "Follow the protocol in that file exactly."
9759
9636
  ].join("\n");
9760
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
+ }
9761
9656
 
9762
9657
  // src/business/usecases/evaluate/evaluate-task.ts
9763
9658
  var MAX_MALFORMED_CRITIQUE_CHARS = 500;
@@ -9897,9 +9792,6 @@ var EvaluateAndFixLoopUseCase = class {
9897
9792
  history: []
9898
9793
  });
9899
9794
  }
9900
- const evaluatorPromptPath = AbsolutePath.trustString(
9901
- input.evaluateWorkspaceDir !== void 0 ? join19(input.evaluateWorkspaceDir, "evaluator-prompt.md") : join19(String(input.contextsDir), `evaluate-${String(input.task.id)}.md`)
9902
- );
9903
9795
  const history = [];
9904
9796
  let previousSignal;
9905
9797
  let previousCritique;
@@ -9923,18 +9815,21 @@ var EvaluateAndFixLoopUseCase = class {
9923
9815
  });
9924
9816
  }
9925
9817
  }
9818
+ const evaluatorPromptPath = AbsolutePath.trustString(
9819
+ join18(evaluatorRoundDir(input.evaluateWorkspaceDir, round), "prompt.md")
9820
+ );
9926
9821
  const evalPromptResult = await this.prompts.buildEvaluatePrompt({
9927
9822
  task: input.task,
9928
9823
  sprint: input.sprint,
9824
+ evaluateWorkspaceDir: input.evaluateWorkspaceDir,
9929
9825
  ...previousCritique !== void 0 ? { previousCritique } : {},
9930
- ...input.evaluateWorkspaceDir !== void 0 ? { evaluateWorkspaceDir: input.evaluateWorkspaceDir } : {},
9931
9826
  ...input.doneCriteriaBullet !== void 0 ? { doneCriteriaBullet: input.doneCriteriaBullet } : {}
9932
9827
  });
9933
9828
  if (!evalPromptResult.ok) return Result.error(evalPromptResult.error);
9934
9829
  const written = await this.writeContextFile.write(evaluatorPromptPath, evalPromptResult.value);
9935
9830
  if (!written.ok) return Result.error(written.error);
9936
9831
  const evaluatorCwd = input.evaluateSessionCwd ?? input.cwd;
9937
- const evaluatorSessionMdPath = input.nextSessionMdPath ? await input.nextSessionMdPath("evaluator") : void 0;
9832
+ const evaluatorSessionMdPath = input.nextSessionMdPath ? await input.nextSessionMdPath("evaluator", round) : void 0;
9938
9833
  const evalResult = await this.evaluator.execute({
9939
9834
  task: input.task,
9940
9835
  sprint: input.sprint,
@@ -9947,6 +9842,16 @@ var EvaluateAndFixLoopUseCase = class {
9947
9842
  if (!evalResult.ok) return Result.error(evalResult.error);
9948
9843
  const { outcome, signal, fullCritique } = evalResult.value;
9949
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
+ }
9950
9855
  log.info(`evaluator round complete for task ${String(input.task.id)}`, { round, outcome });
9951
9856
  if (outcome === "passed") break;
9952
9857
  if (outcome === "malformed") {
@@ -9972,7 +9877,8 @@ var EvaluateAndFixLoopUseCase = class {
9972
9877
  break;
9973
9878
  }
9974
9879
  log.info("resuming generator with critique", { round });
9975
- 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;
9976
9882
  const fixResult = await this.generator.execute({
9977
9883
  task: input.task,
9978
9884
  sprint: input.sprint,
@@ -9980,6 +9886,7 @@ var EvaluateAndFixLoopUseCase = class {
9980
9886
  promptFilePath: input.executePromptFilePath,
9981
9887
  ...resumeSessionId !== void 0 ? { resumeSessionId } : {},
9982
9888
  ...generatorSessionMdPath !== void 0 ? { sessionMdPath: generatorSessionMdPath } : {},
9889
+ fixContext: { critique: fullCritique },
9983
9890
  ...input.abortSignal !== void 0 ? { abortSignal: input.abortSignal } : {}
9984
9891
  });
9985
9892
  if (!fixResult.ok) return Result.error(fixResult.error);
@@ -9991,8 +9898,8 @@ var EvaluateAndFixLoopUseCase = class {
9991
9898
  projectPath: input.cwd,
9992
9899
  checkScript: input.checkScript
9993
9900
  });
9994
- if (!checkResult.ok) return Result.error(checkResult.error);
9995
- if (!checkResult.value.passed) {
9901
+ if (!checkResult.ok) {
9902
+ if (checkResult.error.code !== "check-failed") return Result.error(checkResult.error);
9996
9903
  log.warn("post-task check failed after fix attempt \u2014 re-evaluating anyway", { round });
9997
9904
  }
9998
9905
  }
@@ -10039,7 +9946,7 @@ var ExecuteSingleTaskUseCase = class {
10039
9946
  taskId: input.task.id,
10040
9947
  projectPath: input.cwd
10041
9948
  });
10042
- const wrapper = renderFileHandoffWrapper(input.promptFilePath);
9949
+ const wrapper = input.fixContext !== void 0 ? renderFixHandoffWrapper(input.promptFilePath, input.fixContext.critique) : renderFileHandoffWrapper(input.promptFilePath);
10043
9950
  log.info(`executing task ${String(input.task.id)}${formatNameSuffix2(input.task.name)}`);
10044
9951
  const sessionOptions = {
10045
9952
  cwd: input.cwd,
@@ -10109,6 +10016,22 @@ function formatNameSuffix2(name) {
10109
10016
  return ` \u2014 "${slice}"`;
10110
10017
  }
10111
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
+
10112
10035
  // src/business/usecases/execute/post-task-check.ts
10113
10036
  var PostTaskCheckUseCase = class {
10114
10037
  constructor(external, logger) {
@@ -10131,10 +10054,11 @@ var PostTaskCheckUseCase = class {
10131
10054
  input.timeoutMs
10132
10055
  );
10133
10056
  if (!result.passed) {
10134
- 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 }));
10135
10059
  }
10136
10060
  return Result.ok({
10137
- passed: result.passed,
10061
+ passed: true,
10138
10062
  output: result.output,
10139
10063
  skipped: false
10140
10064
  });
@@ -10292,8 +10216,7 @@ function buildExecutionUnitLeaf(deps, opts = {}) {
10292
10216
  ...ctx,
10293
10217
  executionUnitRoot: out.root,
10294
10218
  executionAddDirs: out.addDirs,
10295
- executionSessionCwd: out.sessionCwd,
10296
- executionEvaluationMdPath: out.evaluationMdPath
10219
+ executionSessionCwd: out.sessionCwd
10297
10220
  })
10298
10221
  });
10299
10222
  }
@@ -10307,8 +10230,21 @@ function collectPriorEvaluations(tasks) {
10307
10230
  return map;
10308
10231
  }
10309
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
+
10310
10246
  // src/application/chains/leaves/render-prompt-to-file.ts
10311
- import { join as join20 } from "path";
10247
+ import { join as join19 } from "path";
10312
10248
  function renderPromptToFileLeaf(deps, opts) {
10313
10249
  return new Leaf("render-prompt-to-file", {
10314
10250
  useCase: {
@@ -10338,7 +10274,7 @@ function defaultPromptPath(ctx, opts) {
10338
10274
  const sprintDir = storagePaths.sprintDir(ctx.sprintId);
10339
10275
  const id = opts.identifier(ctx);
10340
10276
  const basename4 = id.length > 0 ? `${opts.flowName}-${id}.md` : `${opts.flowName}.md`;
10341
- return AbsolutePath.trustString(join20(sprintDir, "contexts", basename4));
10277
+ return AbsolutePath.trustString(join19(sprintDir, "contexts", basename4));
10342
10278
  }
10343
10279
 
10344
10280
  // src/application/chains/execute/per-task-flow.ts
@@ -10385,18 +10321,30 @@ function createPerTaskFlow(deps, opts) {
10385
10321
  catchIf: (err) => err.code === "aborted",
10386
10322
  fallback: markCancelledFallbackLeaf(deps)
10387
10323
  });
10388
- const buildEvalWorkspaceStep = buildExecutionUnitLeaf({
10389
- sessionFolderBuilder: deps.sessionFolderBuilder,
10390
- aiSession: deps.aiSession
10391
- });
10392
- const evaluatorAsLeaf = new OnError(
10393
- new Sequential("evaluate", [buildEvalWorkspaceStep, evaluateLoopLeaf(deps, evaluateLoop)]),
10324
+ const buildEvalWorkspaceStep = new OnError(
10325
+ buildExecutionUnitLeaf({
10326
+ sessionFolderBuilder: deps.sessionFolderBuilder,
10327
+ aiSession: deps.aiSession
10328
+ }),
10394
10329
  {
10395
10330
  catchIf: (err) => err.code !== "aborted",
10396
- 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")
10397
10346
  }
10398
10347
  );
10399
- const postTaskStep = postTaskCheckLeaf(postCheck);
10400
10348
  const renderPromptStep = renderPromptToFileLeaf(
10401
10349
  { writeContextFile: deps.writeContextFile },
10402
10350
  {
@@ -10405,7 +10353,7 @@ function createPerTaskFlow(deps, opts) {
10405
10353
  path: (ctx) => {
10406
10354
  const slug = unitSlug(String(ctx.task.id), ctx.task.name);
10407
10355
  const root = resolveStoragePaths().executionUnitDir(ctx.sprintId, slug);
10408
- return AbsolutePath.trustString(join21(String(root), "prompt.md"));
10356
+ return AbsolutePath.trustString(join20(String(root), "prompt.md"));
10409
10357
  },
10410
10358
  buildPrompt: (ctx) => deps.prompts.buildExecutePrompt({
10411
10359
  task: ctx.task,
@@ -10418,9 +10366,10 @@ function createPerTaskFlow(deps, opts) {
10418
10366
  branchPreflightStep,
10419
10367
  markInProgressLeaf(deps),
10420
10368
  renderPromptStep,
10369
+ buildEvalWorkspaceStep,
10421
10370
  executeTaskStep,
10422
10371
  postTaskStep,
10423
- evaluatorAsLeaf,
10372
+ evaluateLoopStep,
10424
10373
  commitTaskLeaf(deps),
10425
10374
  markDoneLeaf(deps)
10426
10375
  ]);
@@ -10463,6 +10412,22 @@ function markBlockedFallbackLeaf(deps) {
10463
10412
  output: (ctx, task) => ({ ...ctx, task, taskBlocked: true })
10464
10413
  });
10465
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
+ }
10466
10431
  function markCancelledFallbackLeaf(deps) {
10467
10432
  return new Leaf("mark-cancelled", {
10468
10433
  useCase: {
@@ -10498,6 +10463,7 @@ function markInProgressLeaf(deps) {
10498
10463
  });
10499
10464
  }
10500
10465
  function executeTaskLeaf(useCase) {
10466
+ let attempt = 0;
10501
10467
  return new Leaf("execute-task", {
10502
10468
  useCase: {
10503
10469
  async execute(input) {
@@ -10510,7 +10476,9 @@ function executeTaskLeaf(useCase) {
10510
10476
  message: "execute-task: promptFilePath is missing \u2014 render-prompt-to-file must run first"
10511
10477
  });
10512
10478
  }
10513
- 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;
10514
10482
  const result = await useCase.execute({
10515
10483
  sprint: input.sprint,
10516
10484
  task: input.task,
@@ -10618,19 +10586,21 @@ function evaluateLoopLeaf(deps, loop) {
10618
10586
  if (input.taskBlocked) {
10619
10587
  return Result.ok({ task: input.task });
10620
10588
  }
10621
- const doneCriteriaBullet = input.executionUnitRoot !== void 0 ? await readDoneCriteriaBullet(
10622
- join21(String(input.executionUnitRoot), "done-criteria.md"),
10623
- String(input.task.id)
10624
- ) : "";
10589
+ if (input.executionUnitRoot === void 0) {
10590
+ return Result.ok({ task: input.task });
10591
+ }
10625
10592
  if (input.promptFilePath === void 0) {
10626
10593
  return Result.error({
10627
10594
  code: "invalid-state",
10628
10595
  message: "evaluate-task: promptFilePath is missing \u2014 render-prompt-to-file must run first"
10629
10596
  });
10630
10597
  }
10631
- const contextsDir = AbsolutePath.trustString(dirname11(String(input.promptFilePath)));
10632
- const unitMounted = input.executionUnitRoot !== void 0;
10633
- 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 () => {
10634
10604
  await deps.aiSession.ensureReady();
10635
10605
  const aiProvider = deps.aiSession.getProviderName();
10636
10606
  const priorEvaluations = collectPriorEvaluations2(input.tasks);
@@ -10641,30 +10611,34 @@ function evaluateLoopLeaf(deps, loop) {
10641
10611
  aiProvider,
10642
10612
  priorEvaluations
10643
10613
  });
10644
- } : void 0;
10645
- const unitRoot = input.executionUnitRoot;
10646
- 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
+ };
10647
10619
  const result = await loop.execute({
10648
10620
  task: input.task,
10649
10621
  sprint: input.sprint,
10650
10622
  cwd: input.cwd,
10651
10623
  executePromptFilePath: String(input.promptFilePath),
10652
- contextsDir,
10624
+ evaluateWorkspaceDir: String(unitRoot),
10625
+ refreshWorkspace,
10626
+ nextSessionMdPath,
10653
10627
  ...input.checkScript !== void 0 ? { checkScript: input.checkScript } : {},
10654
10628
  ...input.resumeSessionId !== void 0 ? { resumeSessionId: input.resumeSessionId } : {},
10655
10629
  ...input.executionAddDirs !== void 0 ? { addDirs: input.executionAddDirs } : {},
10656
10630
  ...input.executionSessionCwd !== void 0 ? { evaluateSessionCwd: input.executionSessionCwd } : {},
10657
- ...input.executionUnitRoot !== void 0 ? { evaluateWorkspaceDir: String(input.executionUnitRoot) } : {},
10658
- ...refreshWorkspace !== void 0 ? { refreshWorkspace } : {},
10659
- ...nextSessionMdPath !== void 0 ? { nextSessionMdPath } : {},
10660
10631
  ...doneCriteriaBullet.length > 0 ? { doneCriteriaBullet } : {}
10661
10632
  });
10662
10633
  if (!result.ok) return Result.error(result.error);
10663
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);
10664
10638
  const recorded = input.task.recordEvaluation({
10665
10639
  status: result.value.finalSignal.status,
10666
10640
  output: result.value.finalCritique.slice(0, MAX_PREVIEW_CHARS),
10667
- file: input.executionEvaluationMdPath !== void 0 ? String(input.executionEvaluationMdPath) : `execution/${String(input.task.id)}/evaluation.md`
10641
+ file
10668
10642
  });
10669
10643
  const saved = await deps.taskRepo.update(input.sprintId, recorded);
10670
10644
  if (!saved.ok) return Result.error(saved.error);
@@ -10685,8 +10659,7 @@ function evaluateLoopLeaf(deps, loop) {
10685
10659
  taskBlocked: ctx.taskBlocked === true,
10686
10660
  ...ctx.executionUnitRoot !== void 0 ? { executionUnitRoot: ctx.executionUnitRoot } : {},
10687
10661
  ...ctx.executionAddDirs !== void 0 ? { executionAddDirs: ctx.executionAddDirs } : {},
10688
- ...ctx.executionSessionCwd !== void 0 ? { executionSessionCwd: ctx.executionSessionCwd } : {},
10689
- ...ctx.executionEvaluationMdPath !== void 0 ? { executionEvaluationMdPath: ctx.executionEvaluationMdPath } : {}
10662
+ ...ctx.executionSessionCwd !== void 0 ? { executionSessionCwd: ctx.executionSessionCwd } : {}
10690
10663
  }),
10691
10664
  // Push the recorded task back onto the context so the downstream
10692
10665
  // `mark-done` leaf carries the evaluation forward when it flips
@@ -10751,17 +10724,6 @@ function commitTaskLeaf(deps) {
10751
10724
  output: (ctx, task) => ({ ...ctx, task })
10752
10725
  });
10753
10726
  }
10754
- function noopLeaf(name) {
10755
- return new Leaf(name, {
10756
- useCase: {
10757
- execute(input) {
10758
- return Promise.resolve(Result.ok(input));
10759
- }
10760
- },
10761
- input: (ctx) => ctx,
10762
- output: (_, ctx) => ctx
10763
- });
10764
- }
10765
10727
 
10766
10728
  // src/application/chains/execute/execute-flow.ts
10767
10729
  function createExecuteFlow(deps, opts) {
@@ -10784,7 +10746,8 @@ function createExecuteFlow(deps, opts) {
10784
10746
  const initializeStep = new Sequential("initialize", [
10785
10747
  resolveBranchLeaf(deps),
10786
10748
  dirtyTreePreflightLeaf(deps, opts),
10787
- checkScriptsSprintStartLeaf(deps)
10749
+ resolveCheckScriptsLeaf(deps),
10750
+ setupScriptsSprintStartLeaf(deps)
10788
10751
  ]);
10789
10752
  return new Sequential("execute", [
10790
10753
  loadSprintLeaf({ sprintRepo: deps.sprintRepo }),
@@ -10974,14 +10937,16 @@ function bridgePerTaskChain(task, inner, opts, taskRepo) {
10974
10937
  async execute(input) {
10975
10938
  const fresh = await taskRepo.findBySprintId(input.sprintId);
10976
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);
10977
10942
  const innerCtx = {
10978
10943
  sprintId: input.sprintId,
10979
- sprint: opts.sprint,
10944
+ sprint: liveSprint,
10980
10945
  task,
10981
10946
  tasks: liveTasks,
10982
10947
  cwd: task.projectPath,
10983
10948
  expectedBranch: input.expectedBranch,
10984
- ...input.checkScript !== void 0 ? { checkScript: input.checkScript } : {},
10949
+ ...resolvedCheckScript !== void 0 ? { checkScript: resolvedCheckScript } : {},
10985
10950
  ...input.noCommit === true ? { noCommit: true } : {}
10986
10951
  };
10987
10952
  const innerResult = await inner.execute(innerCtx);
@@ -11083,33 +11048,118 @@ function assertTasksAcyclicLeaf(sortResult) {
11083
11048
  output: (ctx) => ctx
11084
11049
  });
11085
11050
  }
11086
- 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) {
11087
11086
  return new Leaf(
11088
- "check-scripts-sprint-start",
11087
+ "setup-scripts-sprint-start",
11089
11088
  {
11090
11089
  useCase: {
11091
11090
  async execute(input) {
11092
- if (input.checkScript === void 0 || input.checkScript.length === 0) {
11093
- return Promise.resolve(Result.ok(void 0));
11091
+ const repoPaths = input.sprint.affectedRepositories;
11092
+ if (repoPaths.length === 0) {
11093
+ return Result.ok(void 0);
11094
11094
  }
11095
- const r = await deps.external.runCheckScript(input.cwd, input.checkScript, "sprint-start");
11096
- if (!r.passed) {
11097
- return Result.error(
11098
- new InvalidStateError({
11099
- entity: "sprint",
11100
- currentState: "check-failed",
11101
- attemptedAction: "execute",
11102
- message: "sprint-start check script failed"
11103
- })
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 }
11104
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
+ }
11105
11155
  }
11106
11156
  return Result.ok(void 0);
11107
11157
  }
11108
11158
  },
11109
- input: (ctx) => ({
11110
- cwd: ctx.cwd,
11111
- ...ctx.checkScript !== void 0 ? { checkScript: ctx.checkScript } : {}
11112
- }),
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
+ },
11113
11163
  output: (ctx) => ctx
11114
11164
  }
11115
11165
  );
@@ -11206,10 +11256,10 @@ function applyFeedbackLeaf(useCase) {
11206
11256
  });
11207
11257
  }
11208
11258
  const { resolveStoragePaths: resolveStoragePaths2 } = await import("./storage-paths-IPNZZM5D.mjs");
11209
- const { join: join35 } = await import("path");
11259
+ const { join: join34 } = await import("path");
11210
11260
  const sprintDir = resolveStoragePaths2().sprintDir(input.sprintId);
11211
11261
  const { AbsolutePath: APV } = await import("./absolute-path-WUTZQ37D.mjs");
11212
- 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`));
11213
11263
  const result = await useCase.execute({
11214
11264
  sprint: input.sprint,
11215
11265
  promptFilePath: String(input.promptFilePath),
@@ -11246,7 +11296,7 @@ function recordFeedbackIterationLeaf(logger) {
11246
11296
  try {
11247
11297
  const { resolveStoragePaths: resolveStoragePaths2 } = await import("./storage-paths-IPNZZM5D.mjs");
11248
11298
  const { mkdir: mkdir16, appendFile: appendFile4 } = await import("fs/promises");
11249
- const { dirname: dirname17 } = await import("path");
11299
+ const { dirname: dirname16 } = await import("path");
11250
11300
  const path = resolveStoragePaths2().feedbackFile(input.sprintId);
11251
11301
  const stamp = (/* @__PURE__ */ new Date()).toISOString();
11252
11302
  const block = `
@@ -11256,7 +11306,7 @@ function recordFeedbackIterationLeaf(logger) {
11256
11306
 
11257
11307
  ${input.feedbackText.trim()}
11258
11308
  `;
11259
- await mkdir16(dirname17(path), { recursive: true });
11309
+ await mkdir16(dirname16(path), { recursive: true });
11260
11310
  await appendFile4(path, block, "utf-8");
11261
11311
  } catch (err) {
11262
11312
  logger.warn("feedback: failed to append to feedback.md", {
@@ -11718,8 +11768,8 @@ function saveSprintLeaf(deps, name = "save-sprint") {
11718
11768
  }
11719
11769
 
11720
11770
  // src/application/chains/leaves/save-tasks.ts
11721
- import { writeFile as writeFile8 } from "fs/promises";
11722
- import { dirname as dirname12 } from "path";
11771
+ import { writeFile as writeFile7 } from "fs/promises";
11772
+ import { dirname as dirname11 } from "path";
11723
11773
  import { mkdir as mkdir12 } from "fs/promises";
11724
11774
  var FALLBACK_CRITERION = "(no explicit criteria \u2014 use task description as proxy)";
11725
11775
  function renderDoneCriteria(tasks) {
@@ -11745,9 +11795,9 @@ function saveTasksLeaf(deps, name = "save-tasks") {
11745
11795
  const storage2 = resolveStoragePaths();
11746
11796
  const criteriaPath = String(storage2.doneCriteriaFile(input.sprintId));
11747
11797
  try {
11748
- await mkdir12(dirname12(criteriaPath), { recursive: true });
11798
+ await mkdir12(dirname11(criteriaPath), { recursive: true });
11749
11799
  const body = renderDoneCriteria(input.tasks);
11750
- await writeFile8(criteriaPath, body, { encoding: "utf-8", mode: 384 });
11800
+ await writeFile7(criteriaPath, body, { encoding: "utf-8", mode: 384 });
11751
11801
  } catch (err) {
11752
11802
  return Result.error(
11753
11803
  new StorageError({
@@ -11850,11 +11900,11 @@ function ideateAndPlanLeaf(useCase) {
11850
11900
 
11851
11901
  // src/application/chains/onboard/onboard-load-leaves.ts
11852
11902
  import { readFile as readFile9 } from "fs/promises";
11853
- import { join as join23 } from "path";
11903
+ import { join as join22 } from "path";
11854
11904
 
11855
11905
  // src/application/chains/onboard/onboard-persist-leaves.ts
11856
- import { mkdir as mkdir13, writeFile as writeFile9 } from "fs/promises";
11857
- 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";
11858
11908
  var HARNESS_MARKER_PREFIX = "<!-- ralphctl onboard:";
11859
11909
  function makeMarker(now = () => /* @__PURE__ */ new Date()) {
11860
11910
  return `${HARNESS_MARKER_PREFIX} ${now().toISOString()} -->`;
@@ -11867,14 +11917,14 @@ function writeContextFileLeaf(now = () => /* @__PURE__ */ new Date()) {
11867
11917
  if (proposals === void 0 || accepted === null || accepted === void 0 || accepted.length === 0) {
11868
11918
  return Result.ok(void 0);
11869
11919
  }
11870
- const targetPath = join22(repo.path, proposals.contextFilePath);
11920
+ const targetPath = join21(repo.path, proposals.contextFilePath);
11871
11921
  try {
11872
- await mkdir13(dirname13(targetPath), { recursive: true });
11922
+ await mkdir13(dirname12(targetPath), { recursive: true });
11873
11923
  const marker = makeMarker(now);
11874
11924
  const firstLine = accepted.split("\n", 1)[0] ?? "";
11875
11925
  const body = firstLine.startsWith(HARNESS_MARKER_PREFIX) ? accepted : `${marker}
11876
11926
  ${accepted}`;
11877
- await writeFile9(targetPath, body, "utf-8");
11927
+ await writeFile8(targetPath, body, "utf-8");
11878
11928
  return Result.ok(void 0);
11879
11929
  } catch (err) {
11880
11930
  return Result.error(
@@ -11946,7 +11996,7 @@ var PRE_EXISTING_CONTEXT_FILES = ["CLAUDE.md", ".github/copilot-instructions.md"
11946
11996
  async function findExistingContextFiles(repoPath) {
11947
11997
  const found = [];
11948
11998
  for (const relPath of PRE_EXISTING_CONTEXT_FILES) {
11949
- const fullPath = join23(repoPath, relPath);
11999
+ const fullPath = join22(repoPath, relPath);
11950
12000
  let body;
11951
12001
  try {
11952
12002
  body = await readFile9(fullPath, "utf-8");
@@ -12066,7 +12116,7 @@ function detectExistingFilesLeaf(deps) {
12066
12116
 
12067
12117
  // src/application/chains/onboard/onboard-ai-leaves.ts
12068
12118
  import { readFile as readFile10 } from "fs/promises";
12069
- import { join as join24 } from "path";
12119
+ import { join as join23 } from "path";
12070
12120
 
12071
12121
  // src/business/usecases/onboard/onboard-repo.ts
12072
12122
  function contextFilePathFor(provider) {
@@ -12223,7 +12273,7 @@ function runOnboardAiLeaf(deps) {
12223
12273
  await deps.aiSession.ensureReady();
12224
12274
  const provider = deps.aiSession.getProviderName();
12225
12275
  const fileName = contextFilePathFor(provider);
12226
- const targetPath = join24(input.repo.path, fileName);
12276
+ const targetPath = join23(input.repo.path, fileName);
12227
12277
  const detected = await detectModeAndBody(targetPath);
12228
12278
  const result = await useCase.execute({
12229
12279
  project: input.project,
@@ -12380,11 +12430,11 @@ function createOnboardFlow(deps, opts) {
12380
12430
 
12381
12431
  // src/application/chains/plan/plan-flow.ts
12382
12432
  import { mkdir as mkdir14 } from "fs/promises";
12383
- import { dirname as dirname15, join as join25 } from "path";
12433
+ import { dirname as dirname14, join as join24 } from "path";
12384
12434
 
12385
12435
  // src/business/usecases/plan/plan-sprint-tasks.ts
12386
12436
  import { readFile as readFile11 } from "fs/promises";
12387
- import { dirname as dirname14 } from "path";
12437
+ import { dirname as dirname13 } from "path";
12388
12438
  var PlanSprintTasksUseCase = class {
12389
12439
  constructor(ai, logger) {
12390
12440
  this.ai = ai;
@@ -12456,8 +12506,8 @@ var PlanSprintTasksUseCase = class {
12456
12506
  // the whole spec before the user sees any response.
12457
12507
  async runInteractive(input, wrapper, log) {
12458
12508
  const handover = input.runInTerminal ?? (async (fn) => fn());
12459
- const promptDir = dirname14(input.promptFilePath);
12460
- 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;
12461
12511
  const repoArgs = buildAdditionalCwdArgs(input.additionalRepoPaths);
12462
12512
  const extraArgs = [...repoArgs, "--add-dir", promptDir];
12463
12513
  if (outputDir !== promptDir) {
@@ -12628,7 +12678,7 @@ function createPlanFlow(deps, opts) {
12628
12678
  if (!ctx.planningFolderRoot) {
12629
12679
  throw new Error("render-prompt-to-file: ctx.planningFolderRoot must be set by build-planning-folder");
12630
12680
  }
12631
- return AbsolutePath.trustString(join25(String(ctx.planningFolderRoot), "prompt.md"));
12681
+ return AbsolutePath.trustString(join24(String(ctx.planningFolderRoot), "prompt.md"));
12632
12682
  },
12633
12683
  buildPrompt: (ctx) => {
12634
12684
  if (!ctx.sprint) {
@@ -12750,7 +12800,7 @@ function planTasksLeaf(useCase, opts) {
12750
12800
  useCase: {
12751
12801
  async execute(input) {
12752
12802
  if (opts.outputFilePath !== void 0 && opts.outputFilePath !== "") {
12753
- await mkdir14(dirname15(opts.outputFilePath), { recursive: true });
12803
+ await mkdir14(dirname14(opts.outputFilePath), { recursive: true });
12754
12804
  }
12755
12805
  const result = await useCase.execute({
12756
12806
  sprint: input.sprint,
@@ -12914,11 +12964,11 @@ function assertAllTicketsApprovedLeaf() {
12914
12964
  }
12915
12965
 
12916
12966
  // src/application/chains/refine/refine-flow.ts
12917
- import { join as join26 } from "path";
12967
+ import { join as join25 } from "path";
12918
12968
 
12919
12969
  // src/business/usecases/refine/refine-single-ticket.ts
12920
12970
  import { readFile as readFile12 } from "fs/promises";
12921
- import { dirname as dirname16 } from "path";
12971
+ import { dirname as dirname15 } from "path";
12922
12972
  var RefineSingleTicketUseCase = class {
12923
12973
  constructor(ai, logger) {
12924
12974
  this.ai = ai;
@@ -12981,8 +13031,8 @@ var RefineSingleTicketUseCase = class {
12981
13031
  // scroll past before it responds.
12982
13032
  async runInteractive(input, wrapper, log) {
12983
13033
  const handover = input.runInTerminal ?? (async (fn) => fn());
12984
- const promptDir = dirname16(input.promptFilePath);
12985
- 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;
12986
13036
  const addDirArgs = ["--add-dir", promptDir];
12987
13037
  if (outputDir !== promptDir) {
12988
13038
  addDirArgs.push("--add-dir", outputDir);
@@ -13195,7 +13245,7 @@ function buildPerTicketChain(deps, refineUseCase, ticket, opts) {
13195
13245
  if (!ctx.refinementUnitRoot) {
13196
13246
  throw new Error(`refine-${String(ticket.id)}: ctx.refinementUnitRoot must be set by build-refinement-unit`);
13197
13247
  }
13198
- return AbsolutePath.trustString(join26(String(ctx.refinementUnitRoot), "prompt.md"));
13248
+ return AbsolutePath.trustString(join25(String(ctx.refinementUnitRoot), "prompt.md"));
13199
13249
  },
13200
13250
  buildPrompt: (ctx) => {
13201
13251
  const outputFilePath = ctx.refinementRequirementsJsonPath !== void 0 ? String(ctx.refinementRequirementsJsonPath) : void 0;
@@ -14485,18 +14535,24 @@ function stepGlyph(status, spinnerFrame) {
14485
14535
  return /* @__PURE__ */ jsx19(Text17, { color: inkColors.muted, bold: true, children: glyphs.emDash });
14486
14536
  return /* @__PURE__ */ jsx19(Text17, { color: inkColors.muted, bold: true, children: glyphs.phasePending });
14487
14537
  }
14538
+ var MAX_RENDERED_STEPS = 50;
14488
14539
  function StepTrace({ steps, isRunning }) {
14489
14540
  const spinnerFrame = useSpinnerFrame();
14490
14541
  if (steps.length === 0) {
14491
14542
  if (isRunning) return /* @__PURE__ */ jsx19(Spinner, { label: "Starting\u2026" });
14492
14543
  return /* @__PURE__ */ jsx19(Text17, { dimColor: true, children: "No steps recorded." });
14493
14544
  }
14494
- return /* @__PURE__ */ jsx19(Box18, { flexDirection: "column", children: steps.map((step, i) => /* @__PURE__ */ jsxs18(Box18, { children: [
14495
- stepGlyph(step.status, spinnerFrame),
14496
- /* @__PURE__ */ jsx19(Text17, { bold: step.status === void 0, children: ` ${step.name}` }),
14497
- step.durationMs !== void 0 ? /* @__PURE__ */ jsx19(Text17, { dimColor: true, children: ` ${glyphs.inlineDot} ${durationLabel(step.durationMs)}` }) : null,
14498
- step.errorMessage ? /* @__PURE__ */ jsx19(Text17, { color: inkColors.error, children: ` ${glyphs.emDash} ${step.errorMessage}` }) : null
14499
- ] }, 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
+ ] });
14500
14556
  }
14501
14557
  function CompactStepSummary({ steps }) {
14502
14558
  if (steps.length === 0) return /* @__PURE__ */ jsx19(Text17, { dimColor: true, children: "No steps recorded." });
@@ -14976,6 +15032,7 @@ var EXECUTE_HINTS_TERMINAL = [
14976
15032
  function isTaskStep2(name) {
14977
15033
  return /^task-[a-zA-Z0-9_-]+$/.test(name);
14978
15034
  }
15035
+ var MAX_LIVE_STEPS = 200;
14979
15036
  function ExecuteView({ sessionId, sessionManager, signalBus }) {
14980
15037
  const router = useRouterOptional();
14981
15038
  const cancelInFlight = useRef2(false);
@@ -15045,12 +15102,14 @@ function ExecuteView({ sessionId, sessionManager, signalBus }) {
15045
15102
  durationMs: entry.durationMs,
15046
15103
  errorMessage: entry.error?.message
15047
15104
  };
15105
+ let next;
15048
15106
  if (idx >= 0) {
15049
- const next = [...prev];
15107
+ next = [...prev];
15050
15108
  next[idx] = settled;
15051
- return next;
15109
+ } else {
15110
+ next = [...prev, settled];
15052
15111
  }
15053
- return [...prev, settled];
15112
+ return next.length > MAX_LIVE_STEPS ? next.slice(-MAX_LIVE_STEPS) : next;
15054
15113
  });
15055
15114
  if (signalBus === void 0 || signalBus === null) {
15056
15115
  if (entry.stepName === "rate-limit-paused") setRateLimitVisible(true);
@@ -18544,13 +18603,13 @@ async function currentSprintReadableCheck(deps) {
18544
18603
  }
18545
18604
 
18546
18605
  // src/application/doctor/checks/data-dir-writable.ts
18547
- import { mkdir as mkdir15, unlink as unlink3, writeFile as writeFile10 } from "fs/promises";
18548
- 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";
18549
18608
  async function dataDirWritableCheck(deps) {
18550
- 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`);
18551
18610
  try {
18552
18611
  await mkdir15(deps.storage.dataDir, { recursive: true });
18553
- await writeFile10(probe, "doctor", { encoding: "utf-8", mode: 384 });
18612
+ await writeFile9(probe, "doctor", { encoding: "utf-8", mode: 384 });
18554
18613
  } catch (err) {
18555
18614
  return {
18556
18615
  name: "Data directory",
@@ -18657,7 +18716,7 @@ function nodeVersionCheck() {
18657
18716
 
18658
18717
  // src/application/doctor/checks/onboarding-status.ts
18659
18718
  import { readFile as readFile13 } from "fs/promises";
18660
- import { join as join28 } from "path";
18719
+ import { join as join27 } from "path";
18661
18720
  var HARNESS_MARKER_PREFIX2 = "<!-- ralphctl onboard:";
18662
18721
  var MIN_HYBRID_PROSE_CHARS = 200;
18663
18722
  var CONTEXT_FILE_BY_PROVIDER = {
@@ -18678,7 +18737,7 @@ function extractPreamble(body) {
18678
18737
  return body.slice(0, idx);
18679
18738
  }
18680
18739
  async function classifyContextFile(repoPath, relPath) {
18681
- const fullPath = join28(repoPath, relPath);
18740
+ const fullPath = join27(repoPath, relPath);
18682
18741
  let body;
18683
18742
  try {
18684
18743
  body = await readFile13(fullPath, "utf-8");
@@ -18761,10 +18820,10 @@ async function onboardingStatusCheck(deps) {
18761
18820
 
18762
18821
  // src/application/doctor/checks/project-paths-exist.ts
18763
18822
  import { stat as stat5 } from "fs/promises";
18764
- import { join as join29 } from "path";
18823
+ import { join as join28 } from "path";
18765
18824
  async function isGitDir(path) {
18766
18825
  try {
18767
- const s = await stat5(join29(path, ".git"));
18826
+ const s = await stat5(join28(path, ".git"));
18768
18827
  return s.isDirectory() || s.isFile();
18769
18828
  } catch {
18770
18829
  return false;
@@ -18825,9 +18884,9 @@ async function projectPathsExistCheck(deps) {
18825
18884
  }
18826
18885
 
18827
18886
  // src/application/doctor/checks/session-log-path.ts
18828
- import { join as join30 } from "path";
18887
+ import { join as join29 } from "path";
18829
18888
  function sessionLogPathCheck(deps) {
18830
- const file = join30(deps.storage.logsDir, `${deps.sessionId}.jsonl`);
18889
+ const file = join29(deps.storage.logsDir, `${deps.sessionId}.jsonl`);
18831
18890
  return Promise.resolve({
18832
18891
  name: "Session log path",
18833
18892
  status: "pass",
@@ -18967,12 +19026,12 @@ function DoctorView() {
18967
19026
 
18968
19027
  // src/application/tui/views/browse/progress-view.tsx
18969
19028
  import { useEffect as useEffect39, useState as useState25 } from "react";
18970
- import { join as join32 } from "path";
19029
+ import { join as join31 } from "path";
18971
19030
  import { Box as Box34, Text as Text29 } from "ink";
18972
19031
 
18973
19032
  // src/business/usecases/sprint/show-progress.ts
18974
19033
  import { readFile as readFile14 } from "fs/promises";
18975
- import { join as join31 } from "path";
19034
+ import { join as join30 } from "path";
18976
19035
  var DEFAULT_STALE_THRESHOLD_HOURS = 24;
18977
19036
  var STALE_THRESHOLD_HOURS = DEFAULT_STALE_THRESHOLD_HOURS;
18978
19037
  var ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/;
@@ -19058,7 +19117,7 @@ function detectBranchInconsistency(sprint, projects, external) {
19058
19117
  return out;
19059
19118
  }
19060
19119
  var ShowProgressUseCase = class {
19061
- 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")) {
19062
19121
  this.sprints = sprints;
19063
19122
  this.tasks = tasks;
19064
19123
  this.projects = projects;
@@ -19154,7 +19213,7 @@ function ProgressView() {
19154
19213
  deps.projectRepo,
19155
19214
  deps.external,
19156
19215
  void 0,
19157
- (id) => join32(String(deps.storage.sprintsDir), String(id), "progress.md")
19216
+ (id) => join31(String(deps.storage.sprintsDir), String(id), "progress.md")
19158
19217
  );
19159
19218
  const result = await uc.execute({ sprintId, now: IsoTimestamp.now() });
19160
19219
  if (cancel.current) return;
@@ -19257,14 +19316,14 @@ function Timeline({ report }) {
19257
19316
 
19258
19317
  // src/application/tui/views/crud/sprint-export-requirements-view.tsx
19259
19318
  import { useEffect as useEffect40 } from "react";
19260
- import { writeFile as writeFile11 } from "fs/promises";
19319
+ import { writeFile as writeFile10 } from "fs/promises";
19261
19320
  import { isAbsolute, resolve } from "path";
19262
19321
 
19263
19322
  // src/business/usecases/sprint/export-requirements.ts
19264
19323
  import { readFile as readFile15 } from "fs/promises";
19265
19324
  var ExportRequirementsUseCase = class {
19266
- constructor(writeFile15, readJsonFile2 = (p) => readFile15(p, "utf-8")) {
19267
- this.writeFile = writeFile15;
19325
+ constructor(writeFile14, readJsonFile2 = (p) => readFile15(p, "utf-8")) {
19326
+ this.writeFile = writeFile14;
19268
19327
  this.readJsonFile = readJsonFile2;
19269
19328
  }
19270
19329
  writeFile;
@@ -19337,7 +19396,7 @@ function SprintExportRequirementsView() {
19337
19396
  const finalPath = trimmed.length === 0 ? defaultPath : isAbsolute(trimmed) ? trimmed : resolve(process.cwd(), trimmed);
19338
19397
  setStep("Writing file\u2026");
19339
19398
  const aggregatePath = resolveStoragePaths().requirementsAggregateFile(sprintId);
19340
- const uc = new ExportRequirementsUseCase((p, b) => writeFile11(p, b, "utf-8"));
19399
+ const uc = new ExportRequirementsUseCase((p, b) => writeFile10(p, b, "utf-8"));
19341
19400
  const result = await uc.execute({
19342
19401
  aggregatePath,
19343
19402
  outputPath: AbsolutePath.trustString(finalPath)
@@ -19374,16 +19433,16 @@ function SprintExportRequirementsView() {
19374
19433
 
19375
19434
  // src/application/tui/views/crud/sprint-export-context-view.tsx
19376
19435
  import { useEffect as useEffect41 } from "react";
19377
- import { writeFile as writeFile12 } from "fs/promises";
19436
+ import { writeFile as writeFile11 } from "fs/promises";
19378
19437
  import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
19379
19438
 
19380
19439
  // src/business/usecases/sprint/export-context.ts
19381
19440
  var ExportContextUseCase = class {
19382
- constructor(sprints, tasks, projects, writeFile15) {
19441
+ constructor(sprints, tasks, projects, writeFile14) {
19383
19442
  this.sprints = sprints;
19384
19443
  this.tasks = tasks;
19385
19444
  this.projects = projects;
19386
- this.writeFile = writeFile15;
19445
+ this.writeFile = writeFile14;
19387
19446
  }
19388
19447
  sprints;
19389
19448
  tasks;
@@ -19545,7 +19604,7 @@ function SprintExportContextView() {
19545
19604
  deps.sprintRepo,
19546
19605
  deps.taskRepo,
19547
19606
  deps.projectRepo,
19548
- (p, b) => writeFile12(p, b, "utf-8")
19607
+ (p, b) => writeFile11(p, b, "utf-8")
19549
19608
  );
19550
19609
  const result = await uc.execute({
19551
19610
  sprintId,
@@ -20128,7 +20187,7 @@ async function handleCompletionRequest(program, deps) {
20128
20187
  // src/application/cli/commands/completion-install.ts
20129
20188
  import { appendFile as appendFile3, readFile as readFile16 } from "fs/promises";
20130
20189
  import { homedir as homedir2 } from "os";
20131
- import { join as join33 } from "path";
20190
+ import { join as join32 } from "path";
20132
20191
  import * as c from "colorette";
20133
20192
 
20134
20193
  // src/application/cli/exit-codes.ts
@@ -20238,11 +20297,11 @@ function rcFileForShell(shell) {
20238
20297
  const home = homedir2();
20239
20298
  switch (shell) {
20240
20299
  case "bash":
20241
- return join33(home, ".bashrc");
20300
+ return join32(home, ".bashrc");
20242
20301
  case "zsh":
20243
- return join33(home, ".zshrc");
20302
+ return join32(home, ".zshrc");
20244
20303
  case "fish":
20245
- return join33(home, ".config", "fish", "config.fish");
20304
+ return join32(home, ".config", "fish", "config.fish");
20246
20305
  }
20247
20306
  }
20248
20307
  function runCompletionShow(requestedShell) {
@@ -21133,12 +21192,12 @@ async function runSprintClose(deps, id) {
21133
21192
  }
21134
21193
 
21135
21194
  // src/application/cli/commands/sprint-context.ts
21136
- import { writeFile as writeFile14 } from "fs/promises";
21195
+ import { writeFile as writeFile13 } from "fs/promises";
21137
21196
  import { isAbsolute as isAbsolute4, resolve as resolve4 } from "path";
21138
21197
  import * as c16 from "colorette";
21139
21198
 
21140
21199
  // src/application/cli/commands/sprint-requirements.ts
21141
- import { writeFile as writeFile13 } from "fs/promises";
21200
+ import { writeFile as writeFile12 } from "fs/promises";
21142
21201
  import { isAbsolute as isAbsolute3, resolve as resolve3 } from "path";
21143
21202
  import * as c15 from "colorette";
21144
21203
  function attachSprintRequirements(group, deps) {
@@ -21155,7 +21214,7 @@ async function runSprintRequirements(deps, id, opts) {
21155
21214
  if (!sprintR.ok) return sprintR;
21156
21215
  const outPath = resolveOutputPath(opts.output, `${String(sprintR.value)}-requirements.md`);
21157
21216
  const aggregatePath = resolveStoragePaths().requirementsAggregateFile(sprintR.value);
21158
- const uc = new ExportRequirementsUseCase((path, body) => writeFile13(path, body, "utf-8"));
21217
+ const uc = new ExportRequirementsUseCase((path, body) => writeFile12(path, body, "utf-8"));
21159
21218
  return uc.execute({ aggregatePath, outputPath: outPath });
21160
21219
  },
21161
21220
  format: (_d, out) => `${c15.green("wrote")} ${String(out.path)} (${String(out.byteCount)} bytes)`
@@ -21222,7 +21281,7 @@ async function runSprintContext(deps, id, opts) {
21222
21281
  deps.sprintRepo,
21223
21282
  deps.taskRepo,
21224
21283
  deps.projectRepo,
21225
- (path, body) => writeFile14(path, body, "utf-8")
21284
+ (path, body) => writeFile13(path, body, "utf-8")
21226
21285
  );
21227
21286
  return uc.execute({ sprintId: sprintR.value, outputPath: outPath });
21228
21287
  },
@@ -21490,7 +21549,7 @@ async function runSprintPlan(deps, opts) {
21490
21549
  }
21491
21550
 
21492
21551
  // src/application/cli/commands/sprint-progress.ts
21493
- import { join as join34 } from "path";
21552
+ import { join as join33 } from "path";
21494
21553
  import * as c19 from "colorette";
21495
21554
  function attachSprintProgress(group, deps) {
21496
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) => {
@@ -21534,7 +21593,7 @@ async function runSprintProgress(deps, id, opts) {
21534
21593
  deps.projectRepo,
21535
21594
  deps.external,
21536
21595
  void 0,
21537
- (sprintId) => join34(String(deps.storage.sprintsDir), String(sprintId), "progress.md")
21596
+ (sprintId) => join33(String(deps.storage.sprintsDir), String(sprintId), "progress.md")
21538
21597
  );
21539
21598
  return uc.execute({ sprintId: idR.value, now: IsoTimestamp.now() });
21540
21599
  },
@@ -21751,7 +21810,10 @@ ${formatTicketsTable(sprint)}`
21751
21810
  // src/application/cli/commands/sprint-start.ts
21752
21811
  import * as c23 from "colorette";
21753
21812
  function attachSprintStart(group, deps) {
21754
- 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) => {
21755
21817
  const code = await runSprintStart(deps, opts);
21756
21818
  if (code !== EXIT_SUCCESS) process.exitCode = code;
21757
21819
  });
@@ -22269,7 +22331,7 @@ async function runTicketRemove(deps, opts) {
22269
22331
  // package.json
22270
22332
  var package_default = {
22271
22333
  name: "ralphctl",
22272
- version: "0.6.2",
22334
+ version: "0.6.3",
22273
22335
  description: "Agent harness for long-running AI coding tasks \u2014 orchestrates Claude Code & GitHub Copilot across repositories",
22274
22336
  homepage: "https://github.com/lukas-grigis/ralphctl",
22275
22337
  type: "module",