codeharness 0.13.2 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -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.1" : "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;
|
|
@@ -3347,7 +3530,7 @@ function validateProofQuality(proofPath) {
|
|
|
3347
3530
|
const bulletMatches = [...content.matchAll(bulletAcPattern)];
|
|
3348
3531
|
const bulletAcNumbers = new Set(bulletMatches.map((m) => m[1]));
|
|
3349
3532
|
if (bulletAcNumbers.size === 0) {
|
|
3350
|
-
return { verified: 0, pending: 0, escalated: 0, total: 0
|
|
3533
|
+
return buildResult({ verified: 0, pending: 0, escalated: 0, total: 0 });
|
|
3351
3534
|
}
|
|
3352
3535
|
let bVerified = 0;
|
|
3353
3536
|
let bPending = 0;
|
|
@@ -3372,13 +3555,12 @@ function validateProofQuality(proofPath) {
|
|
|
3372
3555
|
bVerified = 0;
|
|
3373
3556
|
}
|
|
3374
3557
|
const bTotal = bVerified + bPending + bEscalated;
|
|
3375
|
-
return {
|
|
3558
|
+
return buildResult({
|
|
3376
3559
|
verified: bVerified,
|
|
3377
3560
|
pending: bPending,
|
|
3378
3561
|
escalated: bEscalated,
|
|
3379
|
-
total: bTotal
|
|
3380
|
-
|
|
3381
|
-
};
|
|
3562
|
+
total: bTotal
|
|
3563
|
+
});
|
|
3382
3564
|
}
|
|
3383
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);
|
|
3384
3566
|
for (let i = 0; i < sortedAcs.length; i++) {
|
|
@@ -3395,13 +3577,12 @@ function validateProofQuality(proofPath) {
|
|
|
3395
3577
|
}
|
|
3396
3578
|
}
|
|
3397
3579
|
const narrativeTotal = verified + pending + escalated;
|
|
3398
|
-
return {
|
|
3580
|
+
return buildResult({
|
|
3399
3581
|
verified,
|
|
3400
3582
|
pending,
|
|
3401
3583
|
escalated,
|
|
3402
|
-
total: narrativeTotal
|
|
3403
|
-
|
|
3404
|
-
};
|
|
3584
|
+
total: narrativeTotal
|
|
3585
|
+
});
|
|
3405
3586
|
}
|
|
3406
3587
|
for (const acNum of acNumbers) {
|
|
3407
3588
|
const acPattern = new RegExp(`--- AC ?${acNum}:`, "g");
|
|
@@ -3424,15 +3605,7 @@ function validateProofQuality(proofPath) {
|
|
|
3424
3605
|
}
|
|
3425
3606
|
}
|
|
3426
3607
|
const total = verified + pending + escalated;
|
|
3427
|
-
return {
|
|
3428
|
-
verified,
|
|
3429
|
-
pending,
|
|
3430
|
-
escalated,
|
|
3431
|
-
total,
|
|
3432
|
-
// Proof passes when no pending ACs remain and at least one is verified.
|
|
3433
|
-
// Escalated ACs are allowed — they are explicitly acknowledged as unverifiable.
|
|
3434
|
-
passed: pending === 0 && verified > 0
|
|
3435
|
-
};
|
|
3608
|
+
return buildResult({ verified, pending, escalated, total });
|
|
3436
3609
|
}
|
|
3437
3610
|
function updateVerificationState(storyId, result, dir) {
|
|
3438
3611
|
const { state, body } = readStateWithBody(dir);
|
|
@@ -3532,6 +3705,16 @@ function verifyStory(storyId, isJson, root) {
|
|
|
3532
3705
|
process.exitCode = 1;
|
|
3533
3706
|
return;
|
|
3534
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
|
+
}
|
|
3535
3718
|
const storyFilePath = join11(root, STORY_DIR, `${storyId}.md`);
|
|
3536
3719
|
if (!existsSync13(storyFilePath)) {
|
|
3537
3720
|
fail(`Story file not found: ${storyFilePath}`, { json: isJson });
|
|
@@ -3668,7 +3851,7 @@ function verifyStory(storyId, isJson, root) {
|
|
|
3668
3851
|
}
|
|
3669
3852
|
function extractStoryTitle(filePath) {
|
|
3670
3853
|
try {
|
|
3671
|
-
const content =
|
|
3854
|
+
const content = readFileSync12(filePath, "utf-8");
|
|
3672
3855
|
const match = /^#\s+(.+)$/m.exec(content);
|
|
3673
3856
|
return match ? match[1] : "Unknown Story";
|
|
3674
3857
|
} catch {
|
|
@@ -3683,7 +3866,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
3683
3866
|
|
|
3684
3867
|
// src/lib/coverage.ts
|
|
3685
3868
|
import { execSync as execSync2 } from "child_process";
|
|
3686
|
-
import { existsSync as existsSync14, readFileSync as
|
|
3869
|
+
import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
|
|
3687
3870
|
import { join as join12 } from "path";
|
|
3688
3871
|
function detectCoverageTool(dir) {
|
|
3689
3872
|
const baseDir = dir ?? process.cwd();
|
|
@@ -3716,7 +3899,7 @@ function detectNodeCoverageTool(dir, stateHint) {
|
|
|
3716
3899
|
let pkgScripts = {};
|
|
3717
3900
|
if (existsSync14(pkgPath)) {
|
|
3718
3901
|
try {
|
|
3719
|
-
const pkg = JSON.parse(
|
|
3902
|
+
const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
|
|
3720
3903
|
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
3721
3904
|
hasVitestCoverageV8 = "@vitest/coverage-v8" in allDeps;
|
|
3722
3905
|
hasVitestCoverageIstanbul = "@vitest/coverage-istanbul" in allDeps;
|
|
@@ -3772,7 +3955,7 @@ function detectPythonCoverageTool(dir) {
|
|
|
3772
3955
|
const reqPath = join12(dir, "requirements.txt");
|
|
3773
3956
|
if (existsSync14(reqPath)) {
|
|
3774
3957
|
try {
|
|
3775
|
-
const content =
|
|
3958
|
+
const content = readFileSync13(reqPath, "utf-8");
|
|
3776
3959
|
if (content.includes("pytest-cov") || content.includes("coverage")) {
|
|
3777
3960
|
return {
|
|
3778
3961
|
tool: "coverage.py",
|
|
@@ -3786,7 +3969,7 @@ function detectPythonCoverageTool(dir) {
|
|
|
3786
3969
|
const pyprojectPath = join12(dir, "pyproject.toml");
|
|
3787
3970
|
if (existsSync14(pyprojectPath)) {
|
|
3788
3971
|
try {
|
|
3789
|
-
const content =
|
|
3972
|
+
const content = readFileSync13(pyprojectPath, "utf-8");
|
|
3790
3973
|
if (content.includes("pytest-cov") || content.includes("coverage")) {
|
|
3791
3974
|
return {
|
|
3792
3975
|
tool: "coverage.py",
|
|
@@ -3868,7 +4051,7 @@ function parseVitestCoverage(dir) {
|
|
|
3868
4051
|
return 0;
|
|
3869
4052
|
}
|
|
3870
4053
|
try {
|
|
3871
|
-
const report = JSON.parse(
|
|
4054
|
+
const report = JSON.parse(readFileSync13(reportPath, "utf-8"));
|
|
3872
4055
|
return report.total?.statements?.pct ?? 0;
|
|
3873
4056
|
} catch {
|
|
3874
4057
|
warn("Failed to parse coverage report");
|
|
@@ -3882,7 +4065,7 @@ function parsePythonCoverage(dir) {
|
|
|
3882
4065
|
return 0;
|
|
3883
4066
|
}
|
|
3884
4067
|
try {
|
|
3885
|
-
const report = JSON.parse(
|
|
4068
|
+
const report = JSON.parse(readFileSync13(reportPath, "utf-8"));
|
|
3886
4069
|
return report.totals?.percent_covered ?? 0;
|
|
3887
4070
|
} catch {
|
|
3888
4071
|
warn("Failed to parse coverage report");
|
|
@@ -3983,7 +4166,7 @@ function checkPerFileCoverage(floor, dir) {
|
|
|
3983
4166
|
}
|
|
3984
4167
|
let report;
|
|
3985
4168
|
try {
|
|
3986
|
-
report = JSON.parse(
|
|
4169
|
+
report = JSON.parse(readFileSync13(reportPath, "utf-8"));
|
|
3987
4170
|
} catch {
|
|
3988
4171
|
warn("Failed to parse coverage-summary.json");
|
|
3989
4172
|
return { floor, violations: [], totalFiles: 0 };
|
|
@@ -4683,7 +4866,7 @@ import { join as join17 } from "path";
|
|
|
4683
4866
|
import {
|
|
4684
4867
|
existsSync as existsSync16,
|
|
4685
4868
|
readdirSync as readdirSync3,
|
|
4686
|
-
readFileSync as
|
|
4869
|
+
readFileSync as readFileSync14,
|
|
4687
4870
|
statSync as statSync2
|
|
4688
4871
|
} from "fs";
|
|
4689
4872
|
import { join as join14, relative as relative2 } from "path";
|
|
@@ -4857,7 +5040,7 @@ function readVitestPerFileCoverage(dir) {
|
|
|
4857
5040
|
const reportPath = join14(dir, "coverage", "coverage-summary.json");
|
|
4858
5041
|
if (!existsSync16(reportPath)) return null;
|
|
4859
5042
|
try {
|
|
4860
|
-
const report = JSON.parse(
|
|
5043
|
+
const report = JSON.parse(readFileSync14(reportPath, "utf-8"));
|
|
4861
5044
|
const result = /* @__PURE__ */ new Map();
|
|
4862
5045
|
for (const [key, value] of Object.entries(report)) {
|
|
4863
5046
|
if (key === "total") continue;
|
|
@@ -4872,7 +5055,7 @@ function readPythonPerFileCoverage(dir) {
|
|
|
4872
5055
|
const reportPath = join14(dir, "coverage.json");
|
|
4873
5056
|
if (!existsSync16(reportPath)) return null;
|
|
4874
5057
|
try {
|
|
4875
|
-
const report = JSON.parse(
|
|
5058
|
+
const report = JSON.parse(readFileSync14(reportPath, "utf-8"));
|
|
4876
5059
|
if (!report.files) return null;
|
|
4877
5060
|
const result = /* @__PURE__ */ new Map();
|
|
4878
5061
|
for (const [key, value] of Object.entries(report.files)) {
|
|
@@ -4949,6 +5132,18 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
|
|
|
4949
5132
|
const root = rootDir ?? process.cwd();
|
|
4950
5133
|
const stories = [];
|
|
4951
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
|
+
}
|
|
4952
5147
|
const archDoc = audit.documents.find((d) => d.name === "ARCHITECTURE.md");
|
|
4953
5148
|
if (archDoc && archDoc.grade === "missing") {
|
|
4954
5149
|
stories.push({
|
|
@@ -5122,6 +5317,7 @@ function importOnboardingEpic(epicPath, beadsFns) {
|
|
|
5122
5317
|
function getPriorityFromTitle(title) {
|
|
5123
5318
|
if (title.startsWith("Add test coverage for ")) return PRIORITY_BY_TYPE.coverage;
|
|
5124
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"];
|
|
5125
5321
|
if (title === "Create ARCHITECTURE.md") return PRIORITY_BY_TYPE.architecture;
|
|
5126
5322
|
if (title === "Update stale documentation") return PRIORITY_BY_TYPE["doc-freshness"];
|
|
5127
5323
|
if (title.startsWith("Create verification proof for ")) return PRIORITY_BY_TYPE.verification;
|
|
@@ -5137,6 +5333,9 @@ function getGapIdFromTitle(title) {
|
|
|
5137
5333
|
const mod = title.slice("Create ".length);
|
|
5138
5334
|
return `[gap:docs:${mod}]`;
|
|
5139
5335
|
}
|
|
5336
|
+
if (title === "Create README.md") {
|
|
5337
|
+
return "[gap:docs:README.md]";
|
|
5338
|
+
}
|
|
5140
5339
|
if (title === "Create ARCHITECTURE.md") {
|
|
5141
5340
|
return "[gap:docs:ARCHITECTURE.md]";
|
|
5142
5341
|
}
|
|
@@ -5157,7 +5356,7 @@ function getGapIdFromTitle(title) {
|
|
|
5157
5356
|
}
|
|
5158
5357
|
|
|
5159
5358
|
// src/lib/scan-cache.ts
|
|
5160
|
-
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";
|
|
5161
5360
|
import { join as join16 } from "path";
|
|
5162
5361
|
var CACHE_DIR = ".harness";
|
|
5163
5362
|
var CACHE_FILE = "last-onboard-scan.json";
|
|
@@ -5179,7 +5378,7 @@ function loadScanCache(dir) {
|
|
|
5179
5378
|
return null;
|
|
5180
5379
|
}
|
|
5181
5380
|
try {
|
|
5182
|
-
const raw =
|
|
5381
|
+
const raw = readFileSync15(cachePath, "utf-8");
|
|
5183
5382
|
return JSON.parse(raw);
|
|
5184
5383
|
} catch {
|
|
5185
5384
|
return null;
|
|
@@ -5222,7 +5421,11 @@ function registerOnboardCommand(program) {
|
|
|
5222
5421
|
const minModuleSize = parseInt(opts.minModuleSize ?? "3", 10);
|
|
5223
5422
|
const preconditions = runPreconditions();
|
|
5224
5423
|
if (!preconditions.canProceed) {
|
|
5225
|
-
|
|
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
|
+
}
|
|
5226
5429
|
process.exitCode = 1;
|
|
5227
5430
|
return;
|
|
5228
5431
|
}
|
|
@@ -5251,7 +5454,11 @@ function registerOnboardCommand(program) {
|
|
|
5251
5454
|
const minModuleSize = parseInt(opts.minModuleSize ?? "3", 10);
|
|
5252
5455
|
const preconditions = runPreconditions();
|
|
5253
5456
|
if (!preconditions.canProceed) {
|
|
5254
|
-
|
|
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
|
+
}
|
|
5255
5462
|
process.exitCode = 1;
|
|
5256
5463
|
return;
|
|
5257
5464
|
}
|
|
@@ -5283,7 +5490,11 @@ function registerOnboardCommand(program) {
|
|
|
5283
5490
|
const isJson = opts.json === true;
|
|
5284
5491
|
const preconditions = runPreconditions();
|
|
5285
5492
|
if (!preconditions.canProceed) {
|
|
5286
|
-
|
|
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
|
+
}
|
|
5287
5498
|
process.exitCode = 1;
|
|
5288
5499
|
return;
|
|
5289
5500
|
}
|
|
@@ -5306,7 +5517,11 @@ function registerOnboardCommand(program) {
|
|
|
5306
5517
|
const minModuleSize = parseInt(opts.minModuleSize ?? "3", 10);
|
|
5307
5518
|
const preconditions = runPreconditions();
|
|
5308
5519
|
if (!preconditions.canProceed) {
|
|
5309
|
-
|
|
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
|
+
}
|
|
5310
5525
|
process.exitCode = 1;
|
|
5311
5526
|
return;
|
|
5312
5527
|
}
|
|
@@ -5381,7 +5596,11 @@ function registerOnboardCommand(program) {
|
|
|
5381
5596
|
const minModuleSize = parseInt(opts.minModuleSize ?? "3", 10);
|
|
5382
5597
|
const preconditions = runPreconditions();
|
|
5383
5598
|
if (!preconditions.canProceed) {
|
|
5384
|
-
|
|
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
|
+
}
|
|
5385
5604
|
process.exitCode = 1;
|
|
5386
5605
|
return;
|
|
5387
5606
|
}
|
|
@@ -5513,7 +5732,7 @@ function printEpicOutput(epic) {
|
|
|
5513
5732
|
}
|
|
5514
5733
|
|
|
5515
5734
|
// src/commands/teardown.ts
|
|
5516
|
-
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";
|
|
5517
5736
|
import { join as join18 } from "path";
|
|
5518
5737
|
function buildDefaultResult() {
|
|
5519
5738
|
return {
|
|
@@ -5560,7 +5779,7 @@ function registerTeardownCommand(program) {
|
|
|
5560
5779
|
} else if (otlpMode === "remote-routed") {
|
|
5561
5780
|
if (!options.keepDocker) {
|
|
5562
5781
|
try {
|
|
5563
|
-
const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-
|
|
5782
|
+
const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-6TY2FN43.js");
|
|
5564
5783
|
stopCollectorOnly2();
|
|
5565
5784
|
result.docker.stopped = true;
|
|
5566
5785
|
if (!isJson) {
|
|
@@ -5592,7 +5811,7 @@ function registerTeardownCommand(program) {
|
|
|
5592
5811
|
info("Shared stack: kept running (other projects may use it)");
|
|
5593
5812
|
}
|
|
5594
5813
|
} else if (isLegacyStack) {
|
|
5595
|
-
const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-
|
|
5814
|
+
const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-6TY2FN43.js");
|
|
5596
5815
|
let stackRunning = false;
|
|
5597
5816
|
try {
|
|
5598
5817
|
stackRunning = isStackRunning2(composeFile);
|
|
@@ -5660,7 +5879,7 @@ function registerTeardownCommand(program) {
|
|
|
5660
5879
|
const pkgPath = join18(projectDir, "package.json");
|
|
5661
5880
|
if (existsSync19(pkgPath)) {
|
|
5662
5881
|
try {
|
|
5663
|
-
const raw =
|
|
5882
|
+
const raw = readFileSync16(pkgPath, "utf-8");
|
|
5664
5883
|
const pkg = JSON.parse(raw);
|
|
5665
5884
|
const scripts = pkg["scripts"];
|
|
5666
5885
|
if (scripts) {
|
|
@@ -5734,6 +5953,7 @@ function registerTeardownCommand(program) {
|
|
|
5734
5953
|
import { stringify as stringify3 } from "yaml";
|
|
5735
5954
|
function registerStateCommand(program) {
|
|
5736
5955
|
const stateCmd = program.command("state").description("Manage harness state");
|
|
5956
|
+
stateCmd._hidden = true;
|
|
5737
5957
|
stateCmd.command("show").description("Display full harness state").action((_, cmd) => {
|
|
5738
5958
|
const opts = cmd.optsWithGlobals();
|
|
5739
5959
|
try {
|
|
@@ -6452,7 +6672,7 @@ function registerQueryCommand(program) {
|
|
|
6452
6672
|
}
|
|
6453
6673
|
|
|
6454
6674
|
// src/commands/retro-import.ts
|
|
6455
|
-
import { existsSync as existsSync20, readFileSync as
|
|
6675
|
+
import { existsSync as existsSync20, readFileSync as readFileSync17 } from "fs";
|
|
6456
6676
|
import { join as join19 } from "path";
|
|
6457
6677
|
|
|
6458
6678
|
// src/lib/retro-parser.ts
|
|
@@ -6630,7 +6850,7 @@ function registerRetroImportCommand(program) {
|
|
|
6630
6850
|
}
|
|
6631
6851
|
let content;
|
|
6632
6852
|
try {
|
|
6633
|
-
content =
|
|
6853
|
+
content = readFileSync17(retroPath, "utf-8");
|
|
6634
6854
|
} catch (err) {
|
|
6635
6855
|
const message = err instanceof Error ? err.message : String(err);
|
|
6636
6856
|
fail(`Failed to read retro file: ${message}`, { json: isJson });
|
|
@@ -6896,8 +7116,566 @@ function registerGithubImportCommand(program) {
|
|
|
6896
7116
|
});
|
|
6897
7117
|
}
|
|
6898
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
|
+
|
|
6899
7677
|
// src/index.ts
|
|
6900
|
-
var VERSION = true ? "0.
|
|
7678
|
+
var VERSION = true ? "0.16.1" : "0.0.0-dev";
|
|
6901
7679
|
function createProgram() {
|
|
6902
7680
|
const program = new Command();
|
|
6903
7681
|
program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
|
|
@@ -6916,6 +7694,8 @@ function createProgram() {
|
|
|
6916
7694
|
registerQueryCommand(program);
|
|
6917
7695
|
registerRetroImportCommand(program);
|
|
6918
7696
|
registerGithubImportCommand(program);
|
|
7697
|
+
registerVerifyEnvCommand(program);
|
|
7698
|
+
registerRetryCommand(program);
|
|
6919
7699
|
return program;
|
|
6920
7700
|
}
|
|
6921
7701
|
if (!process.env["VITEST"]) {
|