codeharness 0.23.0 → 0.24.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
@@ -51,17 +51,65 @@ function jsonOutput(data) {
51
51
  }
52
52
 
53
53
  // src/modules/infra/init-project.ts
54
- import { existsSync as existsSync8 } from "fs";
55
- import { basename as basename3 } from "path";
54
+ import { existsSync as existsSync9 } from "fs";
55
+ import { basename as basename3, join as join9 } from "path";
56
56
 
57
57
  // src/lib/stack-detect.ts
58
- import { existsSync, readFileSync } from "fs";
58
+ import { existsSync, readFileSync, readdirSync } from "fs";
59
59
  import { join } from "path";
60
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
61
+ "node_modules",
62
+ ".git",
63
+ "target",
64
+ "__pycache__",
65
+ "dist",
66
+ "build",
67
+ "coverage",
68
+ ".venv",
69
+ "venv",
70
+ ".tox",
71
+ ".mypy_cache",
72
+ ".cache"
73
+ ]);
74
+ var STACK_MARKERS = [
75
+ { stack: "nodejs", files: ["package.json"] },
76
+ { stack: "python", files: ["requirements.txt", "pyproject.toml", "setup.py"] },
77
+ { stack: "rust", files: ["Cargo.toml"] }
78
+ ];
79
+ function detectStacks(dir = process.cwd()) {
80
+ const results = [];
81
+ for (const marker of STACK_MARKERS) {
82
+ for (const file of marker.files) {
83
+ if (existsSync(join(dir, file))) {
84
+ results.push({ stack: marker.stack, dir: "." });
85
+ break;
86
+ }
87
+ }
88
+ }
89
+ let entries;
90
+ try {
91
+ entries = readdirSync(dir, { withFileTypes: true });
92
+ } catch {
93
+ entries = [];
94
+ }
95
+ const subdirs = entries.filter((e) => e.isDirectory() && !SKIP_DIRS.has(e.name)).map((e) => e.name).sort();
96
+ for (const subdir of subdirs) {
97
+ const subdirPath = join(dir, subdir);
98
+ for (const marker of STACK_MARKERS) {
99
+ for (const file of marker.files) {
100
+ if (existsSync(join(subdirPath, file))) {
101
+ results.push({ stack: marker.stack, dir: subdir });
102
+ break;
103
+ }
104
+ }
105
+ }
106
+ }
107
+ return results;
108
+ }
60
109
  function detectStack(dir = process.cwd()) {
61
- if (existsSync(join(dir, "package.json"))) return "nodejs";
62
- if (existsSync(join(dir, "requirements.txt"))) return "python";
63
- if (existsSync(join(dir, "pyproject.toml"))) return "python";
64
- if (existsSync(join(dir, "setup.py"))) return "python";
110
+ const stacks = detectStacks(dir);
111
+ const rootStack = stacks.find((s) => s.dir === ".");
112
+ if (rootStack) return rootStack.stack;
65
113
  warn("No recognized stack detected");
66
114
  return null;
67
115
  }
