codeharness 0.19.1 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +1861 -879
  2. package/package.json +1 -1
  3. package/ralph/ralph.sh +27 -1
package/dist/index.js CHANGED
@@ -1387,7 +1387,7 @@ function getInstallCommand(stack) {
1387
1387
  }
1388
1388
 
1389
1389
  // src/commands/init.ts
1390
- var HARNESS_VERSION = true ? "0.19.1" : "0.0.0-dev";
1390
+ var HARNESS_VERSION = true ? "0.19.3" : "0.0.0-dev";
1391
1391
  function getProjectName(projectDir) {
1392
1392
  try {
1393
1393
  const pkgPath = join7(projectDir, "package.json");
@@ -1501,8 +1501,8 @@ function registerInitCommand(program) {
1501
1501
  readme: "skipped"
1502
1502
  }
1503
1503
  };
1504
- const statePath = getStatePath(projectDir);
1505
- if (existsSync7(statePath)) {
1504
+ const statePath2 = getStatePath(projectDir);
1505
+ if (existsSync7(statePath2)) {
1506
1506
  try {
1507
1507
  const existingState = readState(projectDir);
1508
1508
  const legacyObsDisabled = existingState.enforcement.observability === false;
@@ -2586,7 +2586,7 @@ function buildSpawnArgs(opts) {
2586
2586
  return args;
2587
2587
  }
2588
2588
  function registerRunCommand(program) {
2589
- 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", "14400").option("--iteration-timeout <minutes>", "Per-iteration timeout in minutes", "30").option("--live", "Show live output streaming", 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) => {
2589
+ 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("--live", "Show live output streaming", 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) => {
2590
2590
  const globalOpts = cmd.optsWithGlobals();
2591
2591
  const isJson = !!globalOpts.json;
2592
2592
  const outputOpts = { json: isJson };
@@ -2668,12 +2668,12 @@ function registerRunCommand(program) {
2668
2668
  cwd: projectDir,
2669
2669
  env
2670
2670
  });
2671
- const exitCode = await new Promise((resolve, reject) => {
2671
+ const exitCode = await new Promise((resolve3, reject) => {
2672
2672
  child.on("error", (err) => {
2673
2673
  reject(err);
2674
2674
  });
2675
2675
  child.on("close", (code) => {
2676
- resolve(code ?? 1);
2676
+ resolve3(code ?? 1);
2677
2677
  });
2678
2678
  });
2679
2679
  if (isJson) {
@@ -2730,223 +2730,330 @@ function registerRunCommand(program) {
2730
2730
  }
2731
2731
 
2732
2732
  // src/commands/verify.ts
2733
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
2734
- import { join as join12 } from "path";
2733
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
2734
+ import { join as join13 } from "path";
2735
2735
 
2736
- // src/lib/verify-parser.ts
2737
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
2738
- var UI_KEYWORDS = [
2739
- "agent-browser",
2740
- "screenshot",
2741
- "navigate",
2742
- "click",
2743
- "form",
2744
- "ui verification",
2745
- "ui acceptance"
2746
- ];
2747
- var API_KEYWORDS = [
2748
- "http",
2749
- "api",
2750
- "endpoint",
2751
- "curl",
2752
- "rest",
2753
- "response bod"
2754
- ];
2755
- var DB_KEYWORDS = [
2756
- "database",
2757
- "db state",
2758
- "db mcp",
2759
- "query",
2760
- "sql",
2761
- "table"
2762
- ];
2763
- var INTEGRATION_KEYWORDS = [
2764
- "external system",
2765
- "real infrastructure",
2766
- "manual verification"
2767
- ];
2768
- var ESCALATE_KEYWORDS = [
2769
- "physical hardware",
2770
- "manual human",
2771
- "visual inspection by human",
2772
- "paid external service"
2773
- ];
2774
- function classifyVerifiability(description) {
2775
- const lower = description.toLowerCase();
2776
- for (const kw of INTEGRATION_KEYWORDS) {
2777
- if (lower.includes(kw)) return "integration-required";
2778
- }
2779
- return "cli-verifiable";
2736
+ // src/types/result.ts
2737
+ function ok2(data) {
2738
+ return { success: true, data };
2780
2739
  }
2781
- function classifyStrategy(description) {
2782
- const lower = description.toLowerCase();
2783
- for (const kw of ESCALATE_KEYWORDS) {
2784
- if (lower.includes(kw)) return "escalate";
2740
+ function fail2(error, context) {
2741
+ if (context !== void 0) {
2742
+ return { success: false, error, context };
2785
2743
  }
2786
- return "docker";
2787
- }
2788
- var VERIFICATION_TAG_PATTERN = /<!--\s*verification:\s*(cli-verifiable|integration-required)\s*-->/;
2789
- function parseVerificationTag(text) {
2790
- const match = VERIFICATION_TAG_PATTERN.exec(text);
2791
- return match ? match[1] : null;
2744
+ return { success: false, error };
2792
2745
  }
2793
- function classifyAC(description) {
2794
- const lower = description.toLowerCase();
2795
- for (const kw of UI_KEYWORDS) {
2796
- if (lower.includes(kw)) return "ui";
2797
- }
2798
- for (const kw of API_KEYWORDS) {
2799
- if (lower.includes(kw)) return "api";
2800
- }
2801
- for (const kw of DB_KEYWORDS) {
2802
- if (lower.includes(kw)) return "db";
2746
+
2747
+ // src/modules/verify/proof.ts
2748
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
2749
+ function classifyEvidenceCommands(proofContent) {
2750
+ const results = [];
2751
+ const codeBlockPattern = /```(?:bash|shell)\n([\s\S]*?)```/g;
2752
+ for (const match of proofContent.matchAll(codeBlockPattern)) {
2753
+ const block = match[1].trim();
2754
+ const lines = block.split("\n").filter((l) => l.trim().length > 0);
2755
+ for (const line of lines) {
2756
+ const cmd = line.trim();
2757
+ if (!cmd) continue;
2758
+ results.push({ command: cmd, type: classifyCommand(cmd) });
2759
+ }
2803
2760
  }
2804
- return "general";
2761
+ return results;
2805
2762
  }
2806
- function parseStoryACs(storyFilePath) {
2807
- if (!existsSync11(storyFilePath)) {
2808
- throw new Error(
2809
- `Story file not found: ${storyFilePath}. Ensure the story file exists at the expected path.`
2810
- );
2811
- }
2812
- const content = readFileSync10(storyFilePath, "utf-8");
2813
- const lines = content.split("\n");
2814
- let acSectionStart = -1;
2815
- for (let i = 0; i < lines.length; i++) {
2816
- if (/^##\s+Acceptance\s+Criteria/i.test(lines[i])) {
2817
- acSectionStart = i + 1;
2818
- break;
2819
- }
2763
+ function classifyCommand(cmd) {
2764
+ if (/docker\s+exec\b/.test(cmd)) {
2765
+ return "docker-exec";
2820
2766
  }
2821
- if (acSectionStart === -1) {
2822
- warn('No "## Acceptance Criteria" section found in story file');
2823
- return [];
2767
+ if (/docker\s+(ps|logs|inspect|stats|top|port)\b/.test(cmd)) {
2768
+ return "docker-host";
2824
2769
  }
2825
- let acSectionEnd = lines.length;
2826
- for (let i = acSectionStart; i < lines.length; i++) {
2827
- if (/^##\s+/.test(lines[i]) && i !== acSectionStart - 1) {
2828
- acSectionEnd = i;
2829
- break;
2830
- }
2770
+ if (/curl\b/.test(cmd) && /localhost:(9428|8428|16686)\b/.test(cmd)) {
2771
+ return "observability";
2831
2772
  }
2832
- const acSection = lines.slice(acSectionStart, acSectionEnd).join("\n");
2833
- const acPattern = /^\s*(\d+)\.\s+/;
2834
- const acs = [];
2835
- const acLines = acSection.split("\n");
2836
- let currentId = null;
2837
- let currentDesc = [];
2838
- const flushCurrent = () => {
2839
- if (currentId !== null && currentDesc.length > 0) {
2840
- const description = currentDesc.join(" ").trim();
2841
- if (description) {
2842
- const tag = parseVerificationTag(description);
2843
- const verifiability = tag ?? classifyVerifiability(description);
2844
- const strategy = classifyStrategy(description);
2845
- acs.push({
2846
- id: currentId,
2847
- description,
2848
- type: classifyAC(description),
2849
- verifiability,
2850
- strategy
2851
- });
2852
- } else {
2853
- warn(`Skipping malformed AC #${currentId}: empty description`);
2854
- }
2855
- }
2856
- };
2857
- for (const line of acLines) {
2858
- const match = acPattern.exec(line);
2859
- if (match) {
2860
- flushCurrent();
2861
- currentId = match[1];
2862
- currentDesc = [line.replace(acPattern, "").trim()];
2863
- } else if (currentId !== null && line.trim()) {
2864
- currentDesc.push(line.trim());
2865
- }
2773
+ if (/\bgrep\b/.test(cmd) && /\bsrc\//.test(cmd)) {
2774
+ return "grep-src";
2866
2775
  }
2867
- flushCurrent();
2868
- return acs;
2776
+ return "other";
2869
2777
  }
2870
-
2871
- // src/lib/verify.ts
2872
- import { execFileSync as execFileSync5 } from "child_process";
2873
- import { existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
2874
- import { join as join11 } from "path";
2875
-
2876
- // src/lib/doc-health.ts
2877
- import { execSync } from "child_process";
2878
- import {
2879
- existsSync as existsSync12,
2880
- mkdirSync as mkdirSync4,
2881
- readFileSync as readFileSync11,
2882
- readdirSync as readdirSync2,
2883
- statSync,
2884
- unlinkSync,
2885
- writeFileSync as writeFileSync7
2886
- } from "fs";
2887
- import { join as join10, relative } from "path";
2888
- var DO_NOT_EDIT_HEADER2 = "<!-- DO NOT EDIT MANUALLY";
2889
- var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
2890
- var DEFAULT_MODULE_THRESHOLD = 3;
2891
- function findModules(dir, threshold) {
2892
- const limit = threshold ?? DEFAULT_MODULE_THRESHOLD;
2893
- const root = dir;
2894
- const modules = [];
2895
- function walk(current) {
2896
- let entries;
2897
- try {
2898
- entries = readdirSync2(current);
2899
- } catch {
2900
- return;
2901
- }
2902
- const dirName = current.split("/").pop() ?? "";
2903
- if (dirName === "node_modules" || dirName === ".git" || dirName === "dist" || dirName === "coverage" || dirName.startsWith(".") && current !== root) {
2904
- return;
2905
- }
2906
- let sourceCount = 0;
2907
- const subdirs = [];
2908
- for (const entry of entries) {
2909
- const fullPath = join10(current, entry);
2910
- let stat;
2911
- try {
2912
- stat = statSync(fullPath);
2913
- } catch {
2914
- continue;
2915
- }
2916
- if (stat.isDirectory()) {
2917
- subdirs.push(fullPath);
2918
- } else if (stat.isFile()) {
2919
- const ext = getExtension(entry);
2920
- if (SOURCE_EXTENSIONS.has(ext) && !isTestFile(entry)) {
2921
- sourceCount++;
2922
- }
2923
- }
2924
- }
2925
- if (sourceCount >= limit) {
2926
- const rel = relative(root, current);
2927
- if (rel !== "") {
2928
- modules.push(rel);
2778
+ function checkBlackBoxEnforcement(proofContent) {
2779
+ const commands = classifyEvidenceCommands(proofContent);
2780
+ const grepSrcCount = commands.filter((c) => c.type === "grep-src").length;
2781
+ const dockerExecCount = commands.filter((c) => c.type === "docker-exec").length;
2782
+ const observabilityCount = commands.filter((c) => c.type === "observability").length;
2783
+ const otherCount = commands.filter((c) => c.type === "other").length;
2784
+ const totalCommands = commands.length;
2785
+ const grepRatio = totalCommands > 0 ? grepSrcCount / totalCommands : 0;
2786
+ const acsMissingDockerExec = [];
2787
+ const acHeaderPattern = /^## AC ?(\d+):/gm;
2788
+ const acMatches = [...proofContent.matchAll(acHeaderPattern)];
2789
+ if (acMatches.length > 0) {
2790
+ for (let i = 0; i < acMatches.length; i++) {
2791
+ const acNum = parseInt(acMatches[i][1], 10);
2792
+ const start = acMatches[i].index;
2793
+ const end = i + 1 < acMatches.length ? acMatches[i + 1].index : proofContent.length;
2794
+ const section = proofContent.slice(start, end);
2795
+ if (section.includes("[ESCALATE]")) continue;
2796
+ const sectionCommands = classifyEvidenceCommands(section);
2797
+ const hasBlackBoxEvidence = sectionCommands.some(
2798
+ (c) => c.type === "docker-exec" || c.type === "docker-host" || c.type === "observability"
2799
+ );
2800
+ if (!hasBlackBoxEvidence) {
2801
+ acsMissingDockerExec.push(acNum);
2929
2802
  }
2930
2803
  }
2931
- for (const subdir of subdirs) {
2932
- walk(subdir);
2933
- }
2934
2804
  }
2935
- walk(root);
2936
- return modules;
2937
- }
2938
- function getExtension(filename) {
2939
- const dot = filename.lastIndexOf(".");
2940
- return dot >= 0 ? filename.slice(dot) : "";
2805
+ const grepTooHigh = grepRatio > 0.5;
2806
+ const missingDockerExec = acsMissingDockerExec.length > 0;
2807
+ const hasExtractableCommands = totalCommands > 0;
2808
+ return {
2809
+ blackBoxPass: !hasExtractableCommands || !grepTooHigh && !missingDockerExec,
2810
+ grepSrcCount,
2811
+ dockerExecCount,
2812
+ observabilityCount,
2813
+ otherCount,
2814
+ grepRatio,
2815
+ acsMissingDockerExec
2816
+ };
2941
2817
  }
2942
- function isTestFile(filename) {
2943
- return filename.includes(".test.") || filename.includes(".spec.") || filename.includes("__tests__") || filename.startsWith("test_");
2818
+ function hasFailVerdict(section) {
2819
+ return section.replace(/```[\s\S]*?```/g, "").replace(/`[^`]+`/g, "").includes("[FAIL]");
2944
2820
  }
2945
- function isDocStale(docPath, codeDir) {
2946
- if (!existsSync12(docPath)) return true;
2947
- if (!existsSync12(codeDir)) return false;
2948
- const docMtime = statSync(docPath).mtime;
2949
- const newestCode = getNewestSourceMtime(codeDir);
2821
+ function validateProofQuality(proofPath) {
2822
+ const emptyResult = {
2823
+ verified: 0,
2824
+ pending: 0,
2825
+ escalated: 0,
2826
+ total: 0,
2827
+ passed: false,
2828
+ grepSrcCount: 0,
2829
+ dockerExecCount: 0,
2830
+ observabilityCount: 0,
2831
+ otherCount: 0,
2832
+ blackBoxPass: false
2833
+ };
2834
+ if (!existsSync11(proofPath)) {
2835
+ return emptyResult;
2836
+ }
2837
+ const content = readFileSync10(proofPath, "utf-8");
2838
+ const bbTierMatch = /\*\*Tier:\*\*\s*(unit-testable|black-box)/i.exec(content);
2839
+ const bbIsUnitTestable = bbTierMatch ? bbTierMatch[1].toLowerCase() === "unit-testable" : false;
2840
+ const bbEnforcement = bbIsUnitTestable ? { blackBoxPass: true, grepSrcCount: 0, dockerExecCount: 0, observabilityCount: 0, otherCount: 0, grepRatio: 0, acsMissingDockerExec: [] } : checkBlackBoxEnforcement(content);
2841
+ function buildResult(base) {
2842
+ const basePassed = base.pending === 0 && base.verified > 0;
2843
+ return {
2844
+ ...base,
2845
+ passed: basePassed && bbEnforcement.blackBoxPass,
2846
+ grepSrcCount: bbEnforcement.grepSrcCount,
2847
+ dockerExecCount: bbEnforcement.dockerExecCount,
2848
+ observabilityCount: bbEnforcement.observabilityCount,
2849
+ otherCount: bbEnforcement.otherCount,
2850
+ blackBoxPass: bbEnforcement.blackBoxPass
2851
+ };
2852
+ }
2853
+ const acHeaderPattern = /^## AC ?(\d+):/gm;
2854
+ const matches = [...content.matchAll(acHeaderPattern)];
2855
+ const nonAcHeadingPattern = /^## (?!AC ?\d+:)/gm;
2856
+ const nonAcHeadings = [...content.matchAll(nonAcHeadingPattern)].map((m) => m.index);
2857
+ let verified = 0;
2858
+ let pending = 0;
2859
+ let escalated = 0;
2860
+ if (matches.length > 0) {
2861
+ for (let i = 0; i < matches.length; i++) {
2862
+ const start = matches[i].index;
2863
+ const nextAcEnd = i + 1 < matches.length ? matches[i + 1].index : content.length;
2864
+ const nextNonAcHeading = nonAcHeadings.find((h) => h > start);
2865
+ const end = nextNonAcHeading !== void 0 && nextNonAcHeading < nextAcEnd ? nextNonAcHeading : nextAcEnd;
2866
+ const section = content.slice(start, end);
2867
+ if (section.includes("[ESCALATE]")) {
2868
+ escalated++;
2869
+ continue;
2870
+ }
2871
+ if (hasFailVerdict(section)) {
2872
+ pending++;
2873
+ continue;
2874
+ }
2875
+ const hasEvidence = section.includes("<!-- /showboat exec -->") || section.includes("<!-- showboat image:") || /```(?:bash|shell)\n[\s\S]*?```\n+```output\n/m.test(section);
2876
+ if (hasEvidence) {
2877
+ verified++;
2878
+ } else {
2879
+ pending++;
2880
+ }
2881
+ }
2882
+ } else {
2883
+ const inlineAcPattern = /--- AC ?(\d+):/g;
2884
+ const inlineMatches = [...content.matchAll(inlineAcPattern)];
2885
+ const acNumbers = new Set(inlineMatches.map((m) => m[1]));
2886
+ if (acNumbers.size === 0) {
2887
+ const narrativeAcPattern = /=== AC ?(\d+):/g;
2888
+ const narrativeMatches = [...content.matchAll(narrativeAcPattern)];
2889
+ const narrativeAcNumbers = new Set(narrativeMatches.map((m) => m[1]));
2890
+ if (narrativeAcNumbers.size === 0) {
2891
+ const bulletAcPattern = /^- AC ?(\d+)[^:\n]*:/gm;
2892
+ const bulletMatches = [...content.matchAll(bulletAcPattern)];
2893
+ const bulletAcNumbers = new Set(bulletMatches.map((m) => m[1]));
2894
+ if (bulletAcNumbers.size === 0) {
2895
+ return buildResult({ verified: 0, pending: 0, escalated: 0, total: 0 });
2896
+ }
2897
+ let bVerified = 0;
2898
+ let bPending = 0;
2899
+ let bEscalated = 0;
2900
+ for (const acNum of bulletAcNumbers) {
2901
+ const bulletPattern = new RegExp(`^- AC ?${acNum}[^:\\n]*:(.*)$`, "m");
2902
+ const bulletMatch = content.match(bulletPattern);
2903
+ if (!bulletMatch) {
2904
+ bPending++;
2905
+ continue;
2906
+ }
2907
+ const bulletText = bulletMatch[1].toLowerCase();
2908
+ if (bulletText.includes("n/a") || bulletText.includes("escalat") || bulletText.includes("superseded")) {
2909
+ bEscalated++;
2910
+ } else if (bulletText.includes("fail")) {
2911
+ bPending++;
2912
+ } else {
2913
+ bVerified++;
2914
+ }
2915
+ }
2916
+ const hasAnyEvidence = /```output\n/m.test(content);
2917
+ if (!hasAnyEvidence) {
2918
+ bPending += bVerified;
2919
+ bVerified = 0;
2920
+ }
2921
+ const bTotal = bVerified + bPending + bEscalated;
2922
+ return buildResult({
2923
+ verified: bVerified,
2924
+ pending: bPending,
2925
+ escalated: bEscalated,
2926
+ total: bTotal
2927
+ });
2928
+ }
2929
+ const sortedAcs = narrativeMatches.map((m) => ({ num: m[1], idx: m.index })).filter((v, i, a) => a.findIndex((x) => x.num === v.num) === i).sort((a, b) => a.idx - b.idx);
2930
+ for (let i = 0; i < sortedAcs.length; i++) {
2931
+ const regionStart = i > 0 ? sortedAcs[i - 1].idx : 0;
2932
+ const regionEnd = i + 1 < sortedAcs.length ? sortedAcs[i + 1].idx : content.length;
2933
+ const section = content.slice(regionStart, regionEnd);
2934
+ if (section.includes("[ESCALATE]")) {
2935
+ escalated++;
2936
+ } else if (hasFailVerdict(section)) {
2937
+ pending++;
2938
+ } else if (/```output/m.test(section)) {
2939
+ verified++;
2940
+ } else {
2941
+ pending++;
2942
+ }
2943
+ }
2944
+ const narrativeTotal = verified + pending + escalated;
2945
+ return buildResult({
2946
+ verified,
2947
+ pending,
2948
+ escalated,
2949
+ total: narrativeTotal
2950
+ });
2951
+ }
2952
+ for (const acNum of acNumbers) {
2953
+ const acPattern2 = new RegExp(`--- AC ?${acNum}:`, "g");
2954
+ const acIdx = content.search(acPattern2);
2955
+ if (acIdx === -1) {
2956
+ pending++;
2957
+ continue;
2958
+ }
2959
+ const nextAcPattern = new RegExp(`--- AC ?(?!${acNum})\\d+:`, "g");
2960
+ nextAcPattern.lastIndex = acIdx + 1;
2961
+ const nextMatch = nextAcPattern.exec(content);
2962
+ const section = content.slice(acIdx, nextMatch ? nextMatch.index : content.length);
2963
+ if (section.includes("[ESCALATE]")) {
2964
+ escalated++;
2965
+ } else if (hasFailVerdict(section)) {
2966
+ pending++;
2967
+ } else if (/```output\n/m.test(section)) {
2968
+ verified++;
2969
+ } else {
2970
+ pending++;
2971
+ }
2972
+ }
2973
+ }
2974
+ const total = verified + pending + escalated;
2975
+ return buildResult({ verified, pending, escalated, total });
2976
+ }
2977
+
2978
+ // src/modules/verify/orchestrator.ts
2979
+ import { execFileSync as execFileSync5 } from "child_process";
2980
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "fs";
2981
+ import { join as join11 } from "path";
2982
+
2983
+ // src/lib/doc-health.ts
2984
+ import { execSync } from "child_process";
2985
+ import {
2986
+ existsSync as existsSync12,
2987
+ mkdirSync as mkdirSync4,
2988
+ readFileSync as readFileSync11,
2989
+ readdirSync as readdirSync2,
2990
+ statSync,
2991
+ unlinkSync,
2992
+ writeFileSync as writeFileSync7
2993
+ } from "fs";
2994
+ import { join as join10, relative } from "path";
2995
+ var DO_NOT_EDIT_HEADER2 = "<!-- DO NOT EDIT MANUALLY";
2996
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
2997
+ var DEFAULT_MODULE_THRESHOLD = 3;
2998
+ function findModules(dir, threshold) {
2999
+ const limit = threshold ?? DEFAULT_MODULE_THRESHOLD;
3000
+ const root = dir;
3001
+ const modules = [];
3002
+ function walk(current) {
3003
+ let entries;
3004
+ try {
3005
+ entries = readdirSync2(current);
3006
+ } catch {
3007
+ return;
3008
+ }
3009
+ const dirName = current.split("/").pop() ?? "";
3010
+ if (dirName === "node_modules" || dirName === ".git" || dirName === "dist" || dirName === "coverage" || dirName.startsWith(".") && current !== root) {
3011
+ return;
3012
+ }
3013
+ let sourceCount = 0;
3014
+ const subdirs = [];
3015
+ for (const entry of entries) {
3016
+ const fullPath = join10(current, entry);
3017
+ let stat;
3018
+ try {
3019
+ stat = statSync(fullPath);
3020
+ } catch {
3021
+ continue;
3022
+ }
3023
+ if (stat.isDirectory()) {
3024
+ subdirs.push(fullPath);
3025
+ } else if (stat.isFile()) {
3026
+ const ext = getExtension(entry);
3027
+ if (SOURCE_EXTENSIONS.has(ext) && !isTestFile(entry)) {
3028
+ sourceCount++;
3029
+ }
3030
+ }
3031
+ }
3032
+ if (sourceCount >= limit) {
3033
+ const rel = relative(root, current);
3034
+ if (rel !== "") {
3035
+ modules.push(rel);
3036
+ }
3037
+ }
3038
+ for (const subdir of subdirs) {
3039
+ walk(subdir);
3040
+ }
3041
+ }
3042
+ walk(root);
3043
+ return modules;
3044
+ }
3045
+ function getExtension(filename) {
3046
+ const dot = filename.lastIndexOf(".");
3047
+ return dot >= 0 ? filename.slice(dot) : "";
3048
+ }
3049
+ function isTestFile(filename) {
3050
+ return filename.includes(".test.") || filename.includes(".spec.") || filename.includes("__tests__") || filename.startsWith("test_");
3051
+ }
3052
+ function isDocStale(docPath, codeDir) {
3053
+ if (!existsSync12(docPath)) return true;
3054
+ if (!existsSync12(codeDir)) return false;
3055
+ const docMtime = statSync(docPath).mtime;
3056
+ const newestCode = getNewestSourceMtime(codeDir);
2950
3057
  if (newestCode === null) return false;
2951
3058
  return newestCode.getTime() > docMtime.getTime();
2952
3059
  }
@@ -3408,89 +3515,15 @@ function showboatProofTemplate(config) {
3408
3515
  return sections.join("\n");
3409
3516
  }
3410
3517
 
3411
- // src/lib/verify.ts
3412
- function classifyEvidenceCommands(proofContent) {
3413
- const results = [];
3414
- const codeBlockPattern = /```(?:bash|shell)\n([\s\S]*?)```/g;
3415
- for (const match of proofContent.matchAll(codeBlockPattern)) {
3416
- const block = match[1].trim();
3417
- const lines = block.split("\n").filter((l) => l.trim().length > 0);
3418
- for (const line of lines) {
3419
- const cmd = line.trim();
3420
- if (!cmd) continue;
3421
- results.push({ command: cmd, type: classifyCommand(cmd) });
3422
- }
3423
- }
3424
- return results;
3425
- }
3426
- function classifyCommand(cmd) {
3427
- if (/docker\s+exec\b/.test(cmd)) {
3428
- return "docker-exec";
3429
- }
3430
- if (/docker\s+(ps|logs|inspect|stats|top|port)\b/.test(cmd)) {
3431
- return "docker-host";
3432
- }
3433
- if (/curl\b/.test(cmd) && /localhost:(9428|8428|16686)\b/.test(cmd)) {
3434
- return "observability";
3518
+ // src/modules/verify/orchestrator.ts
3519
+ function checkPreconditions(dir, storyId) {
3520
+ const state = readState(dir);
3521
+ const failures = [];
3522
+ if (!state.session_flags.tests_passed) {
3523
+ failures.push("tests_passed is false \u2014 run tests first");
3435
3524
  }
3436
- if (/\bgrep\b/.test(cmd) && /\bsrc\//.test(cmd)) {
3437
- return "grep-src";
3438
- }
3439
- return "other";
3440
- }
3441
- function checkBlackBoxEnforcement(proofContent) {
3442
- const commands = classifyEvidenceCommands(proofContent);
3443
- const grepSrcCount = commands.filter((c) => c.type === "grep-src").length;
3444
- const dockerExecCount = commands.filter((c) => c.type === "docker-exec").length;
3445
- const observabilityCount = commands.filter((c) => c.type === "observability").length;
3446
- const otherCount = commands.filter((c) => c.type === "other").length;
3447
- const totalCommands = commands.length;
3448
- const grepRatio = totalCommands > 0 ? grepSrcCount / totalCommands : 0;
3449
- const acsMissingDockerExec = [];
3450
- const acHeaderPattern = /^## AC ?(\d+):/gm;
3451
- const acMatches = [...proofContent.matchAll(acHeaderPattern)];
3452
- if (acMatches.length > 0) {
3453
- for (let i = 0; i < acMatches.length; i++) {
3454
- const acNum = parseInt(acMatches[i][1], 10);
3455
- const start = acMatches[i].index;
3456
- const end = i + 1 < acMatches.length ? acMatches[i + 1].index : proofContent.length;
3457
- const section = proofContent.slice(start, end);
3458
- if (section.includes("[ESCALATE]")) continue;
3459
- const sectionCommands = classifyEvidenceCommands(section);
3460
- const hasBlackBoxEvidence = sectionCommands.some(
3461
- (c) => c.type === "docker-exec" || c.type === "docker-host" || c.type === "observability"
3462
- );
3463
- if (!hasBlackBoxEvidence) {
3464
- acsMissingDockerExec.push(acNum);
3465
- }
3466
- }
3467
- }
3468
- const grepTooHigh = grepRatio > 0.5;
3469
- const missingDockerExec = acsMissingDockerExec.length > 0;
3470
- const hasExtractableCommands = totalCommands > 0;
3471
- return {
3472
- blackBoxPass: !hasExtractableCommands || !grepTooHigh && !missingDockerExec,
3473
- grepSrcCount,
3474
- dockerExecCount,
3475
- observabilityCount,
3476
- otherCount,
3477
- grepRatio,
3478
- acsMissingDockerExec
3479
- };
3480
- }
3481
- function hasFailVerdict(section) {
3482
- const withoutCodeBlocks = section.replace(/```[\s\S]*?```/g, "");
3483
- const withoutInlineCode = withoutCodeBlocks.replace(/`[^`]+`/g, "");
3484
- return withoutInlineCode.includes("[FAIL]");
3485
- }
3486
- function checkPreconditions(dir, storyId) {
3487
- const state = readState(dir);
3488
- const failures = [];
3489
- if (!state.session_flags.tests_passed) {
3490
- failures.push("tests_passed is false \u2014 run tests first");
3491
- }
3492
- if (!state.session_flags.coverage_met) {
3493
- failures.push("coverage_met is false \u2014 ensure coverage target is met");
3525
+ if (!state.session_flags.coverage_met) {
3526
+ failures.push("coverage_met is false \u2014 ensure coverage target is met");
3494
3527
  }
3495
3528
  if (storyId) {
3496
3529
  try {
@@ -3551,194 +3584,455 @@ function runShowboatVerify(proofPath) {
3551
3584
  return { passed: false, output: stdout || stderr || message };
3552
3585
  }
3553
3586
  }
3554
- function validateProofQuality(proofPath) {
3555
- const emptyResult = {
3556
- verified: 0,
3557
- pending: 0,
3558
- escalated: 0,
3559
- total: 0,
3560
- passed: false,
3561
- grepSrcCount: 0,
3562
- dockerExecCount: 0,
3563
- observabilityCount: 0,
3564
- otherCount: 0,
3565
- blackBoxPass: false
3566
- };
3567
- if (!existsSync13(proofPath)) {
3568
- return emptyResult;
3587
+ function updateVerificationState(storyId, result, dir) {
3588
+ const { state, body } = readStateWithBody(dir);
3589
+ state.session_flags.verification_run = true;
3590
+ const status = result.success ? "pass" : "fail";
3591
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3592
+ state.verification_log.push(`${storyId}: ${status} at ${timestamp}`);
3593
+ writeState(state, dir, body);
3594
+ }
3595
+ function closeBeadsIssue(storyId, dir) {
3596
+ const root = dir ?? process.cwd();
3597
+ if (!isBeadsInitialized(root)) {
3598
+ warn("Beads not initialized \u2014 skipping issue close");
3599
+ return;
3569
3600
  }
3570
- const content = readFileSync12(proofPath, "utf-8");
3571
- const bbEnforcement = checkBlackBoxEnforcement(content);
3572
- function buildResult(base) {
3573
- const basePassed = base.pending === 0 && base.verified > 0;
3574
- return {
3575
- ...base,
3576
- passed: basePassed && bbEnforcement.blackBoxPass,
3577
- grepSrcCount: bbEnforcement.grepSrcCount,
3578
- dockerExecCount: bbEnforcement.dockerExecCount,
3579
- observabilityCount: bbEnforcement.observabilityCount,
3580
- otherCount: bbEnforcement.otherCount,
3581
- blackBoxPass: bbEnforcement.blackBoxPass
3582
- };
3601
+ try {
3602
+ const issues = listIssues();
3603
+ const issue = issues.find((i) => {
3604
+ const desc = i.description ?? "";
3605
+ return desc.includes(storyId);
3606
+ });
3607
+ if (!issue) {
3608
+ warn(`No beads issue found for story ${storyId} \u2014 skipping issue close`);
3609
+ return;
3610
+ }
3611
+ syncClose(issue.id, { closeIssue, listIssues }, root);
3612
+ } catch (err) {
3613
+ const message = err instanceof Error ? err.message : String(err);
3614
+ warn(`Failed to close beads issue: ${message}`);
3583
3615
  }
3584
- const acHeaderPattern = /^## AC ?(\d+):/gm;
3585
- const matches = [...content.matchAll(acHeaderPattern)];
3586
- let verified = 0;
3587
- let pending = 0;
3588
- let escalated = 0;
3589
- if (matches.length > 0) {
3590
- for (let i = 0; i < matches.length; i++) {
3591
- const start = matches[i].index;
3592
- const end = i + 1 < matches.length ? matches[i + 1].index : content.length;
3593
- const section = content.slice(start, end);
3594
- if (section.includes("[ESCALATE]")) {
3595
- escalated++;
3596
- continue;
3597
- }
3598
- if (hasFailVerdict(section)) {
3599
- pending++;
3600
- continue;
3601
- }
3602
- const hasEvidence = section.includes("<!-- /showboat exec -->") || section.includes("<!-- showboat image:") || /```(?:bash|shell)\n[\s\S]*?```\n+```output\n/m.test(section);
3603
- if (hasEvidence) {
3604
- verified++;
3605
- } else {
3606
- pending++;
3607
- }
3616
+ }
3617
+
3618
+ // src/modules/verify/parser.ts
3619
+ import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
3620
+ var UI_KEYWORDS = [
3621
+ "agent-browser",
3622
+ "screenshot",
3623
+ "navigate",
3624
+ "click",
3625
+ "form",
3626
+ "ui verification",
3627
+ "ui acceptance"
3628
+ ];
3629
+ var API_KEYWORDS = [
3630
+ "http",
3631
+ "api",
3632
+ "endpoint",
3633
+ "curl",
3634
+ "rest",
3635
+ "response bod"
3636
+ ];
3637
+ var DB_KEYWORDS = [
3638
+ "database",
3639
+ "db state",
3640
+ "db mcp",
3641
+ "query",
3642
+ "sql",
3643
+ "table"
3644
+ ];
3645
+ var INTEGRATION_KEYWORDS = [
3646
+ "external system",
3647
+ "real infrastructure",
3648
+ "manual verification"
3649
+ ];
3650
+ var ESCALATE_KEYWORDS = [
3651
+ "physical hardware",
3652
+ "manual human",
3653
+ "visual inspection by human",
3654
+ "paid external service"
3655
+ ];
3656
+ function classifyVerifiability(description) {
3657
+ const lower = description.toLowerCase();
3658
+ for (const kw of INTEGRATION_KEYWORDS) {
3659
+ if (lower.includes(kw)) return "integration-required";
3660
+ }
3661
+ return "cli-verifiable";
3662
+ }
3663
+ function classifyStrategy(description) {
3664
+ const lower = description.toLowerCase();
3665
+ for (const kw of ESCALATE_KEYWORDS) {
3666
+ if (lower.includes(kw)) return "escalate";
3667
+ }
3668
+ return "docker";
3669
+ }
3670
+ var VERIFICATION_TAG_PATTERN = /<!--\s*verification:\s*(cli-verifiable|integration-required)\s*-->/;
3671
+ function parseVerificationTag(text) {
3672
+ const match = VERIFICATION_TAG_PATTERN.exec(text);
3673
+ return match ? match[1] : null;
3674
+ }
3675
+ function classifyAC(description) {
3676
+ const lower = description.toLowerCase();
3677
+ for (const kw of UI_KEYWORDS) {
3678
+ if (lower.includes(kw)) return "ui";
3679
+ }
3680
+ for (const kw of API_KEYWORDS) {
3681
+ if (lower.includes(kw)) return "api";
3682
+ }
3683
+ for (const kw of DB_KEYWORDS) {
3684
+ if (lower.includes(kw)) return "db";
3685
+ }
3686
+ return "general";
3687
+ }
3688
+ function parseStoryACs(storyFilePath) {
3689
+ if (!existsSync14(storyFilePath)) {
3690
+ throw new Error(
3691
+ `Story file not found: ${storyFilePath}. Ensure the story file exists at the expected path.`
3692
+ );
3693
+ }
3694
+ const content = readFileSync12(storyFilePath, "utf-8");
3695
+ const lines = content.split("\n");
3696
+ let acSectionStart = -1;
3697
+ for (let i = 0; i < lines.length; i++) {
3698
+ if (/^##\s+Acceptance\s+Criteria/i.test(lines[i])) {
3699
+ acSectionStart = i + 1;
3700
+ break;
3608
3701
  }
3609
- } else {
3610
- const inlineAcPattern = /--- AC ?(\d+):/g;
3611
- const inlineMatches = [...content.matchAll(inlineAcPattern)];
3612
- const acNumbers = new Set(inlineMatches.map((m) => m[1]));
3613
- if (acNumbers.size === 0) {
3614
- const narrativeAcPattern = /=== AC ?(\d+):/g;
3615
- const narrativeMatches = [...content.matchAll(narrativeAcPattern)];
3616
- const narrativeAcNumbers = new Set(narrativeMatches.map((m) => m[1]));
3617
- if (narrativeAcNumbers.size === 0) {
3618
- const bulletAcPattern = /^- AC ?(\d+)[^:\n]*:/gm;
3619
- const bulletMatches = [...content.matchAll(bulletAcPattern)];
3620
- const bulletAcNumbers = new Set(bulletMatches.map((m) => m[1]));
3621
- if (bulletAcNumbers.size === 0) {
3622
- return buildResult({ verified: 0, pending: 0, escalated: 0, total: 0 });
3623
- }
3624
- let bVerified = 0;
3625
- let bPending = 0;
3626
- let bEscalated = 0;
3627
- for (const acNum of bulletAcNumbers) {
3628
- const bulletPattern = new RegExp(`^- AC ?${acNum}[^:\\n]*:(.*)$`, "m");
3629
- const bulletMatch = content.match(bulletPattern);
3630
- if (!bulletMatch) {
3631
- bPending++;
3632
- continue;
3633
- }
3634
- const bulletText = bulletMatch[1].toLowerCase();
3635
- if (bulletText.includes("n/a") || bulletText.includes("escalat") || bulletText.includes("superseded")) {
3636
- bEscalated++;
3637
- } else if (bulletText.includes("fail")) {
3638
- bPending++;
3639
- } else {
3640
- bVerified++;
3641
- }
3642
- }
3643
- const hasAnyEvidence = /```output\n/m.test(content);
3644
- if (!hasAnyEvidence) {
3645
- bPending += bVerified;
3646
- bVerified = 0;
3647
- }
3648
- const bTotal = bVerified + bPending + bEscalated;
3649
- return buildResult({
3650
- verified: bVerified,
3651
- pending: bPending,
3652
- escalated: bEscalated,
3653
- total: bTotal
3702
+ }
3703
+ if (acSectionStart === -1) {
3704
+ warn('No "## Acceptance Criteria" section found in story file');
3705
+ return [];
3706
+ }
3707
+ let acSectionEnd = lines.length;
3708
+ for (let i = acSectionStart; i < lines.length; i++) {
3709
+ if (/^##\s+/.test(lines[i]) && i !== acSectionStart - 1) {
3710
+ acSectionEnd = i;
3711
+ break;
3712
+ }
3713
+ }
3714
+ const acSection = lines.slice(acSectionStart, acSectionEnd).join("\n");
3715
+ const acPattern = /^\s*(\d+)\.\s+/;
3716
+ const acs = [];
3717
+ const acLines = acSection.split("\n");
3718
+ let currentId = null;
3719
+ let currentDesc = [];
3720
+ const flushCurrent = () => {
3721
+ if (currentId !== null && currentDesc.length > 0) {
3722
+ const description = currentDesc.join(" ").trim();
3723
+ if (description) {
3724
+ const tag = parseVerificationTag(description);
3725
+ const verifiability = tag ?? classifyVerifiability(description);
3726
+ const strategy = classifyStrategy(description);
3727
+ acs.push({
3728
+ id: currentId,
3729
+ description,
3730
+ type: classifyAC(description),
3731
+ verifiability,
3732
+ strategy
3654
3733
  });
3734
+ } else {
3735
+ warn(`Skipping malformed AC #${currentId}: empty description`);
3655
3736
  }
3656
- const sortedAcs = narrativeMatches.map((m) => ({ num: m[1], idx: m.index })).filter((v, i, a) => a.findIndex((x) => x.num === v.num) === i).sort((a, b) => a.idx - b.idx);
3657
- for (let i = 0; i < sortedAcs.length; i++) {
3658
- const { num: acNum, idx: acIdx } = sortedAcs[i];
3659
- const regionStart = i > 0 ? sortedAcs[i - 1].idx : 0;
3660
- const regionEnd = i + 1 < sortedAcs.length ? sortedAcs[i + 1].idx : content.length;
3661
- const section = content.slice(regionStart, regionEnd);
3662
- if (section.includes("[ESCALATE]")) {
3663
- escalated++;
3664
- } else if (hasFailVerdict(section)) {
3665
- pending++;
3666
- } else if (/```output/m.test(section)) {
3667
- verified++;
3668
- } else {
3669
- pending++;
3670
- }
3671
- }
3672
- const narrativeTotal = verified + pending + escalated;
3673
- return buildResult({
3674
- verified,
3675
- pending,
3676
- escalated,
3677
- total: narrativeTotal
3678
- });
3679
3737
  }
3680
- for (const acNum of acNumbers) {
3681
- const acPattern = new RegExp(`--- AC ?${acNum}:`, "g");
3682
- const acIdx = content.search(acPattern);
3683
- if (acIdx === -1) {
3684
- pending++;
3685
- continue;
3686
- }
3687
- const nextAcPattern = new RegExp(`--- AC ?(?!${acNum})\\d+:`, "g");
3688
- nextAcPattern.lastIndex = acIdx + 1;
3689
- const nextMatch = nextAcPattern.exec(content);
3690
- const section = content.slice(acIdx, nextMatch ? nextMatch.index : content.length);
3691
- if (section.includes("[ESCALATE]")) {
3692
- escalated++;
3693
- } else if (hasFailVerdict(section)) {
3694
- pending++;
3695
- } else if (/```output\n/m.test(section)) {
3696
- verified++;
3697
- } else {
3698
- pending++;
3738
+ };
3739
+ for (const line of acLines) {
3740
+ const match = acPattern.exec(line);
3741
+ if (match) {
3742
+ flushCurrent();
3743
+ currentId = match[1];
3744
+ currentDesc = [line.replace(acPattern, "").trim()];
3745
+ } else if (currentId !== null && line.trim()) {
3746
+ currentDesc.push(line.trim());
3747
+ }
3748
+ }
3749
+ flushCurrent();
3750
+ return acs;
3751
+ }
3752
+
3753
+ // src/modules/verify/env.ts
3754
+ import { execFileSync as execFileSync6 } from "child_process";
3755
+ import { existsSync as existsSync15, mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync13, cpSync, rmSync, statSync as statSync2 } from "fs";
3756
+ import { join as join12, basename as basename3 } from "path";
3757
+ import { createHash } from "crypto";
3758
+ var IMAGE_TAG = "codeharness-verify";
3759
+ var STORY_DIR = "_bmad-output/implementation-artifacts";
3760
+ var TEMP_PREFIX = "/tmp/codeharness-verify-";
3761
+ var STATE_KEY_DIST_HASH = "verify_env_dist_hash";
3762
+ function isValidStoryKey(storyKey) {
3763
+ if (!storyKey || storyKey.includes("..") || storyKey.includes("/") || storyKey.includes("\\")) {
3764
+ return false;
3765
+ }
3766
+ return /^[a-zA-Z0-9_-]+$/.test(storyKey);
3767
+ }
3768
+ function computeDistHash(projectDir) {
3769
+ const distDir = join12(projectDir, "dist");
3770
+ if (!existsSync15(distDir)) return null;
3771
+ const hash = createHash("sha256");
3772
+ const files = collectFiles(distDir).sort();
3773
+ for (const file of files) {
3774
+ hash.update(file.slice(distDir.length));
3775
+ hash.update(readFileSync13(file));
3776
+ }
3777
+ return hash.digest("hex");
3778
+ }
3779
+ function collectFiles(dir) {
3780
+ const results = [];
3781
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
3782
+ const fullPath = join12(dir, entry.name);
3783
+ if (entry.isDirectory()) {
3784
+ results.push(...collectFiles(fullPath));
3785
+ } else {
3786
+ results.push(fullPath);
3787
+ }
3788
+ }
3789
+ return results;
3790
+ }
3791
+ function getStoredDistHash(projectDir) {
3792
+ try {
3793
+ const { state } = readStateWithBody(projectDir);
3794
+ return state[STATE_KEY_DIST_HASH] ?? null;
3795
+ } catch {
3796
+ return null;
3797
+ }
3798
+ }
3799
+ function storeDistHash(projectDir, hash) {
3800
+ try {
3801
+ const { state, body } = readStateWithBody(projectDir);
3802
+ state[STATE_KEY_DIST_HASH] = hash;
3803
+ writeState(state, projectDir, body);
3804
+ } catch {
3805
+ info("Could not persist dist hash to state file \u2014 cache will not be available until state is initialized");
3806
+ }
3807
+ }
3808
+ function detectProjectType(projectDir) {
3809
+ if (existsSync15(join12(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
3810
+ const stack = detectStack(projectDir);
3811
+ if (stack === "nodejs") return "nodejs";
3812
+ if (stack === "python") return "python";
3813
+ return "generic";
3814
+ }
3815
+ function buildVerifyImage(options = {}) {
3816
+ const projectDir = options.projectDir ?? process.cwd();
3817
+ if (!isDockerAvailable()) {
3818
+ throw new Error("Docker is not available. Install Docker and ensure the daemon is running.");
3819
+ }
3820
+ const projectType = detectProjectType(projectDir);
3821
+ const currentHash = computeDistHash(projectDir);
3822
+ if (projectType === "generic" || projectType === "plugin") {
3823
+ } else if (!currentHash) {
3824
+ throw new Error("No dist/ directory found. Run your build command first (e.g., npm run build).");
3825
+ }
3826
+ if (currentHash) {
3827
+ const storedHash = getStoredDistHash(projectDir);
3828
+ if (storedHash === currentHash && dockerImageExists(IMAGE_TAG)) {
3829
+ return { imageTag: IMAGE_TAG, imageSize: getImageSize(IMAGE_TAG), buildTimeMs: 0, cached: true };
3830
+ }
3831
+ }
3832
+ const startTime = Date.now();
3833
+ if (projectType === "nodejs") {
3834
+ buildNodeImage(projectDir);
3835
+ } else if (projectType === "python") {
3836
+ buildPythonImage(projectDir);
3837
+ } else if (projectType === "plugin") {
3838
+ buildPluginImage(projectDir);
3839
+ } else {
3840
+ buildGenericImage(projectDir);
3841
+ }
3842
+ if (currentHash) {
3843
+ storeDistHash(projectDir, currentHash);
3844
+ }
3845
+ return { imageTag: IMAGE_TAG, imageSize: getImageSize(IMAGE_TAG), buildTimeMs: Date.now() - startTime, cached: false };
3846
+ }
3847
+ function buildNodeImage(projectDir) {
3848
+ const packOutput = execFileSync6("npm", ["pack", "--pack-destination", "/tmp"], {
3849
+ cwd: projectDir,
3850
+ stdio: "pipe",
3851
+ timeout: 6e4
3852
+ }).toString().trim();
3853
+ const lastLine = packOutput.split("\n").pop()?.trim();
3854
+ if (!lastLine) throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
3855
+ const tarballName = basename3(lastLine);
3856
+ const tarballPath = join12("/tmp", tarballName);
3857
+ const buildContext = join12("/tmp", `codeharness-verify-build-${Date.now()}`);
3858
+ mkdirSync6(buildContext, { recursive: true });
3859
+ try {
3860
+ cpSync(tarballPath, join12(buildContext, tarballName));
3861
+ cpSync(resolveDockerfileTemplate(projectDir), join12(buildContext, "Dockerfile"));
3862
+ execFileSync6("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${tarballName}`, "."], {
3863
+ cwd: buildContext,
3864
+ stdio: "pipe",
3865
+ timeout: 12e4
3866
+ });
3867
+ } finally {
3868
+ rmSync(buildContext, { recursive: true, force: true });
3869
+ rmSync(tarballPath, { force: true });
3870
+ }
3871
+ }
3872
+ function buildPythonImage(projectDir) {
3873
+ const distDir = join12(projectDir, "dist");
3874
+ const distFiles = readdirSync3(distDir).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".whl"));
3875
+ if (distFiles.length === 0) {
3876
+ throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
3877
+ }
3878
+ const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
3879
+ const buildContext = join12("/tmp", `codeharness-verify-build-${Date.now()}`);
3880
+ mkdirSync6(buildContext, { recursive: true });
3881
+ try {
3882
+ cpSync(join12(distDir, distFile), join12(buildContext, distFile));
3883
+ cpSync(resolveDockerfileTemplate(projectDir), join12(buildContext, "Dockerfile"));
3884
+ execFileSync6("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${distFile}`, "."], {
3885
+ cwd: buildContext,
3886
+ stdio: "pipe",
3887
+ timeout: 12e4
3888
+ });
3889
+ } finally {
3890
+ rmSync(buildContext, { recursive: true, force: true });
3891
+ }
3892
+ }
3893
+ function prepareVerifyWorkspace(storyKey, projectDir) {
3894
+ const root = projectDir ?? process.cwd();
3895
+ if (!isValidStoryKey(storyKey)) {
3896
+ throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
3897
+ }
3898
+ const storyFile = join12(root, STORY_DIR, `${storyKey}.md`);
3899
+ if (!existsSync15(storyFile)) throw new Error(`Story file not found: ${storyFile}`);
3900
+ const workspace = `${TEMP_PREFIX}${storyKey}`;
3901
+ if (existsSync15(workspace)) rmSync(workspace, { recursive: true, force: true });
3902
+ mkdirSync6(workspace, { recursive: true });
3903
+ cpSync(storyFile, join12(workspace, "story.md"));
3904
+ const readmePath = join12(root, "README.md");
3905
+ if (existsSync15(readmePath)) cpSync(readmePath, join12(workspace, "README.md"));
3906
+ const docsDir = join12(root, "docs");
3907
+ if (existsSync15(docsDir) && statSync2(docsDir).isDirectory()) {
3908
+ cpSync(docsDir, join12(workspace, "docs"), { recursive: true });
3909
+ }
3910
+ mkdirSync6(join12(workspace, "verification"), { recursive: true });
3911
+ return workspace;
3912
+ }
3913
+ function checkVerifyEnv() {
3914
+ let imageExists = false;
3915
+ let cliWorks = false;
3916
+ let otelReachable = false;
3917
+ imageExists = dockerImageExists(IMAGE_TAG);
3918
+ if (!imageExists) return { imageExists, cliWorks, otelReachable };
3919
+ try {
3920
+ execFileSync6("docker", ["run", "--rm", IMAGE_TAG, "codeharness", "--help"], {
3921
+ stdio: "pipe",
3922
+ timeout: 3e4
3923
+ });
3924
+ cliWorks = true;
3925
+ } catch {
3926
+ cliWorks = false;
3927
+ }
3928
+ try {
3929
+ execFileSync6("docker", [
3930
+ "run",
3931
+ "--rm",
3932
+ "--add-host=host.docker.internal:host-gateway",
3933
+ IMAGE_TAG,
3934
+ "curl",
3935
+ "-sf",
3936
+ "--max-time",
3937
+ "5",
3938
+ "http://host.docker.internal:4318/v1/status"
3939
+ ], { stdio: "pipe", timeout: 3e4 });
3940
+ otelReachable = true;
3941
+ } catch {
3942
+ otelReachable = false;
3943
+ }
3944
+ return { imageExists, cliWorks, otelReachable };
3945
+ }
3946
+ function cleanupVerifyEnv(storyKey) {
3947
+ if (!isValidStoryKey(storyKey)) {
3948
+ throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
3949
+ }
3950
+ const workspace = `${TEMP_PREFIX}${storyKey}`;
3951
+ const containerName = `codeharness-verify-${storyKey}`;
3952
+ if (existsSync15(workspace)) rmSync(workspace, { recursive: true, force: true });
3953
+ try {
3954
+ execFileSync6("docker", ["stop", containerName], { stdio: "pipe", timeout: 15e3 });
3955
+ } catch {
3956
+ }
3957
+ try {
3958
+ execFileSync6("docker", ["rm", "-f", containerName], { stdio: "pipe", timeout: 15e3 });
3959
+ } catch {
3960
+ }
3961
+ }
3962
+ function buildPluginImage(projectDir) {
3963
+ const buildContext = join12("/tmp", `codeharness-verify-build-${Date.now()}`);
3964
+ mkdirSync6(buildContext, { recursive: true });
3965
+ try {
3966
+ const pluginDir = join12(projectDir, ".claude-plugin");
3967
+ cpSync(pluginDir, join12(buildContext, ".claude-plugin"), { recursive: true });
3968
+ for (const dir of ["commands", "hooks", "knowledge", "skills"]) {
3969
+ const src = join12(projectDir, dir);
3970
+ if (existsSync15(src) && statSync2(src).isDirectory()) {
3971
+ cpSync(src, join12(buildContext, dir), { recursive: true });
3699
3972
  }
3700
3973
  }
3974
+ cpSync(resolveDockerfileTemplate(projectDir, "generic"), join12(buildContext, "Dockerfile"));
3975
+ execFileSync6("docker", ["build", "-t", IMAGE_TAG, "."], {
3976
+ cwd: buildContext,
3977
+ stdio: "pipe",
3978
+ timeout: 12e4
3979
+ });
3980
+ } finally {
3981
+ rmSync(buildContext, { recursive: true, force: true });
3982
+ }
3983
+ }
3984
+ function buildGenericImage(projectDir) {
3985
+ const buildContext = join12("/tmp", `codeharness-verify-build-${Date.now()}`);
3986
+ mkdirSync6(buildContext, { recursive: true });
3987
+ try {
3988
+ cpSync(resolveDockerfileTemplate(projectDir, "generic"), join12(buildContext, "Dockerfile"));
3989
+ execFileSync6("docker", ["build", "-t", IMAGE_TAG, "."], {
3990
+ cwd: buildContext,
3991
+ stdio: "pipe",
3992
+ timeout: 12e4
3993
+ });
3994
+ } finally {
3995
+ rmSync(buildContext, { recursive: true, force: true });
3701
3996
  }
3702
- const total = verified + pending + escalated;
3703
- return buildResult({ verified, pending, escalated, total });
3704
3997
  }
3705
- function updateVerificationState(storyId, result, dir) {
3706
- const { state, body } = readStateWithBody(dir);
3707
- state.session_flags.verification_run = true;
3708
- const status = result.success ? "pass" : "fail";
3709
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3710
- state.verification_log.push(`${storyId}: ${status} at ${timestamp}`);
3711
- writeState(state, dir, body);
3998
+ function resolveDockerfileTemplate(projectDir, variant) {
3999
+ const filename = variant === "generic" ? "Dockerfile.verify.generic" : "Dockerfile.verify";
4000
+ const local = join12(projectDir, "templates", filename);
4001
+ if (existsSync15(local)) return local;
4002
+ const pkgDir = new URL("../../", import.meta.url).pathname;
4003
+ const pkg = join12(pkgDir, "templates", filename);
4004
+ if (existsSync15(pkg)) return pkg;
4005
+ throw new Error(`${filename} not found. Ensure templates/${filename} exists.`);
3712
4006
  }
3713
- function closeBeadsIssue(storyId, dir) {
3714
- const root = dir ?? process.cwd();
3715
- if (!isBeadsInitialized(root)) {
3716
- warn("Beads not initialized \u2014 skipping issue close");
3717
- return;
4007
+ function dockerImageExists(tag) {
4008
+ try {
4009
+ execFileSync6("docker", ["image", "inspect", tag], { stdio: "pipe", timeout: 1e4 });
4010
+ return true;
4011
+ } catch {
4012
+ return false;
3718
4013
  }
4014
+ }
4015
+ function getImageSize(tag) {
3719
4016
  try {
3720
- const issues = listIssues();
3721
- const issue = issues.find((i) => {
3722
- const desc = i.description ?? "";
3723
- return desc.includes(storyId);
3724
- });
3725
- if (!issue) {
3726
- warn(`No beads issue found for story ${storyId} \u2014 skipping issue close`);
3727
- return;
3728
- }
3729
- syncClose(issue.id, { closeIssue, listIssues }, root);
3730
- } catch (err) {
3731
- const message = err instanceof Error ? err.message : String(err);
3732
- warn(`Failed to close beads issue: ${message}`);
4017
+ const output = execFileSync6("docker", ["image", "inspect", tag, "--format", "{{.Size}}"], {
4018
+ stdio: "pipe",
4019
+ timeout: 1e4
4020
+ }).toString().trim();
4021
+ const bytes = parseInt(output, 10);
4022
+ if (isNaN(bytes)) return output;
4023
+ if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)}GB`;
4024
+ if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(1)}MB`;
4025
+ if (bytes >= 1e3) return `${(bytes / 1e3).toFixed(1)}KB`;
4026
+ return `${bytes}B`;
4027
+ } catch {
4028
+ return "unknown";
3733
4029
  }
3734
4030
  }
3735
4031
 
3736
4032
  // src/commands/verify.ts
3737
- var STORY_DIR = "_bmad-output/implementation-artifacts";
4033
+ var STORY_DIR2 = "_bmad-output/implementation-artifacts";
3738
4034
  function isValidStoryId(storyId) {
3739
- if (!storyId || storyId.includes("..") || storyId.includes("/") || storyId.includes("\\")) {
3740
- return false;
3741
- }
4035
+ if (!storyId || storyId.includes("..") || storyId.includes("/") || storyId.includes("\\")) return false;
3742
4036
  return /^[a-zA-Z0-9_-]+$/.test(storyId);
3743
4037
  }
3744
4038
  function registerVerifyCommand(program) {
@@ -3771,8 +4065,8 @@ function verifyRetro(opts, isJson, root) {
3771
4065
  return;
3772
4066
  }
3773
4067
  const retroFile = `epic-${epicNum}-retrospective.md`;
3774
- const retroPath = join12(root, STORY_DIR, retroFile);
3775
- if (!existsSync14(retroPath)) {
4068
+ const retroPath = join13(root, STORY_DIR2, retroFile);
4069
+ if (!existsSync16(retroPath)) {
3776
4070
  if (isJson) {
3777
4071
  jsonOutput({ status: "fail", epic: epicNum, retroFile, message: `${retroFile} not found` });
3778
4072
  } else {
@@ -3789,7 +4083,7 @@ function verifyRetro(opts, isJson, root) {
3789
4083
  warn(`Failed to update sprint status: ${message}`);
3790
4084
  }
3791
4085
  if (isJson) {
3792
- jsonOutput({ status: "ok", epic: epicNum, retroFile: join12(STORY_DIR, retroFile) });
4086
+ jsonOutput({ status: "ok", epic: epicNum, retroFile: join13(STORY_DIR2, retroFile) });
3793
4087
  } else {
3794
4088
  ok(`Epic ${epicNum} retrospective: marked done`);
3795
4089
  }
@@ -3800,8 +4094,8 @@ function verifyStory(storyId, isJson, root) {
3800
4094
  process.exitCode = 1;
3801
4095
  return;
3802
4096
  }
3803
- const readmePath = join12(root, "README.md");
3804
- if (!existsSync14(readmePath)) {
4097
+ const readmePath = join13(root, "README.md");
4098
+ if (!existsSync16(readmePath)) {
3805
4099
  if (isJson) {
3806
4100
  jsonOutput({ status: "fail", message: "No README.md found \u2014 verification requires user documentation" });
3807
4101
  } else {
@@ -3810,8 +4104,8 @@ function verifyStory(storyId, isJson, root) {
3810
4104
  process.exitCode = 1;
3811
4105
  return;
3812
4106
  }
3813
- const storyFilePath = join12(root, STORY_DIR, `${storyId}.md`);
3814
- if (!existsSync14(storyFilePath)) {
4107
+ const storyFilePath = join13(root, STORY_DIR2, `${storyId}.md`);
4108
+ if (!existsSync16(storyFilePath)) {
3815
4109
  fail(`Story file not found: ${storyFilePath}`, { json: isJson });
3816
4110
  process.exitCode = 1;
3817
4111
  return;
@@ -3851,8 +4145,8 @@ function verifyStory(storyId, isJson, root) {
3851
4145
  return;
3852
4146
  }
3853
4147
  const storyTitle = extractStoryTitle(storyFilePath);
3854
- const expectedProofPath = join12(root, "verification", `${storyId}-proof.md`);
3855
- const proofPath = existsSync14(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
4148
+ const expectedProofPath = join13(root, "verification", `${storyId}-proof.md`);
4149
+ const proofPath = existsSync16(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
3856
4150
  const proofQuality = validateProofQuality(proofPath);
3857
4151
  if (!proofQuality.passed) {
3858
4152
  if (isJson) {
@@ -3946,7 +4240,7 @@ function verifyStory(storyId, isJson, root) {
3946
4240
  }
3947
4241
  function extractStoryTitle(filePath) {
3948
4242
  try {
3949
- const content = readFileSync13(filePath, "utf-8");
4243
+ const content = readFileSync14(filePath, "utf-8");
3950
4244
  const match = /^#\s+(.+)$/m.exec(content);
3951
4245
  return match ? match[1] : "Unknown Story";
3952
4246
  } catch {
@@ -3955,14 +4249,14 @@ function extractStoryTitle(filePath) {
3955
4249
  }
3956
4250
 
3957
4251
  // src/lib/onboard-checks.ts
3958
- import { existsSync as existsSync16 } from "fs";
3959
- import { join as join14, dirname as dirname4 } from "path";
4252
+ import { existsSync as existsSync18 } from "fs";
4253
+ import { join as join15, dirname as dirname4 } from "path";
3960
4254
  import { fileURLToPath as fileURLToPath3 } from "url";
3961
4255
 
3962
4256
  // src/lib/coverage.ts
3963
4257
  import { execSync as execSync2 } from "child_process";
3964
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
3965
- import { join as join13 } from "path";
4258
+ import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
4259
+ import { join as join14 } from "path";
3966
4260
  function detectCoverageTool(dir) {
3967
4261
  const baseDir = dir ?? process.cwd();
3968
4262
  const stateHint = getStateToolHint(baseDir);
@@ -3985,16 +4279,16 @@ function getStateToolHint(dir) {
3985
4279
  }
3986
4280
  }
3987
4281
  function detectNodeCoverageTool(dir, stateHint) {
3988
- const hasVitestConfig = existsSync15(join13(dir, "vitest.config.ts")) || existsSync15(join13(dir, "vitest.config.js"));
3989
- const pkgPath = join13(dir, "package.json");
4282
+ const hasVitestConfig = existsSync17(join14(dir, "vitest.config.ts")) || existsSync17(join14(dir, "vitest.config.js"));
4283
+ const pkgPath = join14(dir, "package.json");
3990
4284
  let hasVitestCoverageV8 = false;
3991
4285
  let hasVitestCoverageIstanbul = false;
3992
4286
  let hasC8 = false;
3993
4287
  let hasJest = false;
3994
4288
  let pkgScripts = {};
3995
- if (existsSync15(pkgPath)) {
4289
+ if (existsSync17(pkgPath)) {
3996
4290
  try {
3997
- const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
4291
+ const pkg = JSON.parse(readFileSync15(pkgPath, "utf-8"));
3998
4292
  const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
3999
4293
  hasVitestCoverageV8 = "@vitest/coverage-v8" in allDeps;
4000
4294
  hasVitestCoverageIstanbul = "@vitest/coverage-istanbul" in allDeps;
@@ -4047,10 +4341,10 @@ function getNodeTestCommand(scripts, runner) {
4047
4341
  return "npm test";
4048
4342
  }
4049
4343
  function detectPythonCoverageTool(dir) {
4050
- const reqPath = join13(dir, "requirements.txt");
4051
- if (existsSync15(reqPath)) {
4344
+ const reqPath = join14(dir, "requirements.txt");
4345
+ if (existsSync17(reqPath)) {
4052
4346
  try {
4053
- const content = readFileSync14(reqPath, "utf-8");
4347
+ const content = readFileSync15(reqPath, "utf-8");
4054
4348
  if (content.includes("pytest-cov") || content.includes("coverage")) {
4055
4349
  return {
4056
4350
  tool: "coverage.py",
@@ -4061,10 +4355,10 @@ function detectPythonCoverageTool(dir) {
4061
4355
  } catch {
4062
4356
  }
4063
4357
  }
4064
- const pyprojectPath = join13(dir, "pyproject.toml");
4065
- if (existsSync15(pyprojectPath)) {
4358
+ const pyprojectPath = join14(dir, "pyproject.toml");
4359
+ if (existsSync17(pyprojectPath)) {
4066
4360
  try {
4067
- const content = readFileSync14(pyprojectPath, "utf-8");
4361
+ const content = readFileSync15(pyprojectPath, "utf-8");
4068
4362
  if (content.includes("pytest-cov") || content.includes("coverage")) {
4069
4363
  return {
4070
4364
  tool: "coverage.py",
@@ -4146,7 +4440,7 @@ function parseVitestCoverage(dir) {
4146
4440
  return 0;
4147
4441
  }
4148
4442
  try {
4149
- const report = JSON.parse(readFileSync14(reportPath, "utf-8"));
4443
+ const report = JSON.parse(readFileSync15(reportPath, "utf-8"));
4150
4444
  return report.total?.statements?.pct ?? 0;
4151
4445
  } catch {
4152
4446
  warn("Failed to parse coverage report");
@@ -4154,13 +4448,13 @@ function parseVitestCoverage(dir) {
4154
4448
  }
4155
4449
  }
4156
4450
  function parsePythonCoverage(dir) {
4157
- const reportPath = join13(dir, "coverage.json");
4158
- if (!existsSync15(reportPath)) {
4451
+ const reportPath = join14(dir, "coverage.json");
4452
+ if (!existsSync17(reportPath)) {
4159
4453
  warn("Coverage report not found at coverage.json");
4160
4454
  return 0;
4161
4455
  }
4162
4456
  try {
4163
- const report = JSON.parse(readFileSync14(reportPath, "utf-8"));
4457
+ const report = JSON.parse(readFileSync15(reportPath, "utf-8"));
4164
4458
  return report.totals?.percent_covered ?? 0;
4165
4459
  } catch {
4166
4460
  warn("Failed to parse coverage report");
@@ -4261,7 +4555,7 @@ function checkPerFileCoverage(floor, dir) {
4261
4555
  }
4262
4556
  let report;
4263
4557
  try {
4264
- report = JSON.parse(readFileSync14(reportPath, "utf-8"));
4558
+ report = JSON.parse(readFileSync15(reportPath, "utf-8"));
4265
4559
  } catch {
4266
4560
  warn("Failed to parse coverage-summary.json");
4267
4561
  return { floor, violations: [], totalFiles: 0 };
@@ -4291,11 +4585,11 @@ function checkPerFileCoverage(floor, dir) {
4291
4585
  }
4292
4586
  function findCoverageSummary(dir) {
4293
4587
  const candidates = [
4294
- join13(dir, "coverage", "coverage-summary.json"),
4295
- join13(dir, "src", "coverage", "coverage-summary.json")
4588
+ join14(dir, "coverage", "coverage-summary.json"),
4589
+ join14(dir, "src", "coverage", "coverage-summary.json")
4296
4590
  ];
4297
4591
  for (const p of candidates) {
4298
- if (existsSync15(p)) return p;
4592
+ if (existsSync17(p)) return p;
4299
4593
  }
4300
4594
  return null;
4301
4595
  }
@@ -4319,8 +4613,8 @@ function printCoverageOutput(result, evaluation) {
4319
4613
 
4320
4614
  // src/lib/onboard-checks.ts
4321
4615
  function checkHarnessInitialized(dir) {
4322
- const statePath = getStatePath(dir ?? process.cwd());
4323
- return { ok: existsSync16(statePath) };
4616
+ const statePath2 = getStatePath(dir ?? process.cwd());
4617
+ return { ok: existsSync18(statePath2) };
4324
4618
  }
4325
4619
  function checkBmadInstalled(dir) {
4326
4620
  return { ok: isBmadInstalled(dir) };
@@ -4328,8 +4622,8 @@ function checkBmadInstalled(dir) {
4328
4622
  function checkHooksRegistered(dir) {
4329
4623
  const __filename = fileURLToPath3(import.meta.url);
4330
4624
  const __dirname2 = dirname4(__filename);
4331
- const hooksPath = join14(__dirname2, "..", "..", "hooks", "hooks.json");
4332
- return { ok: existsSync16(hooksPath) };
4625
+ const hooksPath = join15(__dirname2, "..", "..", "hooks", "hooks.json");
4626
+ return { ok: existsSync18(hooksPath) };
4333
4627
  }
4334
4628
  function runPreconditions(dir) {
4335
4629
  const harnessCheck = checkHarnessInitialized(dir);
@@ -4369,8 +4663,8 @@ function findVerificationGaps(dir) {
4369
4663
  for (const [key, status] of Object.entries(statuses)) {
4370
4664
  if (status !== "done") continue;
4371
4665
  if (!STORY_KEY_PATTERN2.test(key)) continue;
4372
- const proofPath = join14(root, "verification", `${key}-proof.md`);
4373
- if (!existsSync16(proofPath)) {
4666
+ const proofPath = join15(root, "verification", `${key}-proof.md`);
4667
+ if (!existsSync18(proofPath)) {
4374
4668
  unverified.push(key);
4375
4669
  }
4376
4670
  }
@@ -4497,6 +4791,768 @@ function filterTrackedGaps(stories, beadsFns) {
4497
4791
  return { untracked, trackedCount };
4498
4792
  }
4499
4793
 
4794
+ // src/modules/sprint/state.ts
4795
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync9, renameSync, existsSync as existsSync20 } from "fs";
4796
+ import { join as join17 } from "path";
4797
+
4798
+ // src/modules/sprint/migration.ts
4799
+ import { readFileSync as readFileSync16, existsSync as existsSync19 } from "fs";
4800
+ import { join as join16 } from "path";
4801
+ var OLD_FILES = {
4802
+ storyRetries: "ralph/.story_retries",
4803
+ flaggedStories: "ralph/.flagged_stories",
4804
+ ralphStatus: "ralph/status.json",
4805
+ sprintStatusYaml: "_bmad-output/implementation-artifacts/sprint-status.yaml",
4806
+ sessionIssues: "_bmad-output/implementation-artifacts/.session-issues.md"
4807
+ };
4808
+ function resolve(relative3) {
4809
+ return join16(process.cwd(), relative3);
4810
+ }
4811
+ function readIfExists(relative3) {
4812
+ const p = resolve(relative3);
4813
+ if (!existsSync19(p)) return null;
4814
+ try {
4815
+ return readFileSync16(p, "utf-8");
4816
+ } catch {
4817
+ return null;
4818
+ }
4819
+ }
4820
+ function emptyStory() {
4821
+ return {
4822
+ status: "backlog",
4823
+ attempts: 0,
4824
+ lastAttempt: null,
4825
+ lastError: null,
4826
+ proofPath: null,
4827
+ acResults: null
4828
+ };
4829
+ }
4830
+ function upsertStory(stories, key, patch) {
4831
+ stories[key] = { ...stories[key] ?? emptyStory(), ...patch };
4832
+ }
4833
+ function parseStoryRetries(content, stories) {
4834
+ for (const line of content.split("\n")) {
4835
+ const trimmed = line.trim();
4836
+ if (!trimmed) continue;
4837
+ const parts = trimmed.split(/\s+/);
4838
+ if (parts.length < 2) continue;
4839
+ const count = parseInt(parts[1], 10);
4840
+ if (!isNaN(count)) upsertStory(stories, parts[0], { attempts: count });
4841
+ }
4842
+ }
4843
+ function parseFlaggedStories(content, stories) {
4844
+ for (const line of content.split("\n")) {
4845
+ const key = line.trim();
4846
+ if (key) upsertStory(stories, key, { status: "blocked" });
4847
+ }
4848
+ }
4849
+ function mapYamlStatus(value) {
4850
+ const mapping = {
4851
+ done: "done",
4852
+ backlog: "backlog",
4853
+ verifying: "verifying",
4854
+ "in-progress": "in-progress",
4855
+ "ready-for-dev": "ready",
4856
+ blocked: "blocked",
4857
+ failed: "failed",
4858
+ review: "review",
4859
+ ready: "ready"
4860
+ };
4861
+ return mapping[value.trim().toLowerCase()] ?? null;
4862
+ }
4863
+ function parseSprintStatusYaml(content, stories) {
4864
+ for (const line of content.split("\n")) {
4865
+ const trimmed = line.trim();
4866
+ if (!trimmed || trimmed.startsWith("#")) continue;
4867
+ const match = trimmed.match(/^([a-zA-Z0-9_-]+):\s*(.+)$/);
4868
+ if (!match) continue;
4869
+ const key = match[1];
4870
+ if (key === "development_status" || key.startsWith("epic-")) continue;
4871
+ const status = mapYamlStatus(match[2]);
4872
+ if (status) upsertStory(stories, key, { status });
4873
+ }
4874
+ }
4875
+ function parseRalphStatus(content) {
4876
+ try {
4877
+ const data = JSON.parse(content);
4878
+ return {
4879
+ active: data.status === "running",
4880
+ startedAt: null,
4881
+ iteration: data.loop_count ?? 0,
4882
+ cost: 0,
4883
+ completed: [],
4884
+ failed: []
4885
+ };
4886
+ } catch {
4887
+ return null;
4888
+ }
4889
+ }
4890
+ function parseSessionIssues(content) {
4891
+ const items = [];
4892
+ let currentStory = "";
4893
+ let itemId = 0;
4894
+ for (const line of content.split("\n")) {
4895
+ const headerMatch = line.match(/^###\s+([a-zA-Z0-9_-]+)\s*[—-]/);
4896
+ if (headerMatch) {
4897
+ currentStory = headerMatch[1];
4898
+ continue;
4899
+ }
4900
+ const bulletMatch = line.match(/^-\s+(.+)/);
4901
+ if (bulletMatch && currentStory) {
4902
+ itemId++;
4903
+ items.push({
4904
+ id: `migrated-${itemId}`,
4905
+ story: currentStory,
4906
+ description: bulletMatch[1],
4907
+ source: "retro",
4908
+ resolved: false
4909
+ });
4910
+ }
4911
+ }
4912
+ return items;
4913
+ }
4914
+ function migrateFromOldFormat() {
4915
+ const hasAnyOldFile = Object.values(OLD_FILES).some((rel) => existsSync19(resolve(rel)));
4916
+ if (!hasAnyOldFile) return fail2("No old format files found for migration");
4917
+ try {
4918
+ const stories = {};
4919
+ let run = defaultState().run;
4920
+ let actionItems = [];
4921
+ const yamlContent = readIfExists(OLD_FILES.sprintStatusYaml);
4922
+ if (yamlContent) parseSprintStatusYaml(yamlContent, stories);
4923
+ const retriesContent = readIfExists(OLD_FILES.storyRetries);
4924
+ if (retriesContent) parseStoryRetries(retriesContent, stories);
4925
+ const flaggedContent = readIfExists(OLD_FILES.flaggedStories);
4926
+ if (flaggedContent) parseFlaggedStories(flaggedContent, stories);
4927
+ const statusContent = readIfExists(OLD_FILES.ralphStatus);
4928
+ if (statusContent) {
4929
+ const parsed = parseRalphStatus(statusContent);
4930
+ if (parsed) run = parsed;
4931
+ }
4932
+ const issuesContent = readIfExists(OLD_FILES.sessionIssues);
4933
+ if (issuesContent) actionItems = parseSessionIssues(issuesContent);
4934
+ const sprint = computeSprintCounts(stories);
4935
+ const migrated = {
4936
+ version: 1,
4937
+ sprint,
4938
+ stories,
4939
+ run,
4940
+ actionItems
4941
+ };
4942
+ const writeResult = writeStateAtomic(migrated);
4943
+ if (!writeResult.success) return fail2(writeResult.error);
4944
+ return ok2(migrated);
4945
+ } catch (err) {
4946
+ const msg = err instanceof Error ? err.message : String(err);
4947
+ return fail2(`Migration failed: ${msg}`);
4948
+ }
4949
+ }
4950
+
4951
+ // src/modules/sprint/state.ts
4952
+ function projectRoot() {
4953
+ return process.cwd();
4954
+ }
4955
+ function statePath() {
4956
+ return join17(projectRoot(), "sprint-state.json");
4957
+ }
4958
+ function tmpPath() {
4959
+ return join17(projectRoot(), ".sprint-state.json.tmp");
4960
+ }
4961
+ function defaultState() {
4962
+ return {
4963
+ version: 1,
4964
+ sprint: {
4965
+ total: 0,
4966
+ done: 0,
4967
+ failed: 0,
4968
+ blocked: 0,
4969
+ inProgress: null
4970
+ },
4971
+ stories: {},
4972
+ run: {
4973
+ active: false,
4974
+ startedAt: null,
4975
+ iteration: 0,
4976
+ cost: 0,
4977
+ completed: [],
4978
+ failed: []
4979
+ },
4980
+ actionItems: []
4981
+ };
4982
+ }
4983
+ function writeStateAtomic(state) {
4984
+ try {
4985
+ const data = JSON.stringify(state, null, 2) + "\n";
4986
+ const tmp = tmpPath();
4987
+ const final = statePath();
4988
+ writeFileSync9(tmp, data, "utf-8");
4989
+ renameSync(tmp, final);
4990
+ return ok2(void 0);
4991
+ } catch (err) {
4992
+ const msg = err instanceof Error ? err.message : String(err);
4993
+ return fail2(`Failed to write sprint state: ${msg}`);
4994
+ }
4995
+ }
4996
+ function getSprintState() {
4997
+ const fp = statePath();
4998
+ if (existsSync20(fp)) {
4999
+ try {
5000
+ const raw = readFileSync17(fp, "utf-8");
5001
+ const parsed = JSON.parse(raw);
5002
+ return ok2(parsed);
5003
+ } catch (err) {
5004
+ const msg = err instanceof Error ? err.message : String(err);
5005
+ return fail2(`Failed to read sprint state: ${msg}`);
5006
+ }
5007
+ }
5008
+ const migrationResult = migrateFromOldFormat();
5009
+ if (migrationResult.success) {
5010
+ return migrationResult;
5011
+ }
5012
+ return ok2(defaultState());
5013
+ }
5014
+ function computeSprintCounts(stories) {
5015
+ let total = 0;
5016
+ let done = 0;
5017
+ let failed = 0;
5018
+ let blocked = 0;
5019
+ let inProgress = null;
5020
+ for (const [key, story] of Object.entries(stories)) {
5021
+ total++;
5022
+ if (story.status === "done") done++;
5023
+ else if (story.status === "failed") failed++;
5024
+ else if (story.status === "blocked") blocked++;
5025
+ else if (story.status === "in-progress") inProgress = key;
5026
+ }
5027
+ return { total, done, failed, blocked, inProgress };
5028
+ }
5029
+
5030
+ // src/modules/sprint/selector.ts
5031
+ var MAX_STORY_ATTEMPTS = 10;
5032
+
5033
+ // src/modules/sprint/drill-down.ts
5034
+ var MAX_ATTEMPTS = MAX_STORY_ATTEMPTS;
5035
+ function epicPrefix(key) {
5036
+ const dashIdx = key.indexOf("-");
5037
+ if (dashIdx === -1) return key;
5038
+ return key.slice(0, dashIdx);
5039
+ }
5040
+ function buildAcDetails(story) {
5041
+ if (!story.acResults) return [];
5042
+ return story.acResults.map((ac) => {
5043
+ const detail = { id: ac.id, verdict: ac.verdict };
5044
+ if (ac.verdict === "fail" && story.lastError) {
5045
+ return { ...detail, reason: story.lastError };
5046
+ }
5047
+ return detail;
5048
+ });
5049
+ }
5050
+ function buildAttemptHistory(story) {
5051
+ const records = [];
5052
+ if (story.attempts === 0) return records;
5053
+ for (let i = 1; i < story.attempts; i++) {
5054
+ records.push({
5055
+ number: i,
5056
+ outcome: "details unavailable"
5057
+ });
5058
+ }
5059
+ const lastOutcome = story.status === "done" ? "passed" : story.status === "failed" ? "verify failed" : story.status === "blocked" ? "blocked" : story.status;
5060
+ const lastRecord = {
5061
+ number: story.attempts,
5062
+ outcome: lastOutcome,
5063
+ ...story.lastAttempt ? { timestamp: story.lastAttempt } : {}
5064
+ };
5065
+ if (story.acResults) {
5066
+ const failingAc = story.acResults.find((ac) => ac.verdict === "fail");
5067
+ if (failingAc) {
5068
+ records.push({ ...lastRecord, failingAc: failingAc.id });
5069
+ return records;
5070
+ }
5071
+ }
5072
+ records.push(lastRecord);
5073
+ return records;
5074
+ }
5075
+ function buildProofSummary(story) {
5076
+ if (!story.proofPath) return null;
5077
+ let passCount = 0;
5078
+ let failCount = 0;
5079
+ let escalateCount = 0;
5080
+ let pendingCount = 0;
5081
+ if (story.acResults) {
5082
+ for (const ac of story.acResults) {
5083
+ if (ac.verdict === "pass") passCount++;
5084
+ else if (ac.verdict === "fail") failCount++;
5085
+ else if (ac.verdict === "escalate") escalateCount++;
5086
+ else if (ac.verdict === "pending") pendingCount++;
5087
+ }
5088
+ }
5089
+ return { path: story.proofPath, passCount, failCount, escalateCount, pendingCount };
5090
+ }
5091
+ function getStoryDrillDown(state, key) {
5092
+ try {
5093
+ const story = state.stories[key];
5094
+ if (!story) {
5095
+ return fail2(`Story '${key}' not found in sprint state`);
5096
+ }
5097
+ const epic = epicPrefix(key);
5098
+ const acDetails = buildAcDetails(story);
5099
+ const attemptHistory = buildAttemptHistory(story);
5100
+ const proofSummary = buildProofSummary(story);
5101
+ return ok2({
5102
+ key,
5103
+ status: story.status,
5104
+ epic,
5105
+ attempts: story.attempts,
5106
+ maxAttempts: MAX_ATTEMPTS,
5107
+ lastAttempt: story.lastAttempt,
5108
+ acDetails,
5109
+ attemptHistory,
5110
+ proofSummary
5111
+ });
5112
+ } catch (err) {
5113
+ const msg = err instanceof Error ? err.message : String(err);
5114
+ return fail2(`Failed to get story drill-down: ${msg}`);
5115
+ }
5116
+ }
5117
+
5118
+ // src/modules/sprint/reporter.ts
5119
+ var MAX_ATTEMPTS2 = MAX_STORY_ATTEMPTS;
5120
+ function formatDuration(ms) {
5121
+ const clamped = Math.max(0, ms);
5122
+ const totalMinutes = Math.floor(clamped / 6e4);
5123
+ const hours = Math.floor(totalMinutes / 60);
5124
+ const minutes = totalMinutes % 60;
5125
+ if (hours > 0) return `${hours}h${minutes}m`;
5126
+ return `${minutes}m`;
5127
+ }
5128
+ function computeEpicProgress(stories) {
5129
+ const epicGroups = /* @__PURE__ */ new Map();
5130
+ for (const [key, story] of Object.entries(stories)) {
5131
+ const prefix = epicPrefix(key);
5132
+ const group = epicGroups.get(prefix) ?? { total: 0, done: 0 };
5133
+ group.total++;
5134
+ if (story.status === "done") group.done++;
5135
+ epicGroups.set(prefix, group);
5136
+ }
5137
+ let epicsTotal = 0;
5138
+ let epicsDone = 0;
5139
+ for (const group of epicGroups.values()) {
5140
+ epicsTotal++;
5141
+ if (group.total > 0 && group.done === group.total) epicsDone++;
5142
+ }
5143
+ return { epicsTotal, epicsDone };
5144
+ }
5145
+ function buildFailedDetails(stories) {
5146
+ const details = [];
5147
+ for (const [key, story] of Object.entries(stories)) {
5148
+ if (story.status !== "failed") continue;
5149
+ let acNumber = null;
5150
+ if (story.acResults) {
5151
+ for (const ac of story.acResults) {
5152
+ if (ac.verdict === "fail") {
5153
+ const num = parseInt(ac.id.replace(/\D/g, ""), 10);
5154
+ if (!isNaN(num)) {
5155
+ acNumber = num;
5156
+ break;
5157
+ }
5158
+ }
5159
+ }
5160
+ }
5161
+ details.push({
5162
+ key,
5163
+ acNumber,
5164
+ errorLine: story.lastError ?? "unknown error",
5165
+ attempts: story.attempts,
5166
+ maxAttempts: MAX_ATTEMPTS2
5167
+ });
5168
+ }
5169
+ return details;
5170
+ }
5171
+ function buildActionItemsLabeled(state) {
5172
+ const runStories = /* @__PURE__ */ new Set([
5173
+ ...state.run.completed,
5174
+ ...state.run.failed
5175
+ ]);
5176
+ return state.actionItems.map((item) => {
5177
+ const isNew = item.source === "verification" && runStories.has(item.story);
5178
+ return { item, label: isNew ? "NEW" : "CARRIED" };
5179
+ });
5180
+ }
5181
+ function buildRunSummary(state, now) {
5182
+ if (!state.run.startedAt) return null;
5183
+ const startedAt = new Date(state.run.startedAt);
5184
+ const elapsed = now.getTime() - startedAt.getTime();
5185
+ const blocked = [];
5186
+ const skipped = [];
5187
+ for (const [key, story] of Object.entries(state.stories)) {
5188
+ if (story.status === "blocked") {
5189
+ blocked.push(key);
5190
+ if (story.attempts >= MAX_ATTEMPTS2) {
5191
+ skipped.push(key);
5192
+ }
5193
+ }
5194
+ }
5195
+ return {
5196
+ duration: formatDuration(elapsed),
5197
+ cost: state.run.cost,
5198
+ iterations: state.run.iteration,
5199
+ completed: [...state.run.completed],
5200
+ failed: [...state.run.failed],
5201
+ blocked,
5202
+ skipped
5203
+ };
5204
+ }
5205
+ function generateReport(state, now) {
5206
+ try {
5207
+ const effectiveNow = now ?? /* @__PURE__ */ new Date();
5208
+ const stories = state.stories;
5209
+ let total = 0;
5210
+ let done = 0;
5211
+ let failed = 0;
5212
+ let blocked = 0;
5213
+ const storyStatuses = [];
5214
+ for (const [key, story] of Object.entries(stories)) {
5215
+ total++;
5216
+ if (story.status === "done") done++;
5217
+ else if (story.status === "failed") failed++;
5218
+ else if (story.status === "blocked") blocked++;
5219
+ storyStatuses.push({ key, status: story.status });
5220
+ }
5221
+ const sprintPercent = total > 0 ? Math.round(done / total * 100) : 0;
5222
+ const { epicsTotal, epicsDone } = computeEpicProgress(stories);
5223
+ const runSummary = buildRunSummary(state, effectiveNow);
5224
+ const activeRun = state.run.active ? runSummary : null;
5225
+ const lastRun = !state.run.active ? runSummary : null;
5226
+ const failedDetails = buildFailedDetails(stories);
5227
+ const actionItemsLabeled = buildActionItemsLabeled(state);
5228
+ return ok2({
5229
+ total,
5230
+ done,
5231
+ failed,
5232
+ blocked,
5233
+ inProgress: state.sprint.inProgress,
5234
+ storyStatuses,
5235
+ epicsTotal,
5236
+ epicsDone,
5237
+ sprintPercent,
5238
+ activeRun,
5239
+ lastRun,
5240
+ failedDetails,
5241
+ actionItemsLabeled
5242
+ });
5243
+ } catch (err) {
5244
+ const msg = err instanceof Error ? err.message : String(err);
5245
+ return fail2(`Failed to generate report: ${msg}`);
5246
+ }
5247
+ }
5248
+
5249
+ // src/modules/sprint/timeout.ts
5250
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync10, existsSync as existsSync21, mkdirSync as mkdirSync7 } from "fs";
5251
+ import { execSync as execSync3 } from "child_process";
5252
+ import { join as join18 } from "path";
5253
+ var GIT_TIMEOUT_MS = 5e3;
5254
+ var DEFAULT_MAX_LINES = 100;
5255
+ function captureGitDiff() {
5256
+ try {
5257
+ const unstaged = execSync3("git diff --stat", {
5258
+ timeout: GIT_TIMEOUT_MS,
5259
+ encoding: "utf-8",
5260
+ stdio: ["pipe", "pipe", "pipe"]
5261
+ }).trim();
5262
+ const staged = execSync3("git diff --cached --stat", {
5263
+ timeout: GIT_TIMEOUT_MS,
5264
+ encoding: "utf-8",
5265
+ stdio: ["pipe", "pipe", "pipe"]
5266
+ }).trim();
5267
+ const parts = [];
5268
+ if (unstaged) parts.push("Unstaged:\n" + unstaged);
5269
+ if (staged) parts.push("Staged:\n" + staged);
5270
+ if (parts.length === 0) {
5271
+ return ok2("No changes detected");
5272
+ }
5273
+ return ok2(parts.join("\n\n"));
5274
+ } catch (err) {
5275
+ const msg = err instanceof Error ? err.message : String(err);
5276
+ return fail2(`Failed to capture git diff: ${msg}`);
5277
+ }
5278
+ }
5279
+ function captureStateDelta(beforePath, afterPath) {
5280
+ try {
5281
+ if (!existsSync21(beforePath)) {
5282
+ return fail2(`State snapshot not found: ${beforePath}`);
5283
+ }
5284
+ if (!existsSync21(afterPath)) {
5285
+ return fail2(`Current state file not found: ${afterPath}`);
5286
+ }
5287
+ const beforeRaw = readFileSync18(beforePath, "utf-8");
5288
+ const afterRaw = readFileSync18(afterPath, "utf-8");
5289
+ const before = JSON.parse(beforeRaw);
5290
+ const after = JSON.parse(afterRaw);
5291
+ const beforeStories = before.stories ?? {};
5292
+ const afterStories = after.stories ?? {};
5293
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(beforeStories), ...Object.keys(afterStories)]);
5294
+ const changes = [];
5295
+ for (const key of allKeys) {
5296
+ const beforeStatus = beforeStories[key]?.status ?? "(absent)";
5297
+ const afterStatus = afterStories[key]?.status ?? "(absent)";
5298
+ if (beforeStatus !== afterStatus) {
5299
+ changes.push(`${key}: ${beforeStatus} \u2192 ${afterStatus}`);
5300
+ }
5301
+ }
5302
+ if (changes.length === 0) {
5303
+ return ok2("No state changes");
5304
+ }
5305
+ return ok2(changes.join("\n"));
5306
+ } catch (err) {
5307
+ const msg = err instanceof Error ? err.message : String(err);
5308
+ return fail2(`Failed to capture state delta: ${msg}`);
5309
+ }
5310
+ }
5311
+ function capturePartialStderr(outputFile, maxLines = DEFAULT_MAX_LINES) {
5312
+ try {
5313
+ if (!existsSync21(outputFile)) {
5314
+ return fail2(`Output file not found: ${outputFile}`);
5315
+ }
5316
+ const content = readFileSync18(outputFile, "utf-8");
5317
+ const lines = content.split("\n");
5318
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
5319
+ lines.pop();
5320
+ }
5321
+ const lastLines = lines.slice(-maxLines).join("\n");
5322
+ return ok2(lastLines);
5323
+ } catch (err) {
5324
+ const msg = err instanceof Error ? err.message : String(err);
5325
+ return fail2(`Failed to capture partial stderr: ${msg}`);
5326
+ }
5327
+ }
5328
+ function formatReport(capture) {
5329
+ const lines = [
5330
+ `# Timeout Report: Iteration ${capture.iteration}`,
5331
+ "",
5332
+ `- **Story:** ${capture.storyKey}`,
5333
+ `- **Duration:** ${capture.durationMinutes} minutes (timeout)`,
5334
+ `- **Timestamp:** ${capture.timestamp}`,
5335
+ "",
5336
+ "## Git Changes",
5337
+ "",
5338
+ capture.gitDiff,
5339
+ "",
5340
+ "## State Delta",
5341
+ "",
5342
+ capture.stateDelta,
5343
+ "",
5344
+ "## Partial Output (last 100 lines)",
5345
+ "",
5346
+ "```",
5347
+ capture.partialStderr,
5348
+ "```",
5349
+ ""
5350
+ ];
5351
+ return lines.join("\n");
5352
+ }
5353
+ function captureTimeoutReport(opts) {
5354
+ try {
5355
+ if (opts.iteration < 1 || !Number.isInteger(opts.iteration)) {
5356
+ return fail2(`Invalid iteration number: ${opts.iteration}`);
5357
+ }
5358
+ if (opts.durationMinutes < 0) {
5359
+ return fail2(`Invalid duration: ${opts.durationMinutes}`);
5360
+ }
5361
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
5362
+ const gitResult = captureGitDiff();
5363
+ const gitDiff = gitResult.success ? gitResult.data : `(unavailable: ${gitResult.error})`;
5364
+ const statePath2 = join18(process.cwd(), "sprint-state.json");
5365
+ const deltaResult = captureStateDelta(opts.stateSnapshotPath, statePath2);
5366
+ const stateDelta = deltaResult.success ? deltaResult.data : `(unavailable: ${deltaResult.error})`;
5367
+ const stderrResult = capturePartialStderr(opts.outputFile);
5368
+ const partialStderr = stderrResult.success ? stderrResult.data : `(unavailable: ${stderrResult.error})`;
5369
+ const capture = {
5370
+ storyKey: opts.storyKey,
5371
+ iteration: opts.iteration,
5372
+ durationMinutes: opts.durationMinutes,
5373
+ gitDiff,
5374
+ stateDelta,
5375
+ partialStderr,
5376
+ timestamp
5377
+ };
5378
+ const reportDir = join18(process.cwd(), "ralph", "logs");
5379
+ const safeStoryKey = opts.storyKey.replace(/[^a-zA-Z0-9._-]/g, "_");
5380
+ const reportFileName = `timeout-report-${opts.iteration}-${safeStoryKey}.md`;
5381
+ const reportPath = join18(reportDir, reportFileName);
5382
+ if (!existsSync21(reportDir)) {
5383
+ mkdirSync7(reportDir, { recursive: true });
5384
+ }
5385
+ const reportContent = formatReport(capture);
5386
+ writeFileSync10(reportPath, reportContent, "utf-8");
5387
+ return ok2({
5388
+ filePath: reportPath,
5389
+ capture
5390
+ });
5391
+ } catch (err) {
5392
+ const msg = err instanceof Error ? err.message : String(err);
5393
+ return fail2(`Failed to capture timeout report: ${msg}`);
5394
+ }
5395
+ }
5396
+
5397
+ // src/modules/sprint/feedback.ts
5398
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync11 } from "fs";
5399
+ import { existsSync as existsSync22 } from "fs";
5400
+ import { join as join19 } from "path";
5401
+
5402
+ // src/modules/sprint/validator.ts
5403
+ import { readFileSync as readFileSync20, existsSync as existsSync23 } from "fs";
5404
+ var VALID_STATUSES = /* @__PURE__ */ new Set([
5405
+ "backlog",
5406
+ "ready",
5407
+ "in-progress",
5408
+ "review",
5409
+ "verifying",
5410
+ "done",
5411
+ "failed",
5412
+ "blocked"
5413
+ ]);
5414
+ var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
5415
+ function parseSprintStatusKeys(content) {
5416
+ try {
5417
+ const keys = [];
5418
+ const lines = content.split("\n");
5419
+ for (let i = 0; i < lines.length; i++) {
5420
+ const line = lines[i];
5421
+ const match = line.match(/^([a-zA-Z0-9][a-zA-Z0-9._-]*):$/);
5422
+ if (match) {
5423
+ keys.push(match[1]);
5424
+ }
5425
+ }
5426
+ return ok2(keys);
5427
+ } catch (err) {
5428
+ const msg = err instanceof Error ? err.message : String(err);
5429
+ return fail2(`Failed to parse sprint-status.yaml: ${msg}`);
5430
+ }
5431
+ }
5432
+ function parseStateFile(statePath2) {
5433
+ try {
5434
+ if (!existsSync23(statePath2)) {
5435
+ return fail2(`State file not found: ${statePath2}`);
5436
+ }
5437
+ const raw = readFileSync20(statePath2, "utf-8");
5438
+ const parsed = JSON.parse(raw);
5439
+ return ok2(parsed);
5440
+ } catch (err) {
5441
+ const msg = err instanceof Error ? err.message : String(err);
5442
+ return fail2(`Failed to parse state file: ${msg}`);
5443
+ }
5444
+ }
5445
+ function validateStateConsistency(statePath2, sprintStatusPath) {
5446
+ try {
5447
+ const stateResult = parseStateFile(statePath2);
5448
+ if (!stateResult.success) {
5449
+ return fail2(stateResult.error);
5450
+ }
5451
+ const state = stateResult.data;
5452
+ if (!existsSync23(sprintStatusPath)) {
5453
+ return fail2(`Sprint status file not found: ${sprintStatusPath}`);
5454
+ }
5455
+ const statusContent = readFileSync20(sprintStatusPath, "utf-8");
5456
+ const keysResult = parseSprintStatusKeys(statusContent);
5457
+ if (!keysResult.success) {
5458
+ return fail2(keysResult.error);
5459
+ }
5460
+ const expectedKeys = keysResult.data;
5461
+ const issues = [];
5462
+ const missingKeys = [];
5463
+ for (const key of expectedKeys) {
5464
+ if (!(key in state.stories)) {
5465
+ missingKeys.push(key);
5466
+ issues.push({
5467
+ storyKey: key,
5468
+ field: "entry",
5469
+ message: `Story "${key}" in sprint-status.yaml has no entry in sprint-state.json`
5470
+ });
5471
+ }
5472
+ }
5473
+ const actualStoryCount = Object.keys(state.stories).length;
5474
+ if (state.sprint.total !== actualStoryCount) {
5475
+ issues.push({
5476
+ storyKey: "_sprint",
5477
+ field: "sprint.total",
5478
+ message: `sprint.total is ${state.sprint.total} but state has ${actualStoryCount} stories`
5479
+ });
5480
+ }
5481
+ for (const key of Object.keys(state.stories)) {
5482
+ if (!expectedKeys.includes(key)) {
5483
+ issues.push({
5484
+ storyKey: key,
5485
+ field: "entry",
5486
+ message: `Story "${key}" in sprint-state.json has no entry in sprint-status.yaml`
5487
+ });
5488
+ }
5489
+ }
5490
+ for (const [key, story] of Object.entries(state.stories)) {
5491
+ if (!VALID_STATUSES.has(story.status)) {
5492
+ issues.push({
5493
+ storyKey: key,
5494
+ field: "status",
5495
+ message: `Invalid status "${story.status}" (expected one of: ${[...VALID_STATUSES].join(", ")})`
5496
+ });
5497
+ }
5498
+ if (typeof story.attempts !== "number" || !Number.isInteger(story.attempts) || story.attempts < 0) {
5499
+ issues.push({
5500
+ storyKey: key,
5501
+ field: "attempts",
5502
+ message: `Invalid attempts value "${story.attempts}" (must be non-negative integer)`
5503
+ });
5504
+ }
5505
+ if (story.lastError !== null && story.lastAttempt !== null) {
5506
+ const attemptTime = new Date(story.lastAttempt).getTime();
5507
+ const now = Date.now();
5508
+ if (!isNaN(attemptTime) && now - attemptTime > STALE_THRESHOLD_MS) {
5509
+ issues.push({
5510
+ storyKey: key,
5511
+ field: "lastError",
5512
+ message: `Stale lastError: lastAttempt was "${story.lastAttempt}" (>24h ago)`
5513
+ });
5514
+ }
5515
+ }
5516
+ }
5517
+ const allKeys = /* @__PURE__ */ new Set([...expectedKeys, ...Object.keys(state.stories)]);
5518
+ const totalStories = allKeys.size;
5519
+ const invalidCount = new Set(issues.map((i) => i.storyKey)).size;
5520
+ const validCount = totalStories - invalidCount;
5521
+ return ok2({
5522
+ totalStories,
5523
+ validCount,
5524
+ invalidCount,
5525
+ missingKeys,
5526
+ issues
5527
+ });
5528
+ } catch (err) {
5529
+ const msg = err instanceof Error ? err.message : String(err);
5530
+ return fail2(`Validation failed: ${msg}`);
5531
+ }
5532
+ }
5533
+
5534
+ // src/modules/sprint/index.ts
5535
+ function generateReport2() {
5536
+ const stateResult = getSprintState();
5537
+ if (!stateResult.success) {
5538
+ return fail2(stateResult.error);
5539
+ }
5540
+ return generateReport(stateResult.data);
5541
+ }
5542
+ function getStoryDrillDown2(key) {
5543
+ const stateResult = getSprintState();
5544
+ if (!stateResult.success) {
5545
+ return fail2(stateResult.error);
5546
+ }
5547
+ return getStoryDrillDown(stateResult.data, key);
5548
+ }
5549
+ function captureTimeoutReport2(opts) {
5550
+ return captureTimeoutReport(opts);
5551
+ }
5552
+ function validateStateConsistency2(statePath2, sprintStatusPath) {
5553
+ return validateStateConsistency(statePath2, sprintStatusPath);
5554
+ }
5555
+
4500
5556
  // src/commands/status.ts
4501
5557
  function buildScopedEndpoints(endpoints, serviceName) {
4502
5558
  const encoded = encodeURIComponent(serviceName);
@@ -4513,9 +5569,13 @@ var DEFAULT_ENDPOINTS = {
4513
5569
  otel_http: "http://localhost:4318"
4514
5570
  };
4515
5571
  function registerStatusCommand(program) {
4516
- program.command("status").description("Show current harness status and health").option("--check-docker", "Check Docker stack health").option("--check", "Run health checks with pass/fail exit code").action(async (options, cmd) => {
5572
+ program.command("status").description("Show current harness status and health").option("--check-docker", "Check Docker stack health").option("--check", "Run health checks with pass/fail exit code").option("--story <id>", "Show detailed status for a specific story").action(async (options, cmd) => {
4517
5573
  const opts = cmd.optsWithGlobals();
4518
5574
  const isJson = opts.json === true;
5575
+ if (options.story) {
5576
+ handleStoryDrillDown(options.story, isJson);
5577
+ return;
5578
+ }
4519
5579
  if (options.checkDocker) {
4520
5580
  await handleDockerCheck(isJson);
4521
5581
  return;
@@ -4547,6 +5607,7 @@ function handleFullStatus(isJson) {
4547
5607
  handleFullStatusJson(state);
4548
5608
  return;
4549
5609
  }
5610
+ printSprintState();
4550
5611
  console.log(`Harness: codeharness v${state.harness_version}`);
4551
5612
  console.log(`Stack: ${state.stack ?? "unknown"}`);
4552
5613
  if (state.app_type) {
@@ -4683,10 +5744,12 @@ function handleFullStatusJson(state) {
4683
5744
  const scoped_endpoints = serviceName ? buildScopedEndpoints(endpoints, serviceName) : void 0;
4684
5745
  const beads = getBeadsData();
4685
5746
  const onboarding = getOnboardingProgressData();
5747
+ const sprint = getSprintReportData();
4686
5748
  jsonOutput({
4687
5749
  version: state.harness_version,
4688
5750
  stack: state.stack,
4689
5751
  ...state.app_type ? { app_type: state.app_type } : {},
5752
+ ...sprint ? { sprint } : {},
4690
5753
  enforcement: state.enforcement,
4691
5754
  docker,
4692
5755
  endpoints,
@@ -4878,6 +5941,116 @@ async function handleDockerCheck(isJson) {
4878
5941
  }
4879
5942
  }
4880
5943
  }
5944
+ function handleStoryDrillDown(storyId, isJson) {
5945
+ const result = getStoryDrillDown2(storyId);
5946
+ if (!result.success) {
5947
+ if (isJson) {
5948
+ jsonOutput({ status: "fail", message: result.error });
5949
+ } else {
5950
+ fail(result.error);
5951
+ }
5952
+ process.exitCode = 1;
5953
+ return;
5954
+ }
5955
+ const d = result.data;
5956
+ if (isJson) {
5957
+ jsonOutput({
5958
+ key: d.key,
5959
+ status: d.status,
5960
+ epic: d.epic,
5961
+ attempts: d.attempts,
5962
+ maxAttempts: d.maxAttempts,
5963
+ lastAttempt: d.lastAttempt,
5964
+ acResults: d.acDetails,
5965
+ attemptHistory: d.attemptHistory,
5966
+ proof: d.proofSummary
5967
+ });
5968
+ return;
5969
+ }
5970
+ console.log(`Story: ${d.key}`);
5971
+ console.log(`Status: ${d.status} (attempt ${d.attempts}/${d.maxAttempts})`);
5972
+ console.log(`Epic: ${d.epic}`);
5973
+ console.log(`Last attempt: ${d.lastAttempt ?? "none"}`);
5974
+ console.log("");
5975
+ console.log("-- AC Results -------------------------------------------------------");
5976
+ if (d.acDetails.length === 0) {
5977
+ console.log("No AC results recorded");
5978
+ } else {
5979
+ for (const ac of d.acDetails) {
5980
+ const tag = ac.verdict.toUpperCase();
5981
+ console.log(`${ac.id}: [${tag}]`);
5982
+ if (ac.verdict === "fail") {
5983
+ if (ac.command) console.log(` Command: ${ac.command}`);
5984
+ if (ac.expected) console.log(` Expected: ${ac.expected}`);
5985
+ if (ac.actual) console.log(` Actual: ${ac.actual}`);
5986
+ if (ac.reason) console.log(` Reason: ${ac.reason}`);
5987
+ if (ac.suggestedFix) console.log(` Suggest: ${ac.suggestedFix}`);
5988
+ }
5989
+ }
5990
+ }
5991
+ if (d.attemptHistory.length > 0) {
5992
+ console.log("");
5993
+ console.log("-- History ----------------------------------------------------------");
5994
+ for (const attempt of d.attemptHistory) {
5995
+ const acPart = attempt.failingAc ? ` (${attempt.failingAc})` : "";
5996
+ console.log(`Attempt ${attempt.number}: ${attempt.outcome}${acPart}`);
5997
+ }
5998
+ }
5999
+ if (d.proofSummary) {
6000
+ console.log("");
6001
+ const p = d.proofSummary;
6002
+ const total = p.passCount + p.failCount + p.escalateCount + p.pendingCount;
6003
+ console.log(
6004
+ `Proof: ${p.path} (${p.passCount}/${total} pass, ${p.failCount} fail, ${p.escalateCount} escalate)`
6005
+ );
6006
+ }
6007
+ }
6008
+ function printSprintState() {
6009
+ const reportResult = generateReport2();
6010
+ if (!reportResult.success) {
6011
+ console.log("Sprint state: unavailable");
6012
+ return;
6013
+ }
6014
+ const r = reportResult.data;
6015
+ console.log(`\u2500\u2500 Project State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
6016
+ console.log(`Sprint: ${r.done}/${r.total} done (${r.sprintPercent}%) | ${r.epicsDone}/${r.epicsTotal} epics complete`);
6017
+ if (r.activeRun) {
6018
+ console.log("");
6019
+ console.log(`\u2500\u2500 Active Run \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
6020
+ const currentStory = r.inProgress ?? "none";
6021
+ console.log(`Status: running (iteration ${r.activeRun.iterations}, ${r.activeRun.duration} elapsed)`);
6022
+ console.log(`Current: ${currentStory}`);
6023
+ console.log(`Budget: $${r.activeRun.cost.toFixed(2)} spent`);
6024
+ } else if (r.lastRun) {
6025
+ console.log("");
6026
+ console.log(`\u2500\u2500 Last Run Summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
6027
+ console.log(`Duration: ${r.lastRun.duration} | Cost: $${r.lastRun.cost.toFixed(2)} | Iterations: ${r.lastRun.iterations}`);
6028
+ console.log(`Completed: ${r.lastRun.completed.length} stories${r.lastRun.completed.length > 0 ? ` (${r.lastRun.completed.join(", ")})` : ""}`);
6029
+ if (r.failedDetails.length > 0) {
6030
+ console.log(`Failed: ${r.failedDetails.length} stor${r.failedDetails.length === 1 ? "y" : "ies"}`);
6031
+ for (const fd of r.failedDetails) {
6032
+ const acPart = fd.acNumber !== null ? `AC ${fd.acNumber}` : "unknown AC";
6033
+ console.log(` \u2514 ${fd.key}: ${acPart} \u2014 ${fd.errorLine} (attempt ${fd.attempts}/${fd.maxAttempts})`);
6034
+ }
6035
+ }
6036
+ if (r.lastRun.blocked.length > 0) {
6037
+ console.log(`Blocked: ${r.lastRun.blocked.length} stories (retry-exhausted)`);
6038
+ }
6039
+ }
6040
+ if (r.actionItemsLabeled.length > 0) {
6041
+ console.log("");
6042
+ console.log(`\u2500\u2500 Action Items \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
6043
+ for (const la of r.actionItemsLabeled) {
6044
+ console.log(` [${la.label}] ${la.item.story}: ${la.item.description}`);
6045
+ }
6046
+ }
6047
+ console.log("");
6048
+ }
6049
+ function getSprintReportData() {
6050
+ const reportResult = generateReport2();
6051
+ if (!reportResult.success) return null;
6052
+ return reportResult.data;
6053
+ }
4881
6054
  function printBeadsSummary() {
4882
6055
  if (!isBeadsInitialized()) {
4883
6056
  console.log("Beads: not initialized");
@@ -4944,16 +6117,16 @@ function getBeadsData() {
4944
6117
  }
4945
6118
 
4946
6119
  // src/commands/onboard.ts
4947
- import { join as join18 } from "path";
6120
+ import { join as join23 } from "path";
4948
6121
 
4949
6122
  // src/lib/scanner.ts
4950
6123
  import {
4951
- existsSync as existsSync17,
4952
- readdirSync as readdirSync3,
4953
- readFileSync as readFileSync15,
4954
- statSync as statSync2
6124
+ existsSync as existsSync24,
6125
+ readdirSync as readdirSync4,
6126
+ readFileSync as readFileSync21,
6127
+ statSync as statSync3
4955
6128
  } from "fs";
4956
- import { join as join15, relative as relative2 } from "path";
6129
+ import { join as join20, relative as relative2 } from "path";
4957
6130
  var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
4958
6131
  var DEFAULT_MIN_MODULE_SIZE = 3;
4959
6132
  function getExtension2(filename) {
@@ -4971,17 +6144,17 @@ function countSourceFiles(dir) {
4971
6144
  function walk(current) {
4972
6145
  let entries;
4973
6146
  try {
4974
- entries = readdirSync3(current);
6147
+ entries = readdirSync4(current);
4975
6148
  } catch {
4976
6149
  return;
4977
6150
  }
4978
6151
  for (const entry of entries) {
4979
6152
  if (isSkippedDir(entry)) continue;
4980
6153
  if (entry.startsWith(".") && current !== dir) continue;
4981
- const fullPath = join15(current, entry);
6154
+ const fullPath = join20(current, entry);
4982
6155
  let stat;
4983
6156
  try {
4984
- stat = statSync2(fullPath);
6157
+ stat = statSync3(fullPath);
4985
6158
  } catch {
4986
6159
  continue;
4987
6160
  }
@@ -5001,22 +6174,22 @@ function countSourceFiles(dir) {
5001
6174
  return count;
5002
6175
  }
5003
6176
  function countModuleFiles(modulePath, rootDir) {
5004
- const fullModulePath = join15(rootDir, modulePath);
6177
+ const fullModulePath = join20(rootDir, modulePath);
5005
6178
  let sourceFiles = 0;
5006
6179
  let testFiles = 0;
5007
6180
  function walk(current) {
5008
6181
  let entries;
5009
6182
  try {
5010
- entries = readdirSync3(current);
6183
+ entries = readdirSync4(current);
5011
6184
  } catch {
5012
6185
  return;
5013
6186
  }
5014
6187
  for (const entry of entries) {
5015
6188
  if (isSkippedDir(entry)) continue;
5016
- const fullPath = join15(current, entry);
6189
+ const fullPath = join20(current, entry);
5017
6190
  let stat;
5018
6191
  try {
5019
- stat = statSync2(fullPath);
6192
+ stat = statSync3(fullPath);
5020
6193
  } catch {
5021
6194
  continue;
5022
6195
  }
@@ -5038,8 +6211,8 @@ function countModuleFiles(modulePath, rootDir) {
5038
6211
  return { sourceFiles, testFiles };
5039
6212
  }
5040
6213
  function detectArtifacts(dir) {
5041
- const bmadPath = join15(dir, "_bmad");
5042
- const hasBmad = existsSync17(bmadPath);
6214
+ const bmadPath = join20(dir, "_bmad");
6215
+ const hasBmad = existsSync24(bmadPath);
5043
6216
  return {
5044
6217
  hasBmad,
5045
6218
  bmadPath: hasBmad ? relative2(dir, bmadPath) || "_bmad" : null
@@ -5121,10 +6294,10 @@ function readPerFileCoverage(dir, format) {
5121
6294
  return null;
5122
6295
  }
5123
6296
  function readVitestPerFileCoverage(dir) {
5124
- const reportPath = join15(dir, "coverage", "coverage-summary.json");
5125
- if (!existsSync17(reportPath)) return null;
6297
+ const reportPath = join20(dir, "coverage", "coverage-summary.json");
6298
+ if (!existsSync24(reportPath)) return null;
5126
6299
  try {
5127
- const report = JSON.parse(readFileSync15(reportPath, "utf-8"));
6300
+ const report = JSON.parse(readFileSync21(reportPath, "utf-8"));
5128
6301
  const result = /* @__PURE__ */ new Map();
5129
6302
  for (const [key, value] of Object.entries(report)) {
5130
6303
  if (key === "total") continue;
@@ -5136,10 +6309,10 @@ function readVitestPerFileCoverage(dir) {
5136
6309
  }
5137
6310
  }
5138
6311
  function readPythonPerFileCoverage(dir) {
5139
- const reportPath = join15(dir, "coverage.json");
5140
- if (!existsSync17(reportPath)) return null;
6312
+ const reportPath = join20(dir, "coverage.json");
6313
+ if (!existsSync24(reportPath)) return null;
5141
6314
  try {
5142
- const report = JSON.parse(readFileSync15(reportPath, "utf-8"));
6315
+ const report = JSON.parse(readFileSync21(reportPath, "utf-8"));
5143
6316
  if (!report.files) return null;
5144
6317
  const result = /* @__PURE__ */ new Map();
5145
6318
  for (const [key, value] of Object.entries(report.files)) {
@@ -5155,13 +6328,13 @@ function auditDocumentation(dir) {
5155
6328
  const root = dir ?? process.cwd();
5156
6329
  const documents = [];
5157
6330
  for (const docName of AUDIT_DOCUMENTS) {
5158
- const docPath = join15(root, docName);
5159
- if (!existsSync17(docPath)) {
6331
+ const docPath = join20(root, docName);
6332
+ if (!existsSync24(docPath)) {
5160
6333
  documents.push({ name: docName, grade: "missing", path: null });
5161
6334
  continue;
5162
6335
  }
5163
- const srcDir = join15(root, "src");
5164
- const codeDir = existsSync17(srcDir) ? srcDir : root;
6336
+ const srcDir = join20(root, "src");
6337
+ const codeDir = existsSync24(srcDir) ? srcDir : root;
5165
6338
  const stale = isDocStale(docPath, codeDir);
5166
6339
  documents.push({
5167
6340
  name: docName,
@@ -5169,10 +6342,10 @@ function auditDocumentation(dir) {
5169
6342
  path: docName
5170
6343
  });
5171
6344
  }
5172
- const docsDir = join15(root, "docs");
5173
- if (existsSync17(docsDir)) {
6345
+ const docsDir = join20(root, "docs");
6346
+ if (existsSync24(docsDir)) {
5174
6347
  try {
5175
- const stat = statSync2(docsDir);
6348
+ const stat = statSync3(docsDir);
5176
6349
  if (stat.isDirectory()) {
5177
6350
  documents.push({ name: "docs/", grade: "present", path: "docs/" });
5178
6351
  }
@@ -5182,10 +6355,10 @@ function auditDocumentation(dir) {
5182
6355
  } else {
5183
6356
  documents.push({ name: "docs/", grade: "missing", path: null });
5184
6357
  }
5185
- const indexPath = join15(root, "docs", "index.md");
5186
- if (existsSync17(indexPath)) {
5187
- const srcDir = join15(root, "src");
5188
- const indexCodeDir = existsSync17(srcDir) ? srcDir : root;
6358
+ const indexPath = join20(root, "docs", "index.md");
6359
+ if (existsSync24(indexPath)) {
6360
+ const srcDir = join20(root, "src");
6361
+ const indexCodeDir = existsSync24(srcDir) ? srcDir : root;
5189
6362
  const indexStale = isDocStale(indexPath, indexCodeDir);
5190
6363
  documents.push({
5191
6364
  name: "docs/index.md",
@@ -5202,8 +6375,8 @@ function auditDocumentation(dir) {
5202
6375
 
5203
6376
  // src/lib/epic-generator.ts
5204
6377
  import { createInterface } from "readline";
5205
- import { existsSync as existsSync18, mkdirSync as mkdirSync6, writeFileSync as writeFileSync9 } from "fs";
5206
- import { dirname as dirname5, join as join16 } from "path";
6378
+ import { existsSync as existsSync25, mkdirSync as mkdirSync8, writeFileSync as writeFileSync12 } from "fs";
6379
+ import { dirname as dirname6, join as join21 } from "path";
5207
6380
  var PRIORITY_BY_TYPE = {
5208
6381
  observability: 1,
5209
6382
  coverage: 2,
@@ -5241,8 +6414,8 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
5241
6414
  storyNum++;
5242
6415
  }
5243
6416
  for (const mod of scan.modules) {
5244
- const agentsPath = join16(root, mod.path, "AGENTS.md");
5245
- if (!existsSync18(agentsPath)) {
6417
+ const agentsPath = join21(root, mod.path, "AGENTS.md");
6418
+ if (!existsSync25(agentsPath)) {
5246
6419
  stories.push({
5247
6420
  key: `0.${storyNum}`,
5248
6421
  title: `Create ${mod.path}/AGENTS.md`,
@@ -5308,7 +6481,7 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
5308
6481
  };
5309
6482
  }
5310
6483
  function writeOnboardingEpic(epic, outputPath) {
5311
- mkdirSync6(dirname5(outputPath), { recursive: true });
6484
+ mkdirSync8(dirname6(outputPath), { recursive: true });
5312
6485
  const lines = [];
5313
6486
  lines.push(`# ${epic.title}`);
5314
6487
  lines.push("");
@@ -5344,14 +6517,14 @@ function writeOnboardingEpic(epic, outputPath) {
5344
6517
  lines.push("");
5345
6518
  lines.push("Review and approve before execution.");
5346
6519
  lines.push("");
5347
- writeFileSync9(outputPath, lines.join("\n"), "utf-8");
6520
+ writeFileSync12(outputPath, lines.join("\n"), "utf-8");
5348
6521
  }
5349
6522
  function formatEpicSummary(epic) {
5350
6523
  const { totalStories, coverageStories, docStories, verificationStories, observabilityStories } = epic.summary;
5351
6524
  return `Onboarding plan: ${totalStories} stories (${coverageStories} coverage, ${docStories} documentation, ${verificationStories} verification, ${observabilityStories} observability)`;
5352
6525
  }
5353
6526
  function promptApproval() {
5354
- return new Promise((resolve) => {
6527
+ return new Promise((resolve3) => {
5355
6528
  let answered = false;
5356
6529
  const rl = createInterface({
5357
6530
  input: process.stdin,
@@ -5360,14 +6533,14 @@ function promptApproval() {
5360
6533
  rl.on("close", () => {
5361
6534
  if (!answered) {
5362
6535
  answered = true;
5363
- resolve(false);
6536
+ resolve3(false);
5364
6537
  }
5365
6538
  });
5366
6539
  rl.question("Review the onboarding plan. Approve? [Y/n] ", (answer) => {
5367
6540
  answered = true;
5368
6541
  rl.close();
5369
6542
  const trimmed = answer.trim().toLowerCase();
5370
- resolve(trimmed === "" || trimmed === "y");
6543
+ resolve3(trimmed === "" || trimmed === "y");
5371
6544
  });
5372
6545
  });
5373
6546
  }
@@ -5440,29 +6613,29 @@ function getGapIdFromTitle(title) {
5440
6613
  }
5441
6614
 
5442
6615
  // src/lib/scan-cache.ts
5443
- import { existsSync as existsSync19, mkdirSync as mkdirSync7, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
5444
- import { join as join17 } from "path";
6616
+ import { existsSync as existsSync26, mkdirSync as mkdirSync9, readFileSync as readFileSync22, writeFileSync as writeFileSync13 } from "fs";
6617
+ import { join as join22 } from "path";
5445
6618
  var CACHE_DIR = ".harness";
5446
6619
  var CACHE_FILE = "last-onboard-scan.json";
5447
6620
  var DEFAULT_MAX_AGE_MS = 864e5;
5448
6621
  function saveScanCache(entry, dir) {
5449
6622
  try {
5450
6623
  const root = dir ?? process.cwd();
5451
- const cacheDir = join17(root, CACHE_DIR);
5452
- mkdirSync7(cacheDir, { recursive: true });
5453
- const cachePath = join17(cacheDir, CACHE_FILE);
5454
- writeFileSync10(cachePath, JSON.stringify(entry, null, 2), "utf-8");
6624
+ const cacheDir = join22(root, CACHE_DIR);
6625
+ mkdirSync9(cacheDir, { recursive: true });
6626
+ const cachePath = join22(cacheDir, CACHE_FILE);
6627
+ writeFileSync13(cachePath, JSON.stringify(entry, null, 2), "utf-8");
5455
6628
  } catch {
5456
6629
  }
5457
6630
  }
5458
6631
  function loadScanCache(dir) {
5459
6632
  const root = dir ?? process.cwd();
5460
- const cachePath = join17(root, CACHE_DIR, CACHE_FILE);
5461
- if (!existsSync19(cachePath)) {
6633
+ const cachePath = join22(root, CACHE_DIR, CACHE_FILE);
6634
+ if (!existsSync26(cachePath)) {
5462
6635
  return null;
5463
6636
  }
5464
6637
  try {
5465
- const raw = readFileSync16(cachePath, "utf-8");
6638
+ const raw = readFileSync22(cachePath, "utf-8");
5466
6639
  return JSON.parse(raw);
5467
6640
  } catch {
5468
6641
  return null;
@@ -5635,7 +6808,7 @@ function registerOnboardCommand(program) {
5635
6808
  }
5636
6809
  coverage = lastCoverageResult ?? runCoverageAnalysis(scan);
5637
6810
  audit = lastAuditResult ?? runAudit();
5638
- const epicPath = join18(process.cwd(), "ralph", "onboarding-epic.md");
6811
+ const epicPath = join23(process.cwd(), "ralph", "onboarding-epic.md");
5639
6812
  const epic = generateOnboardingEpic(scan, coverage, audit);
5640
6813
  mergeExtendedGaps(epic);
5641
6814
  if (!isFull) {
@@ -5708,7 +6881,7 @@ function registerOnboardCommand(program) {
5708
6881
  coverage,
5709
6882
  audit
5710
6883
  });
5711
- const epicPath = join18(process.cwd(), "ralph", "onboarding-epic.md");
6884
+ const epicPath = join23(process.cwd(), "ralph", "onboarding-epic.md");
5712
6885
  const epic = generateOnboardingEpic(scan, coverage, audit);
5713
6886
  mergeExtendedGaps(epic);
5714
6887
  if (!isFull) {
@@ -5816,8 +6989,8 @@ function printEpicOutput(epic) {
5816
6989
  }
5817
6990
 
5818
6991
  // src/commands/teardown.ts
5819
- import { existsSync as existsSync20, unlinkSync as unlinkSync2, readFileSync as readFileSync17, writeFileSync as writeFileSync11, rmSync } from "fs";
5820
- import { join as join19 } from "path";
6992
+ import { existsSync as existsSync27, unlinkSync as unlinkSync2, readFileSync as readFileSync23, writeFileSync as writeFileSync14, rmSync as rmSync2 } from "fs";
6993
+ import { join as join24 } from "path";
5821
6994
  function buildDefaultResult() {
5822
6995
  return {
5823
6996
  status: "ok",
@@ -5920,16 +7093,16 @@ function registerTeardownCommand(program) {
5920
7093
  info("Docker stack: not running, skipping");
5921
7094
  }
5922
7095
  }
5923
- const composeFilePath = join19(projectDir, composeFile);
5924
- if (existsSync20(composeFilePath)) {
7096
+ const composeFilePath = join24(projectDir, composeFile);
7097
+ if (existsSync27(composeFilePath)) {
5925
7098
  unlinkSync2(composeFilePath);
5926
7099
  result.removed.push(composeFile);
5927
7100
  if (!isJson) {
5928
7101
  ok(`Removed: ${composeFile}`);
5929
7102
  }
5930
7103
  }
5931
- const otelConfigPath = join19(projectDir, "otel-collector-config.yaml");
5932
- if (existsSync20(otelConfigPath)) {
7104
+ const otelConfigPath = join24(projectDir, "otel-collector-config.yaml");
7105
+ if (existsSync27(otelConfigPath)) {
5933
7106
  unlinkSync2(otelConfigPath);
5934
7107
  result.removed.push("otel-collector-config.yaml");
5935
7108
  if (!isJson) {
@@ -5939,8 +7112,8 @@ function registerTeardownCommand(program) {
5939
7112
  }
5940
7113
  let patchesRemoved = 0;
5941
7114
  for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
5942
- const filePath = join19(projectDir, "_bmad", relativePath);
5943
- if (!existsSync20(filePath)) {
7115
+ const filePath = join24(projectDir, "_bmad", relativePath);
7116
+ if (!existsSync27(filePath)) {
5944
7117
  continue;
5945
7118
  }
5946
7119
  try {
@@ -5960,10 +7133,10 @@ function registerTeardownCommand(program) {
5960
7133
  }
5961
7134
  }
5962
7135
  if (state.otlp?.enabled && state.stack === "nodejs") {
5963
- const pkgPath = join19(projectDir, "package.json");
5964
- if (existsSync20(pkgPath)) {
7136
+ const pkgPath = join24(projectDir, "package.json");
7137
+ if (existsSync27(pkgPath)) {
5965
7138
  try {
5966
- const raw = readFileSync17(pkgPath, "utf-8");
7139
+ const raw = readFileSync23(pkgPath, "utf-8");
5967
7140
  const pkg = JSON.parse(raw);
5968
7141
  const scripts = pkg["scripts"];
5969
7142
  if (scripts) {
@@ -5977,7 +7150,7 @@ function registerTeardownCommand(program) {
5977
7150
  for (const key of keysToRemove) {
5978
7151
  delete scripts[key];
5979
7152
  }
5980
- writeFileSync11(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
7153
+ writeFileSync14(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
5981
7154
  result.otlp_cleaned = true;
5982
7155
  if (!isJson) {
5983
7156
  ok("OTLP: removed instrumented scripts from package.json");
@@ -6003,17 +7176,17 @@ function registerTeardownCommand(program) {
6003
7176
  }
6004
7177
  }
6005
7178
  }
6006
- const harnessDir = join19(projectDir, ".harness");
6007
- if (existsSync20(harnessDir)) {
6008
- rmSync(harnessDir, { recursive: true, force: true });
7179
+ const harnessDir = join24(projectDir, ".harness");
7180
+ if (existsSync27(harnessDir)) {
7181
+ rmSync2(harnessDir, { recursive: true, force: true });
6009
7182
  result.removed.push(".harness/");
6010
7183
  if (!isJson) {
6011
7184
  ok("Removed: .harness/");
6012
7185
  }
6013
7186
  }
6014
- const statePath = getStatePath(projectDir);
6015
- if (existsSync20(statePath)) {
6016
- unlinkSync2(statePath);
7187
+ const statePath2 = getStatePath(projectDir);
7188
+ if (existsSync27(statePath2)) {
7189
+ unlinkSync2(statePath2);
6017
7190
  result.removed.push(".claude/codeharness.local.md");
6018
7191
  if (!isJson) {
6019
7192
  ok("Removed: .claude/codeharness.local.md");
@@ -6756,8 +7929,8 @@ function registerQueryCommand(program) {
6756
7929
  }
6757
7930
 
6758
7931
  // src/commands/retro-import.ts
6759
- import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
6760
- import { join as join20 } from "path";
7932
+ import { existsSync as existsSync28, readFileSync as readFileSync24 } from "fs";
7933
+ import { join as join25 } from "path";
6761
7934
 
6762
7935
  // src/lib/retro-parser.ts
6763
7936
  var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
@@ -6814,7 +7987,7 @@ function derivePriority(item) {
6814
7987
  }
6815
7988
 
6816
7989
  // src/lib/github.ts
6817
- import { execFileSync as execFileSync6 } from "child_process";
7990
+ import { execFileSync as execFileSync7 } from "child_process";
6818
7991
  var GitHubError = class extends Error {
6819
7992
  constructor(command, originalMessage) {
6820
7993
  super(`GitHub CLI failed: ${originalMessage}. Command: ${command}`);
@@ -6825,7 +7998,7 @@ var GitHubError = class extends Error {
6825
7998
  };
6826
7999
  function isGhAvailable() {
6827
8000
  try {
6828
- execFileSync6("which", ["gh"], { stdio: "pipe", timeout: 5e3 });
8001
+ execFileSync7("which", ["gh"], { stdio: "pipe", timeout: 5e3 });
6829
8002
  return true;
6830
8003
  } catch {
6831
8004
  return false;
@@ -6839,7 +8012,7 @@ function ghIssueCreate(repo, title, body, labels) {
6839
8012
  args.push("--json", "number,url");
6840
8013
  const cmdStr = `gh ${args.join(" ")}`;
6841
8014
  try {
6842
- const output = execFileSync6("gh", args, {
8015
+ const output = execFileSync7("gh", args, {
6843
8016
  stdio: "pipe",
6844
8017
  timeout: 3e4
6845
8018
  });
@@ -6854,7 +8027,7 @@ function ghIssueSearch(repo, query) {
6854
8027
  const args = ["issue", "list", "--repo", repo, "--search", query, "--state", "all", "--json", "number,title,body,url,labels"];
6855
8028
  const cmdStr = `gh ${args.join(" ")}`;
6856
8029
  try {
6857
- const output = execFileSync6("gh", args, {
8030
+ const output = execFileSync7("gh", args, {
6858
8031
  stdio: "pipe",
6859
8032
  timeout: 3e4
6860
8033
  });
@@ -6876,7 +8049,7 @@ function findExistingGhIssue(repo, gapId) {
6876
8049
  }
6877
8050
  function getRepoFromRemote() {
6878
8051
  try {
6879
- const output = execFileSync6("git", ["remote", "get-url", "origin"], {
8052
+ const output = execFileSync7("git", ["remote", "get-url", "origin"], {
6880
8053
  stdio: "pipe",
6881
8054
  timeout: 5e3
6882
8055
  });
@@ -6896,7 +8069,7 @@ function parseRepoFromUrl(url) {
6896
8069
  function ensureLabels(repo, labels) {
6897
8070
  for (const label of labels) {
6898
8071
  try {
6899
- execFileSync6("gh", ["label", "create", label, "--repo", repo], {
8072
+ execFileSync7("gh", ["label", "create", label, "--repo", repo], {
6900
8073
  stdio: "pipe",
6901
8074
  timeout: 1e4
6902
8075
  });
@@ -6906,7 +8079,7 @@ function ensureLabels(repo, labels) {
6906
8079
  }
6907
8080
 
6908
8081
  // src/commands/retro-import.ts
6909
- var STORY_DIR2 = "_bmad-output/implementation-artifacts";
8082
+ var STORY_DIR3 = "_bmad-output/implementation-artifacts";
6910
8083
  var MAX_TITLE_LENGTH = 120;
6911
8084
  function classificationToString(c) {
6912
8085
  if (c.type === "tool") {
@@ -6926,15 +8099,15 @@ function registerRetroImportCommand(program) {
6926
8099
  return;
6927
8100
  }
6928
8101
  const retroFile = `epic-${epicNum}-retrospective.md`;
6929
- const retroPath = join20(root, STORY_DIR2, retroFile);
6930
- if (!existsSync21(retroPath)) {
8102
+ const retroPath = join25(root, STORY_DIR3, retroFile);
8103
+ if (!existsSync28(retroPath)) {
6931
8104
  fail(`Retro file not found: ${retroFile}`, { json: isJson });
6932
8105
  process.exitCode = 1;
6933
8106
  return;
6934
8107
  }
6935
8108
  let content;
6936
8109
  try {
6937
- content = readFileSync18(retroPath, "utf-8");
8110
+ content = readFileSync24(retroPath, "utf-8");
6938
8111
  } catch (err) {
6939
8112
  const message = err instanceof Error ? err.message : String(err);
6940
8113
  fail(`Failed to read retro file: ${message}`, { json: isJson });
@@ -7200,292 +8373,6 @@ function registerGithubImportCommand(program) {
7200
8373
  });
7201
8374
  }
7202
8375
 
7203
- // src/lib/verify-env.ts
7204
- import { execFileSync as execFileSync7 } from "child_process";
7205
- import { existsSync as existsSync22, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync19, cpSync, rmSync as rmSync2, statSync as statSync3 } from "fs";
7206
- import { join as join21, basename as basename3 } from "path";
7207
- import { createHash } from "crypto";
7208
- var IMAGE_TAG = "codeharness-verify";
7209
- var STORY_DIR3 = "_bmad-output/implementation-artifacts";
7210
- var TEMP_PREFIX = "/tmp/codeharness-verify-";
7211
- var STATE_KEY_DIST_HASH = "verify_env_dist_hash";
7212
- function isValidStoryKey(storyKey) {
7213
- if (!storyKey || storyKey.includes("..") || storyKey.includes("/") || storyKey.includes("\\")) {
7214
- return false;
7215
- }
7216
- return /^[a-zA-Z0-9_-]+$/.test(storyKey);
7217
- }
7218
- function computeDistHash(projectDir) {
7219
- const distDir = join21(projectDir, "dist");
7220
- if (!existsSync22(distDir)) {
7221
- return null;
7222
- }
7223
- const hash = createHash("sha256");
7224
- const files = collectFiles(distDir).sort();
7225
- for (const file of files) {
7226
- const content = readFileSync19(file);
7227
- hash.update(file.slice(distDir.length));
7228
- hash.update(content);
7229
- }
7230
- return hash.digest("hex");
7231
- }
7232
- function collectFiles(dir) {
7233
- const results = [];
7234
- const entries = readdirSync4(dir, { withFileTypes: true });
7235
- for (const entry of entries) {
7236
- const fullPath = join21(dir, entry.name);
7237
- if (entry.isDirectory()) {
7238
- results.push(...collectFiles(fullPath));
7239
- } else {
7240
- results.push(fullPath);
7241
- }
7242
- }
7243
- return results;
7244
- }
7245
- function getStoredDistHash(projectDir) {
7246
- try {
7247
- const { state } = readStateWithBody(projectDir);
7248
- const raw = state;
7249
- return raw[STATE_KEY_DIST_HASH] ?? null;
7250
- } catch {
7251
- return null;
7252
- }
7253
- }
7254
- function storeDistHash(projectDir, hash) {
7255
- try {
7256
- const { state, body } = readStateWithBody(projectDir);
7257
- const raw = state;
7258
- raw[STATE_KEY_DIST_HASH] = hash;
7259
- writeState(state, projectDir, body);
7260
- } catch {
7261
- info("Could not persist dist hash to state file \u2014 cache will not be available until state is initialized");
7262
- }
7263
- }
7264
- function buildVerifyImage(options = {}) {
7265
- const projectDir = options.projectDir ?? process.cwd();
7266
- if (!isDockerAvailable()) {
7267
- throw new Error("Docker is not available. Install Docker and ensure the daemon is running.");
7268
- }
7269
- const stack = detectStack(projectDir);
7270
- if (!stack) {
7271
- throw new Error("Cannot detect project stack. Ensure package.json (Node.js) or requirements.txt/pyproject.toml (Python) exists.");
7272
- }
7273
- const currentHash = computeDistHash(projectDir);
7274
- if (!currentHash) {
7275
- throw new Error("No dist/ directory found. Run your build command first (e.g., npm run build).");
7276
- }
7277
- const storedHash = getStoredDistHash(projectDir);
7278
- if (storedHash === currentHash) {
7279
- if (dockerImageExists(IMAGE_TAG)) {
7280
- const imageSize2 = getImageSize(IMAGE_TAG);
7281
- return { imageTag: IMAGE_TAG, imageSize: imageSize2, buildTimeMs: 0, cached: true };
7282
- }
7283
- }
7284
- const startTime = Date.now();
7285
- if (stack === "nodejs") {
7286
- buildNodeImage(projectDir);
7287
- } else if (stack === "python") {
7288
- buildPythonImage(projectDir);
7289
- } else {
7290
- throw new Error(`Unsupported stack for verify-env: ${stack}`);
7291
- }
7292
- const buildTimeMs = Date.now() - startTime;
7293
- storeDistHash(projectDir, currentHash);
7294
- const imageSize = getImageSize(IMAGE_TAG);
7295
- return { imageTag: IMAGE_TAG, imageSize, buildTimeMs, cached: false };
7296
- }
7297
- function buildNodeImage(projectDir) {
7298
- const packOutput = execFileSync7("npm", ["pack", "--pack-destination", "/tmp"], {
7299
- cwd: projectDir,
7300
- stdio: "pipe",
7301
- timeout: 6e4
7302
- }).toString().trim();
7303
- const lastLine = packOutput.split("\n").pop()?.trim();
7304
- if (!lastLine) {
7305
- throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
7306
- }
7307
- const tarballName = basename3(lastLine);
7308
- const tarballPath = join21("/tmp", tarballName);
7309
- const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
7310
- mkdirSync8(buildContext, { recursive: true });
7311
- try {
7312
- cpSync(tarballPath, join21(buildContext, tarballName));
7313
- const dockerfileSrc = resolveDockerfileTemplate(projectDir);
7314
- cpSync(dockerfileSrc, join21(buildContext, "Dockerfile"));
7315
- execFileSync7("docker", [
7316
- "build",
7317
- "-t",
7318
- IMAGE_TAG,
7319
- "--build-arg",
7320
- `TARBALL=${tarballName}`,
7321
- "."
7322
- ], {
7323
- cwd: buildContext,
7324
- stdio: "pipe",
7325
- timeout: 12e4
7326
- });
7327
- } finally {
7328
- rmSync2(buildContext, { recursive: true, force: true });
7329
- rmSync2(tarballPath, { force: true });
7330
- }
7331
- }
7332
- function buildPythonImage(projectDir) {
7333
- const distDir = join21(projectDir, "dist");
7334
- const distFiles = readdirSync4(distDir).filter(
7335
- (f) => f.endsWith(".tar.gz") || f.endsWith(".whl")
7336
- );
7337
- if (distFiles.length === 0) {
7338
- throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
7339
- }
7340
- const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
7341
- const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
7342
- mkdirSync8(buildContext, { recursive: true });
7343
- try {
7344
- cpSync(join21(distDir, distFile), join21(buildContext, distFile));
7345
- const dockerfileSrc = resolveDockerfileTemplate(projectDir);
7346
- cpSync(dockerfileSrc, join21(buildContext, "Dockerfile"));
7347
- execFileSync7("docker", [
7348
- "build",
7349
- "-t",
7350
- IMAGE_TAG,
7351
- "--build-arg",
7352
- `TARBALL=${distFile}`,
7353
- "."
7354
- ], {
7355
- cwd: buildContext,
7356
- stdio: "pipe",
7357
- timeout: 12e4
7358
- });
7359
- } finally {
7360
- rmSync2(buildContext, { recursive: true, force: true });
7361
- }
7362
- }
7363
- function prepareVerifyWorkspace(storyKey, projectDir) {
7364
- const root = projectDir ?? process.cwd();
7365
- if (!isValidStoryKey(storyKey)) {
7366
- throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
7367
- }
7368
- const storyFile = join21(root, STORY_DIR3, `${storyKey}.md`);
7369
- if (!existsSync22(storyFile)) {
7370
- throw new Error(`Story file not found: ${storyFile}`);
7371
- }
7372
- const workspace = `${TEMP_PREFIX}${storyKey}`;
7373
- if (existsSync22(workspace)) {
7374
- rmSync2(workspace, { recursive: true, force: true });
7375
- }
7376
- mkdirSync8(workspace, { recursive: true });
7377
- cpSync(storyFile, join21(workspace, "story.md"));
7378
- const readmePath = join21(root, "README.md");
7379
- if (existsSync22(readmePath)) {
7380
- cpSync(readmePath, join21(workspace, "README.md"));
7381
- }
7382
- const docsDir = join21(root, "docs");
7383
- if (existsSync22(docsDir) && statSync3(docsDir).isDirectory()) {
7384
- cpSync(docsDir, join21(workspace, "docs"), { recursive: true });
7385
- }
7386
- mkdirSync8(join21(workspace, "verification"), { recursive: true });
7387
- return workspace;
7388
- }
7389
- function checkVerifyEnv() {
7390
- const result = {
7391
- imageExists: false,
7392
- cliWorks: false,
7393
- otelReachable: false
7394
- };
7395
- result.imageExists = dockerImageExists(IMAGE_TAG);
7396
- if (!result.imageExists) {
7397
- return result;
7398
- }
7399
- try {
7400
- execFileSync7("docker", ["run", "--rm", IMAGE_TAG, "codeharness", "--help"], {
7401
- stdio: "pipe",
7402
- timeout: 3e4
7403
- });
7404
- result.cliWorks = true;
7405
- } catch {
7406
- result.cliWorks = false;
7407
- }
7408
- try {
7409
- execFileSync7("docker", [
7410
- "run",
7411
- "--rm",
7412
- "--add-host=host.docker.internal:host-gateway",
7413
- IMAGE_TAG,
7414
- "curl",
7415
- "-sf",
7416
- "--max-time",
7417
- "5",
7418
- "http://host.docker.internal:4318/v1/status"
7419
- ], {
7420
- stdio: "pipe",
7421
- timeout: 3e4
7422
- });
7423
- result.otelReachable = true;
7424
- } catch {
7425
- result.otelReachable = false;
7426
- }
7427
- return result;
7428
- }
7429
- function cleanupVerifyEnv(storyKey) {
7430
- if (!isValidStoryKey(storyKey)) {
7431
- throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
7432
- }
7433
- const workspace = `${TEMP_PREFIX}${storyKey}`;
7434
- const containerName = `codeharness-verify-${storyKey}`;
7435
- if (existsSync22(workspace)) {
7436
- rmSync2(workspace, { recursive: true, force: true });
7437
- }
7438
- try {
7439
- execFileSync7("docker", ["stop", containerName], {
7440
- stdio: "pipe",
7441
- timeout: 15e3
7442
- });
7443
- } catch {
7444
- }
7445
- try {
7446
- execFileSync7("docker", ["rm", "-f", containerName], {
7447
- stdio: "pipe",
7448
- timeout: 15e3
7449
- });
7450
- } catch {
7451
- }
7452
- }
7453
- function resolveDockerfileTemplate(projectDir) {
7454
- const local = join21(projectDir, "templates", "Dockerfile.verify");
7455
- if (existsSync22(local)) return local;
7456
- const pkgDir = new URL("../../", import.meta.url).pathname;
7457
- const pkg = join21(pkgDir, "templates", "Dockerfile.verify");
7458
- if (existsSync22(pkg)) return pkg;
7459
- throw new Error("Dockerfile.verify not found. Ensure templates/Dockerfile.verify exists in the project or installed package.");
7460
- }
7461
- function dockerImageExists(tag) {
7462
- try {
7463
- execFileSync7("docker", ["image", "inspect", tag], {
7464
- stdio: "pipe",
7465
- timeout: 1e4
7466
- });
7467
- return true;
7468
- } catch {
7469
- return false;
7470
- }
7471
- }
7472
- function getImageSize(tag) {
7473
- try {
7474
- const output = execFileSync7("docker", ["image", "inspect", tag, "--format", "{{.Size}}"], {
7475
- stdio: "pipe",
7476
- timeout: 1e4
7477
- }).toString().trim();
7478
- const bytes = parseInt(output, 10);
7479
- if (isNaN(bytes)) return output;
7480
- if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)}GB`;
7481
- if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(1)}MB`;
7482
- if (bytes >= 1e3) return `${(bytes / 1e3).toFixed(1)}KB`;
7483
- return `${bytes}B`;
7484
- } catch {
7485
- return "unknown";
7486
- }
7487
- }
7488
-
7489
8376
  // src/commands/verify-env.ts
7490
8377
  function registerVerifyEnvCommand(program) {
7491
8378
  const verifyEnv = program.command("verify-env").description("Manage verification environment (Docker image + clean workspace)");
@@ -7601,26 +8488,26 @@ function registerVerifyEnvCommand(program) {
7601
8488
  }
7602
8489
 
7603
8490
  // src/commands/retry.ts
7604
- import { join as join23 } from "path";
8491
+ import { join as join27 } from "path";
7605
8492
 
7606
8493
  // src/lib/retry-state.ts
7607
- import { existsSync as existsSync23, readFileSync as readFileSync20, writeFileSync as writeFileSync12 } from "fs";
7608
- import { join as join22 } from "path";
8494
+ import { existsSync as existsSync29, readFileSync as readFileSync25, writeFileSync as writeFileSync15 } from "fs";
8495
+ import { join as join26 } from "path";
7609
8496
  var RETRIES_FILE = ".story_retries";
7610
8497
  var FLAGGED_FILE = ".flagged_stories";
7611
8498
  var LINE_PATTERN = /^([^=]+)=(\d+)$/;
7612
8499
  function retriesPath(dir) {
7613
- return join22(dir, RETRIES_FILE);
8500
+ return join26(dir, RETRIES_FILE);
7614
8501
  }
7615
8502
  function flaggedPath(dir) {
7616
- return join22(dir, FLAGGED_FILE);
8503
+ return join26(dir, FLAGGED_FILE);
7617
8504
  }
7618
8505
  function readRetries(dir) {
7619
8506
  const filePath = retriesPath(dir);
7620
- if (!existsSync23(filePath)) {
8507
+ if (!existsSync29(filePath)) {
7621
8508
  return /* @__PURE__ */ new Map();
7622
8509
  }
7623
- const raw = readFileSync20(filePath, "utf-8");
8510
+ const raw = readFileSync25(filePath, "utf-8");
7624
8511
  const result = /* @__PURE__ */ new Map();
7625
8512
  for (const line of raw.split("\n")) {
7626
8513
  const trimmed = line.trim();
@@ -7642,7 +8529,7 @@ function writeRetries(dir, retries) {
7642
8529
  for (const [key, count] of retries) {
7643
8530
  lines.push(`${key}=${count}`);
7644
8531
  }
7645
- writeFileSync12(filePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
8532
+ writeFileSync15(filePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
7646
8533
  }
7647
8534
  function resetRetry(dir, storyKey) {
7648
8535
  if (storyKey) {
@@ -7657,15 +8544,15 @@ function resetRetry(dir, storyKey) {
7657
8544
  }
7658
8545
  function readFlaggedStories(dir) {
7659
8546
  const filePath = flaggedPath(dir);
7660
- if (!existsSync23(filePath)) {
8547
+ if (!existsSync29(filePath)) {
7661
8548
  return [];
7662
8549
  }
7663
- const raw = readFileSync20(filePath, "utf-8");
8550
+ const raw = readFileSync25(filePath, "utf-8");
7664
8551
  return raw.split("\n").map((l) => l.trim()).filter((l) => l !== "");
7665
8552
  }
7666
8553
  function writeFlaggedStories(dir, stories) {
7667
8554
  const filePath = flaggedPath(dir);
7668
- writeFileSync12(filePath, stories.length > 0 ? stories.join("\n") + "\n" : "", "utf-8");
8555
+ writeFileSync15(filePath, stories.length > 0 ? stories.join("\n") + "\n" : "", "utf-8");
7669
8556
  }
7670
8557
  function removeFlaggedStory(dir, key) {
7671
8558
  const stories = readFlaggedStories(dir);
@@ -7685,7 +8572,7 @@ function registerRetryCommand(program) {
7685
8572
  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) => {
7686
8573
  const opts = cmd.optsWithGlobals();
7687
8574
  const isJson = opts.json === true;
7688
- const dir = join23(process.cwd(), RALPH_SUBDIR);
8575
+ const dir = join27(process.cwd(), RALPH_SUBDIR);
7689
8576
  if (opts.story && !isValidStoryKey3(opts.story)) {
7690
8577
  if (isJson) {
7691
8578
  jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
@@ -7758,8 +8645,101 @@ function handleStatus(dir, isJson, filterStory) {
7758
8645
  }
7759
8646
  }
7760
8647
 
8648
+ // src/commands/timeout-report.ts
8649
+ function registerTimeoutReportCommand(program) {
8650
+ program.command("timeout-report").description("Capture diagnostic data from a timed-out iteration").requiredOption("--story <key>", "Story key").requiredOption("--iteration <n>", "Iteration number").requiredOption("--duration <minutes>", "Timeout duration in minutes").requiredOption("--output-file <path>", "Path to iteration output log").requiredOption("--state-snapshot <path>", "Path to pre-iteration state snapshot").action((options, cmd) => {
8651
+ const opts = cmd.optsWithGlobals();
8652
+ const isJson = opts.json === true;
8653
+ const iteration = parseInt(options.iteration, 10);
8654
+ const duration = parseInt(options.duration, 10);
8655
+ if (isNaN(iteration) || isNaN(duration)) {
8656
+ if (isJson) {
8657
+ jsonOutput({ status: "fail", message: "iteration and duration must be numbers" });
8658
+ } else {
8659
+ fail("iteration and duration must be numbers");
8660
+ }
8661
+ process.exitCode = 1;
8662
+ return;
8663
+ }
8664
+ const result = captureTimeoutReport2({
8665
+ storyKey: options.story,
8666
+ iteration,
8667
+ durationMinutes: duration,
8668
+ outputFile: options.outputFile,
8669
+ stateSnapshotPath: options.stateSnapshot
8670
+ });
8671
+ if (!result.success) {
8672
+ if (isJson) {
8673
+ jsonOutput({ status: "fail", message: result.error });
8674
+ } else {
8675
+ fail(result.error);
8676
+ }
8677
+ process.exitCode = 1;
8678
+ return;
8679
+ }
8680
+ if (isJson) {
8681
+ jsonOutput({
8682
+ status: "ok",
8683
+ reportPath: result.data.filePath,
8684
+ storyKey: result.data.capture.storyKey,
8685
+ iteration: result.data.capture.iteration
8686
+ });
8687
+ } else {
8688
+ ok(`Timeout report written: ${result.data.filePath}`);
8689
+ }
8690
+ });
8691
+ }
8692
+
8693
+ // src/commands/validate-state.ts
8694
+ import { resolve as resolve2 } from "path";
8695
+ function registerValidateStateCommand(program) {
8696
+ program.command("validate-state").description("Validate sprint-state.json consistency against sprint-status.yaml").option("--state <path>", "Path to sprint-state.json", "sprint-state.json").option("--sprint-status <path>", "Path to sprint-status.yaml", "sprint-status.yaml").action((options, cmd) => {
8697
+ const opts = cmd.optsWithGlobals();
8698
+ const isJson = opts.json === true;
8699
+ const statePath2 = resolve2(process.cwd(), options.state);
8700
+ const sprintStatusPath = resolve2(process.cwd(), options.sprintStatus);
8701
+ const result = validateStateConsistency2(statePath2, sprintStatusPath);
8702
+ if (!result.success) {
8703
+ if (isJson) {
8704
+ jsonOutput({ status: "fail", message: result.error });
8705
+ } else {
8706
+ fail(result.error);
8707
+ }
8708
+ process.exitCode = 1;
8709
+ return;
8710
+ }
8711
+ const report = result.data;
8712
+ if (isJson) {
8713
+ jsonOutput({
8714
+ status: report.invalidCount === 0 ? "ok" : "fail",
8715
+ totalStories: report.totalStories,
8716
+ validCount: report.validCount,
8717
+ invalidCount: report.invalidCount,
8718
+ missingKeys: report.missingKeys,
8719
+ issues: report.issues
8720
+ });
8721
+ } else {
8722
+ console.log(`Total stories: ${report.totalStories}`);
8723
+ console.log(`Valid: ${report.validCount}`);
8724
+ console.log(`Invalid: ${report.invalidCount}`);
8725
+ if (report.missingKeys.length > 0) {
8726
+ console.log(`Missing keys: ${report.missingKeys.join(", ")}`);
8727
+ }
8728
+ for (const issue of report.issues) {
8729
+ console.log(` [${issue.storyKey}] ${issue.field}: ${issue.message}`);
8730
+ }
8731
+ if (report.invalidCount === 0) {
8732
+ ok("All stories valid");
8733
+ } else {
8734
+ fail(`${report.invalidCount} story/stories have issues`);
8735
+ }
8736
+ }
8737
+ process.exitCode = report.invalidCount === 0 ? 0 : 1;
8738
+ });
8739
+ }
8740
+
7761
8741
  // src/index.ts
7762
- var VERSION = true ? "0.19.1" : "0.0.0-dev";
8742
+ var VERSION = true ? "0.19.3" : "0.0.0-dev";
7763
8743
  function createProgram() {
7764
8744
  const program = new Command();
7765
8745
  program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
@@ -7780,6 +8760,8 @@ function createProgram() {
7780
8760
  registerGithubImportCommand(program);
7781
8761
  registerVerifyEnvCommand(program);
7782
8762
  registerRetryCommand(program);
8763
+ registerTimeoutReportCommand(program);
8764
+ registerValidateStateCommand(program);
7783
8765
  return program;
7784
8766
  }
7785
8767
  if (!process.env["VITEST"]) {