codeharness 0.23.0 → 0.24.1

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.1" : "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,
@@ -2645,176 +3304,25 @@ function generateRalphPrompt(config) {
2645
3304
 
2646
3305
  `;
2647
3306
  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 };
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
  },
@@ -2919,18 +3562,16 @@ function startRenderer(options) {
2919
3562
  retryInfo: null
2920
3563
  };
2921
3564
  let cleaned = false;
2922
- const inkInstance = inkRender(/* @__PURE__ */ jsx2(App, { state }), {
3565
+ const inkInstance = inkRender(/* @__PURE__ */ jsx4(App, { state }), {
2923
3566
  exitOnCtrlC: false,
2924
- patchConsole: false,
3567
+ patchConsole: !options?._forceTTY,
2925
3568
  incrementalRendering: true,
2926
- // Only redraw changed lines (v6.5+)
2927
3569
  maxFps: 15
2928
- // Dashboard doesn't need 30fps
2929
3570
  });
2930
3571
  function rerender() {
2931
3572
  if (!cleaned) {
2932
3573
  state = { ...state };
2933
- inkInstance.rerender(/* @__PURE__ */ jsx2(App, { state }));
3574
+ inkInstance.rerender(/* @__PURE__ */ jsx4(App, { state }));
2934
3575
  }
2935
3576
  }
2936
3577
  function cleanup() {
@@ -2968,7 +3609,8 @@ function startRenderer(options) {
2968
3609
  break;
2969
3610
  case "tool-input":
2970
3611
  state.activeToolArgs += event.partial;
2971
- break;
3612
+ return;
3613
+ // Skip rerender — args only shown on completion
2972
3614
  case "tool-complete":
2973
3615
  if (state.activeTool) {
2974
3616
  const entry = {
@@ -2989,13 +3631,28 @@ function startRenderer(options) {
2989
3631
  state.retryInfo = { attempt: event.attempt, delay: event.delay };
2990
3632
  break;
2991
3633
  case "result":
3634
+ if (event.cost > 0 && state.sprintInfo) {
3635
+ state.sprintInfo = {
3636
+ ...state.sprintInfo,
3637
+ totalCost: (state.sprintInfo.totalCost ?? 0) + event.cost
3638
+ };
3639
+ }
2992
3640
  break;
2993
3641
  }
2994
3642
  rerender();
2995
3643
  }
2996
3644
  function updateSprintState(sprintState) {
2997
3645
  if (cleaned) return;
2998
- state.sprintInfo = sprintState ?? null;
3646
+ if (sprintState && state.sprintInfo) {
3647
+ state.sprintInfo = {
3648
+ ...sprintState,
3649
+ totalCost: sprintState.totalCost ?? state.sprintInfo.totalCost,
3650
+ acProgress: sprintState.acProgress ?? state.sprintInfo.acProgress,
3651
+ currentCommand: sprintState.currentCommand ?? state.sprintInfo.currentCommand
3652
+ };
3653
+ } else {
3654
+ state.sprintInfo = sprintState ?? null;
3655
+ }
2999
3656
  rerender();
3000
3657
  }
3001
3658
  function updateStories(stories) {
@@ -3012,12 +3669,12 @@ function startRenderer(options) {
3012
3669
  }
3013
3670
 
3014
3671
  // 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";
3672
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, renameSync, existsSync as existsSync14 } from "fs";
3673
+ import { join as join13 } from "path";
3017
3674
 
3018
3675
  // src/modules/sprint/migration.ts
3019
- import { readFileSync as readFileSync9, existsSync as existsSync11 } from "fs";
3020
- import { join as join9 } from "path";
3676
+ import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
3677
+ import { join as join12 } from "path";
3021
3678
  var OLD_FILES = {
3022
3679
  storyRetries: "ralph/.story_retries",
3023
3680
  flaggedStories: "ralph/.flagged_stories",
@@ -3026,13 +3683,13 @@ var OLD_FILES = {
3026
3683
  sessionIssues: "_bmad-output/implementation-artifacts/.session-issues.md"
3027
3684
  };
3028
3685
  function resolve(relative2) {
3029
- return join9(process.cwd(), relative2);
3686
+ return join12(process.cwd(), relative2);
3030
3687
  }
3031
3688
  function readIfExists(relative2) {
3032
3689
  const p = resolve(relative2);
3033
- if (!existsSync11(p)) return null;
3690
+ if (!existsSync13(p)) return null;
3034
3691
  try {
3035
- return readFileSync9(p, "utf-8");
3692
+ return readFileSync10(p, "utf-8");
3036
3693
  } catch {
3037
3694
  return null;
3038
3695
  }
@@ -3101,7 +3758,11 @@ function parseRalphStatus(content) {
3101
3758
  iteration: data.loop_count ?? 0,
3102
3759
  cost: 0,
3103
3760
  completed: [],
3104
- failed: []
3761
+ failed: [],
3762
+ currentStory: null,
3763
+ currentPhase: null,
3764
+ lastAction: null,
3765
+ acProgress: null
3105
3766
  };
3106
3767
  } catch {
3107
3768
  return null;
@@ -3132,7 +3793,7 @@ function parseSessionIssues(content) {
3132
3793
  return items;
3133
3794
  }
3134
3795
  function migrateFromOldFormat() {
3135
- const hasAnyOldFile = Object.values(OLD_FILES).some((rel) => existsSync11(resolve(rel)));
3796
+ const hasAnyOldFile = Object.values(OLD_FILES).some((rel) => existsSync13(resolve(rel)));
3136
3797
  if (!hasAnyOldFile) return fail2("No old format files found for migration");
3137
3798
  try {
3138
3799
  const stories = {};
@@ -3173,10 +3834,10 @@ function projectRoot() {
3173
3834
  return process.cwd();
3174
3835
  }
3175
3836
  function statePath() {
3176
- return join10(projectRoot(), "sprint-state.json");
3837
+ return join13(projectRoot(), "sprint-state.json");
3177
3838
  }
3178
3839
  function tmpPath() {
3179
- return join10(projectRoot(), ".sprint-state.json.tmp");
3840
+ return join13(projectRoot(), ".sprint-state.json.tmp");
3180
3841
  }
3181
3842
  function defaultState() {
3182
3843
  return {
@@ -3209,7 +3870,7 @@ function writeStateAtomic(state) {
3209
3870
  const data = JSON.stringify(state, null, 2) + "\n";
3210
3871
  const tmp = tmpPath();
3211
3872
  const final = statePath();
3212
- writeFileSync6(tmp, data, "utf-8");
3873
+ writeFileSync7(tmp, data, "utf-8");
3213
3874
  renameSync(tmp, final);
3214
3875
  return ok2(void 0);
3215
3876
  } catch (err) {
@@ -3219,9 +3880,9 @@ function writeStateAtomic(state) {
3219
3880
  }
3220
3881
  function getSprintState() {
3221
3882
  const fp = statePath();
3222
- if (existsSync12(fp)) {
3883
+ if (existsSync14(fp)) {
3223
3884
  try {
3224
- const raw = readFileSync10(fp, "utf-8");
3885
+ const raw = readFileSync11(fp, "utf-8");
3225
3886
  const parsed = JSON.parse(raw);
3226
3887
  const defaults = defaultState();
3227
3888
  const run = parsed.run;
@@ -3518,9 +4179,9 @@ function generateReport(state, now) {
3518
4179
  }
3519
4180
 
3520
4181
  // src/modules/sprint/timeout.ts
3521
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, existsSync as existsSync13, mkdirSync as mkdirSync3, readdirSync as readdirSync2 } from "fs";
4182
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync15, mkdirSync as mkdirSync3, readdirSync as readdirSync3 } from "fs";
3522
4183
  import { execSync } from "child_process";
3523
- import { join as join11 } from "path";
4184
+ import { join as join14 } from "path";
3524
4185
  var GIT_TIMEOUT_MS = 5e3;
3525
4186
  var DEFAULT_MAX_LINES = 100;
3526
4187
  function captureGitDiff() {
@@ -3549,14 +4210,14 @@ function captureGitDiff() {
3549
4210
  }
3550
4211
  function captureStateDelta(beforePath, afterPath) {
3551
4212
  try {
3552
- if (!existsSync13(beforePath)) {
4213
+ if (!existsSync15(beforePath)) {
3553
4214
  return fail2(`State snapshot not found: ${beforePath}`);
3554
4215
  }
3555
- if (!existsSync13(afterPath)) {
4216
+ if (!existsSync15(afterPath)) {
3556
4217
  return fail2(`Current state file not found: ${afterPath}`);
3557
4218
  }
3558
- const beforeRaw = readFileSync11(beforePath, "utf-8");
3559
- const afterRaw = readFileSync11(afterPath, "utf-8");
4219
+ const beforeRaw = readFileSync12(beforePath, "utf-8");
4220
+ const afterRaw = readFileSync12(afterPath, "utf-8");
3560
4221
  const before = JSON.parse(beforeRaw);
3561
4222
  const after = JSON.parse(afterRaw);
3562
4223
  const beforeStories = before.stories ?? {};
@@ -3581,10 +4242,10 @@ function captureStateDelta(beforePath, afterPath) {
3581
4242
  }
3582
4243
  function capturePartialStderr(outputFile, maxLines = DEFAULT_MAX_LINES) {
3583
4244
  try {
3584
- if (!existsSync13(outputFile)) {
4245
+ if (!existsSync15(outputFile)) {
3585
4246
  return fail2(`Output file not found: ${outputFile}`);
3586
4247
  }
3587
- const content = readFileSync11(outputFile, "utf-8");
4248
+ const content = readFileSync12(outputFile, "utf-8");
3588
4249
  const lines = content.split("\n");
3589
4250
  if (lines.length > 0 && lines[lines.length - 1] === "") {
3590
4251
  lines.pop();
@@ -3632,7 +4293,7 @@ function captureTimeoutReport(opts) {
3632
4293
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3633
4294
  const gitResult = captureGitDiff();
3634
4295
  const gitDiff = gitResult.success ? gitResult.data : `(unavailable: ${gitResult.error})`;
3635
- const statePath2 = join11(process.cwd(), "sprint-state.json");
4296
+ const statePath2 = join14(process.cwd(), "sprint-state.json");
3636
4297
  const deltaResult = captureStateDelta(opts.stateSnapshotPath, statePath2);
3637
4298
  const stateDelta = deltaResult.success ? deltaResult.data : `(unavailable: ${deltaResult.error})`;
3638
4299
  const stderrResult = capturePartialStderr(opts.outputFile);
@@ -3646,15 +4307,15 @@ function captureTimeoutReport(opts) {
3646
4307
  partialStderr,
3647
4308
  timestamp
3648
4309
  };
3649
- const reportDir = join11(process.cwd(), "ralph", "logs");
4310
+ const reportDir = join14(process.cwd(), "ralph", "logs");
3650
4311
  const safeStoryKey = opts.storyKey.replace(/[^a-zA-Z0-9._-]/g, "_");
3651
4312
  const reportFileName = `timeout-report-${opts.iteration}-${safeStoryKey}.md`;
3652
- const reportPath = join11(reportDir, reportFileName);
3653
- if (!existsSync13(reportDir)) {
4313
+ const reportPath = join14(reportDir, reportFileName);
4314
+ if (!existsSync15(reportDir)) {
3654
4315
  mkdirSync3(reportDir, { recursive: true });
3655
4316
  }
3656
4317
  const reportContent = formatReport(capture);
3657
- writeFileSync7(reportPath, reportContent, "utf-8");
4318
+ writeFileSync8(reportPath, reportContent, "utf-8");
3658
4319
  return ok2({
3659
4320
  filePath: reportPath,
3660
4321
  capture
@@ -3666,14 +4327,14 @@ function captureTimeoutReport(opts) {
3666
4327
  }
3667
4328
  function findLatestTimeoutReport(storyKey) {
3668
4329
  try {
3669
- const reportDir = join11(process.cwd(), "ralph", "logs");
3670
- if (!existsSync13(reportDir)) {
4330
+ const reportDir = join14(process.cwd(), "ralph", "logs");
4331
+ if (!existsSync15(reportDir)) {
3671
4332
  return ok2(null);
3672
4333
  }
3673
4334
  const safeStoryKey = storyKey.replace(/[^a-zA-Z0-9._-]/g, "_");
3674
4335
  const prefix = `timeout-report-`;
3675
4336
  const suffix = `-${safeStoryKey}.md`;
3676
- const files = readdirSync2(reportDir, { encoding: "utf-8" });
4337
+ const files = readdirSync3(reportDir, { encoding: "utf-8" });
3677
4338
  const matches = [];
3678
4339
  for (const f of files) {
3679
4340
  if (f.startsWith(prefix) && f.endsWith(suffix)) {
@@ -3689,8 +4350,8 @@ function findLatestTimeoutReport(storyKey) {
3689
4350
  }
3690
4351
  matches.sort((a, b) => b.iteration - a.iteration);
3691
4352
  const latest = matches[0];
3692
- const reportPath = join11(reportDir, latest.fileName);
3693
- const content = readFileSync11(reportPath, "utf-8");
4353
+ const reportPath = join14(reportDir, latest.fileName);
4354
+ const content = readFileSync12(reportPath, "utf-8");
3694
4355
  let durationMinutes = 0;
3695
4356
  let filesChanged = 0;
3696
4357
  const durationMatch = content.match(/\*\*Duration:\*\*\s*(\d+)\s*minutes/);
@@ -3719,12 +4380,12 @@ function findLatestTimeoutReport(storyKey) {
3719
4380
  }
3720
4381
 
3721
4382
  // 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";
4383
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
4384
+ import { existsSync as existsSync16 } from "fs";
4385
+ import { join as join15 } from "path";
3725
4386
 
3726
4387
  // src/modules/sprint/validator.ts
3727
- import { readFileSync as readFileSync13, existsSync as existsSync15 } from "fs";
4388
+ import { readFileSync as readFileSync14, existsSync as existsSync17 } from "fs";
3728
4389
  var VALID_STATUSES = /* @__PURE__ */ new Set([
3729
4390
  "backlog",
3730
4391
  "ready",
@@ -3755,10 +4416,10 @@ function parseSprintStatusKeys(content) {
3755
4416
  }
3756
4417
  function parseStateFile(statePath2) {
3757
4418
  try {
3758
- if (!existsSync15(statePath2)) {
4419
+ if (!existsSync17(statePath2)) {
3759
4420
  return fail2(`State file not found: ${statePath2}`);
3760
4421
  }
3761
- const raw = readFileSync13(statePath2, "utf-8");
4422
+ const raw = readFileSync14(statePath2, "utf-8");
3762
4423
  const parsed = JSON.parse(raw);
3763
4424
  return ok2(parsed);
3764
4425
  } catch (err) {
@@ -3773,10 +4434,10 @@ function validateStateConsistency(statePath2, sprintStatusPath) {
3773
4434
  return fail2(stateResult.error);
3774
4435
  }
3775
4436
  const state = stateResult.data;
3776
- if (!existsSync15(sprintStatusPath)) {
4437
+ if (!existsSync17(sprintStatusPath)) {
3777
4438
  return fail2(`Sprint status file not found: ${sprintStatusPath}`);
3778
4439
  }
3779
- const statusContent = readFileSync13(sprintStatusPath, "utf-8");
4440
+ const statusContent = readFileSync14(sprintStatusPath, "utf-8");
3780
4441
  const keysResult = parseSprintStatusKeys(statusContent);
3781
4442
  if (!keysResult.success) {
3782
4443
  return fail2(keysResult.error);
@@ -3894,6 +4555,109 @@ function clearRunProgress2() {
3894
4555
  return clearRunProgress();
3895
4556
  }
3896
4557
 
4558
+ // src/lib/run-helpers.ts
4559
+ import { StringDecoder } from "string_decoder";
4560
+
4561
+ // src/lib/stream-parser.ts
4562
+ function parseStreamLine(line) {
4563
+ const trimmed = line.trim();
4564
+ if (trimmed.length === 0) {
4565
+ return null;
4566
+ }
4567
+ let parsed;
4568
+ try {
4569
+ parsed = JSON.parse(trimmed);
4570
+ } catch {
4571
+ return null;
4572
+ }
4573
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4574
+ return null;
4575
+ }
4576
+ const wrapperType = parsed.type;
4577
+ if (wrapperType === "stream_event") {
4578
+ return parseStreamEvent(parsed);
4579
+ }
4580
+ if (wrapperType === "system") {
4581
+ return parseSystemEvent(parsed);
4582
+ }
4583
+ if (wrapperType === "result") {
4584
+ return parseResultEvent(parsed);
4585
+ }
4586
+ return null;
4587
+ }
4588
+ function parseStreamEvent(parsed) {
4589
+ const event = parsed.event;
4590
+ if (!event || typeof event !== "object") {
4591
+ return null;
4592
+ }
4593
+ const eventType = event.type;
4594
+ if (eventType === "content_block_start") {
4595
+ return parseContentBlockStart(event);
4596
+ }
4597
+ if (eventType === "content_block_delta") {
4598
+ return parseContentBlockDelta(event);
4599
+ }
4600
+ if (eventType === "content_block_stop") {
4601
+ return { type: "tool-complete" };
4602
+ }
4603
+ return null;
4604
+ }
4605
+ function parseContentBlockStart(event) {
4606
+ const contentBlock = event.content_block;
4607
+ if (!contentBlock || typeof contentBlock !== "object") {
4608
+ return null;
4609
+ }
4610
+ if (contentBlock.type === "tool_use") {
4611
+ const name = contentBlock.name;
4612
+ const id = contentBlock.id;
4613
+ if (typeof name === "string" && typeof id === "string") {
4614
+ return { type: "tool-start", name, id };
4615
+ }
4616
+ }
4617
+ return null;
4618
+ }
4619
+ function parseContentBlockDelta(event) {
4620
+ const delta = event.delta;
4621
+ if (!delta || typeof delta !== "object") {
4622
+ return null;
4623
+ }
4624
+ if (delta.type === "input_json_delta") {
4625
+ const partialJson = delta.partial_json;
4626
+ if (typeof partialJson === "string") {
4627
+ return { type: "tool-input", partial: partialJson };
4628
+ }
4629
+ return null;
4630
+ }
4631
+ if (delta.type === "text_delta") {
4632
+ const text = delta.text;
4633
+ if (typeof text === "string") {
4634
+ return { type: "text", text };
4635
+ }
4636
+ return null;
4637
+ }
4638
+ return null;
4639
+ }
4640
+ function parseSystemEvent(parsed) {
4641
+ const subtype = parsed.subtype;
4642
+ if (subtype === "api_retry") {
4643
+ const attempt = parsed.attempt;
4644
+ const delay = parsed.retry_delay_ms;
4645
+ if (typeof attempt === "number" && typeof delay === "number") {
4646
+ return { type: "retry", attempt, delay };
4647
+ }
4648
+ return null;
4649
+ }
4650
+ return null;
4651
+ }
4652
+ function parseResultEvent(parsed) {
4653
+ const costUsd = parsed.cost_usd;
4654
+ const sessionId = parsed.session_id;
4655
+ if (typeof costUsd === "number" && typeof sessionId === "string") {
4656
+ return { type: "result", cost: costUsd, sessionId };
4657
+ }
4658
+ return null;
4659
+ }
4660
+
3897
4661
  // src/lib/run-helpers.ts
3898
4662
  var STORY_KEY_PATTERN = /^\d+-\d+-/;
3899
4663
  function countStories(statuses) {
@@ -3928,6 +4692,9 @@ function buildSpawnArgs(opts) {
3928
4692
  "--prompt",
3929
4693
  opts.promptFile
3930
4694
  ];
4695
+ if (!opts.quiet) {
4696
+ args.push("--live");
4697
+ }
3931
4698
  if (opts.maxStoryRetries !== void 0) {
3932
4699
  args.push("--max-story-retries", String(opts.maxStoryRetries));
3933
4700
  }
@@ -3979,6 +4746,7 @@ var TIMESTAMP_PREFIX = /^\[[\d-]+\s[\d:]+\]\s*/;
3979
4746
  var SUCCESS_STORY = /\[SUCCESS\]\s+Story\s+([\w-]+):\s+DONE(.*)/;
3980
4747
  var WARN_STORY_RETRY = /\[WARN\]\s+Story\s+([\w-]+)\s+exceeded retry limit/;
3981
4748
  var WARN_STORY_RETRYING = /\[WARN\]\s+Story\s+([\w-]+)\s+.*retry\s+(\d+)\/(\d+)/;
4749
+ var LOOP_ITERATION = /\[LOOP\]\s+iteration\s+(\d+)/;
3982
4750
  var ERROR_LINE = /\[ERROR\]\s+(.+)/;
3983
4751
  function parseRalphMessage(rawLine) {
3984
4752
  const clean = rawLine.replace(ANSI_ESCAPE, "").replace(TIMESTAMP_PREFIX, "").trim();
@@ -4023,6 +4791,41 @@ function parseRalphMessage(rawLine) {
4023
4791
  }
4024
4792
  return null;
4025
4793
  }
4794
+ function parseIterationMessage(rawLine) {
4795
+ const clean = rawLine.replace(ANSI_ESCAPE, "").replace(TIMESTAMP_PREFIX, "").trim();
4796
+ if (clean.length === 0) return null;
4797
+ const match = LOOP_ITERATION.exec(clean);
4798
+ if (match) {
4799
+ return parseInt(match[1], 10);
4800
+ }
4801
+ return null;
4802
+ }
4803
+ function createLineProcessor(callbacks, opts) {
4804
+ let partial = "";
4805
+ const decoder = new StringDecoder("utf8");
4806
+ return (data) => {
4807
+ const text = partial + decoder.write(data);
4808
+ const parts = text.split("\n");
4809
+ partial = parts.pop() ?? "";
4810
+ for (const line of parts) {
4811
+ if (line.trim().length === 0) continue;
4812
+ const event = parseStreamLine(line);
4813
+ if (event) {
4814
+ callbacks.onEvent(event);
4815
+ }
4816
+ if (opts?.parseRalph) {
4817
+ const msg = parseRalphMessage(line);
4818
+ if (msg && callbacks.onMessage) {
4819
+ callbacks.onMessage(msg);
4820
+ }
4821
+ const iteration = parseIterationMessage(line);
4822
+ if (iteration !== null && callbacks.onIteration) {
4823
+ callbacks.onIteration(iteration);
4824
+ }
4825
+ }
4826
+ }
4827
+ };
4828
+ }
4026
4829
 
4027
4830
  // src/commands/run.ts
4028
4831
  var SPRINT_STATUS_REL = "_bmad-output/implementation-artifacts/sprint-status.yaml";
@@ -4033,10 +4836,10 @@ function resolveRalphPath() {
4033
4836
  if (root.endsWith("/src") || root.endsWith("\\src")) {
4034
4837
  root = dirname4(root);
4035
4838
  }
4036
- return join13(root, "ralph", "ralph.sh");
4839
+ return join16(root, "ralph", "ralph.sh");
4037
4840
  }
4038
4841
  function resolvePluginDir() {
4039
- return join13(process.cwd(), ".claude");
4842
+ return join16(process.cwd(), ".claude");
4040
4843
  }
4041
4844
  function registerRunCommand(program) {
4042
4845
  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 +4847,19 @@ function registerRunCommand(program) {
4044
4847
  const isJson = !!globalOpts.json;
4045
4848
  const outputOpts = { json: isJson };
4046
4849
  const ralphPath = resolveRalphPath();
4047
- if (!existsSync16(ralphPath)) {
4850
+ if (!existsSync18(ralphPath)) {
4048
4851
  fail("Ralph loop not found \u2014 reinstall codeharness", outputOpts);
4049
4852
  process.exitCode = 1;
4050
4853
  return;
4051
4854
  }
4052
4855
  const pluginDir = resolvePluginDir();
4053
- if (!existsSync16(pluginDir)) {
4856
+ if (!existsSync18(pluginDir)) {
4054
4857
  fail("Plugin directory not found \u2014 run codeharness init first", outputOpts);
4055
4858
  process.exitCode = 1;
4056
4859
  return;
4057
4860
  }
4058
4861
  const projectDir = process.cwd();
4059
- const sprintStatusPath = join13(projectDir, SPRINT_STATUS_REL);
4862
+ const sprintStatusPath = join16(projectDir, SPRINT_STATUS_REL);
4060
4863
  const statuses = readSprintStatus(projectDir);
4061
4864
  const counts = countStories(statuses);
4062
4865
  if (counts.total === 0) {
@@ -4075,12 +4878,12 @@ function registerRunCommand(program) {
4075
4878
  process.exitCode = 1;
4076
4879
  return;
4077
4880
  }
4078
- const promptFile = join13(projectDir, "ralph", ".harness-prompt.md");
4079
- const flaggedFilePath = join13(projectDir, "ralph", ".flagged_stories");
4881
+ const promptFile = join16(projectDir, "ralph", ".harness-prompt.md");
4882
+ const flaggedFilePath = join16(projectDir, "ralph", ".flagged_stories");
4080
4883
  let flaggedStories;
4081
- if (existsSync16(flaggedFilePath)) {
4884
+ if (existsSync18(flaggedFilePath)) {
4082
4885
  try {
4083
- const flaggedContent = readFileSync14(flaggedFilePath, "utf-8");
4886
+ const flaggedContent = readFileSync15(flaggedFilePath, "utf-8");
4084
4887
  flaggedStories = flaggedContent.split("\n").filter((s) => s.trim().length > 0);
4085
4888
  } catch {
4086
4889
  }
@@ -4092,7 +4895,7 @@ function registerRunCommand(program) {
4092
4895
  });
4093
4896
  try {
4094
4897
  mkdirSync4(dirname4(promptFile), { recursive: true });
4095
- writeFileSync9(promptFile, promptContent, "utf-8");
4898
+ writeFileSync10(promptFile, promptContent, "utf-8");
4096
4899
  } catch (err) {
4097
4900
  const message = err instanceof Error ? err.message : String(err);
4098
4901
  fail(`Failed to write prompt file: ${message}`, outputOpts);
@@ -4119,6 +4922,7 @@ function registerRunCommand(program) {
4119
4922
  const rendererHandle = startRenderer({ quiet });
4120
4923
  let sprintStateInterval = null;
4121
4924
  const sessionStartTime = Date.now();
4925
+ let currentIterationCount = 0;
4122
4926
  try {
4123
4927
  const initialState = getSprintState2();
4124
4928
  if (initialState.success) {
@@ -4128,7 +4932,8 @@ function registerRunCommand(program) {
4128
4932
  phase: s.run.currentPhase ?? "",
4129
4933
  done: s.sprint.done,
4130
4934
  total: s.sprint.total,
4131
- elapsed: formatElapsed(Date.now() - sessionStartTime)
4935
+ elapsed: formatElapsed(Date.now() - sessionStartTime),
4936
+ iterationCount: currentIterationCount
4132
4937
  };
4133
4938
  rendererHandle.updateSprintState(sprintInfo);
4134
4939
  }
@@ -4143,30 +4948,18 @@ function registerRunCommand(program) {
4143
4948
  env
4144
4949
  });
4145
4950
  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 }));
4951
+ const stdoutHandler = createLineProcessor({
4952
+ onEvent: (event) => rendererHandle.update(event)
4953
+ });
4954
+ const stderrHandler = createLineProcessor({
4955
+ onEvent: (event) => rendererHandle.update(event),
4956
+ onMessage: (msg) => rendererHandle.addMessage(msg),
4957
+ onIteration: (iteration) => {
4958
+ currentIterationCount = iteration;
4959
+ }
4960
+ }, { parseRalph: true });
4961
+ child.stdout.on("data", stdoutHandler);
4962
+ child.stderr.on("data", stderrHandler);
4170
4963
  sprintStateInterval = setInterval(() => {
4171
4964
  try {
4172
4965
  const stateResult = getSprintState2();
@@ -4177,7 +4970,8 @@ function registerRunCommand(program) {
4177
4970
  phase: s.run.currentPhase ?? "",
4178
4971
  done: s.sprint.done,
4179
4972
  total: s.sprint.total,
4180
- elapsed: formatElapsed(Date.now() - sessionStartTime)
4973
+ elapsed: formatElapsed(Date.now() - sessionStartTime),
4974
+ iterationCount: currentIterationCount
4181
4975
  };
4182
4976
  rendererHandle.updateSprintState(sprintInfo);
4183
4977
  }
@@ -4201,10 +4995,10 @@ function registerRunCommand(program) {
4201
4995
  });
4202
4996
  });
4203
4997
  if (isJson) {
4204
- const statusFile = join13(projectDir, "ralph", "status.json");
4205
- if (existsSync16(statusFile)) {
4998
+ const statusFile = join16(projectDir, "ralph", "status.json");
4999
+ if (existsSync18(statusFile)) {
4206
5000
  try {
4207
- const statusData = JSON.parse(readFileSync14(statusFile, "utf-8"));
5001
+ const statusData = JSON.parse(readFileSync15(statusFile, "utf-8"));
4208
5002
  const finalStatuses = readSprintStatus(projectDir);
4209
5003
  const finalCounts = countStories(finalStatuses);
4210
5004
  jsonOutput({
@@ -4256,14 +5050,14 @@ function registerRunCommand(program) {
4256
5050
  }
4257
5051
 
4258
5052
  // src/commands/verify.ts
4259
- import { existsSync as existsSync25, readFileSync as readFileSync23 } from "fs";
4260
- import { join as join22 } from "path";
5053
+ import { existsSync as existsSync27, readFileSync as readFileSync24 } from "fs";
5054
+ import { join as join25 } from "path";
4261
5055
 
4262
5056
  // src/modules/verify/index.ts
4263
- import { readFileSync as readFileSync22 } from "fs";
5057
+ import { readFileSync as readFileSync23 } from "fs";
4264
5058
 
4265
5059
  // src/modules/verify/proof.ts
4266
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
5060
+ import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
4267
5061
  function classifyEvidenceCommands(proofContent) {
4268
5062
  const results = [];
4269
5063
  const codeBlockPattern = /```(?:bash|shell)\n([\s\S]*?)```/g;
@@ -4349,10 +5143,10 @@ function validateProofQuality(proofPath) {
4349
5143
  otherCount: 0,
4350
5144
  blackBoxPass: false
4351
5145
  };
4352
- if (!existsSync17(proofPath)) {
5146
+ if (!existsSync19(proofPath)) {
4353
5147
  return emptyResult;
4354
5148
  }
4355
- const content = readFileSync15(proofPath, "utf-8");
5149
+ const content = readFileSync16(proofPath, "utf-8");
4356
5150
  const bbTierMatch = /\*\*Tier:\*\*\s*(unit-testable|black-box)/i.exec(content);
4357
5151
  const bbIsUnitTestable = bbTierMatch ? bbTierMatch[1].toLowerCase() === "unit-testable" : false;
4358
5152
  const bbEnforcement = bbIsUnitTestable ? { blackBoxPass: true, grepSrcCount: 0, dockerExecCount: 0, observabilityCount: 0, otherCount: 0, grepRatio: 0, acsMissingDockerExec: [] } : checkBlackBoxEnforcement(content);
@@ -4495,21 +5289,21 @@ function validateProofQuality(proofPath) {
4495
5289
 
4496
5290
  // src/modules/verify/orchestrator.ts
4497
5291
  import { execFileSync as execFileSync7 } from "child_process";
4498
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync11 } from "fs";
4499
- import { join as join15 } from "path";
5292
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync12 } from "fs";
5293
+ import { join as join18 } from "path";
4500
5294
 
4501
5295
  // src/lib/doc-health.ts
4502
5296
  import { execSync as execSync2 } from "child_process";
4503
5297
  import {
4504
- existsSync as existsSync18,
5298
+ existsSync as existsSync20,
4505
5299
  mkdirSync as mkdirSync5,
4506
- readFileSync as readFileSync16,
4507
- readdirSync as readdirSync3,
5300
+ readFileSync as readFileSync17,
5301
+ readdirSync as readdirSync4,
4508
5302
  statSync,
4509
5303
  unlinkSync,
4510
- writeFileSync as writeFileSync10
5304
+ writeFileSync as writeFileSync11
4511
5305
  } from "fs";
4512
- import { join as join14, relative } from "path";
5306
+ import { join as join17, relative } from "path";
4513
5307
  var DO_NOT_EDIT_HEADER2 = "<!-- DO NOT EDIT MANUALLY";
4514
5308
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
4515
5309
  var DEFAULT_MODULE_THRESHOLD = 3;
@@ -4520,7 +5314,7 @@ function findModules(dir, threshold) {
4520
5314
  function walk(current) {
4521
5315
  let entries;
4522
5316
  try {
4523
- entries = readdirSync3(current);
5317
+ entries = readdirSync4(current);
4524
5318
  } catch {
4525
5319
  return;
4526
5320
  }
@@ -4531,7 +5325,7 @@ function findModules(dir, threshold) {
4531
5325
  let sourceCount = 0;
4532
5326
  const subdirs = [];
4533
5327
  for (const entry of entries) {
4534
- const fullPath = join14(current, entry);
5328
+ const fullPath = join17(current, entry);
4535
5329
  let stat;
4536
5330
  try {
4537
5331
  stat = statSync(fullPath);
@@ -4572,14 +5366,14 @@ function getNewestSourceMtime(dir) {
4572
5366
  function walk(current) {
4573
5367
  let entries;
4574
5368
  try {
4575
- entries = readdirSync3(current);
5369
+ entries = readdirSync4(current);
4576
5370
  } catch {
4577
5371
  return;
4578
5372
  }
4579
5373
  const dirName = current.split("/").pop() ?? "";
4580
5374
  if (dirName === "node_modules" || dirName === ".git") return;
4581
5375
  for (const entry of entries) {
4582
- const fullPath = join14(current, entry);
5376
+ const fullPath = join17(current, entry);
4583
5377
  let stat;
4584
5378
  try {
4585
5379
  stat = statSync(fullPath);
@@ -4608,14 +5402,14 @@ function getSourceFilesInModule(modulePath) {
4608
5402
  function walk(current) {
4609
5403
  let entries;
4610
5404
  try {
4611
- entries = readdirSync3(current);
5405
+ entries = readdirSync4(current);
4612
5406
  } catch {
4613
5407
  return;
4614
5408
  }
4615
5409
  const dirName = current.split("/").pop() ?? "";
4616
5410
  if (dirName === "node_modules" || dirName === ".git" || dirName === "__tests__" || dirName === "dist" || dirName === "coverage" || dirName.startsWith(".") && current !== modulePath) return;
4617
5411
  for (const entry of entries) {
4618
- const fullPath = join14(current, entry);
5412
+ const fullPath = join17(current, entry);
4619
5413
  let stat;
4620
5414
  try {
4621
5415
  stat = statSync(fullPath);
@@ -4636,8 +5430,8 @@ function getSourceFilesInModule(modulePath) {
4636
5430
  return files;
4637
5431
  }
4638
5432
  function getMentionedFilesInAgentsMd(agentsPath) {
4639
- if (!existsSync18(agentsPath)) return [];
4640
- const content = readFileSync16(agentsPath, "utf-8");
5433
+ if (!existsSync20(agentsPath)) return [];
5434
+ const content = readFileSync17(agentsPath, "utf-8");
4641
5435
  const mentioned = /* @__PURE__ */ new Set();
4642
5436
  const filenamePattern = /[\w./-]*[\w-]+\.(?:ts|js|py)\b/g;
4643
5437
  let match;
@@ -4661,12 +5455,12 @@ function checkAgentsMdCompleteness(agentsPath, modulePath) {
4661
5455
  }
4662
5456
  function checkAgentsMdForModule(modulePath, dir) {
4663
5457
  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");
5458
+ const fullModulePath = join17(root, modulePath);
5459
+ let agentsPath = join17(fullModulePath, "AGENTS.md");
5460
+ if (!existsSync20(agentsPath)) {
5461
+ agentsPath = join17(root, "AGENTS.md");
4668
5462
  }
4669
- if (!existsSync18(agentsPath)) {
5463
+ if (!existsSync20(agentsPath)) {
4670
5464
  return {
4671
5465
  path: relative(root, agentsPath),
4672
5466
  grade: "missing",
@@ -4697,9 +5491,9 @@ function checkAgentsMdForModule(modulePath, dir) {
4697
5491
  };
4698
5492
  }
4699
5493
  function checkDoNotEditHeaders(docPath) {
4700
- if (!existsSync18(docPath)) return false;
5494
+ if (!existsSync20(docPath)) return false;
4701
5495
  try {
4702
- const content = readFileSync16(docPath, "utf-8");
5496
+ const content = readFileSync17(docPath, "utf-8");
4703
5497
  if (content.length === 0) return false;
4704
5498
  return content.trimStart().startsWith(DO_NOT_EDIT_HEADER2);
4705
5499
  } catch {
@@ -4711,17 +5505,17 @@ function scanDocHealth(dir) {
4711
5505
  const root = dir ?? process.cwd();
4712
5506
  const documents = [];
4713
5507
  const modules = findModules(root);
4714
- const rootAgentsPath = join14(root, "AGENTS.md");
4715
- if (existsSync18(rootAgentsPath)) {
5508
+ const rootAgentsPath = join17(root, "AGENTS.md");
5509
+ if (existsSync20(rootAgentsPath)) {
4716
5510
  if (modules.length > 0) {
4717
5511
  const docMtime = statSync(rootAgentsPath).mtime;
4718
5512
  let allMissing = [];
4719
5513
  let staleModule = "";
4720
5514
  let newestCode = null;
4721
5515
  for (const mod of modules) {
4722
- const fullModPath = join14(root, mod);
4723
- const modAgentsPath = join14(fullModPath, "AGENTS.md");
4724
- if (existsSync18(modAgentsPath)) continue;
5516
+ const fullModPath = join17(root, mod);
5517
+ const modAgentsPath = join17(fullModPath, "AGENTS.md");
5518
+ if (existsSync20(modAgentsPath)) continue;
4725
5519
  const { missing } = checkAgentsMdCompleteness(rootAgentsPath, fullModPath);
4726
5520
  if (missing.length > 0 && staleModule === "") {
4727
5521
  staleModule = mod;
@@ -4769,8 +5563,8 @@ function scanDocHealth(dir) {
4769
5563
  });
4770
5564
  }
4771
5565
  for (const mod of modules) {
4772
- const modAgentsPath = join14(root, mod, "AGENTS.md");
4773
- if (existsSync18(modAgentsPath)) {
5566
+ const modAgentsPath = join17(root, mod, "AGENTS.md");
5567
+ if (existsSync20(modAgentsPath)) {
4774
5568
  const result = checkAgentsMdForModule(mod, root);
4775
5569
  if (result.path !== "AGENTS.md") {
4776
5570
  documents.push(result);
@@ -4778,9 +5572,9 @@ function scanDocHealth(dir) {
4778
5572
  }
4779
5573
  }
4780
5574
  }
4781
- const indexPath = join14(root, "docs", "index.md");
4782
- if (existsSync18(indexPath)) {
4783
- const content = readFileSync16(indexPath, "utf-8");
5575
+ const indexPath = join17(root, "docs", "index.md");
5576
+ if (existsSync20(indexPath)) {
5577
+ const content = readFileSync17(indexPath, "utf-8");
4784
5578
  const hasAbsolutePaths = /https?:\/\/|file:\/\//i.test(content);
4785
5579
  documents.push({
4786
5580
  path: "docs/index.md",
@@ -4790,11 +5584,11 @@ function scanDocHealth(dir) {
4790
5584
  reason: hasAbsolutePaths ? "Contains absolute URLs (may violate NFR25)" : "Uses relative paths"
4791
5585
  });
4792
5586
  }
4793
- const activeDir = join14(root, "docs", "exec-plans", "active");
4794
- if (existsSync18(activeDir)) {
4795
- const files = readdirSync3(activeDir).filter((f) => f.endsWith(".md"));
5587
+ const activeDir = join17(root, "docs", "exec-plans", "active");
5588
+ if (existsSync20(activeDir)) {
5589
+ const files = readdirSync4(activeDir).filter((f) => f.endsWith(".md"));
4796
5590
  for (const file of files) {
4797
- const filePath = join14(activeDir, file);
5591
+ const filePath = join17(activeDir, file);
4798
5592
  documents.push({
4799
5593
  path: `docs/exec-plans/active/${file}`,
4800
5594
  grade: "fresh",
@@ -4805,11 +5599,11 @@ function scanDocHealth(dir) {
4805
5599
  }
4806
5600
  }
4807
5601
  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("."));
5602
+ const dirPath = join17(root, "docs", subdir);
5603
+ if (!existsSync20(dirPath)) continue;
5604
+ const files = readdirSync4(dirPath).filter((f) => !f.startsWith("."));
4811
5605
  for (const file of files) {
4812
- const filePath = join14(dirPath, file);
5606
+ const filePath = join17(dirPath, file);
4813
5607
  let stat;
4814
5608
  try {
4815
5609
  stat = statSync(filePath);
@@ -4842,7 +5636,7 @@ function scanDocHealth(dir) {
4842
5636
  }
4843
5637
  function checkAgentsMdLineCount(filePath, docPath, documents) {
4844
5638
  try {
4845
- const content = readFileSync16(filePath, "utf-8");
5639
+ const content = readFileSync17(filePath, "utf-8");
4846
5640
  const lineCount = content.split("\n").length;
4847
5641
  if (lineCount > 100) {
4848
5642
  documents.push({
@@ -4880,15 +5674,15 @@ function checkStoryDocFreshness(storyId, dir) {
4880
5674
  for (const mod of modulesToCheck) {
4881
5675
  const result = checkAgentsMdForModule(mod, root);
4882
5676
  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)) {
5677
+ const moduleAgentsPath = join17(root, mod, "AGENTS.md");
5678
+ const actualAgentsPath = existsSync20(moduleAgentsPath) ? moduleAgentsPath : join17(root, "AGENTS.md");
5679
+ if (existsSync20(actualAgentsPath)) {
4886
5680
  checkAgentsMdLineCount(actualAgentsPath, result.path, documents);
4887
5681
  }
4888
5682
  }
4889
5683
  if (modulesToCheck.length === 0) {
4890
- const rootAgentsPath = join14(root, "AGENTS.md");
4891
- if (existsSync18(rootAgentsPath)) {
5684
+ const rootAgentsPath = join17(root, "AGENTS.md");
5685
+ if (existsSync20(rootAgentsPath)) {
4892
5686
  documents.push({
4893
5687
  path: "AGENTS.md",
4894
5688
  grade: "fresh",
@@ -4926,11 +5720,11 @@ function getRecentlyChangedFiles(dir) {
4926
5720
  }
4927
5721
  function completeExecPlan(storyId, dir) {
4928
5722
  const root = dir ?? process.cwd();
4929
- const activePath = join14(root, "docs", "exec-plans", "active", `${storyId}.md`);
4930
- if (!existsSync18(activePath)) {
5723
+ const activePath = join17(root, "docs", "exec-plans", "active", `${storyId}.md`);
5724
+ if (!existsSync20(activePath)) {
4931
5725
  return null;
4932
5726
  }
4933
- let content = readFileSync16(activePath, "utf-8");
5727
+ let content = readFileSync17(activePath, "utf-8");
4934
5728
  content = content.replace(/^Status:\s*active$/m, "Status: completed");
4935
5729
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
4936
5730
  content = content.replace(
@@ -4938,10 +5732,10 @@ function completeExecPlan(storyId, dir) {
4938
5732
  `$1
4939
5733
  Completed: ${timestamp}`
4940
5734
  );
4941
- const completedDir = join14(root, "docs", "exec-plans", "completed");
5735
+ const completedDir = join17(root, "docs", "exec-plans", "completed");
4942
5736
  mkdirSync5(completedDir, { recursive: true });
4943
- const completedPath = join14(completedDir, `${storyId}.md`);
4944
- writeFileSync10(completedPath, content, "utf-8");
5737
+ const completedPath = join17(completedDir, `${storyId}.md`);
5738
+ writeFileSync11(completedPath, content, "utf-8");
4945
5739
  try {
4946
5740
  unlinkSync(activePath);
4947
5741
  } catch {
@@ -5066,8 +5860,8 @@ function checkPreconditions(dir, storyId) {
5066
5860
  }
5067
5861
  function createProofDocument(storyId, storyTitle, acs, dir) {
5068
5862
  const root = dir ?? process.cwd();
5069
- const verificationDir = join15(root, "verification");
5070
- const screenshotsDir = join15(verificationDir, "screenshots");
5863
+ const verificationDir = join18(root, "verification");
5864
+ const screenshotsDir = join18(verificationDir, "screenshots");
5071
5865
  mkdirSync6(verificationDir, { recursive: true });
5072
5866
  mkdirSync6(screenshotsDir, { recursive: true });
5073
5867
  const criteria = acs.map((ac) => ({
@@ -5081,8 +5875,8 @@ function createProofDocument(storyId, storyTitle, acs, dir) {
5081
5875
  storyTitle,
5082
5876
  acceptanceCriteria: criteria
5083
5877
  });
5084
- const proofPath = join15(verificationDir, `${storyId}-proof.md`);
5085
- writeFileSync11(proofPath, content, "utf-8");
5878
+ const proofPath = join18(verificationDir, `${storyId}-proof.md`);
5879
+ writeFileSync12(proofPath, content, "utf-8");
5086
5880
  return proofPath;
5087
5881
  }
5088
5882
  function runShowboatVerify(proofPath) {
@@ -5134,7 +5928,7 @@ function closeBeadsIssue(storyId, dir) {
5134
5928
  }
5135
5929
 
5136
5930
  // src/modules/verify/parser.ts
5137
- import { existsSync as existsSync20, readFileSync as readFileSync17 } from "fs";
5931
+ import { existsSync as existsSync22, readFileSync as readFileSync18 } from "fs";
5138
5932
  var UI_KEYWORDS = [
5139
5933
  "agent-browser",
5140
5934
  "screenshot",
@@ -5204,12 +5998,12 @@ function classifyAC(description) {
5204
5998
  return "general";
5205
5999
  }
5206
6000
  function parseStoryACs(storyFilePath) {
5207
- if (!existsSync20(storyFilePath)) {
6001
+ if (!existsSync22(storyFilePath)) {
5208
6002
  throw new Error(
5209
6003
  `Story file not found: ${storyFilePath}. Ensure the story file exists at the expected path.`
5210
6004
  );
5211
6005
  }
5212
- const content = readFileSync17(storyFilePath, "utf-8");
6006
+ const content = readFileSync18(storyFilePath, "utf-8");
5213
6007
  const lines = content.split("\n");
5214
6008
  let acSectionStart = -1;
5215
6009
  for (let i = 0; i < lines.length; i++) {
@@ -5301,7 +6095,7 @@ function parseObservabilityGaps(proofContent) {
5301
6095
 
5302
6096
  // src/modules/observability/analyzer.ts
5303
6097
  import { execFileSync as execFileSync8 } from "child_process";
5304
- import { join as join16 } from "path";
6098
+ import { join as join19 } from "path";
5305
6099
  var DEFAULT_RULES_DIR = "patches/observability/";
5306
6100
  var DEFAULT_TIMEOUT = 6e4;
5307
6101
  var FUNCTION_NO_LOG_RULE = "function-no-debug-log";
@@ -5335,7 +6129,7 @@ function analyze(projectDir, config) {
5335
6129
  }
5336
6130
  const rulesDir = config?.rulesDir ?? DEFAULT_RULES_DIR;
5337
6131
  const timeout = config?.timeout ?? DEFAULT_TIMEOUT;
5338
- const fullRulesDir = join16(projectDir, rulesDir);
6132
+ const fullRulesDir = join19(projectDir, rulesDir);
5339
6133
  const rawResult = runSemgrep(projectDir, fullRulesDir, timeout);
5340
6134
  if (!rawResult.success) {
5341
6135
  return fail2(rawResult.error);
@@ -5419,8 +6213,8 @@ function normalizeSeverity(severity) {
5419
6213
  }
5420
6214
 
5421
6215
  // 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";
6216
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync13, renameSync as renameSync2, existsSync as existsSync23 } from "fs";
6217
+ import { join as join20 } from "path";
5424
6218
  var STATE_FILE2 = "sprint-state.json";
5425
6219
  var DEFAULT_STATIC_TARGET = 80;
5426
6220
  function defaultCoverageState() {
@@ -5436,12 +6230,12 @@ function defaultCoverageState() {
5436
6230
  };
5437
6231
  }
5438
6232
  function readStateFile(projectDir) {
5439
- const fp = join17(projectDir, STATE_FILE2);
5440
- if (!existsSync21(fp)) {
6233
+ const fp = join20(projectDir, STATE_FILE2);
6234
+ if (!existsSync23(fp)) {
5441
6235
  return ok2({});
5442
6236
  }
5443
6237
  try {
5444
- const raw = readFileSync18(fp, "utf-8");
6238
+ const raw = readFileSync19(fp, "utf-8");
5445
6239
  const parsed = JSON.parse(raw);
5446
6240
  return ok2(parsed);
5447
6241
  } catch (err) {
@@ -5508,8 +6302,8 @@ function parseGapArray(raw) {
5508
6302
  }
5509
6303
 
5510
6304
  // 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";
6305
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync14, renameSync as renameSync3, existsSync as existsSync24 } from "fs";
6306
+ import { join as join21 } from "path";
5513
6307
 
5514
6308
  // src/modules/observability/coverage-gate.ts
5515
6309
  var DEFAULT_STATIC_TARGET2 = 80;
@@ -5551,8 +6345,8 @@ function checkObservabilityCoverageGate(projectDir, overrides) {
5551
6345
 
5552
6346
  // src/modules/observability/runtime-validator.ts
5553
6347
  import { execSync as execSync3 } from "child_process";
5554
- import { readdirSync as readdirSync4, statSync as statSync2 } from "fs";
5555
- import { join as join19 } from "path";
6348
+ import { readdirSync as readdirSync5, statSync as statSync2 } from "fs";
6349
+ import { join as join22 } from "path";
5556
6350
  var DEFAULT_CONFIG = {
5557
6351
  testCommand: "npm test",
5558
6352
  otlpEndpoint: "http://localhost:4318",
@@ -5679,11 +6473,11 @@ function mapEventsToModules(events, projectDir, modules) {
5679
6473
  });
5680
6474
  }
5681
6475
  function discoverModules(projectDir) {
5682
- const srcDir = join19(projectDir, "src");
6476
+ const srcDir = join22(projectDir, "src");
5683
6477
  try {
5684
- return readdirSync4(srcDir).filter((name) => {
6478
+ return readdirSync5(srcDir).filter((name) => {
5685
6479
  try {
5686
- return statSync2(join19(srcDir, name)).isDirectory();
6480
+ return statSync2(join22(srcDir, name)).isDirectory();
5687
6481
  } catch {
5688
6482
  return false;
5689
6483
  }
@@ -5712,7 +6506,7 @@ function parseLogEvents(text) {
5712
6506
 
5713
6507
  // src/modules/verify/browser.ts
5714
6508
  import { execFileSync as execFileSync9 } from "child_process";
5715
- import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
6509
+ import { existsSync as existsSync25, readFileSync as readFileSync21 } from "fs";
5716
6510
 
5717
6511
  // src/modules/verify/validation-ac-fr.ts
5718
6512
  var FR_ACS = [
@@ -6350,8 +7144,8 @@ function getACById(id) {
6350
7144
 
6351
7145
  // src/modules/verify/validation-runner.ts
6352
7146
  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";
7147
+ import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync7 } from "fs";
7148
+ import { join as join23, dirname as dirname5 } from "path";
6355
7149
  var MAX_VALIDATION_ATTEMPTS = 10;
6356
7150
  var AC_COMMAND_TIMEOUT_MS = 3e4;
6357
7151
  var VAL_KEY_PREFIX = "val-";
@@ -6460,7 +7254,7 @@ function executeValidationAC(ac) {
6460
7254
  function createFixStory(ac, error) {
6461
7255
  try {
6462
7256
  const storyKey = `val-fix-${ac.id}`;
6463
- const storyPath = join20(
7257
+ const storyPath = join23(
6464
7258
  process.cwd(),
6465
7259
  "_bmad-output",
6466
7260
  "implementation-artifacts",
@@ -6504,7 +7298,7 @@ function createFixStory(ac, error) {
6504
7298
  ""
6505
7299
  ].join("\n");
6506
7300
  mkdirSync7(dirname5(storyPath), { recursive: true });
6507
- writeFileSync14(storyPath, markdown, "utf-8");
7301
+ writeFileSync15(storyPath, markdown, "utf-8");
6508
7302
  return ok2(storyKey);
6509
7303
  } catch (err) {
6510
7304
  const msg = err instanceof Error ? err.message : String(err);
@@ -6830,8 +7624,8 @@ function runValidationCycle() {
6830
7624
 
6831
7625
  // src/modules/verify/env.ts
6832
7626
  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";
7627
+ import { existsSync as existsSync26, mkdirSync as mkdirSync8, readdirSync as readdirSync6, readFileSync as readFileSync22, cpSync, rmSync, statSync as statSync3 } from "fs";
7628
+ import { join as join24, basename as basename4 } from "path";
6835
7629
  import { createHash } from "crypto";
6836
7630
  var IMAGE_TAG = "codeharness-verify";
6837
7631
  var STORY_DIR = "_bmad-output/implementation-artifacts";
@@ -6844,20 +7638,20 @@ function isValidStoryKey(storyKey) {
6844
7638
  return /^[a-zA-Z0-9_-]+$/.test(storyKey);
6845
7639
  }
6846
7640
  function computeDistHash(projectDir) {
6847
- const distDir = join21(projectDir, "dist");
6848
- if (!existsSync24(distDir)) return null;
7641
+ const distDir = join24(projectDir, "dist");
7642
+ if (!existsSync26(distDir)) return null;
6849
7643
  const hash = createHash("sha256");
6850
7644
  const files = collectFiles(distDir).sort();
6851
7645
  for (const file of files) {
6852
7646
  hash.update(file.slice(distDir.length));
6853
- hash.update(readFileSync21(file));
7647
+ hash.update(readFileSync22(file));
6854
7648
  }
6855
7649
  return hash.digest("hex");
6856
7650
  }
6857
7651
  function collectFiles(dir) {
6858
7652
  const results = [];
6859
- for (const entry of readdirSync5(dir, { withFileTypes: true })) {
6860
- const fullPath = join21(dir, entry.name);
7653
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
7654
+ const fullPath = join24(dir, entry.name);
6861
7655
  if (entry.isDirectory()) {
6862
7656
  results.push(...collectFiles(fullPath));
6863
7657
  } else {
@@ -6884,10 +7678,13 @@ function storeDistHash(projectDir, hash) {
6884
7678
  }
6885
7679
  }
6886
7680
  function detectProjectType(projectDir) {
6887
- const stack = detectStack(projectDir);
7681
+ const allStacks = detectStacks(projectDir);
7682
+ const rootDetection = allStacks.find((s) => s.dir === ".");
7683
+ const stack = rootDetection ? rootDetection.stack : null;
6888
7684
  if (stack === "nodejs") return "nodejs";
6889
7685
  if (stack === "python") return "python";
6890
- if (existsSync24(join21(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
7686
+ if (stack === "rust") return "rust";
7687
+ if (existsSync26(join24(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
6891
7688
  return "generic";
6892
7689
  }
6893
7690
  function buildVerifyImage(options = {}) {
@@ -6897,7 +7694,7 @@ function buildVerifyImage(options = {}) {
6897
7694
  }
6898
7695
  const projectType = detectProjectType(projectDir);
6899
7696
  const currentHash = computeDistHash(projectDir);
6900
- if (projectType === "generic" || projectType === "plugin") {
7697
+ if (projectType === "generic" || projectType === "plugin" || projectType === "rust") {
6901
7698
  } else if (!currentHash) {
6902
7699
  throw new Error("No dist/ directory found. Run your build command first (e.g., npm run build).");
6903
7700
  }
@@ -6912,10 +7709,12 @@ function buildVerifyImage(options = {}) {
6912
7709
  buildNodeImage(projectDir);
6913
7710
  } else if (projectType === "python") {
6914
7711
  buildPythonImage(projectDir);
7712
+ } else if (projectType === "rust") {
7713
+ buildSimpleImage(projectDir, "rust", 3e5);
6915
7714
  } else if (projectType === "plugin") {
6916
7715
  buildPluginImage(projectDir);
6917
7716
  } else {
6918
- buildGenericImage(projectDir);
7717
+ buildSimpleImage(projectDir, "generic");
6919
7718
  }
6920
7719
  if (currentHash) {
6921
7720
  storeDistHash(projectDir, currentHash);
@@ -6931,12 +7730,12 @@ function buildNodeImage(projectDir) {
6931
7730
  const lastLine = packOutput.split("\n").pop()?.trim();
6932
7731
  if (!lastLine) throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
6933
7732
  const tarballName = basename4(lastLine);
6934
- const tarballPath = join21("/tmp", tarballName);
6935
- const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
7733
+ const tarballPath = join24("/tmp", tarballName);
7734
+ const buildContext = join24("/tmp", `codeharness-verify-build-${Date.now()}`);
6936
7735
  mkdirSync8(buildContext, { recursive: true });
6937
7736
  try {
6938
- cpSync(tarballPath, join21(buildContext, tarballName));
6939
- cpSync(resolveDockerfileTemplate(projectDir), join21(buildContext, "Dockerfile"));
7737
+ cpSync(tarballPath, join24(buildContext, tarballName));
7738
+ cpSync(resolveDockerfileTemplate(projectDir), join24(buildContext, "Dockerfile"));
6940
7739
  execFileSync11("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${tarballName}`, "."], {
6941
7740
  cwd: buildContext,
6942
7741
  stdio: "pipe",
@@ -6948,17 +7747,17 @@ function buildNodeImage(projectDir) {
6948
7747
  }
6949
7748
  }
