codeharness 0.19.2 → 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.
- package/dist/index.js +1117 -936
- package/package.json +1 -1
- package/ralph/ralph.sh +1 -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.
|
|
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");
|
|
@@ -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", "
|
|
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((
|
|
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
|
-
|
|
2676
|
+
resolve3(code ?? 1);
|
|
2677
2677
|
});
|
|
2678
2678
|
});
|
|
2679
2679
|
if (isJson) {
|
|
@@ -2730,216 +2730,323 @@ function registerRunCommand(program) {
|
|
|
2730
2730
|
}
|
|
2731
2731
|
|
|
2732
2732
|
// src/commands/verify.ts
|
|
2733
|
-
import { existsSync as
|
|
2734
|
-
import { join as
|
|
2733
|
+
import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
|
|
2734
|
+
import { join as join13 } from "path";
|
|
2735
2735
|
|
|
2736
|
-
// src/
|
|
2737
|
-
|
|
2738
|
-
|
|
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
|
|
2782
|
-
|
|
2783
|
-
|
|
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
|
|
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
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
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
|
|
2761
|
+
return results;
|
|
2805
2762
|
}
|
|
2806
|
-
function
|
|
2807
|
-
if (
|
|
2808
|
-
|
|
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 (
|
|
2822
|
-
|
|
2823
|
-
return [];
|
|
2767
|
+
if (/docker\s+(ps|logs|inspect|stats|top|port)\b/.test(cmd)) {
|
|
2768
|
+
return "docker-host";
|
|
2824
2769
|
}
|
|
2825
|
-
|
|
2826
|
-
|
|
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
|
-
|
|
2833
|
-
|
|
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
|
-
|
|
2868
|
-
return acs;
|
|
2776
|
+
return "other";
|
|
2869
2777
|
}
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
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
|
-
|
|
2936
|
-
|
|
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
|
+
};
|
|
2937
2817
|
}
|
|
2938
|
-
function
|
|
2939
|
-
|
|
2940
|
-
return dot >= 0 ? filename.slice(dot) : "";
|
|
2818
|
+
function hasFailVerdict(section) {
|
|
2819
|
+
return section.replace(/```[\s\S]*?```/g, "").replace(/`[^`]+`/g, "").includes("[FAIL]");
|
|
2941
2820
|
}
|
|
2942
|
-
function
|
|
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) {
|
|
2943
3050
|
return filename.includes(".test.") || filename.includes(".spec.") || filename.includes("__tests__") || filename.startsWith("test_");
|
|
2944
3051
|
}
|
|
2945
3052
|
function isDocStale(docPath, codeDir) {
|
|
@@ -3408,105 +3515,31 @@ function showboatProofTemplate(config) {
|
|
|
3408
3515
|
return sections.join("\n");
|
|
3409
3516
|
}
|
|
3410
3517
|
|
|
3411
|
-
// src/
|
|
3412
|
-
function
|
|
3413
|
-
const
|
|
3414
|
-
const
|
|
3415
|
-
|
|
3416
|
-
|
|
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
|
-
}
|
|
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");
|
|
3423
3524
|
}
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
function classifyCommand(cmd) {
|
|
3427
|
-
if (/docker\s+exec\b/.test(cmd)) {
|
|
3428
|
-
return "docker-exec";
|
|
3525
|
+
if (!state.session_flags.coverage_met) {
|
|
3526
|
+
failures.push("coverage_met is false \u2014 ensure coverage target is met");
|
|
3429
3527
|
}
|
|
3430
|
-
if (
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
}
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
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");
|
|
3494
|
-
}
|
|
3495
|
-
if (storyId) {
|
|
3496
|
-
try {
|
|
3497
|
-
const docReport = checkStoryDocFreshness(storyId, dir);
|
|
3498
|
-
if (!docReport.passed) {
|
|
3499
|
-
for (const doc of docReport.documents) {
|
|
3500
|
-
if (doc.grade === "stale") {
|
|
3501
|
-
failures.push(doc.reason);
|
|
3502
|
-
} else if (doc.grade === "missing") {
|
|
3503
|
-
failures.push(doc.reason);
|
|
3504
|
-
}
|
|
3505
|
-
}
|
|
3506
|
-
}
|
|
3507
|
-
} catch {
|
|
3508
|
-
warn("Doc health check failed \u2014 skipping");
|
|
3509
|
-
}
|
|
3528
|
+
if (storyId) {
|
|
3529
|
+
try {
|
|
3530
|
+
const docReport = checkStoryDocFreshness(storyId, dir);
|
|
3531
|
+
if (!docReport.passed) {
|
|
3532
|
+
for (const doc of docReport.documents) {
|
|
3533
|
+
if (doc.grade === "stale") {
|
|
3534
|
+
failures.push(doc.reason);
|
|
3535
|
+
} else if (doc.grade === "missing") {
|
|
3536
|
+
failures.push(doc.reason);
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
} catch {
|
|
3541
|
+
warn("Doc health check failed \u2014 skipping");
|
|
3542
|
+
}
|
|
3510
3543
|
}
|
|
3511
3544
|
return {
|
|
3512
3545
|
passed: failures.length === 0,
|
|
@@ -3551,194 +3584,455 @@ function runShowboatVerify(proofPath) {
|
|
|
3551
3584
|
return { passed: false, output: stdout || stderr || message };
|
|
3552
3585
|
}
|
|
3553
3586
|
}
|
|
3554
|
-
function
|
|
3555
|
-
const
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
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
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
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
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
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
|
-
}
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
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
|
|
3654
|
-
});
|
|
3655
|
-
}
|
|
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
|
-
});
|
|
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;
|
|
3679
3712
|
}
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
const
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
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
|
|
3733
|
+
});
|
|
3697
3734
|
} else {
|
|
3698
|
-
|
|
3735
|
+
warn(`Skipping malformed AC #${currentId}: empty description`);
|
|
3699
3736
|
}
|
|
3700
3737
|
}
|
|
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
|
+
}
|
|
3701
3788
|
}
|
|
3702
|
-
|
|
3703
|
-
return buildResult({ verified, pending, escalated, total });
|
|
3789
|
+
return results;
|
|
3704
3790
|
}
|
|
3705
|
-
function
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
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
|
+
}
|
|
3712
3798
|
}
|
|
3713
|
-
function
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
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);
|
|
3718
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 });
|
|
3719
3859
|
try {
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
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
|
|
3724
3866
|
});
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
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 });
|
|
3972
|
+
}
|
|
3728
3973
|
}
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
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 });
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
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.`);
|
|
4006
|
+
}
|
|
4007
|
+
function dockerImageExists(tag) {
|
|
4008
|
+
try {
|
|
4009
|
+
execFileSync6("docker", ["image", "inspect", tag], { stdio: "pipe", timeout: 1e4 });
|
|
4010
|
+
return true;
|
|
4011
|
+
} catch {
|
|
4012
|
+
return false;
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
function getImageSize(tag) {
|
|
4016
|
+
try {
|
|
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
|
|
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 =
|
|
3775
|
-
if (!
|
|
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:
|
|
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 =
|
|
3804
|
-
if (!
|
|
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 =
|
|
3814
|
-
if (!
|
|
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 =
|
|
3855
|
-
const proofPath =
|
|
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 =
|
|
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
|
|
3959
|
-
import { join as
|
|
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
|
|
3965
|
-
import { join as
|
|
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 =
|
|
3989
|
-
const pkgPath =
|
|
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 (
|
|
4289
|
+
if (existsSync17(pkgPath)) {
|
|
3996
4290
|
try {
|
|
3997
|
-
const pkg = JSON.parse(
|
|
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 =
|
|
4051
|
-
if (
|
|
4344
|
+
const reqPath = join14(dir, "requirements.txt");
|
|
4345
|
+
if (existsSync17(reqPath)) {
|
|
4052
4346
|
try {
|
|
4053
|
-
const content =
|
|
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 =
|
|
4065
|
-
if (
|
|
4358
|
+
const pyprojectPath = join14(dir, "pyproject.toml");
|
|
4359
|
+
if (existsSync17(pyprojectPath)) {
|
|
4066
4360
|
try {
|
|
4067
|
-
const content =
|
|
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(
|
|
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 =
|
|
4158
|
-
if (!
|
|
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(
|
|
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(
|
|
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
|
-
|
|
4295
|
-
|
|
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 (
|
|
4592
|
+
if (existsSync17(p)) return p;
|
|
4299
4593
|
}
|
|
4300
4594
|
return null;
|
|
4301
4595
|
}
|
|
@@ -4320,7 +4614,7 @@ function printCoverageOutput(result, evaluation) {
|
|
|
4320
4614
|
// src/lib/onboard-checks.ts
|
|
4321
4615
|
function checkHarnessInitialized(dir) {
|
|
4322
4616
|
const statePath2 = getStatePath(dir ?? process.cwd());
|
|
4323
|
-
return { ok:
|
|
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 =
|
|
4332
|
-
return { ok:
|
|
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 =
|
|
4373
|
-
if (!
|
|
4666
|
+
const proofPath = join15(root, "verification", `${key}-proof.md`);
|
|
4667
|
+
if (!existsSync18(proofPath)) {
|
|
4374
4668
|
unverified.push(key);
|
|
4375
4669
|
}
|
|
4376
4670
|
}
|
|
@@ -4497,24 +4791,13 @@ function filterTrackedGaps(stories, beadsFns) {
|
|
|
4497
4791
|
return { untracked, trackedCount };
|
|
4498
4792
|
}
|
|
4499
4793
|
|
|
4500
|
-
// src/types/result.ts
|
|
4501
|
-
function ok2(data) {
|
|
4502
|
-
return { success: true, data };
|
|
4503
|
-
}
|
|
4504
|
-
function fail2(error, context) {
|
|
4505
|
-
if (context !== void 0) {
|
|
4506
|
-
return { success: false, error, context };
|
|
4507
|
-
}
|
|
4508
|
-
return { success: false, error };
|
|
4509
|
-
}
|
|
4510
|
-
|
|
4511
4794
|
// src/modules/sprint/state.ts
|
|
4512
|
-
import { readFileSync as
|
|
4513
|
-
import { join as
|
|
4795
|
+
import { readFileSync as readFileSync17, writeFileSync as writeFileSync9, renameSync, existsSync as existsSync20 } from "fs";
|
|
4796
|
+
import { join as join17 } from "path";
|
|
4514
4797
|
|
|
4515
4798
|
// src/modules/sprint/migration.ts
|
|
4516
|
-
import { readFileSync as
|
|
4517
|
-
import { join as
|
|
4799
|
+
import { readFileSync as readFileSync16, existsSync as existsSync19 } from "fs";
|
|
4800
|
+
import { join as join16 } from "path";
|
|
4518
4801
|
var OLD_FILES = {
|
|
4519
4802
|
storyRetries: "ralph/.story_retries",
|
|
4520
4803
|
flaggedStories: "ralph/.flagged_stories",
|
|
@@ -4523,13 +4806,13 @@ var OLD_FILES = {
|
|
|
4523
4806
|
sessionIssues: "_bmad-output/implementation-artifacts/.session-issues.md"
|
|
4524
4807
|
};
|
|
4525
4808
|
function resolve(relative3) {
|
|
4526
|
-
return
|
|
4809
|
+
return join16(process.cwd(), relative3);
|
|
4527
4810
|
}
|
|
4528
4811
|
function readIfExists(relative3) {
|
|
4529
4812
|
const p = resolve(relative3);
|
|
4530
|
-
if (!
|
|
4813
|
+
if (!existsSync19(p)) return null;
|
|
4531
4814
|
try {
|
|
4532
|
-
return
|
|
4815
|
+
return readFileSync16(p, "utf-8");
|
|
4533
4816
|
} catch {
|
|
4534
4817
|
return null;
|
|
4535
4818
|
}
|
|
@@ -4629,7 +4912,7 @@ function parseSessionIssues(content) {
|
|
|
4629
4912
|
return items;
|
|
4630
4913
|
}
|
|
4631
4914
|
function migrateFromOldFormat() {
|
|
4632
|
-
const hasAnyOldFile = Object.values(OLD_FILES).some((rel) =>
|
|
4915
|
+
const hasAnyOldFile = Object.values(OLD_FILES).some((rel) => existsSync19(resolve(rel)));
|
|
4633
4916
|
if (!hasAnyOldFile) return fail2("No old format files found for migration");
|
|
4634
4917
|
try {
|
|
4635
4918
|
const stories = {};
|
|
@@ -4670,10 +4953,10 @@ function projectRoot() {
|
|
|
4670
4953
|
return process.cwd();
|
|
4671
4954
|
}
|
|
4672
4955
|
function statePath() {
|
|
4673
|
-
return
|
|
4956
|
+
return join17(projectRoot(), "sprint-state.json");
|
|
4674
4957
|
}
|
|
4675
4958
|
function tmpPath() {
|
|
4676
|
-
return
|
|
4959
|
+
return join17(projectRoot(), ".sprint-state.json.tmp");
|
|
4677
4960
|
}
|
|
4678
4961
|
function defaultState() {
|
|
4679
4962
|
return {
|
|
@@ -4712,9 +4995,9 @@ function writeStateAtomic(state) {
|
|
|
4712
4995
|
}
|
|
4713
4996
|
function getSprintState() {
|
|
4714
4997
|
const fp = statePath();
|
|
4715
|
-
if (
|
|
4998
|
+
if (existsSync20(fp)) {
|
|
4716
4999
|
try {
|
|
4717
|
-
const raw =
|
|
5000
|
+
const raw = readFileSync17(fp, "utf-8");
|
|
4718
5001
|
const parsed = JSON.parse(raw);
|
|
4719
5002
|
return ok2(parsed);
|
|
4720
5003
|
} catch (err) {
|
|
@@ -4964,9 +5247,9 @@ function generateReport(state, now) {
|
|
|
4964
5247
|
}
|
|
4965
5248
|
|
|
4966
5249
|
// src/modules/sprint/timeout.ts
|
|
4967
|
-
import { readFileSync as
|
|
5250
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync10, existsSync as existsSync21, mkdirSync as mkdirSync7 } from "fs";
|
|
4968
5251
|
import { execSync as execSync3 } from "child_process";
|
|
4969
|
-
import { join as
|
|
5252
|
+
import { join as join18 } from "path";
|
|
4970
5253
|
var GIT_TIMEOUT_MS = 5e3;
|
|
4971
5254
|
var DEFAULT_MAX_LINES = 100;
|
|
4972
5255
|
function captureGitDiff() {
|
|
@@ -4995,14 +5278,14 @@ function captureGitDiff() {
|
|
|
4995
5278
|
}
|
|
4996
5279
|
function captureStateDelta(beforePath, afterPath) {
|
|
4997
5280
|
try {
|
|
4998
|
-
if (!
|
|
5281
|
+
if (!existsSync21(beforePath)) {
|
|
4999
5282
|
return fail2(`State snapshot not found: ${beforePath}`);
|
|
5000
5283
|
}
|
|
5001
|
-
if (!
|
|
5284
|
+
if (!existsSync21(afterPath)) {
|
|
5002
5285
|
return fail2(`Current state file not found: ${afterPath}`);
|
|
5003
5286
|
}
|
|
5004
|
-
const beforeRaw =
|
|
5005
|
-
const afterRaw =
|
|
5287
|
+
const beforeRaw = readFileSync18(beforePath, "utf-8");
|
|
5288
|
+
const afterRaw = readFileSync18(afterPath, "utf-8");
|
|
5006
5289
|
const before = JSON.parse(beforeRaw);
|
|
5007
5290
|
const after = JSON.parse(afterRaw);
|
|
5008
5291
|
const beforeStories = before.stories ?? {};
|
|
@@ -5027,10 +5310,10 @@ function captureStateDelta(beforePath, afterPath) {
|
|
|
5027
5310
|
}
|
|
5028
5311
|
function capturePartialStderr(outputFile, maxLines = DEFAULT_MAX_LINES) {
|
|
5029
5312
|
try {
|
|
5030
|
-
if (!
|
|
5313
|
+
if (!existsSync21(outputFile)) {
|
|
5031
5314
|
return fail2(`Output file not found: ${outputFile}`);
|
|
5032
5315
|
}
|
|
5033
|
-
const content =
|
|
5316
|
+
const content = readFileSync18(outputFile, "utf-8");
|
|
5034
5317
|
const lines = content.split("\n");
|
|
5035
5318
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
5036
5319
|
lines.pop();
|
|
@@ -5072,50 +5355,182 @@ function captureTimeoutReport(opts) {
|
|
|
5072
5355
|
if (opts.iteration < 1 || !Number.isInteger(opts.iteration)) {
|
|
5073
5356
|
return fail2(`Invalid iteration number: ${opts.iteration}`);
|
|
5074
5357
|
}
|
|
5075
|
-
if (opts.durationMinutes < 0) {
|
|
5076
|
-
return fail2(`Invalid duration: ${opts.durationMinutes}`);
|
|
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
|
+
}
|
|
5077
5489
|
}
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
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
|
+
}
|
|
5101
5516
|
}
|
|
5102
|
-
const
|
|
5103
|
-
|
|
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;
|
|
5104
5521
|
return ok2({
|
|
5105
|
-
|
|
5106
|
-
|
|
5522
|
+
totalStories,
|
|
5523
|
+
validCount,
|
|
5524
|
+
invalidCount,
|
|
5525
|
+
missingKeys,
|
|
5526
|
+
issues
|
|
5107
5527
|
});
|
|
5108
5528
|
} catch (err) {
|
|
5109
5529
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5110
|
-
return fail2(`
|
|
5530
|
+
return fail2(`Validation failed: ${msg}`);
|
|
5111
5531
|
}
|
|
5112
5532
|
}
|
|
5113
5533
|
|
|
5114
|
-
// src/modules/sprint/feedback.ts
|
|
5115
|
-
import { readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
|
|
5116
|
-
import { existsSync as existsSync20 } from "fs";
|
|
5117
|
-
import { join as join18 } from "path";
|
|
5118
|
-
|
|
5119
5534
|
// src/modules/sprint/index.ts
|
|
5120
5535
|
function generateReport2() {
|
|
5121
5536
|
const stateResult = getSprintState();
|
|
@@ -5134,6 +5549,9 @@ function getStoryDrillDown2(key) {
|
|
|
5134
5549
|
function captureTimeoutReport2(opts) {
|
|
5135
5550
|
return captureTimeoutReport(opts);
|
|
5136
5551
|
}
|
|
5552
|
+
function validateStateConsistency2(statePath2, sprintStatusPath) {
|
|
5553
|
+
return validateStateConsistency(statePath2, sprintStatusPath);
|
|
5554
|
+
}
|
|
5137
5555
|
|
|
5138
5556
|
// src/commands/status.ts
|
|
5139
5557
|
function buildScopedEndpoints(endpoints, serviceName) {
|
|
@@ -5699,16 +6117,16 @@ function getBeadsData() {
|
|
|
5699
6117
|
}
|
|
5700
6118
|
|
|
5701
6119
|
// src/commands/onboard.ts
|
|
5702
|
-
import { join as
|
|
6120
|
+
import { join as join23 } from "path";
|
|
5703
6121
|
|
|
5704
6122
|
// src/lib/scanner.ts
|
|
5705
6123
|
import {
|
|
5706
|
-
existsSync as
|
|
5707
|
-
readdirSync as
|
|
5708
|
-
readFileSync as
|
|
5709
|
-
statSync as
|
|
6124
|
+
existsSync as existsSync24,
|
|
6125
|
+
readdirSync as readdirSync4,
|
|
6126
|
+
readFileSync as readFileSync21,
|
|
6127
|
+
statSync as statSync3
|
|
5710
6128
|
} from "fs";
|
|
5711
|
-
import { join as
|
|
6129
|
+
import { join as join20, relative as relative2 } from "path";
|
|
5712
6130
|
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
|
|
5713
6131
|
var DEFAULT_MIN_MODULE_SIZE = 3;
|
|
5714
6132
|
function getExtension2(filename) {
|
|
@@ -5726,17 +6144,17 @@ function countSourceFiles(dir) {
|
|
|
5726
6144
|
function walk(current) {
|
|
5727
6145
|
let entries;
|
|
5728
6146
|
try {
|
|
5729
|
-
entries =
|
|
6147
|
+
entries = readdirSync4(current);
|
|
5730
6148
|
} catch {
|
|
5731
6149
|
return;
|
|
5732
6150
|
}
|
|
5733
6151
|
for (const entry of entries) {
|
|
5734
6152
|
if (isSkippedDir(entry)) continue;
|
|
5735
6153
|
if (entry.startsWith(".") && current !== dir) continue;
|
|
5736
|
-
const fullPath =
|
|
6154
|
+
const fullPath = join20(current, entry);
|
|
5737
6155
|
let stat;
|
|
5738
6156
|
try {
|
|
5739
|
-
stat =
|
|
6157
|
+
stat = statSync3(fullPath);
|
|
5740
6158
|
} catch {
|
|
5741
6159
|
continue;
|
|
5742
6160
|
}
|
|
@@ -5756,22 +6174,22 @@ function countSourceFiles(dir) {
|
|
|
5756
6174
|
return count;
|
|
5757
6175
|
}
|
|
5758
6176
|
function countModuleFiles(modulePath, rootDir) {
|
|
5759
|
-
const fullModulePath =
|
|
6177
|
+
const fullModulePath = join20(rootDir, modulePath);
|
|
5760
6178
|
let sourceFiles = 0;
|
|
5761
6179
|
let testFiles = 0;
|
|
5762
6180
|
function walk(current) {
|
|
5763
6181
|
let entries;
|
|
5764
6182
|
try {
|
|
5765
|
-
entries =
|
|
6183
|
+
entries = readdirSync4(current);
|
|
5766
6184
|
} catch {
|
|
5767
6185
|
return;
|
|
5768
6186
|
}
|
|
5769
6187
|
for (const entry of entries) {
|
|
5770
6188
|
if (isSkippedDir(entry)) continue;
|
|
5771
|
-
const fullPath =
|
|
6189
|
+
const fullPath = join20(current, entry);
|
|
5772
6190
|
let stat;
|
|
5773
6191
|
try {
|
|
5774
|
-
stat =
|
|
6192
|
+
stat = statSync3(fullPath);
|
|
5775
6193
|
} catch {
|
|
5776
6194
|
continue;
|
|
5777
6195
|
}
|
|
@@ -5793,8 +6211,8 @@ function countModuleFiles(modulePath, rootDir) {
|
|
|
5793
6211
|
return { sourceFiles, testFiles };
|
|
5794
6212
|
}
|
|
5795
6213
|
function detectArtifacts(dir) {
|
|
5796
|
-
const bmadPath =
|
|
5797
|
-
const hasBmad =
|
|
6214
|
+
const bmadPath = join20(dir, "_bmad");
|
|
6215
|
+
const hasBmad = existsSync24(bmadPath);
|
|
5798
6216
|
return {
|
|
5799
6217
|
hasBmad,
|
|
5800
6218
|
bmadPath: hasBmad ? relative2(dir, bmadPath) || "_bmad" : null
|
|
@@ -5876,10 +6294,10 @@ function readPerFileCoverage(dir, format) {
|
|
|
5876
6294
|
return null;
|
|
5877
6295
|
}
|
|
5878
6296
|
function readVitestPerFileCoverage(dir) {
|
|
5879
|
-
const reportPath =
|
|
5880
|
-
if (!
|
|
6297
|
+
const reportPath = join20(dir, "coverage", "coverage-summary.json");
|
|
6298
|
+
if (!existsSync24(reportPath)) return null;
|
|
5881
6299
|
try {
|
|
5882
|
-
const report = JSON.parse(
|
|
6300
|
+
const report = JSON.parse(readFileSync21(reportPath, "utf-8"));
|
|
5883
6301
|
const result = /* @__PURE__ */ new Map();
|
|
5884
6302
|
for (const [key, value] of Object.entries(report)) {
|
|
5885
6303
|
if (key === "total") continue;
|
|
@@ -5891,10 +6309,10 @@ function readVitestPerFileCoverage(dir) {
|
|
|
5891
6309
|
}
|
|
5892
6310
|
}
|
|
5893
6311
|
function readPythonPerFileCoverage(dir) {
|
|
5894
|
-
const reportPath =
|
|
5895
|
-
if (!
|
|
6312
|
+
const reportPath = join20(dir, "coverage.json");
|
|
6313
|
+
if (!existsSync24(reportPath)) return null;
|
|
5896
6314
|
try {
|
|
5897
|
-
const report = JSON.parse(
|
|
6315
|
+
const report = JSON.parse(readFileSync21(reportPath, "utf-8"));
|
|
5898
6316
|
if (!report.files) return null;
|
|
5899
6317
|
const result = /* @__PURE__ */ new Map();
|
|
5900
6318
|
for (const [key, value] of Object.entries(report.files)) {
|
|
@@ -5910,13 +6328,13 @@ function auditDocumentation(dir) {
|
|
|
5910
6328
|
const root = dir ?? process.cwd();
|
|
5911
6329
|
const documents = [];
|
|
5912
6330
|
for (const docName of AUDIT_DOCUMENTS) {
|
|
5913
|
-
const docPath =
|
|
5914
|
-
if (!
|
|
6331
|
+
const docPath = join20(root, docName);
|
|
6332
|
+
if (!existsSync24(docPath)) {
|
|
5915
6333
|
documents.push({ name: docName, grade: "missing", path: null });
|
|
5916
6334
|
continue;
|
|
5917
6335
|
}
|
|
5918
|
-
const srcDir =
|
|
5919
|
-
const codeDir =
|
|
6336
|
+
const srcDir = join20(root, "src");
|
|
6337
|
+
const codeDir = existsSync24(srcDir) ? srcDir : root;
|
|
5920
6338
|
const stale = isDocStale(docPath, codeDir);
|
|
5921
6339
|
documents.push({
|
|
5922
6340
|
name: docName,
|
|
@@ -5924,10 +6342,10 @@ function auditDocumentation(dir) {
|
|
|
5924
6342
|
path: docName
|
|
5925
6343
|
});
|
|
5926
6344
|
}
|
|
5927
|
-
const docsDir =
|
|
5928
|
-
if (
|
|
6345
|
+
const docsDir = join20(root, "docs");
|
|
6346
|
+
if (existsSync24(docsDir)) {
|
|
5929
6347
|
try {
|
|
5930
|
-
const stat =
|
|
6348
|
+
const stat = statSync3(docsDir);
|
|
5931
6349
|
if (stat.isDirectory()) {
|
|
5932
6350
|
documents.push({ name: "docs/", grade: "present", path: "docs/" });
|
|
5933
6351
|
}
|
|
@@ -5937,10 +6355,10 @@ function auditDocumentation(dir) {
|
|
|
5937
6355
|
} else {
|
|
5938
6356
|
documents.push({ name: "docs/", grade: "missing", path: null });
|
|
5939
6357
|
}
|
|
5940
|
-
const indexPath =
|
|
5941
|
-
if (
|
|
5942
|
-
const srcDir =
|
|
5943
|
-
const indexCodeDir =
|
|
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;
|
|
5944
6362
|
const indexStale = isDocStale(indexPath, indexCodeDir);
|
|
5945
6363
|
documents.push({
|
|
5946
6364
|
name: "docs/index.md",
|
|
@@ -5957,8 +6375,8 @@ function auditDocumentation(dir) {
|
|
|
5957
6375
|
|
|
5958
6376
|
// src/lib/epic-generator.ts
|
|
5959
6377
|
import { createInterface } from "readline";
|
|
5960
|
-
import { existsSync as
|
|
5961
|
-
import { dirname as dirname6, join as
|
|
6378
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync8, writeFileSync as writeFileSync12 } from "fs";
|
|
6379
|
+
import { dirname as dirname6, join as join21 } from "path";
|
|
5962
6380
|
var PRIORITY_BY_TYPE = {
|
|
5963
6381
|
observability: 1,
|
|
5964
6382
|
coverage: 2,
|
|
@@ -5996,8 +6414,8 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
|
|
|
5996
6414
|
storyNum++;
|
|
5997
6415
|
}
|
|
5998
6416
|
for (const mod of scan.modules) {
|
|
5999
|
-
const agentsPath =
|
|
6000
|
-
if (!
|
|
6417
|
+
const agentsPath = join21(root, mod.path, "AGENTS.md");
|
|
6418
|
+
if (!existsSync25(agentsPath)) {
|
|
6001
6419
|
stories.push({
|
|
6002
6420
|
key: `0.${storyNum}`,
|
|
6003
6421
|
title: `Create ${mod.path}/AGENTS.md`,
|
|
@@ -6063,7 +6481,7 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
|
|
|
6063
6481
|
};
|
|
6064
6482
|
}
|
|
6065
6483
|
function writeOnboardingEpic(epic, outputPath) {
|
|
6066
|
-
|
|
6484
|
+
mkdirSync8(dirname6(outputPath), { recursive: true });
|
|
6067
6485
|
const lines = [];
|
|
6068
6486
|
lines.push(`# ${epic.title}`);
|
|
6069
6487
|
lines.push("");
|
|
@@ -6106,7 +6524,7 @@ function formatEpicSummary(epic) {
|
|
|
6106
6524
|
return `Onboarding plan: ${totalStories} stories (${coverageStories} coverage, ${docStories} documentation, ${verificationStories} verification, ${observabilityStories} observability)`;
|
|
6107
6525
|
}
|
|
6108
6526
|
function promptApproval() {
|
|
6109
|
-
return new Promise((
|
|
6527
|
+
return new Promise((resolve3) => {
|
|
6110
6528
|
let answered = false;
|
|
6111
6529
|
const rl = createInterface({
|
|
6112
6530
|
input: process.stdin,
|
|
@@ -6115,14 +6533,14 @@ function promptApproval() {
|
|
|
6115
6533
|
rl.on("close", () => {
|
|
6116
6534
|
if (!answered) {
|
|
6117
6535
|
answered = true;
|
|
6118
|
-
|
|
6536
|
+
resolve3(false);
|
|
6119
6537
|
}
|
|
6120
6538
|
});
|
|
6121
6539
|
rl.question("Review the onboarding plan. Approve? [Y/n] ", (answer) => {
|
|
6122
6540
|
answered = true;
|
|
6123
6541
|
rl.close();
|
|
6124
6542
|
const trimmed = answer.trim().toLowerCase();
|
|
6125
|
-
|
|
6543
|
+
resolve3(trimmed === "" || trimmed === "y");
|
|
6126
6544
|
});
|
|
6127
6545
|
});
|
|
6128
6546
|
}
|
|
@@ -6195,29 +6613,29 @@ function getGapIdFromTitle(title) {
|
|
|
6195
6613
|
}
|
|
6196
6614
|
|
|
6197
6615
|
// src/lib/scan-cache.ts
|
|
6198
|
-
import { existsSync as
|
|
6199
|
-
import { join as
|
|
6616
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync9, readFileSync as readFileSync22, writeFileSync as writeFileSync13 } from "fs";
|
|
6617
|
+
import { join as join22 } from "path";
|
|
6200
6618
|
var CACHE_DIR = ".harness";
|
|
6201
6619
|
var CACHE_FILE = "last-onboard-scan.json";
|
|
6202
6620
|
var DEFAULT_MAX_AGE_MS = 864e5;
|
|
6203
6621
|
function saveScanCache(entry, dir) {
|
|
6204
6622
|
try {
|
|
6205
6623
|
const root = dir ?? process.cwd();
|
|
6206
|
-
const cacheDir =
|
|
6207
|
-
|
|
6208
|
-
const cachePath =
|
|
6624
|
+
const cacheDir = join22(root, CACHE_DIR);
|
|
6625
|
+
mkdirSync9(cacheDir, { recursive: true });
|
|
6626
|
+
const cachePath = join22(cacheDir, CACHE_FILE);
|
|
6209
6627
|
writeFileSync13(cachePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
6210
6628
|
} catch {
|
|
6211
6629
|
}
|
|
6212
6630
|
}
|
|
6213
6631
|
function loadScanCache(dir) {
|
|
6214
6632
|
const root = dir ?? process.cwd();
|
|
6215
|
-
const cachePath =
|
|
6216
|
-
if (!
|
|
6633
|
+
const cachePath = join22(root, CACHE_DIR, CACHE_FILE);
|
|
6634
|
+
if (!existsSync26(cachePath)) {
|
|
6217
6635
|
return null;
|
|
6218
6636
|
}
|
|
6219
6637
|
try {
|
|
6220
|
-
const raw =
|
|
6638
|
+
const raw = readFileSync22(cachePath, "utf-8");
|
|
6221
6639
|
return JSON.parse(raw);
|
|
6222
6640
|
} catch {
|
|
6223
6641
|
return null;
|
|
@@ -6390,7 +6808,7 @@ function registerOnboardCommand(program) {
|
|
|
6390
6808
|
}
|
|
6391
6809
|
coverage = lastCoverageResult ?? runCoverageAnalysis(scan);
|
|
6392
6810
|
audit = lastAuditResult ?? runAudit();
|
|
6393
|
-
const epicPath =
|
|
6811
|
+
const epicPath = join23(process.cwd(), "ralph", "onboarding-epic.md");
|
|
6394
6812
|
const epic = generateOnboardingEpic(scan, coverage, audit);
|
|
6395
6813
|
mergeExtendedGaps(epic);
|
|
6396
6814
|
if (!isFull) {
|
|
@@ -6463,7 +6881,7 @@ function registerOnboardCommand(program) {
|
|
|
6463
6881
|
coverage,
|
|
6464
6882
|
audit
|
|
6465
6883
|
});
|
|
6466
|
-
const epicPath =
|
|
6884
|
+
const epicPath = join23(process.cwd(), "ralph", "onboarding-epic.md");
|
|
6467
6885
|
const epic = generateOnboardingEpic(scan, coverage, audit);
|
|
6468
6886
|
mergeExtendedGaps(epic);
|
|
6469
6887
|
if (!isFull) {
|
|
@@ -6571,8 +6989,8 @@ function printEpicOutput(epic) {
|
|
|
6571
6989
|
}
|
|
6572
6990
|
|
|
6573
6991
|
// src/commands/teardown.ts
|
|
6574
|
-
import { existsSync as
|
|
6575
|
-
import { join as
|
|
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";
|
|
6576
6994
|
function buildDefaultResult() {
|
|
6577
6995
|
return {
|
|
6578
6996
|
status: "ok",
|
|
@@ -6675,16 +7093,16 @@ function registerTeardownCommand(program) {
|
|
|
6675
7093
|
info("Docker stack: not running, skipping");
|
|
6676
7094
|
}
|
|
6677
7095
|
}
|
|
6678
|
-
const composeFilePath =
|
|
6679
|
-
if (
|
|
7096
|
+
const composeFilePath = join24(projectDir, composeFile);
|
|
7097
|
+
if (existsSync27(composeFilePath)) {
|
|
6680
7098
|
unlinkSync2(composeFilePath);
|
|
6681
7099
|
result.removed.push(composeFile);
|
|
6682
7100
|
if (!isJson) {
|
|
6683
7101
|
ok(`Removed: ${composeFile}`);
|
|
6684
7102
|
}
|
|
6685
7103
|
}
|
|
6686
|
-
const otelConfigPath =
|
|
6687
|
-
if (
|
|
7104
|
+
const otelConfigPath = join24(projectDir, "otel-collector-config.yaml");
|
|
7105
|
+
if (existsSync27(otelConfigPath)) {
|
|
6688
7106
|
unlinkSync2(otelConfigPath);
|
|
6689
7107
|
result.removed.push("otel-collector-config.yaml");
|
|
6690
7108
|
if (!isJson) {
|
|
@@ -6694,8 +7112,8 @@ function registerTeardownCommand(program) {
|
|
|
6694
7112
|
}
|
|
6695
7113
|
let patchesRemoved = 0;
|
|
6696
7114
|
for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
|
|
6697
|
-
const filePath =
|
|
6698
|
-
if (!
|
|
7115
|
+
const filePath = join24(projectDir, "_bmad", relativePath);
|
|
7116
|
+
if (!existsSync27(filePath)) {
|
|
6699
7117
|
continue;
|
|
6700
7118
|
}
|
|
6701
7119
|
try {
|
|
@@ -6715,10 +7133,10 @@ function registerTeardownCommand(program) {
|
|
|
6715
7133
|
}
|
|
6716
7134
|
}
|
|
6717
7135
|
if (state.otlp?.enabled && state.stack === "nodejs") {
|
|
6718
|
-
const pkgPath =
|
|
6719
|
-
if (
|
|
7136
|
+
const pkgPath = join24(projectDir, "package.json");
|
|
7137
|
+
if (existsSync27(pkgPath)) {
|
|
6720
7138
|
try {
|
|
6721
|
-
const raw =
|
|
7139
|
+
const raw = readFileSync23(pkgPath, "utf-8");
|
|
6722
7140
|
const pkg = JSON.parse(raw);
|
|
6723
7141
|
const scripts = pkg["scripts"];
|
|
6724
7142
|
if (scripts) {
|
|
@@ -6758,16 +7176,16 @@ function registerTeardownCommand(program) {
|
|
|
6758
7176
|
}
|
|
6759
7177
|
}
|
|
6760
7178
|
}
|
|
6761
|
-
const harnessDir =
|
|
6762
|
-
if (
|
|
6763
|
-
|
|
7179
|
+
const harnessDir = join24(projectDir, ".harness");
|
|
7180
|
+
if (existsSync27(harnessDir)) {
|
|
7181
|
+
rmSync2(harnessDir, { recursive: true, force: true });
|
|
6764
7182
|
result.removed.push(".harness/");
|
|
6765
7183
|
if (!isJson) {
|
|
6766
7184
|
ok("Removed: .harness/");
|
|
6767
7185
|
}
|
|
6768
7186
|
}
|
|
6769
7187
|
const statePath2 = getStatePath(projectDir);
|
|
6770
|
-
if (
|
|
7188
|
+
if (existsSync27(statePath2)) {
|
|
6771
7189
|
unlinkSync2(statePath2);
|
|
6772
7190
|
result.removed.push(".claude/codeharness.local.md");
|
|
6773
7191
|
if (!isJson) {
|
|
@@ -7511,8 +7929,8 @@ function registerQueryCommand(program) {
|
|
|
7511
7929
|
}
|
|
7512
7930
|
|
|
7513
7931
|
// src/commands/retro-import.ts
|
|
7514
|
-
import { existsSync as
|
|
7515
|
-
import { join as
|
|
7932
|
+
import { existsSync as existsSync28, readFileSync as readFileSync24 } from "fs";
|
|
7933
|
+
import { join as join25 } from "path";
|
|
7516
7934
|
|
|
7517
7935
|
// src/lib/retro-parser.ts
|
|
7518
7936
|
var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
|
|
@@ -7569,7 +7987,7 @@ function derivePriority(item) {
|
|
|
7569
7987
|
}
|
|
7570
7988
|
|
|
7571
7989
|
// src/lib/github.ts
|
|
7572
|
-
import { execFileSync as
|
|
7990
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
7573
7991
|
var GitHubError = class extends Error {
|
|
7574
7992
|
constructor(command, originalMessage) {
|
|
7575
7993
|
super(`GitHub CLI failed: ${originalMessage}. Command: ${command}`);
|
|
@@ -7580,7 +7998,7 @@ var GitHubError = class extends Error {
|
|
|
7580
7998
|
};
|
|
7581
7999
|
function isGhAvailable() {
|
|
7582
8000
|
try {
|
|
7583
|
-
|
|
8001
|
+
execFileSync7("which", ["gh"], { stdio: "pipe", timeout: 5e3 });
|
|
7584
8002
|
return true;
|
|
7585
8003
|
} catch {
|
|
7586
8004
|
return false;
|
|
@@ -7594,7 +8012,7 @@ function ghIssueCreate(repo, title, body, labels) {
|
|
|
7594
8012
|
args.push("--json", "number,url");
|
|
7595
8013
|
const cmdStr = `gh ${args.join(" ")}`;
|
|
7596
8014
|
try {
|
|
7597
|
-
const output =
|
|
8015
|
+
const output = execFileSync7("gh", args, {
|
|
7598
8016
|
stdio: "pipe",
|
|
7599
8017
|
timeout: 3e4
|
|
7600
8018
|
});
|
|
@@ -7609,7 +8027,7 @@ function ghIssueSearch(repo, query) {
|
|
|
7609
8027
|
const args = ["issue", "list", "--repo", repo, "--search", query, "--state", "all", "--json", "number,title,body,url,labels"];
|
|
7610
8028
|
const cmdStr = `gh ${args.join(" ")}`;
|
|
7611
8029
|
try {
|
|
7612
|
-
const output =
|
|
8030
|
+
const output = execFileSync7("gh", args, {
|
|
7613
8031
|
stdio: "pipe",
|
|
7614
8032
|
timeout: 3e4
|
|
7615
8033
|
});
|
|
@@ -7631,7 +8049,7 @@ function findExistingGhIssue(repo, gapId) {
|
|
|
7631
8049
|
}
|
|
7632
8050
|
function getRepoFromRemote() {
|
|
7633
8051
|
try {
|
|
7634
|
-
const output =
|
|
8052
|
+
const output = execFileSync7("git", ["remote", "get-url", "origin"], {
|
|
7635
8053
|
stdio: "pipe",
|
|
7636
8054
|
timeout: 5e3
|
|
7637
8055
|
});
|
|
@@ -7651,7 +8069,7 @@ function parseRepoFromUrl(url) {
|
|
|
7651
8069
|
function ensureLabels(repo, labels) {
|
|
7652
8070
|
for (const label of labels) {
|
|
7653
8071
|
try {
|
|
7654
|
-
|
|
8072
|
+
execFileSync7("gh", ["label", "create", label, "--repo", repo], {
|
|
7655
8073
|
stdio: "pipe",
|
|
7656
8074
|
timeout: 1e4
|
|
7657
8075
|
});
|
|
@@ -7661,7 +8079,7 @@ function ensureLabels(repo, labels) {
|
|
|
7661
8079
|
}
|
|
7662
8080
|
|
|
7663
8081
|
// src/commands/retro-import.ts
|
|
7664
|
-
var
|
|
8082
|
+
var STORY_DIR3 = "_bmad-output/implementation-artifacts";
|
|
7665
8083
|
var MAX_TITLE_LENGTH = 120;
|
|
7666
8084
|
function classificationToString(c) {
|
|
7667
8085
|
if (c.type === "tool") {
|
|
@@ -7681,15 +8099,15 @@ function registerRetroImportCommand(program) {
|
|
|
7681
8099
|
return;
|
|
7682
8100
|
}
|
|
7683
8101
|
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
7684
|
-
const retroPath =
|
|
7685
|
-
if (!
|
|
8102
|
+
const retroPath = join25(root, STORY_DIR3, retroFile);
|
|
8103
|
+
if (!existsSync28(retroPath)) {
|
|
7686
8104
|
fail(`Retro file not found: ${retroFile}`, { json: isJson });
|
|
7687
8105
|
process.exitCode = 1;
|
|
7688
8106
|
return;
|
|
7689
8107
|
}
|
|
7690
8108
|
let content;
|
|
7691
8109
|
try {
|
|
7692
|
-
content =
|
|
8110
|
+
content = readFileSync24(retroPath, "utf-8");
|
|
7693
8111
|
} catch (err) {
|
|
7694
8112
|
const message = err instanceof Error ? err.message : String(err);
|
|
7695
8113
|
fail(`Failed to read retro file: ${message}`, { json: isJson });
|
|
@@ -7955,292 +8373,6 @@ function registerGithubImportCommand(program) {
|
|
|
7955
8373
|
});
|
|
7956
8374
|
}
|
|
7957
8375
|
|
|
7958
|
-
// src/lib/verify-env.ts
|
|
7959
|
-
import { execFileSync as execFileSync7 } from "child_process";
|
|
7960
|
-
import { existsSync as existsSync26, mkdirSync as mkdirSync9, readdirSync as readdirSync4, readFileSync as readFileSync23, cpSync, rmSync as rmSync2, statSync as statSync3 } from "fs";
|
|
7961
|
-
import { join as join25, basename as basename3 } from "path";
|
|
7962
|
-
import { createHash } from "crypto";
|
|
7963
|
-
var IMAGE_TAG = "codeharness-verify";
|
|
7964
|
-
var STORY_DIR3 = "_bmad-output/implementation-artifacts";
|
|
7965
|
-
var TEMP_PREFIX = "/tmp/codeharness-verify-";
|
|
7966
|
-
var STATE_KEY_DIST_HASH = "verify_env_dist_hash";
|
|
7967
|
-
function isValidStoryKey(storyKey) {
|
|
7968
|
-
if (!storyKey || storyKey.includes("..") || storyKey.includes("/") || storyKey.includes("\\")) {
|
|
7969
|
-
return false;
|
|
7970
|
-
}
|
|
7971
|
-
return /^[a-zA-Z0-9_-]+$/.test(storyKey);
|
|
7972
|
-
}
|
|
7973
|
-
function computeDistHash(projectDir) {
|
|
7974
|
-
const distDir = join25(projectDir, "dist");
|
|
7975
|
-
if (!existsSync26(distDir)) {
|
|
7976
|
-
return null;
|
|
7977
|
-
}
|
|
7978
|
-
const hash = createHash("sha256");
|
|
7979
|
-
const files = collectFiles(distDir).sort();
|
|
7980
|
-
for (const file of files) {
|
|
7981
|
-
const content = readFileSync23(file);
|
|
7982
|
-
hash.update(file.slice(distDir.length));
|
|
7983
|
-
hash.update(content);
|
|
7984
|
-
}
|
|
7985
|
-
return hash.digest("hex");
|
|
7986
|
-
}
|
|
7987
|
-
function collectFiles(dir) {
|
|
7988
|
-
const results = [];
|
|
7989
|
-
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
7990
|
-
for (const entry of entries) {
|
|
7991
|
-
const fullPath = join25(dir, entry.name);
|
|
7992
|
-
if (entry.isDirectory()) {
|
|
7993
|
-
results.push(...collectFiles(fullPath));
|
|
7994
|
-
} else {
|
|
7995
|
-
results.push(fullPath);
|
|
7996
|
-
}
|
|
7997
|
-
}
|
|
7998
|
-
return results;
|
|
7999
|
-
}
|
|
8000
|
-
function getStoredDistHash(projectDir) {
|
|
8001
|
-
try {
|
|
8002
|
-
const { state } = readStateWithBody(projectDir);
|
|
8003
|
-
const raw = state;
|
|
8004
|
-
return raw[STATE_KEY_DIST_HASH] ?? null;
|
|
8005
|
-
} catch {
|
|
8006
|
-
return null;
|
|
8007
|
-
}
|
|
8008
|
-
}
|
|
8009
|
-
function storeDistHash(projectDir, hash) {
|
|
8010
|
-
try {
|
|
8011
|
-
const { state, body } = readStateWithBody(projectDir);
|
|
8012
|
-
const raw = state;
|
|
8013
|
-
raw[STATE_KEY_DIST_HASH] = hash;
|
|
8014
|
-
writeState(state, projectDir, body);
|
|
8015
|
-
} catch {
|
|
8016
|
-
info("Could not persist dist hash to state file \u2014 cache will not be available until state is initialized");
|
|
8017
|
-
}
|
|
8018
|
-
}
|
|
8019
|
-
function buildVerifyImage(options = {}) {
|
|
8020
|
-
const projectDir = options.projectDir ?? process.cwd();
|
|
8021
|
-
if (!isDockerAvailable()) {
|
|
8022
|
-
throw new Error("Docker is not available. Install Docker and ensure the daemon is running.");
|
|
8023
|
-
}
|
|
8024
|
-
const stack = detectStack(projectDir);
|
|
8025
|
-
if (!stack) {
|
|
8026
|
-
throw new Error("Cannot detect project stack. Ensure package.json (Node.js) or requirements.txt/pyproject.toml (Python) exists.");
|
|
8027
|
-
}
|
|
8028
|
-
const currentHash = computeDistHash(projectDir);
|
|
8029
|
-
if (!currentHash) {
|
|
8030
|
-
throw new Error("No dist/ directory found. Run your build command first (e.g., npm run build).");
|
|
8031
|
-
}
|
|
8032
|
-
const storedHash = getStoredDistHash(projectDir);
|
|
8033
|
-
if (storedHash === currentHash) {
|
|
8034
|
-
if (dockerImageExists(IMAGE_TAG)) {
|
|
8035
|
-
const imageSize2 = getImageSize(IMAGE_TAG);
|
|
8036
|
-
return { imageTag: IMAGE_TAG, imageSize: imageSize2, buildTimeMs: 0, cached: true };
|
|
8037
|
-
}
|
|
8038
|
-
}
|
|
8039
|
-
const startTime = Date.now();
|
|
8040
|
-
if (stack === "nodejs") {
|
|
8041
|
-
buildNodeImage(projectDir);
|
|
8042
|
-
} else if (stack === "python") {
|
|
8043
|
-
buildPythonImage(projectDir);
|
|
8044
|
-
} else {
|
|
8045
|
-
throw new Error(`Unsupported stack for verify-env: ${stack}`);
|
|
8046
|
-
}
|
|
8047
|
-
const buildTimeMs = Date.now() - startTime;
|
|
8048
|
-
storeDistHash(projectDir, currentHash);
|
|
8049
|
-
const imageSize = getImageSize(IMAGE_TAG);
|
|
8050
|
-
return { imageTag: IMAGE_TAG, imageSize, buildTimeMs, cached: false };
|
|
8051
|
-
}
|
|
8052
|
-
function buildNodeImage(projectDir) {
|
|
8053
|
-
const packOutput = execFileSync7("npm", ["pack", "--pack-destination", "/tmp"], {
|
|
8054
|
-
cwd: projectDir,
|
|
8055
|
-
stdio: "pipe",
|
|
8056
|
-
timeout: 6e4
|
|
8057
|
-
}).toString().trim();
|
|
8058
|
-
const lastLine = packOutput.split("\n").pop()?.trim();
|
|
8059
|
-
if (!lastLine) {
|
|
8060
|
-
throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
|
|
8061
|
-
}
|
|
8062
|
-
const tarballName = basename3(lastLine);
|
|
8063
|
-
const tarballPath = join25("/tmp", tarballName);
|
|
8064
|
-
const buildContext = join25("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
8065
|
-
mkdirSync9(buildContext, { recursive: true });
|
|
8066
|
-
try {
|
|
8067
|
-
cpSync(tarballPath, join25(buildContext, tarballName));
|
|
8068
|
-
const dockerfileSrc = resolveDockerfileTemplate(projectDir);
|
|
8069
|
-
cpSync(dockerfileSrc, join25(buildContext, "Dockerfile"));
|
|
8070
|
-
execFileSync7("docker", [
|
|
8071
|
-
"build",
|
|
8072
|
-
"-t",
|
|
8073
|
-
IMAGE_TAG,
|
|
8074
|
-
"--build-arg",
|
|
8075
|
-
`TARBALL=${tarballName}`,
|
|
8076
|
-
"."
|
|
8077
|
-
], {
|
|
8078
|
-
cwd: buildContext,
|
|
8079
|
-
stdio: "pipe",
|
|
8080
|
-
timeout: 12e4
|
|
8081
|
-
});
|
|
8082
|
-
} finally {
|
|
8083
|
-
rmSync2(buildContext, { recursive: true, force: true });
|
|
8084
|
-
rmSync2(tarballPath, { force: true });
|
|
8085
|
-
}
|
|
8086
|
-
}
|
|
8087
|
-
function buildPythonImage(projectDir) {
|
|
8088
|
-
const distDir = join25(projectDir, "dist");
|
|
8089
|
-
const distFiles = readdirSync4(distDir).filter(
|
|
8090
|
-
(f) => f.endsWith(".tar.gz") || f.endsWith(".whl")
|
|
8091
|
-
);
|
|
8092
|
-
if (distFiles.length === 0) {
|
|
8093
|
-
throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
|
|
8094
|
-
}
|
|
8095
|
-
const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
|
|
8096
|
-
const buildContext = join25("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
8097
|
-
mkdirSync9(buildContext, { recursive: true });
|
|
8098
|
-
try {
|
|
8099
|
-
cpSync(join25(distDir, distFile), join25(buildContext, distFile));
|
|
8100
|
-
const dockerfileSrc = resolveDockerfileTemplate(projectDir);
|
|
8101
|
-
cpSync(dockerfileSrc, join25(buildContext, "Dockerfile"));
|
|
8102
|
-
execFileSync7("docker", [
|
|
8103
|
-
"build",
|
|
8104
|
-
"-t",
|
|
8105
|
-
IMAGE_TAG,
|
|
8106
|
-
"--build-arg",
|
|
8107
|
-
`TARBALL=${distFile}`,
|
|
8108
|
-
"."
|
|
8109
|
-
], {
|
|
8110
|
-
cwd: buildContext,
|
|
8111
|
-
stdio: "pipe",
|
|
8112
|
-
timeout: 12e4
|
|
8113
|
-
});
|
|
8114
|
-
} finally {
|
|
8115
|
-
rmSync2(buildContext, { recursive: true, force: true });
|
|
8116
|
-
}
|
|
8117
|
-
}
|
|
8118
|
-
function prepareVerifyWorkspace(storyKey, projectDir) {
|
|
8119
|
-
const root = projectDir ?? process.cwd();
|
|
8120
|
-
if (!isValidStoryKey(storyKey)) {
|
|
8121
|
-
throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
|
|
8122
|
-
}
|
|
8123
|
-
const storyFile = join25(root, STORY_DIR3, `${storyKey}.md`);
|
|
8124
|
-
if (!existsSync26(storyFile)) {
|
|
8125
|
-
throw new Error(`Story file not found: ${storyFile}`);
|
|
8126
|
-
}
|
|
8127
|
-
const workspace = `${TEMP_PREFIX}${storyKey}`;
|
|
8128
|
-
if (existsSync26(workspace)) {
|
|
8129
|
-
rmSync2(workspace, { recursive: true, force: true });
|
|
8130
|
-
}
|
|
8131
|
-
mkdirSync9(workspace, { recursive: true });
|
|
8132
|
-
cpSync(storyFile, join25(workspace, "story.md"));
|
|
8133
|
-
const readmePath = join25(root, "README.md");
|
|
8134
|
-
if (existsSync26(readmePath)) {
|
|
8135
|
-
cpSync(readmePath, join25(workspace, "README.md"));
|
|
8136
|
-
}
|
|
8137
|
-
const docsDir = join25(root, "docs");
|
|
8138
|
-
if (existsSync26(docsDir) && statSync3(docsDir).isDirectory()) {
|
|
8139
|
-
cpSync(docsDir, join25(workspace, "docs"), { recursive: true });
|
|
8140
|
-
}
|
|
8141
|
-
mkdirSync9(join25(workspace, "verification"), { recursive: true });
|
|
8142
|
-
return workspace;
|
|
8143
|
-
}
|
|
8144
|
-
function checkVerifyEnv() {
|
|
8145
|
-
const result = {
|
|
8146
|
-
imageExists: false,
|
|
8147
|
-
cliWorks: false,
|
|
8148
|
-
otelReachable: false
|
|
8149
|
-
};
|
|
8150
|
-
result.imageExists = dockerImageExists(IMAGE_TAG);
|
|
8151
|
-
if (!result.imageExists) {
|
|
8152
|
-
return result;
|
|
8153
|
-
}
|
|
8154
|
-
try {
|
|
8155
|
-
execFileSync7("docker", ["run", "--rm", IMAGE_TAG, "codeharness", "--help"], {
|
|
8156
|
-
stdio: "pipe",
|
|
8157
|
-
timeout: 3e4
|
|
8158
|
-
});
|
|
8159
|
-
result.cliWorks = true;
|
|
8160
|
-
} catch {
|
|
8161
|
-
result.cliWorks = false;
|
|
8162
|
-
}
|
|
8163
|
-
try {
|
|
8164
|
-
execFileSync7("docker", [
|
|
8165
|
-
"run",
|
|
8166
|
-
"--rm",
|
|
8167
|
-
"--add-host=host.docker.internal:host-gateway",
|
|
8168
|
-
IMAGE_TAG,
|
|
8169
|
-
"curl",
|
|
8170
|
-
"-sf",
|
|
8171
|
-
"--max-time",
|
|
8172
|
-
"5",
|
|
8173
|
-
"http://host.docker.internal:4318/v1/status"
|
|
8174
|
-
], {
|
|
8175
|
-
stdio: "pipe",
|
|
8176
|
-
timeout: 3e4
|
|
8177
|
-
});
|
|
8178
|
-
result.otelReachable = true;
|
|
8179
|
-
} catch {
|
|
8180
|
-
result.otelReachable = false;
|
|
8181
|
-
}
|
|
8182
|
-
return result;
|
|
8183
|
-
}
|
|
8184
|
-
function cleanupVerifyEnv(storyKey) {
|
|
8185
|
-
if (!isValidStoryKey(storyKey)) {
|
|
8186
|
-
throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
|
|
8187
|
-
}
|
|
8188
|
-
const workspace = `${TEMP_PREFIX}${storyKey}`;
|
|
8189
|
-
const containerName = `codeharness-verify-${storyKey}`;
|
|
8190
|
-
if (existsSync26(workspace)) {
|
|
8191
|
-
rmSync2(workspace, { recursive: true, force: true });
|
|
8192
|
-
}
|
|
8193
|
-
try {
|
|
8194
|
-
execFileSync7("docker", ["stop", containerName], {
|
|
8195
|
-
stdio: "pipe",
|
|
8196
|
-
timeout: 15e3
|
|
8197
|
-
});
|
|
8198
|
-
} catch {
|
|
8199
|
-
}
|
|
8200
|
-
try {
|
|
8201
|
-
execFileSync7("docker", ["rm", "-f", containerName], {
|
|
8202
|
-
stdio: "pipe",
|
|
8203
|
-
timeout: 15e3
|
|
8204
|
-
});
|
|
8205
|
-
} catch {
|
|
8206
|
-
}
|
|
8207
|
-
}
|
|
8208
|
-
function resolveDockerfileTemplate(projectDir) {
|
|
8209
|
-
const local = join25(projectDir, "templates", "Dockerfile.verify");
|
|
8210
|
-
if (existsSync26(local)) return local;
|
|
8211
|
-
const pkgDir = new URL("../../", import.meta.url).pathname;
|
|
8212
|
-
const pkg = join25(pkgDir, "templates", "Dockerfile.verify");
|
|
8213
|
-
if (existsSync26(pkg)) return pkg;
|
|
8214
|
-
throw new Error("Dockerfile.verify not found. Ensure templates/Dockerfile.verify exists in the project or installed package.");
|
|
8215
|
-
}
|
|
8216
|
-
function dockerImageExists(tag) {
|
|
8217
|
-
try {
|
|
8218
|
-
execFileSync7("docker", ["image", "inspect", tag], {
|
|
8219
|
-
stdio: "pipe",
|
|
8220
|
-
timeout: 1e4
|
|
8221
|
-
});
|
|
8222
|
-
return true;
|
|
8223
|
-
} catch {
|
|
8224
|
-
return false;
|
|
8225
|
-
}
|
|
8226
|
-
}
|
|
8227
|
-
function getImageSize(tag) {
|
|
8228
|
-
try {
|
|
8229
|
-
const output = execFileSync7("docker", ["image", "inspect", tag, "--format", "{{.Size}}"], {
|
|
8230
|
-
stdio: "pipe",
|
|
8231
|
-
timeout: 1e4
|
|
8232
|
-
}).toString().trim();
|
|
8233
|
-
const bytes = parseInt(output, 10);
|
|
8234
|
-
if (isNaN(bytes)) return output;
|
|
8235
|
-
if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)}GB`;
|
|
8236
|
-
if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(1)}MB`;
|
|
8237
|
-
if (bytes >= 1e3) return `${(bytes / 1e3).toFixed(1)}KB`;
|
|
8238
|
-
return `${bytes}B`;
|
|
8239
|
-
} catch {
|
|
8240
|
-
return "unknown";
|
|
8241
|
-
}
|
|
8242
|
-
}
|
|
8243
|
-
|
|
8244
8376
|
// src/commands/verify-env.ts
|
|
8245
8377
|
function registerVerifyEnvCommand(program) {
|
|
8246
8378
|
const verifyEnv = program.command("verify-env").description("Manage verification environment (Docker image + clean workspace)");
|
|
@@ -8359,7 +8491,7 @@ function registerVerifyEnvCommand(program) {
|
|
|
8359
8491
|
import { join as join27 } from "path";
|
|
8360
8492
|
|
|
8361
8493
|
// src/lib/retry-state.ts
|
|
8362
|
-
import { existsSync as
|
|
8494
|
+
import { existsSync as existsSync29, readFileSync as readFileSync25, writeFileSync as writeFileSync15 } from "fs";
|
|
8363
8495
|
import { join as join26 } from "path";
|
|
8364
8496
|
var RETRIES_FILE = ".story_retries";
|
|
8365
8497
|
var FLAGGED_FILE = ".flagged_stories";
|
|
@@ -8372,10 +8504,10 @@ function flaggedPath(dir) {
|
|
|
8372
8504
|
}
|
|
8373
8505
|
function readRetries(dir) {
|
|
8374
8506
|
const filePath = retriesPath(dir);
|
|
8375
|
-
if (!
|
|
8507
|
+
if (!existsSync29(filePath)) {
|
|
8376
8508
|
return /* @__PURE__ */ new Map();
|
|
8377
8509
|
}
|
|
8378
|
-
const raw =
|
|
8510
|
+
const raw = readFileSync25(filePath, "utf-8");
|
|
8379
8511
|
const result = /* @__PURE__ */ new Map();
|
|
8380
8512
|
for (const line of raw.split("\n")) {
|
|
8381
8513
|
const trimmed = line.trim();
|
|
@@ -8412,10 +8544,10 @@ function resetRetry(dir, storyKey) {
|
|
|
8412
8544
|
}
|
|
8413
8545
|
function readFlaggedStories(dir) {
|
|
8414
8546
|
const filePath = flaggedPath(dir);
|
|
8415
|
-
if (!
|
|
8547
|
+
if (!existsSync29(filePath)) {
|
|
8416
8548
|
return [];
|
|
8417
8549
|
}
|
|
8418
|
-
const raw =
|
|
8550
|
+
const raw = readFileSync25(filePath, "utf-8");
|
|
8419
8551
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l !== "");
|
|
8420
8552
|
}
|
|
8421
8553
|
function writeFlaggedStories(dir, stories) {
|
|
@@ -8558,8 +8690,56 @@ function registerTimeoutReportCommand(program) {
|
|
|
8558
8690
|
});
|
|
8559
8691
|
}
|
|
8560
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
|
+
|
|
8561
8741
|
// src/index.ts
|
|
8562
|
-
var VERSION = true ? "0.19.
|
|
8742
|
+
var VERSION = true ? "0.19.3" : "0.0.0-dev";
|
|
8563
8743
|
function createProgram() {
|
|
8564
8744
|
const program = new Command();
|
|
8565
8745
|
program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
|
|
@@ -8581,6 +8761,7 @@ function createProgram() {
|
|
|
8581
8761
|
registerVerifyEnvCommand(program);
|
|
8582
8762
|
registerRetryCommand(program);
|
|
8583
8763
|
registerTimeoutReportCommand(program);
|
|
8764
|
+
registerValidateStateCommand(program);
|
|
8584
8765
|
return program;
|
|
8585
8766
|
}
|
|
8586
8767
|
if (!process.env["VITEST"]) {
|