codeharness 0.13.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -13,13 +13,13 @@ import {
|
|
|
13
13
|
startSharedStack,
|
|
14
14
|
stopCollectorOnly,
|
|
15
15
|
stopSharedStack
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-CVXXI3N6.js";
|
|
17
17
|
|
|
18
18
|
// src/index.ts
|
|
19
19
|
import { Command } from "commander";
|
|
20
20
|
|
|
21
21
|
// src/commands/init.ts
|
|
22
|
-
import { existsSync as existsSync6 } from "fs";
|
|
22
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
23
23
|
import { join as join6, basename as basename2 } from "path";
|
|
24
24
|
|
|
25
25
|
// src/lib/output.ts
|
|
@@ -1347,8 +1347,75 @@ function importStoriesToBeads(stories, opts, beadsFns) {
|
|
|
1347
1347
|
return results;
|
|
1348
1348
|
}
|
|
1349
1349
|
|
|
1350
|
+
// src/templates/readme.ts
|
|
1351
|
+
function readmeTemplate(config) {
|
|
1352
|
+
const { projectName, stack, cliHelpOutput } = config;
|
|
1353
|
+
const installCommand = getInstallCommand(stack);
|
|
1354
|
+
const firstRunCommand = "codeharness init";
|
|
1355
|
+
const exampleUsage = "codeharness status";
|
|
1356
|
+
const lines = [
|
|
1357
|
+
`# ${projectName}`,
|
|
1358
|
+
"",
|
|
1359
|
+
"## Quick Start",
|
|
1360
|
+
"",
|
|
1361
|
+
"```bash",
|
|
1362
|
+
`# Install`,
|
|
1363
|
+
installCommand,
|
|
1364
|
+
"",
|
|
1365
|
+
"# Initialize the project",
|
|
1366
|
+
firstRunCommand,
|
|
1367
|
+
"",
|
|
1368
|
+
"# Check project status",
|
|
1369
|
+
exampleUsage,
|
|
1370
|
+
"```",
|
|
1371
|
+
"",
|
|
1372
|
+
"## Installation",
|
|
1373
|
+
"",
|
|
1374
|
+
"```bash",
|
|
1375
|
+
installCommand,
|
|
1376
|
+
"```",
|
|
1377
|
+
"",
|
|
1378
|
+
"## Usage",
|
|
1379
|
+
"",
|
|
1380
|
+
`After installation, initialize ${projectName} in your project directory:`,
|
|
1381
|
+
"",
|
|
1382
|
+
"```bash",
|
|
1383
|
+
"codeharness init",
|
|
1384
|
+
"```",
|
|
1385
|
+
"",
|
|
1386
|
+
"This sets up the harness with stack detection, observability, and documentation scaffolding.",
|
|
1387
|
+
"",
|
|
1388
|
+
"## CLI Reference",
|
|
1389
|
+
"",
|
|
1390
|
+
"```",
|
|
1391
|
+
cliHelpOutput.trimEnd(),
|
|
1392
|
+
"```",
|
|
1393
|
+
""
|
|
1394
|
+
];
|
|
1395
|
+
return lines.join("\n");
|
|
1396
|
+
}
|
|
1397
|
+
function getInstallCommand(stack) {
|
|
1398
|
+
if (stack === "python") {
|
|
1399
|
+
return "pip install codeharness";
|
|
1400
|
+
}
|
|
1401
|
+
return "npm install -g codeharness";
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1350
1404
|
// src/commands/init.ts
|
|
1351
|
-
var HARNESS_VERSION = true ? "0.
|
|
1405
|
+
var HARNESS_VERSION = true ? "0.16.0" : "0.0.0-dev";
|
|
1406
|
+
function getProjectName(projectDir) {
|
|
1407
|
+
try {
|
|
1408
|
+
const pkgPath = join6(projectDir, "package.json");
|
|
1409
|
+
if (existsSync6(pkgPath)) {
|
|
1410
|
+
const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
1411
|
+
if (pkg.name && typeof pkg.name === "string") {
|
|
1412
|
+
return pkg.name;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
} catch {
|
|
1416
|
+
}
|
|
1417
|
+
return basename2(projectDir);
|
|
1418
|
+
}
|
|
1352
1419
|
function getStackLabel(stack) {
|
|
1353
1420
|
if (stack === "nodejs") return "Node.js (package.json)";
|
|
1354
1421
|
if (stack === "python") return "Python";
|
|
@@ -1445,7 +1512,8 @@ function registerInitCommand(program) {
|
|
|
1445
1512
|
},
|
|
1446
1513
|
documentation: {
|
|
1447
1514
|
agents_md: "created",
|
|
1448
|
-
docs_scaffold: "created"
|
|
1515
|
+
docs_scaffold: "created",
|
|
1516
|
+
readme: "created"
|
|
1449
1517
|
}
|
|
1450
1518
|
};
|
|
1451
1519
|
const statePath = getStatePath(projectDir);
|
|
@@ -1458,6 +1526,7 @@ function registerInitCommand(program) {
|
|
|
1458
1526
|
result.enforcement = existingState.enforcement;
|
|
1459
1527
|
result.documentation.agents_md = "exists";
|
|
1460
1528
|
result.documentation.docs_scaffold = "exists";
|
|
1529
|
+
result.documentation.readme = "exists";
|
|
1461
1530
|
if (isJson) {
|
|
1462
1531
|
jsonOutput(result);
|
|
1463
1532
|
} else {
|
|
@@ -1658,10 +1727,35 @@ function registerInitCommand(program) {
|
|
|
1658
1727
|
} else {
|
|
1659
1728
|
result.documentation.docs_scaffold = "exists";
|
|
1660
1729
|
}
|
|
1730
|
+
const readmePath = join6(projectDir, "README.md");
|
|
1731
|
+
if (!existsSync6(readmePath)) {
|
|
1732
|
+
let cliHelpOutput = "";
|
|
1733
|
+
try {
|
|
1734
|
+
const { execFileSync: execFileSync8 } = await import("child_process");
|
|
1735
|
+
cliHelpOutput = execFileSync8(process.execPath, [process.argv[1], "--help"], {
|
|
1736
|
+
stdio: "pipe",
|
|
1737
|
+
timeout: 1e4
|
|
1738
|
+
}).toString();
|
|
1739
|
+
} catch {
|
|
1740
|
+
cliHelpOutput = "Run: codeharness --help";
|
|
1741
|
+
}
|
|
1742
|
+
const readmeContent = readmeTemplate({
|
|
1743
|
+
projectName: getProjectName(projectDir),
|
|
1744
|
+
stack,
|
|
1745
|
+
cliHelpOutput
|
|
1746
|
+
});
|
|
1747
|
+
generateFile(readmePath, readmeContent);
|
|
1748
|
+
result.documentation.readme = "created";
|
|
1749
|
+
} else {
|
|
1750
|
+
result.documentation.readme = "exists";
|
|
1751
|
+
}
|
|
1661
1752
|
if (!isJson) {
|
|
1662
1753
|
if (result.documentation.agents_md === "created" || result.documentation.docs_scaffold === "created") {
|
|
1663
1754
|
ok("Documentation: AGENTS.md + docs/ scaffold created");
|
|
1664
1755
|
}
|
|
1756
|
+
if (result.documentation.readme === "created") {
|
|
1757
|
+
ok("Documentation: README.md created");
|
|
1758
|
+
}
|
|
1665
1759
|
}
|
|
1666
1760
|
const otlpResult = instrumentProject(projectDir, stack, { json: isJson, appType });
|
|
1667
1761
|
result.otlp = otlpResult;
|
|
@@ -1901,12 +1995,12 @@ function registerBridgeCommand(program) {
|
|
|
1901
1995
|
|
|
1902
1996
|
// src/commands/run.ts
|
|
1903
1997
|
import { spawn } from "child_process";
|
|
1904
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as
|
|
1998
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
1905
1999
|
import { join as join8, dirname as dirname2 } from "path";
|
|
1906
2000
|
import { fileURLToPath } from "url";
|
|
1907
2001
|
|
|
1908
2002
|
// src/lib/beads-sync.ts
|
|
1909
|
-
import { existsSync as existsSync8, readFileSync as
|
|
2003
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
1910
2004
|
import { join as join7 } from "path";
|
|
1911
2005
|
import { parse as parse2 } from "yaml";
|
|
1912
2006
|
var BEADS_TO_STORY_STATUS = {
|
|
@@ -1941,7 +2035,7 @@ function readStoryFileStatus(filePath) {
|
|
|
1941
2035
|
if (!existsSync8(filePath)) {
|
|
1942
2036
|
return null;
|
|
1943
2037
|
}
|
|
1944
|
-
const content =
|
|
2038
|
+
const content = readFileSync7(filePath, "utf-8");
|
|
1945
2039
|
const match = content.match(/^Status:\s*(.+)$/m);
|
|
1946
2040
|
if (!match) {
|
|
1947
2041
|
return null;
|
|
@@ -1949,7 +2043,7 @@ function readStoryFileStatus(filePath) {
|
|
|
1949
2043
|
return match[1].trim();
|
|
1950
2044
|
}
|
|
1951
2045
|
function updateStoryFileStatus(filePath, newStatus) {
|
|
1952
|
-
const content =
|
|
2046
|
+
const content = readFileSync7(filePath, "utf-8");
|
|
1953
2047
|
const statusRegex = /^Status:\s*.+$/m;
|
|
1954
2048
|
if (statusRegex.test(content)) {
|
|
1955
2049
|
const updated = content.replace(statusRegex, `Status: ${newStatus}`);
|
|
@@ -1973,7 +2067,7 @@ function readSprintStatus(dir) {
|
|
|
1973
2067
|
return {};
|
|
1974
2068
|
}
|
|
1975
2069
|
try {
|
|
1976
|
-
const content =
|
|
2070
|
+
const content = readFileSync7(filePath, "utf-8");
|
|
1977
2071
|
const parsed = parse2(content);
|
|
1978
2072
|
if (!parsed || typeof parsed !== "object") {
|
|
1979
2073
|
return {};
|
|
@@ -1994,7 +2088,7 @@ function updateSprintStatus(storyKey, newStatus, dir) {
|
|
|
1994
2088
|
warn(`sprint-status.yaml not found at ${filePath}, skipping update`);
|
|
1995
2089
|
return;
|
|
1996
2090
|
}
|
|
1997
|
-
const content =
|
|
2091
|
+
const content = readFileSync7(filePath, "utf-8");
|
|
1998
2092
|
const keyPattern = new RegExp(`^(\\s*${escapeRegExp(storyKey)}:\\s*)\\S+(.*)$`, "m");
|
|
1999
2093
|
if (!keyPattern.test(content)) {
|
|
2000
2094
|
return;
|
|
@@ -2039,7 +2133,7 @@ function appendOnboardingEpicToSprint(stories, dir) {
|
|
|
2039
2133
|
}
|
|
2040
2134
|
lines.push(` epic-${epicNum}-retrospective: optional`);
|
|
2041
2135
|
lines.push("");
|
|
2042
|
-
const content =
|
|
2136
|
+
const content = readFileSync7(filePath, "utf-8");
|
|
2043
2137
|
const updated = content.trimEnd() + "\n" + lines.join("\n");
|
|
2044
2138
|
writeFileSync5(filePath, updated, "utf-8");
|
|
2045
2139
|
return { epicNumber: epicNum, storyKeys };
|
|
@@ -2452,7 +2546,7 @@ function registerRunCommand(program) {
|
|
|
2452
2546
|
let flaggedStories;
|
|
2453
2547
|
if (existsSync9(flaggedFilePath)) {
|
|
2454
2548
|
try {
|
|
2455
|
-
const flaggedContent =
|
|
2549
|
+
const flaggedContent = readFileSync8(flaggedFilePath, "utf-8");
|
|
2456
2550
|
flaggedStories = flaggedContent.split("\n").filter((s) => s.trim().length > 0);
|
|
2457
2551
|
} catch {
|
|
2458
2552
|
}
|
|
@@ -2505,7 +2599,7 @@ function registerRunCommand(program) {
|
|
|
2505
2599
|
const statusFile = join8(projectDir, "ralph", "status.json");
|
|
2506
2600
|
if (existsSync9(statusFile)) {
|
|
2507
2601
|
try {
|
|
2508
|
-
const statusData = JSON.parse(
|
|
2602
|
+
const statusData = JSON.parse(readFileSync8(statusFile, "utf-8"));
|
|
2509
2603
|
const finalStatuses = readSprintStatus(projectDir);
|
|
2510
2604
|
const finalCounts = countStories(finalStatuses);
|
|
2511
2605
|
jsonOutput({
|
|
@@ -2555,11 +2649,11 @@ function registerRunCommand(program) {
|
|
|
2555
2649
|
}
|
|
2556
2650
|
|
|
2557
2651
|
// src/commands/verify.ts
|
|
2558
|
-
import { existsSync as existsSync13, readFileSync as
|
|
2652
|
+
import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
|
|
2559
2653
|
import { join as join11 } from "path";
|
|
2560
2654
|
|
|
2561
2655
|
// src/lib/verify-parser.ts
|
|
2562
|
-
import { existsSync as existsSync10, readFileSync as
|
|
2656
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
2563
2657
|
var UI_KEYWORDS = [
|
|
2564
2658
|
"agent-browser",
|
|
2565
2659
|
"screenshot",
|
|
@@ -2640,7 +2734,7 @@ function parseStoryACs(storyFilePath) {
|
|
|
2640
2734
|
`Story file not found: ${storyFilePath}. Ensure the story file exists at the expected path.`
|
|
2641
2735
|
);
|
|
2642
2736
|
}
|
|
2643
|
-
const content =
|
|
2737
|
+
const content = readFileSync9(storyFilePath, "utf-8");
|
|
2644
2738
|
const lines = content.split("\n");
|
|
2645
2739
|
let acSectionStart = -1;
|
|
2646
2740
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -2701,7 +2795,7 @@ function parseStoryACs(storyFilePath) {
|
|
|
2701
2795
|
|
|
2702
2796
|
// src/lib/verify.ts
|
|
2703
2797
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
2704
|
-
import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as
|
|
2798
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
|
|
2705
2799
|
import { join as join10 } from "path";
|
|
2706
2800
|
|
|
2707
2801
|
// src/lib/doc-health.ts
|
|
@@ -2709,7 +2803,7 @@ import { execSync } from "child_process";
|
|
|
2709
2803
|
import {
|
|
2710
2804
|
existsSync as existsSync11,
|
|
2711
2805
|
mkdirSync as mkdirSync4,
|
|
2712
|
-
readFileSync as
|
|
2806
|
+
readFileSync as readFileSync10,
|
|
2713
2807
|
readdirSync as readdirSync2,
|
|
2714
2808
|
statSync,
|
|
2715
2809
|
unlinkSync,
|
|
@@ -2851,15 +2945,15 @@ function getSourceFilesInModule(modulePath) {
|
|
|
2851
2945
|
}
|
|
2852
2946
|
function getMentionedFilesInAgentsMd(agentsPath) {
|
|
2853
2947
|
if (!existsSync11(agentsPath)) return [];
|
|
2854
|
-
const content =
|
|
2948
|
+
const content = readFileSync10(agentsPath, "utf-8");
|
|
2855
2949
|
const mentioned = /* @__PURE__ */ new Set();
|
|
2856
2950
|
const filenamePattern = /[\w./-]*[\w-]+\.(?:ts|js|py)\b/g;
|
|
2857
2951
|
let match;
|
|
2858
2952
|
while ((match = filenamePattern.exec(content)) !== null) {
|
|
2859
2953
|
const fullMatch = match[0];
|
|
2860
|
-
const
|
|
2861
|
-
if (!isTestFile(
|
|
2862
|
-
mentioned.add(
|
|
2954
|
+
const basename4 = fullMatch.split("/").pop();
|
|
2955
|
+
if (!isTestFile(basename4)) {
|
|
2956
|
+
mentioned.add(basename4);
|
|
2863
2957
|
}
|
|
2864
2958
|
}
|
|
2865
2959
|
return Array.from(mentioned);
|
|
@@ -2913,7 +3007,7 @@ function checkAgentsMdForModule(modulePath, dir) {
|
|
|
2913
3007
|
function checkDoNotEditHeaders(docPath) {
|
|
2914
3008
|
if (!existsSync11(docPath)) return false;
|
|
2915
3009
|
try {
|
|
2916
|
-
const content =
|
|
3010
|
+
const content = readFileSync10(docPath, "utf-8");
|
|
2917
3011
|
if (content.length === 0) return false;
|
|
2918
3012
|
return content.trimStart().startsWith(DO_NOT_EDIT_HEADER2);
|
|
2919
3013
|
} catch {
|
|
@@ -2994,7 +3088,7 @@ function scanDocHealth(dir) {
|
|
|
2994
3088
|
}
|
|
2995
3089
|
const indexPath = join9(root, "docs", "index.md");
|
|
2996
3090
|
if (existsSync11(indexPath)) {
|
|
2997
|
-
const content =
|
|
3091
|
+
const content = readFileSync10(indexPath, "utf-8");
|
|
2998
3092
|
const hasAbsolutePaths = /https?:\/\/|file:\/\//i.test(content);
|
|
2999
3093
|
documents.push({
|
|
3000
3094
|
path: "docs/index.md",
|
|
@@ -3056,7 +3150,7 @@ function scanDocHealth(dir) {
|
|
|
3056
3150
|
}
|
|
3057
3151
|
function checkAgentsMdLineCount(filePath, docPath, documents) {
|
|
3058
3152
|
try {
|
|
3059
|
-
const content =
|
|
3153
|
+
const content = readFileSync10(filePath, "utf-8");
|
|
3060
3154
|
const lineCount = content.split("\n").length;
|
|
3061
3155
|
if (lineCount > 100) {
|
|
3062
3156
|
documents.push({
|
|
@@ -3144,7 +3238,7 @@ function completeExecPlan(storyId, dir) {
|
|
|
3144
3238
|
if (!existsSync11(activePath)) {
|
|
3145
3239
|
return null;
|
|
3146
3240
|
}
|
|
3147
|
-
let content =
|
|
3241
|
+
let content = readFileSync10(activePath, "utf-8");
|
|
3148
3242
|
content = content.replace(/^Status:\s*active$/m, "Status: completed");
|
|
3149
3243
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3150
3244
|
content = content.replace(
|
|
@@ -3240,6 +3334,70 @@ function showboatProofTemplate(config) {
|
|
|
3240
3334
|
}
|
|
3241
3335
|
|
|
3242
3336
|
// src/lib/verify.ts
|
|
3337
|
+
function classifyEvidenceCommands(proofContent) {
|
|
3338
|
+
const results = [];
|
|
3339
|
+
const codeBlockPattern = /```(?:bash|shell)\n([\s\S]*?)```/g;
|
|
3340
|
+
for (const match of proofContent.matchAll(codeBlockPattern)) {
|
|
3341
|
+
const block = match[1].trim();
|
|
3342
|
+
const lines = block.split("\n").filter((l) => l.trim().length > 0);
|
|
3343
|
+
for (const line of lines) {
|
|
3344
|
+
const cmd = line.trim();
|
|
3345
|
+
if (!cmd) continue;
|
|
3346
|
+
results.push({ command: cmd, type: classifyCommand(cmd) });
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
return results;
|
|
3350
|
+
}
|
|
3351
|
+
function classifyCommand(cmd) {
|
|
3352
|
+
if (/docker\s+exec\b/.test(cmd)) {
|
|
3353
|
+
return "docker-exec";
|
|
3354
|
+
}
|
|
3355
|
+
if (/curl\b/.test(cmd) && /localhost:(9428|8428|16686)\b/.test(cmd)) {
|
|
3356
|
+
return "observability";
|
|
3357
|
+
}
|
|
3358
|
+
if (/\bgrep\b/.test(cmd) && /\bsrc\//.test(cmd)) {
|
|
3359
|
+
return "grep-src";
|
|
3360
|
+
}
|
|
3361
|
+
return "other";
|
|
3362
|
+
}
|
|
3363
|
+
function checkBlackBoxEnforcement(proofContent) {
|
|
3364
|
+
const commands = classifyEvidenceCommands(proofContent);
|
|
3365
|
+
const grepSrcCount = commands.filter((c) => c.type === "grep-src").length;
|
|
3366
|
+
const dockerExecCount = commands.filter((c) => c.type === "docker-exec").length;
|
|
3367
|
+
const observabilityCount = commands.filter((c) => c.type === "observability").length;
|
|
3368
|
+
const otherCount = commands.filter((c) => c.type === "other").length;
|
|
3369
|
+
const totalCommands = commands.length;
|
|
3370
|
+
const grepRatio = totalCommands > 0 ? grepSrcCount / totalCommands : 0;
|
|
3371
|
+
const acsMissingDockerExec = [];
|
|
3372
|
+
const acHeaderPattern = /^## AC ?(\d+):/gm;
|
|
3373
|
+
const acMatches = [...proofContent.matchAll(acHeaderPattern)];
|
|
3374
|
+
if (acMatches.length > 0) {
|
|
3375
|
+
for (let i = 0; i < acMatches.length; i++) {
|
|
3376
|
+
const acNum = parseInt(acMatches[i][1], 10);
|
|
3377
|
+
const start = acMatches[i].index;
|
|
3378
|
+
const end = i + 1 < acMatches.length ? acMatches[i + 1].index : proofContent.length;
|
|
3379
|
+
const section = proofContent.slice(start, end);
|
|
3380
|
+
if (section.includes("[ESCALATE]")) continue;
|
|
3381
|
+
const sectionCommands = classifyEvidenceCommands(section);
|
|
3382
|
+
const hasDockerExec = sectionCommands.some((c) => c.type === "docker-exec");
|
|
3383
|
+
if (!hasDockerExec) {
|
|
3384
|
+
acsMissingDockerExec.push(acNum);
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
const grepTooHigh = grepRatio > 0.5;
|
|
3389
|
+
const missingDockerExec = acsMissingDockerExec.length > 0;
|
|
3390
|
+
const hasExtractableCommands = totalCommands > 0;
|
|
3391
|
+
return {
|
|
3392
|
+
blackBoxPass: !hasExtractableCommands || !grepTooHigh && !missingDockerExec,
|
|
3393
|
+
grepSrcCount,
|
|
3394
|
+
dockerExecCount,
|
|
3395
|
+
observabilityCount,
|
|
3396
|
+
otherCount,
|
|
3397
|
+
grepRatio,
|
|
3398
|
+
acsMissingDockerExec
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3243
3401
|
function checkPreconditions(dir, storyId) {
|
|
3244
3402
|
const state = readState(dir);
|
|
3245
3403
|
const failures = [];
|
|
@@ -3309,10 +3467,35 @@ function runShowboatVerify(proofPath) {
|
|
|
3309
3467
|
}
|
|
3310
3468
|
}
|
|
3311
3469
|
function validateProofQuality(proofPath) {
|
|
3470
|
+
const emptyResult = {
|
|
3471
|
+
verified: 0,
|
|
3472
|
+
pending: 0,
|
|
3473
|
+
escalated: 0,
|
|
3474
|
+
total: 0,
|
|
3475
|
+
passed: false,
|
|
3476
|
+
grepSrcCount: 0,
|
|
3477
|
+
dockerExecCount: 0,
|
|
3478
|
+
observabilityCount: 0,
|
|
3479
|
+
otherCount: 0,
|
|
3480
|
+
blackBoxPass: false
|
|
3481
|
+
};
|
|
3312
3482
|
if (!existsSync12(proofPath)) {
|
|
3313
|
-
return
|
|
3483
|
+
return emptyResult;
|
|
3484
|
+
}
|
|
3485
|
+
const content = readFileSync11(proofPath, "utf-8");
|
|
3486
|
+
const bbEnforcement = checkBlackBoxEnforcement(content);
|
|
3487
|
+
function buildResult(base) {
|
|
3488
|
+
const basePassed = base.pending === 0 && base.verified > 0;
|
|
3489
|
+
return {
|
|
3490
|
+
...base,
|
|
3491
|
+
passed: basePassed && bbEnforcement.blackBoxPass,
|
|
3492
|
+
grepSrcCount: bbEnforcement.grepSrcCount,
|
|
3493
|
+
dockerExecCount: bbEnforcement.dockerExecCount,
|
|
3494
|
+
observabilityCount: bbEnforcement.observabilityCount,
|
|
3495
|
+
otherCount: bbEnforcement.otherCount,
|
|
3496
|
+
blackBoxPass: bbEnforcement.blackBoxPass
|
|
3497
|
+
};
|
|
3314
3498
|
}
|
|
3315
|
-
const content = readFileSync10(proofPath, "utf-8");
|
|
3316
3499
|
const acHeaderPattern = /^## AC ?(\d+):/gm;
|
|
3317
3500
|
const matches = [...content.matchAll(acHeaderPattern)];
|
|
3318
3501
|
let verified = 0;
|
|
@@ -3343,7 +3526,41 @@ function validateProofQuality(proofPath) {
|
|
|
3343
3526
|
const narrativeMatches = [...content.matchAll(narrativeAcPattern)];
|
|
3344
3527
|
const narrativeAcNumbers = new Set(narrativeMatches.map((m) => m[1]));
|
|
3345
3528
|
if (narrativeAcNumbers.size === 0) {
|
|
3346
|
-
|
|
3529
|
+
const bulletAcPattern = /^- AC ?(\d+)[^:\n]*:/gm;
|
|
3530
|
+
const bulletMatches = [...content.matchAll(bulletAcPattern)];
|
|
3531
|
+
const bulletAcNumbers = new Set(bulletMatches.map((m) => m[1]));
|
|
3532
|
+
if (bulletAcNumbers.size === 0) {
|
|
3533
|
+
return buildResult({ verified: 0, pending: 0, escalated: 0, total: 0 });
|
|
3534
|
+
}
|
|
3535
|
+
let bVerified = 0;
|
|
3536
|
+
let bPending = 0;
|
|
3537
|
+
let bEscalated = 0;
|
|
3538
|
+
for (const acNum of bulletAcNumbers) {
|
|
3539
|
+
const bulletPattern = new RegExp(`^- AC ?${acNum}[^:\\n]*:(.*)$`, "m");
|
|
3540
|
+
const bulletMatch = content.match(bulletPattern);
|
|
3541
|
+
if (!bulletMatch) {
|
|
3542
|
+
bPending++;
|
|
3543
|
+
continue;
|
|
3544
|
+
}
|
|
3545
|
+
const bulletText = bulletMatch[1].toLowerCase();
|
|
3546
|
+
if (bulletText.includes("n/a") || bulletText.includes("escalat") || bulletText.includes("superseded")) {
|
|
3547
|
+
bEscalated++;
|
|
3548
|
+
} else {
|
|
3549
|
+
bVerified++;
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
const hasAnyEvidence = /```output\n/m.test(content);
|
|
3553
|
+
if (!hasAnyEvidence) {
|
|
3554
|
+
bPending += bVerified;
|
|
3555
|
+
bVerified = 0;
|
|
3556
|
+
}
|
|
3557
|
+
const bTotal = bVerified + bPending + bEscalated;
|
|
3558
|
+
return buildResult({
|
|
3559
|
+
verified: bVerified,
|
|
3560
|
+
pending: bPending,
|
|
3561
|
+
escalated: bEscalated,
|
|
3562
|
+
total: bTotal
|
|
3563
|
+
});
|
|
3347
3564
|
}
|
|
3348
3565
|
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);
|
|
3349
3566
|
for (let i = 0; i < sortedAcs.length; i++) {
|
|
@@ -3360,13 +3577,12 @@ function validateProofQuality(proofPath) {
|
|
|
3360
3577
|
}
|
|
3361
3578
|
}
|
|
3362
3579
|
const narrativeTotal = verified + pending + escalated;
|
|
3363
|
-
return {
|
|
3580
|
+
return buildResult({
|
|
3364
3581
|
verified,
|
|
3365
3582
|
pending,
|
|
3366
3583
|
escalated,
|
|
3367
|
-
total: narrativeTotal
|
|
3368
|
-
|
|
3369
|
-
};
|
|
3584
|
+
total: narrativeTotal
|
|
3585
|
+
});
|
|
3370
3586
|
}
|
|
3371
3587
|
for (const acNum of acNumbers) {
|
|
3372
3588
|
const acPattern = new RegExp(`--- AC ?${acNum}:`, "g");
|
|
@@ -3389,15 +3605,7 @@ function validateProofQuality(proofPath) {
|
|
|
3389
3605
|
}
|
|
3390
3606
|
}
|
|
3391
3607
|
const total = verified + pending + escalated;
|
|
3392
|
-
return {
|
|
3393
|
-
verified,
|
|
3394
|
-
pending,
|
|
3395
|
-
escalated,
|
|
3396
|
-
total,
|
|
3397
|
-
// Proof passes when no pending ACs remain and at least one is verified.
|
|
3398
|
-
// Escalated ACs are allowed — they are explicitly acknowledged as unverifiable.
|
|
3399
|
-
passed: pending === 0 && verified > 0
|
|
3400
|
-
};
|
|
3608
|
+
return buildResult({ verified, pending, escalated, total });
|
|
3401
3609
|
}
|
|
3402
3610
|
function updateVerificationState(storyId, result, dir) {
|
|
3403
3611
|
const { state, body } = readStateWithBody(dir);
|
|
@@ -3497,6 +3705,16 @@ function verifyStory(storyId, isJson, root) {
|
|
|
3497
3705
|
process.exitCode = 1;
|
|
3498
3706
|
return;
|
|
3499
3707
|
}
|
|
3708
|
+
const readmePath = join11(root, "README.md");
|
|
3709
|
+
if (!existsSync13(readmePath)) {
|
|
3710
|
+
if (isJson) {
|
|
3711
|
+
jsonOutput({ status: "fail", message: "No README.md found \u2014 verification requires user documentation" });
|
|
3712
|
+
} else {
|
|
3713
|
+
fail("No README.md found \u2014 verification requires user documentation");
|
|
3714
|
+
}
|
|
3715
|
+
process.exitCode = 1;
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3500
3718
|
const storyFilePath = join11(root, STORY_DIR, `${storyId}.md`);
|
|
3501
3719
|
if (!existsSync13(storyFilePath)) {
|
|
3502
3720
|
fail(`Story file not found: ${storyFilePath}`, { json: isJson });
|
|
@@ -3566,7 +3784,15 @@ function verifyStory(storyId, isJson, root) {
|
|
|
3566
3784
|
} else {
|
|
3567
3785
|
showboatStatus = showboatResult.passed ? "pass" : "fail";
|
|
3568
3786
|
if (!showboatResult.passed) {
|
|
3569
|
-
|
|
3787
|
+
if (isJson) {
|
|
3788
|
+
jsonOutput({
|
|
3789
|
+
status: "fail",
|
|
3790
|
+
message: `Showboat verify failed: ${showboatResult.output}`,
|
|
3791
|
+
proofQuality: { verified: proofQuality.verified, pending: proofQuality.pending, escalated: proofQuality.escalated, total: proofQuality.total }
|
|
3792
|
+
});
|
|
3793
|
+
} else {
|
|
3794
|
+
fail(`Showboat verify failed: ${showboatResult.output}`);
|
|
3795
|
+
}
|
|
3570
3796
|
process.exitCode = 1;
|
|
3571
3797
|
return;
|
|
3572
3798
|
}
|
|
@@ -3625,7 +3851,7 @@ function verifyStory(storyId, isJson, root) {
|
|
|
3625
3851
|
}
|
|
3626
3852
|
function extractStoryTitle(filePath) {
|
|
3627
3853
|
try {
|
|
3628
|
-
const content =
|
|
3854
|
+
const content = readFileSync12(filePath, "utf-8");
|
|
3629
3855
|
const match = /^#\s+(.+)$/m.exec(content);
|
|
3630
3856
|
return match ? match[1] : "Unknown Story";
|
|
3631
3857
|
} catch {
|
|
@@ -3640,7 +3866,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
3640
3866
|
|
|
3641
3867
|
// src/lib/coverage.ts
|
|
3642
3868
|
import { execSync as execSync2 } from "child_process";
|
|
3643
|
-
import { existsSync as existsSync14, readFileSync as
|
|
3869
|
+
import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
|
|
3644
3870
|
import { join as join12 } from "path";
|
|
3645
3871
|
function detectCoverageTool(dir) {
|
|
3646
3872
|
const baseDir = dir ?? process.cwd();
|
|
@@ -3673,7 +3899,7 @@ function detectNodeCoverageTool(dir, stateHint) {
|
|
|
3673
3899
|
let pkgScripts = {};
|
|
3674
3900
|
if (existsSync14(pkgPath)) {
|
|
3675
3901
|
try {
|
|
3676
|
-
const pkg = JSON.parse(
|
|
3902
|
+
const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
|
|
3677
3903
|
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
3678
3904
|
hasVitestCoverageV8 = "@vitest/coverage-v8" in allDeps;
|
|
3679
3905
|
hasVitestCoverageIstanbul = "@vitest/coverage-istanbul" in allDeps;
|
|
@@ -3729,7 +3955,7 @@ function detectPythonCoverageTool(dir) {
|
|
|
3729
3955
|
const reqPath = join12(dir, "requirements.txt");
|
|
3730
3956
|
if (existsSync14(reqPath)) {
|
|
3731
3957
|
try {
|
|
3732
|
-
const content =
|
|
3958
|
+
const content = readFileSync13(reqPath, "utf-8");
|
|
3733
3959
|
if (content.includes("pytest-cov") || content.includes("coverage")) {
|
|
3734
3960
|
return {
|
|
3735
3961
|
tool: "coverage.py",
|
|
@@ -3743,7 +3969,7 @@ function detectPythonCoverageTool(dir) {
|
|
|
3743
3969
|
const pyprojectPath = join12(dir, "pyproject.toml");
|
|
3744
3970
|
if (existsSync14(pyprojectPath)) {
|
|
3745
3971
|
try {
|
|
3746
|
-
const content =
|
|
3972
|
+
const content = readFileSync13(pyprojectPath, "utf-8");
|
|
3747
3973
|
if (content.includes("pytest-cov") || content.includes("coverage")) {
|
|
3748
3974
|
return {
|
|
3749
3975
|
tool: "coverage.py",
|
|
@@ -3825,7 +4051,7 @@ function parseVitestCoverage(dir) {
|
|
|
3825
4051
|
return 0;
|
|
3826
4052
|
}
|
|
3827
4053
|
try {
|
|
3828
|
-
const report = JSON.parse(
|
|
4054
|
+
const report = JSON.parse(readFileSync13(reportPath, "utf-8"));
|
|
3829
4055
|
return report.total?.statements?.pct ?? 0;
|
|
3830
4056
|
} catch {
|
|
3831
4057
|
warn("Failed to parse coverage report");
|
|
@@ -3839,7 +4065,7 @@ function parsePythonCoverage(dir) {
|
|
|
3839
4065
|
return 0;
|
|
3840
4066
|
}
|
|
3841
4067
|
try {
|
|
3842
|
-
const report = JSON.parse(
|
|
4068
|
+
const report = JSON.parse(readFileSync13(reportPath, "utf-8"));
|
|
3843
4069
|
return report.totals?.percent_covered ?? 0;
|
|
3844
4070
|
} catch {
|
|
3845
4071
|
warn("Failed to parse coverage report");
|
|
@@ -3940,7 +4166,7 @@ function checkPerFileCoverage(floor, dir) {
|
|
|
3940
4166
|
}
|
|
3941
4167
|
let report;
|
|
3942
4168
|
try {
|
|
3943
|
-
report = JSON.parse(
|
|
4169
|
+
report = JSON.parse(readFileSync13(reportPath, "utf-8"));
|
|
3944
4170
|
} catch {
|
|
3945
4171
|
warn("Failed to parse coverage-summary.json");
|
|
3946
4172
|
return { floor, violations: [], totalFiles: 0 };
|
|
@@ -4640,7 +4866,7 @@ import { join as join17 } from "path";
|
|
|
4640
4866
|
import {
|
|
4641
4867
|
existsSync as existsSync16,
|
|
4642
4868
|
readdirSync as readdirSync3,
|
|
4643
|
-
readFileSync as
|
|
4869
|
+
readFileSync as readFileSync14,
|
|
4644
4870
|
statSync as statSync2
|
|
4645
4871
|
} from "fs";
|
|
4646
4872
|
import { join as join14, relative as relative2 } from "path";
|
|
@@ -4814,7 +5040,7 @@ function readVitestPerFileCoverage(dir) {
|
|
|
4814
5040
|
const reportPath = join14(dir, "coverage", "coverage-summary.json");
|
|
4815
5041
|
if (!existsSync16(reportPath)) return null;
|
|
4816
5042
|
try {
|
|
4817
|
-
const report = JSON.parse(
|
|
5043
|
+
const report = JSON.parse(readFileSync14(reportPath, "utf-8"));
|
|
4818
5044
|
const result = /* @__PURE__ */ new Map();
|
|
4819
5045
|
for (const [key, value] of Object.entries(report)) {
|
|
4820
5046
|
if (key === "total") continue;
|
|
@@ -4829,7 +5055,7 @@ function readPythonPerFileCoverage(dir) {
|
|
|
4829
5055
|
const reportPath = join14(dir, "coverage.json");
|
|
4830
5056
|
if (!existsSync16(reportPath)) return null;
|
|
4831
5057
|
try {
|
|
4832
|
-
const report = JSON.parse(
|
|
5058
|
+
const report = JSON.parse(readFileSync14(reportPath, "utf-8"));
|
|
4833
5059
|
if (!report.files) return null;
|
|
4834
5060
|
const result = /* @__PURE__ */ new Map();
|
|
4835
5061
|
for (const [key, value] of Object.entries(report.files)) {
|
|
@@ -4906,6 +5132,18 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
|
|
|
4906
5132
|
const root = rootDir ?? process.cwd();
|
|
4907
5133
|
const stories = [];
|
|
4908
5134
|
let storyNum = 1;
|
|
5135
|
+
const readmeDoc = audit.documents.find((d) => d.name === "README.md");
|
|
5136
|
+
if (readmeDoc && readmeDoc.grade === "missing") {
|
|
5137
|
+
stories.push({
|
|
5138
|
+
key: `0.${storyNum}`,
|
|
5139
|
+
title: "Create README.md",
|
|
5140
|
+
type: "doc-freshness",
|
|
5141
|
+
acceptanceCriteria: [
|
|
5142
|
+
"**Given** no README.md exists\n**When** the agent runs `codeharness init`\n**Then** README.md is created with project name, installation command, Quick Start section, and CLI reference"
|
|
5143
|
+
]
|
|
5144
|
+
});
|
|
5145
|
+
storyNum++;
|
|
5146
|
+
}
|
|
4909
5147
|
const archDoc = audit.documents.find((d) => d.name === "ARCHITECTURE.md");
|
|
4910
5148
|
if (archDoc && archDoc.grade === "missing") {
|
|
4911
5149
|
stories.push({
|
|
@@ -5079,6 +5317,7 @@ function importOnboardingEpic(epicPath, beadsFns) {
|
|
|
5079
5317
|
function getPriorityFromTitle(title) {
|
|
5080
5318
|
if (title.startsWith("Add test coverage for ")) return PRIORITY_BY_TYPE.coverage;
|
|
5081
5319
|
if (title.startsWith("Create ") && title.endsWith("AGENTS.md")) return PRIORITY_BY_TYPE["agents-md"];
|
|
5320
|
+
if (title === "Create README.md") return PRIORITY_BY_TYPE["doc-freshness"];
|
|
5082
5321
|
if (title === "Create ARCHITECTURE.md") return PRIORITY_BY_TYPE.architecture;
|
|
5083
5322
|
if (title === "Update stale documentation") return PRIORITY_BY_TYPE["doc-freshness"];
|
|
5084
5323
|
if (title.startsWith("Create verification proof for ")) return PRIORITY_BY_TYPE.verification;
|
|
@@ -5094,6 +5333,9 @@ function getGapIdFromTitle(title) {
|
|
|
5094
5333
|
const mod = title.slice("Create ".length);
|
|
5095
5334
|
return `[gap:docs:${mod}]`;
|
|
5096
5335
|
}
|
|
5336
|
+
if (title === "Create README.md") {
|
|
5337
|
+
return "[gap:docs:README.md]";
|
|
5338
|
+
}
|
|
5097
5339
|
if (title === "Create ARCHITECTURE.md") {
|
|
5098
5340
|
return "[gap:docs:ARCHITECTURE.md]";
|
|
5099
5341
|
}
|
|
@@ -5114,7 +5356,7 @@ function getGapIdFromTitle(title) {
|
|
|
5114
5356
|
}
|
|
5115
5357
|
|
|
5116
5358
|
// src/lib/scan-cache.ts
|
|
5117
|
-
import { existsSync as existsSync18, mkdirSync as mkdirSync7, readFileSync as
|
|
5359
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync7, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
|
|
5118
5360
|
import { join as join16 } from "path";
|
|
5119
5361
|
var CACHE_DIR = ".harness";
|
|
5120
5362
|
var CACHE_FILE = "last-onboard-scan.json";
|
|
@@ -5136,7 +5378,7 @@ function loadScanCache(dir) {
|
|
|
5136
5378
|
return null;
|
|
5137
5379
|
}
|
|
5138
5380
|
try {
|
|
5139
|
-
const raw =
|
|
5381
|
+
const raw = readFileSync15(cachePath, "utf-8");
|
|
5140
5382
|
return JSON.parse(raw);
|
|
5141
5383
|
} catch {
|
|
5142
5384
|
return null;
|
|
@@ -5179,7 +5421,11 @@ function registerOnboardCommand(program) {
|
|
|
5179
5421
|
const minModuleSize = parseInt(opts.minModuleSize ?? "3", 10);
|
|
5180
5422
|
const preconditions = runPreconditions();
|
|
5181
5423
|
if (!preconditions.canProceed) {
|
|
5182
|
-
|
|
5424
|
+
if (isJson) {
|
|
5425
|
+
jsonOutput({ status: "fail", message: "Harness not initialized \u2014 run codeharness init first" });
|
|
5426
|
+
} else {
|
|
5427
|
+
fail("Harness not initialized \u2014 run codeharness init first");
|
|
5428
|
+
}
|
|
5183
5429
|
process.exitCode = 1;
|
|
5184
5430
|
return;
|
|
5185
5431
|
}
|
|
@@ -5208,7 +5454,11 @@ function registerOnboardCommand(program) {
|
|
|
5208
5454
|
const minModuleSize = parseInt(opts.minModuleSize ?? "3", 10);
|
|
5209
5455
|
const preconditions = runPreconditions();
|
|
5210
5456
|
if (!preconditions.canProceed) {
|
|
5211
|
-
|
|
5457
|
+
if (isJson) {
|
|
5458
|
+
jsonOutput({ status: "fail", message: "Harness not initialized \u2014 run codeharness init first" });
|
|
5459
|
+
} else {
|
|
5460
|
+
fail("Harness not initialized \u2014 run codeharness init first");
|
|
5461
|
+
}
|
|
5212
5462
|
process.exitCode = 1;
|
|
5213
5463
|
return;
|
|
5214
5464
|
}
|
|
@@ -5240,7 +5490,11 @@ function registerOnboardCommand(program) {
|
|
|
5240
5490
|
const isJson = opts.json === true;
|
|
5241
5491
|
const preconditions = runPreconditions();
|
|
5242
5492
|
if (!preconditions.canProceed) {
|
|
5243
|
-
|
|
5493
|
+
if (isJson) {
|
|
5494
|
+
jsonOutput({ status: "fail", message: "Harness not initialized \u2014 run codeharness init first" });
|
|
5495
|
+
} else {
|
|
5496
|
+
fail("Harness not initialized \u2014 run codeharness init first");
|
|
5497
|
+
}
|
|
5244
5498
|
process.exitCode = 1;
|
|
5245
5499
|
return;
|
|
5246
5500
|
}
|
|
@@ -5263,7 +5517,11 @@ function registerOnboardCommand(program) {
|
|
|
5263
5517
|
const minModuleSize = parseInt(opts.minModuleSize ?? "3", 10);
|
|
5264
5518
|
const preconditions = runPreconditions();
|
|
5265
5519
|
if (!preconditions.canProceed) {
|
|
5266
|
-
|
|
5520
|
+
if (isJson) {
|
|
5521
|
+
jsonOutput({ status: "fail", message: "Harness not initialized \u2014 run codeharness init first" });
|
|
5522
|
+
} else {
|
|
5523
|
+
fail("Harness not initialized \u2014 run codeharness init first");
|
|
5524
|
+
}
|
|
5267
5525
|
process.exitCode = 1;
|
|
5268
5526
|
return;
|
|
5269
5527
|
}
|
|
@@ -5338,7 +5596,11 @@ function registerOnboardCommand(program) {
|
|
|
5338
5596
|
const minModuleSize = parseInt(opts.minModuleSize ?? "3", 10);
|
|
5339
5597
|
const preconditions = runPreconditions();
|
|
5340
5598
|
if (!preconditions.canProceed) {
|
|
5341
|
-
|
|
5599
|
+
if (isJson) {
|
|
5600
|
+
jsonOutput({ status: "fail", message: "Harness not initialized \u2014 run codeharness init first" });
|
|
5601
|
+
} else {
|
|
5602
|
+
fail("Harness not initialized \u2014 run codeharness init first");
|
|
5603
|
+
}
|
|
5342
5604
|
process.exitCode = 1;
|
|
5343
5605
|
return;
|
|
5344
5606
|
}
|
|
@@ -5470,7 +5732,7 @@ function printEpicOutput(epic) {
|
|
|
5470
5732
|
}
|
|
5471
5733
|
|
|
5472
5734
|
// src/commands/teardown.ts
|
|
5473
|
-
import { existsSync as existsSync19, unlinkSync as unlinkSync2, readFileSync as
|
|
5735
|
+
import { existsSync as existsSync19, unlinkSync as unlinkSync2, readFileSync as readFileSync16, writeFileSync as writeFileSync11, rmSync } from "fs";
|
|
5474
5736
|
import { join as join18 } from "path";
|
|
5475
5737
|
function buildDefaultResult() {
|
|
5476
5738
|
return {
|
|
@@ -5517,7 +5779,7 @@ function registerTeardownCommand(program) {
|
|
|
5517
5779
|
} else if (otlpMode === "remote-routed") {
|
|
5518
5780
|
if (!options.keepDocker) {
|
|
5519
5781
|
try {
|
|
5520
|
-
const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-
|
|
5782
|
+
const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-6TY2FN43.js");
|
|
5521
5783
|
stopCollectorOnly2();
|
|
5522
5784
|
result.docker.stopped = true;
|
|
5523
5785
|
if (!isJson) {
|
|
@@ -5549,7 +5811,7 @@ function registerTeardownCommand(program) {
|
|
|
5549
5811
|
info("Shared stack: kept running (other projects may use it)");
|
|
5550
5812
|
}
|
|
5551
5813
|
} else if (isLegacyStack) {
|
|
5552
|
-
const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-
|
|
5814
|
+
const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-6TY2FN43.js");
|
|
5553
5815
|
let stackRunning = false;
|
|
5554
5816
|
try {
|
|
5555
5817
|
stackRunning = isStackRunning2(composeFile);
|
|
@@ -5617,7 +5879,7 @@ function registerTeardownCommand(program) {
|
|
|
5617
5879
|
const pkgPath = join18(projectDir, "package.json");
|
|
5618
5880
|
if (existsSync19(pkgPath)) {
|
|
5619
5881
|
try {
|
|
5620
|
-
const raw =
|
|
5882
|
+
const raw = readFileSync16(pkgPath, "utf-8");
|
|
5621
5883
|
const pkg = JSON.parse(raw);
|
|
5622
5884
|
const scripts = pkg["scripts"];
|
|
5623
5885
|
if (scripts) {
|
|
@@ -5691,6 +5953,7 @@ function registerTeardownCommand(program) {
|
|
|
5691
5953
|
import { stringify as stringify3 } from "yaml";
|
|
5692
5954
|
function registerStateCommand(program) {
|
|
5693
5955
|
const stateCmd = program.command("state").description("Manage harness state");
|
|
5956
|
+
stateCmd._hidden = true;
|
|
5694
5957
|
stateCmd.command("show").description("Display full harness state").action((_, cmd) => {
|
|
5695
5958
|
const opts = cmd.optsWithGlobals();
|
|
5696
5959
|
try {
|
|
@@ -6409,7 +6672,7 @@ function registerQueryCommand(program) {
|
|
|
6409
6672
|
}
|
|
6410
6673
|
|
|
6411
6674
|
// src/commands/retro-import.ts
|
|
6412
|
-
import { existsSync as existsSync20, readFileSync as
|
|
6675
|
+
import { existsSync as existsSync20, readFileSync as readFileSync17 } from "fs";
|
|
6413
6676
|
import { join as join19 } from "path";
|
|
6414
6677
|
|
|
6415
6678
|
// src/lib/retro-parser.ts
|
|
@@ -6587,7 +6850,7 @@ function registerRetroImportCommand(program) {
|
|
|
6587
6850
|
}
|
|
6588
6851
|
let content;
|
|
6589
6852
|
try {
|
|
6590
|
-
content =
|
|
6853
|
+
content = readFileSync17(retroPath, "utf-8");
|
|
6591
6854
|
} catch (err) {
|
|
6592
6855
|
const message = err instanceof Error ? err.message : String(err);
|
|
6593
6856
|
fail(`Failed to read retro file: ${message}`, { json: isJson });
|
|
@@ -6853,8 +7116,566 @@ function registerGithubImportCommand(program) {
|
|
|
6853
7116
|
});
|
|
6854
7117
|
}
|
|
6855
7118
|
|
|
7119
|
+
// src/lib/verify-env.ts
|
|
7120
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
7121
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync18, cpSync, rmSync as rmSync2, statSync as statSync3 } from "fs";
|
|
7122
|
+
import { join as join20, basename as basename3 } from "path";
|
|
7123
|
+
import { createHash } from "crypto";
|
|
7124
|
+
var IMAGE_TAG = "codeharness-verify";
|
|
7125
|
+
var STORY_DIR3 = "_bmad-output/implementation-artifacts";
|
|
7126
|
+
var TEMP_PREFIX = "/tmp/codeharness-verify-";
|
|
7127
|
+
var STATE_KEY_DIST_HASH = "verify_env_dist_hash";
|
|
7128
|
+
function isValidStoryKey(storyKey) {
|
|
7129
|
+
if (!storyKey || storyKey.includes("..") || storyKey.includes("/") || storyKey.includes("\\")) {
|
|
7130
|
+
return false;
|
|
7131
|
+
}
|
|
7132
|
+
return /^[a-zA-Z0-9_-]+$/.test(storyKey);
|
|
7133
|
+
}
|
|
7134
|
+
function computeDistHash(projectDir) {
|
|
7135
|
+
const distDir = join20(projectDir, "dist");
|
|
7136
|
+
if (!existsSync21(distDir)) {
|
|
7137
|
+
return null;
|
|
7138
|
+
}
|
|
7139
|
+
const hash = createHash("sha256");
|
|
7140
|
+
const files = collectFiles(distDir).sort();
|
|
7141
|
+
for (const file of files) {
|
|
7142
|
+
const content = readFileSync18(file);
|
|
7143
|
+
hash.update(file.slice(distDir.length));
|
|
7144
|
+
hash.update(content);
|
|
7145
|
+
}
|
|
7146
|
+
return hash.digest("hex");
|
|
7147
|
+
}
|
|
7148
|
+
function collectFiles(dir) {
|
|
7149
|
+
const results = [];
|
|
7150
|
+
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
7151
|
+
for (const entry of entries) {
|
|
7152
|
+
const fullPath = join20(dir, entry.name);
|
|
7153
|
+
if (entry.isDirectory()) {
|
|
7154
|
+
results.push(...collectFiles(fullPath));
|
|
7155
|
+
} else {
|
|
7156
|
+
results.push(fullPath);
|
|
7157
|
+
}
|
|
7158
|
+
}
|
|
7159
|
+
return results;
|
|
7160
|
+
}
|
|
7161
|
+
function getStoredDistHash(projectDir) {
|
|
7162
|
+
try {
|
|
7163
|
+
const { state } = readStateWithBody(projectDir);
|
|
7164
|
+
const raw = state;
|
|
7165
|
+
return raw[STATE_KEY_DIST_HASH] ?? null;
|
|
7166
|
+
} catch {
|
|
7167
|
+
return null;
|
|
7168
|
+
}
|
|
7169
|
+
}
|
|
7170
|
+
function storeDistHash(projectDir, hash) {
|
|
7171
|
+
try {
|
|
7172
|
+
const { state, body } = readStateWithBody(projectDir);
|
|
7173
|
+
const raw = state;
|
|
7174
|
+
raw[STATE_KEY_DIST_HASH] = hash;
|
|
7175
|
+
writeState(state, projectDir, body);
|
|
7176
|
+
} catch {
|
|
7177
|
+
info("Could not persist dist hash to state file \u2014 cache will not be available until state is initialized");
|
|
7178
|
+
}
|
|
7179
|
+
}
|
|
7180
|
+
function buildVerifyImage(options = {}) {
|
|
7181
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
7182
|
+
if (!isDockerAvailable()) {
|
|
7183
|
+
throw new Error("Docker is not available. Install Docker and ensure the daemon is running.");
|
|
7184
|
+
}
|
|
7185
|
+
const stack = detectStack(projectDir);
|
|
7186
|
+
if (!stack) {
|
|
7187
|
+
throw new Error("Cannot detect project stack. Ensure package.json (Node.js) or requirements.txt/pyproject.toml (Python) exists.");
|
|
7188
|
+
}
|
|
7189
|
+
const currentHash = computeDistHash(projectDir);
|
|
7190
|
+
if (!currentHash) {
|
|
7191
|
+
throw new Error("No dist/ directory found. Run your build command first (e.g., npm run build).");
|
|
7192
|
+
}
|
|
7193
|
+
const storedHash = getStoredDistHash(projectDir);
|
|
7194
|
+
if (storedHash === currentHash) {
|
|
7195
|
+
if (dockerImageExists(IMAGE_TAG)) {
|
|
7196
|
+
const imageSize2 = getImageSize(IMAGE_TAG);
|
|
7197
|
+
return { imageTag: IMAGE_TAG, imageSize: imageSize2, buildTimeMs: 0, cached: true };
|
|
7198
|
+
}
|
|
7199
|
+
}
|
|
7200
|
+
const startTime = Date.now();
|
|
7201
|
+
if (stack === "nodejs") {
|
|
7202
|
+
buildNodeImage(projectDir);
|
|
7203
|
+
} else if (stack === "python") {
|
|
7204
|
+
buildPythonImage(projectDir);
|
|
7205
|
+
} else {
|
|
7206
|
+
throw new Error(`Unsupported stack for verify-env: ${stack}`);
|
|
7207
|
+
}
|
|
7208
|
+
const buildTimeMs = Date.now() - startTime;
|
|
7209
|
+
storeDistHash(projectDir, currentHash);
|
|
7210
|
+
const imageSize = getImageSize(IMAGE_TAG);
|
|
7211
|
+
return { imageTag: IMAGE_TAG, imageSize, buildTimeMs, cached: false };
|
|
7212
|
+
}
|
|
7213
|
+
function buildNodeImage(projectDir) {
|
|
7214
|
+
const packOutput = execFileSync7("npm", ["pack", "--pack-destination", "/tmp"], {
|
|
7215
|
+
cwd: projectDir,
|
|
7216
|
+
stdio: "pipe",
|
|
7217
|
+
timeout: 6e4
|
|
7218
|
+
}).toString().trim();
|
|
7219
|
+
const lastLine = packOutput.split("\n").pop()?.trim();
|
|
7220
|
+
if (!lastLine) {
|
|
7221
|
+
throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
|
|
7222
|
+
}
|
|
7223
|
+
const tarballName = basename3(lastLine);
|
|
7224
|
+
const tarballPath = join20("/tmp", tarballName);
|
|
7225
|
+
const buildContext = join20("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
7226
|
+
mkdirSync8(buildContext, { recursive: true });
|
|
7227
|
+
try {
|
|
7228
|
+
cpSync(tarballPath, join20(buildContext, tarballName));
|
|
7229
|
+
const dockerfileSrc = resolveDockerfileTemplate(projectDir);
|
|
7230
|
+
cpSync(dockerfileSrc, join20(buildContext, "Dockerfile"));
|
|
7231
|
+
execFileSync7("docker", [
|
|
7232
|
+
"build",
|
|
7233
|
+
"-t",
|
|
7234
|
+
IMAGE_TAG,
|
|
7235
|
+
"--build-arg",
|
|
7236
|
+
`TARBALL=${tarballName}`,
|
|
7237
|
+
"."
|
|
7238
|
+
], {
|
|
7239
|
+
cwd: buildContext,
|
|
7240
|
+
stdio: "pipe",
|
|
7241
|
+
timeout: 12e4
|
|
7242
|
+
});
|
|
7243
|
+
} finally {
|
|
7244
|
+
rmSync2(buildContext, { recursive: true, force: true });
|
|
7245
|
+
rmSync2(tarballPath, { force: true });
|
|
7246
|
+
}
|
|
7247
|
+
}
|
|
7248
|
+
function buildPythonImage(projectDir) {
|
|
7249
|
+
const distDir = join20(projectDir, "dist");
|
|
7250
|
+
const distFiles = readdirSync4(distDir).filter(
|
|
7251
|
+
(f) => f.endsWith(".tar.gz") || f.endsWith(".whl")
|
|
7252
|
+
);
|
|
7253
|
+
if (distFiles.length === 0) {
|
|
7254
|
+
throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
|
|
7255
|
+
}
|
|
7256
|
+
const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
|
|
7257
|
+
const buildContext = join20("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
7258
|
+
mkdirSync8(buildContext, { recursive: true });
|
|
7259
|
+
try {
|
|
7260
|
+
cpSync(join20(distDir, distFile), join20(buildContext, distFile));
|
|
7261
|
+
const dockerfileSrc = resolveDockerfileTemplate(projectDir);
|
|
7262
|
+
cpSync(dockerfileSrc, join20(buildContext, "Dockerfile"));
|
|
7263
|
+
execFileSync7("docker", [
|
|
7264
|
+
"build",
|
|
7265
|
+
"-t",
|
|
7266
|
+
IMAGE_TAG,
|
|
7267
|
+
"--build-arg",
|
|
7268
|
+
`TARBALL=${distFile}`,
|
|
7269
|
+
"."
|
|
7270
|
+
], {
|
|
7271
|
+
cwd: buildContext,
|
|
7272
|
+
stdio: "pipe",
|
|
7273
|
+
timeout: 12e4
|
|
7274
|
+
});
|
|
7275
|
+
} finally {
|
|
7276
|
+
rmSync2(buildContext, { recursive: true, force: true });
|
|
7277
|
+
}
|
|
7278
|
+
}
|
|
7279
|
+
function prepareVerifyWorkspace(storyKey, projectDir) {
|
|
7280
|
+
const root = projectDir ?? process.cwd();
|
|
7281
|
+
if (!isValidStoryKey(storyKey)) {
|
|
7282
|
+
throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
|
|
7283
|
+
}
|
|
7284
|
+
const storyFile = join20(root, STORY_DIR3, `${storyKey}.md`);
|
|
7285
|
+
if (!existsSync21(storyFile)) {
|
|
7286
|
+
throw new Error(`Story file not found: ${storyFile}`);
|
|
7287
|
+
}
|
|
7288
|
+
const workspace = `${TEMP_PREFIX}${storyKey}`;
|
|
7289
|
+
if (existsSync21(workspace)) {
|
|
7290
|
+
rmSync2(workspace, { recursive: true, force: true });
|
|
7291
|
+
}
|
|
7292
|
+
mkdirSync8(workspace, { recursive: true });
|
|
7293
|
+
cpSync(storyFile, join20(workspace, "story.md"));
|
|
7294
|
+
const readmePath = join20(root, "README.md");
|
|
7295
|
+
if (existsSync21(readmePath)) {
|
|
7296
|
+
cpSync(readmePath, join20(workspace, "README.md"));
|
|
7297
|
+
}
|
|
7298
|
+
const docsDir = join20(root, "docs");
|
|
7299
|
+
if (existsSync21(docsDir) && statSync3(docsDir).isDirectory()) {
|
|
7300
|
+
cpSync(docsDir, join20(workspace, "docs"), { recursive: true });
|
|
7301
|
+
}
|
|
7302
|
+
mkdirSync8(join20(workspace, "verification"), { recursive: true });
|
|
7303
|
+
return workspace;
|
|
7304
|
+
}
|
|
7305
|
+
function checkVerifyEnv() {
|
|
7306
|
+
const result = {
|
|
7307
|
+
imageExists: false,
|
|
7308
|
+
cliWorks: false,
|
|
7309
|
+
otelReachable: false
|
|
7310
|
+
};
|
|
7311
|
+
result.imageExists = dockerImageExists(IMAGE_TAG);
|
|
7312
|
+
if (!result.imageExists) {
|
|
7313
|
+
return result;
|
|
7314
|
+
}
|
|
7315
|
+
try {
|
|
7316
|
+
execFileSync7("docker", ["run", "--rm", IMAGE_TAG, "codeharness", "--version"], {
|
|
7317
|
+
stdio: "pipe",
|
|
7318
|
+
timeout: 3e4
|
|
7319
|
+
});
|
|
7320
|
+
result.cliWorks = true;
|
|
7321
|
+
} catch {
|
|
7322
|
+
result.cliWorks = false;
|
|
7323
|
+
}
|
|
7324
|
+
try {
|
|
7325
|
+
execFileSync7("docker", [
|
|
7326
|
+
"run",
|
|
7327
|
+
"--rm",
|
|
7328
|
+
"--add-host=host.docker.internal:host-gateway",
|
|
7329
|
+
IMAGE_TAG,
|
|
7330
|
+
"curl",
|
|
7331
|
+
"-sf",
|
|
7332
|
+
"--max-time",
|
|
7333
|
+
"5",
|
|
7334
|
+
"http://host.docker.internal:4318/v1/status"
|
|
7335
|
+
], {
|
|
7336
|
+
stdio: "pipe",
|
|
7337
|
+
timeout: 3e4
|
|
7338
|
+
});
|
|
7339
|
+
result.otelReachable = true;
|
|
7340
|
+
} catch {
|
|
7341
|
+
result.otelReachable = false;
|
|
7342
|
+
}
|
|
7343
|
+
return result;
|
|
7344
|
+
}
|
|
7345
|
+
function cleanupVerifyEnv(storyKey) {
|
|
7346
|
+
if (!isValidStoryKey(storyKey)) {
|
|
7347
|
+
throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
|
|
7348
|
+
}
|
|
7349
|
+
const workspace = `${TEMP_PREFIX}${storyKey}`;
|
|
7350
|
+
const containerName = `codeharness-verify-${storyKey}`;
|
|
7351
|
+
if (existsSync21(workspace)) {
|
|
7352
|
+
rmSync2(workspace, { recursive: true, force: true });
|
|
7353
|
+
}
|
|
7354
|
+
try {
|
|
7355
|
+
execFileSync7("docker", ["stop", containerName], {
|
|
7356
|
+
stdio: "pipe",
|
|
7357
|
+
timeout: 15e3
|
|
7358
|
+
});
|
|
7359
|
+
} catch {
|
|
7360
|
+
}
|
|
7361
|
+
try {
|
|
7362
|
+
execFileSync7("docker", ["rm", "-f", containerName], {
|
|
7363
|
+
stdio: "pipe",
|
|
7364
|
+
timeout: 15e3
|
|
7365
|
+
});
|
|
7366
|
+
} catch {
|
|
7367
|
+
}
|
|
7368
|
+
}
|
|
7369
|
+
function resolveDockerfileTemplate(projectDir) {
|
|
7370
|
+
const local = join20(projectDir, "templates", "Dockerfile.verify");
|
|
7371
|
+
if (existsSync21(local)) return local;
|
|
7372
|
+
const pkgDir = new URL("../../", import.meta.url).pathname;
|
|
7373
|
+
const pkg = join20(pkgDir, "templates", "Dockerfile.verify");
|
|
7374
|
+
if (existsSync21(pkg)) return pkg;
|
|
7375
|
+
throw new Error("Dockerfile.verify not found. Ensure templates/Dockerfile.verify exists in the project or installed package.");
|
|
7376
|
+
}
|
|
7377
|
+
function dockerImageExists(tag) {
|
|
7378
|
+
try {
|
|
7379
|
+
execFileSync7("docker", ["image", "inspect", tag], {
|
|
7380
|
+
stdio: "pipe",
|
|
7381
|
+
timeout: 1e4
|
|
7382
|
+
});
|
|
7383
|
+
return true;
|
|
7384
|
+
} catch {
|
|
7385
|
+
return false;
|
|
7386
|
+
}
|
|
7387
|
+
}
|
|
7388
|
+
function getImageSize(tag) {
|
|
7389
|
+
try {
|
|
7390
|
+
const output = execFileSync7("docker", ["image", "inspect", tag, "--format", "{{.Size}}"], {
|
|
7391
|
+
stdio: "pipe",
|
|
7392
|
+
timeout: 1e4
|
|
7393
|
+
}).toString().trim();
|
|
7394
|
+
const bytes = parseInt(output, 10);
|
|
7395
|
+
if (isNaN(bytes)) return output;
|
|
7396
|
+
if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)}GB`;
|
|
7397
|
+
if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(1)}MB`;
|
|
7398
|
+
if (bytes >= 1e3) return `${(bytes / 1e3).toFixed(1)}KB`;
|
|
7399
|
+
return `${bytes}B`;
|
|
7400
|
+
} catch {
|
|
7401
|
+
return "unknown";
|
|
7402
|
+
}
|
|
7403
|
+
}
|
|
7404
|
+
|
|
7405
|
+
// src/commands/verify-env.ts
|
|
7406
|
+
function registerVerifyEnvCommand(program) {
|
|
7407
|
+
const verifyEnv = program.command("verify-env").description("Manage verification environment (Docker image + clean workspace)");
|
|
7408
|
+
verifyEnv.command("build").description("Build the verification Docker image from project artifacts").action((_opts, cmd) => {
|
|
7409
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
7410
|
+
const isJson = globalOpts.json === true;
|
|
7411
|
+
try {
|
|
7412
|
+
const result = buildVerifyImage();
|
|
7413
|
+
if (isJson) {
|
|
7414
|
+
jsonOutput({
|
|
7415
|
+
status: "ok",
|
|
7416
|
+
imageTag: result.imageTag,
|
|
7417
|
+
imageSize: result.imageSize,
|
|
7418
|
+
buildTimeMs: result.buildTimeMs,
|
|
7419
|
+
cached: result.cached
|
|
7420
|
+
});
|
|
7421
|
+
} else {
|
|
7422
|
+
if (result.cached) {
|
|
7423
|
+
ok(`Image ${result.imageTag}: up to date (cached)`);
|
|
7424
|
+
} else {
|
|
7425
|
+
ok(`Image ${result.imageTag}: built in ${result.buildTimeMs}ms (${result.imageSize})`);
|
|
7426
|
+
}
|
|
7427
|
+
}
|
|
7428
|
+
} catch (err) {
|
|
7429
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7430
|
+
if (isJson) {
|
|
7431
|
+
jsonOutput({ status: "fail", message });
|
|
7432
|
+
} else {
|
|
7433
|
+
fail(message);
|
|
7434
|
+
}
|
|
7435
|
+
process.exitCode = 1;
|
|
7436
|
+
}
|
|
7437
|
+
});
|
|
7438
|
+
verifyEnv.command("prepare").description("Create a clean temp workspace for verification").requiredOption("--story <key>", "Story key (e.g., 13-1-verification-dockerfile-generator)").action((opts, cmd) => {
|
|
7439
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
7440
|
+
const isJson = globalOpts.json === true;
|
|
7441
|
+
try {
|
|
7442
|
+
const workspace = prepareVerifyWorkspace(opts.story);
|
|
7443
|
+
if (isJson) {
|
|
7444
|
+
jsonOutput({ status: "ok", workspace, storyKey: opts.story });
|
|
7445
|
+
} else {
|
|
7446
|
+
ok(`Workspace prepared: ${workspace}`);
|
|
7447
|
+
}
|
|
7448
|
+
} catch (err) {
|
|
7449
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7450
|
+
if (isJson) {
|
|
7451
|
+
jsonOutput({ status: "fail", message });
|
|
7452
|
+
} else {
|
|
7453
|
+
fail(message);
|
|
7454
|
+
}
|
|
7455
|
+
process.exitCode = 1;
|
|
7456
|
+
}
|
|
7457
|
+
});
|
|
7458
|
+
verifyEnv.command("check").description("Validate verification environment (image, CLI, observability)").action((_opts, cmd) => {
|
|
7459
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
7460
|
+
const isJson = globalOpts.json === true;
|
|
7461
|
+
const result = checkVerifyEnv();
|
|
7462
|
+
const allPassed = result.imageExists && result.cliWorks && result.otelReachable;
|
|
7463
|
+
if (isJson) {
|
|
7464
|
+
const jsonResult = {
|
|
7465
|
+
status: allPassed ? "ok" : "fail",
|
|
7466
|
+
imageExists: result.imageExists,
|
|
7467
|
+
cliWorks: result.cliWorks,
|
|
7468
|
+
otelReachable: result.otelReachable
|
|
7469
|
+
};
|
|
7470
|
+
if (result.imageExists && !result.cliWorks) {
|
|
7471
|
+
jsonResult.message = "CLI does not work inside verification container \u2014 build or packaging is broken";
|
|
7472
|
+
}
|
|
7473
|
+
jsonOutput(jsonResult);
|
|
7474
|
+
} else {
|
|
7475
|
+
info(`Image exists: ${result.imageExists ? "yes" : "no"}`);
|
|
7476
|
+
info(`CLI works in container: ${result.cliWorks ? "yes" : "no"}`);
|
|
7477
|
+
info(`OTEL endpoints reachable: ${result.otelReachable ? "yes" : "no"}`);
|
|
7478
|
+
if (allPassed) {
|
|
7479
|
+
ok("Verification environment: ready");
|
|
7480
|
+
} else {
|
|
7481
|
+
fail("Verification environment: not ready");
|
|
7482
|
+
if (!result.imageExists) {
|
|
7483
|
+
info("Run: codeharness verify-env build");
|
|
7484
|
+
}
|
|
7485
|
+
if (result.imageExists && !result.cliWorks) {
|
|
7486
|
+
fail("CLI does not work inside verification container \u2014 build or packaging is broken");
|
|
7487
|
+
}
|
|
7488
|
+
if (!result.otelReachable) {
|
|
7489
|
+
info("Run: codeharness stack start");
|
|
7490
|
+
}
|
|
7491
|
+
}
|
|
7492
|
+
}
|
|
7493
|
+
if (!allPassed) {
|
|
7494
|
+
process.exitCode = 1;
|
|
7495
|
+
}
|
|
7496
|
+
});
|
|
7497
|
+
verifyEnv.command("cleanup").description("Remove temp workspace and stop/remove container for a story").requiredOption("--story <key>", "Story key (e.g., 13-1-verification-dockerfile-generator)").action((opts, cmd) => {
|
|
7498
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
7499
|
+
const isJson = globalOpts.json === true;
|
|
7500
|
+
try {
|
|
7501
|
+
cleanupVerifyEnv(opts.story);
|
|
7502
|
+
if (isJson) {
|
|
7503
|
+
jsonOutput({ status: "ok", storyKey: opts.story, message: "Cleanup complete" });
|
|
7504
|
+
} else {
|
|
7505
|
+
ok(`Cleanup complete for story: ${opts.story}`);
|
|
7506
|
+
}
|
|
7507
|
+
} catch (err) {
|
|
7508
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7509
|
+
if (isJson) {
|
|
7510
|
+
jsonOutput({ status: "fail", message });
|
|
7511
|
+
} else {
|
|
7512
|
+
fail(message);
|
|
7513
|
+
}
|
|
7514
|
+
process.exitCode = 1;
|
|
7515
|
+
}
|
|
7516
|
+
});
|
|
7517
|
+
}
|
|
7518
|
+
|
|
7519
|
+
// src/commands/retry.ts
|
|
7520
|
+
import { join as join22 } from "path";
|
|
7521
|
+
|
|
7522
|
+
// src/lib/retry-state.ts
|
|
7523
|
+
import { existsSync as existsSync22, readFileSync as readFileSync19, writeFileSync as writeFileSync12 } from "fs";
|
|
7524
|
+
import { join as join21 } from "path";
|
|
7525
|
+
var RETRIES_FILE = ".story_retries";
|
|
7526
|
+
var FLAGGED_FILE = ".flagged_stories";
|
|
7527
|
+
var LINE_PATTERN = /^([^=]+)=(\d+)$/;
|
|
7528
|
+
function retriesPath(dir) {
|
|
7529
|
+
return join21(dir, RETRIES_FILE);
|
|
7530
|
+
}
|
|
7531
|
+
function flaggedPath(dir) {
|
|
7532
|
+
return join21(dir, FLAGGED_FILE);
|
|
7533
|
+
}
|
|
7534
|
+
function readRetries(dir) {
|
|
7535
|
+
const filePath = retriesPath(dir);
|
|
7536
|
+
if (!existsSync22(filePath)) {
|
|
7537
|
+
return /* @__PURE__ */ new Map();
|
|
7538
|
+
}
|
|
7539
|
+
const raw = readFileSync19(filePath, "utf-8");
|
|
7540
|
+
const result = /* @__PURE__ */ new Map();
|
|
7541
|
+
for (const line of raw.split("\n")) {
|
|
7542
|
+
const trimmed = line.trim();
|
|
7543
|
+
if (trimmed === "") continue;
|
|
7544
|
+
const match = LINE_PATTERN.exec(trimmed);
|
|
7545
|
+
if (!match) {
|
|
7546
|
+
warn(`Ignoring malformed retry line: ${trimmed}`);
|
|
7547
|
+
continue;
|
|
7548
|
+
}
|
|
7549
|
+
const key = match[1];
|
|
7550
|
+
const count = parseInt(match[2], 10);
|
|
7551
|
+
result.set(key, count);
|
|
7552
|
+
}
|
|
7553
|
+
return result;
|
|
7554
|
+
}
|
|
7555
|
+
function writeRetries(dir, retries) {
|
|
7556
|
+
const filePath = retriesPath(dir);
|
|
7557
|
+
const lines = [];
|
|
7558
|
+
for (const [key, count] of retries) {
|
|
7559
|
+
lines.push(`${key}=${count}`);
|
|
7560
|
+
}
|
|
7561
|
+
writeFileSync12(filePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
|
|
7562
|
+
}
|
|
7563
|
+
function resetRetry(dir, storyKey) {
|
|
7564
|
+
if (storyKey) {
|
|
7565
|
+
const retries = readRetries(dir);
|
|
7566
|
+
retries.delete(storyKey);
|
|
7567
|
+
writeRetries(dir, retries);
|
|
7568
|
+
removeFlaggedStory(dir, storyKey);
|
|
7569
|
+
} else {
|
|
7570
|
+
writeRetries(dir, /* @__PURE__ */ new Map());
|
|
7571
|
+
writeFlaggedStories(dir, []);
|
|
7572
|
+
}
|
|
7573
|
+
}
|
|
7574
|
+
function readFlaggedStories(dir) {
|
|
7575
|
+
const filePath = flaggedPath(dir);
|
|
7576
|
+
if (!existsSync22(filePath)) {
|
|
7577
|
+
return [];
|
|
7578
|
+
}
|
|
7579
|
+
const raw = readFileSync19(filePath, "utf-8");
|
|
7580
|
+
return raw.split("\n").map((l) => l.trim()).filter((l) => l !== "");
|
|
7581
|
+
}
|
|
7582
|
+
function writeFlaggedStories(dir, stories) {
|
|
7583
|
+
const filePath = flaggedPath(dir);
|
|
7584
|
+
writeFileSync12(filePath, stories.length > 0 ? stories.join("\n") + "\n" : "", "utf-8");
|
|
7585
|
+
}
|
|
7586
|
+
function removeFlaggedStory(dir, key) {
|
|
7587
|
+
const stories = readFlaggedStories(dir);
|
|
7588
|
+
const filtered = stories.filter((s) => s !== key);
|
|
7589
|
+
writeFlaggedStories(dir, filtered);
|
|
7590
|
+
}
|
|
7591
|
+
|
|
7592
|
+
// src/commands/retry.ts
|
|
7593
|
+
var RALPH_SUBDIR = "ralph";
|
|
7594
|
+
function isValidStoryKey3(key) {
|
|
7595
|
+
if (!key || key.includes("..") || key.includes("/") || key.includes("\\")) {
|
|
7596
|
+
return false;
|
|
7597
|
+
}
|
|
7598
|
+
return /^[a-zA-Z0-9_-]+$/.test(key);
|
|
7599
|
+
}
|
|
7600
|
+
function registerRetryCommand(program) {
|
|
7601
|
+
program.command("retry").description("Manage retry state for stories").option("--reset", "Clear retry counters and flagged stories").option("--story <key>", "Target a specific story key (used with --reset or --status)").option("--status", "Show retry status for all stories").action((_options, cmd) => {
|
|
7602
|
+
const opts = cmd.optsWithGlobals();
|
|
7603
|
+
const isJson = opts.json === true;
|
|
7604
|
+
const dir = join22(process.cwd(), RALPH_SUBDIR);
|
|
7605
|
+
if (opts.story && !isValidStoryKey3(opts.story)) {
|
|
7606
|
+
if (isJson) {
|
|
7607
|
+
jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
|
|
7608
|
+
} else {
|
|
7609
|
+
warn(`Invalid story key: ${opts.story}`);
|
|
7610
|
+
}
|
|
7611
|
+
process.exitCode = 1;
|
|
7612
|
+
return;
|
|
7613
|
+
}
|
|
7614
|
+
if (opts.reset) {
|
|
7615
|
+
handleReset(dir, opts.story, isJson);
|
|
7616
|
+
return;
|
|
7617
|
+
}
|
|
7618
|
+
if (opts.story && !opts.status) {
|
|
7619
|
+
warn("--story without --reset or --status; showing status for that story");
|
|
7620
|
+
}
|
|
7621
|
+
handleStatus(dir, isJson, opts.story);
|
|
7622
|
+
});
|
|
7623
|
+
}
|
|
7624
|
+
function handleReset(dir, storyKey, isJson) {
|
|
7625
|
+
if (storyKey) {
|
|
7626
|
+
resetRetry(dir, storyKey);
|
|
7627
|
+
if (isJson) {
|
|
7628
|
+
jsonOutput({ status: "ok", action: "reset", story: storyKey });
|
|
7629
|
+
} else {
|
|
7630
|
+
ok(`Retry counter and flagged status cleared for ${storyKey}`);
|
|
7631
|
+
}
|
|
7632
|
+
} else {
|
|
7633
|
+
resetRetry(dir);
|
|
7634
|
+
if (isJson) {
|
|
7635
|
+
jsonOutput({ status: "ok", action: "reset_all" });
|
|
7636
|
+
} else {
|
|
7637
|
+
ok("All retry counters and flagged stories cleared");
|
|
7638
|
+
}
|
|
7639
|
+
}
|
|
7640
|
+
}
|
|
7641
|
+
function handleStatus(dir, isJson, filterStory) {
|
|
7642
|
+
const retries = readRetries(dir);
|
|
7643
|
+
const flagged = new Set(readFlaggedStories(dir));
|
|
7644
|
+
if (isJson) {
|
|
7645
|
+
const entries = {};
|
|
7646
|
+
for (const [key, count] of retries) {
|
|
7647
|
+
if (filterStory && key !== filterStory) continue;
|
|
7648
|
+
entries[key] = { count, flagged: flagged.has(key) };
|
|
7649
|
+
}
|
|
7650
|
+
for (const key of flagged) {
|
|
7651
|
+
if (filterStory && key !== filterStory) continue;
|
|
7652
|
+
if (!entries[key]) {
|
|
7653
|
+
entries[key] = { count: 0, flagged: true };
|
|
7654
|
+
}
|
|
7655
|
+
}
|
|
7656
|
+
jsonOutput({ status: "ok", entries });
|
|
7657
|
+
return;
|
|
7658
|
+
}
|
|
7659
|
+
const allKeys = /* @__PURE__ */ new Set([...retries.keys(), ...flagged]);
|
|
7660
|
+
const displayKeys = filterStory ? [...allKeys].filter((k) => k === filterStory) : [...allKeys];
|
|
7661
|
+
if (displayKeys.length === 0) {
|
|
7662
|
+
console.log("No retry entries.");
|
|
7663
|
+
return;
|
|
7664
|
+
}
|
|
7665
|
+
console.log("Story Retries Flagged");
|
|
7666
|
+
console.log("\u2500".repeat(55));
|
|
7667
|
+
for (const key of displayKeys) {
|
|
7668
|
+
const count = retries.get(key) ?? 0;
|
|
7669
|
+
const isFlagged = flagged.has(key);
|
|
7670
|
+
const paddedKey = key.padEnd(38);
|
|
7671
|
+
const paddedCount = String(count).padStart(4);
|
|
7672
|
+
const flagStr = isFlagged ? " yes" : " no";
|
|
7673
|
+
console.log(`${paddedKey} ${paddedCount}${flagStr}`);
|
|
7674
|
+
}
|
|
7675
|
+
}
|
|
7676
|
+
|
|
6856
7677
|
// src/index.ts
|
|
6857
|
-
var VERSION = true ? "0.
|
|
7678
|
+
var VERSION = true ? "0.16.0" : "0.0.0-dev";
|
|
6858
7679
|
function createProgram() {
|
|
6859
7680
|
const program = new Command();
|
|
6860
7681
|
program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
|
|
@@ -6873,6 +7694,8 @@ function createProgram() {
|
|
|
6873
7694
|
registerQueryCommand(program);
|
|
6874
7695
|
registerRetroImportCommand(program);
|
|
6875
7696
|
registerGithubImportCommand(program);
|
|
7697
|
+
registerVerifyEnvCommand(program);
|
|
7698
|
+
registerRetryCommand(program);
|
|
6876
7699
|
return program;
|
|
6877
7700
|
}
|
|
6878
7701
|
if (!process.env["VITEST"]) {
|