6950
7749
  function buildPythonImage(projectDir) {
6951
- const distDir = join21(projectDir, "dist");
6952
- const distFiles = readdirSync5(distDir).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".whl"));
7750
+ const distDir = join24(projectDir, "dist");
7751
+ const distFiles = readdirSync6(distDir).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".whl"));
6953
7752
  if (distFiles.length === 0) {
6954
7753
  throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
6955
7754
  }
6956
7755
  const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
6957
- const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
7756
+ const buildContext = join24("/tmp", `codeharness-verify-build-${Date.now()}`);
6958
7757
  mkdirSync8(buildContext, { recursive: true });
6959
7758
  try {
6960
- cpSync(join21(distDir, distFile), join21(buildContext, distFile));
6961
- cpSync(resolveDockerfileTemplate(projectDir), join21(buildContext, "Dockerfile"));
7759
+ cpSync(join24(distDir, distFile), join24(buildContext, distFile));
7760
+ cpSync(resolveDockerfileTemplate(projectDir), join24(buildContext, "Dockerfile"));
6962
7761
  execFileSync11("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${distFile}`, "."], {
6963
7762
  cwd: buildContext,
6964
7763
  stdio: "pipe",
@@ -6973,19 +7772,19 @@ function prepareVerifyWorkspace(storyKey, projectDir) {
6973
7772
  if (!isValidStoryKey(storyKey)) {
6974
7773
  throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
6975
7774
  }
6976
- const storyFile = join21(root, STORY_DIR, `${storyKey}.md`);
6977
- if (!existsSync24(storyFile)) throw new Error(`Story file not found: ${storyFile}`);
7775
+ const storyFile = join24(root, STORY_DIR, `${storyKey}.md`);
7776
+ if (!existsSync26(storyFile)) throw new Error(`Story file not found: ${storyFile}`);
6978
7777
  const workspace = `${TEMP_PREFIX}${storyKey}`;
6979
- if (existsSync24(workspace)) rmSync(workspace, { recursive: true, force: true });
7778
+ if (existsSync26(workspace)) rmSync(workspace, { recursive: true, force: true });
6980
7779
  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 });
7780
+ cpSync(storyFile, join24(workspace, "story.md"));
7781
+ const readmePath = join24(root, "README.md");
7782
+ if (existsSync26(readmePath)) cpSync(readmePath, join24(workspace, "README.md"));
7783
+ const docsDir = join24(root, "docs");
7784
+ if (existsSync26(docsDir) && statSync3(docsDir).isDirectory()) {
7785
+ cpSync(docsDir, join24(workspace, "docs"), { recursive: true });
7786
+ }
7787
+ mkdirSync8(join24(workspace, "verification"), { recursive: true });
6989
7788
  return workspace;
6990
7789
  }
