codeharness 0.17.7 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -19,8 +19,8 @@ import {
19
19
  import { Command } from "commander";
20
20
 
21
21
  // src/commands/init.ts
22
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
23
- import { join as join6, basename as basename2 } from "path";
22
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
23
+ import { join as join7, basename as basename2 } from "path";
24
24
 
25
25
  // src/lib/output.ts
26
26
  function ok(message, options) {
@@ -865,8 +865,8 @@ function configureHookCoexistence(dir) {
865
865
 
866
866
  // src/lib/bmad.ts
867
867
  import { execFileSync as execFileSync4 } from "child_process";
868
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
869
- import { join as join5 } from "path";
868
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
869
+ import { join as join6 } from "path";
870
870
 
871
871
  // src/lib/patch-engine.ts
872
872
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
@@ -935,133 +935,81 @@ function removePatch(filePath, patchName) {
935
935
  }
936
936
 
937
937
  // src/templates/bmad-patches.ts
938
+ import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
939
+ import { join as join5, dirname as dirname2 } from "path";
940
+ import { fileURLToPath } from "url";
941
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
942
+ function readPatchFile(name) {
943
+ const projectRoot = join5(__dirname, "..", "..");
944
+ const projectPath = join5(projectRoot, "patches", `${name}.md`);
945
+ if (existsSync5(projectPath)) {
946
+ return readFileSync5(projectPath, "utf-8").trim();
947
+ }
948
+ const pkgPath = join5(__dirname, "..", "..", "patches", `${name}.md`);
949
+ if (existsSync5(pkgPath)) {
950
+ return readFileSync5(pkgPath, "utf-8").trim();
951
+ }
952
+ return null;
953
+ }
938
954
  function storyVerificationPatch() {
939
- return `## Verification Requirements
955
+ return readPatchFile("story-verification") ?? `## Verification Requirements
940
956
 
941
- - [ ] Showboat proof document created (\`docs/exec-plans/active/<story-key>.proof.md\`)
942
- - [ ] All acceptance criteria verified with real-world evidence
957
+ - [ ] Showboat proof document created (verification/<story-key>-proof.md)
958
+ - [ ] All acceptance criteria verified with real-world evidence via docker exec
943
959
  - [ ] Test coverage meets target (100%)
944
960
 
945
- ### Verification Tags
946
-
947
- For each AC, append a verification tag to indicate how it can be verified:
948
- - \`<!-- verification: cli-verifiable -->\` \u2014 AC can be verified by running CLI commands in a subprocess
949
- - \`<!-- verification: integration-required -->\` \u2014 AC requires integration testing, multi-system interaction, or manual verification
950
-
951
- ACs referencing workflows, sprint planning, user sessions, or external system interactions should be tagged as \`integration-required\`. If no tag is present, a heuristic classifier will attempt to determine verifiability at runtime.
952
-
953
961
  ## Documentation Requirements
954
962
 
955
- - [ ] Relevant AGENTS.md files updated (list modules touched)
956
- - [ ] Exec-plan created in \`docs/exec-plans/active/<story-key>.md\`
963
+ - [ ] Relevant AGENTS.md files updated
964
+ - [ ] Exec-plan created in docs/exec-plans/active/<story-key>.md
957
965
 
958
966
  ## Testing Requirements
959
967
 
960
968
  - [ ] Unit tests written for all new/changed code
961
- - [ ] Integration tests for cross-module interactions
962
969
  - [ ] Coverage target: 100%`;
963
970
  }
964
971
  function devEnforcementPatch() {
965
- return `## Codeharness Enforcement
972
+ return readPatchFile("dev-enforcement") ?? `## Codeharness Enforcement
966
973
 
967
974
  ### Observability Check
968
975
  - [ ] Query VictoriaLogs after test runs to verify telemetry flows
969
- - [ ] Confirm logs, metrics, and traces are being collected
970
976
 
971
977
  ### Documentation Update
972
978
  - [ ] AGENTS.md updated for all changed modules
973
- - [ ] Exec-plan reflects current implementation state
974
979
 
975
980
  ### Test Enforcement
976
- - [ ] All tests pass (\`npm test\` / \`pytest\`)
977
- - [ ] Coverage gate: 100% of new/changed code
978
- - [ ] No skipped or pending tests without justification`;
981
+ - [ ] All tests pass
982
+ - [ ] Coverage gate: 100% of new/changed code`;
979
983
  }
980
984
  function reviewEnforcementPatch() {
981
- return `## Codeharness Review Gates
985
+ return readPatchFile("review-enforcement") ?? `## Codeharness Review Gates
982
986
 
983
987
  ### Verification
984
- - [ ] Showboat proof document exists and passes \`showboat verify\`
988
+ - [ ] Proof document exists and passes codeharness verify
985
989
  - [ ] All acceptance criteria have evidence in proof document
986
990
 
987
- ### Documentation Freshness
988
- - [ ] AGENTS.md is current for all changed modules
989
- - [ ] No stale references to removed or renamed modules
990
-
991
991
  ### Coverage
992
- - [ ] Coverage delta reported (before vs after)
993
- - [ ] No coverage regression in changed files
994
- - [ ] Overall coverage meets project target`;
992
+ - [ ] No coverage regression in changed files`;
995
993
  }
996
994
  function retroEnforcementPatch() {
997
- return `## Codeharness Quality Metrics
995
+ return readPatchFile("retro-enforcement") ?? `## Codeharness Quality Metrics
998
996
 
999
997
  ### Verification Effectiveness
1000
998
  - [ ] How many ACs were caught by verification vs manual review?
1001
- - [ ] Were there any false positives in Showboat proofs?
1002
- - [ ] Time spent on verification vs value delivered
1003
-
1004
- ### Documentation Health
1005
- - [ ] AGENTS.md accuracy grade (A/B/C/D/F)
1006
- - [ ] Exec-plans completeness \u2014 are all active stories documented?
1007
- - [ ] Stale documentation identified and cleaned up
999
+ - [ ] Were there any false positives in proofs?
1008
1000
 
1009
1001
  ### Test Quality
1010
- - [ ] Coverage trend (improving, stable, declining)
1011
- - [ ] Test reliability \u2014 any flaky tests introduced?
1012
- - [ ] Integration test coverage for cross-module interactions`;
1002
+ - [ ] Coverage trend (improving, stable, declining)`;
1013
1003
  }
1014
1004
  function sprintBeadsPatch() {
1015
- return `## Codeharness Backlog Integration
1016
-
1017
- ### Pre-Triage Import Verification
1018
- - [ ] Confirm \`codeharness retro-import\` was run for all completed retrospectives
1019
- - [ ] Confirm \`codeharness github-import\` was run to pull labeled GitHub issues
1020
- - [ ] Verify all sources are reflected in beads before starting triage
1021
-
1022
- ### Beads Issue Status
1023
- - [ ] Run \`bd ready\` to display issues ready for development
1024
- - [ ] Review beads issue counts by status (open, in-progress, done)
1025
- - [ ] Verify issues from all sources are visible: retro (\`[gap:retro:...]\`), GitHub (\`[source:github:...]\`), and manual
1026
- - [ ] Verify no blocked issues without documented reason
1005
+ return readPatchFile("sprint-planning") ?? `## Codeharness Sprint Planning
1027
1006
 
1028
- ### Sprint Readiness
1029
- - [ ] All selected stories have corresponding beads issues
1030
- - [ ] Dependencies between stories are reflected in beads deps
1031
- - [ ] Capacity aligns with estimated story complexity`;
1007
+ - [ ] Review unresolved retrospective action items
1008
+ - [ ] Import from all backlog sources before triage
1009
+ - [ ] Verify story ACs are testable via CLI + Docker`;
1032
1010
  }
1033
1011
  function sprintPlanningRetroPatch() {
1034
- return `## Retrospective Action Items Review
1035
-
1036
- ### Unresolved Action Items from Previous Retrospectives
1037
-
1038
- Before starting sprint planning, review all completed retrospectives for unresolved action items:
1039
-
1040
- 1. **Scan for retrospective files:** Look for all \`epic-N-retrospective.md\` files in \`_bmad-output/implementation-artifacts/\`
1041
- 2. **Import retro findings to beads:** For each retrospective not yet imported, run \`codeharness retro-import --epic N\` to classify findings and create beads issues with \`[gap:retro:epic-N-item-M]\` gap-ids
1042
- 3. **Import GitHub issues to beads:** Run \`codeharness github-import\` to pull labeled issues into beads with \`[source:github:owner/repo#N]\` gap-ids
1043
- 4. **Display combined backlog:** Run \`bd ready\` to present the unified backlog containing retro findings, GitHub issues, and manually created issues
1044
- 5. **Identify unresolved items:** Filter for action items that are NOT marked as completed/done
1045
- 6. **Surface during planning:** Present unresolved items to the team before selecting stories for the sprint
1046
-
1047
- ### Source-Aware Backlog Presentation
1048
-
1049
- When presenting the backlog during triage, issues should be identifiable by source:
1050
-
1051
- - **Retro findings** have gap-ids matching \`[gap:retro:...]\` \u2014 originated from retrospective action items
1052
- - **GitHub issues** have gap-ids matching \`[source:github:...]\` \u2014 imported from GitHub via label query
1053
- - **Manual issues** have no gap-id prefix \u2014 created directly in beads
1054
-
1055
- ### Integration with Sprint Planning
1056
-
1057
- - [ ] All \`epic-N-retrospective.md\` files scanned for action items
1058
- - [ ] \`codeharness retro-import --epic N\` run for each unimported retrospective
1059
- - [ ] \`codeharness github-import\` run to pull labeled GitHub issues
1060
- - [ ] \`bd ready\` run to display combined backlog from all sources
1061
- - [ ] Unresolved action items listed and reviewed
1062
- - [ ] Relevant action items incorporated into sprint goals or new stories
1063
- - [ ] Recurring issues from multiple retros flagged for systemic fixes
1064
- - [ ] All sources (retro, GitHub, manual) triaged uniformly \u2014 no source left unreviewed`;
1012
+ return sprintBeadsPatch();
1065
1013
  }
1066
1014
  var PATCH_TEMPLATES = {
1067
1015
  "story-verification": storyVerificationPatch,
@@ -1090,15 +1038,15 @@ var PATCH_TARGETS = {
1090
1038
  "sprint-retro": "bmm/workflows/4-implementation/sprint-planning/instructions.md"
1091
1039
  };
1092
1040
  function isBmadInstalled(dir) {
1093
- const bmadDir = join5(dir ?? process.cwd(), "_bmad");
1094
- return existsSync5(bmadDir);
1041
+ const bmadDir = join6(dir ?? process.cwd(), "_bmad");
1042
+ return existsSync6(bmadDir);
1095
1043
  }
1096
1044
  function detectBmadVersion(dir) {
1097
1045
  const root = dir ?? process.cwd();
1098
- const moduleYamlPath = join5(root, "_bmad", "core", "module.yaml");
1099
- if (existsSync5(moduleYamlPath)) {
1046
+ const moduleYamlPath = join6(root, "_bmad", "core", "module.yaml");
1047
+ if (existsSync6(moduleYamlPath)) {
1100
1048
  try {
1101
- const content = readFileSync5(moduleYamlPath, "utf-8");
1049
+ const content = readFileSync6(moduleYamlPath, "utf-8");
1102
1050
  const versionMatch = content.match(/version:\s*["']?([^\s"']+)["']?/);
1103
1051
  if (versionMatch) {
1104
1052
  return versionMatch[1];
@@ -1106,17 +1054,17 @@ function detectBmadVersion(dir) {
1106
1054
  } catch {
1107
1055
  }
1108
1056
  }
1109
- const versionFilePath = join5(root, "_bmad", "VERSION");
1110
- if (existsSync5(versionFilePath)) {
1057
+ const versionFilePath = join6(root, "_bmad", "VERSION");
1058
+ if (existsSync6(versionFilePath)) {
1111
1059
  try {
1112
- return readFileSync5(versionFilePath, "utf-8").trim() || null;
1060
+ return readFileSync6(versionFilePath, "utf-8").trim() || null;
1113
1061
  } catch {
1114
1062
  }
1115
1063
  }
1116
- const packageJsonPath = join5(root, "_bmad", "package.json");
1117
- if (existsSync5(packageJsonPath)) {
1064
+ const packageJsonPath = join6(root, "_bmad", "package.json");
1065
+ if (existsSync6(packageJsonPath)) {
1118
1066
  try {
1119
- const pkg = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
1067
+ const pkg = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
1120
1068
  return pkg.version ?? null;
1121
1069
  } catch {
1122
1070
  }
@@ -1134,15 +1082,27 @@ function installBmad(dir) {
1134
1082
  };
1135
1083
  }
1136
1084
  const cmdStr = "npx bmad-method install";
1137
- try {
1138
- execFileSync4("npx", ["bmad-method", "install"], {
1139
- stdio: "pipe",
1140
- timeout: 6e4,
1141
- cwd: root
1142
- });
1143
- } catch (err) {
1144
- const message = err instanceof Error ? err.message : String(err);
1145
- throw new BmadError(cmdStr, message);
1085
+ const maxAttempts = 3;
1086
+ let lastError = "";
1087
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1088
+ try {
1089
+ execFileSync4("npx", ["bmad-method", "install"], {
1090
+ stdio: "pipe",
1091
+ timeout: 12e4,
1092
+ // 2 min npx may download the package
1093
+ cwd: root
1094
+ });
1095
+ lastError = "";
1096
+ break;
1097
+ } catch (err) {
1098
+ lastError = err instanceof Error ? err.message : String(err);
1099
+ if (attempt < maxAttempts) {
1100
+ execFileSync4("sleep", ["2"]);
1101
+ }
1102
+ }
1103
+ }
1104
+ if (lastError) {
1105
+ throw new BmadError(cmdStr, lastError);
1146
1106
  }
1147
1107
  if (!isBmadInstalled(root)) {
1148
1108
  throw new BmadError(cmdStr, "_bmad/ directory was not created after successful npx bmad-method install");
@@ -1158,8 +1118,8 @@ function applyAllPatches(dir) {
1158
1118
  const root = dir ?? process.cwd();
1159
1119
  const results = [];
1160
1120
  for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
1161
- const targetFile = join5(root, "_bmad", relativePath);
1162
- if (!existsSync5(targetFile)) {
1121
+ const targetFile = join6(root, "_bmad", relativePath);
1122
+ if (!existsSync6(targetFile)) {
1163
1123
  warn(`Patch target not found: ${relativePath}`);
1164
1124
  results.push({
1165
1125
  patchName,
@@ -1206,12 +1166,12 @@ function applyAllPatches(dir) {
1206
1166
  function detectBmalph(dir) {
1207
1167
  const root = dir ?? process.cwd();
1208
1168
  const files = [];
1209
- const ralphRcPath = join5(root, ".ralph", ".ralphrc");
1210
- if (existsSync5(ralphRcPath)) {
1169
+ const ralphRcPath = join6(root, ".ralph", ".ralphrc");
1170
+ if (existsSync6(ralphRcPath)) {
1211
1171
  files.push(".ralph/.ralphrc");
1212
1172
  }
1213
- const dotRalphDir = join5(root, ".ralph");
1214
- if (existsSync5(dotRalphDir)) {
1173
+ const dotRalphDir = join6(root, ".ralph");
1174
+ if (existsSync6(dotRalphDir)) {
1215
1175
  if (files.length === 0) {
1216
1176
  files.push(".ralph/");
1217
1177
  }
@@ -1226,10 +1186,10 @@ function getStoryFilePath(storyKey) {
1226
1186
  return `_bmad-output/implementation-artifacts/${storyKey}.md`;
1227
1187
  }
1228
1188
  function parseEpicsFile(filePath) {
1229
- if (!existsSync5(filePath)) {
1189
+ if (!existsSync6(filePath)) {
1230
1190
  return [];
1231
1191
  }
1232
- const content = readFileSync5(filePath, "utf-8");
1192
+ const content = readFileSync6(filePath, "utf-8");
1233
1193
  if (!content.trim()) {
1234
1194
  return [];
1235
1195
  }
@@ -1443,12 +1403,12 @@ function getInstallCommand(stack) {
1443
1403
  }
1444
1404
 
1445
1405
  // src/commands/init.ts
1446
- var HARNESS_VERSION = true ? "0.17.7" : "0.0.0-dev";
1406
+ var HARNESS_VERSION = true ? "0.18.0" : "0.0.0-dev";
1447
1407
  function getProjectName(projectDir) {
1448
1408
  try {
1449
- const pkgPath = join6(projectDir, "package.json");
1450
- if (existsSync6(pkgPath)) {
1451
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
1409
+ const pkgPath = join7(projectDir, "package.json");
1410
+ if (existsSync7(pkgPath)) {
1411
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
1452
1412
  if (pkg.name && typeof pkg.name === "string") {
1453
1413
  return pkg.name;
1454
1414
  }
@@ -1558,7 +1518,7 @@ function registerInitCommand(program) {
1558
1518
  }
1559
1519
  };
1560
1520
  const statePath = getStatePath(projectDir);
1561
- if (existsSync6(statePath)) {
1521
+ if (existsSync7(statePath)) {
1562
1522
  try {
1563
1523
  const existingState = readState(projectDir);
1564
1524
  const legacyObsDisabled = existingState.enforcement.observability === false;
@@ -1820,27 +1780,27 @@ function registerInitCommand(program) {
1820
1780
  if (!isJson) {
1821
1781
  ok("State file: .claude/codeharness.local.md created");
1822
1782
  }
1823
- const agentsMdPath = join6(projectDir, "AGENTS.md");
1824
- if (!existsSync6(agentsMdPath)) {
1783
+ const agentsMdPath = join7(projectDir, "AGENTS.md");
1784
+ if (!existsSync7(agentsMdPath)) {
1825
1785
  const agentsMdContent = generateAgentsMdContent(projectDir, stack);
1826
1786
  generateFile(agentsMdPath, agentsMdContent);
1827
1787
  result.documentation.agents_md = "created";
1828
1788
  } else {
1829
1789
  result.documentation.agents_md = "exists";
1830
1790
  }
1831
- const docsDir = join6(projectDir, "docs");
1832
- if (!existsSync6(docsDir)) {
1833
- generateFile(join6(docsDir, "index.md"), generateDocsIndexContent());
1834
- generateFile(join6(docsDir, "exec-plans", "active", ".gitkeep"), "");
1835
- generateFile(join6(docsDir, "exec-plans", "completed", ".gitkeep"), "");
1836
- generateFile(join6(docsDir, "quality", ".gitkeep"), DO_NOT_EDIT_HEADER);
1837
- generateFile(join6(docsDir, "generated", ".gitkeep"), DO_NOT_EDIT_HEADER);
1791
+ const docsDir = join7(projectDir, "docs");
1792
+ if (!existsSync7(docsDir)) {
1793
+ generateFile(join7(docsDir, "index.md"), generateDocsIndexContent());
1794
+ generateFile(join7(docsDir, "exec-plans", "active", ".gitkeep"), "");
1795
+ generateFile(join7(docsDir, "exec-plans", "completed", ".gitkeep"), "");
1796
+ generateFile(join7(docsDir, "quality", ".gitkeep"), DO_NOT_EDIT_HEADER);
1797
+ generateFile(join7(docsDir, "generated", ".gitkeep"), DO_NOT_EDIT_HEADER);
1838
1798
  result.documentation.docs_scaffold = "created";
1839
1799
  } else {
1840
1800
  result.documentation.docs_scaffold = "exists";
1841
1801
  }
1842
- const readmePath = join6(projectDir, "README.md");
1843
- if (!existsSync6(readmePath)) {
1802
+ const readmePath = join7(projectDir, "README.md");
1803
+ if (!existsSync7(readmePath)) {
1844
1804
  let cliHelpOutput = "";
1845
1805
  try {
1846
1806
  const { execFileSync: execFileSync8 } = await import("child_process");
@@ -2047,7 +2007,7 @@ function registerInitCommand(program) {
2047
2007
  }
2048
2008
 
2049
2009
  // src/commands/bridge.ts
2050
- import { existsSync as existsSync7 } from "fs";
2010
+ import { existsSync as existsSync8 } from "fs";
2051
2011
  function registerBridgeCommand(program) {
2052
2012
  program.command("bridge").description("Bridge BMAD epics/stories into beads task store").option("--epics <path>", "Path to BMAD epics markdown file").option("--dry-run", "Parse and display without creating beads issues").action((opts, cmd) => {
2053
2013
  const globalOpts = cmd.optsWithGlobals();
@@ -2059,7 +2019,7 @@ function registerBridgeCommand(program) {
2059
2019
  process.exitCode = 2;
2060
2020
  return;
2061
2021
  }
2062
- if (!existsSync7(epicsPath)) {
2022
+ if (!existsSync8(epicsPath)) {
2063
2023
  fail(`Epics file not found: ${epicsPath}`, { json: isJson });
2064
2024
  process.exitCode = 1;
2065
2025
  return;
@@ -2132,13 +2092,13 @@ function registerBridgeCommand(program) {
2132
2092
 
2133
2093
  // src/commands/run.ts
2134
2094
  import { spawn } from "child_process";
2135
- import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
2136
- import { join as join8, dirname as dirname2 } from "path";
2137
- import { fileURLToPath } from "url";
2095
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
2096
+ import { join as join9, dirname as dirname3 } from "path";
2097
+ import { fileURLToPath as fileURLToPath2 } from "url";
2138
2098
 
2139
2099
  // src/lib/beads-sync.ts
2140
- import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
2141
- import { join as join7 } from "path";
2100
+ import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
2101
+ import { join as join8 } from "path";
2142
2102
  import { parse as parse2 } from "yaml";
2143
2103
  var BEADS_TO_STORY_STATUS = {
2144
2104
  open: "in-progress",
@@ -2169,10 +2129,10 @@ function resolveStoryFilePath(beadsIssue) {
2169
2129
  return trimmed;
2170
2130
  }
2171
2131
  function readStoryFileStatus(filePath) {
2172
- if (!existsSync8(filePath)) {
2132
+ if (!existsSync9(filePath)) {
2173
2133
  return null;
2174
2134
  }
2175
- const content = readFileSync7(filePath, "utf-8");
2135
+ const content = readFileSync8(filePath, "utf-8");
2176
2136
  const match = content.match(/^Status:\s*(.+)$/m);
2177
2137
  if (!match) {
2178
2138
  return null;
@@ -2180,7 +2140,7 @@ function readStoryFileStatus(filePath) {
2180
2140
  return match[1].trim();
2181
2141
  }
2182
2142
  function updateStoryFileStatus(filePath, newStatus) {
2183
- const content = readFileSync7(filePath, "utf-8");
2143
+ const content = readFileSync8(filePath, "utf-8");
2184
2144
  const statusRegex = /^Status:\s*.+$/m;
2185
2145
  if (statusRegex.test(content)) {
2186
2146
  const updated = content.replace(statusRegex, `Status: ${newStatus}`);
@@ -2199,12 +2159,12 @@ function updateStoryFileStatus(filePath, newStatus) {
2199
2159
  var SPRINT_STATUS_PATH = "_bmad-output/implementation-artifacts/sprint-status.yaml";
2200
2160
  function readSprintStatus(dir) {
2201
2161
  const root = dir ?? process.cwd();
2202
- const filePath = join7(root, SPRINT_STATUS_PATH);
2203
- if (!existsSync8(filePath)) {
2162
+ const filePath = join8(root, SPRINT_STATUS_PATH);
2163
+ if (!existsSync9(filePath)) {
2204
2164
  return {};
2205
2165
  }
2206
2166
  try {
2207
- const content = readFileSync7(filePath, "utf-8");
2167
+ const content = readFileSync8(filePath, "utf-8");
2208
2168
  const parsed = parse2(content);
2209
2169
  if (!parsed || typeof parsed !== "object") {
2210
2170
  return {};
@@ -2220,12 +2180,12 @@ function readSprintStatus(dir) {
2220
2180
  }
2221
2181
  function updateSprintStatus(storyKey, newStatus, dir) {
2222
2182
  const root = dir ?? process.cwd();
2223
- const filePath = join7(root, SPRINT_STATUS_PATH);
2224
- if (!existsSync8(filePath)) {
2183
+ const filePath = join8(root, SPRINT_STATUS_PATH);
2184
+ if (!existsSync9(filePath)) {
2225
2185
  warn(`sprint-status.yaml not found at ${filePath}, skipping update`);
2226
2186
  return;
2227
2187
  }
2228
- const content = readFileSync7(filePath, "utf-8");
2188
+ const content = readFileSync8(filePath, "utf-8");
2229
2189
  const keyPattern = new RegExp(`^(\\s*${escapeRegExp(storyKey)}:\\s*)\\S+(.*)$`, "m");
2230
2190
  if (!keyPattern.test(content)) {
2231
2191
  return;
@@ -2252,8 +2212,8 @@ function slugify(title) {
2252
2212
  }
2253
2213
  function appendOnboardingEpicToSprint(stories, dir) {
2254
2214
  const root = dir ?? process.cwd();
2255
- const filePath = join7(root, SPRINT_STATUS_PATH);
2256
- if (!existsSync8(filePath)) {
2215
+ const filePath = join8(root, SPRINT_STATUS_PATH);
2216
+ if (!existsSync9(filePath)) {
2257
2217
  warn(`sprint-status.yaml not found at ${filePath}, cannot append onboarding epic`);
2258
2218
  return { epicNumber: -1, storyKeys: [] };
2259
2219
  }
@@ -2270,7 +2230,7 @@ function appendOnboardingEpicToSprint(stories, dir) {
2270
2230
  }
2271
2231
  lines.push(` epic-${epicNum}-retrospective: optional`);
2272
2232
  lines.push("");
2273
- const content = readFileSync7(filePath, "utf-8");
2233
+ const content = readFileSync8(filePath, "utf-8");
2274
2234
  const updated = content.trimEnd() + "\n" + lines.join("\n");
2275
2235
  writeFileSync5(filePath, updated, "utf-8");
2276
2236
  return { epicNumber: epicNum, storyKeys };
@@ -2305,7 +2265,7 @@ function syncBeadsToStoryFile(beadsId, beadsFns, dir) {
2305
2265
  };
2306
2266
  }
2307
2267
  const storyKey = storyKeyFromPath(storyFilePath);
2308
- const fullPath = join7(root, storyFilePath);
2268
+ const fullPath = join8(root, storyFilePath);
2309
2269
  const currentStoryStatus = readStoryFileStatus(fullPath);
2310
2270
  if (currentStoryStatus === null) {
2311
2271
  return {
@@ -2350,7 +2310,7 @@ function syncBeadsToStoryFile(beadsId, beadsFns, dir) {
2350
2310
  function syncStoryFileToBeads(storyKey, beadsFns, dir) {
2351
2311
  const root = dir ?? process.cwd();
2352
2312
  const storyFilePath = `_bmad-output/implementation-artifacts/${storyKey}.md`;
2353
- const fullPath = join7(root, storyFilePath);
2313
+ const fullPath = join8(root, storyFilePath);
2354
2314
  const currentStoryStatus = readStoryFileStatus(fullPath);
2355
2315
  if (currentStoryStatus === null) {
2356
2316
  return {
@@ -2439,10 +2399,10 @@ function syncClose(beadsId, beadsFns, dir) {
2439
2399
  };
2440
2400
  }
2441
2401
  const storyKey = storyKeyFromPath(storyFilePath);
2442
- const fullPath = join7(root, storyFilePath);
2402
+ const fullPath = join8(root, storyFilePath);
2443
2403
  const previousStatus = readStoryFileStatus(fullPath);
2444
2404
  if (previousStatus === null) {
2445
- if (!existsSync8(fullPath)) {
2405
+ if (!existsSync9(fullPath)) {
2446
2406
  return {
2447
2407
  storyKey,
2448
2408
  beadsId,
@@ -2587,16 +2547,16 @@ function generateRalphPrompt(config) {
2587
2547
  var SPRINT_STATUS_REL = "_bmad-output/implementation-artifacts/sprint-status.yaml";
2588
2548
  var STORY_KEY_PATTERN = /^\d+-\d+-/;
2589
2549
  function resolveRalphPath() {
2590
- const currentFile = fileURLToPath(import.meta.url);
2591
- const currentDir = dirname2(currentFile);
2592
- let root = dirname2(currentDir);
2550
+ const currentFile = fileURLToPath2(import.meta.url);
2551
+ const currentDir = dirname3(currentFile);
2552
+ let root = dirname3(currentDir);
2593
2553
  if (root.endsWith("/src") || root.endsWith("\\src")) {
2594
- root = dirname2(root);
2554
+ root = dirname3(root);
2595
2555
  }
2596
- return join8(root, "ralph", "ralph.sh");
2556
+ return join9(root, "ralph", "ralph.sh");
2597
2557
  }
2598
2558
  function resolvePluginDir() {
2599
- return join8(process.cwd(), ".claude");
2559
+ return join9(process.cwd(), ".claude");
2600
2560
  }
2601
2561
  function countStories(statuses) {
2602
2562
  let total = 0;
@@ -2647,19 +2607,19 @@ function registerRunCommand(program) {
2647
2607
  const isJson = !!globalOpts.json;
2648
2608
  const outputOpts = { json: isJson };
2649
2609
  const ralphPath = resolveRalphPath();
2650
- if (!existsSync9(ralphPath)) {
2610
+ if (!existsSync10(ralphPath)) {
2651
2611
  fail("Ralph loop not found \u2014 reinstall codeharness", outputOpts);
2652
2612
  process.exitCode = 1;
2653
2613
  return;
2654
2614
  }
2655
2615
  const pluginDir = resolvePluginDir();
2656
- if (!existsSync9(pluginDir)) {
2616
+ if (!existsSync10(pluginDir)) {
2657
2617
  fail("Plugin directory not found \u2014 run codeharness init first", outputOpts);
2658
2618
  process.exitCode = 1;
2659
2619
  return;
2660
2620
  }
2661
2621
  const projectDir = process.cwd();
2662
- const sprintStatusPath = join8(projectDir, SPRINT_STATUS_REL);
2622
+ const sprintStatusPath = join9(projectDir, SPRINT_STATUS_REL);
2663
2623
  const statuses = readSprintStatus(projectDir);
2664
2624
  const counts = countStories(statuses);
2665
2625
  if (counts.total === 0) {
@@ -2678,12 +2638,12 @@ function registerRunCommand(program) {
2678
2638
  process.exitCode = 1;
2679
2639
  return;
2680
2640
  }
2681
- const promptFile = join8(projectDir, "ralph", ".harness-prompt.md");
2682
- const flaggedFilePath = join8(projectDir, "ralph", ".flagged_stories");
2641
+ const promptFile = join9(projectDir, "ralph", ".harness-prompt.md");
2642
+ const flaggedFilePath = join9(projectDir, "ralph", ".flagged_stories");
2683
2643
  let flaggedStories;
2684
- if (existsSync9(flaggedFilePath)) {
2644
+ if (existsSync10(flaggedFilePath)) {
2685
2645
  try {
2686
- const flaggedContent = readFileSync8(flaggedFilePath, "utf-8");
2646
+ const flaggedContent = readFileSync9(flaggedFilePath, "utf-8");
2687
2647
  flaggedStories = flaggedContent.split("\n").filter((s) => s.trim().length > 0);
2688
2648
  } catch {
2689
2649
  }
@@ -2694,7 +2654,7 @@ function registerRunCommand(program) {
2694
2654
  flaggedStories
2695
2655
  });
2696
2656
  try {
2697
- mkdirSync3(dirname2(promptFile), { recursive: true });
2657
+ mkdirSync3(dirname3(promptFile), { recursive: true });
2698
2658
  writeFileSync6(promptFile, promptContent, "utf-8");
2699
2659
  } catch (err) {
2700
2660
  const message = err instanceof Error ? err.message : String(err);
@@ -2733,10 +2693,10 @@ function registerRunCommand(program) {
2733
2693
  });
2734
2694
  });
2735
2695
  if (isJson) {
2736
- const statusFile = join8(projectDir, "ralph", "status.json");
2737
- if (existsSync9(statusFile)) {
2696
+ const statusFile = join9(projectDir, "ralph", "status.json");
2697
+ if (existsSync10(statusFile)) {
2738
2698
  try {
2739
- const statusData = JSON.parse(readFileSync8(statusFile, "utf-8"));
2699
+ const statusData = JSON.parse(readFileSync9(statusFile, "utf-8"));
2740
2700
  const finalStatuses = readSprintStatus(projectDir);
2741
2701
  const finalCounts = countStories(finalStatuses);
2742
2702
  jsonOutput({
@@ -2786,11 +2746,11 @@ function registerRunCommand(program) {
2786
2746
  }
2787
2747
 
2788
2748
  // src/commands/verify.ts
2789
- import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
2790
- import { join as join11 } from "path";
2749
+ import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
2750
+ import { join as join12 } from "path";
2791
2751
 
2792
2752
  // src/lib/verify-parser.ts
2793
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
2753
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
2794
2754
  var UI_KEYWORDS = [
2795
2755
  "agent-browser",
2796
2756
  "screenshot",
@@ -2860,12 +2820,12 @@ function classifyAC(description) {
2860
2820
  return "general";
2861
2821
  }
2862
2822
  function parseStoryACs(storyFilePath) {
2863
- if (!existsSync10(storyFilePath)) {
2823
+ if (!existsSync11(storyFilePath)) {
2864
2824
  throw new Error(
2865
2825
  `Story file not found: ${storyFilePath}. Ensure the story file exists at the expected path.`
2866
2826
  );
2867
2827
  }
2868
- const content = readFileSync9(storyFilePath, "utf-8");
2828
+ const content = readFileSync10(storyFilePath, "utf-8");
2869
2829
  const lines = content.split("\n");
2870
2830
  let acSectionStart = -1;
2871
2831
  for (let i = 0; i < lines.length; i++) {
@@ -2926,21 +2886,21 @@ function parseStoryACs(storyFilePath) {
2926
2886
 
2927
2887
  // src/lib/verify.ts
2928
2888
  import { execFileSync as execFileSync5 } from "child_process";
2929
- import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
2930
- import { join as join10 } from "path";
2889
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
2890
+ import { join as join11 } from "path";
2931
2891
 
2932
2892
  // src/lib/doc-health.ts
2933
2893
  import { execSync } from "child_process";
2934
2894
  import {
2935
- existsSync as existsSync11,
2895
+ existsSync as existsSync12,
2936
2896
  mkdirSync as mkdirSync4,
2937
- readFileSync as readFileSync10,
2897
+ readFileSync as readFileSync11,
2938
2898
  readdirSync as readdirSync2,
2939
2899
  statSync,
2940
2900
  unlinkSync,
2941
2901
  writeFileSync as writeFileSync7
2942
2902
  } from "fs";
2943
- import { join as join9, relative } from "path";
2903
+ import { join as join10, relative } from "path";
2944
2904
  var DO_NOT_EDIT_HEADER2 = "<!-- DO NOT EDIT MANUALLY";
2945
2905
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
2946
2906
  var DEFAULT_MODULE_THRESHOLD = 3;
@@ -2962,7 +2922,7 @@ function findModules(dir, threshold) {
2962
2922
  let sourceCount = 0;
2963
2923
  const subdirs = [];
2964
2924
  for (const entry of entries) {
2965
- const fullPath = join9(current, entry);
2925
+ const fullPath = join10(current, entry);
2966
2926
  let stat;
2967
2927
  try {
2968
2928
  stat = statSync(fullPath);
@@ -2999,8 +2959,8 @@ function isTestFile(filename) {
2999
2959
  return filename.includes(".test.") || filename.includes(".spec.") || filename.includes("__tests__") || filename.startsWith("test_");
3000
2960
  }
3001
2961
  function isDocStale(docPath, codeDir) {
3002
- if (!existsSync11(docPath)) return true;
3003
- if (!existsSync11(codeDir)) return false;
2962
+ if (!existsSync12(docPath)) return true;
2963
+ if (!existsSync12(codeDir)) return false;
3004
2964
  const docMtime = statSync(docPath).mtime;
3005
2965
  const newestCode = getNewestSourceMtime(codeDir);
3006
2966
  if (newestCode === null) return false;
@@ -3018,7 +2978,7 @@ function getNewestSourceMtime(dir) {
3018
2978
  const dirName = current.split("/").pop() ?? "";
3019
2979
  if (dirName === "node_modules" || dirName === ".git") return;
3020
2980
  for (const entry of entries) {
3021
- const fullPath = join9(current, entry);
2981
+ const fullPath = join10(current, entry);
3022
2982
  let stat;
3023
2983
  try {
3024
2984
  stat = statSync(fullPath);
@@ -3054,7 +3014,7 @@ function getSourceFilesInModule(modulePath) {
3054
3014
  const dirName = current.split("/").pop() ?? "";
3055
3015
  if (dirName === "node_modules" || dirName === ".git" || dirName === "__tests__" || dirName === "dist" || dirName === "coverage" || dirName.startsWith(".") && current !== modulePath) return;
3056
3016
  for (const entry of entries) {
3057
- const fullPath = join9(current, entry);
3017
+ const fullPath = join10(current, entry);
3058
3018
  let stat;
3059
3019
  try {
3060
3020
  stat = statSync(fullPath);
@@ -3075,8 +3035,8 @@ function getSourceFilesInModule(modulePath) {
3075
3035
  return files;
3076
3036
  }
3077
3037
  function getMentionedFilesInAgentsMd(agentsPath) {
3078
- if (!existsSync11(agentsPath)) return [];
3079
- const content = readFileSync10(agentsPath, "utf-8");
3038
+ if (!existsSync12(agentsPath)) return [];
3039
+ const content = readFileSync11(agentsPath, "utf-8");
3080
3040
  const mentioned = /* @__PURE__ */ new Set();
3081
3041
  const filenamePattern = /[\w./-]*[\w-]+\.(?:ts|js|py)\b/g;
3082
3042
  let match;
@@ -3100,12 +3060,12 @@ function checkAgentsMdCompleteness(agentsPath, modulePath) {
3100
3060
  }
3101
3061
  function checkAgentsMdForModule(modulePath, dir) {
3102
3062
  const root = dir ?? process.cwd();
3103
- const fullModulePath = join9(root, modulePath);
3104
- let agentsPath = join9(fullModulePath, "AGENTS.md");
3105
- if (!existsSync11(agentsPath)) {
3106
- agentsPath = join9(root, "AGENTS.md");
3063
+ const fullModulePath = join10(root, modulePath);
3064
+ let agentsPath = join10(fullModulePath, "AGENTS.md");
3065
+ if (!existsSync12(agentsPath)) {
3066
+ agentsPath = join10(root, "AGENTS.md");
3107
3067
  }
3108
- if (!existsSync11(agentsPath)) {
3068
+ if (!existsSync12(agentsPath)) {
3109
3069
  return {
3110
3070
  path: relative(root, agentsPath),
3111
3071
  grade: "missing",
@@ -3136,9 +3096,9 @@ function checkAgentsMdForModule(modulePath, dir) {
3136
3096
  };
3137
3097
  }
3138
3098
  function checkDoNotEditHeaders(docPath) {
3139
- if (!existsSync11(docPath)) return false;
3099
+ if (!existsSync12(docPath)) return false;
3140
3100
  try {
3141
- const content = readFileSync10(docPath, "utf-8");
3101
+ const content = readFileSync11(docPath, "utf-8");
3142
3102
  if (content.length === 0) return false;
3143
3103
  return content.trimStart().startsWith(DO_NOT_EDIT_HEADER2);
3144
3104
  } catch {
@@ -3150,17 +3110,17 @@ function scanDocHealth(dir) {
3150
3110
  const root = dir ?? process.cwd();
3151
3111
  const documents = [];
3152
3112
  const modules = findModules(root);
3153
- const rootAgentsPath = join9(root, "AGENTS.md");
3154
- if (existsSync11(rootAgentsPath)) {
3113
+ const rootAgentsPath = join10(root, "AGENTS.md");
3114
+ if (existsSync12(rootAgentsPath)) {
3155
3115
  if (modules.length > 0) {
3156
3116
  const docMtime = statSync(rootAgentsPath).mtime;
3157
3117
  let allMissing = [];
3158
3118
  let staleModule = "";
3159
3119
  let newestCode = null;
3160
3120
  for (const mod of modules) {
3161
- const fullModPath = join9(root, mod);
3162
- const modAgentsPath = join9(fullModPath, "AGENTS.md");
3163
- if (existsSync11(modAgentsPath)) continue;
3121
+ const fullModPath = join10(root, mod);
3122
+ const modAgentsPath = join10(fullModPath, "AGENTS.md");
3123
+ if (existsSync12(modAgentsPath)) continue;
3164
3124
  const { missing } = checkAgentsMdCompleteness(rootAgentsPath, fullModPath);
3165
3125
  if (missing.length > 0 && staleModule === "") {
3166
3126
  staleModule = mod;
@@ -3208,8 +3168,8 @@ function scanDocHealth(dir) {
3208
3168
  });
3209
3169
  }
3210
3170
  for (const mod of modules) {
3211
- const modAgentsPath = join9(root, mod, "AGENTS.md");
3212
- if (existsSync11(modAgentsPath)) {
3171
+ const modAgentsPath = join10(root, mod, "AGENTS.md");
3172
+ if (existsSync12(modAgentsPath)) {
3213
3173
  const result = checkAgentsMdForModule(mod, root);
3214
3174
  if (result.path !== "AGENTS.md") {
3215
3175
  documents.push(result);
@@ -3217,9 +3177,9 @@ function scanDocHealth(dir) {
3217
3177
  }
3218
3178
  }
3219
3179
  }
3220
- const indexPath = join9(root, "docs", "index.md");
3221
- if (existsSync11(indexPath)) {
3222
- const content = readFileSync10(indexPath, "utf-8");
3180
+ const indexPath = join10(root, "docs", "index.md");
3181
+ if (existsSync12(indexPath)) {
3182
+ const content = readFileSync11(indexPath, "utf-8");
3223
3183
  const hasAbsolutePaths = /https?:\/\/|file:\/\//i.test(content);
3224
3184
  documents.push({
3225
3185
  path: "docs/index.md",
@@ -3229,11 +3189,11 @@ function scanDocHealth(dir) {
3229
3189
  reason: hasAbsolutePaths ? "Contains absolute URLs (may violate NFR25)" : "Uses relative paths"
3230
3190
  });
3231
3191
  }
3232
- const activeDir = join9(root, "docs", "exec-plans", "active");
3233
- if (existsSync11(activeDir)) {
3192
+ const activeDir = join10(root, "docs", "exec-plans", "active");
3193
+ if (existsSync12(activeDir)) {
3234
3194
  const files = readdirSync2(activeDir).filter((f) => f.endsWith(".md"));
3235
3195
  for (const file of files) {
3236
- const filePath = join9(activeDir, file);
3196
+ const filePath = join10(activeDir, file);
3237
3197
  documents.push({
3238
3198
  path: `docs/exec-plans/active/${file}`,
3239
3199
  grade: "fresh",
@@ -3244,11 +3204,11 @@ function scanDocHealth(dir) {
3244
3204
  }
3245
3205
  }
3246
3206
  for (const subdir of ["quality", "generated"]) {
3247
- const dirPath = join9(root, "docs", subdir);
3248
- if (!existsSync11(dirPath)) continue;
3207
+ const dirPath = join10(root, "docs", subdir);
3208
+ if (!existsSync12(dirPath)) continue;
3249
3209
  const files = readdirSync2(dirPath).filter((f) => !f.startsWith("."));
3250
3210
  for (const file of files) {
3251
- const filePath = join9(dirPath, file);
3211
+ const filePath = join10(dirPath, file);
3252
3212
  let stat;
3253
3213
  try {
3254
3214
  stat = statSync(filePath);
@@ -3281,7 +3241,7 @@ function scanDocHealth(dir) {
3281
3241
  }
3282
3242
  function checkAgentsMdLineCount(filePath, docPath, documents) {
3283
3243
  try {
3284
- const content = readFileSync10(filePath, "utf-8");
3244
+ const content = readFileSync11(filePath, "utf-8");
3285
3245
  const lineCount = content.split("\n").length;
3286
3246
  if (lineCount > 100) {
3287
3247
  documents.push({
@@ -3319,15 +3279,15 @@ function checkStoryDocFreshness(storyId, dir) {
3319
3279
  for (const mod of modulesToCheck) {
3320
3280
  const result = checkAgentsMdForModule(mod, root);
3321
3281
  documents.push(result);
3322
- const moduleAgentsPath = join9(root, mod, "AGENTS.md");
3323
- const actualAgentsPath = existsSync11(moduleAgentsPath) ? moduleAgentsPath : join9(root, "AGENTS.md");
3324
- if (existsSync11(actualAgentsPath)) {
3282
+ const moduleAgentsPath = join10(root, mod, "AGENTS.md");
3283
+ const actualAgentsPath = existsSync12(moduleAgentsPath) ? moduleAgentsPath : join10(root, "AGENTS.md");
3284
+ if (existsSync12(actualAgentsPath)) {
3325
3285
  checkAgentsMdLineCount(actualAgentsPath, result.path, documents);
3326
3286
  }
3327
3287
  }
3328
3288
  if (modulesToCheck.length === 0) {
3329
- const rootAgentsPath = join9(root, "AGENTS.md");
3330
- if (existsSync11(rootAgentsPath)) {
3289
+ const rootAgentsPath = join10(root, "AGENTS.md");
3290
+ if (existsSync12(rootAgentsPath)) {
3331
3291
  documents.push({
3332
3292
  path: "AGENTS.md",
3333
3293
  grade: "fresh",
@@ -3365,11 +3325,11 @@ function getRecentlyChangedFiles(dir) {
3365
3325
  }
3366
3326
  function completeExecPlan(storyId, dir) {
3367
3327
  const root = dir ?? process.cwd();
3368
- const activePath = join9(root, "docs", "exec-plans", "active", `${storyId}.md`);
3369
- if (!existsSync11(activePath)) {
3328
+ const activePath = join10(root, "docs", "exec-plans", "active", `${storyId}.md`);
3329
+ if (!existsSync12(activePath)) {
3370
3330
  return null;
3371
3331
  }
3372
- let content = readFileSync10(activePath, "utf-8");
3332
+ let content = readFileSync11(activePath, "utf-8");
3373
3333
  content = content.replace(/^Status:\s*active$/m, "Status: completed");
3374
3334
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3375
3335
  content = content.replace(
@@ -3377,9 +3337,9 @@ function completeExecPlan(storyId, dir) {
3377
3337
  `$1
3378
3338
  Completed: ${timestamp}`
3379
3339
  );
3380
- const completedDir = join9(root, "docs", "exec-plans", "completed");
3340
+ const completedDir = join10(root, "docs", "exec-plans", "completed");
3381
3341
  mkdirSync4(completedDir, { recursive: true });
3382
- const completedPath = join9(completedDir, `${storyId}.md`);
3342
+ const completedPath = join10(completedDir, `${storyId}.md`);
3383
3343
  writeFileSync7(completedPath, content, "utf-8");
3384
3344
  try {
3385
3345
  unlinkSync(activePath);
@@ -3571,8 +3531,8 @@ function checkPreconditions(dir, storyId) {
3571
3531
  }
3572
3532
  function createProofDocument(storyId, storyTitle, acs, dir) {
3573
3533
  const root = dir ?? process.cwd();
3574
- const verificationDir = join10(root, "verification");
3575
- const screenshotsDir = join10(verificationDir, "screenshots");
3534
+ const verificationDir = join11(root, "verification");
3535
+ const screenshotsDir = join11(verificationDir, "screenshots");
3576
3536
  mkdirSync5(verificationDir, { recursive: true });
3577
3537
  mkdirSync5(screenshotsDir, { recursive: true });
3578
3538
  const criteria = acs.map((ac) => ({
@@ -3586,7 +3546,7 @@ function createProofDocument(storyId, storyTitle, acs, dir) {
3586
3546
  storyTitle,
3587
3547
  acceptanceCriteria: criteria
3588
3548
  });
3589
- const proofPath = join10(verificationDir, `${storyId}-proof.md`);
3549
+ const proofPath = join11(verificationDir, `${storyId}-proof.md`);
3590
3550
  writeFileSync8(proofPath, content, "utf-8");
3591
3551
  return proofPath;
3592
3552
  }
@@ -3620,10 +3580,10 @@ function validateProofQuality(proofPath) {
3620
3580
  otherCount: 0,
3621
3581
  blackBoxPass: false
3622
3582
  };
3623
- if (!existsSync12(proofPath)) {
3583
+ if (!existsSync13(proofPath)) {
3624
3584
  return emptyResult;
3625
3585
  }
3626
- const content = readFileSync11(proofPath, "utf-8");
3586
+ const content = readFileSync12(proofPath, "utf-8");
3627
3587
  const bbEnforcement = checkBlackBoxEnforcement(content);
3628
3588
  function buildResult(base) {
3629
3589
  const basePassed = base.pending === 0 && base.verified > 0;
@@ -3827,8 +3787,8 @@ function verifyRetro(opts, isJson, root) {
3827
3787
  return;
3828
3788
  }
3829
3789
  const retroFile = `epic-${epicNum}-retrospective.md`;
3830
- const retroPath = join11(root, STORY_DIR, retroFile);
3831
- if (!existsSync13(retroPath)) {
3790
+ const retroPath = join12(root, STORY_DIR, retroFile);
3791
+ if (!existsSync14(retroPath)) {
3832
3792
  if (isJson) {
3833
3793
  jsonOutput({ status: "fail", epic: epicNum, retroFile, message: `${retroFile} not found` });
3834
3794
  } else {
@@ -3845,7 +3805,7 @@ function verifyRetro(opts, isJson, root) {
3845
3805
  warn(`Failed to update sprint status: ${message}`);
3846
3806
  }
3847
3807
  if (isJson) {
3848
- jsonOutput({ status: "ok", epic: epicNum, retroFile: join11(STORY_DIR, retroFile) });
3808
+ jsonOutput({ status: "ok", epic: epicNum, retroFile: join12(STORY_DIR, retroFile) });
3849
3809
  } else {
3850
3810
  ok(`Epic ${epicNum} retrospective: marked done`);
3851
3811
  }
@@ -3856,8 +3816,8 @@ function verifyStory(storyId, isJson, root) {
3856
3816
  process.exitCode = 1;
3857
3817
  return;
3858
3818
  }
3859
- const readmePath = join11(root, "README.md");
3860
- if (!existsSync13(readmePath)) {
3819
+ const readmePath = join12(root, "README.md");
3820
+ if (!existsSync14(readmePath)) {
3861
3821
  if (isJson) {
3862
3822
  jsonOutput({ status: "fail", message: "No README.md found \u2014 verification requires user documentation" });
3863
3823
  } else {
@@ -3866,8 +3826,8 @@ function verifyStory(storyId, isJson, root) {
3866
3826
  process.exitCode = 1;
3867
3827
  return;
3868
3828
  }
3869
- const storyFilePath = join11(root, STORY_DIR, `${storyId}.md`);
3870
- if (!existsSync13(storyFilePath)) {
3829
+ const storyFilePath = join12(root, STORY_DIR, `${storyId}.md`);
3830
+ if (!existsSync14(storyFilePath)) {
3871
3831
  fail(`Story file not found: ${storyFilePath}`, { json: isJson });
3872
3832
  process.exitCode = 1;
3873
3833
  return;
@@ -3907,8 +3867,8 @@ function verifyStory(storyId, isJson, root) {
3907
3867
  return;
3908
3868
  }
3909
3869
  const storyTitle = extractStoryTitle(storyFilePath);
3910
- const expectedProofPath = join11(root, "verification", `${storyId}-proof.md`);
3911
- const proofPath = existsSync13(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
3870
+ const expectedProofPath = join12(root, "verification", `${storyId}-proof.md`);
3871
+ const proofPath = existsSync14(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
3912
3872
  const proofQuality = validateProofQuality(proofPath);
3913
3873
  if (!proofQuality.passed) {
3914
3874
  if (isJson) {
@@ -4002,7 +3962,7 @@ function verifyStory(storyId, isJson, root) {
4002
3962
  }
4003
3963
  function extractStoryTitle(filePath) {
4004
3964
  try {
4005
- const content = readFileSync12(filePath, "utf-8");
3965
+ const content = readFileSync13(filePath, "utf-8");
4006
3966
  const match = /^#\s+(.+)$/m.exec(content);
4007
3967
  return match ? match[1] : "Unknown Story";
4008
3968
  } catch {
@@ -4011,14 +3971,14 @@ function extractStoryTitle(filePath) {
4011
3971
  }
4012
3972
 
4013
3973
  // src/lib/onboard-checks.ts
4014
- import { existsSync as existsSync15 } from "fs";
4015
- import { join as join13, dirname as dirname3 } from "path";
4016
- import { fileURLToPath as fileURLToPath2 } from "url";
3974
+ import { existsSync as existsSync16 } from "fs";
3975
+ import { join as join14, dirname as dirname4 } from "path";
3976
+ import { fileURLToPath as fileURLToPath3 } from "url";
4017
3977
 
4018
3978
  // src/lib/coverage.ts
4019
3979
  import { execSync as execSync2 } from "child_process";
4020
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
4021
- import { join as join12 } from "path";
3980
+ import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
3981
+ import { join as join13 } from "path";
4022
3982
  function detectCoverageTool(dir) {
4023
3983
  const baseDir = dir ?? process.cwd();
4024
3984
  const stateHint = getStateToolHint(baseDir);
@@ -4041,16 +4001,16 @@ function getStateToolHint(dir) {
4041
4001
  }
4042
4002
  }
4043
4003
  function detectNodeCoverageTool(dir, stateHint) {
4044
- const hasVitestConfig = existsSync14(join12(dir, "vitest.config.ts")) || existsSync14(join12(dir, "vitest.config.js"));
4045
- const pkgPath = join12(dir, "package.json");
4004
+ const hasVitestConfig = existsSync15(join13(dir, "vitest.config.ts")) || existsSync15(join13(dir, "vitest.config.js"));
4005
+ const pkgPath = join13(dir, "package.json");
4046
4006
  let hasVitestCoverageV8 = false;
4047
4007
  let hasVitestCoverageIstanbul = false;
4048
4008
  let hasC8 = false;
4049
4009
  let hasJest = false;
4050
4010
  let pkgScripts = {};
4051
- if (existsSync14(pkgPath)) {
4011
+ if (existsSync15(pkgPath)) {
4052
4012
  try {
4053
- const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
4013
+ const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
4054
4014
  const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
4055
4015
  hasVitestCoverageV8 = "@vitest/coverage-v8" in allDeps;
4056
4016
  hasVitestCoverageIstanbul = "@vitest/coverage-istanbul" in allDeps;
@@ -4103,10 +4063,10 @@ function getNodeTestCommand(scripts, runner) {
4103
4063
  return "npm test";
4104
4064
  }
4105
4065
  function detectPythonCoverageTool(dir) {
4106
- const reqPath = join12(dir, "requirements.txt");
4107
- if (existsSync14(reqPath)) {
4066
+ const reqPath = join13(dir, "requirements.txt");
4067
+ if (existsSync15(reqPath)) {
4108
4068
  try {
4109
- const content = readFileSync13(reqPath, "utf-8");
4069
+ const content = readFileSync14(reqPath, "utf-8");
4110
4070
  if (content.includes("pytest-cov") || content.includes("coverage")) {
4111
4071
  return {
4112
4072
  tool: "coverage.py",
@@ -4117,10 +4077,10 @@ function detectPythonCoverageTool(dir) {
4117
4077
  } catch {
4118
4078
  }
4119
4079
  }
4120
- const pyprojectPath = join12(dir, "pyproject.toml");
4121
- if (existsSync14(pyprojectPath)) {
4080
+ const pyprojectPath = join13(dir, "pyproject.toml");
4081
+ if (existsSync15(pyprojectPath)) {
4122
4082
  try {
4123
- const content = readFileSync13(pyprojectPath, "utf-8");
4083
+ const content = readFileSync14(pyprojectPath, "utf-8");
4124
4084
  if (content.includes("pytest-cov") || content.includes("coverage")) {
4125
4085
  return {
4126
4086
  tool: "coverage.py",
@@ -4202,7 +4162,7 @@ function parseVitestCoverage(dir) {
4202
4162
  return 0;
4203
4163
  }
4204
4164
  try {
4205
- const report = JSON.parse(readFileSync13(reportPath, "utf-8"));
4165
+ const report = JSON.parse(readFileSync14(reportPath, "utf-8"));
4206
4166
  return report.total?.statements?.pct ?? 0;
4207
4167
  } catch {
4208
4168
  warn("Failed to parse coverage report");
@@ -4210,13 +4170,13 @@ function parseVitestCoverage(dir) {
4210
4170
  }
4211
4171
  }
4212
4172
  function parsePythonCoverage(dir) {
4213
- const reportPath = join12(dir, "coverage.json");
4214
- if (!existsSync14(reportPath)) {
4173
+ const reportPath = join13(dir, "coverage.json");
4174
+ if (!existsSync15(reportPath)) {
4215
4175
  warn("Coverage report not found at coverage.json");
4216
4176
  return 0;
4217
4177
  }
4218
4178
  try {
4219
- const report = JSON.parse(readFileSync13(reportPath, "utf-8"));
4179
+ const report = JSON.parse(readFileSync14(reportPath, "utf-8"));
4220
4180
  return report.totals?.percent_covered ?? 0;
4221
4181
  } catch {
4222
4182
  warn("Failed to parse coverage report");
@@ -4317,7 +4277,7 @@ function checkPerFileCoverage(floor, dir) {
4317
4277
  }
4318
4278
  let report;
4319
4279
  try {
4320
- report = JSON.parse(readFileSync13(reportPath, "utf-8"));
4280
+ report = JSON.parse(readFileSync14(reportPath, "utf-8"));
4321
4281
  } catch {
4322
4282
  warn("Failed to parse coverage-summary.json");
4323
4283
  return { floor, violations: [], totalFiles: 0 };
@@ -4347,11 +4307,11 @@ function checkPerFileCoverage(floor, dir) {
4347
4307
  }
4348
4308
  function findCoverageSummary(dir) {
4349
4309
  const candidates = [
4350
- join12(dir, "coverage", "coverage-summary.json"),
4351
- join12(dir, "src", "coverage", "coverage-summary.json")
4310
+ join13(dir, "coverage", "coverage-summary.json"),
4311
+ join13(dir, "src", "coverage", "coverage-summary.json")
4352
4312
  ];
4353
4313
  for (const p of candidates) {
4354
- if (existsSync14(p)) return p;
4314
+ if (existsSync15(p)) return p;
4355
4315
  }
4356
4316
  return null;
4357
4317
  }
@@ -4376,16 +4336,16 @@ function printCoverageOutput(result, evaluation) {
4376
4336
  // src/lib/onboard-checks.ts
4377
4337
  function checkHarnessInitialized(dir) {
4378
4338
  const statePath = getStatePath(dir ?? process.cwd());
4379
- return { ok: existsSync15(statePath) };
4339
+ return { ok: existsSync16(statePath) };
4380
4340
  }
4381
4341
  function checkBmadInstalled(dir) {
4382
4342
  return { ok: isBmadInstalled(dir) };
4383
4343
  }
4384
4344
  function checkHooksRegistered(dir) {
4385
- const __filename = fileURLToPath2(import.meta.url);
4386
- const __dirname = dirname3(__filename);
4387
- const hooksPath = join13(__dirname, "..", "..", "hooks", "hooks.json");
4388
- return { ok: existsSync15(hooksPath) };
4345
+ const __filename = fileURLToPath3(import.meta.url);
4346
+ const __dirname2 = dirname4(__filename);
4347
+ const hooksPath = join14(__dirname2, "..", "..", "hooks", "hooks.json");
4348
+ return { ok: existsSync16(hooksPath) };
4389
4349
  }
4390
4350
  function runPreconditions(dir) {
4391
4351
  const harnessCheck = checkHarnessInitialized(dir);
@@ -4425,8 +4385,8 @@ function findVerificationGaps(dir) {
4425
4385
  for (const [key, status] of Object.entries(statuses)) {
4426
4386
  if (status !== "done") continue;
4427
4387
  if (!STORY_KEY_PATTERN2.test(key)) continue;
4428
- const proofPath = join13(root, "verification", `${key}-proof.md`);
4429
- if (!existsSync15(proofPath)) {
4388
+ const proofPath = join14(root, "verification", `${key}-proof.md`);
4389
+ if (!existsSync16(proofPath)) {
4430
4390
  unverified.push(key);
4431
4391
  }
4432
4392
  }
@@ -4632,29 +4592,18 @@ function handleFullStatus(isJson) {
4632
4592
  const composeFile = state.docker?.compose_file ?? "docker-compose.harness.yml";
4633
4593
  const stackDir = getStackDir();
4634
4594
  const isShared = composeFile.startsWith(stackDir);
4635
- if (isShared) {
4636
- console.log(`Docker: shared stack at ~/.codeharness/stack/`);
4637
- const sharedComposeFile = getComposeFilePath();
4638
- const health = getStackHealth(sharedComposeFile, "codeharness-shared");
4639
- for (const svc of health.services) {
4640
- console.log(` ${svc.name}: ${svc.running ? "running" : "stopped"}`);
4641
- }
4642
- if (health.healthy) {
4643
- console.log(
4644
- ` Endpoints: logs=${DEFAULT_ENDPOINTS.logs} metrics=${DEFAULT_ENDPOINTS.metrics} traces=${DEFAULT_ENDPOINTS.traces}`
4645
- );
4646
- }
4647
- } else {
4648
- const health = getStackHealth(composeFile);
4649
- console.log("Docker:");
4650
- for (const svc of health.services) {
4651
- console.log(` ${svc.name}: ${svc.running ? "running" : "stopped"}`);
4652
- }
4653
- if (health.healthy) {
4654
- console.log(
4655
- ` Endpoints: logs=${DEFAULT_ENDPOINTS.logs} metrics=${DEFAULT_ENDPOINTS.metrics} traces=${DEFAULT_ENDPOINTS.traces}`
4656
- );
4657
- }
4595
+ const resolvedComposeFile = isShared ? getComposeFilePath() : composeFile;
4596
+ const projectName = isShared ? "codeharness-shared" : void 0;
4597
+ const header = isShared ? "Docker: shared stack at ~/.codeharness/stack/" : "Docker:";
4598
+ console.log(header);
4599
+ const health = getStackHealth(resolvedComposeFile, projectName);
4600
+ for (const svc of health.services) {
4601
+ console.log(` ${svc.name}: ${svc.running ? "running" : "stopped"}`);
4602
+ }
4603
+ if (health.healthy) {
4604
+ console.log(
4605
+ ` Endpoints: logs=${DEFAULT_ENDPOINTS.logs} metrics=${DEFAULT_ENDPOINTS.metrics} traces=${DEFAULT_ENDPOINTS.traces}`
4606
+ );
4658
4607
  }
4659
4608
  }
4660
4609
  }
@@ -5011,16 +4960,16 @@ function getBeadsData() {
5011
4960
  }
5012
4961
 
5013
4962
  // src/commands/onboard.ts
5014
- import { join as join17 } from "path";
4963
+ import { join as join18 } from "path";
5015
4964
 
5016
4965
  // src/lib/scanner.ts
5017
4966
  import {
5018
- existsSync as existsSync16,
4967
+ existsSync as existsSync17,
5019
4968
  readdirSync as readdirSync3,
5020
- readFileSync as readFileSync14,
4969
+ readFileSync as readFileSync15,
5021
4970
  statSync as statSync2
5022
4971
  } from "fs";
5023
- import { join as join14, relative as relative2 } from "path";
4972
+ import { join as join15, relative as relative2 } from "path";
5024
4973
  var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
5025
4974
  var DEFAULT_MIN_MODULE_SIZE = 3;
5026
4975
  function getExtension2(filename) {
@@ -5045,7 +4994,7 @@ function countSourceFiles(dir) {
5045
4994
  for (const entry of entries) {
5046
4995
  if (isSkippedDir(entry)) continue;
5047
4996
  if (entry.startsWith(".") && current !== dir) continue;
5048
- const fullPath = join14(current, entry);
4997
+ const fullPath = join15(current, entry);
5049
4998
  let stat;
5050
4999
  try {
5051
5000
  stat = statSync2(fullPath);
@@ -5068,7 +5017,7 @@ function countSourceFiles(dir) {
5068
5017
  return count;
5069
5018
  }
5070
5019
  function countModuleFiles(modulePath, rootDir) {
5071
- const fullModulePath = join14(rootDir, modulePath);
5020
+ const fullModulePath = join15(rootDir, modulePath);
5072
5021
  let sourceFiles = 0;
5073
5022
  let testFiles = 0;
5074
5023
  function walk(current) {
@@ -5080,7 +5029,7 @@ function countModuleFiles(modulePath, rootDir) {
5080
5029
  }
5081
5030
  for (const entry of entries) {
5082
5031
  if (isSkippedDir(entry)) continue;
5083
- const fullPath = join14(current, entry);
5032
+ const fullPath = join15(current, entry);
5084
5033
  let stat;
5085
5034
  try {
5086
5035
  stat = statSync2(fullPath);
@@ -5105,8 +5054,8 @@ function countModuleFiles(modulePath, rootDir) {
5105
5054
  return { sourceFiles, testFiles };
5106
5055
  }
5107
5056
  function detectArtifacts(dir) {
5108
- const bmadPath = join14(dir, "_bmad");
5109
- const hasBmad = existsSync16(bmadPath);
5057
+ const bmadPath = join15(dir, "_bmad");
5058
+ const hasBmad = existsSync17(bmadPath);
5110
5059
  return {
5111
5060
  hasBmad,
5112
5061
  bmadPath: hasBmad ? relative2(dir, bmadPath) || "_bmad" : null
@@ -5188,10 +5137,10 @@ function readPerFileCoverage(dir, format) {
5188
5137
  return null;
5189
5138
  }
5190
5139
  function readVitestPerFileCoverage(dir) {
5191
- const reportPath = join14(dir, "coverage", "coverage-summary.json");
5192
- if (!existsSync16(reportPath)) return null;
5140
+ const reportPath = join15(dir, "coverage", "coverage-summary.json");
5141
+ if (!existsSync17(reportPath)) return null;
5193
5142
  try {
5194
- const report = JSON.parse(readFileSync14(reportPath, "utf-8"));
5143
+ const report = JSON.parse(readFileSync15(reportPath, "utf-8"));
5195
5144
  const result = /* @__PURE__ */ new Map();
5196
5145
  for (const [key, value] of Object.entries(report)) {
5197
5146
  if (key === "total") continue;
@@ -5203,10 +5152,10 @@ function readVitestPerFileCoverage(dir) {
5203
5152
  }
5204
5153
  }
5205
5154
  function readPythonPerFileCoverage(dir) {
5206
- const reportPath = join14(dir, "coverage.json");
5207
- if (!existsSync16(reportPath)) return null;
5155
+ const reportPath = join15(dir, "coverage.json");
5156
+ if (!existsSync17(reportPath)) return null;
5208
5157
  try {
5209
- const report = JSON.parse(readFileSync14(reportPath, "utf-8"));
5158
+ const report = JSON.parse(readFileSync15(reportPath, "utf-8"));
5210
5159
  if (!report.files) return null;
5211
5160
  const result = /* @__PURE__ */ new Map();
5212
5161
  for (const [key, value] of Object.entries(report.files)) {
@@ -5222,13 +5171,13 @@ function auditDocumentation(dir) {
5222
5171
  const root = dir ?? process.cwd();
5223
5172
  const documents = [];
5224
5173
  for (const docName of AUDIT_DOCUMENTS) {
5225
- const docPath = join14(root, docName);
5226
- if (!existsSync16(docPath)) {
5174
+ const docPath = join15(root, docName);
5175
+ if (!existsSync17(docPath)) {
5227
5176
  documents.push({ name: docName, grade: "missing", path: null });
5228
5177
  continue;
5229
5178
  }
5230
- const srcDir = join14(root, "src");
5231
- const codeDir = existsSync16(srcDir) ? srcDir : root;
5179
+ const srcDir = join15(root, "src");
5180
+ const codeDir = existsSync17(srcDir) ? srcDir : root;
5232
5181
  const stale = isDocStale(docPath, codeDir);
5233
5182
  documents.push({
5234
5183
  name: docName,
@@ -5236,8 +5185,8 @@ function auditDocumentation(dir) {
5236
5185
  path: docName
5237
5186
  });
5238
5187
  }
5239
- const docsDir = join14(root, "docs");
5240
- if (existsSync16(docsDir)) {
5188
+ const docsDir = join15(root, "docs");
5189
+ if (existsSync17(docsDir)) {
5241
5190
  try {
5242
5191
  const stat = statSync2(docsDir);
5243
5192
  if (stat.isDirectory()) {
@@ -5249,10 +5198,10 @@ function auditDocumentation(dir) {
5249
5198
  } else {
5250
5199
  documents.push({ name: "docs/", grade: "missing", path: null });
5251
5200
  }
5252
- const indexPath = join14(root, "docs", "index.md");
5253
- if (existsSync16(indexPath)) {
5254
- const srcDir = join14(root, "src");
5255
- const indexCodeDir = existsSync16(srcDir) ? srcDir : root;
5201
+ const indexPath = join15(root, "docs", "index.md");
5202
+ if (existsSync17(indexPath)) {
5203
+ const srcDir = join15(root, "src");
5204
+ const indexCodeDir = existsSync17(srcDir) ? srcDir : root;
5256
5205
  const indexStale = isDocStale(indexPath, indexCodeDir);
5257
5206
  documents.push({
5258
5207
  name: "docs/index.md",
@@ -5269,8 +5218,8 @@ function auditDocumentation(dir) {
5269
5218
 
5270
5219
  // src/lib/epic-generator.ts
5271
5220
  import { createInterface } from "readline";
5272
- import { existsSync as existsSync17, mkdirSync as mkdirSync6, writeFileSync as writeFileSync9 } from "fs";
5273
- import { dirname as dirname4, join as join15 } from "path";
5221
+ import { existsSync as existsSync18, mkdirSync as mkdirSync6, writeFileSync as writeFileSync9 } from "fs";
5222
+ import { dirname as dirname5, join as join16 } from "path";
5274
5223
  var PRIORITY_BY_TYPE = {
5275
5224
  observability: 1,
5276
5225
  coverage: 2,
@@ -5308,8 +5257,8 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
5308
5257
  storyNum++;
5309
5258
  }
5310
5259
  for (const mod of scan.modules) {
5311
- const agentsPath = join15(root, mod.path, "AGENTS.md");
5312
- if (!existsSync17(agentsPath)) {
5260
+ const agentsPath = join16(root, mod.path, "AGENTS.md");
5261
+ if (!existsSync18(agentsPath)) {
5313
5262
  stories.push({
5314
5263
  key: `0.${storyNum}`,
5315
5264
  title: `Create ${mod.path}/AGENTS.md`,
@@ -5375,7 +5324,7 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
5375
5324
  };
5376
5325
  }
5377
5326
  function writeOnboardingEpic(epic, outputPath) {
5378
- mkdirSync6(dirname4(outputPath), { recursive: true });
5327
+ mkdirSync6(dirname5(outputPath), { recursive: true });
5379
5328
  const lines = [];
5380
5329
  lines.push(`# ${epic.title}`);
5381
5330
  lines.push("");
@@ -5507,29 +5456,29 @@ function getGapIdFromTitle(title) {
5507
5456
  }
5508
5457
 
5509
5458
  // src/lib/scan-cache.ts
5510
- import { existsSync as existsSync18, mkdirSync as mkdirSync7, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
5511
- import { join as join16 } from "path";
5459
+ import { existsSync as existsSync19, mkdirSync as mkdirSync7, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
5460
+ import { join as join17 } from "path";
5512
5461
  var CACHE_DIR = ".harness";
5513
5462
  var CACHE_FILE = "last-onboard-scan.json";
5514
5463
  var DEFAULT_MAX_AGE_MS = 864e5;
5515
5464
  function saveScanCache(entry, dir) {
5516
5465
  try {
5517
5466
  const root = dir ?? process.cwd();
5518
- const cacheDir = join16(root, CACHE_DIR);
5467
+ const cacheDir = join17(root, CACHE_DIR);
5519
5468
  mkdirSync7(cacheDir, { recursive: true });
5520
- const cachePath = join16(cacheDir, CACHE_FILE);
5469
+ const cachePath = join17(cacheDir, CACHE_FILE);
5521
5470
  writeFileSync10(cachePath, JSON.stringify(entry, null, 2), "utf-8");
5522
5471
  } catch {
5523
5472
  }
5524
5473
  }
5525
5474
  function loadScanCache(dir) {
5526
5475
  const root = dir ?? process.cwd();
5527
- const cachePath = join16(root, CACHE_DIR, CACHE_FILE);
5528
- if (!existsSync18(cachePath)) {
5476
+ const cachePath = join17(root, CACHE_DIR, CACHE_FILE);
5477
+ if (!existsSync19(cachePath)) {
5529
5478
  return null;
5530
5479
  }
5531
5480
  try {
5532
- const raw = readFileSync15(cachePath, "utf-8");
5481
+ const raw = readFileSync16(cachePath, "utf-8");
5533
5482
  return JSON.parse(raw);
5534
5483
  } catch {
5535
5484
  return null;
@@ -5702,7 +5651,7 @@ function registerOnboardCommand(program) {
5702
5651
  }
5703
5652
  coverage = lastCoverageResult ?? runCoverageAnalysis(scan);
5704
5653
  audit = lastAuditResult ?? runAudit();
5705
- const epicPath = join17(process.cwd(), "ralph", "onboarding-epic.md");
5654
+ const epicPath = join18(process.cwd(), "ralph", "onboarding-epic.md");
5706
5655
  const epic = generateOnboardingEpic(scan, coverage, audit);
5707
5656
  mergeExtendedGaps(epic);
5708
5657
  if (!isFull) {
@@ -5775,7 +5724,7 @@ function registerOnboardCommand(program) {
5775
5724
  coverage,
5776
5725
  audit
5777
5726
  });
5778
- const epicPath = join17(process.cwd(), "ralph", "onboarding-epic.md");
5727
+ const epicPath = join18(process.cwd(), "ralph", "onboarding-epic.md");
5779
5728
  const epic = generateOnboardingEpic(scan, coverage, audit);
5780
5729
  mergeExtendedGaps(epic);
5781
5730
  if (!isFull) {
@@ -5883,8 +5832,8 @@ function printEpicOutput(epic) {
5883
5832
  }
5884
5833
 
5885
5834
  // src/commands/teardown.ts
5886
- import { existsSync as existsSync19, unlinkSync as unlinkSync2, readFileSync as readFileSync16, writeFileSync as writeFileSync11, rmSync } from "fs";
5887
- import { join as join18 } from "path";
5835
+ import { existsSync as existsSync20, unlinkSync as unlinkSync2, readFileSync as readFileSync17, writeFileSync as writeFileSync11, rmSync } from "fs";
5836
+ import { join as join19 } from "path";
5888
5837
  function buildDefaultResult() {
5889
5838
  return {
5890
5839
  status: "ok",
@@ -5987,16 +5936,16 @@ function registerTeardownCommand(program) {
5987
5936
  info("Docker stack: not running, skipping");
5988
5937
  }
5989
5938
  }
5990
- const composeFilePath = join18(projectDir, composeFile);
5991
- if (existsSync19(composeFilePath)) {
5939
+ const composeFilePath = join19(projectDir, composeFile);
5940
+ if (existsSync20(composeFilePath)) {
5992
5941
  unlinkSync2(composeFilePath);
5993
5942
  result.removed.push(composeFile);
5994
5943
  if (!isJson) {
5995
5944
  ok(`Removed: ${composeFile}`);
5996
5945
  }
5997
5946
  }
5998
- const otelConfigPath = join18(projectDir, "otel-collector-config.yaml");
5999
- if (existsSync19(otelConfigPath)) {
5947
+ const otelConfigPath = join19(projectDir, "otel-collector-config.yaml");
5948
+ if (existsSync20(otelConfigPath)) {
6000
5949
  unlinkSync2(otelConfigPath);
6001
5950
  result.removed.push("otel-collector-config.yaml");
6002
5951
  if (!isJson) {
@@ -6006,8 +5955,8 @@ function registerTeardownCommand(program) {
6006
5955
  }
6007
5956
  let patchesRemoved = 0;
6008
5957
  for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
6009
- const filePath = join18(projectDir, "_bmad", relativePath);
6010
- if (!existsSync19(filePath)) {
5958
+ const filePath = join19(projectDir, "_bmad", relativePath);
5959
+ if (!existsSync20(filePath)) {
6011
5960
  continue;
6012
5961
  }
6013
5962
  try {
@@ -6027,10 +5976,10 @@ function registerTeardownCommand(program) {
6027
5976
  }
6028
5977
  }
6029
5978
  if (state.otlp?.enabled && state.stack === "nodejs") {
6030
- const pkgPath = join18(projectDir, "package.json");
6031
- if (existsSync19(pkgPath)) {
5979
+ const pkgPath = join19(projectDir, "package.json");
5980
+ if (existsSync20(pkgPath)) {
6032
5981
  try {
6033
- const raw = readFileSync16(pkgPath, "utf-8");
5982
+ const raw = readFileSync17(pkgPath, "utf-8");
6034
5983
  const pkg = JSON.parse(raw);
6035
5984
  const scripts = pkg["scripts"];
6036
5985
  if (scripts) {
@@ -6070,8 +6019,8 @@ function registerTeardownCommand(program) {
6070
6019
  }
6071
6020
  }
6072
6021
  }
6073
- const harnessDir = join18(projectDir, ".harness");
6074
- if (existsSync19(harnessDir)) {
6022
+ const harnessDir = join19(projectDir, ".harness");
6023
+ if (existsSync20(harnessDir)) {
6075
6024
  rmSync(harnessDir, { recursive: true, force: true });
6076
6025
  result.removed.push(".harness/");
6077
6026
  if (!isJson) {
@@ -6079,7 +6028,7 @@ function registerTeardownCommand(program) {
6079
6028
  }
6080
6029
  }
6081
6030
  const statePath = getStatePath(projectDir);
6082
- if (existsSync19(statePath)) {
6031
+ if (existsSync20(statePath)) {
6083
6032
  unlinkSync2(statePath);
6084
6033
  result.removed.push(".claude/codeharness.local.md");
6085
6034
  if (!isJson) {
@@ -6823,8 +6772,8 @@ function registerQueryCommand(program) {
6823
6772
  }
6824
6773
 
6825
6774
  // src/commands/retro-import.ts
6826
- import { existsSync as existsSync20, readFileSync as readFileSync17 } from "fs";
6827
- import { join as join19 } from "path";
6775
+ import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
6776
+ import { join as join20 } from "path";
6828
6777
 
6829
6778
  // src/lib/retro-parser.ts
6830
6779
  var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
@@ -6993,15 +6942,15 @@ function registerRetroImportCommand(program) {
6993
6942
  return;
6994
6943
  }
6995
6944
  const retroFile = `epic-${epicNum}-retrospective.md`;
6996
- const retroPath = join19(root, STORY_DIR2, retroFile);
6997
- if (!existsSync20(retroPath)) {
6945
+ const retroPath = join20(root, STORY_DIR2, retroFile);
6946
+ if (!existsSync21(retroPath)) {
6998
6947
  fail(`Retro file not found: ${retroFile}`, { json: isJson });
6999
6948
  process.exitCode = 1;
7000
6949
  return;
7001
6950
  }
7002
6951
  let content;
7003
6952
  try {
7004
- content = readFileSync17(retroPath, "utf-8");
6953
+ content = readFileSync18(retroPath, "utf-8");
7005
6954
  } catch (err) {
7006
6955
  const message = err instanceof Error ? err.message : String(err);
7007
6956
  fail(`Failed to read retro file: ${message}`, { json: isJson });
@@ -7269,8 +7218,8 @@ function registerGithubImportCommand(program) {
7269
7218
 
7270
7219
  // src/lib/verify-env.ts
7271
7220
  import { execFileSync as execFileSync7 } from "child_process";
7272
- import { existsSync as existsSync21, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync18, cpSync, rmSync as rmSync2, statSync as statSync3 } from "fs";
7273
- import { join as join20, basename as basename3 } from "path";
7221
+ import { existsSync as existsSync22, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync19, cpSync, rmSync as rmSync2, statSync as statSync3 } from "fs";
7222
+ import { join as join21, basename as basename3 } from "path";
7274
7223
  import { createHash } from "crypto";
7275
7224
  var IMAGE_TAG = "codeharness-verify";
7276
7225
  var STORY_DIR3 = "_bmad-output/implementation-artifacts";
@@ -7283,14 +7232,14 @@ function isValidStoryKey(storyKey) {
7283
7232
  return /^[a-zA-Z0-9_-]+$/.test(storyKey);
7284
7233
  }
7285
7234
  function computeDistHash(projectDir) {
7286
- const distDir = join20(projectDir, "dist");
7287
- if (!existsSync21(distDir)) {
7235
+ const distDir = join21(projectDir, "dist");
7236
+ if (!existsSync22(distDir)) {
7288
7237
  return null;
7289
7238
  }
7290
7239
  const hash = createHash("sha256");
7291
7240
  const files = collectFiles(distDir).sort();
7292
7241
  for (const file of files) {
7293
- const content = readFileSync18(file);
7242
+ const content = readFileSync19(file);
7294
7243
  hash.update(file.slice(distDir.length));
7295
7244
  hash.update(content);
7296
7245
  }
@@ -7300,7 +7249,7 @@ function collectFiles(dir) {
7300
7249
  const results = [];
7301
7250
  const entries = readdirSync4(dir, { withFileTypes: true });
7302
7251
  for (const entry of entries) {
7303
- const fullPath = join20(dir, entry.name);
7252
+ const fullPath = join21(dir, entry.name);
7304
7253
  if (entry.isDirectory()) {
7305
7254
  results.push(...collectFiles(fullPath));
7306
7255
  } else {
@@ -7372,13 +7321,13 @@ function buildNodeImage(projectDir) {
7372
7321
  throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
7373
7322
  }
7374
7323
  const tarballName = basename3(lastLine);
7375
- const tarballPath = join20("/tmp", tarballName);
7376
- const buildContext = join20("/tmp", `codeharness-verify-build-${Date.now()}`);
7324
+ const tarballPath = join21("/tmp", tarballName);
7325
+ const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
7377
7326
  mkdirSync8(buildContext, { recursive: true });
7378
7327
  try {
7379
- cpSync(tarballPath, join20(buildContext, tarballName));
7328
+ cpSync(tarballPath, join21(buildContext, tarballName));
7380
7329
  const dockerfileSrc = resolveDockerfileTemplate(projectDir);
7381
- cpSync(dockerfileSrc, join20(buildContext, "Dockerfile"));
7330
+ cpSync(dockerfileSrc, join21(buildContext, "Dockerfile"));
7382
7331
  execFileSync7("docker", [
7383
7332
  "build",
7384
7333
  "-t",
@@ -7397,7 +7346,7 @@ function buildNodeImage(projectDir) {
7397
7346
  }
7398
7347
  }
7399
7348
  function buildPythonImage(projectDir) {
7400
- const distDir = join20(projectDir, "dist");
7349
+ const distDir = join21(projectDir, "dist");
7401
7350
  const distFiles = readdirSync4(distDir).filter(
7402
7351
  (f) => f.endsWith(".tar.gz") || f.endsWith(".whl")
7403
7352
  );
@@ -7405,12 +7354,12 @@ function buildPythonImage(projectDir) {
7405
7354
  throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
7406
7355
  }
7407
7356
  const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
7408
- const buildContext = join20("/tmp", `codeharness-verify-build-${Date.now()}`);
7357
+ const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
7409
7358
  mkdirSync8(buildContext, { recursive: true });
7410
7359
  try {
7411
- cpSync(join20(distDir, distFile), join20(buildContext, distFile));
7360
+ cpSync(join21(distDir, distFile), join21(buildContext, distFile));
7412
7361
  const dockerfileSrc = resolveDockerfileTemplate(projectDir);
7413
- cpSync(dockerfileSrc, join20(buildContext, "Dockerfile"));
7362
+ cpSync(dockerfileSrc, join21(buildContext, "Dockerfile"));
7414
7363
  execFileSync7("docker", [
7415
7364
  "build",
7416
7365
  "-t",
@@ -7432,25 +7381,25 @@ function prepareVerifyWorkspace(storyKey, projectDir) {
7432
7381
  if (!isValidStoryKey(storyKey)) {
7433
7382
  throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
7434
7383
  }
7435
- const storyFile = join20(root, STORY_DIR3, `${storyKey}.md`);
7436
- if (!existsSync21(storyFile)) {
7384
+ const storyFile = join21(root, STORY_DIR3, `${storyKey}.md`);
7385
+ if (!existsSync22(storyFile)) {
7437
7386
  throw new Error(`Story file not found: ${storyFile}`);
7438
7387
  }
7439
7388
  const workspace = `${TEMP_PREFIX}${storyKey}`;
7440
- if (existsSync21(workspace)) {
7389
+ if (existsSync22(workspace)) {
7441
7390
  rmSync2(workspace, { recursive: true, force: true });
7442
7391
  }
7443
7392
  mkdirSync8(workspace, { recursive: true });
7444
- cpSync(storyFile, join20(workspace, "story.md"));
7445
- const readmePath = join20(root, "README.md");
7446
- if (existsSync21(readmePath)) {
7447
- cpSync(readmePath, join20(workspace, "README.md"));
7393
+ cpSync(storyFile, join21(workspace, "story.md"));
7394
+ const readmePath = join21(root, "README.md");
7395
+ if (existsSync22(readmePath)) {
7396
+ cpSync(readmePath, join21(workspace, "README.md"));
7448
7397
  }
7449
- const docsDir = join20(root, "docs");
7450
- if (existsSync21(docsDir) && statSync3(docsDir).isDirectory()) {
7451
- cpSync(docsDir, join20(workspace, "docs"), { recursive: true });
7398
+ const docsDir = join21(root, "docs");
7399
+ if (existsSync22(docsDir) && statSync3(docsDir).isDirectory()) {
7400
+ cpSync(docsDir, join21(workspace, "docs"), { recursive: true });
7452
7401
  }
7453
- mkdirSync8(join20(workspace, "verification"), { recursive: true });
7402
+ mkdirSync8(join21(workspace, "verification"), { recursive: true });
7454
7403
  return workspace;
7455
7404
  }
7456
7405
  function checkVerifyEnv() {
@@ -7499,7 +7448,7 @@ function cleanupVerifyEnv(storyKey) {
7499
7448
  }
7500
7449
  const workspace = `${TEMP_PREFIX}${storyKey}`;
7501
7450
  const containerName = `codeharness-verify-${storyKey}`;
7502
- if (existsSync21(workspace)) {
7451
+ if (existsSync22(workspace)) {
7503
7452
  rmSync2(workspace, { recursive: true, force: true });
7504
7453
  }
7505
7454
  try {
@@ -7518,11 +7467,11 @@ function cleanupVerifyEnv(storyKey) {
7518
7467
  }
7519
7468
  }
7520
7469
  function resolveDockerfileTemplate(projectDir) {
7521
- const local = join20(projectDir, "templates", "Dockerfile.verify");
7522
- if (existsSync21(local)) return local;
7470
+ const local = join21(projectDir, "templates", "Dockerfile.verify");
7471
+ if (existsSync22(local)) return local;
7523
7472
  const pkgDir = new URL("../../", import.meta.url).pathname;
7524
- const pkg = join20(pkgDir, "templates", "Dockerfile.verify");
7525
- if (existsSync21(pkg)) return pkg;
7473
+ const pkg = join21(pkgDir, "templates", "Dockerfile.verify");
7474
+ if (existsSync22(pkg)) return pkg;
7526
7475
  throw new Error("Dockerfile.verify not found. Ensure templates/Dockerfile.verify exists in the project or installed package.");
7527
7476
  }
7528
7477
  function dockerImageExists(tag) {
@@ -7668,26 +7617,26 @@ function registerVerifyEnvCommand(program) {
7668
7617
  }
7669
7618
 
7670
7619
  // src/commands/retry.ts
7671
- import { join as join22 } from "path";
7620
+ import { join as join23 } from "path";
7672
7621
 
7673
7622
  // src/lib/retry-state.ts
7674
- import { existsSync as existsSync22, readFileSync as readFileSync19, writeFileSync as writeFileSync12 } from "fs";
7675
- import { join as join21 } from "path";
7623
+ import { existsSync as existsSync23, readFileSync as readFileSync20, writeFileSync as writeFileSync12 } from "fs";
7624
+ import { join as join22 } from "path";
7676
7625
  var RETRIES_FILE = ".story_retries";
7677
7626
  var FLAGGED_FILE = ".flagged_stories";
7678
7627
  var LINE_PATTERN = /^([^=]+)=(\d+)$/;
7679
7628
  function retriesPath(dir) {
7680
- return join21(dir, RETRIES_FILE);
7629
+ return join22(dir, RETRIES_FILE);
7681
7630
  }
7682
7631
  function flaggedPath(dir) {
7683
- return join21(dir, FLAGGED_FILE);
7632
+ return join22(dir, FLAGGED_FILE);
7684
7633
  }
7685
7634
  function readRetries(dir) {
7686
7635
  const filePath = retriesPath(dir);
7687
- if (!existsSync22(filePath)) {
7636
+ if (!existsSync23(filePath)) {
7688
7637
  return /* @__PURE__ */ new Map();
7689
7638
  }
7690
- const raw = readFileSync19(filePath, "utf-8");
7639
+ const raw = readFileSync20(filePath, "utf-8");
7691
7640
  const result = /* @__PURE__ */ new Map();
7692
7641
  for (const line of raw.split("\n")) {
7693
7642
  const trimmed = line.trim();
@@ -7724,10 +7673,10 @@ function resetRetry(dir, storyKey) {
7724
7673
  }
7725
7674
  function readFlaggedStories(dir) {
7726
7675
  const filePath = flaggedPath(dir);
7727
- if (!existsSync22(filePath)) {
7676
+ if (!existsSync23(filePath)) {
7728
7677
  return [];
7729
7678
  }
7730
- const raw = readFileSync19(filePath, "utf-8");
7679
+ const raw = readFileSync20(filePath, "utf-8");
7731
7680
  return raw.split("\n").map((l) => l.trim()).filter((l) => l !== "");
7732
7681
  }
7733
7682
  function writeFlaggedStories(dir, stories) {
@@ -7752,7 +7701,7 @@ function registerRetryCommand(program) {
7752
7701
  program.command("retry").description("Manage retry state for stories").option("--reset", "Clear retry counters and flagged stories").option("--story <key>", "Target a specific story key (used with --reset or --status)").option("--status", "Show retry status for all stories").action((_options, cmd) => {
7753
7702
  const opts = cmd.optsWithGlobals();
7754
7703
  const isJson = opts.json === true;
7755
- const dir = join22(process.cwd(), RALPH_SUBDIR);
7704
+ const dir = join23(process.cwd(), RALPH_SUBDIR);
7756
7705
  if (opts.story && !isValidStoryKey3(opts.story)) {
7757
7706
  if (isJson) {
7758
7707
  jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
@@ -7826,7 +7775,7 @@ function handleStatus(dir, isJson, filterStory) {
7826
7775
  }
7827
7776
 
7828
7777
  // src/index.ts
7829
- var VERSION = true ? "0.17.7" : "0.0.0-dev";
7778
+ var VERSION = true ? "0.18.0" : "0.0.0-dev";
7830
7779
  function createProgram() {
7831
7780
  const program = new Command();
7832
7781
  program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");