@@ -69,6 +117,8 @@ var AGENT_DEPS_NODE = ["anthropic", "@anthropic-ai/sdk", "openai", "langchain",
69
117
  var AGENT_DEPS_PYTHON = ["anthropic", "openai", "langchain", "llama-index", "traceloop-sdk"];
70
118
  var WEB_FRAMEWORK_DEPS = ["react", "vue", "svelte", "angular", "@angular/core", "next", "nuxt", "vite", "webpack"];
71
119
  var PYTHON_WEB_FRAMEWORKS = ["flask", "django", "fastapi", "streamlit"];
120
+ var RUST_WEB_FRAMEWORKS = ["actix-web", "axum", "rocket", "tide", "warp"];
121
+ var RUST_AGENT_DEPS = ["async-openai", "anthropic", "llm-chain"];
72
122
  function readJsonSafe(path) {
73
123
  try {
74
124
  if (!existsSync(path)) return null;
@@ -102,6 +152,18 @@ function hasPythonDep(content, dep) {
102
152
  const pattern = new RegExp(`(?:^|[\\s"',])${escaped}(?:[\\[>=<~!;\\s"',]|$)`, "m");
103
153
  return pattern.test(content);
104
154
  }
155
+ function getCargoDepsSection(content) {
156
+ const match = content.match(/^\[dependencies\]\s*$/m);
157
+ if (!match || match.index === void 0) return "";
158
+ const start = match.index + match[0].length;
159
+ const nextSection = content.slice(start).search(/^\[/m);
160
+ return nextSection === -1 ? content.slice(start) : content.slice(start, start + nextSection);
161
+ }
162
+ function hasCargoDep(depsSection, dep) {
163
+ const escaped = dep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
164
+ const pattern = new RegExp(`(?:^|\\s)${escaped}(?:\\s*=|\\s*\\{)`, "m");
165
+ return pattern.test(depsSection);
166
+ }
105
167
  function getPythonDepsContent(dir) {
106
168
  const files = ["requirements.txt", "pyproject.toml", "setup.py"];
107
169
  const parts = [];
@@ -155,6 +217,24 @@ function detectAppType(dir, stack) {
155
217
  }
156
218
  return "generic";
157
219
  }
220
+ if (stack === "rust") {
221
+ const cargoContent = readTextSafe(join(dir, "Cargo.toml"));
222
+ if (!cargoContent) return "generic";
223
+ const depsSection = getCargoDepsSection(cargoContent);
224
+ if (RUST_AGENT_DEPS.some((d) => hasCargoDep(depsSection, d))) {
225
+ return "agent";
226
+ }
227
+ if (RUST_WEB_FRAMEWORKS.some((d) => hasCargoDep(depsSection, d))) {
228
+ return "server";
229
+ }
230
+ if (/^\[\[bin\]\]\s*$/m.test(cargoContent)) {
231
+ return "cli";
232
+ }
233
+ if (/^\[lib\]\s*$/m.test(cargoContent)) {
234
+ return "generic";
235
+ }
236
+ return "generic";
237
+ }
158
238
  return "generic";
159
239
  }
160
240
 
@@ -162,14 +242,35 @@ function detectAppType(dir, stack) {
162
242
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
163
243
  import { join as join2 } from "path";
164
244
  import { parse, stringify } from "yaml";
245
+ function migrateState(state) {
246
+ const raw = state;
247
+ if (Array.isArray(raw.stacks) && raw.stacks.length > 0) {
248
+ state.stacks = raw.stacks;
249
+ state.stack = state.stacks[0] ?? null;
250
+ return state;
251
+ }
252
+ if (typeof raw.stack === "string" && raw.stack) {
253
+ state.stacks = [raw.stack];
254
+ return state;
255
+ }
256
+ state.stacks = [];
257
+ state.stack = null;
258
+ return state;
259
+ }
165
260
  var STATE_DIR = ".claude";
166
261
  var STATE_FILE = "codeharness.local.md";
167
262
  var DEFAULT_BODY = "\n# Codeharness State\n\nThis file is managed by the codeharness CLI. Do not edit manually.\n";
263
+ function getDefaultCoverageTool(stack) {
264
+ if (stack === "python") return "coverage.py";
265
+ if (stack === "rust") return "cargo-tarpaulin";
266
+ return "c8";
267
+ }
168
268
  function getDefaultState(stack) {
169
269
  return {
170
270
  harness_version: "0.1.0",
171
271
  initialized: false,
172
272
  stack: stack ?? null,
273
+ stacks: stack ? [stack] : [],
173
274
  enforcement: {
174
275
  frontend: true,
175
276
  database: true,
@@ -179,7 +280,7 @@ function getDefaultState(stack) {
179
280
  target: 90,
180
281
  baseline: null,
181
282
  current: null,
182
- tool: "c8"
283
+ tool: getDefaultCoverageTool(stack)
183
284
  },
184
285
  session_flags: {
185
286
  logs_queried: false,
@@ -197,7 +298,11 @@ function writeState(state, dir, body) {
197
298
  const baseDir = dir ?? process.cwd();
198
299
  const claudeDir = join2(baseDir, STATE_DIR);
199
300
  mkdirSync(claudeDir, { recursive: true });
200
- const yamlContent = stringify(state, { nullStr: "null" });
301
+ const toWrite = { ...state };
302
+ if (toWrite.stacks && toWrite.stacks.length > 0) {
303
+ toWrite.stack = toWrite.stacks[0];
304
+ }
305
+ const yamlContent = stringify(toWrite, { nullStr: "null" });
201
306
  const markdownBody = body ?? DEFAULT_BODY;
202
307
  const fileContent = `---
203
308
  ${yamlContent}---
@@ -220,7 +325,7 @@ function readState(dir) {
220
325
  if (!isValidState(state)) {
221
326
  return recoverCorruptedState(baseDir);
222
327
  }
223
- return state;
328
+ return migrateState(state);
224
329
  } catch {
225
330
  return recoverCorruptedState(baseDir);
226
331
  }
@@ -244,7 +349,7 @@ function readStateWithBody(dir) {
244
349
  return { state: recovered, body: DEFAULT_BODY };
245
350
  }
246
351
  const body = parts.slice(2).join("---");
247
- return { state, body: body || DEFAULT_BODY };
352
+ return { state: migrateState(state), body: body || DEFAULT_BODY };
248
353
  } catch {
249
354
  const state = recoverCorruptedState(baseDir);
250
355
  return { state, body: DEFAULT_BODY };
@@ -260,12 +365,20 @@ function isValidState(state) {
260
365
  if (!s.coverage || typeof s.coverage !== "object") return false;
261
366
  if (!s.session_flags || typeof s.session_flags !== "object") return false;
262
367
  if (!Array.isArray(s.verification_log)) return false;
368
+ if (s.stacks !== void 0 && !Array.isArray(s.stacks)) return false;
369
+ if (Array.isArray(s.stacks) && s.stacks.some((v) => typeof v !== "string")) return false;
263
370
  return true;
264
371
  }
265
372
  function recoverCorruptedState(dir) {
266
373
  warn("State file corrupted \u2014 recreating from detected config");
267
374
  const stack = detectStack(dir);
375
+ const allStacks = detectStacks(dir);
268
376
  const state = getDefaultState(stack);
377
+ const uniqueStackNames = [...new Set(allStacks.map((s) => s.stack))];
378
+ state.stacks = uniqueStackNames;
379
+ if (state.stack === null && uniqueStackNames.length > 0) {
380
+ state.stack = uniqueStackNames[0];
381
+ }
269
382
  writeState(state, dir);
270
383
  return state;
271
384
  }
@@ -332,6 +445,12 @@ var PYTHON_OTLP_PACKAGES = [
332
445
  "opentelemetry-distro",
333
446
  "opentelemetry-exporter-otlp"
334
447
  ];
448
+ var RUST_OTLP_PACKAGES = [
449
+ "opentelemetry",
450
+ "opentelemetry-otlp",
451
+ "tracing-opentelemetry",
452
+ "tracing-subscriber"
453
+ ];
335
454
  var WEB_OTLP_PACKAGES = [
336
455
  "@opentelemetry/sdk-trace-web",
337
456
  "@opentelemetry/instrumentation-fetch",
@@ -423,6 +542,26 @@ function installPythonOtlp(projectDir) {
423
542
  error: "Failed to install Python OTLP packages"
424
543
  };
425
544
  }
545
+ function installRustOtlp(projectDir) {
546
+ try {
547
+ execFileSync("cargo", ["add", ...RUST_OTLP_PACKAGES], { cwd: projectDir, stdio: "pipe", timeout: 3e5 });
548
+ return {
549
+ status: "configured",
550
+ packages_installed: true,
551
+ start_script_patched: false,
552
+ env_vars_configured: false
553
+ };
554
+ } catch (err) {
555
+ const message = err instanceof Error ? err.message : "Unknown error";
556
+ return {
557
+ status: "failed",
558
+ packages_installed: false,
559
+ start_script_patched: false,
560
+ env_vars_configured: false,
561
+ error: `Failed to install Rust OTLP packages: ${truncateError(message)}`
562
+ };
563
+ }
564
+ }
426
565
  function configureCli(projectDir) {
427
566
  const { state, body } = readStateWithBody(projectDir);
428
567
  if (!state.otlp) return;
@@ -496,6 +635,9 @@ function configureAgent(projectDir, stack) {
496
635
  } catch {
497
636
  }
498
637
  }
638
+ } else if (stack === "rust") {
639
+ info("Rust agent SDK not yet supported \u2014 skipping agent configuration");
640
+ return;
499
641
  }
500
642
  const { state, body } = readStateWithBody(projectDir);
501
643
  if (state.otlp) {
@@ -521,6 +663,23 @@ function ensureServiceNameEnvVar(projectDir, serviceName) {
521
663
  writeFileSync2(envFilePath, envLine + "\n", "utf-8");
522
664
  }
523
665
  }
666
+ function ensureEndpointEnvVar(projectDir, endpoint) {
667
+ const envFilePath = join3(projectDir, ".env.codeharness");
668
+ const envLine = `OTEL_EXPORTER_OTLP_ENDPOINT=${endpoint}`;
669
+ if (existsSync3(envFilePath)) {
670
+ const content = readFileSync3(envFilePath, "utf-8");
671
+ const lines = content.split("\n").filter((l, i, arr) => i < arr.length - 1 || l.trim() !== "");
672
+ const idx = lines.findIndex((l) => l.startsWith("OTEL_EXPORTER_OTLP_ENDPOINT="));
673
+ if (idx !== -1) {
674
+ lines[idx] = envLine;
675
+ } else {
676
+ lines.push(envLine);
677
+ }
678
+ writeFileSync2(envFilePath, lines.join("\n") + "\n", "utf-8");
679
+ } else {
680
+ writeFileSync2(envFilePath, envLine + "\n", "utf-8");
681
+ }
682
+ }
524
683
  function configureOtlpEnvVars(projectDir, stack, opts) {
525
684
  const projectName = basename(projectDir);
526
685
  const { state, body } = readStateWithBody(projectDir);
@@ -530,11 +689,15 @@ function configureOtlpEnvVars(projectDir, stack, opts) {
530
689
  service_name: projectName,
531
690
  mode: state.otlp?.mode ?? "local-shared",
532
691
  ...stack === "nodejs" ? { node_require: NODE_REQUIRE_FLAG } : {},
533
- ...stack === "python" ? { python_wrapper: "opentelemetry-instrument" } : {}
692
+ ...stack === "python" ? { python_wrapper: "opentelemetry-instrument" } : {},
693
+ ...stack === "rust" ? { rust_env_hint: "OTEL_EXPORTER_OTLP_ENDPOINT" } : {}
534
694
  };
535
695
  state.otlp.resource_attributes = "service.instance.id=$(hostname)-$$";
536
696
  writeState(state, projectDir, body);
537
697
  ensureServiceNameEnvVar(projectDir, projectName);
698
+ if (stack === "rust") {
699
+ ensureEndpointEnvVar(projectDir, state.otlp.endpoint);
700
+ }
538
701
  }
539
702
  function instrumentProject(projectDir, stack, opts) {
540
703
  const isJson = opts?.json === true;
@@ -560,6 +723,11 @@ function instrumentProject(projectDir, stack, opts) {
560
723
  ok("OTLP: Python packages installed");
561
724
  info("OTLP: wrap your command with: opentelemetry-instrument <command>");
562
725
  }
726
+ } else if (stack === "rust") {
727
+ result = installRustOtlp(projectDir);
728
+ if (result.status === "configured" && !isJson) {
729
+ ok("OTLP: Rust packages installed");
730
+ }
563
731
  } else {
564
732
  return {
565
733
  status: "skipped",
@@ -861,6 +1029,33 @@ var DEPENDENCY_REGISTRY = [
861
1029
  ],
862
1030
  checkCommand: { cmd: "bd", args: ["--version"] },
863
1031
  critical: false
1032
+ },
1033
+ {
1034
+ name: "semgrep",
1035
+ displayName: "Semgrep",
1036
+ installCommands: [
1037
+ { cmd: "pipx", args: ["install", "semgrep"] },
1038
+ { cmd: "pip", args: ["install", "semgrep"] }
1039
+ ],
1040
+ checkCommand: { cmd: "semgrep", args: ["--version"] },
1041
+ critical: false
1042
+ },
1043
+ {
1044
+ name: "bats",
1045
+ displayName: "BATS",
1046
+ installCommands: [
1047
+ { cmd: "brew", args: ["install", "bats-core"] },
1048
+ { cmd: "npm", args: ["install", "-g", "bats"] }
1049
+ ],
1050
+ checkCommand: { cmd: "bats", args: ["--version"] },
1051
+ critical: false
1052
+ },
1053
+ {
1054
+ name: "cargo-tarpaulin",
1055
+ displayName: "cargo-tarpaulin",
1056
+ installCommands: [{ cmd: "cargo", args: ["install", "cargo-tarpaulin"] }],
1057
+ checkCommand: { cmd: "cargo", args: ["tarpaulin", "--version"] },
1058
+ critical: false
864
1059
  }
865
1060
  ];
866
1061
  function checkInstalled(spec) {
@@ -1138,7 +1333,7 @@ var PATCH_TEMPLATES = {
1138
1333
 
1139
1334
  // src/lib/beads.ts
1140
1335
  import { execFileSync as execFileSync3 } from "child_process";
1141
- import { existsSync as existsSync5, readdirSync } from "fs";
1336
+ import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
1142
1337
  import { join as join5 } from "path";
1143
1338
  var BeadsError = class extends Error {
1144
1339
  constructor(command, originalMessage) {
@@ -1228,7 +1423,7 @@ function detectBeadsHooks(dir) {
1228
1423
  return { hasHooks: false, hookTypes: [] };
1229
1424
  }
1230
1425
  try {
1231
- const entries = readdirSync(hooksDir);
1426
+ const entries = readdirSync2(hooksDir);
1232
1427
  const hookTypes = entries.filter((e) => !e.startsWith("."));
1233
1428
  return {
1234
1429
  hasHooks: hookTypes.length > 0,
@@ -1776,9 +1971,15 @@ function readmeTemplate(config) {
1776
1971
  return lines.join("\n");
1777
1972
  }
1778
1973
  function getInstallCommand(stack) {
1779
- if (stack === "python") {
1780
- return "pip install codeharness";
1974
+ if (Array.isArray(stack)) {
1975
+ const commands = stack.map((s) => getSingleInstallCommand(s));
1976
+ return [...new Set(commands)].join("\n");
1781
1977
  }
1978
+ return getSingleInstallCommand(stack);
1979
+ }
1980
+ function getSingleInstallCommand(stack) {
1981
+ if (stack === "python") return "pip install codeharness";
1982
+ if (stack === "rust") return "cargo install codeharness";
1782
1983
  return "npm install -g codeharness";
1783
1984
  }
1784
1985
 
@@ -1795,20 +1996,46 @@ function getProjectName(projectDir) {
1795
1996
  }
1796
1997
  } catch {
1797
1998
  }
1999
+ try {
2000
+ const cargoPath = join7(projectDir, "Cargo.toml");
2001
+ if (existsSync7(cargoPath)) {
2002
+ const content = readFileSync7(cargoPath, "utf-8");
2003
+ const packageMatch = content.match(/\[package\]([\s\S]*?)(?=\n\[|$)/s);
2004
+ if (packageMatch) {
2005
+ const nameMatch = packageMatch[1].match(/^\s*name\s*=\s*["']([^"']+)["']/m);
2006
+ if (nameMatch) {
2007
+ return nameMatch[1];
2008
+ }
2009
+ }
2010
+ }
2011
+ } catch {
2012
+ }
1798
2013
  return basename2(projectDir);
1799
2014
  }
1800
2015
  function getStackLabel(stack) {
2016
+ if (Array.isArray(stack)) {
2017
+ if (stack.length === 0) return "Unknown";
2018
+ return stack.map((s) => getStackLabel(s)).join(" + ");
2019
+ }
1801
2020
  if (stack === "nodejs") return "Node.js (package.json)";
1802
2021
  if (stack === "python") return "Python";
2022
+ if (stack === "rust") return "Rust (Cargo.toml)";
1803
2023
  return "Unknown";
1804
2024
  }
1805
2025
  function getCoverageTool(stack) {
1806
2026
  if (stack === "python") return "coverage.py";
2027
+ if (stack === "rust") return "cargo-tarpaulin";
1807
2028
  return "c8";
1808
2029
  }
1809
2030
  function generateAgentsMdContent(projectDir, stack) {
2031
+ if (Array.isArray(stack) && stack.length > 1) {
2032
+ return generateMultiStackAgentsMd(projectDir, stack);
2033
+ }
2034
+ if (Array.isArray(stack)) {
2035
+ stack = stack.length === 1 ? stack[0].stack : null;
2036
+ }
1810
2037
  const projectName = basename2(projectDir);
1811
- const stackLabel = stack === "nodejs" ? "Node.js" : stack === "python" ? "Python" : "Unknown";
2038
+ const stackLabel = stack === "nodejs" ? "Node.js" : stack === "python" ? "Python" : stack === "rust" ? "Rust" : "Unknown";
1812
2039
  const lines = [
1813
2040
  `# ${projectName}`,
1814
2041
  "",
@@ -1834,6 +2061,14 @@ function generateAgentsMdContent(projectDir, stack) {
1834
2061
  "python -m pytest # Run tests",
1835
2062
  "```"
1836
2063
  );
2064
+ } else if (stack === "rust") {
2065
+ lines.push(
2066
+ "```bash",
2067
+ "cargo build # Build the project",
2068
+ "cargo test # Run tests",
2069
+ "cargo tarpaulin --out json # Run coverage",
2070
+ "```"
2071
+ );
1837
2072
  } else {
1838
2073
  lines.push("```bash", "# No recognized stack \u2014 add build/test commands here", "```");
1839
2074
  }
@@ -1858,6 +2093,70 @@ function generateAgentsMdContent(projectDir, stack) {
1858
2093
  );
1859
2094
  return lines.join("\n");
1860
2095
  }
2096
+ function stackDisplayName(stack) {
2097
+ if (stack === "nodejs") return "Node.js";
2098
+ if (stack === "python") return "Python";
2099
+ if (stack === "rust") return "Rust";
2100
+ return "Unknown";
2101
+ }
2102
+ function generateMultiStackAgentsMd(projectDir, stacks) {
2103
+ const projectName = basename2(projectDir);
2104
+ const stackNames = stacks.map((s) => stackDisplayName(s.stack));
2105
+ const lines = [
2106
+ `# ${projectName}`,
2107
+ "",
2108
+ "## Stack",
2109
+ "",
2110
+ `- **Language/Runtime:** ${stackNames.join(" + ")}`,
2111
+ "",
2112
+ "## Build & Test Commands",
2113
+ ""
2114
+ ];
2115
+ for (const detection of stacks) {
2116
+ const label = stackDisplayName(detection.stack);
2117
+ const heading = detection.dir === "." ? `### ${label}` : `### ${label} (${detection.dir}/)`;
2118
+ const prefix = detection.dir === "." ? "" : `cd ${detection.dir} && `;
2119
+ lines.push(heading, "", "```bash");
2120
+ if (detection.stack === "nodejs") {
2121
+ lines.push(
2122
+ `${prefix}npm install # Install dependencies`,
2123
+ `${prefix}npm run build # Build the project`,
2124
+ `${prefix}npm test # Run tests`
2125
+ );
2126
+ } else if (detection.stack === "python") {
2127
+ lines.push(
2128
+ `${prefix}pip install -r requirements.txt # Install dependencies`,
2129
+ `${prefix}python -m pytest # Run tests`
2130
+ );
2131
+ } else if (detection.stack === "rust") {
2132
+ lines.push(
2133
+ `${prefix}cargo build # Build the project`,
2134
+ `${prefix}cargo test # Run tests`,
2135
+ `${prefix}cargo tarpaulin --out json # Run coverage`
2136
+ );
2137
+ }
2138
+ lines.push("```", "");
2139
+ }
2140
+ lines.push(
2141
+ "## Project Structure",
2142
+ "",
2143
+ "```",
2144
+ `${projectName}/`,
2145
+ "\u251C\u2500\u2500 src/ # Source code",
2146
+ "\u251C\u2500\u2500 tests/ # Test files",
2147
+ "\u251C\u2500\u2500 docs/ # Documentation",
2148
+ "\u2514\u2500\u2500 .claude/ # Codeharness state",
2149
+ "```",
2150
+ "",
2151
+ "## Conventions",
2152
+ "",
2153
+ "- All changes must pass tests before commit",
2154
+ "- Maintain test coverage targets",
2155
+ "- Follow existing code style and patterns",
2156
+ ""
2157
+ );
2158
+ return lines.join("\n");
2159
+ }
1861
2160
  function generateDocsIndexContent() {
1862
2161
  return [
1863
2162
  "# Project Documentation",
@@ -1884,7 +2183,8 @@ async function scaffoldDocs(opts) {
1884
2183
  let readme = "skipped";
1885
2184
  const agentsMdPath = join7(opts.projectDir, "AGENTS.md");
1886
2185
  if (!existsSync7(agentsMdPath)) {
1887
- const content = generateAgentsMdContent(opts.projectDir, opts.stack);
2186
+ const stackArg = opts.stacks && opts.stacks.length > 1 ? opts.stacks : opts.stack;
2187
+ const content = generateAgentsMdContent(opts.projectDir, stackArg);
1888
2188
  generateFile(agentsMdPath, content);
1889
2189
  agentsMd = "created";
1890
2190
  } else {
@@ -1913,9 +2213,10 @@ async function scaffoldDocs(opts) {
1913
2213
  } catch {
1914
2214
  cliHelpOutput = "Run: codeharness --help";
1915
2215
  }
2216
+ const readmeStack = opts.stacks && opts.stacks.length > 1 ? opts.stacks.map((s) => s.stack) : opts.stack;
1916
2217
  const readmeContent = readmeTemplate({
1917
2218
  projectName: getProjectName(opts.projectDir),
1918
- stack: opts.stack,
2219
+ stack: readmeStack,
1919
2220
  cliHelpOutput
1920
2221
  });
1921
2222
  generateFile(readmePath, readmeContent);
@@ -1939,12 +2240,209 @@ async function scaffoldDocs(opts) {
1939
2240
  }
1940
2241
  }
1941
2242
 
2243
+ // src/modules/infra/dockerfile-template.ts
2244
+ import { existsSync as existsSync8, writeFileSync as writeFileSync5 } from "fs";
2245
+ import { join as join8 } from "path";
2246
+ function nodejsTemplate() {
2247
+ return `# Base image \u2014 pinned version for reproducibility
2248
+ FROM node:22-slim
2249
+
2250
+ ARG TARBALL=package.tgz
2251
+
2252
+ # System utilities for verification
2253
+ RUN apt-get update && apt-get install -y --no-install-recommends curl jq && rm -rf /var/lib/apt/lists/*
2254
+
2255
+ # Install project from tarball (black-box: no source code)
2256
+ COPY \${TARBALL} /tmp/\${TARBALL}
2257
+ RUN npm install -g /tmp/\${TARBALL} && rm /tmp/\${TARBALL}
2258
+
2259
+ # Run as non-root user
2260
+ USER node
2261
+
2262
+ WORKDIR /workspace
2263
+ `;
2264
+ }
2265
+ function pythonTemplate() {
2266
+ return `# Base image \u2014 pinned version for reproducibility
2267
+ FROM python:3.12-slim
2268
+
2269
+ # System utilities for verification
2270
+ RUN apt-get update && apt-get install -y --no-install-recommends curl jq && rm -rf /var/lib/apt/lists/*
2271
+
2272
+ # Install project from wheel or sdist
2273
+ COPY dist/ /tmp/dist/
2274
+ RUN pip install /tmp/dist/*.whl && rm -rf /tmp/dist/ && pip cache purge
2275
+
2276
+ # Run as non-root user
2277
+ USER nobody
2278
+
2279
+ WORKDIR /workspace
2280
+ `;
2281
+ }
2282
+ function rustTemplate() {
2283
+ return `# === Builder stage ===
2284
+ FROM rust:1.82-slim AS builder
2285
+
2286
+ WORKDIR /build
2287
+
2288
+ # Copy project files
2289
+ COPY . .
2290
+
2291
+ # Build release binary
2292
+ RUN cargo build --release
2293
+
2294
+ # === Runtime stage ===
2295
+ FROM debian:bookworm-slim
2296
+
2297
+ # System utilities for verification
2298
+ RUN apt-get update && apt-get install -y --no-install-recommends curl jq && rm -rf /var/lib/apt/lists/*
2299
+
2300
+ # Install compiled binary from builder (update 'myapp' to your binary name)
2301
+ COPY --from=builder /build/target/release/myapp /usr/local/bin/myapp
2302
+
2303
+ # Run as non-root user
2304
+ USER nobody
2305
+
2306
+ WORKDIR /workspace
2307
+ `;
2308
+ }
2309
+ function genericTemplate() {
2310
+ return `# Base image \u2014 pinned version for reproducibility
2311
+ FROM node:22-slim
2312
+
2313
+ # System utilities for verification
2314
+ RUN apt-get update && apt-get install -y --no-install-recommends bash curl jq git && rm -rf /var/lib/apt/lists/*
2315
+
2316
+ # Install project binary (update this for your project)
2317
+ RUN npm install -g placeholder && npm cache clean --force
2318
+
2319
+ # Run as non-root user
2320
+ USER node
2321
+
2322
+ WORKDIR /workspace
2323
+ `;
2324
+ }
2325
+ function nodejsBuildStage() {
2326
+ return `# === Build stage: nodejs ===
2327
+ FROM node:22-slim AS build-nodejs
2328
+ WORKDIR /build
2329
+ COPY package*.json ./
2330
+ RUN npm ci --production
2331
+ COPY . .
2332
+ `;
2333
+ }
2334
+ function pythonBuildStage() {
2335
+ return `# === Build stage: python ===
2336
+ FROM python:3.12-slim AS build-python
2337
+ WORKDIR /build
2338
+ COPY . .
2339
+ RUN pip install --target=/build/dist .
2340
+ `;
2341
+ }
2342
+ function rustBuildStage() {
2343
+ return `# === Build stage: rust ===
2344
+ FROM rust:1.82-slim AS build-rust
2345
+ WORKDIR /build
2346
+ COPY . .
2347
+ RUN cargo build --release
2348
+ `;
2349
+ }
2350
+ function runtimeCopyDirectives(stacks) {
2351
+ const lines = [];
2352
+ for (const stack of stacks) {
2353
+ if (stack === "nodejs") {
2354
+ lines.push("COPY --from=build-nodejs /build/node_modules ./node_modules");
2355
+ lines.push("COPY --from=build-nodejs /build/ ./app/");
2356
+ } else if (stack === "python") {
2357
+ lines.push("COPY --from=build-python /build/dist /opt/app/python/");
2358
+ } else if (stack === "rust") {
2359
+ lines.push("COPY --from=build-rust /build/target/release/myapp /usr/local/bin/myapp");
2360
+ }
2361
+ }
2362
+ return lines.join("\n");
2363
+ }
2364
+ var MULTI_STAGE_STACKS = /* @__PURE__ */ new Set(["nodejs", "python", "rust"]);
2365
+ function multiStageTemplate(detections) {
2366
+ const supported = detections.filter((d) => MULTI_STAGE_STACKS.has(d.stack));
2367
+ if (supported.length === 0) {
2368
+ return genericTemplate();
2369
+ }
2370
+ const buildStages = [];
2371
+ const stacks = supported.map((d) => d.stack);
2372
+ for (const stack of stacks) {
2373
+ if (stack === "nodejs") buildStages.push(nodejsBuildStage());
2374
+ else if (stack === "python") buildStages.push(pythonBuildStage());
2375
+ else if (stack === "rust") buildStages.push(rustBuildStage());
2376
+ }
2377
+ const copyLines = runtimeCopyDirectives(stacks);
2378
+ return `# NOTE: Customize COPY paths for your monorepo layout. Each build stage should only copy its own sources.
2379
+ ${buildStages.join("\n")}
2380
+ # === Runtime stage ===
2381
+ FROM debian:bookworm-slim
2382
+
2383
+ # System utilities for verification
2384
+ RUN apt-get update && apt-get install -y --no-install-recommends curl jq && rm -rf /var/lib/apt/lists/*
2385
+
2386
+ # Install artifacts from build stages
2387
+ ${copyLines}
2388
+
2389
+ # Run as non-root user
2390
+ USER nobody
2391
+
2392
+ WORKDIR /workspace
2393
+ `;
2394
+ }
2395
+ function generateDockerfileTemplate(projectDir, stackOrDetections) {
2396
+ if (!projectDir) {
2397
+ return fail2("projectDir is required");
2398
+ }
2399
+ const dockerfilePath = join8(projectDir, "Dockerfile");
2400
+ if (existsSync8(dockerfilePath)) {
2401
+ return fail2("Dockerfile already exists");
2402
+ }
2403
+ if (Array.isArray(stackOrDetections) && stackOrDetections.length > 1) {
2404
+ const content2 = multiStageTemplate(stackOrDetections);
2405
+ const stacks = stackOrDetections.map((d) => d.stack);
2406
+ try {
2407
+ writeFileSync5(dockerfilePath, content2, "utf-8");
2408
+ } catch (err) {
2409
+ const message = err instanceof Error ? err.message : String(err);
2410
+ return fail2(`Failed to write Dockerfile: ${message}`);
2411
+ }
2412
+ return ok2({ path: dockerfilePath, stack: stacks[0], stacks });
2413
+ }
2414
+ const stack = Array.isArray(stackOrDetections) ? stackOrDetections.length === 1 ? stackOrDetections[0].stack : null : stackOrDetections;
2415
+ let content;
2416
+ let resolvedStack;
2417
+ if (stack === "nodejs") {
2418
+ content = nodejsTemplate();
2419
+ resolvedStack = "nodejs";
2420
+ } else if (stack === "python") {
2421
+ content = pythonTemplate();
2422
+ resolvedStack = "python";
2423
+ } else if (stack === "rust") {
2424
+ content = rustTemplate();
2425
+ resolvedStack = "rust";
2426
+ } else {
2427
+ content = genericTemplate();
2428
+ resolvedStack = "generic";
2429
+ }
2430
+ try {
2431
+ writeFileSync5(dockerfilePath, content, "utf-8");
2432
+ } catch (err) {
2433
+ const message = err instanceof Error ? err.message : String(err);
2434
+ return fail2(`Failed to write Dockerfile: ${message}`);
2435
+ }
2436
+ return ok2({ path: dockerfilePath, stack: resolvedStack, stacks: [resolvedStack] });
2437
+ }
2438
+
1942
2439
  // src/modules/infra/init-project.ts
1943
- var HARNESS_VERSION = true ? "0.23.0" : "0.0.0-dev";
2440
+ var HARNESS_VERSION = true ? "0.24.0" : "0.0.0-dev";
1944
2441
  function failResult(opts, error) {
1945
2442
  return {
1946
2443
  status: "fail",
1947
2444
  stack: null,
2445
+ stacks: [],
1948
2446
  error,
1949
2447
  enforcement: { frontend: opts.frontend, database: opts.database, api: opts.api },
1950
2448
  documentation: { agents_md: "skipped", docs_scaffold: "skipped", readme: "skipped" }
@@ -1971,6 +2469,7 @@ async function initProjectInner(opts) {
1971
2469
  const result = {
1972
2470
  status: "ok",
1973
2471
  stack: null,
2472
+ stacks: [],
1974
2473
  enforcement: { frontend: opts.frontend, database: opts.database, api: opts.api },
1975
2474
  documentation: { agents_md: "skipped", docs_scaffold: "skipped", readme: "skipped" }
1976
2475
  };
@@ -1981,14 +2480,29 @@ async function initProjectInner(opts) {
1981
2480
  emitError(urlError, isJson);
1982
2481
  return ok2(failResult(opts, urlError));
1983
2482
  }
1984
- const stack = detectStack(projectDir);
2483
+ const allStacks = detectStacks(projectDir);
2484
+ const rootDetection = allStacks.find((s) => s.dir === ".");
2485
+ const stack = rootDetection ? rootDetection.stack : null;
2486
+ if (!stack && !isJson) warn("No recognized stack detected");
1985
2487
  result.stack = stack;
2488
+ result.stacks = [...new Set(allStacks.map((s) => s.stack))];
1986
2489
  const appType = detectAppType(projectDir, stack);
1987
2490
  result.app_type = appType;
1988
2491
  if (!isJson) {
1989
- if (stack) info(`Stack detected: ${getStackLabel(stack)}`);
2492
+ if (result.stacks.length > 0) {
2493
+ info(`Stack detected: ${getStackLabel(result.stacks)}`);
2494
+ } else if (stack) {
2495
+ info(`Stack detected: ${getStackLabel(stack)}`);
2496
+ }
1990
2497
  info(`App type: ${appType}`);
1991
2498
  }
2499
+ const dfResult = generateDockerfileTemplate(projectDir, allStacks);
2500
+ if (isOk(dfResult)) {
2501
+ result.dockerfile = { generated: true, stack: dfResult.data.stack, stacks: dfResult.data.stacks };
2502
+ if (!isJson) info(`Generated Dockerfile for ${dfResult.data.stacks.join("+") || dfResult.data.stack} project.`);
2503
+ } else {
2504
+ if (!isJson) info("Dockerfile already exists -- skipping template generation.");
2505
+ }
1992
2506
  const dockerCheck = checkDocker({ observability: opts.observability, otelEndpoint: opts.otelEndpoint, logsUrl: opts.logsUrl, opensearchUrl: opts.opensearchUrl, isJson });
1993
2507
  if (!isOk(dockerCheck)) return fail2(dockerCheck.error);
1994
2508
  const { available: dockerAvailable, criticalFailure, dockerResult: criticalDockerResult } = dockerCheck.data;
@@ -2027,16 +2541,31 @@ async function initProjectInner(opts) {
2027
2541
  state.initialized = true;
2028
2542
  state.app_type = appType;
2029
2543
  state.enforcement = { frontend: opts.frontend, database: opts.database, api: opts.api };
2544
+ const coverageTools = {};
2545
+ for (const detection of allStacks) {
2546
+ coverageTools[detection.stack] = getCoverageTool(detection.stack);
2547
+ }
2030
2548
  state.coverage.tool = getCoverageTool(stack);
2549
+ state.coverage.tools = coverageTools;
2550
+ state.stacks = result.stacks;
2031
2551
  writeState(state, projectDir);
2032
2552
  if (!isJson) ok("State file: .claude/codeharness.local.md created");
2033
- const docsResult = await scaffoldDocs({ projectDir, stack, isJson });
2553
+ const docsResult = await scaffoldDocs({ projectDir, stack, stacks: allStacks, isJson });
2034
2554
  if (isOk(docsResult)) result.documentation = docsResult.data;
2035
2555
  if (!opts.observability) {
2036
2556
  result.otlp = { status: "skipped", packages_installed: false, start_script_patched: false, env_vars_configured: false };
2037
2557
  if (!isJson) info("OTLP: skipped (--no-observability)");
2038
2558
  } else {
2039
- result.otlp = instrumentProject(projectDir, stack, { json: isJson, appType });
2559
+ for (const detection of allStacks) {
2560
+ const stackDir = detection.dir === "." ? projectDir : join9(projectDir, detection.dir);
2561
+ const stackOtlp = instrumentProject(stackDir, detection.stack, { json: isJson, appType });
2562
+ if (detection.dir === "." && detection.stack === stack) {
2563
+ result.otlp = stackOtlp;
2564
+ }
2565
+ }
2566
+ if (!result.otlp) {
2567
+ result.otlp = instrumentProject(projectDir, stack, { json: isJson, appType });
2568
+ }
2040
2569
  }
2041
2570
  try {
2042
2571
  const u = readState(projectDir);
@@ -2071,7 +2600,7 @@ async function initProjectInner(opts) {
2071
2600
  }
2072
2601
  function handleRerun(opts, result) {
2073
2602
  const { projectDir, json: isJson = false } = opts;
2074
- if (!existsSync8(getStatePath(projectDir))) return null;
2603
+ if (!existsSync9(getStatePath(projectDir))) return null;
2075
2604
  try {
2076
2605
  const existingState = readState(projectDir);
2077
2606
  const legacyObsDisabled = existingState.enforcement.observability === false;
@@ -2080,6 +2609,7 @@ function handleRerun(opts, result) {
2080
2609
  return null;
2081
2610
  }
2082
2611
  result.stack = existingState.stack;
2612
+ result.stacks = existingState.stacks ?? [];
2083
2613
  result.enforcement = existingState.enforcement;
2084
2614
  result.documentation = { agents_md: "exists", docs_scaffold: "exists", readme: "exists" };
2085
2615
  result.dependencies = verifyDeps(isJson);
@@ -2123,6 +2653,136 @@ var DEFAULT_LOGS_URL = `http://localhost:${DEFAULT_PORTS.logs}`;
2123
2653
  var DEFAULT_METRICS_URL = `http://localhost:${DEFAULT_PORTS.metrics}`;
2124
2654
  var DEFAULT_TRACES_URL = `http://localhost:${DEFAULT_PORTS.traces}`;
2125
2655
 
2656
+ // src/modules/infra/dockerfile-validator.ts
2657
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
2658
+ import { join as join10 } from "path";
2659
+ var DEFAULT_RULES = {
2660
+ requirePinnedFrom: true,
2661
+ requireBinaryOnPath: true,
2662
+ verificationTools: ["curl", "jq"],
2663
+ forbidSourceCopy: true,
2664
+ requireNonRootUser: true,
2665
+ requireCacheCleanup: true
2666
+ };
2667
+ function dfGap(rule, description, suggestedFix, line) {
2668
+ const g = { rule, description, suggestedFix };
2669
+ if (line !== void 0) return { ...g, line };
2670
+ return g;
2671
+ }
2672
+ function loadRules(projectDir) {
2673
+ const rulesPath = join10(projectDir, "patches", "infra", "dockerfile-rules.md");
2674
+ if (!existsSync10(rulesPath)) {
2675
+ return {
2676
+ rules: DEFAULT_RULES,
2677
+ warnings: ["dockerfile-rules.md not found -- using defaults."]
2678
+ };
2679
+ }
2680
+ return { rules: DEFAULT_RULES, warnings: [] };
2681
+ }
2682
+ function checkPinnedFrom(lines) {
2683
+ const gaps = [];
2684
+ for (let i = 0; i < lines.length; i++) {
2685
+ if (!/^\s*FROM\s+/i.test(lines[i])) continue;
2686
+ const ref = lines[i].replace(/^\s*FROM\s+/i, "").split(/\s+/)[0];
2687
+ if (ref.endsWith(":latest")) {
2688
+ gaps.push(dfGap("pinned-from", `unpinned base image -- use specific version.`, `Pin ${ref} to a specific version tag`, i + 1));
2689
+ } else if (!ref.includes(":") && !ref.includes("@")) {
2690
+ gaps.push(dfGap("pinned-from", `unpinned base image -- use specific version.`, `Pin ${ref} to a specific version tag (e.g., ${ref}:22-slim)`, i + 1));
2691
+ }
2692
+ }
2693
+ return gaps;
2694
+ }
2695
+ function checkBinaryOnPath(lines) {
2696
+ const content = lines.join("\n");
2697
+ const hasBinary = /npm\s+install\s+(-g|--global)\b/i.test(content) || /pip\s+install\b/i.test(content) || /COPY\s+--from=/i.test(content);
2698
+ if (!hasBinary) {
2699
+ return [dfGap("binary-on-path", "project binary not installed.", "Add npm install -g, pip install, or COPY --from to install the project binary")];
2700
+ }
2701
+ return [];
2702
+ }
2703
+ function checkVerificationTools(lines, tools) {
2704
+ const gaps = [];
2705
+ for (const tool of tools) {
2706
+ let found = false;
2707
+ for (const line of lines) {
2708
+ const lower = line.toLowerCase();
2709
+ const isInstallLine = lower.includes("apt-get install") || lower.includes("apk add");
2710
+ if (isInstallLine && new RegExp(`\\b${tool.toLowerCase()}\\b`).test(lower)) {
2711
+ found = true;
2712
+ break;
2713
+ }
2714
+ }
2715
+ if (!found) {
2716
+ gaps.push(dfGap("verification-tools", `verification tool missing: ${tool}`, `Install ${tool} via apt-get install or apk add`));
2717
+ }
2718
+ }
2719
+ return gaps;
2720
+ }
2721
+ function checkNoSourceCopy(lines) {
2722
+ const gaps = [];
2723
+ const sourcePatterns = [/COPY\s+(?:--\S+\s+)*src\//i, /COPY\s+(?:--\S+\s+)*lib\//i, /COPY\s+(?:--\S+\s+)*test\//i];
2724
+ for (let i = 0; i < lines.length; i++) {
2725
+ for (const pattern of sourcePatterns) {
2726
+ if (pattern.test(lines[i])) {
2727
+ gaps.push(dfGap("no-source-copy", "source code copied into container -- use build artifact instead.", "Use COPY --from=builder or COPY dist/ instead of copying source", i + 1));
2728
+ }
2729
+ }
2730
+ }
2731
+ return gaps;
2732
+ }
2733
+ function checkNonRootUser(lines) {
2734
+ const userLines = lines.filter((l) => /^\s*USER\s+/i.test(l));
2735
+ if (userLines.length === 0) {
2736
+ return [dfGap("non-root-user", "no non-root USER instruction found.", "Add USER <non-root-user> instruction (e.g., USER node)")];
2737
+ }
2738
+ const hasNonRoot = userLines.some((l) => {
2739
+ const user = l.replace(/^\s*USER\s+/i, "").trim().split(/\s+/)[0];
2740
+ return user.toLowerCase() !== "root";
2741
+ });
2742
+ if (!hasNonRoot) {
2743
+ return [dfGap("non-root-user", "no non-root USER instruction found.", "Add USER <non-root-user> instruction (e.g., USER node)")];
2744
+ }
2745
+ return [];
2746
+ }
2747
+ function checkCacheCleanup(lines) {
2748
+ const content = lines.join("\n");
2749
+ const hasCleanup = /rm\s+-rf\s+\/var\/lib\/apt\/lists/i.test(content) || /rm\s+-rf\s+\/var\/cache\/apk/i.test(content) || /npm\s+cache\s+clean/i.test(content) || /pip\s+cache\s+purge/i.test(content);
2750
+ if (!hasCleanup) {
2751
+ return [dfGap("cache-cleanup", "no cache cleanup detected.", "Add cache cleanup: rm -rf /var/lib/apt/lists/*, npm cache clean --force, or pip cache purge")];
2752
+ }
2753
+ return [];
2754
+ }
2755
+ function validateDockerfile(projectDir) {
2756
+ const dfPath = join10(projectDir, "Dockerfile");
2757
+ if (!existsSync10(dfPath)) {
2758
+ return fail2("No Dockerfile found");
2759
+ }
2760
+ let content;
2761
+ try {
2762
+ content = readFileSync8(dfPath, "utf-8");
2763
+ } catch {
2764
+ return fail2("Dockerfile exists but could not be read");
2765
+ }
2766
+ const lines = content.split("\n");
2767
+ const fromLines = lines.filter((l) => /^\s*FROM\s+/i.test(l));
2768
+ if (fromLines.length === 0) {
2769
+ return fail2("Dockerfile has no FROM instruction");
2770
+ }
2771
+ const { rules, warnings } = loadRules(projectDir);
2772
+ const gaps = [];
2773
+ if (rules.requirePinnedFrom) gaps.push(...checkPinnedFrom(lines));
2774
+ if (rules.requireBinaryOnPath) gaps.push(...checkBinaryOnPath(lines));
2775
+ gaps.push(...checkVerificationTools(lines, rules.verificationTools));
2776
+ if (rules.forbidSourceCopy) gaps.push(...checkNoSourceCopy(lines));
2777
+ if (rules.requireNonRootUser) gaps.push(...checkNonRootUser(lines));
2778
+ if (rules.requireCacheCleanup) gaps.push(...checkCacheCleanup(lines));
2779
+ return ok2({
2780
+ passed: gaps.length === 0,
2781
+ gaps,
2782
+ warnings
2783
+ });
2784
+ }
2785
+
2126
2786
  // src/modules/infra/index.ts
2127
2787
  async function initProject2(opts) {
2128
2788
  return initProject(opts);
@@ -2156,7 +2816,7 @@ function registerInitCommand(program) {
2156
2816
  }
2157
2817
 
2158
2818
  // src/commands/bridge.ts
2159
- import { existsSync as existsSync9 } from "fs";
2819
+ import { existsSync as existsSync11 } from "fs";
2160
2820
  function registerBridgeCommand(program) {
2161
2821
  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) => {
2162
2822
  const globalOpts = cmd.optsWithGlobals();
@@ -2168,7 +2828,7 @@ function registerBridgeCommand(program) {
2168
2828
  process.exitCode = 2;
2169
2829
  return;
2170
2830
  }
2171
- if (!existsSync9(epicsPath)) {
2831
+ if (!existsSync11(epicsPath)) {
2172
2832
  fail(`Epics file not found: ${epicsPath}`, { json: isJson });
2173
2833
  process.exitCode = 1;
2174
2834
  return;
@@ -2241,14 +2901,13 @@ function registerBridgeCommand(program) {
2241
2901
 
2242
2902
  // src/commands/run.ts
2243
2903
  import { spawn } from "child_process";
2244
- import { existsSync as existsSync16, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
2245
- import { join as join13, dirname as dirname4 } from "path";
2904
+ import { existsSync as existsSync18, mkdirSync as mkdirSync4, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
2905
+ import { join as join16, dirname as dirname4 } from "path";
2246
2906
  import { fileURLToPath as fileURLToPath2 } from "url";
2247
- import { StringDecoder } from "string_decoder";
2248
2907
 
2249
2908
  // src/lib/beads-sync.ts
2250
- import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
2251
- import { join as join8 } from "path";
2909
+ import { existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
2910
+ import { join as join11 } from "path";
2252
2911
  import { parse as parse2 } from "yaml";
2253
2912
  var BEADS_TO_STORY_STATUS = {
2254
2913
  open: "in-progress",
@@ -2279,10 +2938,10 @@ function resolveStoryFilePath(beadsIssue) {
2279
2938
  return trimmed;
2280
2939
  }
2281
2940
  function readStoryFileStatus(filePath) {
2282
- if (!existsSync10(filePath)) {
2941
+ if (!existsSync12(filePath)) {
2283
2942
  return null;
2284
2943
  }
2285
- const content = readFileSync8(filePath, "utf-8");
2944
+ const content = readFileSync9(filePath, "utf-8");
2286
2945
  const match = content.match(/^Status:\s*(.+)$/m);
2287
2946
  if (!match) {
2288
2947
  return null;
@@ -2290,11 +2949,11 @@ function readStoryFileStatus(filePath) {
2290
2949
  return match[1].trim();
2291
2950
  }
2292
2951
  function updateStoryFileStatus(filePath, newStatus) {
2293
- const content = readFileSync8(filePath, "utf-8");
2952
+ const content = readFileSync9(filePath, "utf-8");
2294
2953
  const statusRegex = /^Status:\s*.+$/m;
2295
2954
  if (statusRegex.test(content)) {
2296
2955
  const updated = content.replace(statusRegex, `Status: ${newStatus}`);
2297
- writeFileSync5(filePath, updated, "utf-8");
2956
+ writeFileSync6(filePath, updated, "utf-8");
2298
2957
  } else {
2299
2958
  const lines = content.split("\n");
2300
2959
  const titleIndex = lines.findIndex((l) => l.startsWith("# "));
@@ -2303,18 +2962,18 @@ function updateStoryFileStatus(filePath, newStatus) {
2303
2962
  } else {
2304
2963
  lines.unshift(`Status: ${newStatus}`, "");
2305
2964
  }
2306
- writeFileSync5(filePath, lines.join("\n"), "utf-8");
2965
+ writeFileSync6(filePath, lines.join("\n"), "utf-8");
2307
2966
  }
2308
2967
  }
2309
2968
  var SPRINT_STATUS_PATH = "_bmad-output/implementation-artifacts/sprint-status.yaml";
2310
2969
  function readSprintStatus(dir) {
2311
2970
  const root = dir ?? process.cwd();
2312
- const filePath = join8(root, SPRINT_STATUS_PATH);
2313
- if (!existsSync10(filePath)) {
2971
+ const filePath = join11(root, SPRINT_STATUS_PATH);
2972
+ if (!existsSync12(filePath)) {
2314
2973
  return {};
2315
2974
  }
2316
2975
  try {
2317
- const content = readFileSync8(filePath, "utf-8");
2976
+ const content = readFileSync9(filePath, "utf-8");
2318
2977
  const parsed = parse2(content);
2319
2978
  if (!parsed || typeof parsed !== "object") {
2320
2979
  return {};
@@ -2330,18 +2989,18 @@ function readSprintStatus(dir) {
2330
2989
  }
2331
2990
  function updateSprintStatus(storyKey, newStatus, dir) {
2332
2991
  const root = dir ?? process.cwd();
2333
- const filePath = join8(root, SPRINT_STATUS_PATH);
2334
- if (!existsSync10(filePath)) {
2992
+ const filePath = join11(root, SPRINT_STATUS_PATH);
2993
+ if (!existsSync12(filePath)) {
2335
2994
  warn(`sprint-status.yaml not found at ${filePath}, skipping update`);
2336
2995
  return;
2337
2996
  }
2338
- const content = readFileSync8(filePath, "utf-8");
2997
+ const content = readFileSync9(filePath, "utf-8");
2339
2998
  const keyPattern = new RegExp(`^(\\s*${escapeRegExp(storyKey)}:\\s*)\\S+(.*)$`, "m");
2340
2999
  if (!keyPattern.test(content)) {
2341
3000
  return;
2342
3001
  }
2343
3002
  const updated = content.replace(keyPattern, `$1${newStatus}$2`);
2344
- writeFileSync5(filePath, updated, "utf-8");
3003
+ writeFileSync6(filePath, updated, "utf-8");
2345
3004
  }
2346
3005
  function escapeRegExp(s) {
2347
3006
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -2376,7 +3035,7 @@ function syncBeadsToStoryFile(beadsId, beadsFns, dir) {
2376
3035
  };
2377
3036
  }
2378
3037
  const storyKey = storyKeyFromPath(storyFilePath);
2379
- const fullPath = join8(root, storyFilePath);
3038
+ const fullPath = join11(root, storyFilePath);
2380
3039
  const currentStoryStatus = readStoryFileStatus(fullPath);
2381
3040
  if (currentStoryStatus === null) {
2382
3041
  return {
@@ -2421,7 +3080,7 @@ function syncBeadsToStoryFile(beadsId, beadsFns, dir) {
2421
3080
  function syncStoryFileToBeads(storyKey, beadsFns, dir) {
2422
3081
  const root = dir ?? process.cwd();
2423
3082
  const storyFilePath = `_bmad-output/implementation-artifacts/${storyKey}.md`;
2424
- const fullPath = join8(root, storyFilePath);
3083
+ const fullPath = join11(root, storyFilePath);
2425
3084
  const currentStoryStatus = readStoryFileStatus(fullPath);
2426
3085
  if (currentStoryStatus === null) {
2427
3086
  return {
@@ -2510,10 +3169,10 @@ function syncClose(beadsId, beadsFns, dir) {
2510
3169
  };
2511
3170
  }
2512
3171
  const storyKey = storyKeyFromPath(storyFilePath);
2513
- const fullPath = join8(root, storyFilePath);
3172
+ const fullPath = join11(root, storyFilePath);
2514
3173
  const previousStatus = readStoryFileStatus(fullPath);
2515
3174
  if (previousStatus === null) {
2516
- if (!existsSync10(fullPath)) {
3175
+ if (!existsSync12(fullPath)) {
2517
3176
  return {
2518
3177
  storyKey,
2519
3178
  beadsId,
@@ -2643,178 +3302,27 @@ function generateRalphPrompt(config) {
2643
3302
  prompt += `
2644
3303
  ## Flagged Stories (Skip These)
2645
3304
 
2646
- `;
2647
- prompt += `The following stories have exceeded the retry limit and should be skipped:
2648
- `;
2649
- for (const story of config.flaggedStories) {
2650
- prompt += `- \`${story}\`
2651
- `;
2652
- }
2653
- }
2654
- return prompt;
2655
- }
2656
-
2657
- // src/lib/stream-parser.ts
2658
- function parseStreamLine(line) {
2659
- const trimmed = line.trim();
2660
- if (trimmed.length === 0) {
2661
- return null;
2662
- }
2663
- let parsed;
2664
- try {
2665
- parsed = JSON.parse(trimmed);
2666
- } catch {
2667
- return null;
2668
- }
2669
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2670
- return null;
2671
- }
2672
- const wrapperType = parsed.type;
2673
- if (wrapperType === "stream_event") {
2674
- return parseStreamEvent(parsed);
2675
- }
2676
- if (wrapperType === "system") {
2677
- return parseSystemEvent(parsed);
2678
- }
2679
- if (wrapperType === "result") {
2680
- return parseResultEvent(parsed);
2681
- }
2682
- return null;
2683
- }
2684
- function parseStreamEvent(parsed) {
2685
- const event = parsed.event;
2686
- if (!event || typeof event !== "object") {
2687
- return null;
2688
- }
2689
- const eventType = event.type;
2690
- if (eventType === "content_block_start") {
2691
- return parseContentBlockStart(event);
2692
- }
2693
- if (eventType === "content_block_delta") {
2694
- return parseContentBlockDelta(event);
2695
- }
2696
- if (eventType === "content_block_stop") {
2697
- return { type: "tool-complete" };
2698
- }
2699
- return null;
2700
- }
2701
- function parseContentBlockStart(event) {
2702
- const contentBlock = event.content_block;
2703
- if (!contentBlock || typeof contentBlock !== "object") {
2704
- return null;
2705
- }
2706
- if (contentBlock.type === "tool_use") {
2707
- const name = contentBlock.name;
2708
- const id = contentBlock.id;
2709
- if (typeof name === "string" && typeof id === "string") {
2710
- return { type: "tool-start", name, id };
2711
- }
2712
- }
2713
- return null;
2714
- }
2715
- function parseContentBlockDelta(event) {
2716
- const delta = event.delta;
2717
- if (!delta || typeof delta !== "object") {
2718
- return null;
2719
- }
2720
- if (delta.type === "input_json_delta") {
2721
- const partialJson = delta.partial_json;
2722
- if (typeof partialJson === "string") {
2723
- return { type: "tool-input", partial: partialJson };
2724
- }
2725
- return null;
2726
- }
2727
- if (delta.type === "text_delta") {
2728
- const text = delta.text;
2729
- if (typeof text === "string") {
2730
- return { type: "text", text };
2731
- }
2732
- return null;
2733
- }
2734
- return null;
2735
- }
2736
- function parseSystemEvent(parsed) {
2737
- const subtype = parsed.subtype;
2738
- if (subtype === "api_retry") {
2739
- const attempt = parsed.attempt;
2740
- const delay = parsed.retry_delay_ms;
2741
- if (typeof attempt === "number" && typeof delay === "number") {
2742
- return { type: "retry", attempt, delay };
3305
+ `;
3306
+ prompt += `The following stories have exceeded the retry limit and should be skipped:
3307
+ `;
3308
+ for (const story of config.flaggedStories) {
3309
+ prompt += `- \`${story}\`
3310
+ `;
2743
3311
  }
2744
- return null;
2745
- }
2746
- return null;
2747
- }
2748
- function parseResultEvent(parsed) {
2749
- const costUsd = parsed.cost_usd;
2750
- const sessionId = parsed.session_id;
2751
- if (typeof costUsd === "number" && typeof sessionId === "string") {
2752
- return { type: "result", cost: costUsd, sessionId };
2753
3312
  }
2754
- return null;
3313
+ return prompt;
2755
3314
  }
2756
3315
 
2757
3316
  // src/lib/ink-renderer.tsx
2758
3317
  import { render as inkRender } from "ink";
2759
3318
 
2760
3319
  // src/lib/ink-components.tsx
2761
- import { Text, Box, Static } from "ink";
3320
+ import { Text as Text2, Box as Box3 } from "ink";
3321
+
3322
+ // src/lib/ink-activity-components.tsx
3323
+ import { Text, Box } from "ink";
2762
3324
  import { Spinner } from "@inkjs/ui";
2763
3325
  import { jsx, jsxs } from "react/jsx-runtime";
2764
- function Header({ info: info2 }) {
2765
- if (!info2) return null;
2766
- const pct = info2.total > 0 ? Math.round(info2.done / info2.total * 100) : 0;
2767
- return /* @__PURE__ */ jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
2768
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "\u25C6 " }),
2769
- /* @__PURE__ */ jsx(Text, { bold: true, children: info2.storyKey || "(waiting)" }),
2770
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2014 " }),
2771
- /* @__PURE__ */ jsx(Text, { color: "yellow", children: info2.phase || "..." }),
2772
- info2.elapsed && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u2502 ${info2.elapsed}` }),
2773
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 Sprint: " }),
2774
- /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: info2.done }),
2775
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "/" }),
2776
- /* @__PURE__ */ jsx(Text, { children: String(info2.total) }),
2777
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` (${pct}%)` })
2778
- ] });
2779
- }
2780
- function shortKey(key) {
2781
- const m = key.match(/^(\d+-\d+)/);
2782
- return m ? m[1] : key;
2783
- }
2784
- function StoryBreakdown({ stories }) {
2785
- if (stories.length === 0) return null;
2786
- const groups = {};
2787
- for (const s of stories) {
2788
- if (!groups[s.status]) groups[s.status] = [];
2789
- groups[s.status].push(s.key);
2790
- }
2791
- return /* @__PURE__ */ jsxs(Box, { paddingX: 1, gap: 2, children: [
2792
- groups["done"]?.length && /* @__PURE__ */ jsxs(Text, { children: [
2793
- /* @__PURE__ */ jsxs(Text, { color: "green", children: [
2794
- groups["done"].length,
2795
- " \u2713"
2796
- ] }),
2797
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " done" })
2798
- ] }),
2799
- groups["in-progress"]?.map((k) => /* @__PURE__ */ jsxs(Text, { children: [
2800
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u25C6 " }),
2801
- /* @__PURE__ */ jsx(Text, { bold: true, children: k })
2802
- ] }, k)),
2803
- groups["pending"]?.length && /* @__PURE__ */ jsxs(Text, { children: [
2804
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "next: " }),
2805
- /* @__PURE__ */ jsx(Text, { children: groups["pending"].slice(0, 3).map((k) => shortKey(k)).join(" ") }),
2806
- groups["pending"].length > 3 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` +${groups["pending"].length - 3}` })
2807
- ] }),
2808
- groups["failed"]?.map((k) => /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsxs(Text, { color: "red", children: [
2809
- "\u2717 ",
2810
- shortKey(k)
2811
- ] }) }, k)),
2812
- groups["blocked"]?.map((k) => /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
2813
- "\u2715 ",
2814
- shortKey(k)
2815
- ] }) }, k))
2816
- ] });
2817
- }
2818
3326
  var MESSAGE_STYLE = {
2819
3327
  ok: { prefix: "[OK]", color: "green" },
2820
3328
  warn: { prefix: "[WARN]", color: "yellow" },
@@ -2873,24 +3381,159 @@ function RetryNotice({ info: info2 }) {
2873
3381
  "ms)"
2874
3382
  ] });
2875
3383
  }
2876
- function App({
2877
- state
2878
- }) {
2879
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2880
- /* @__PURE__ */ jsx(Static, { items: state.messages, children: (msg, i) => /* @__PURE__ */ jsx(StoryMessageLine, { msg }, i) }),
2881
- /* @__PURE__ */ jsx(Header, { info: state.sprintInfo }),
2882
- /* @__PURE__ */ jsx(StoryBreakdown, { stories: state.stories }),
2883
- /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [
2884
- /* @__PURE__ */ jsx(CompletedTools, { tools: state.completedTools }),
2885
- state.activeTool && /* @__PURE__ */ jsx(ActiveTool, { name: state.activeTool.name }),
2886
- state.lastThought && /* @__PURE__ */ jsx(LastThought, { text: state.lastThought }),
2887
- state.retryInfo && /* @__PURE__ */ jsx(RetryNotice, { info: state.retryInfo })
3384
+
3385
+ // src/lib/ink-app.tsx
3386
+ import { Box as Box2, Static } from "ink";
3387
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
3388
+ function App({ state }) {
3389
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
3390
+ /* @__PURE__ */ jsx2(Static, { items: state.messages, children: (msg, i) => /* @__PURE__ */ jsx2(StoryMessageLine, { msg }, i) }),
3391
+ /* @__PURE__ */ jsx2(Header, { info: state.sprintInfo }),
3392
+ /* @__PURE__ */ jsx2(StoryBreakdown, { stories: state.stories, sprintInfo: state.sprintInfo }),
3393
+ state.stories.length > 0 && /* @__PURE__ */ jsx2(Separator, {}),
3394
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingLeft: 1, children: [
3395
+ /* @__PURE__ */ jsx2(CompletedTools, { tools: state.completedTools }),
3396
+ state.activeTool && /* @__PURE__ */ jsx2(ActiveTool, { name: state.activeTool.name }),
3397
+ state.lastThought && /* @__PURE__ */ jsx2(LastThought, { text: state.lastThought }),
3398
+ state.retryInfo && /* @__PURE__ */ jsx2(RetryNotice, { info: state.retryInfo })
2888
3399
  ] })
2889
3400
  ] });
2890
3401
  }
2891
3402
 
3403
+ // src/lib/ink-components.tsx
3404
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
3405
+ function Separator() {
3406
+ const width = process.stdout.columns || 60;
3407
+ return /* @__PURE__ */ jsx3(Text2, { children: "\u2501".repeat(width) });
3408
+ }
3409
+ function shortKey(key) {
3410
+ const m = key.match(/^(\d+-\d+)/);
3411
+ return m ? m[1] : key;
3412
+ }
3413
+ function formatCost(cost) {
3414
+ return `$${cost.toFixed(2)}`;
3415
+ }
3416
+ function Header({ info: info2 }) {
3417
+ if (!info2) return null;
3418
+ const parts = ["codeharness run"];
3419
+ if (info2.iterationCount != null) {
3420
+ parts.push(`iteration ${info2.iterationCount}`);
3421
+ }
3422
+ if (info2.elapsed) {
3423
+ parts.push(`${info2.elapsed} elapsed`);
3424
+ }
3425
+ if (info2.totalCost != null) {
3426
+ parts.push(`${formatCost(info2.totalCost)} spent`);
3427
+ }
3428
+ const headerLine = parts.join(" | ");
3429
+ let phaseLine = "";
3430
+ if (info2.phase) {
3431
+ phaseLine = `Phase: ${info2.phase}`;
3432
+ if (info2.acProgress) {
3433
+ phaseLine += ` \u2192 AC ${info2.acProgress}`;
3434
+ }
3435
+ if (info2.currentCommand) {
3436
+ phaseLine += ` (${info2.currentCommand})`;
3437
+ }
3438
+ }
3439
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
3440
+ /* @__PURE__ */ jsx3(Text2, { children: headerLine }),
3441
+ /* @__PURE__ */ jsx3(Separator, {}),
3442
+ /* @__PURE__ */ jsx3(Text2, { children: `Story: ${info2.storyKey || "(waiting)"}` }),
3443
+ phaseLine && /* @__PURE__ */ jsx3(Text2, { children: phaseLine })
3444
+ ] });
3445
+ }
3446
+ function StoryBreakdown({ stories, sprintInfo }) {
3447
+ if (stories.length === 0) return null;
3448
+ const done = [];
3449
+ const inProgress = [];
3450
+ const pending = [];
3451
+ const failed = [];
3452
+ const blocked = [];
3453
+ for (const s of stories) {
3454
+ switch (s.status) {
3455
+ case "done":
3456
+ done.push(s);
3457
+ break;
3458
+ case "in-progress":
3459
+ inProgress.push(s);
3460
+ break;
3461
+ case "pending":
3462
+ pending.push(s);
3463
+ break;
3464
+ case "failed":
3465
+ failed.push(s);
3466
+ break;
3467
+ case "blocked":
3468
+ blocked.push(s);
3469
+ break;
3470
+ }
3471
+ }
3472
+ const lines = [];
3473
+ if (done.length > 0) {
3474
+ const doneItems = done.map((s) => `${shortKey(s.key)} \u2713`).join(" ");
3475
+ lines.push(
3476
+ /* @__PURE__ */ jsxs3(Text2, { children: [
3477
+ /* @__PURE__ */ jsx3(Text2, { color: "green", children: "Done: " }),
3478
+ /* @__PURE__ */ jsx3(Text2, { color: "green", children: doneItems })
3479
+ ] }, "done")
3480
+ );
3481
+ }
3482
+ if (inProgress.length > 0) {
3483
+ for (const s of inProgress) {
3484
+ let thisText = `${shortKey(s.key)} \u25C6`;
3485
+ if (sprintInfo && sprintInfo.storyKey && shortKey(s.key) === shortKey(sprintInfo.storyKey)) {
3486
+ if (sprintInfo.phase) thisText += ` ${sprintInfo.phase}`;
3487
+ if (sprintInfo.acProgress) thisText += ` (${sprintInfo.acProgress} ACs)`;
3488
+ }
3489
+ lines.push(
3490
+ /* @__PURE__ */ jsxs3(Text2, { children: [
3491
+ /* @__PURE__ */ jsx3(Text2, { color: "cyan", children: "This: " }),
3492
+ /* @__PURE__ */ jsx3(Text2, { color: "cyan", children: thisText })
3493
+ ] }, `this-${s.key}`)
3494
+ );
3495
+ }
3496
+ }
3497
+ if (pending.length > 0) {
3498
+ lines.push(
3499
+ /* @__PURE__ */ jsxs3(Text2, { children: [
3500
+ /* @__PURE__ */ jsx3(Text2, { children: "Next: " }),
3501
+ /* @__PURE__ */ jsx3(Text2, { children: shortKey(pending[0].key) }),
3502
+ pending.length > 1 && /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: ` (+${pending.length - 1} more)` })
3503
+ ] }, "next")
3504
+ );
3505
+ }
3506
+ if (blocked.length > 0) {
3507
+ const blockedItems = blocked.map((s) => {
3508
+ let item = `${shortKey(s.key)} \u2715`;
3509
+ if (s.retryCount != null && s.maxRetries != null) item += ` (${s.retryCount}/${s.maxRetries})`;
3510
+ return item;
3511
+ }).join(" ");
3512
+ lines.push(
3513
+ /* @__PURE__ */ jsxs3(Text2, { children: [
3514
+ /* @__PURE__ */ jsx3(Text2, { color: "yellow", children: "Blocked: " }),
3515
+ /* @__PURE__ */ jsx3(Text2, { color: "yellow", children: blockedItems })
3516
+ ] }, "blocked")
3517
+ );
3518
+ }
3519
+ if (failed.length > 0) {
3520
+ const failedItems = failed.map((s) => {
3521
+ let item = `${shortKey(s.key)} \u2717`;
3522
+ if (s.retryCount != null && s.maxRetries != null) item += ` (${s.retryCount}/${s.maxRetries})`;
3523
+ return item;
3524
+ }).join(" ");
3525
+ lines.push(
3526
+ /* @__PURE__ */ jsxs3(Text2, { children: [
3527
+ /* @__PURE__ */ jsx3(Text2, { color: "red", children: "Failed: " }),
3528
+ /* @__PURE__ */ jsx3(Text2, { color: "red", children: failedItems })
3529
+ ] }, "failed")
3530
+ );
3531
+ }
3532
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: lines });
3533
+ }
3534
+
2892
3535
  // src/lib/ink-renderer.tsx
2893
- import { jsx as jsx2 } from "react/jsx-runtime";
3536
+ import { jsx as jsx4 } from "react/jsx-runtime";
2894
3537
  var noopHandle = {
2895
3538
  update() {
2896
3539
  },
@@ -2908,6 +3551,7 @@ function startRenderer(options) {
2908
3551
  if (options?.quiet || !process.stdout.isTTY && !options?._forceTTY) {
2909
3552
  return noopHandle;
2910
3553
  }
3554
+ process.stdout.write("\x1B[2J\x1B[H");
2911
3555
  let state = {
2912
3556
  sprintInfo: options?.sprintState ?? null,
2913
3557
  stories: [],
@@ -2919,7 +3563,7 @@ function startRenderer(options) {
2919
3563
  retryInfo: null
2920
3564
  };
2921
3565
  let cleaned = false;
2922
- const inkInstance = inkRender(/* @__PURE__ */ jsx2(App, { state }), {
3566
+ const inkInstance = inkRender(/* @__PURE__ */ jsx4(App, { state }), {
2923
3567
  exitOnCtrlC: false,
2924
3568
  patchConsole: false,
2925
3569
  incrementalRendering: true,
@@ -2930,7 +3574,7 @@ function startRenderer(options) {
2930
3574
  function rerender() {
2931
3575
  if (!cleaned) {
2932
3576
  state = { ...state };
2933
- inkInstance.rerender(/* @__PURE__ */ jsx2(App, { state }));
3577
+ inkInstance.rerender(/* @__PURE__ */ jsx4(App, { state }));
2934
3578
  }
2935
3579
  }
2936
3580
  function cleanup() {
@@ -2989,13 +3633,28 @@ function startRenderer(options) {
2989
3633
  state.retryInfo = { attempt: event.attempt, delay: event.delay };
2990
3634
  break;
2991
3635
  case "result":
3636
+ if (event.cost > 0 && state.sprintInfo) {
3637
+ state.sprintInfo = {
3638
+ ...state.sprintInfo,
3639
+ totalCost: (state.sprintInfo.totalCost ?? 0) + event.cost
3640
+ };
3641
+ }
2992
3642
  break;
2993
3643
  }
2994
3644
  rerender();
2995
3645
  }
2996
3646
  function updateSprintState(sprintState) {
2997
3647
  if (cleaned) return;
2998
- state.sprintInfo = sprintState ?? null;
3648
+ if (sprintState && state.sprintInfo) {
3649
+ state.sprintInfo = {
3650
+ ...sprintState,
3651
+ totalCost: sprintState.totalCost ?? state.sprintInfo.totalCost,
3652
+ acProgress: sprintState.acProgress ?? state.sprintInfo.acProgress,
3653
+ currentCommand: sprintState.currentCommand ?? state.sprintInfo.currentCommand
3654
+ };
3655
+ } else {
3656
+ state.sprintInfo = sprintState ?? null;
3657
+ }
2999
3658
  rerender();
3000
3659
  }
3001
3660
  function updateStories(stories) {
@@ -3012,12 +3671,12 @@ function startRenderer(options) {
3012
3671
  }
3013
3672
 
3014
3673
  // src/modules/sprint/state.ts
3015
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, renameSync, existsSync as existsSync12 } from "fs";
3016
- import { join as join10 } from "path";
3674
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, renameSync, existsSync as existsSync14 } from "fs";
3675
+ import { join as join13 } from "path";
3017
3676
 
3018
3677
  // src/modules/sprint/migration.ts
3019
- import { readFileSync as readFileSync9, existsSync as existsSync11 } from "fs";
3020
- import { join as join9 } from "path";
3678
+ import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
3679
+ import { join as join12 } from "path";
3021
3680
  var OLD_FILES = {
3022
3681
  storyRetries: "ralph/.story_retries",
3023
3682
  flaggedStories: "ralph/.flagged_stories",
@@ -3026,13 +3685,13 @@ var OLD_FILES = {
3026
3685
  sessionIssues: "_bmad-output/implementation-artifacts/.session-issues.md"
3027
3686
  };
3028
3687
  function resolve(relative2) {
3029
- return join9(process.cwd(), relative2);
3688
+ return join12(process.cwd(), relative2);
3030
3689
  }
3031
3690
  function readIfExists(relative2) {
3032
3691
  const p = resolve(relative2);
3033
- if (!existsSync11(p)) return null;
3692
+ if (!existsSync13(p)) return null;
3034
3693
  try {
3035
- return readFileSync9(p, "utf-8");
3694
+ return readFileSync10(p, "utf-8");
3036
3695
  } catch {
3037
3696
  return null;
3038
3697
  }
@@ -3101,7 +3760,11 @@ function parseRalphStatus(content) {
3101
3760
  iteration: data.loop_count ?? 0,
3102
3761
  cost: 0,
3103
3762
  completed: [],
3104
- failed: []
3763
+ failed: [],
3764
+ currentStory: null,
3765
+ currentPhase: null,
3766
+ lastAction: null,
3767
+ acProgress: null
3105
3768
  };
3106
3769
  } catch {
3107
3770
  return null;
@@ -3132,7 +3795,7 @@ function parseSessionIssues(content) {
3132
3795
  return items;
3133
3796
  }
3134
3797
  function migrateFromOldFormat() {
3135
- const hasAnyOldFile = Object.values(OLD_FILES).some((rel) => existsSync11(resolve(rel)));
3798
+ const hasAnyOldFile = Object.values(OLD_FILES).some((rel) => existsSync13(resolve(rel)));
3136
3799
  if (!hasAnyOldFile) return fail2("No old format files found for migration");
3137
3800
  try {
3138
3801
  const stories = {};
@@ -3173,10 +3836,10 @@ function projectRoot() {
3173
3836
  return process.cwd();
3174
3837
  }
3175
3838
  function statePath() {
3176
- return join10(projectRoot(), "sprint-state.json");
3839
+ return join13(projectRoot(), "sprint-state.json");
3177
3840
  }
3178
3841
  function tmpPath() {
3179
- return join10(projectRoot(), ".sprint-state.json.tmp");
3842
+ return join13(projectRoot(), ".sprint-state.json.tmp");
3180
3843
  }
3181
3844
  function defaultState() {
3182
3845
  return {
@@ -3209,7 +3872,7 @@ function writeStateAtomic(state) {
3209
3872
  const data = JSON.stringify(state, null, 2) + "\n";
3210
3873
  const tmp = tmpPath();
3211
3874
  const final = statePath();
3212
- writeFileSync6(tmp, data, "utf-8");
3875
+ writeFileSync7(tmp, data, "utf-8");
3213
3876
  renameSync(tmp, final);
3214
3877
  return ok2(void 0);
3215
3878
  } catch (err) {
@@ -3219,9 +3882,9 @@ function writeStateAtomic(state) {
3219
3882
  }
3220
3883
  function getSprintState() {
3221
3884
  const fp = statePath();
3222
- if (existsSync12(fp)) {
3885
+ if (existsSync14(fp)) {
3223
3886
  try {
3224
- const raw = readFileSync10(fp, "utf-8");
3887
+ const raw = readFileSync11(fp, "utf-8");
3225
3888
  const parsed = JSON.parse(raw);
3226
3889
  const defaults = defaultState();
3227
3890
  const run = parsed.run;
@@ -3518,9 +4181,9 @@ function generateReport(state, now) {
3518
4181
  }
3519
4182
 
3520
4183
  // src/modules/sprint/timeout.ts
3521
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, existsSync as existsSync13, mkdirSync as mkdirSync3, readdirSync as readdirSync2 } from "fs";
4184
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync15, mkdirSync as mkdirSync3, readdirSync as readdirSync3 } from "fs";
3522
4185
  import { execSync } from "child_process";
3523
- import { join as join11 } from "path";
4186
+ import { join as join14 } from "path";
3524
4187
  var GIT_TIMEOUT_MS = 5e3;
3525
4188
  var DEFAULT_MAX_LINES = 100;
3526
4189
  function captureGitDiff() {
@@ -3549,14 +4212,14 @@ function captureGitDiff() {
3549
4212
  }
3550
4213
  function captureStateDelta(beforePath, afterPath) {
3551
4214
  try {
3552
- if (!existsSync13(beforePath)) {
4215
+ if (!existsSync15(beforePath)) {
3553
4216
  return fail2(`State snapshot not found: ${beforePath}`);
3554
4217
  }
3555
- if (!existsSync13(afterPath)) {
4218
+ if (!existsSync15(afterPath)) {
3556
4219
  return fail2(`Current state file not found: ${afterPath}`);
3557
4220
  }
3558
- const beforeRaw = readFileSync11(beforePath, "utf-8");
3559
- const afterRaw = readFileSync11(afterPath, "utf-8");
4221
+ const beforeRaw = readFileSync12(beforePath, "utf-8");
4222
+ const afterRaw = readFileSync12(afterPath, "utf-8");
3560
4223
  const before = JSON.parse(beforeRaw);
3561
4224
  const after = JSON.parse(afterRaw);
3562
4225
  const beforeStories = before.stories ?? {};
@@ -3581,10 +4244,10 @@ function captureStateDelta(beforePath, afterPath) {
3581
4244
  }
3582
4245
  function capturePartialStderr(outputFile, maxLines = DEFAULT_MAX_LINES) {
3583
4246
  try {
3584
- if (!existsSync13(outputFile)) {
4247
+ if (!existsSync15(outputFile)) {
3585
4248
  return fail2(`Output file not found: ${outputFile}`);
3586
4249
  }
3587
- const content = readFileSync11(outputFile, "utf-8");
4250
+ const content = readFileSync12(outputFile, "utf-8");
3588
4251
  const lines = content.split("\n");
3589
4252
  if (lines.length > 0 && lines[lines.length - 1] === "") {
3590
4253
  lines.pop();
@@ -3632,7 +4295,7 @@ function captureTimeoutReport(opts) {
3632
4295
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3633
4296
  const gitResult = captureGitDiff();
3634
4297
  const gitDiff = gitResult.success ? gitResult.data : `(unavailable: ${gitResult.error})`;
3635
- const statePath2 = join11(process.cwd(), "sprint-state.json");
4298
+ const statePath2 = join14(process.cwd(), "sprint-state.json");
3636
4299
  const deltaResult = captureStateDelta(opts.stateSnapshotPath, statePath2);
3637
4300
  const stateDelta = deltaResult.success ? deltaResult.data : `(unavailable: ${deltaResult.error})`;
3638
4301
  const stderrResult = capturePartialStderr(opts.outputFile);
@@ -3646,15 +4309,15 @@ function captureTimeoutReport(opts) {
3646
4309
  partialStderr,
3647
4310
  timestamp
3648
4311
  };
3649
- const reportDir = join11(process.cwd(), "ralph", "logs");
4312
+ const reportDir = join14(process.cwd(), "ralph", "logs");
3650
4313
  const safeStoryKey = opts.storyKey.replace(/[^a-zA-Z0-9._-]/g, "_");
3651
4314
  const reportFileName = `timeout-report-${opts.iteration}-${safeStoryKey}.md`;
3652
- const reportPath = join11(reportDir, reportFileName);
3653
- if (!existsSync13(reportDir)) {
4315
+ const reportPath = join14(reportDir, reportFileName);
4316
+ if (!existsSync15(reportDir)) {
3654
4317
  mkdirSync3(reportDir, { recursive: true });
3655
4318
  }
3656
4319
  const reportContent = formatReport(capture);
3657
- writeFileSync7(reportPath, reportContent, "utf-8");
4320
+ writeFileSync8(reportPath, reportContent, "utf-8");
3658
4321
  return ok2({
3659
4322
  filePath: reportPath,
3660
4323
  capture
@@ -3666,14 +4329,14 @@ function captureTimeoutReport(opts) {
3666
4329
  }
3667
4330
  function findLatestTimeoutReport(storyKey) {
3668
4331
  try {
3669
- const reportDir = join11(process.cwd(), "ralph", "logs");
3670
- if (!existsSync13(reportDir)) {
4332
+ const reportDir = join14(process.cwd(), "ralph", "logs");
4333
+ if (!existsSync15(reportDir)) {
3671
4334
  return ok2(null);
3672
4335
  }
3673
4336
  const safeStoryKey = storyKey.replace(/[^a-zA-Z0-9._-]/g, "_");
3674
4337
  const prefix = `timeout-report-`;
3675
4338
  const suffix = `-${safeStoryKey}.md`;
3676
- const files = readdirSync2(reportDir, { encoding: "utf-8" });
4339
+ const files = readdirSync3(reportDir, { encoding: "utf-8" });
3677
4340
  const matches = [];
3678
4341
  for (const f of files) {
3679
4342
  if (f.startsWith(prefix) && f.endsWith(suffix)) {
@@ -3689,8 +4352,8 @@ function findLatestTimeoutReport(storyKey) {
3689
4352
  }
3690
4353
  matches.sort((a, b) => b.iteration - a.iteration);
3691
4354
  const latest = matches[0];
3692
- const reportPath = join11(reportDir, latest.fileName);
3693
- const content = readFileSync11(reportPath, "utf-8");
4355
+ const reportPath = join14(reportDir, latest.fileName);
4356
+ const content = readFileSync12(reportPath, "utf-8");
3694
4357
  let durationMinutes = 0;
3695
4358
  let filesChanged = 0;
3696
4359
  const durationMatch = content.match(/\*\*Duration:\*\*\s*(\d+)\s*minutes/);
@@ -3719,12 +4382,12 @@ function findLatestTimeoutReport(storyKey) {
3719
4382
  }
3720
4383
 
3721
4384
  // src/modules/sprint/feedback.ts
3722
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
3723
- import { existsSync as existsSync14 } from "fs";
3724
- import { join as join12 } from "path";
4385
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
4386
+ import { existsSync as existsSync16 } from "fs";
4387
+ import { join as join15 } from "path";
3725
4388
 
3726
4389
  // src/modules/sprint/validator.ts
3727
- import { readFileSync as readFileSync13, existsSync as existsSync15 } from "fs";
4390
+ import { readFileSync as readFileSync14, existsSync as existsSync17 } from "fs";
3728
4391
  var VALID_STATUSES = /* @__PURE__ */ new Set([
3729
4392
  "backlog",
3730
4393
  "ready",
@@ -3755,10 +4418,10 @@ function parseSprintStatusKeys(content) {
3755
4418
  }
3756
4419
  function parseStateFile(statePath2) {
3757
4420
  try {
3758
- if (!existsSync15(statePath2)) {
4421
+ if (!existsSync17(statePath2)) {
3759
4422
  return fail2(`State file not found: ${statePath2}`);
3760
4423
  }
3761
- const raw = readFileSync13(statePath2, "utf-8");
4424
+ const raw = readFileSync14(statePath2, "utf-8");
3762
4425
  const parsed = JSON.parse(raw);
3763
4426
  return ok2(parsed);
3764
4427
  } catch (err) {
@@ -3773,10 +4436,10 @@ function validateStateConsistency(statePath2, sprintStatusPath) {
3773
4436
  return fail2(stateResult.error);
3774
4437
  }
3775
4438
  const state = stateResult.data;
3776
- if (!existsSync15(sprintStatusPath)) {
4439
+ if (!existsSync17(sprintStatusPath)) {
3777
4440
  return fail2(`Sprint status file not found: ${sprintStatusPath}`);
3778
4441
  }
3779
- const statusContent = readFileSync13(sprintStatusPath, "utf-8");
4442
+ const statusContent = readFileSync14(sprintStatusPath, "utf-8");
3780
4443
  const keysResult = parseSprintStatusKeys(statusContent);
3781
4444
  if (!keysResult.success) {
3782
4445
  return fail2(keysResult.error);
@@ -3894,6 +4557,109 @@ function clearRunProgress2() {
3894
4557
  return clearRunProgress();
3895
4558
  }
3896
4559
 
4560
+ // src/lib/run-helpers.ts
4561
+ import { StringDecoder } from "string_decoder";
4562
+
4563
+ // src/lib/stream-parser.ts
4564
+ function parseStreamLine(line) {
4565
+ const trimmed = line.trim();
4566
+ if (trimmed.length === 0) {
4567
+ return null;
4568
+ }
4569
+ let parsed;
4570
+ try {
4571
+ parsed = JSON.parse(trimmed);
4572
+ } catch {
4573
+ return null;
4574
+ }
4575
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4576
+ return null;
4577
+ }
4578
+ const wrapperType = parsed.type;
4579
+ if (wrapperType === "stream_event") {
4580
+ return parseStreamEvent(parsed);
4581
+ }
4582
+ if (wrapperType === "system") {
4583
+ return parseSystemEvent(parsed);
4584
+ }
4585
+ if (wrapperType === "result") {
4586
+ return parseResultEvent(parsed);
4587
+ }
4588
+ return null;
4589
+ }
4590
+ function parseStreamEvent(parsed) {
4591
+ const event = parsed.event;
4592
+ if (!event || typeof event !== "object") {
4593
+ return null;
4594
+ }
4595
+ const eventType = event.type;
4596
+ if (eventType === "content_block_start") {
4597
+ return parseContentBlockStart(event);
4598
+ }
4599
+ if (eventType === "content_block_delta") {
4600
+ return parseContentBlockDelta(event);
4601
+ }
4602
+ if (eventType === "content_block_stop") {
4603
+ return { type: "tool-complete" };
4604
+ }
4605
+ return null;
4606
+ }
4607
+ function parseContentBlockStart(event) {
4608
+ const contentBlock = event.content_block;
4609
+ if (!contentBlock || typeof contentBlock !== "object") {
4610
+ return null;
4611
+ }
4612
+ if (contentBlock.type === "tool_use") {
4613
+ const name = contentBlock.name;
4614
+ const id = contentBlock.id;
4615
+ if (typeof name === "string" && typeof id === "string") {
4616
+ return { type: "tool-start", name, id };
4617
+ }
4618
+ }
4619
+ return null;
4620
+ }
4621
+ function parseContentBlockDelta(event) {
4622
+ const delta = event.delta;
4623
+ if (!delta || typeof delta !== "object") {
4624
+ return null;
4625
+ }
4626
+ if (delta.type === "input_json_delta") {
4627
+ const partialJson = delta.partial_json;
4628
+ if (typeof partialJson === "string") {
4629
+ return { type: "tool-input", partial: partialJson };
4630
+ }
4631
+ return null;
4632
+ }
4633
+ if (delta.type === "text_delta") {
4634
+ const text = delta.text;
4635
+ if (typeof text === "string") {
4636
+ return { type: "text", text };
4637
+ }
4638
+ return null;
4639
+ }
4640
+ return null;
4641
+ }
4642
+ function parseSystemEvent(parsed) {
4643
+ const subtype = parsed.subtype;
4644
+ if (subtype === "api_retry") {
4645
+ const attempt = parsed.attempt;
4646
+ const delay = parsed.retry_delay_ms;
4647
+ if (typeof attempt === "number" && typeof delay === "number") {
4648
+ return { type: "retry", attempt, delay };
4649
+ }
4650
+ return null;
4651
+ }
4652
+ return null;
4653
+ }
4654
+ function parseResultEvent(parsed) {
4655
+ const costUsd = parsed.cost_usd;
4656
+ const sessionId = parsed.session_id;
4657
+ if (typeof costUsd === "number" && typeof sessionId === "string") {
4658
+ return { type: "result", cost: costUsd, sessionId };
4659
+ }
4660
+ return null;
4661
+ }
4662
+
3897
4663
  // src/lib/run-helpers.ts
3898
4664
  var STORY_KEY_PATTERN = /^\d+-\d+-/;
3899
4665
  function countStories(statuses) {
@@ -3928,6 +4694,9 @@ function buildSpawnArgs(opts) {
3928
4694
  "--prompt",
3929
4695
  opts.promptFile
3930
4696
  ];
4697
+ if (!opts.quiet) {
4698
+ args.push("--live");
4699
+ }
3931
4700
  if (opts.maxStoryRetries !== void 0) {
3932
4701
  args.push("--max-story-retries", String(opts.maxStoryRetries));
3933
4702
  }
@@ -3979,6 +4748,7 @@ var TIMESTAMP_PREFIX = /^\[[\d-]+\s[\d:]+\]\s*/;
3979
4748
  var SUCCESS_STORY = /\[SUCCESS\]\s+Story\s+([\w-]+):\s+DONE(.*)/;
3980
4749
  var WARN_STORY_RETRY = /\[WARN\]\s+Story\s+([\w-]+)\s+exceeded retry limit/;
3981
4750
  var WARN_STORY_RETRYING = /\[WARN\]\s+Story\s+([\w-]+)\s+.*retry\s+(\d+)\/(\d+)/;
4751
+ var LOOP_ITERATION = /\[LOOP\]\s+iteration\s+(\d+)/;
3982
4752
  var ERROR_LINE = /\[ERROR\]\s+(.+)/;
3983
4753
  function parseRalphMessage(rawLine) {
3984
4754
  const clean = rawLine.replace(ANSI_ESCAPE, "").replace(TIMESTAMP_PREFIX, "").trim();
@@ -4023,6 +4793,41 @@ function parseRalphMessage(rawLine) {
4023
4793
  }
4024
4794
  return null;
4025
4795
  }
4796
+ function parseIterationMessage(rawLine) {
4797
+ const clean = rawLine.replace(ANSI_ESCAPE, "").replace(TIMESTAMP_PREFIX, "").trim();
4798
+ if (clean.length === 0) return null;
4799
+ const match = LOOP_ITERATION.exec(clean);
4800
+ if (match) {
4801
+ return parseInt(match[1], 10);
4802
+ }
4803
+ return null;
4804
+ }
4805
+ function createLineProcessor(callbacks, opts) {
4806
+ let partial = "";
4807
+ const decoder = new StringDecoder("utf8");
4808
+ return (data) => {
4809
+ const text = partial + decoder.write(data);
4810
+ const parts = text.split("\n");
4811
+ partial = parts.pop() ?? "";
4812
+ for (const line of parts) {
4813
+ if (line.trim().length === 0) continue;
4814
+ const event = parseStreamLine(line);
4815
+ if (event) {
4816
+ callbacks.onEvent(event);
4817
+ }
4818
+ if (opts?.parseRalph) {
4819
+ const msg = parseRalphMessage(line);
4820
+ if (msg && callbacks.onMessage) {
4821
+ callbacks.onMessage(msg);
4822
+ }
4823
+ const iteration = parseIterationMessage(line);
4824
+ if (iteration !== null && callbacks.onIteration) {
4825
+ callbacks.onIteration(iteration);
4826
+ }
4827
+ }
4828
+ }
4829
+ };
4830
+ }
4026
4831
 
4027
4832
  // src/commands/run.ts
4028
4833
  var SPRINT_STATUS_REL = "_bmad-output/implementation-artifacts/sprint-status.yaml";
@@ -4033,10 +4838,10 @@ function resolveRalphPath() {
4033
4838
  if (root.endsWith("/src") || root.endsWith("\\src")) {
4034
4839
  root = dirname4(root);
4035
4840
  }
4036
- return join13(root, "ralph", "ralph.sh");
4841
+ return join16(root, "ralph", "ralph.sh");
4037
4842
  }
4038
4843
  function resolvePluginDir() {
4039
- return join13(process.cwd(), ".claude");
4844
+ return join16(process.cwd(), ".claude");
4040
4845
  }
4041
4846
  function registerRunCommand(program) {
4042
4847
  program.command("run").description("Execute the autonomous coding loop").option("--max-iterations <n>", "Maximum loop iterations", "50").option("--timeout <seconds>", "Total loop timeout in seconds", "43200").option("--iteration-timeout <minutes>", "Per-iteration timeout in minutes", "30").option("--quiet", "Suppress terminal output (background mode)", false).option("--calls <n>", "Max API calls per hour", "100").option("--max-story-retries <n>", "Max retries per story before flagging", "10").option("--reset", "Clear retry counters, flagged stories, and circuit breaker before starting", false).action(async (options, cmd) => {
@@ -4044,19 +4849,19 @@ function registerRunCommand(program) {
4044
4849
  const isJson = !!globalOpts.json;
4045
4850
  const outputOpts = { json: isJson };
4046
4851
  const ralphPath = resolveRalphPath();
4047
- if (!existsSync16(ralphPath)) {
4852
+ if (!existsSync18(ralphPath)) {
4048
4853
  fail("Ralph loop not found \u2014 reinstall codeharness", outputOpts);
4049
4854
  process.exitCode = 1;
4050
4855
  return;
4051
4856
  }
4052
4857
  const pluginDir = resolvePluginDir();
4053
- if (!existsSync16(pluginDir)) {
4858
+ if (!existsSync18(pluginDir)) {
4054
4859
  fail("Plugin directory not found \u2014 run codeharness init first", outputOpts);
4055
4860
  process.exitCode = 1;
4056
4861
  return;
4057
4862
  }
4058
4863
  const projectDir = process.cwd();
4059
- const sprintStatusPath = join13(projectDir, SPRINT_STATUS_REL);
4864
+ const sprintStatusPath = join16(projectDir, SPRINT_STATUS_REL);
4060
4865
  const statuses = readSprintStatus(projectDir);
4061
4866
  const counts = countStories(statuses);
4062
4867
  if (counts.total === 0) {
@@ -4075,12 +4880,12 @@ function registerRunCommand(program) {
4075
4880
  process.exitCode = 1;
4076
4881
  return;
4077
4882
  }
4078
- const promptFile = join13(projectDir, "ralph", ".harness-prompt.md");
4079
- const flaggedFilePath = join13(projectDir, "ralph", ".flagged_stories");
4883
+ const promptFile = join16(projectDir, "ralph", ".harness-prompt.md");
4884
+ const flaggedFilePath = join16(projectDir, "ralph", ".flagged_stories");
4080
4885
  let flaggedStories;
4081
- if (existsSync16(flaggedFilePath)) {
4886
+ if (existsSync18(flaggedFilePath)) {
4082
4887
  try {
4083
- const flaggedContent = readFileSync14(flaggedFilePath, "utf-8");
4888
+ const flaggedContent = readFileSync15(flaggedFilePath, "utf-8");
4084
4889
  flaggedStories = flaggedContent.split("\n").filter((s) => s.trim().length > 0);
4085
4890
  } catch {
4086
4891
  }
@@ -4092,7 +4897,7 @@ function registerRunCommand(program) {
4092
4897
  });
4093
4898
  try {
4094
4899
  mkdirSync4(dirname4(promptFile), { recursive: true });
4095
- writeFileSync9(promptFile, promptContent, "utf-8");
4900
+ writeFileSync10(promptFile, promptContent, "utf-8");
4096
4901
  } catch (err) {
4097
4902
  const message = err instanceof Error ? err.message : String(err);
4098
4903
  fail(`Failed to write prompt file: ${message}`, outputOpts);
@@ -4119,6 +4924,7 @@ function registerRunCommand(program) {
4119
4924
  const rendererHandle = startRenderer({ quiet });
4120
4925
  let sprintStateInterval = null;
4121
4926
  const sessionStartTime = Date.now();
4927
+ let currentIterationCount = 0;
4122
4928
  try {
4123
4929
  const initialState = getSprintState2();
4124
4930
  if (initialState.success) {
@@ -4128,7 +4934,8 @@ function registerRunCommand(program) {
4128
4934
  phase: s.run.currentPhase ?? "",
4129
4935
  done: s.sprint.done,
4130
4936
  total: s.sprint.total,
4131
- elapsed: formatElapsed(Date.now() - sessionStartTime)
4937
+ elapsed: formatElapsed(Date.now() - sessionStartTime),
4938
+ iterationCount: currentIterationCount
4132
4939
  };
4133
4940
  rendererHandle.updateSprintState(sprintInfo);
4134
4941
  }
@@ -4143,30 +4950,18 @@ function registerRunCommand(program) {
4143
4950
  env
4144
4951
  });
4145
4952
  if (!quiet && child.stdout && child.stderr) {
4146
- const makeLineHandler = (opts) => {
4147
- let partial = "";
4148
- const decoder = new StringDecoder("utf8");
4149
- return (data) => {
4150
- const text = partial + decoder.write(data);
4151
- const parts = text.split("\n");
4152
- partial = parts.pop() ?? "";
4153
- for (const line of parts) {
4154
- if (line.trim().length === 0) continue;
4155
- const event = parseStreamLine(line);
4156
- if (event) {
4157
- rendererHandle.update(event);
4158
- }
4159
- if (opts?.parseRalph) {
4160
- const msg = parseRalphMessage(line);
4161
- if (msg) {
4162
- rendererHandle.addMessage(msg);
4163
- }
4164
- }
4165
- }
4166
- };
4167
- };
4168
- child.stdout.on("data", makeLineHandler());
4169
- child.stderr.on("data", makeLineHandler({ parseRalph: true }));
4953
+ const stdoutHandler = createLineProcessor({
4954
+ onEvent: (event) => rendererHandle.update(event)
4955
+ });
4956
+ const stderrHandler = createLineProcessor({
4957
+ onEvent: (event) => rendererHandle.update(event),
4958
+ onMessage: (msg) => rendererHandle.addMessage(msg),
4959
+ onIteration: (iteration) => {
4960
+ currentIterationCount = iteration;
4961
+ }
4962
+ }, { parseRalph: true });
4963
+ child.stdout.on("data", stdoutHandler);
4964
+ child.stderr.on("data", stderrHandler);
4170
4965
  sprintStateInterval = setInterval(() => {
4171
4966
  try {
4172
4967
  const stateResult = getSprintState2();
@@ -4177,7 +4972,8 @@ function registerRunCommand(program) {
4177
4972
  phase: s.run.currentPhase ?? "",
4178
4973
  done: s.sprint.done,
4179
4974
  total: s.sprint.total,
4180
- elapsed: formatElapsed(Date.now() - sessionStartTime)
4975
+ elapsed: formatElapsed(Date.now() - sessionStartTime),
4976
+ iterationCount: currentIterationCount
4181
4977
  };
4182
4978
  rendererHandle.updateSprintState(sprintInfo);
4183
4979
  }
@@ -4201,10 +4997,10 @@ function registerRunCommand(program) {
4201
4997
  });
4202
4998
  });
4203
4999
  if (isJson) {
4204
- const statusFile = join13(projectDir, "ralph", "status.json");
4205
- if (existsSync16(statusFile)) {
5000
+ const statusFile = join16(projectDir, "ralph", "status.json");
5001
+ if (existsSync18(statusFile)) {
4206
5002
  try {
4207
- const statusData = JSON.parse(readFileSync14(statusFile, "utf-8"));
5003
+ const statusData = JSON.parse(readFileSync15(statusFile, "utf-8"));
4208
5004
  const finalStatuses = readSprintStatus(projectDir);
4209
5005
  const finalCounts = countStories(finalStatuses);
4210
5006
  jsonOutput({
@@ -4256,14 +5052,14 @@ function registerRunCommand(program) {
4256
5052
  }
4257
5053
 
4258
5054
  // src/commands/verify.ts
4259
- import { existsSync as existsSync25, readFileSync as readFileSync23 } from "fs";
4260
- import { join as join22 } from "path";
5055
+ import { existsSync as existsSync27, readFileSync as readFileSync24 } from "fs";
5056
+ import { join as join25 } from "path";
4261
5057
 
4262
5058
  // src/modules/verify/index.ts
4263
- import { readFileSync as readFileSync22 } from "fs";
5059
+ import { readFileSync as readFileSync23 } from "fs";
4264
5060
 
4265
5061
  // src/modules/verify/proof.ts
4266
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
5062
+ import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
4267
5063
  function classifyEvidenceCommands(proofContent) {
4268
5064
  const results = [];
4269
5065
  const codeBlockPattern = /```(?:bash|shell)\n([\s\S]*?)```/g;
@@ -4349,10 +5145,10 @@ function validateProofQuality(proofPath) {
4349
5145
  otherCount: 0,
4350
5146
  blackBoxPass: false
4351
5147
  };
4352
- if (!existsSync17(proofPath)) {
5148
+ if (!existsSync19(proofPath)) {
4353
5149
  return emptyResult;
4354
5150
  }
4355
- const content = readFileSync15(proofPath, "utf-8");
5151
+ const content = readFileSync16(proofPath, "utf-8");
4356
5152
  const bbTierMatch = /\*\*Tier:\*\*\s*(unit-testable|black-box)/i.exec(content);
4357
5153
  const bbIsUnitTestable = bbTierMatch ? bbTierMatch[1].toLowerCase() === "unit-testable" : false;
4358
5154
  const bbEnforcement = bbIsUnitTestable ? { blackBoxPass: true, grepSrcCount: 0, dockerExecCount: 0, observabilityCount: 0, otherCount: 0, grepRatio: 0, acsMissingDockerExec: [] } : checkBlackBoxEnforcement(content);
@@ -4495,21 +5291,21 @@ function validateProofQuality(proofPath) {
4495
5291
 
4496
5292
  // src/modules/verify/orchestrator.ts
4497
5293
  import { execFileSync as execFileSync7 } from "child_process";
4498
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync11 } from "fs";
4499
- import { join as join15 } from "path";
5294
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync12 } from "fs";
5295
+ import { join as join18 } from "path";
4500
5296
 
4501
5297
  // src/lib/doc-health.ts
4502
5298
  import { execSync as execSync2 } from "child_process";
4503
5299
  import {
4504
- existsSync as existsSync18,
5300
+ existsSync as existsSync20,
4505
5301
  mkdirSync as mkdirSync5,
4506
- readFileSync as readFileSync16,
4507
- readdirSync as readdirSync3,
5302
+ readFileSync as readFileSync17,
5303
+ readdirSync as readdirSync4,
4508
5304
  statSync,
4509
5305
  unlinkSync,
4510
- writeFileSync as writeFileSync10
5306
+ writeFileSync as writeFileSync11
4511
5307
  } from "fs";
4512
- import { join as join14, relative } from "path";
5308
+ import { join as join17, relative } from "path";
4513
5309
  var DO_NOT_EDIT_HEADER2 = "<!-- DO NOT EDIT MANUALLY";
4514
5310
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
4515
5311
  var DEFAULT_MODULE_THRESHOLD = 3;
@@ -4520,7 +5316,7 @@ function findModules(dir, threshold) {
4520
5316
  function walk(current) {
4521
5317
  let entries;
4522
5318
  try {
4523
- entries = readdirSync3(current);
5319
+ entries = readdirSync4(current);
4524
5320
  } catch {
4525
5321
  return;
4526
5322
  }
@@ -4531,7 +5327,7 @@ function findModules(dir, threshold) {
4531
5327
  let sourceCount = 0;
4532
5328
  const subdirs = [];
4533
5329
  for (const entry of entries) {
4534
- const fullPath = join14(current, entry);
5330
+ const fullPath = join17(current, entry);
4535
5331
  let stat;
4536
5332
  try {
4537
5333
  stat = statSync(fullPath);
@@ -4572,14 +5368,14 @@ function getNewestSourceMtime(dir) {
4572
5368
  function walk(current) {
4573
5369
  let entries;
4574
5370
  try {
4575
- entries = readdirSync3(current);
5371
+ entries = readdirSync4(current);
4576
5372
  } catch {
4577
5373
  return;
4578
5374
  }
4579
5375
  const dirName = current.split("/").pop() ?? "";
4580
5376
  if (dirName === "node_modules" || dirName === ".git") return;
4581
5377
  for (const entry of entries) {
4582
- const fullPath = join14(current, entry);
5378
+ const fullPath = join17(current, entry);
4583
5379
  let stat;
4584
5380
  try {
4585
5381
  stat = statSync(fullPath);
@@ -4608,14 +5404,14 @@ function getSourceFilesInModule(modulePath) {
4608
5404
  function walk(current) {
4609
5405
  let entries;
4610
5406
  try {
4611
- entries = readdirSync3(current);
5407
+ entries = readdirSync4(current);
4612
5408
  } catch {
4613
5409
  return;
4614
5410
  }
4615
5411
  const dirName = current.split("/").pop() ?? "";
4616
5412
  if (dirName === "node_modules" || dirName === ".git" || dirName === "__tests__" || dirName === "dist" || dirName === "coverage" || dirName.startsWith(".") && current !== modulePath) return;
4617
5413
  for (const entry of entries) {
4618
- const fullPath = join14(current, entry);
5414
+ const fullPath = join17(current, entry);
4619
5415
  let stat;
4620
5416
  try {
4621
5417
  stat = statSync(fullPath);
@@ -4636,8 +5432,8 @@ function getSourceFilesInModule(modulePath) {
4636
5432
  return files;
4637
5433
  }
4638
5434
  function getMentionedFilesInAgentsMd(agentsPath) {
4639
- if (!existsSync18(agentsPath)) return [];
4640
- const content = readFileSync16(agentsPath, "utf-8");
5435
+ if (!existsSync20(agentsPath)) return [];
5436
+ const content = readFileSync17(agentsPath, "utf-8");
4641
5437
  const mentioned = /* @__PURE__ */ new Set();
4642
5438
  const filenamePattern = /[\w./-]*[\w-]+\.(?:ts|js|py)\b/g;
4643
5439
  let match;
@@ -4661,12 +5457,12 @@ function checkAgentsMdCompleteness(agentsPath, modulePath) {
4661
5457
  }
4662
5458
  function checkAgentsMdForModule(modulePath, dir) {
4663
5459
  const root = dir ?? process.cwd();
4664
- const fullModulePath = join14(root, modulePath);
4665
- let agentsPath = join14(fullModulePath, "AGENTS.md");
4666
- if (!existsSync18(agentsPath)) {
4667
- agentsPath = join14(root, "AGENTS.md");
5460
+ const fullModulePath = join17(root, modulePath);
5461
+ let agentsPath = join17(fullModulePath, "AGENTS.md");
5462
+ if (!existsSync20(agentsPath)) {
5463
+ agentsPath = join17(root, "AGENTS.md");
4668
5464
  }
4669
- if (!existsSync18(agentsPath)) {
5465
+ if (!existsSync20(agentsPath)) {
4670
5466
  return {
4671
5467
  path: relative(root, agentsPath),
4672
5468
  grade: "missing",
@@ -4697,9 +5493,9 @@ function checkAgentsMdForModule(modulePath, dir) {
4697
5493
  };
4698
5494
  }
4699
5495
  function checkDoNotEditHeaders(docPath) {
4700
- if (!existsSync18(docPath)) return false;
5496
+ if (!existsSync20(docPath)) return false;
4701
5497
  try {
4702
- const content = readFileSync16(docPath, "utf-8");
5498
+ const content = readFileSync17(docPath, "utf-8");
4703
5499
  if (content.length === 0) return false;
4704
5500
  return content.trimStart().startsWith(DO_NOT_EDIT_HEADER2);
4705
5501
  } catch {
@@ -4711,17 +5507,17 @@ function scanDocHealth(dir) {
4711
5507
  const root = dir ?? process.cwd();
4712
5508
  const documents = [];
4713
5509
  const modules = findModules(root);
4714
- const rootAgentsPath = join14(root, "AGENTS.md");
4715
- if (existsSync18(rootAgentsPath)) {
5510
+ const rootAgentsPath = join17(root, "AGENTS.md");
5511
+ if (existsSync20(rootAgentsPath)) {
4716
5512
  if (modules.length > 0) {
4717
5513
  const docMtime = statSync(rootAgentsPath).mtime;
4718
5514
  let allMissing = [];
4719
5515
  let staleModule = "";
4720
5516
  let newestCode = null;
4721
5517
  for (const mod of modules) {
4722
- const fullModPath = join14(root, mod);
4723
- const modAgentsPath = join14(fullModPath, "AGENTS.md");
4724
- if (existsSync18(modAgentsPath)) continue;
5518
+ const fullModPath = join17(root, mod);
5519
+ const modAgentsPath = join17(fullModPath, "AGENTS.md");
5520
+ if (existsSync20(modAgentsPath)) continue;
4725
5521
  const { missing } = checkAgentsMdCompleteness(rootAgentsPath, fullModPath);
4726
5522
  if (missing.length > 0 && staleModule === "") {
4727
5523
  staleModule = mod;
@@ -4769,8 +5565,8 @@ function scanDocHealth(dir) {
4769
5565
  });
4770
5566
  }
4771
5567
  for (const mod of modules) {
4772
- const modAgentsPath = join14(root, mod, "AGENTS.md");
4773
- if (existsSync18(modAgentsPath)) {
5568
+ const modAgentsPath = join17(root, mod, "AGENTS.md");
5569
+ if (existsSync20(modAgentsPath)) {
4774
5570
  const result = checkAgentsMdForModule(mod, root);
4775
5571
  if (result.path !== "AGENTS.md") {
4776
5572
  documents.push(result);
@@ -4778,9 +5574,9 @@ function scanDocHealth(dir) {
4778
5574
  }
4779
5575
  }
4780
5576
  }
4781
- const indexPath = join14(root, "docs", "index.md");
4782
- if (existsSync18(indexPath)) {
4783
- const content = readFileSync16(indexPath, "utf-8");
5577
+ const indexPath = join17(root, "docs", "index.md");
5578
+ if (existsSync20(indexPath)) {
5579
+ const content = readFileSync17(indexPath, "utf-8");
4784
5580
  const hasAbsolutePaths = /https?:\/\/|file:\/\//i.test(content);
4785
5581
  documents.push({
4786
5582
  path: "docs/index.md",
@@ -4790,11 +5586,11 @@ function scanDocHealth(dir) {
4790
5586
  reason: hasAbsolutePaths ? "Contains absolute URLs (may violate NFR25)" : "Uses relative paths"
4791
5587
  });
4792
5588
  }
4793
- const activeDir = join14(root, "docs", "exec-plans", "active");
4794
- if (existsSync18(activeDir)) {
4795
- const files = readdirSync3(activeDir).filter((f) => f.endsWith(".md"));
5589
+ const activeDir = join17(root, "docs", "exec-plans", "active");
5590
+ if (existsSync20(activeDir)) {
5591
+ const files = readdirSync4(activeDir).filter((f) => f.endsWith(".md"));
4796
5592
  for (const file of files) {
4797
- const filePath = join14(activeDir, file);
5593
+ const filePath = join17(activeDir, file);
4798
5594
  documents.push({
4799
5595
  path: `docs/exec-plans/active/${file}`,
4800
5596
  grade: "fresh",
@@ -4805,11 +5601,11 @@ function scanDocHealth(dir) {
4805
5601
  }
4806
5602
  }
4807
5603
  for (const subdir of ["quality", "generated"]) {
4808
- const dirPath = join14(root, "docs", subdir);
4809
- if (!existsSync18(dirPath)) continue;
4810
- const files = readdirSync3(dirPath).filter((f) => !f.startsWith("."));
5604
+ const dirPath = join17(root, "docs", subdir);
5605
+ if (!existsSync20(dirPath)) continue;
5606
+ const files = readdirSync4(dirPath).filter((f) => !f.startsWith("."));
4811
5607
  for (const file of files) {
4812
- const filePath = join14(dirPath, file);
5608
+ const filePath = join17(dirPath, file);
4813
5609
  let stat;
4814
5610
  try {
4815
5611
  stat = statSync(filePath);
@@ -4842,7 +5638,7 @@ function scanDocHealth(dir) {
4842
5638
  }
4843
5639
  function checkAgentsMdLineCount(filePath, docPath, documents) {
4844
5640
  try {
4845
- const content = readFileSync16(filePath, "utf-8");
5641
+ const content = readFileSync17(filePath, "utf-8");
4846
5642
  const lineCount = content.split("\n").length;
4847
5643
  if (lineCount > 100) {
4848
5644
  documents.push({
@@ -4880,15 +5676,15 @@ function checkStoryDocFreshness(storyId, dir) {
4880
5676
  for (const mod of modulesToCheck) {
4881
5677
  const result = checkAgentsMdForModule(mod, root);
4882
5678
  documents.push(result);
4883
- const moduleAgentsPath = join14(root, mod, "AGENTS.md");
4884
- const actualAgentsPath = existsSync18(moduleAgentsPath) ? moduleAgentsPath : join14(root, "AGENTS.md");
4885
- if (existsSync18(actualAgentsPath)) {
5679
+ const moduleAgentsPath = join17(root, mod, "AGENTS.md");
5680
+ const actualAgentsPath = existsSync20(moduleAgentsPath) ? moduleAgentsPath : join17(root, "AGENTS.md");
5681
+ if (existsSync20(actualAgentsPath)) {
4886
5682
  checkAgentsMdLineCount(actualAgentsPath, result.path, documents);
4887
5683
  }
4888
5684
  }
4889
5685
  if (modulesToCheck.length === 0) {
4890
- const rootAgentsPath = join14(root, "AGENTS.md");
4891
- if (existsSync18(rootAgentsPath)) {
5686
+ const rootAgentsPath = join17(root, "AGENTS.md");
5687
+ if (existsSync20(rootAgentsPath)) {
4892
5688
  documents.push({
4893
5689
  path: "AGENTS.md",
4894
5690
  grade: "fresh",
@@ -4926,11 +5722,11 @@ function getRecentlyChangedFiles(dir) {
4926
5722
  }
4927
5723
  function completeExecPlan(storyId, dir) {
4928
5724
  const root = dir ?? process.cwd();
4929
- const activePath = join14(root, "docs", "exec-plans", "active", `${storyId}.md`);
4930
- if (!existsSync18(activePath)) {
5725
+ const activePath = join17(root, "docs", "exec-plans", "active", `${storyId}.md`);
5726
+ if (!existsSync20(activePath)) {
4931
5727
  return null;
4932
5728
  }
4933
- let content = readFileSync16(activePath, "utf-8");
5729
+ let content = readFileSync17(activePath, "utf-8");
4934
5730
  content = content.replace(/^Status:\s*active$/m, "Status: completed");
4935
5731
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
4936
5732
  content = content.replace(
@@ -4938,10 +5734,10 @@ function completeExecPlan(storyId, dir) {
4938
5734
  `$1
4939
5735
  Completed: ${timestamp}`
4940
5736
  );
4941
- const completedDir = join14(root, "docs", "exec-plans", "completed");
5737
+ const completedDir = join17(root, "docs", "exec-plans", "completed");
4942
5738
  mkdirSync5(completedDir, { recursive: true });
4943
- const completedPath = join14(completedDir, `${storyId}.md`);
4944
- writeFileSync10(completedPath, content, "utf-8");
5739
+ const completedPath = join17(completedDir, `${storyId}.md`);
5740
+ writeFileSync11(completedPath, content, "utf-8");
4945
5741
  try {
4946
5742
  unlinkSync(activePath);
4947
5743
  } catch {
@@ -5066,8 +5862,8 @@ function checkPreconditions(dir, storyId) {
5066
5862
  }
5067
5863
  function createProofDocument(storyId, storyTitle, acs, dir) {
5068
5864
  const root = dir ?? process.cwd();
5069
- const verificationDir = join15(root, "verification");
5070
- const screenshotsDir = join15(verificationDir, "screenshots");
5865
+ const verificationDir = join18(root, "verification");
5866
+ const screenshotsDir = join18(verificationDir, "screenshots");
5071
5867
  mkdirSync6(verificationDir, { recursive: true });
5072
5868
  mkdirSync6(screenshotsDir, { recursive: true });
5073
5869
  const criteria = acs.map((ac) => ({
@@ -5081,8 +5877,8 @@ function createProofDocument(storyId, storyTitle, acs, dir) {
5081
5877
  storyTitle,
5082
5878
  acceptanceCriteria: criteria
5083
5879
  });
5084
- const proofPath = join15(verificationDir, `${storyId}-proof.md`);
5085
- writeFileSync11(proofPath, content, "utf-8");
5880
+ const proofPath = join18(verificationDir, `${storyId}-proof.md`);
5881
+ writeFileSync12(proofPath, content, "utf-8");
5086
5882
  return proofPath;
5087
5883
  }
5088
5884
  function runShowboatVerify(proofPath) {
@@ -5134,7 +5930,7 @@ function closeBeadsIssue(storyId, dir) {
5134
5930
  }
5135
5931
 
5136
5932
  // src/modules/verify/parser.ts
5137
- import { existsSync as existsSync20, readFileSync as readFileSync17 } from "fs";
5933
+ import { existsSync as existsSync22, readFileSync as readFileSync18 } from "fs";
5138
5934
  var UI_KEYWORDS = [
5139
5935
  "agent-browser",
5140
5936
  "screenshot",
@@ -5204,12 +6000,12 @@ function classifyAC(description) {
5204
6000
  return "general";
5205
6001
  }
5206
6002
  function parseStoryACs(storyFilePath) {
5207
- if (!existsSync20(storyFilePath)) {
6003
+ if (!existsSync22(storyFilePath)) {
5208
6004
  throw new Error(
5209
6005
  `Story file not found: ${storyFilePath}. Ensure the story file exists at the expected path.`
5210
6006
  );
5211
6007
  }
5212
- const content = readFileSync17(storyFilePath, "utf-8");
6008
+ const content = readFileSync18(storyFilePath, "utf-8");
5213
6009
  const lines = content.split("\n");
5214
6010
  let acSectionStart = -1;
5215
6011
  for (let i = 0; i < lines.length; i++) {
@@ -5301,7 +6097,7 @@ function parseObservabilityGaps(proofContent) {
5301
6097
 
5302
6098
  // src/modules/observability/analyzer.ts
5303
6099
  import { execFileSync as execFileSync8 } from "child_process";
5304
- import { join as join16 } from "path";
6100
+ import { join as join19 } from "path";
5305
6101
  var DEFAULT_RULES_DIR = "patches/observability/";
5306
6102
  var DEFAULT_TIMEOUT = 6e4;
5307
6103
  var FUNCTION_NO_LOG_RULE = "function-no-debug-log";
@@ -5335,7 +6131,7 @@ function analyze(projectDir, config) {
5335
6131
  }
5336
6132
  const rulesDir = config?.rulesDir ?? DEFAULT_RULES_DIR;
5337
6133
  const timeout = config?.timeout ?? DEFAULT_TIMEOUT;
5338
- const fullRulesDir = join16(projectDir, rulesDir);
6134
+ const fullRulesDir = join19(projectDir, rulesDir);
5339
6135
  const rawResult = runSemgrep(projectDir, fullRulesDir, timeout);
5340
6136
  if (!rawResult.success) {
5341
6137
  return fail2(rawResult.error);
@@ -5419,8 +6215,8 @@ function normalizeSeverity(severity) {
5419
6215
  }
5420
6216
 
5421
6217
  // src/modules/observability/coverage.ts
5422
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, renameSync as renameSync2, existsSync as existsSync21 } from "fs";
5423
- import { join as join17 } from "path";
6218
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync13, renameSync as renameSync2, existsSync as existsSync23 } from "fs";
6219
+ import { join as join20 } from "path";
5424
6220
  var STATE_FILE2 = "sprint-state.json";
5425
6221
  var DEFAULT_STATIC_TARGET = 80;
5426
6222
  function defaultCoverageState() {
@@ -5436,12 +6232,12 @@ function defaultCoverageState() {
5436
6232
  };
5437
6233
  }
5438
6234
  function readStateFile(projectDir) {
5439
- const fp = join17(projectDir, STATE_FILE2);
5440
- if (!existsSync21(fp)) {
6235
+ const fp = join20(projectDir, STATE_FILE2);
6236
+ if (!existsSync23(fp)) {
5441
6237
  return ok2({});
5442
6238
  }
5443
6239
  try {
5444
- const raw = readFileSync18(fp, "utf-8");
6240
+ const raw = readFileSync19(fp, "utf-8");
5445
6241
  const parsed = JSON.parse(raw);
5446
6242
  return ok2(parsed);
5447
6243
  } catch (err) {
@@ -5508,8 +6304,8 @@ function parseGapArray(raw) {
5508
6304
  }
5509
6305
 
5510
6306
  // src/modules/observability/runtime-coverage.ts
5511
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync13, renameSync as renameSync3, existsSync as existsSync22 } from "fs";
5512
- import { join as join18 } from "path";
6307
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync14, renameSync as renameSync3, existsSync as existsSync24 } from "fs";
6308
+ import { join as join21 } from "path";
5513
6309
 
5514
6310
  // src/modules/observability/coverage-gate.ts
5515
6311
  var DEFAULT_STATIC_TARGET2 = 80;
@@ -5551,8 +6347,8 @@ function checkObservabilityCoverageGate(projectDir, overrides) {
5551
6347
 
5552
6348
  // src/modules/observability/runtime-validator.ts
5553
6349
  import { execSync as execSync3 } from "child_process";
5554
- import { readdirSync as readdirSync4, statSync as statSync2 } from "fs";
5555
- import { join as join19 } from "path";
6350
+ import { readdirSync as readdirSync5, statSync as statSync2 } from "fs";
6351
+ import { join as join22 } from "path";
5556
6352
  var DEFAULT_CONFIG = {
5557
6353
  testCommand: "npm test",
5558
6354
  otlpEndpoint: "http://localhost:4318",
@@ -5679,11 +6475,11 @@ function mapEventsToModules(events, projectDir, modules) {
5679
6475
  });
5680
6476
  }
5681
6477
  function discoverModules(projectDir) {
5682
- const srcDir = join19(projectDir, "src");
6478
+ const srcDir = join22(projectDir, "src");
5683
6479
  try {
5684
- return readdirSync4(srcDir).filter((name) => {
6480
+ return readdirSync5(srcDir).filter((name) => {
5685
6481
  try {
5686
- return statSync2(join19(srcDir, name)).isDirectory();
6482
+ return statSync2(join22(srcDir, name)).isDirectory();
5687
6483
  } catch {
5688
6484
  return false;
5689
6485
  }
@@ -5712,7 +6508,7 @@ function parseLogEvents(text) {
5712
6508
 
5713
6509
  // src/modules/verify/browser.ts
5714
6510
  import { execFileSync as execFileSync9 } from "child_process";
5715
- import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
6511
+ import { existsSync as existsSync25, readFileSync as readFileSync21 } from "fs";
5716
6512
 
5717
6513
  // src/modules/verify/validation-ac-fr.ts
5718
6514
  var FR_ACS = [
@@ -6350,8 +7146,8 @@ function getACById(id) {
6350
7146
 
6351
7147
  // src/modules/verify/validation-runner.ts
6352
7148
  import { execSync as execSync4 } from "child_process";
6353
- import { writeFileSync as writeFileSync14, mkdirSync as mkdirSync7 } from "fs";
6354
- import { join as join20, dirname as dirname5 } from "path";
7149
+ import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync7 } from "fs";
7150
+ import { join as join23, dirname as dirname5 } from "path";
6355
7151
  var MAX_VALIDATION_ATTEMPTS = 10;
6356
7152
  var AC_COMMAND_TIMEOUT_MS = 3e4;
6357
7153
  var VAL_KEY_PREFIX = "val-";
@@ -6460,7 +7256,7 @@ function executeValidationAC(ac) {
6460
7256
  function createFixStory(ac, error) {
6461
7257
  try {
6462
7258
  const storyKey = `val-fix-${ac.id}`;
6463
- const storyPath = join20(
7259
+ const storyPath = join23(
6464
7260
  process.cwd(),
6465
7261
  "_bmad-output",
6466
7262
  "implementation-artifacts",
@@ -6504,7 +7300,7 @@ function createFixStory(ac, error) {
6504
7300
  ""
6505
7301
  ].join("\n");
6506
7302
  mkdirSync7(dirname5(storyPath), { recursive: true });
6507
- writeFileSync14(storyPath, markdown, "utf-8");
7303
+ writeFileSync15(storyPath, markdown, "utf-8");
6508
7304
  return ok2(storyKey);
6509
7305
  } catch (err) {
6510
7306
  const msg = err instanceof Error ? err.message : String(err);
@@ -6830,8 +7626,8 @@ function runValidationCycle() {
6830
7626
 
6831
7627
  // src/modules/verify/env.ts
6832
7628
  import { execFileSync as execFileSync11 } from "child_process";
6833
- import { existsSync as existsSync24, mkdirSync as mkdirSync8, readdirSync as readdirSync5, readFileSync as readFileSync21, cpSync, rmSync, statSync as statSync3 } from "fs";
6834
- import { join as join21, basename as basename4 } from "path";
7629
+ import { existsSync as existsSync26, mkdirSync as mkdirSync8, readdirSync as readdirSync6, readFileSync as readFileSync22, cpSync, rmSync, statSync as statSync3 } from "fs";
7630
+ import { join as join24, basename as basename4 } from "path";
6835
7631
  import { createHash } from "crypto";
6836
7632
  var IMAGE_TAG = "codeharness-verify";
6837
7633
  var STORY_DIR = "_bmad-output/implementation-artifacts";
@@ -6844,20 +7640,20 @@ function isValidStoryKey(storyKey) {
6844
7640
  return /^[a-zA-Z0-9_-]+$/.test(storyKey);
6845
7641
  }
6846
7642
  function computeDistHash(projectDir) {
6847
- const distDir = join21(projectDir, "dist");
6848
- if (!existsSync24(distDir)) return null;
7643
+ const distDir = join24(projectDir, "dist");
7644
+ if (!existsSync26(distDir)) return null;
6849
7645
  const hash = createHash("sha256");
6850
7646
  const files = collectFiles(distDir).sort();
6851
7647
  for (const file of files) {
6852
7648
  hash.update(file.slice(distDir.length));
6853
- hash.update(readFileSync21(file));
7649
+ hash.update(readFileSync22(file));
6854
7650
  }
6855
7651
  return hash.digest("hex");
6856
7652
  }
6857
7653
  function collectFiles(dir) {
6858
7654
  const results = [];
6859
- for (const entry of readdirSync5(dir, { withFileTypes: true })) {
6860
- const fullPath = join21(dir, entry.name);
7655
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
7656
+ const fullPath = join24(dir, entry.name);
6861
7657
  if (entry.isDirectory()) {
6862
7658
  results.push(...collectFiles(fullPath));
6863
7659
  } else {
@@ -6884,10 +7680,13 @@ function storeDistHash(projectDir, hash) {
6884
7680
  }
6885
7681
  }
6886
7682
  function detectProjectType(projectDir) {
6887
- const stack = detectStack(projectDir);
7683
+ const allStacks = detectStacks(projectDir);
7684
+ const rootDetection = allStacks.find((s) => s.dir === ".");
7685
+ const stack = rootDetection ? rootDetection.stack : null;
6888
7686
  if (stack === "nodejs") return "nodejs";
6889
7687
  if (stack === "python") return "python";
6890
- if (existsSync24(join21(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
7688
+ if (stack === "rust") return "rust";
7689
+ if (existsSync26(join24(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
6891
7690
  return "generic";
6892
7691
  }
6893
7692
  function buildVerifyImage(options = {}) {
@@ -6897,7 +7696,7 @@ function buildVerifyImage(options = {}) {
6897
7696
  }
6898
7697
  const projectType = detectProjectType(projectDir);
6899
7698
  const currentHash = computeDistHash(projectDir);
6900
- if (projectType === "generic" || projectType === "plugin") {
7699
+ if (projectType === "generic" || projectType === "plugin" || projectType === "rust") {
6901
7700
  } else if (!currentHash) {
6902
7701
  throw new Error("No dist/ directory found. Run your build command first (e.g., npm run build).");
6903
7702
  }
@@ -6912,10 +7711,12 @@ function buildVerifyImage(options = {}) {
6912
7711
  buildNodeImage(projectDir);
6913
7712
  } else if (projectType === "python") {
6914
7713
  buildPythonImage(projectDir);
7714
+ } else if (projectType === "rust") {
7715
+ buildSimpleImage(projectDir, "rust", 3e5);
6915
7716
  } else if (projectType === "plugin") {
6916
7717
  buildPluginImage(projectDir);
6917
7718
  } else {
6918
- buildGenericImage(projectDir);
7719
+ buildSimpleImage(projectDir, "generic");
6919
7720
  }
6920
7721
  if (currentHash) {
6921
7722
  storeDistHash(projectDir, currentHash);
@@ -6931,12 +7732,12 @@ function buildNodeImage(projectDir) {
6931
7732
  const lastLine = packOutput.split("\n").pop()?.trim();
6932
7733
  if (!lastLine) throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
6933
7734
  const tarballName = basename4(lastLine);
6934
- const tarballPath = join21("/tmp", tarballName);
6935
- const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
7735
+ const tarballPath = join24("/tmp", tarballName);
7736
+ const buildContext = join24("/tmp", `codeharness-verify-build-${Date.now()}`);
6936
7737
  mkdirSync8(buildContext, { recursive: true });
6937
7738
  try {
6938
- cpSync(tarballPath, join21(buildContext, tarballName));
6939
- cpSync(resolveDockerfileTemplate(projectDir), join21(buildContext, "Dockerfile"));
7739
+ cpSync(tarballPath, join24(buildContext, tarballName));
7740
+ cpSync(resolveDockerfileTemplate(projectDir), join24(buildContext, "Dockerfile"));
6940
7741
  execFileSync11("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${tarballName}`, "."], {
6941
7742
  cwd: buildContext,
6942
7743
  stdio: "pipe",
@@ -6948,17 +7749,17 @@ function buildNodeImage(projectDir) {
6948
7749
  }
6949
7750
  }
6950
7751
  function buildPythonImage(projectDir) {
6951
- const distDir = join21(projectDir, "dist");
6952
- const distFiles = readdirSync5(distDir).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".whl"));
7752
+ const distDir = join24(projectDir, "dist");
7753
+ const distFiles = readdirSync6(distDir).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".whl"));
6953
7754
  if (distFiles.length === 0) {
6954
7755
  throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
6955
7756
  }
6956
7757
  const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
6957
- const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
7758
+ const buildContext = join24("/tmp", `codeharness-verify-build-${Date.now()}`);
6958
7759
  mkdirSync8(buildContext, { recursive: true });
6959
7760
  try {
6960
- cpSync(join21(distDir, distFile), join21(buildContext, distFile));
6961
- cpSync(resolveDockerfileTemplate(projectDir), join21(buildContext, "Dockerfile"));
7761
+ cpSync(join24(distDir, distFile), join24(buildContext, distFile));
7762
+ cpSync(resolveDockerfileTemplate(projectDir), join24(buildContext, "Dockerfile"));
6962
7763
  execFileSync11("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${distFile}`, "."], {
6963
7764
  cwd: buildContext,
6964
7765
  stdio: "pipe",
@@ -6973,19 +7774,19 @@ function prepareVerifyWorkspace(storyKey, projectDir) {
6973
7774
  if (!isValidStoryKey(storyKey)) {
6974
7775
  throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
6975
7776
  }
6976
- const storyFile = join21(root, STORY_DIR, `${storyKey}.md`);
6977
- if (!existsSync24(storyFile)) throw new Error(`Story file not found: ${storyFile}`);
7777
+ const storyFile = join24(root, STORY_DIR, `${storyKey}.md`);
7778
+ if (!existsSync26(storyFile)) throw new Error(`Story file not found: ${storyFile}`);
6978
7779
  const workspace = `${TEMP_PREFIX}${storyKey}`;
6979
- if (existsSync24(workspace)) rmSync(workspace, { recursive: true, force: true });
7780
+ if (existsSync26(workspace)) rmSync(workspace, { recursive: true, force: true });
6980
7781
  mkdirSync8(workspace, { recursive: true });
6981
- cpSync(storyFile, join21(workspace, "story.md"));
6982
- const readmePath = join21(root, "README.md");
6983
- if (existsSync24(readmePath)) cpSync(readmePath, join21(workspace, "README.md"));
6984
- const docsDir = join21(root, "docs");
6985
- if (existsSync24(docsDir) && statSync3(docsDir).isDirectory()) {
6986
- cpSync(docsDir, join21(workspace, "docs"), { recursive: true });
6987
- }
6988
- mkdirSync8(join21(workspace, "verification"), { recursive: true });
7782
+ cpSync(storyFile, join24(workspace, "story.md"));
7783
+ const readmePath = join24(root, "README.md");
7784
+ if (existsSync26(readmePath)) cpSync(readmePath, join24(workspace, "README.md"));
7785
+ const docsDir = join24(root, "docs");
7786
+ if (existsSync26(docsDir) && statSync3(docsDir).isDirectory()) {
7787
+ cpSync(docsDir, join24(workspace, "docs"), { recursive: true });
7788
+ }
7789
+ mkdirSync8(join24(workspace, "verification"), { recursive: true });
6989
7790
  return workspace;
6990
7791
  }
6991
7792
  function checkVerifyEnv() {
@@ -7027,7 +7828,7 @@ function cleanupVerifyEnv(storyKey) {
7027
7828
  }
7028
7829
  const workspace = `${TEMP_PREFIX}${storyKey}`;
7029
7830
  const containerName = `codeharness-verify-${storyKey}`;
7030
- if (existsSync24(workspace)) rmSync(workspace, { recursive: true, force: true });
7831
+ if (existsSync26(workspace)) rmSync(workspace, { recursive: true, force: true });
7031
7832
  try {
7032
7833
  execFileSync11("docker", ["stop", containerName], { stdio: "pipe", timeout: 15e3 });
7033
7834
  } catch {
@@ -7038,18 +7839,18 @@ function cleanupVerifyEnv(storyKey) {
7038
7839
  }
7039
7840
  }
7040
7841
  function buildPluginImage(projectDir) {
7041
- const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
7842
+ const buildContext = join24("/tmp", `codeharness-verify-build-${Date.now()}`);
7042
7843
  mkdirSync8(buildContext, { recursive: true });
7043
7844
  try {
7044
- const pluginDir = join21(projectDir, ".claude-plugin");
7045
- cpSync(pluginDir, join21(buildContext, ".claude-plugin"), { recursive: true });
7845
+ const pluginDir = join24(projectDir, ".claude-plugin");
7846
+ cpSync(pluginDir, join24(buildContext, ".claude-plugin"), { recursive: true });
7046
7847
  for (const dir of ["commands", "hooks", "knowledge", "skills"]) {
7047
- const src = join21(projectDir, dir);
7048
- if (existsSync24(src) && statSync3(src).isDirectory()) {
7049
- cpSync(src, join21(buildContext, dir), { recursive: true });
7848
+ const src = join24(projectDir, dir);
7849
+ if (existsSync26(src) && statSync3(src).isDirectory()) {
7850
+ cpSync(src, join24(buildContext, dir), { recursive: true });
7050
7851
  }
7051
7852
  }
7052
- cpSync(resolveDockerfileTemplate(projectDir, "generic"), join21(buildContext, "Dockerfile"));
7853
+ cpSync(resolveDockerfileTemplate(projectDir, "generic"), join24(buildContext, "Dockerfile"));
7053
7854
  execFileSync11("docker", ["build", "-t", IMAGE_TAG, "."], {
7054
7855
  cwd: buildContext,
7055
7856
  stdio: "pipe",
@@ -7059,27 +7860,31 @@ function buildPluginImage(projectDir) {
7059
7860
  rmSync(buildContext, { recursive: true, force: true });
7060
7861
  }
7061
7862
  }
7062
- function buildGenericImage(projectDir) {
7063
- const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
7863
+ function buildSimpleImage(projectDir, variant, timeout = 12e4) {
7864
+ const buildContext = join24("/tmp", `codeharness-verify-build-${Date.now()}`);
7064
7865
  mkdirSync8(buildContext, { recursive: true });
7065
7866
  try {
7066
- cpSync(resolveDockerfileTemplate(projectDir, "generic"), join21(buildContext, "Dockerfile"));
7867
+ cpSync(resolveDockerfileTemplate(projectDir, variant), join24(buildContext, "Dockerfile"));
7067
7868
  execFileSync11("docker", ["build", "-t", IMAGE_TAG, "."], {
7068
7869
  cwd: buildContext,
7069
7870
  stdio: "pipe",
7070
- timeout: 12e4
7871
+ timeout
7071
7872
  });
7072
7873
  } finally {
7073
7874
  rmSync(buildContext, { recursive: true, force: true });
7074
7875
  }
7075
7876
  }
7877
+ var DOCKERFILE_VARIANTS = {
7878
+ generic: "Dockerfile.verify.generic",
7879
+ rust: "Dockerfile.verify.rust"
7880
+ };
7076
7881
  function resolveDockerfileTemplate(projectDir, variant) {
7077
- const filename = variant === "generic" ? "Dockerfile.verify.generic" : "Dockerfile.verify";
7078
- const local = join21(projectDir, "templates", filename);
7079
- if (existsSync24(local)) return local;
7882
+ const filename = (variant && DOCKERFILE_VARIANTS[variant]) ?? "Dockerfile.verify";
7883
+ const local = join24(projectDir, "templates", filename);
7884
+ if (existsSync26(local)) return local;
7080
7885
  const pkgDir = new URL("../../", import.meta.url).pathname;
7081
- const pkg = join21(pkgDir, "templates", filename);
7082
- if (existsSync24(pkg)) return pkg;
7886
+ const pkg = join24(pkgDir, "templates", filename);
7887
+ if (existsSync26(pkg)) return pkg;
7083
7888
  throw new Error(`${filename} not found. Ensure templates/${filename} exists.`);
7084
7889
  }
7085
7890
  function dockerImageExists(tag) {
@@ -7154,8 +7959,8 @@ function verifyRetro(opts, isJson, root) {
7154
7959
  return;
7155
7960
  }
7156
7961
  const retroFile = `epic-${epicNum}-retrospective.md`;
7157
- const retroPath = join22(root, STORY_DIR2, retroFile);
7158
- if (!existsSync25(retroPath)) {
7962
+ const retroPath = join25(root, STORY_DIR2, retroFile);
7963
+ if (!existsSync27(retroPath)) {
7159
7964
  if (isJson) {
7160
7965
  jsonOutput({ status: "fail", epic: epicNum, retroFile, message: `${retroFile} not found` });
7161
7966
  } else {
@@ -7172,7 +7977,7 @@ function verifyRetro(opts, isJson, root) {
7172
7977
  warn(`Failed to update sprint status: ${message}`);
7173
7978
  }
7174
7979
  if (isJson) {
7175
- jsonOutput({ status: "ok", epic: epicNum, retroFile: join22(STORY_DIR2, retroFile) });
7980
+ jsonOutput({ status: "ok", epic: epicNum, retroFile: join25(STORY_DIR2, retroFile) });
7176
7981
  } else {
7177
7982
  ok(`Epic ${epicNum} retrospective: marked done`);
7178
7983
  }
@@ -7183,8 +7988,8 @@ function verifyStory(storyId, isJson, root) {
7183
7988
  process.exitCode = 1;
7184
7989
  return;
7185
7990
  }
7186
- const readmePath = join22(root, "README.md");
7187
- if (!existsSync25(readmePath)) {
7991
+ const readmePath = join25(root, "README.md");
7992
+ if (!existsSync27(readmePath)) {
7188
7993
  if (isJson) {
7189
7994
  jsonOutput({ status: "fail", message: "No README.md found \u2014 verification requires user documentation" });
7190
7995
  } else {
@@ -7193,8 +7998,8 @@ function verifyStory(storyId, isJson, root) {
7193
7998
  process.exitCode = 1;
7194
7999
  return;
7195
8000
  }
7196
- const storyFilePath = join22(root, STORY_DIR2, `${storyId}.md`);
7197
- if (!existsSync25(storyFilePath)) {
8001
+ const storyFilePath = join25(root, STORY_DIR2, `${storyId}.md`);
8002
+ if (!existsSync27(storyFilePath)) {
7198
8003
  fail(`Story file not found: ${storyFilePath}`, { json: isJson });
7199
8004
  process.exitCode = 1;
7200
8005
  return;
@@ -7234,8 +8039,8 @@ function verifyStory(storyId, isJson, root) {
7234
8039
  return;
7235
8040
  }
7236
8041
  const storyTitle = extractStoryTitle(storyFilePath);
7237
- const expectedProofPath = join22(root, "verification", `${storyId}-proof.md`);
7238
- const proofPath = existsSync25(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
8042
+ const expectedProofPath = join25(root, "verification", `${storyId}-proof.md`);
8043
+ const proofPath = existsSync27(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
7239
8044
  const proofQuality = validateProofQuality(proofPath);
7240
8045
  if (!proofQuality.passed) {
7241
8046
  if (isJson) {
@@ -7278,7 +8083,7 @@ function verifyStory(storyId, isJson, root) {
7278
8083
  let observabilityGapCount = 0;
7279
8084
  let runtimeCoveragePercent = 0;
7280
8085
  try {
7281
- const proofContent = readFileSync23(proofPath, "utf-8");
8086
+ const proofContent = readFileSync24(proofPath, "utf-8");
7282
8087
  const gapResult = parseObservabilityGaps(proofContent);
7283
8088
  observabilityGapCount = gapResult.gapCount;
7284
8089
  runtimeCoveragePercent = gapResult.totalACs === 0 ? 0 : gapResult.coveredCount / gapResult.totalACs * 100;
@@ -7340,7 +8145,7 @@ function verifyStory(storyId, isJson, root) {
7340
8145
  }
7341
8146
  function extractStoryTitle(filePath) {
7342
8147
  try {
7343
- const content = readFileSync23(filePath, "utf-8");
8148
+ const content = readFileSync24(filePath, "utf-8");
7344
8149
  const match = /^#\s+(.+)$/m.exec(content);
7345
8150
  return match ? match[1] : "Unknown Story";
7346
8151
  } catch {
@@ -7349,14 +8154,14 @@ function extractStoryTitle(filePath) {
7349
8154
  }
7350
8155
 
7351
8156
  // src/lib/onboard-checks.ts
7352
- import { existsSync as existsSync27 } from "fs";
7353
- import { join as join24, dirname as dirname6 } from "path";
8157
+ import { existsSync as existsSync29 } from "fs";
8158
+ import { join as join27, dirname as dirname6 } from "path";
7354
8159
  import { fileURLToPath as fileURLToPath3 } from "url";
7355
8160
 
7356
8161
  // src/lib/coverage.ts
7357
8162
  import { execSync as execSync6 } from "child_process";
7358
- import { existsSync as existsSync26, readFileSync as readFileSync24 } from "fs";
7359
- import { join as join23 } from "path";
8163
+ import { existsSync as existsSync28, readFileSync as readFileSync25 } from "fs";
8164
+ import { join as join26 } from "path";
7360
8165
  function detectCoverageTool(dir) {
7361
8166
  const baseDir = dir ?? process.cwd();
7362
8167
  const stateHint = getStateToolHint(baseDir);
@@ -7367,6 +8172,27 @@ function detectCoverageTool(dir) {
7367
8172
  if (stack === "python") {
7368
8173
  return detectPythonCoverageTool(baseDir);
7369
8174
  }
8175
+ if (stack === "rust") {
8176
+ try {
8177
+ execSync6("cargo tarpaulin --version", { stdio: "pipe", timeout: 1e4 });
8178
+ } catch {
8179
+ warn("cargo-tarpaulin not installed \u2014 coverage detection unavailable");
8180
+ return { tool: "unknown", runCommand: "", reportFormat: "" };
8181
+ }
8182
+ const cargoPath = join26(baseDir, "Cargo.toml");
8183
+ let isWorkspace = false;
8184
+ try {
8185
+ const cargoContent = readFileSync25(cargoPath, "utf-8");
8186
+ isWorkspace = /^\[workspace\]/m.test(cargoContent);
8187
+ } catch {
8188
+ }
8189
+ const wsFlag = isWorkspace ? " --workspace" : "";
8190
+ return {
8191
+ tool: "cargo-tarpaulin",
8192
+ runCommand: `cargo tarpaulin --out json --output-dir coverage/${wsFlag}`,
8193
+ reportFormat: "tarpaulin-json"
8194
+ };
8195
+ }
7370
8196
  warn("No recognized stack detected \u2014 cannot determine coverage tool");
7371
8197
  return { tool: "unknown", runCommand: "", reportFormat: "" };
7372
8198
  }
@@ -7379,16 +8205,16 @@ function getStateToolHint(dir) {
7379
8205
  }
7380
8206
  }
7381
8207
  function detectNodeCoverageTool(dir, stateHint) {
7382
- const hasVitestConfig = existsSync26(join23(dir, "vitest.config.ts")) || existsSync26(join23(dir, "vitest.config.js"));
7383
- const pkgPath = join23(dir, "package.json");
8208
+ const hasVitestConfig = existsSync28(join26(dir, "vitest.config.ts")) || existsSync28(join26(dir, "vitest.config.js"));
8209
+ const pkgPath = join26(dir, "package.json");
7384
8210
  let hasVitestCoverageV8 = false;
7385
8211
  let hasVitestCoverageIstanbul = false;
7386
8212
  let hasC8 = false;
7387
8213
  let hasJest = false;
7388
8214
  let pkgScripts = {};
7389
- if (existsSync26(pkgPath)) {
8215
+ if (existsSync28(pkgPath)) {
7390
8216
  try {
7391
- const pkg = JSON.parse(readFileSync24(pkgPath, "utf-8"));
8217
+ const pkg = JSON.parse(readFileSync25(pkgPath, "utf-8"));
7392
8218
  const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
7393
8219
  hasVitestCoverageV8 = "@vitest/coverage-v8" in allDeps;
7394
8220
  hasVitestCoverageIstanbul = "@vitest/coverage-istanbul" in allDeps;
@@ -7441,10 +8267,10 @@ function getNodeTestCommand(scripts, runner) {
7441
8267
  return "npm test";
7442
8268
  }
7443
8269
  function detectPythonCoverageTool(dir) {
7444
- const reqPath = join23(dir, "requirements.txt");
7445
- if (existsSync26(reqPath)) {
8270
+ const reqPath = join26(dir, "requirements.txt");
8271
+ if (existsSync28(reqPath)) {
7446
8272
  try {
7447
- const content = readFileSync24(reqPath, "utf-8");
8273
+ const content = readFileSync25(reqPath, "utf-8");
7448
8274
  if (content.includes("pytest-cov") || content.includes("coverage")) {
7449
8275
  return {
7450
8276
  tool: "coverage.py",
@@ -7455,10 +8281,10 @@ function detectPythonCoverageTool(dir) {
7455
8281
  } catch {
7456
8282
  }
7457
8283
  }
7458
- const pyprojectPath = join23(dir, "pyproject.toml");
7459
- if (existsSync26(pyprojectPath)) {
8284
+ const pyprojectPath = join26(dir, "pyproject.toml");
8285
+ if (existsSync28(pyprojectPath)) {
7460
8286
  try {
7461
- const content = readFileSync24(pyprojectPath, "utf-8");
8287
+ const content = readFileSync25(pyprojectPath, "utf-8");
7462
8288
  if (content.includes("pytest-cov") || content.includes("coverage")) {
7463
8289
  return {
7464
8290
  tool: "coverage.py",
@@ -7531,6 +8357,9 @@ function parseCoverageReport(dir, format) {
7531
8357
  if (format === "coverage-py-json") {
7532
8358
  return parsePythonCoverage(dir);
7533
8359
  }
8360
+ if (format === "tarpaulin-json") {
8361
+ return parseTarpaulinCoverage(dir);
8362
+ }
7534
8363
  return 0;
7535
8364
  }
7536
8365
  function parseVitestCoverage(dir) {
@@ -7540,7 +8369,7 @@ function parseVitestCoverage(dir) {
7540
8369
  return 0;
7541
8370
  }
7542
8371
  try {
7543
- const report = JSON.parse(readFileSync24(reportPath, "utf-8"));
8372
+ const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
7544
8373
  return report.total?.statements?.pct ?? 0;
7545
8374
  } catch {
7546
8375
  warn("Failed to parse coverage report");
@@ -7548,19 +8377,33 @@ function parseVitestCoverage(dir) {
7548
8377
  }
7549
8378
  }
7550
8379
  function parsePythonCoverage(dir) {
7551
- const reportPath = join23(dir, "coverage.json");
7552
- if (!existsSync26(reportPath)) {
8380
+ const reportPath = join26(dir, "coverage.json");
8381
+ if (!existsSync28(reportPath)) {
7553
8382
  warn("Coverage report not found at coverage.json");
7554
8383
  return 0;
7555
8384
  }
7556
8385
  try {
7557
- const report = JSON.parse(readFileSync24(reportPath, "utf-8"));
8386
+ const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
7558
8387
  return report.totals?.percent_covered ?? 0;
7559
8388
  } catch {
7560
8389
  warn("Failed to parse coverage report");
7561
8390
  return 0;
7562
8391
  }
7563
8392
  }
8393
+ function parseTarpaulinCoverage(dir) {
8394
+ const reportPath = join26(dir, "coverage", "tarpaulin-report.json");
8395
+ if (!existsSync28(reportPath)) {
8396
+ warn("Tarpaulin report not found at coverage/tarpaulin-report.json");
8397
+ return 0;
8398
+ }
8399
+ try {
8400
+ const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
8401
+ return report.coverage ?? 0;
8402
+ } catch {
8403
+ warn("Failed to parse tarpaulin coverage report");
8404
+ return 0;
8405
+ }
8406
+ }
7564
8407
  function parseTestCounts(output) {
7565
8408
  const vitestMatch = /Tests\s+(\d+)\s+passed(?:\s*\|\s*(\d+)\s+failed)?/i.exec(output);
7566
8409
  if (vitestMatch) {
@@ -7576,6 +8419,18 @@ function parseTestCounts(output) {
7576
8419
  failCount: jestMatch[1] ? parseInt(jestMatch[1], 10) : 0
7577
8420
  };
7578
8421
  }
8422
+ const cargoRegex = /test result:.*?(\d+)\s+passed;\s*(\d+)\s+failed/gi;
8423
+ let cargoMatch = cargoRegex.exec(output);
8424
+ if (cargoMatch) {
8425
+ let totalPass = 0;
8426
+ let totalFail = 0;
8427
+ while (cargoMatch) {
8428
+ totalPass += parseInt(cargoMatch[1], 10);
8429
+ totalFail += parseInt(cargoMatch[2], 10);
8430
+ cargoMatch = cargoRegex.exec(output);
8431
+ }
8432
+ return { passCount: totalPass, failCount: totalFail };
8433
+ }
7579
8434
  const pytestMatch = /(\d+)\s+passed(?:,\s*(\d+)\s+failed)?/i.exec(output);
7580
8435
  if (pytestMatch) {
7581
8436
  return {
@@ -7655,7 +8510,7 @@ function checkPerFileCoverage(floor, dir) {
7655
8510
  }
7656
8511
  let report;
7657
8512
  try {
7658
- report = JSON.parse(readFileSync24(reportPath, "utf-8"));
8513
+ report = JSON.parse(readFileSync25(reportPath, "utf-8"));
7659
8514
  } catch {
7660
8515
  warn("Failed to parse coverage-summary.json");
7661
8516
  return { floor, violations: [], totalFiles: 0 };
@@ -7685,11 +8540,11 @@ function checkPerFileCoverage(floor, dir) {
7685
8540
  }
7686
8541
  function findCoverageSummary(dir) {
7687
8542
  const candidates = [
7688
- join23(dir, "coverage", "coverage-summary.json"),
7689
- join23(dir, "src", "coverage", "coverage-summary.json")
8543
+ join26(dir, "coverage", "coverage-summary.json"),
8544
+ join26(dir, "src", "coverage", "coverage-summary.json")
7690
8545
  ];
7691
8546
  for (const p of candidates) {
7692
- if (existsSync26(p)) return p;
8547
+ if (existsSync28(p)) return p;
7693
8548
  }
7694
8549
  return null;
7695
8550
  }
@@ -7714,7 +8569,7 @@ function printCoverageOutput(result, evaluation) {
7714
8569
  // src/lib/onboard-checks.ts
7715
8570
  function checkHarnessInitialized(dir) {
7716
8571
  const statePath2 = getStatePath(dir ?? process.cwd());
7717
- return { ok: existsSync27(statePath2) };
8572
+ return { ok: existsSync29(statePath2) };
7718
8573
  }
7719
8574
  function checkBmadInstalled(dir) {
7720
8575
  return { ok: isBmadInstalled(dir) };
@@ -7722,8 +8577,8 @@ function checkBmadInstalled(dir) {
7722
8577
  function checkHooksRegistered(dir) {
7723
8578
  const __filename = fileURLToPath3(import.meta.url);
7724
8579
  const __dirname2 = dirname6(__filename);
7725
- const hooksPath = join24(__dirname2, "..", "..", "hooks", "hooks.json");
7726
- return { ok: existsSync27(hooksPath) };
8580
+ const hooksPath = join27(__dirname2, "..", "..", "hooks", "hooks.json");
8581
+ return { ok: existsSync29(hooksPath) };
7727
8582
  }
7728
8583
  function runPreconditions(dir) {
7729
8584
  const harnessCheck = checkHarnessInitialized(dir);
@@ -8361,8 +9216,8 @@ function getBeadsData() {
8361
9216
  }
8362
9217
 
8363
9218
  // src/modules/audit/dimensions.ts
8364
- import { existsSync as existsSync28, readFileSync as readFileSync25, readdirSync as readdirSync6 } from "fs";
8365
- import { join as join25 } from "path";
9219
+ import { existsSync as existsSync30, readdirSync as readdirSync7 } from "fs";
9220
+ import { join as join28 } from "path";
8366
9221
  function gap(dimension, description, suggestedFix) {
8367
9222
  return { dimension, description, suggestedFix };
8368
9223
  }
@@ -8474,15 +9329,15 @@ function checkDocumentation(projectDir) {
8474
9329
  function checkVerification(projectDir) {
8475
9330
  try {
8476
9331
  const gaps = [];
8477
- const sprintPath = join25(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
8478
- if (!existsSync28(sprintPath)) return dimOk("verification", "warn", "no sprint data", [gap("verification", "No sprint-status.yaml found", "Run sprint planning to create sprint status")]);
8479
- const vDir = join25(projectDir, "verification");
9332
+ const sprintPath = join28(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
9333
+ if (!existsSync30(sprintPath)) return dimOk("verification", "warn", "no sprint data", [gap("verification", "No sprint-status.yaml found", "Run sprint planning to create sprint status")]);
9334
+ const vDir = join28(projectDir, "verification");
8480
9335
  let proofCount = 0, totalChecked = 0;
8481
- if (existsSync28(vDir)) {
9336
+ if (existsSync30(vDir)) {
8482
9337
  for (const file of readdirSafe(vDir)) {
8483
9338
  if (!file.endsWith("-proof.md")) continue;
8484
9339
  totalChecked++;
8485
- const r = parseProof(join25(vDir, file));
9340
+ const r = parseProof(join28(vDir, file));
8486
9341
  if (isOk(r) && r.data.passed) {
8487
9342
  proofCount++;
8488
9343
  } else {
@@ -8504,30 +9359,21 @@ function checkVerification(projectDir) {
8504
9359
  }
8505
9360
  function checkInfrastructure(projectDir) {
8506
9361
  try {
8507
- const dfPath = join25(projectDir, "Dockerfile");
8508
- if (!existsSync28(dfPath)) return dimOk("infrastructure", "fail", "no Dockerfile", [gap("infrastructure", "No Dockerfile found", "Create a Dockerfile for containerized deployment")]);
8509
- let content;
8510
- try {
8511
- content = readFileSync25(dfPath, "utf-8");
8512
- } catch {
8513
- return dimOk("infrastructure", "warn", "Dockerfile unreadable", [gap("infrastructure", "Dockerfile exists but could not be read", "Check Dockerfile permissions")]);
8514
- }
8515
- const fromLines = content.split("\n").filter((l) => /^\s*FROM\s+/i.test(l));
8516
- if (fromLines.length === 0) return dimOk("infrastructure", "fail", "invalid Dockerfile", [gap("infrastructure", "Dockerfile has no FROM instruction", "Add a FROM instruction with a pinned base image")]);
8517
- const gaps = [];
8518
- let hasUnpinned = false;
8519
- for (const line of fromLines) {
8520
- const ref = line.replace(/^\s*FROM\s+/i, "").split(/\s+/)[0];
8521
- if (ref.endsWith(":latest")) {
8522
- hasUnpinned = true;
8523
- gaps.push(gap("infrastructure", `Unpinned base image: ${ref}`, `Pin ${ref} to a specific version tag`));
8524
- } else if (!ref.includes(":") && !ref.includes("@")) {
8525
- hasUnpinned = true;
8526
- gaps.push(gap("infrastructure", `Unpinned base image (no tag): ${ref}`, `Pin ${ref} to a specific version tag (e.g., ${ref}:22-slim)`));
8527
- }
8528
- }
8529
- const status = hasUnpinned ? "warn" : "pass";
8530
- const metric = hasUnpinned ? `Dockerfile exists (${gaps.length} issue${gaps.length !== 1 ? "s" : ""})` : "Dockerfile valid";
9362
+ const result = validateDockerfile(projectDir);
9363
+ if (!result.success) {
9364
+ const err = result.error;
9365
+ if (err.includes("No Dockerfile")) return dimOk("infrastructure", "fail", "no Dockerfile", [gap("infrastructure", "No Dockerfile found", "Create a Dockerfile for containerized deployment")]);
9366
+ if (err.includes("could not be read")) return dimOk("infrastructure", "warn", "Dockerfile unreadable", [gap("infrastructure", "Dockerfile exists but could not be read", "Check Dockerfile permissions")]);
9367
+ if (err.includes("no FROM")) return dimOk("infrastructure", "fail", "invalid Dockerfile", [gap("infrastructure", "Dockerfile has no FROM instruction", "Add a FROM instruction with a pinned base image")]);
9368
+ return dimOk("infrastructure", "fail", "validation failed", [gap("infrastructure", err, "Fix Dockerfile validation errors")]);
9369
+ }
9370
+ const gaps = result.data.gaps.map((g) => gap("infrastructure", g.description, g.suggestedFix));
9371
+ for (const w of result.data.warnings) {
9372
+ gaps.push(gap("infrastructure", w, "Provide the missing configuration file"));
9373
+ }
9374
+ const issueCount = gaps.length;
9375
+ const status = issueCount > 0 ? "warn" : "pass";
9376
+ const metric = issueCount > 0 ? `Dockerfile exists (${issueCount} issue${issueCount !== 1 ? "s" : ""})` : "Dockerfile valid";
8531
9377
  return dimOk("infrastructure", status, metric, gaps);
8532
9378
  } catch (err) {
8533
9379
  return dimCatch("infrastructure", err);
@@ -8535,7 +9381,7 @@ function checkInfrastructure(projectDir) {
8535
9381
  }
8536
9382
  function readdirSafe(dir) {
8537
9383
  try {
8538
- return readdirSync6(dir);
9384
+ return readdirSync7(dir);
8539
9385
  } catch {
8540
9386
  return [];
8541
9387
  }
@@ -8568,8 +9414,8 @@ function formatAuditJson(result) {
8568
9414
  }
8569
9415
 
8570
9416
  // src/modules/audit/fix-generator.ts
8571
- import { existsSync as existsSync29, writeFileSync as writeFileSync15, mkdirSync as mkdirSync9 } from "fs";
8572
- import { join as join26, dirname as dirname7 } from "path";
9417
+ import { existsSync as existsSync31, writeFileSync as writeFileSync16, mkdirSync as mkdirSync9 } from "fs";
9418
+ import { join as join29, dirname as dirname7 } from "path";
8573
9419
  function buildStoryKey(gap2, index) {
8574
9420
  const safeDimension = gap2.dimension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
8575
9421
  return `audit-fix-${safeDimension}-${index}`;
@@ -8601,7 +9447,7 @@ function generateFixStories(auditResult) {
8601
9447
  const stories = [];
8602
9448
  let created = 0;
8603
9449
  let skipped = 0;
8604
- const artifactsDir = join26(
9450
+ const artifactsDir = join29(
8605
9451
  process.cwd(),
8606
9452
  "_bmad-output",
8607
9453
  "implementation-artifacts"
@@ -8610,8 +9456,8 @@ function generateFixStories(auditResult) {
8610
9456
  for (let i = 0; i < dimension.gaps.length; i++) {
8611
9457
  const gap2 = dimension.gaps[i];
8612
9458
  const key = buildStoryKey(gap2, i + 1);
8613
- const filePath = join26(artifactsDir, `${key}.md`);
8614
- if (existsSync29(filePath)) {
9459
+ const filePath = join29(artifactsDir, `${key}.md`);
9460
+ if (existsSync31(filePath)) {
8615
9461
  stories.push({
8616
9462
  key,
8617
9463
  filePath,
@@ -8624,7 +9470,7 @@ function generateFixStories(auditResult) {
8624
9470
  }
8625
9471
  const markdown = buildStoryMarkdown(gap2, key);
8626
9472
  mkdirSync9(dirname7(filePath), { recursive: true });
8627
- writeFileSync15(filePath, markdown, "utf-8");
9473
+ writeFileSync16(filePath, markdown, "utf-8");
8628
9474
  stories.push({ key, filePath, gap: gap2, skipped: false });
8629
9475
  created++;
8630
9476
  }
@@ -8800,8 +9646,8 @@ function registerOnboardCommand(program) {
8800
9646
  }
8801
9647
 
8802
9648
  // src/commands/teardown.ts
8803
- import { existsSync as existsSync30, unlinkSync as unlinkSync2, readFileSync as readFileSync26, writeFileSync as writeFileSync16, rmSync as rmSync2 } from "fs";
8804
- import { join as join27 } from "path";
9649
+ import { existsSync as existsSync32, unlinkSync as unlinkSync2, readFileSync as readFileSync26, writeFileSync as writeFileSync17, rmSync as rmSync2 } from "fs";
9650
+ import { join as join30 } from "path";
8805
9651
  function buildDefaultResult() {
8806
9652
  return {
8807
9653
  status: "ok",
@@ -8904,16 +9750,16 @@ function registerTeardownCommand(program) {
8904
9750
  info("Docker stack: not running, skipping");
8905
9751
  }
8906
9752
  }
8907
- const composeFilePath = join27(projectDir, composeFile);
8908
- if (existsSync30(composeFilePath)) {
9753
+ const composeFilePath = join30(projectDir, composeFile);
9754
+ if (existsSync32(composeFilePath)) {
8909
9755
  unlinkSync2(composeFilePath);
8910
9756
  result.removed.push(composeFile);
8911
9757
  if (!isJson) {
8912
9758
  ok(`Removed: ${composeFile}`);
8913
9759
  }
8914
9760
  }
8915
- const otelConfigPath = join27(projectDir, "otel-collector-config.yaml");
8916
- if (existsSync30(otelConfigPath)) {
9761
+ const otelConfigPath = join30(projectDir, "otel-collector-config.yaml");
9762
+ if (existsSync32(otelConfigPath)) {
8917
9763
  unlinkSync2(otelConfigPath);
8918
9764
  result.removed.push("otel-collector-config.yaml");
8919
9765
  if (!isJson) {
@@ -8923,8 +9769,8 @@ function registerTeardownCommand(program) {
8923
9769
  }
8924
9770
  let patchesRemoved = 0;
8925
9771
  for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
8926
- const filePath = join27(projectDir, "_bmad", relativePath);
8927
- if (!existsSync30(filePath)) {
9772
+ const filePath = join30(projectDir, "_bmad", relativePath);
9773
+ if (!existsSync32(filePath)) {
8928
9774
  continue;
8929
9775
  }
8930
9776
  try {
@@ -8943,9 +9789,9 @@ function registerTeardownCommand(program) {
8943
9789
  info("BMAD patches: none found");
8944
9790
  }
8945
9791
  }
8946
- if (state.otlp?.enabled && state.stack === "nodejs") {
8947
- const pkgPath = join27(projectDir, "package.json");
8948
- if (existsSync30(pkgPath)) {
9792
+ if (state.otlp?.enabled && (state.stacks?.includes("nodejs") ?? state.stack === "nodejs")) {
9793
+ const pkgPath = join30(projectDir, "package.json");
9794
+ if (existsSync32(pkgPath)) {
8949
9795
  try {
8950
9796
  const raw = readFileSync26(pkgPath, "utf-8");
8951
9797
  const pkg = JSON.parse(raw);
@@ -8961,7 +9807,7 @@ function registerTeardownCommand(program) {
8961
9807
  for (const key of keysToRemove) {
8962
9808
  delete scripts[key];
8963
9809
  }
8964
- writeFileSync16(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
9810
+ writeFileSync17(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
8965
9811
  result.otlp_cleaned = true;
8966
9812
  if (!isJson) {
8967
9813
  ok("OTLP: removed instrumented scripts from package.json");
@@ -8987,8 +9833,8 @@ function registerTeardownCommand(program) {
8987
9833
  }
8988
9834
  }
8989
9835
  }
8990
- const harnessDir = join27(projectDir, ".harness");
8991
- if (existsSync30(harnessDir)) {
9836
+ const harnessDir = join30(projectDir, ".harness");
9837
+ if (existsSync32(harnessDir)) {
8992
9838
  rmSync2(harnessDir, { recursive: true, force: true });
8993
9839
  result.removed.push(".harness/");
8994
9840
  if (!isJson) {
@@ -8996,7 +9842,7 @@ function registerTeardownCommand(program) {
8996
9842
  }
8997
9843
  }
8998
9844
  const statePath2 = getStatePath(projectDir);
8999
- if (existsSync30(statePath2)) {
9845
+ if (existsSync32(statePath2)) {
9000
9846
  unlinkSync2(statePath2);
9001
9847
  result.removed.push(".claude/codeharness.local.md");
9002
9848
  if (!isJson) {
@@ -9740,8 +10586,8 @@ function registerQueryCommand(program) {
9740
10586
  }
9741
10587
 
9742
10588
  // src/commands/retro-import.ts
9743
- import { existsSync as existsSync31, readFileSync as readFileSync27 } from "fs";
9744
- import { join as join28 } from "path";
10589
+ import { existsSync as existsSync33, readFileSync as readFileSync27 } from "fs";
10590
+ import { join as join31 } from "path";
9745
10591
 
9746
10592
  // src/lib/retro-parser.ts
9747
10593
  var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
@@ -9910,8 +10756,8 @@ function registerRetroImportCommand(program) {
9910
10756
  return;
9911
10757
  }
9912
10758
  const retroFile = `epic-${epicNum}-retrospective.md`;
9913
- const retroPath = join28(root, STORY_DIR3, retroFile);
9914
- if (!existsSync31(retroPath)) {
10759
+ const retroPath = join31(root, STORY_DIR3, retroFile);
10760
+ if (!existsSync33(retroPath)) {
9915
10761
  fail(`Retro file not found: ${retroFile}`, { json: isJson });
9916
10762
  process.exitCode = 1;
9917
10763
  return;
@@ -10299,23 +11145,23 @@ function registerVerifyEnvCommand(program) {
10299
11145
  }
10300
11146
 
10301
11147
  // src/commands/retry.ts
10302
- import { join as join30 } from "path";
11148
+ import { join as join33 } from "path";
10303
11149
 
10304
11150
  // src/lib/retry-state.ts
10305
- import { existsSync as existsSync32, readFileSync as readFileSync28, writeFileSync as writeFileSync17 } from "fs";
10306
- import { join as join29 } from "path";
11151
+ import { existsSync as existsSync34, readFileSync as readFileSync28, writeFileSync as writeFileSync18 } from "fs";
11152
+ import { join as join32 } from "path";
10307
11153
  var RETRIES_FILE = ".story_retries";
10308
11154
  var FLAGGED_FILE = ".flagged_stories";
10309
11155
  var LINE_PATTERN = /^([^=]+)=(\d+)$/;
10310
11156
  function retriesPath(dir) {
10311
- return join29(dir, RETRIES_FILE);
11157
+ return join32(dir, RETRIES_FILE);
10312
11158
  }
10313
11159
  function flaggedPath(dir) {
10314
- return join29(dir, FLAGGED_FILE);
11160
+ return join32(dir, FLAGGED_FILE);
10315
11161
  }
10316
11162
  function readRetries(dir) {
10317
11163
  const filePath = retriesPath(dir);
10318
- if (!existsSync32(filePath)) {
11164
+ if (!existsSync34(filePath)) {
10319
11165
  return /* @__PURE__ */ new Map();
10320
11166
  }
10321
11167
  const raw = readFileSync28(filePath, "utf-8");
@@ -10340,7 +11186,7 @@ function writeRetries(dir, retries) {
10340
11186
  for (const [key, count] of retries) {
10341
11187
  lines.push(`${key}=${count}`);
10342
11188
  }
10343
- writeFileSync17(filePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
11189
+ writeFileSync18(filePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
10344
11190
  }
10345
11191
  function resetRetry(dir, storyKey) {
10346
11192
  if (storyKey) {
@@ -10355,7 +11201,7 @@ function resetRetry(dir, storyKey) {
10355
11201
  }
10356
11202
  function readFlaggedStories(dir) {
10357
11203
  const filePath = flaggedPath(dir);
10358
- if (!existsSync32(filePath)) {
11204
+ if (!existsSync34(filePath)) {
10359
11205
  return [];
10360
11206
  }
10361
11207
  const raw = readFileSync28(filePath, "utf-8");
@@ -10363,7 +11209,7 @@ function readFlaggedStories(dir) {
10363
11209
  }
10364
11210
  function writeFlaggedStories(dir, stories) {
10365
11211
  const filePath = flaggedPath(dir);
10366
- writeFileSync17(filePath, stories.length > 0 ? stories.join("\n") + "\n" : "", "utf-8");
11212
+ writeFileSync18(filePath, stories.length > 0 ? stories.join("\n") + "\n" : "", "utf-8");
10367
11213
  }
10368
11214
  function removeFlaggedStory(dir, key) {
10369
11215
  const stories = readFlaggedStories(dir);
@@ -10383,7 +11229,7 @@ function registerRetryCommand(program) {
10383
11229
  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) => {
10384
11230
  const opts = cmd.optsWithGlobals();
10385
11231
  const isJson = opts.json === true;
10386
- const dir = join30(process.cwd(), RALPH_SUBDIR);
11232
+ const dir = join33(process.cwd(), RALPH_SUBDIR);
10387
11233
  if (opts.story && !isValidStoryKey3(opts.story)) {
10388
11234
  if (isJson) {
10389
11235
  jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
@@ -10792,7 +11638,7 @@ function registerAuditCommand(program) {
10792
11638
  }
10793
11639
 
10794
11640
  // src/index.ts
10795
- var VERSION = true ? "0.23.0" : "0.0.0-dev";
11641
+ var VERSION = true ? "0.24.0" : "0.0.0-dev";
10796
11642
  function createProgram() {
10797
11643
  const program = new Command();
10798
11644
  program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
@@ -10826,5 +11672,6 @@ if (!process.env["VITEST"]) {
10826
11672
  program.parse(process.argv);
10827
11673
  }
10828
11674
  export {
10829
- createProgram
11675
+ createProgram,
11676
+ parseStreamLine
10830
11677
  };