6991
7790
  function checkVerifyEnv() {
@@ -7027,7 +7826,7 @@ function cleanupVerifyEnv(storyKey) {
7027
7826
  }
7028
7827
  const workspace = `${TEMP_PREFIX}${storyKey}`;
7029
7828
  const containerName = `codeharness-verify-${storyKey}`;
7030
- if (existsSync24(workspace)) rmSync(workspace, { recursive: true, force: true });
7829
+ if (existsSync26(workspace)) rmSync(workspace, { recursive: true, force: true });
7031
7830
  try {
7032
7831
  execFileSync11("docker", ["stop", containerName], { stdio: "pipe", timeout: 15e3 });
7033
7832
  } catch {
@@ -7038,18 +7837,18 @@ function cleanupVerifyEnv(storyKey) {
7038
7837
  }
7039
7838
  }
7040
7839
  function buildPluginImage(projectDir) {
7041
- const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
7840
+ const buildContext = join24("/tmp", `codeharness-verify-build-${Date.now()}`);
7042
7841
  mkdirSync8(buildContext, { recursive: true });
7043
7842
  try {
7044
- const pluginDir = join21(projectDir, ".claude-plugin");
7045
- cpSync(pluginDir, join21(buildContext, ".claude-plugin"), { recursive: true });
7843
+ const pluginDir = join24(projectDir, ".claude-plugin");
7844
+ cpSync(pluginDir, join24(buildContext, ".claude-plugin"), { recursive: true });
7046
7845
  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 });
7846
+ const src = join24(projectDir, dir);
7847
+ if (existsSync26(src) && statSync3(src).isDirectory()) {
7848
+ cpSync(src, join24(buildContext, dir), { recursive: true });
7050
7849
  }
7051
7850
  }
