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-7ZD2ZNDU.js";
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.13.2" : "0.0.0-dev";
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 readFileSync7, writeFileSync as writeFileSync6 } from "fs";
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 readFileSync6, writeFileSync as writeFileSync5 } from "fs";
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 = readFileSync6(filePath, "utf-8");
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 = readFileSync6(filePath, "utf-8");
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 = readFileSync6(filePath, "utf-8");
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 = readFileSync6(filePath, "utf-8");
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 = readFileSync6(filePath, "utf-8");
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 = readFileSync7(flaggedFilePath, "utf-8");
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(readFileSync7(statusFile, "utf-8"));
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 readFileSync11 } from "fs";
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 readFileSync8 } from "fs";
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 = readFileSync8(storyFilePath, "utf-8");
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 readFileSync10, writeFileSync as writeFileSync8 } from "fs";
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 readFileSync9,
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 = readFileSync9(agentsPath, "utf-8");
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 basename3 = fullMatch.split("/").pop();
2861
- if (!isTestFile(basename3)) {
2862
- mentioned.add(basename3);
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 = readFileSync9(docPath, "utf-8");
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 = readFileSync9(indexPath, "utf-8");
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 = readFileSync9(filePath, "utf-8");
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 = readFileSync9(activePath, "utf-8");
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 { verified: 0, pending: 0, escalated: 0, total: 0, passed: false };
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, passed: false };
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
- passed: bPending === 0 && bVerified > 0
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
- passed: pending === 0 && verified > 0
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 = readFileSync11(filePath, "utf-8");
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 readFileSync12 } from "fs";
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(readFileSync12(pkgPath, "utf-8"));
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 = readFileSync12(reqPath, "utf-8");
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 = readFileSync12(pyprojectPath, "utf-8");
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(readFileSync12(reportPath, "utf-8"));
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(readFileSync12(reportPath, "utf-8"));
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(readFileSync12(reportPath, "utf-8"));
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 readFileSync13,
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(readFileSync13(reportPath, "utf-8"));
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(readFileSync13(reportPath, "utf-8"));
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 readFileSync14, writeFileSync as writeFileSync10 } from "fs";
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 = readFileSync14(cachePath, "utf-8");
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
- fail("Harness not initialized \u2014 run codeharness init first");
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
- fail("Harness not initialized \u2014 run codeharness init first");
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
- fail("Harness not initialized \u2014 run codeharness init first");
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
- fail("Harness not initialized \u2014 run codeharness init first");
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
- fail("Harness not initialized \u2014 run codeharness init first");
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 readFileSync15, writeFileSync as writeFileSync11, rmSync } from "fs";
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-CT57JGM7.js");
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-CT57JGM7.js");
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 = readFileSync15(pkgPath, "utf-8");
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 readFileSync16 } from "fs";
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 = readFileSync16(retroPath, "utf-8");
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.13.2" : "0.0.0-dev";
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"]) {