7052
- cpSync(resolveDockerfileTemplate(projectDir, "generic"), join21(buildContext, "Dockerfile"));
7851
+ cpSync(resolveDockerfileTemplate(projectDir, "generic"), join24(buildContext, "Dockerfile"));
7053
7852
  execFileSync11("docker", ["build", "-t", IMAGE_TAG, "."], {
7054
7853
  cwd: buildContext,
7055
7854
  stdio: "pipe",
@@ -7059,27 +7858,31 @@ function buildPluginImage(projectDir) {
7059
7858
  rmSync(buildContext, { recursive: true, force: true });
7060
7859
  }
7061
7860
  }
7062
- function buildGenericImage(projectDir) {
7063
- const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
7861
+ function buildSimpleImage(projectDir, variant, timeout = 12e4) {
7862
+ const buildContext = join24("/tmp", `codeharness-verify-build-${Date.now()}`);
7064
7863
  mkdirSync8(buildContext, { recursive: true });
7065
7864
  try {
7066
- cpSync(resolveDockerfileTemplate(projectDir, "generic"), join21(buildContext, "Dockerfile"));
7865
+ cpSync(resolveDockerfileTemplate(projectDir, variant), join24(buildContext, "Dockerfile"));
7067
7866
  execFileSync11("docker", ["build", "-t", IMAGE_TAG, "."], {
7068
7867
  cwd: buildContext,
7069
7868
  stdio: "pipe",
7070
- timeout: 12e4
7869
+ timeout
7071
7870
  });
7072
7871
  } finally {
7073
7872
  rmSync(buildContext, { recursive: true, force: true });
7074
7873
  }
7075
7874
  }
7875
+ var DOCKERFILE_VARIANTS = {
7876
+ generic: "Dockerfile.verify.generic",
7877
+ rust: "Dockerfile.verify.rust"
7878
+ };
7076
7879
  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;
7880
+ const filename = (variant && DOCKERFILE_VARIANTS[variant]) ?? "Dockerfile.verify";
7881
+ const local = join24(projectDir, "templates", filename);
7882
+ if (existsSync26(local)) return local;
7080
7883
  const pkgDir = new URL("../../", import.meta.url).pathname;
7081
- const pkg = join21(pkgDir, "templates", filename);
7082
- if (existsSync24(pkg)) return pkg;
7884
+ const pkg = join24(pkgDir, "templates", filename);
7885
+ if (existsSync26(pkg)) return pkg;
7083
7886
  throw new Error(`${filename} not found. Ensure templates/${filename} exists.`);
7084
7887
  }
7085
7888
  function dockerImageExists(tag) {
@@ -7154,8 +7957,8 @@ function verifyRetro(opts, isJson, root) {
7154
7957
  return;
7155
7958
  }
7156
7959
  const retroFile = `epic-${epicNum}-retrospective.md`;
7157
- const retroPath = join22(root, STORY_DIR2, retroFile);
7158
- if (!existsSync25(retroPath)) {
7960
+ const retroPath = join25(root, STORY_DIR2, retroFile);
7961
+ if (!existsSync27(retroPath)) {
7159
7962
  if (isJson) {
7160
7963
  jsonOutput({ status: "fail", epic: epicNum, retroFile, message: `${retroFile} not found` });
7161
7964
  } else {
@@ -7172,7 +7975,7 @@ function verifyRetro(opts, isJson, root) {
7172
7975
  warn(`Failed to update sprint status: ${message}`);
7173
7976
  }
7174
7977
  if (isJson) {
7175
- jsonOutput({ status: "ok", epic: epicNum, retroFile: join22(STORY_DIR2, retroFile) });
7978
+ jsonOutput({ status: "ok", epic: epicNum, retroFile: join25(STORY_DIR2, retroFile) });
7176
7979
  } else {
7177
7980
  ok(`Epic ${epicNum} retrospective: marked done`);
7178
7981
  }
@@ -7183,8 +7986,8 @@ function verifyStory(storyId, isJson, root) {
7183
7986
  process.exitCode = 1;
7184
7987
  return;
7185
7988
  }
7186
- const readmePath = join22(root, "README.md");
7187
- if (!existsSync25(readmePath)) {
7989
+ const readmePath = join25(root, "README.md");
7990
+ if (!existsSync27(readmePath)) {
7188
7991
  if (isJson) {
7189
7992
  jsonOutput({ status: "fail", message: "No README.md found \u2014 verification requires user documentation" });
7190
7993
  } else {
@@ -7193,8 +7996,8 @@ function verifyStory(storyId, isJson, root) {
7193
7996
  process.exitCode = 1;
7194
7997
  return;
7195
7998
  }
7196
- const storyFilePath = join22(root, STORY_DIR2, `${storyId}.md`);
7197
- if (!existsSync25(storyFilePath)) {
7999
+ const storyFilePath = join25(root, STORY_DIR2, `${storyId}.md`);
8000
+ if (!existsSync27(storyFilePath)) {
7198
8001
  fail(`Story file not found: ${storyFilePath}`, { json: isJson });
7199
8002
  process.exitCode = 1;
7200
8003
  return;
@@ -7234,8 +8037,8 @@ function verifyStory(storyId, isJson, root) {
7234
8037
  return;
7235
8038
  }
7236
8039
  const storyTitle = extractStoryTitle(storyFilePath);
7237
- const expectedProofPath = join22(root, "verification", `${storyId}-proof.md`);
7238
- const proofPath = existsSync25(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
8040
+ const expectedProofPath = join25(root, "verification", `${storyId}-proof.md`);
8041
+ const proofPath = existsSync27(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
7239
8042
  const proofQuality = validateProofQuality(proofPath);
7240
8043
  if (!proofQuality.passed) {
7241
8044
  if (isJson) {
@@ -7278,7 +8081,7 @@ function verifyStory(storyId, isJson, root) {
7278
8081
  let observabilityGapCount = 0;
7279
8082
  let runtimeCoveragePercent = 0;
7280
8083
  try {
7281
- const proofContent = readFileSync23(proofPath, "utf-8");
8084
+ const proofContent = readFileSync24(proofPath, "utf-8");
7282
8085
  const gapResult = parseObservabilityGaps(proofContent);
7283
8086
  observabilityGapCount = gapResult.gapCount;
7284
8087
  runtimeCoveragePercent = gapResult.totalACs === 0 ? 0 : gapResult.coveredCount / gapResult.totalACs * 100;
@@ -7340,7 +8143,7 @@ function verifyStory(storyId, isJson, root) {
7340
8143
  }
7341
8144
  function extractStoryTitle(filePath) {
7342
8145
  try {
7343
- const content = readFileSync23(filePath, "utf-8");
8146
+ const content = readFileSync24(filePath, "utf-8");
7344
8147
  const match = /^#\s+(.+)$/m.exec(content);
7345
8148
  return match ? match[1] : "Unknown Story";
7346
8149
  } catch {
@@ -7349,14 +8152,14 @@ function extractStoryTitle(filePath) {
7349
8152
  }
7350
8153
 
7351
8154
  // src/lib/onboard-checks.ts
7352
- import { existsSync as existsSync27 } from "fs";
7353
- import { join as join24, dirname as dirname6 } from "path";
8155
+ import { existsSync as existsSync29 } from "fs";
8156
+ import { join as join27, dirname as dirname6 } from "path";
7354
8157
  import { fileURLToPath as fileURLToPath3 } from "url";
7355
8158
 
7356
8159
  // src/lib/coverage.ts
7357
8160
  import { execSync as execSync6 } from "child_process";
7358
- import { existsSync as existsSync26, readFileSync as readFileSync24 } from "fs";
7359
- import { join as join23 } from "path";
8161
+ import { existsSync as existsSync28, readFileSync as readFileSync25 } from "fs";
8162
+ import { join as join26 } from "path";
7360
8163
  function detectCoverageTool(dir) {
7361
8164
  const baseDir = dir ?? process.cwd();
7362
8165
  const stateHint = getStateToolHint(baseDir);
@@ -7367,6 +8170,27 @@ function detectCoverageTool(dir) {
7367
8170
  if (stack === "python") {
7368
8171
  return detectPythonCoverageTool(baseDir);
7369
8172
  }
8173
+ if (stack === "rust") {
8174
+ try {
8175
+ execSync6("cargo tarpaulin --version", { stdio: "pipe", timeout: 1e4 });
8176
+ } catch {
8177
+ warn("cargo-tarpaulin not installed \u2014 coverage detection unavailable");
8178
+ return { tool: "unknown", runCommand: "", reportFormat: "" };
8179
+ }
8180
+ const cargoPath = join26(baseDir, "Cargo.toml");
8181
+ let isWorkspace = false;
8182
+ try {
8183
+ const cargoContent = readFileSync25(cargoPath, "utf-8");
8184
+ isWorkspace = /^\[workspace\]/m.test(cargoContent);
8185
+ } catch {
8186
+ }
8187
+ const wsFlag = isWorkspace ? " --workspace" : "";
8188
+ return {
8189
+ tool: "cargo-tarpaulin",
8190
+ runCommand: `cargo tarpaulin --out json --output-dir coverage/${wsFlag}`,
8191
+ reportFormat: "tarpaulin-json"
8192
+ };
8193
+ }
7370
8194
  warn("No recognized stack detected \u2014 cannot determine coverage tool");
7371
8195
  return { tool: "unknown", runCommand: "", reportFormat: "" };
7372
8196
  }
@@ -7379,16 +8203,16 @@ function getStateToolHint(dir) {
7379
8203
  }
7380
8204
  }
7381
8205
  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");
8206
+ const hasVitestConfig = existsSync28(join26(dir, "vitest.config.ts")) || existsSync28(join26(dir, "vitest.config.js"));
8207
+ const pkgPath = join26(dir, "package.json");
7384
8208
  let hasVitestCoverageV8 = false;
7385
8209
  let hasVitestCoverageIstanbul = false;
7386
8210
  let hasC8 = false;
7387
8211
  let hasJest = false;
7388
8212
  let pkgScripts = {};
7389
- if (existsSync26(pkgPath)) {
8213
+ if (existsSync28(pkgPath)) {
7390
8214
  try {
7391
- const pkg = JSON.parse(readFileSync24(pkgPath, "utf-8"));
8215
+ const pkg = JSON.parse(readFileSync25(pkgPath, "utf-8"));
7392
8216
  const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
7393
8217
  hasVitestCoverageV8 = "@vitest/coverage-v8" in allDeps;
7394
8218
  hasVitestCoverageIstanbul = "@vitest/coverage-istanbul" in allDeps;
@@ -7441,10 +8265,10 @@ function getNodeTestCommand(scripts, runner) {
7441
8265
  return "npm test";
7442
8266
  }
7443
8267
  function detectPythonCoverageTool(dir) {
7444
- const reqPath = join23(dir, "requirements.txt");
7445
- if (existsSync26(reqPath)) {
8268
+ const reqPath = join26(dir, "requirements.txt");
8269
+ if (existsSync28(reqPath)) {
7446
8270
  try {
7447
- const content = readFileSync24(reqPath, "utf-8");
8271
+ const content = readFileSync25(reqPath, "utf-8");
7448
8272
  if (content.includes("pytest-cov") || content.includes("coverage")) {
7449
8273
  return {
7450
8274
  tool: "coverage.py",
@@ -7455,10 +8279,10 @@ function detectPythonCoverageTool(dir) {
7455
8279
  } catch {
7456
8280
  }
7457
8281
  }
7458
- const pyprojectPath = join23(dir, "pyproject.toml");
7459
- if (existsSync26(pyprojectPath)) {
8282
+ const pyprojectPath = join26(dir, "pyproject.toml");
8283
+ if (existsSync28(pyprojectPath)) {
7460
8284
  try {
7461
- const content = readFileSync24(pyprojectPath, "utf-8");
8285
+ const content = readFileSync25(pyprojectPath, "utf-8");
7462
8286
  if (content.includes("pytest-cov") || content.includes("coverage")) {
7463
8287
  return {
7464
8288
  tool: "coverage.py",
@@ -7531,6 +8355,9 @@ function parseCoverageReport(dir, format) {
7531
8355
  if (format === "coverage-py-json") {
7532
8356
  return parsePythonCoverage(dir);
7533
8357
  }
8358
+ if (format === "tarpaulin-json") {
8359
+ return parseTarpaulinCoverage(dir);
8360
+ }
7534
8361
  return 0;
7535
8362
  }
7536
8363
  function parseVitestCoverage(dir) {
@@ -7540,7 +8367,7 @@ function parseVitestCoverage(dir) {
7540
8367
  return 0;
7541
8368
  }
7542
8369
  try {
7543
- const report = JSON.parse(readFileSync24(reportPath, "utf-8"));
8370
+ const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
7544
8371
  return report.total?.statements?.pct ?? 0;
7545
8372
  } catch {
7546
8373
  warn("Failed to parse coverage report");
@@ -7548,19 +8375,33 @@ function parseVitestCoverage(dir) {
7548
8375
  }
7549
8376
  }
7550
8377
  function parsePythonCoverage(dir) {
7551
- const reportPath = join23(dir, "coverage.json");
7552
- if (!existsSync26(reportPath)) {
8378
+ const reportPath = join26(dir, "coverage.json");
8379
+ if (!existsSync28(reportPath)) {
7553
8380
  warn("Coverage report not found at coverage.json");
7554
8381
  return 0;
7555
8382
  }
7556
8383
  try {
7557
- const report = JSON.parse(readFileSync24(reportPath, "utf-8"));
8384
+ const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
7558
8385
  return report.totals?.percent_covered ?? 0;
7559
8386
  } catch {
7560
8387
  warn("Failed to parse coverage report");
7561
8388
  return 0;
7562
8389
  }
7563
8390
  }
8391
+ function parseTarpaulinCoverage(dir) {
8392
+ const reportPath = join26(dir, "coverage", "tarpaulin-report.json");
8393
+ if (!existsSync28(reportPath)) {
8394
+ warn("Tarpaulin report not found at coverage/tarpaulin-report.json");
8395
+ return 0;
8396
+ }
8397
+ try {
8398
+ const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
8399
+ return report.coverage ?? 0;
8400
+ } catch {
8401
+ warn("Failed to parse tarpaulin coverage report");
8402
+ return 0;
8403
+ }
8404
+ }
7564
8405
  function parseTestCounts(output) {
7565
8406
  const vitestMatch = /Tests\s+(\d+)\s+passed(?:\s*\|\s*(\d+)\s+failed)?/i.exec(output);
7566
8407
  if (vitestMatch) {
@@ -7576,6 +8417,18 @@ function parseTestCounts(output) {
7576
8417
  failCount: jestMatch[1] ? parseInt(jestMatch[1], 10) : 0
7577
8418
  };
7578
8419
  }
8420
+ const cargoRegex = /test result:.*?(\d+)\s+passed;\s*(\d+)\s+failed/gi;
8421
+ let cargoMatch = cargoRegex.exec(output);
8422
+ if (cargoMatch) {
8423
+ let totalPass = 0;
8424
+ let totalFail = 0;
8425
+ while (cargoMatch) {
8426
+ totalPass += parseInt(cargoMatch[1], 10);
8427
+ totalFail += parseInt(cargoMatch[2], 10);
8428
+ cargoMatch = cargoRegex.exec(output);
8429
+ }
8430
+ return { passCount: totalPass, failCount: totalFail };
8431
+ }
7579
8432
  const pytestMatch = /(\d+)\s+passed(?:,\s*(\d+)\s+failed)?/i.exec(output);
7580
8433
  if (pytestMatch) {
7581
8434
  return {
@@ -7655,7 +8508,7 @@ function checkPerFileCoverage(floor, dir) {
7655
8508
  }
7656
8509
  let report;
7657
8510
  try {
7658
- report = JSON.parse(readFileSync24(reportPath, "utf-8"));
8511
+ report = JSON.parse(readFileSync25(reportPath, "utf-8"));
7659
8512
  } catch {
7660
8513
  warn("Failed to parse coverage-summary.json");
7661
8514
  return { floor, violations: [], totalFiles: 0 };
@@ -7685,11 +8538,11 @@ function checkPerFileCoverage(floor, dir) {
7685
8538
  }
7686
8539
  function findCoverageSummary(dir) {
7687
8540
  const candidates = [
7688
- join23(dir, "coverage", "coverage-summary.json"),
7689
- join23(dir, "src", "coverage", "coverage-summary.json")
8541
+ join26(dir, "coverage", "coverage-summary.json"),
8542
+ join26(dir, "src", "coverage", "coverage-summary.json")
7690
8543
  ];
7691
8544
  for (const p of candidates) {
7692
- if (existsSync26(p)) return p;
8545
+ if (existsSync28(p)) return p;
7693
8546
  }
7694
8547
  return null;
7695
8548
  }
@@ -7714,7 +8567,7 @@ function printCoverageOutput(result, evaluation) {
7714
8567
  // src/lib/onboard-checks.ts
7715
8568
  function checkHarnessInitialized(dir) {
7716
8569
  const statePath2 = getStatePath(dir ?? process.cwd());
7717
- return { ok: existsSync27(statePath2) };
8570
+ return { ok: existsSync29(statePath2) };
7718
8571
  }
7719
8572
  function checkBmadInstalled(dir) {
7720
8573
  return { ok: isBmadInstalled(dir) };
@@ -7722,8 +8575,8 @@ function checkBmadInstalled(dir) {
7722
8575
  function checkHooksRegistered(dir) {
7723
8576
  const __filename = fileURLToPath3(import.meta.url);
7724
8577
  const __dirname2 = dirname6(__filename);
7725
- const hooksPath = join24(__dirname2, "..", "..", "hooks", "hooks.json");
7726
- return { ok: existsSync27(hooksPath) };
8578
+ const hooksPath = join27(__dirname2, "..", "..", "hooks", "hooks.json");
8579
+ return { ok: existsSync29(hooksPath) };
7727
8580
  }
7728
8581
  function runPreconditions(dir) {
7729
8582
  const harnessCheck = checkHarnessInitialized(dir);
@@ -8361,8 +9214,8 @@ function getBeadsData() {
8361
9214
  }
8362
9215
 
8363
9216
  // 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";
9217
+ import { existsSync as existsSync30, readdirSync as readdirSync7 } from "fs";
9218
+ import { join as join28 } from "path";
8366
9219
  function gap(dimension, description, suggestedFix) {
8367
9220
  return { dimension, description, suggestedFix };
8368
9221
  }
@@ -8474,15 +9327,15 @@ function checkDocumentation(projectDir) {
8474
9327
  function checkVerification(projectDir) {
8475
9328
  try {
8476
9329
  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");
9330
+ const sprintPath = join28(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
9331
+ if (!existsSync30(sprintPath)) return dimOk("verification", "warn", "no sprint data", [gap("verification", "No sprint-status.yaml found", "Run sprint planning to create sprint status")]);
9332
+ const vDir = join28(projectDir, "verification");
8480
9333
  let proofCount = 0, totalChecked = 0;
8481
- if (existsSync28(vDir)) {
9334
+ if (existsSync30(vDir)) {
8482
9335
  for (const file of readdirSafe(vDir)) {
8483
9336
  if (!file.endsWith("-proof.md")) continue;
8484
9337
  totalChecked++;
8485
- const r = parseProof(join25(vDir, file));
9338
+ const r = parseProof(join28(vDir, file));
8486
9339
  if (isOk(r) && r.data.passed) {
8487
9340
  proofCount++;
8488
9341
  } else {
@@ -8504,30 +9357,21 @@ function checkVerification(projectDir) {
8504
9357
  }
8505
9358
  function checkInfrastructure(projectDir) {
8506
9359
  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";
9360
+ const result = validateDockerfile(projectDir);
9361
+ if (!result.success) {
9362
+ const err = result.error;
9363
+ if (err.includes("No Dockerfile")) return dimOk("infrastructure", "fail", "no Dockerfile", [gap("infrastructure", "No Dockerfile found", "Create a Dockerfile for containerized deployment")]);
9364
+ 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")]);
9365
+ 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")]);
9366
+ return dimOk("infrastructure", "fail", "validation failed", [gap("infrastructure", err, "Fix Dockerfile validation errors")]);
9367
+ }
9368
+ const gaps = result.data.gaps.map((g) => gap("infrastructure", g.description, g.suggestedFix));
9369
+ for (const w of result.data.warnings) {
9370
+ gaps.push(gap("infrastructure", w, "Provide the missing configuration file"));
9371
+ }
9372
+ const issueCount = gaps.length;
9373
+ const status = issueCount > 0 ? "warn" : "pass";
9374
+ const metric = issueCount > 0 ? `Dockerfile exists (${issueCount} issue${issueCount !== 1 ? "s" : ""})` : "Dockerfile valid";
8531
9375
  return dimOk("infrastructure", status, metric, gaps);
8532
9376
  } catch (err) {
8533
9377
  return dimCatch("infrastructure", err);
@@ -8535,7 +9379,7 @@ function checkInfrastructure(projectDir) {
8535
9379
  }
8536
9380
  function readdirSafe(dir) {
8537
9381
  try {
8538
- return readdirSync6(dir);
9382
+ return readdirSync7(dir);
8539
9383
  } catch {
8540
9384
  return [];
8541
9385
  }
@@ -8568,8 +9412,8 @@ function formatAuditJson(result) {
8568
9412
  }
8569
9413
 
8570
9414
  // 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";
9415
+ import { existsSync as existsSync31, writeFileSync as writeFileSync16, mkdirSync as mkdirSync9 } from "fs";
9416
+ import { join as join29, dirname as dirname7 } from "path";
8573
9417
  function buildStoryKey(gap2, index) {
8574
9418
  const safeDimension = gap2.dimension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
8575
9419
  return `audit-fix-${safeDimension}-${index}`;
@@ -8601,7 +9445,7 @@ function generateFixStories(auditResult) {
8601
9445
  const stories = [];
8602
9446
  let created = 0;
8603
9447
  let skipped = 0;
8604
- const artifactsDir = join26(
9448
+ const artifactsDir = join29(
8605
9449
  process.cwd(),
8606
9450
  "_bmad-output",
8607
9451
  "implementation-artifacts"
@@ -8610,8 +9454,8 @@ function generateFixStories(auditResult) {
8610
9454
  for (let i = 0; i < dimension.gaps.length; i++) {
8611
9455
  const gap2 = dimension.gaps[i];
8612
9456
  const key = buildStoryKey(gap2, i + 1);
8613
- const filePath = join26(artifactsDir, `${key}.md`);
8614
- if (existsSync29(filePath)) {
9457
+ const filePath = join29(artifactsDir, `${key}.md`);
9458
+ if (existsSync31(filePath)) {
8615
9459
  stories.push({
8616
9460
  key,
8617
9461
  filePath,
@@ -8624,7 +9468,7 @@ function generateFixStories(auditResult) {
8624
9468
  }
8625
9469
  const markdown = buildStoryMarkdown(gap2, key);
8626
9470
  mkdirSync9(dirname7(filePath), { recursive: true });
8627
- writeFileSync15(filePath, markdown, "utf-8");
9471
+ writeFileSync16(filePath, markdown, "utf-8");
8628
9472
  stories.push({ key, filePath, gap: gap2, skipped: false });
8629
9473
  created++;
8630
9474
  }
@@ -8800,8 +9644,8 @@ function registerOnboardCommand(program) {
8800
9644
  }
8801
9645
 
8802
9646
  // 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";
9647
+ import { existsSync as existsSync32, unlinkSync as unlinkSync2, readFileSync as readFileSync26, writeFileSync as writeFileSync17, rmSync as rmSync2 } from "fs";
9648
+ import { join as join30 } from "path";
8805
9649
  function buildDefaultResult() {
8806
9650
  return {
8807
9651
  status: "ok",
@@ -8904,16 +9748,16 @@ function registerTeardownCommand(program) {
8904
9748
  info("Docker stack: not running, skipping");
8905
9749
  }
8906
9750
  }
8907
- const composeFilePath = join27(projectDir, composeFile);
8908
- if (existsSync30(composeFilePath)) {
9751
+ const composeFilePath = join30(projectDir, composeFile);
9752
+ if (existsSync32(composeFilePath)) {
8909
9753
  unlinkSync2(composeFilePath);
8910
9754
  result.removed.push(composeFile);
8911
9755
  if (!isJson) {
8912
9756
  ok(`Removed: ${composeFile}`);
8913
9757
  }
8914
9758
  }
8915
- const otelConfigPath = join27(projectDir, "otel-collector-config.yaml");
8916
- if (existsSync30(otelConfigPath)) {
9759
+ const otelConfigPath = join30(projectDir, "otel-collector-config.yaml");
9760
+ if (existsSync32(otelConfigPath)) {
8917
9761
  unlinkSync2(otelConfigPath);
8918
9762
  result.removed.push("otel-collector-config.yaml");
8919
9763
  if (!isJson) {
@@ -8923,8 +9767,8 @@ function registerTeardownCommand(program) {
8923
9767
  }
8924
9768
  let patchesRemoved = 0;
8925
9769
  for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
8926
- const filePath = join27(projectDir, "_bmad", relativePath);
8927
- if (!existsSync30(filePath)) {
9770
+ const filePath = join30(projectDir, "_bmad", relativePath);
9771
+ if (!existsSync32(filePath)) {
8928
9772
  continue;
8929
9773
  }
8930
9774
  try {
@@ -8943,9 +9787,9 @@ function registerTeardownCommand(program) {
8943
9787
  info("BMAD patches: none found");
8944
9788
  }
8945
9789
  }
8946
- if (state.otlp?.enabled && state.stack === "nodejs") {
8947
- const pkgPath = join27(projectDir, "package.json");
8948
- if (existsSync30(pkgPath)) {
9790
+ if (state.otlp?.enabled && (state.stacks?.includes("nodejs") ?? state.stack === "nodejs")) {
9791
+ const pkgPath = join30(projectDir, "package.json");
9792
+ if (existsSync32(pkgPath)) {
8949
9793
  try {
8950
9794
  const raw = readFileSync26(pkgPath, "utf-8");
8951
9795
  const pkg = JSON.parse(raw);
@@ -8961,7 +9805,7 @@ function registerTeardownCommand(program) {
8961
9805
  for (const key of keysToRemove) {
8962
9806
  delete scripts[key];
8963
9807
  }
8964
- writeFileSync16(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
9808
+ writeFileSync17(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
8965
9809
  result.otlp_cleaned = true;
8966
9810
  if (!isJson) {
8967
9811
  ok("OTLP: removed instrumented scripts from package.json");
@@ -8987,8 +9831,8 @@ function registerTeardownCommand(program) {
8987
9831
  }
8988
9832
  }
8989
9833
  }
8990
- const harnessDir = join27(projectDir, ".harness");
8991
- if (existsSync30(harnessDir)) {
9834
+ const harnessDir = join30(projectDir, ".harness");
9835
+ if (existsSync32(harnessDir)) {
8992
9836
  rmSync2(harnessDir, { recursive: true, force: true });
8993
9837
  result.removed.push(".harness/");
8994
9838
  if (!isJson) {
@@ -8996,7 +9840,7 @@ function registerTeardownCommand(program) {
8996
9840
  }
8997
9841
  }
8998
9842
  const statePath2 = getStatePath(projectDir);
8999
- if (existsSync30(statePath2)) {
9843
+ if (existsSync32(statePath2)) {
9000
9844
  unlinkSync2(statePath2);
9001
9845
  result.removed.push(".claude/codeharness.local.md");
9002
9846
  if (!isJson) {
@@ -9740,8 +10584,8 @@ function registerQueryCommand(program) {
9740
10584
  }
9741
10585
 
9742
10586
  // src/commands/retro-import.ts
9743
- import { existsSync as existsSync31, readFileSync as readFileSync27 } from "fs";
9744
- import { join as join28 } from "path";
10587
+ import { existsSync as existsSync33, readFileSync as readFileSync27 } from "fs";
10588
+ import { join as join31 } from "path";
9745
10589
 
9746
10590
  // src/lib/retro-parser.ts
9747
10591
  var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
@@ -9910,8 +10754,8 @@ function registerRetroImportCommand(program) {
9910
10754
  return;
9911
10755
  }
9912
10756
  const retroFile = `epic-${epicNum}-retrospective.md`;
9913
- const retroPath = join28(root, STORY_DIR3, retroFile);
9914
- if (!existsSync31(retroPath)) {
10757
+ const retroPath = join31(root, STORY_DIR3, retroFile);
10758
+ if (!existsSync33(retroPath)) {
9915
10759
  fail(`Retro file not found: ${retroFile}`, { json: isJson });
9916
10760
  process.exitCode = 1;
9917
10761
  return;
@@ -10299,23 +11143,23 @@ function registerVerifyEnvCommand(program) {
10299
11143
  }
10300
11144
 
10301
11145
  // src/commands/retry.ts
10302
- import { join as join30 } from "path";
11146
+ import { join as join33 } from "path";
10303
11147
 
10304
11148
  // 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";
11149
+ import { existsSync as existsSync34, readFileSync as readFileSync28, writeFileSync as writeFileSync18 } from "fs";
11150
+ import { join as join32 } from "path";
10307
11151
  var RETRIES_FILE = ".story_retries";
10308
11152
  var FLAGGED_FILE = ".flagged_stories";
10309
11153
  var LINE_PATTERN = /^([^=]+)=(\d+)$/;
10310
11154
  function retriesPath(dir) {
10311
- return join29(dir, RETRIES_FILE);
11155
+ return join32(dir, RETRIES_FILE);
10312
11156
  }
10313
11157
  function flaggedPath(dir) {
10314
- return join29(dir, FLAGGED_FILE);
11158
+ return join32(dir, FLAGGED_FILE);
10315
11159
  }
10316
11160
  function readRetries(dir) {
10317
11161
  const filePath = retriesPath(dir);
10318
- if (!existsSync32(filePath)) {
11162
+ if (!existsSync34(filePath)) {
10319
11163
  return /* @__PURE__ */ new Map();
10320
11164
  }
10321
11165
  const raw = readFileSync28(filePath, "utf-8");
@@ -10340,7 +11184,7 @@ function writeRetries(dir, retries) {
10340
11184
  for (const [key, count] of retries) {
10341
11185
  lines.push(`${key}=${count}`);
10342
11186
  }
10343
- writeFileSync17(filePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
11187
+ writeFileSync18(filePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
10344
11188
  }
10345
11189
  function resetRetry(dir, storyKey) {
10346
11190
  if (storyKey) {
@@ -10355,7 +11199,7 @@ function resetRetry(dir, storyKey) {
10355
11199
  }
10356
11200
  function readFlaggedStories(dir) {
10357
11201
  const filePath = flaggedPath(dir);
10358
- if (!existsSync32(filePath)) {
11202
+ if (!existsSync34(filePath)) {
10359
11203
  return [];
10360
11204
  }
10361
11205
  const raw = readFileSync28(filePath, "utf-8");
@@ -10363,7 +11207,7 @@ function readFlaggedStories(dir) {
10363
11207
  }
10364
11208
  function writeFlaggedStories(dir, stories) {
10365
11209
  const filePath = flaggedPath(dir);
10366
- writeFileSync17(filePath, stories.length > 0 ? stories.join("\n") + "\n" : "", "utf-8");
11210
+ writeFileSync18(filePath, stories.length > 0 ? stories.join("\n") + "\n" : "", "utf-8");
10367
11211
  }
10368
11212
  function removeFlaggedStory(dir, key) {
10369
11213
  const stories = readFlaggedStories(dir);
@@ -10383,7 +11227,7 @@ function registerRetryCommand(program) {
10383
11227
  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
11228
  const opts = cmd.optsWithGlobals();
10385
11229
  const isJson = opts.json === true;
10386
- const dir = join30(process.cwd(), RALPH_SUBDIR);
11230
+ const dir = join33(process.cwd(), RALPH_SUBDIR);
10387
11231
  if (opts.story && !isValidStoryKey3(opts.story)) {
10388
11232
  if (isJson) {
10389
11233
  jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
@@ -10792,7 +11636,7 @@ function registerAuditCommand(program) {
10792
11636
  }
10793
11637
 
10794
11638
  // src/index.ts
10795
- var VERSION = true ? "0.23.0" : "0.0.0-dev";
11639
+ var VERSION = true ? "0.24.1" : "0.0.0-dev";
10796
11640
  function createProgram() {
10797
11641
  const program = new Command();
10798
11642
  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 +11670,6 @@ if (!process.env["VITEST"]) {
10826
11670
  program.parse(process.argv);
10827
11671
  }
10828
11672
  export {
10829
- createProgram
11673
+ createProgram,
11674
+ parseStreamLine
10830
11675
  };