codeharness 0.26.3 → 0.26.5

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.
@@ -1096,25 +1096,33 @@ RUN cargo build --release
1096
1096
  // ── Task 16: getProjectName ───────────────────────────────────────────
1097
1097
  // ── getVerifyDockerfileSection ──────────────────────────────────────
1098
1098
  getVerifyDockerfileSection(projectDir) {
1099
+ let needsBevy = false;
1100
+ const cargoContent = readTextSafe(join6(projectDir, "Cargo.toml"));
1101
+ if (cargoContent) {
1102
+ const depsSection = getCargoDepsSection(cargoContent);
1103
+ needsBevy = hasCargoDep(depsSection, "bevy");
1104
+ }
1105
+ const aptPackages = ["build-essential", "pkg-config", "libssl-dev"];
1106
+ if (needsBevy) {
1107
+ aptPackages.push(
1108
+ "libudev-dev",
1109
+ "libasound2-dev",
1110
+ "libwayland-dev",
1111
+ "libxkbcommon-dev",
1112
+ "libfontconfig1-dev",
1113
+ "libx11-dev"
1114
+ );
1115
+ }
1099
1116
  const lines = [
1100
1117
  "# --- Rust tooling ---",
1118
+ "RUN apt-get update && apt-get install -y --no-install-recommends \\",
1119
+ ` ${aptPackages.join(" ")} \\`,
1120
+ " && rm -rf /var/lib/apt/lists/*",
1101
1121
  'RUN curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable',
1102
1122
  'ENV PATH="/root/.cargo/bin:$PATH"',
1103
1123
  "RUN rustup component add clippy",
1104
1124
  "RUN cargo install cargo-tarpaulin"
1105
1125
  ];
1106
- const cargoContent = readTextSafe(join6(projectDir, "Cargo.toml"));
1107
- if (cargoContent) {
1108
- const depsSection = getCargoDepsSection(cargoContent);
1109
- if (hasCargoDep(depsSection, "bevy")) {
1110
- lines.push(
1111
- "RUN apt-get update && apt-get install -y --no-install-recommends \\",
1112
- " libudev-dev libasound2-dev libwayland-dev libxkbcommon-dev \\",
1113
- " libfontconfig1-dev libx11-dev \\",
1114
- " && rm -rf /var/lib/apt/lists/*"
1115
- );
1116
- }
1117
- }
1118
1126
  return lines.join("\n");
1119
1127
  }
1120
1128
  getProjectName(dir) {
@@ -1337,9 +1345,8 @@ function parseValue(raw) {
1337
1345
  if (raw === "true") return true;
1338
1346
  if (raw === "false") return false;
1339
1347
  if (raw === "null") return null;
1340
- const num = Number(raw);
1341
- if (!Number.isNaN(num) && raw.trim() !== "") return num;
1342
- return raw;
1348
+ const n = Number(raw);
1349
+ return !Number.isNaN(n) && raw.trim() !== "" ? n : raw;
1343
1350
  }
1344
1351
 
1345
1352
  // src/lib/observability/instrument.ts
@@ -1932,9 +1939,7 @@ function handleLocalShared(opts, state) {
1932
1939
  }
1933
1940
  if (!opts.isJson) {
1934
1941
  fail("Observability stack: failed to start");
1935
- if (startResult.error) {
1936
- info(`Error: ${startResult.error}`);
1937
- }
1942
+ if (startResult.error) info(`Error: ${startResult.error}`);
1938
1943
  }
1939
1944
  const docker = {
1940
1945
  compose_file: sharedComposeFile,
@@ -3207,7 +3212,7 @@ function generateDockerfileTemplate(projectDir, stackOrDetections) {
3207
3212
  }
3208
3213
 
3209
3214
  // src/modules/infra/init-project.ts
3210
- var HARNESS_VERSION = true ? "0.26.3" : "0.0.0-dev";
3215
+ var HARNESS_VERSION = true ? "0.26.5" : "0.0.0-dev";
3211
3216
  function failResult(opts, error) {
3212
3217
  return {
3213
3218
  status: "fail",
@@ -16,7 +16,7 @@ import {
16
16
  stopCollectorOnly,
17
17
  stopSharedStack,
18
18
  stopStack
19
- } from "./chunk-NYZZCLQG.js";
19
+ } from "./chunk-F6L7CXLK.js";
20
20
  export {
21
21
  checkRemoteEndpoint,
22
22
  cleanupOrphanedContainers,
package/dist/index.js CHANGED
@@ -51,7 +51,7 @@ import {
51
51
  validateDockerfile,
52
52
  warn,
53
53
  writeState
54
- } from "./chunk-NYZZCLQG.js";
54
+ } from "./chunk-F6L7CXLK.js";
55
55
 
56
56
  // src/index.ts
57
57
  import { Command } from "commander";
@@ -805,16 +805,6 @@ function defaultState() {
805
805
  actionItems: []
806
806
  };
807
807
  }
808
- function defaultStoryState() {
809
- return {
810
- status: "backlog",
811
- attempts: 0,
812
- lastAttempt: null,
813
- lastError: null,
814
- proofPath: null,
815
- acResults: null
816
- };
817
- }
818
808
  function getStoryStatusesFromState(state) {
819
809
  const result = {};
820
810
  for (const [key, story] of Object.entries(state.stories)) {
@@ -982,7 +972,10 @@ function updateStoryStatus(key, status, detail) {
982
972
  return fail2(stateResult.error);
983
973
  }
984
974
  const current = stateResult.data;
985
- const existingStory = current.stories[key] ?? defaultStoryState();
975
+ const existingStory = current.stories[key];
976
+ if (!existingStory) {
977
+ return fail2(`Story '${key}' does not exist in sprint state \u2014 refusing to create phantom entry`);
978
+ }
986
979
  const isNewAttempt = status === "in-progress";
987
980
  const updatedStory = {
988
981
  ...existingStory,
@@ -1130,17 +1123,30 @@ function reconcileState() {
1130
1123
  }
1131
1124
  const updatedEpics = { ...state.epics };
1132
1125
  for (const [epicKey, storyKeys] of epicStories) {
1126
+ const total = storyKeys.length;
1127
+ const doneCount = storyKeys.filter((k) => state.stories[k].status === "done").length;
1128
+ const failedCount = storyKeys.filter((k) => state.stories[k].status === "failed").length;
1129
+ const computedStatus = doneCount === total ? "done" : doneCount + failedCount === total ? "done" : "in-progress";
1133
1130
  if (!(epicKey in updatedEpics)) {
1134
- const total = storyKeys.length;
1135
- const doneCount = storyKeys.filter((k) => state.stories[k].status === "done").length;
1136
- const epicStatus = doneCount === total ? "done" : "in-progress";
1137
1131
  updatedEpics[epicKey] = {
1138
- status: epicStatus,
1132
+ status: computedStatus,
1139
1133
  storiesTotal: total,
1140
1134
  storiesDone: doneCount
1141
1135
  };
1142
1136
  changed = true;
1143
1137
  corrections.push(`created missing epic entry: ${epicKey}`);
1138
+ } else {
1139
+ const existing = updatedEpics[epicKey];
1140
+ if (existing.storiesTotal !== total || existing.storiesDone !== doneCount || existing.status !== computedStatus) {
1141
+ updatedEpics[epicKey] = {
1142
+ ...existing,
1143
+ status: computedStatus,
1144
+ storiesTotal: total,
1145
+ storiesDone: doneCount
1146
+ };
1147
+ changed = true;
1148
+ corrections.push(`fixed epic ${epicKey}: ${existing.status}\u2192${computedStatus} (${doneCount}/${total} done, ${failedCount} failed)`);
1149
+ }
1144
1150
  }
1145
1151
  }
1146
1152
  state.epics = updatedEpics;
@@ -2317,6 +2323,7 @@ var ERROR_LINE = /\[ERROR\]\s+(.+)/;
2317
2323
  function parseRalphMessage(rawLine) {
2318
2324
  const clean = rawLine.replace(ANSI_ESCAPE, "").replace(TIMESTAMP_PREFIX, "").trim();
2319
2325
  if (clean.length === 0) return null;
2326
+ if (clean.startsWith("{")) return null;
2320
2327
  const success = SUCCESS_STORY.exec(clean);
2321
2328
  if (success) {
2322
2329
  const key = success[1];
@@ -2559,10 +2566,6 @@ function handleAgentEvent(event, rendererHandle, state) {
2559
2566
  rendererHandle.update(event);
2560
2567
  break;
2561
2568
  case "story-complete": {
2562
- const completeResult = updateStoryStatus2(event.key, "review");
2563
- if (!completeResult.success) {
2564
- info(`[WARN] Failed to update status for ${event.key}: ${completeResult.error}`);
2565
- }
2566
2569
  rendererHandle.addMessage({ type: "ok", key: event.key, message: event.details });
2567
2570
  break;
2568
2571
  }
@@ -2799,13 +2802,29 @@ function registerRunCommand(program) {
2799
2802
 
2800
2803
  // src/commands/verify.ts
2801
2804
  import { existsSync as existsSync22, readFileSync as readFileSync19 } from "fs";
2802
- import { join as join20 } from "path";
2805
+ import { join as join21 } from "path";
2803
2806
 
2804
2807
  // src/modules/verify/index.ts
2805
2808
  import { readFileSync as readFileSync18 } from "fs";
2806
2809
 
2807
2810
  // src/modules/verify/proof.ts
2808
2811
  import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
2812
+
2813
+ // src/modules/verify/types.ts
2814
+ var TIER_HIERARCHY = [
2815
+ "test-provable",
2816
+ "runtime-provable",
2817
+ "environment-provable",
2818
+ "escalate"
2819
+ ];
2820
+ var LEGACY_TIER_MAP = {
2821
+ "cli-verifiable": "test-provable",
2822
+ "integration-required": "environment-provable",
2823
+ "unit-testable": "test-provable",
2824
+ "black-box": "environment-provable"
2825
+ };
2826
+
2827
+ // src/modules/verify/proof.ts
2809
2828
  function classifyEvidenceCommands(proofContent) {
2810
2829
  const results = [];
2811
2830
  const codeBlockPattern = /```(?:bash|shell)\n([\s\S]*?)```/g;
@@ -2895,9 +2914,15 @@ function validateProofQuality(proofPath) {
2895
2914
  return emptyResult;
2896
2915
  }
2897
2916
  const content = readFileSync9(proofPath, "utf-8");
2898
- const bbTierMatch = /\*\*Tier:\*\*\s*(unit-testable|black-box)/i.exec(content);
2899
- const bbIsUnitTestable = bbTierMatch ? bbTierMatch[1].toLowerCase() === "unit-testable" : false;
2900
- const bbEnforcement = bbIsUnitTestable ? { blackBoxPass: true, grepSrcCount: 0, dockerExecCount: 0, observabilityCount: 0, otherCount: 0, grepRatio: 0, acsMissingDockerExec: [] } : checkBlackBoxEnforcement(content);
2917
+ const allTierNames = [...TIER_HIERARCHY, ...Object.keys(LEGACY_TIER_MAP)];
2918
+ const uniqueTierNames = [...new Set(allTierNames)];
2919
+ const tierPattern = new RegExp(`\\*\\*Tier:\\*\\*\\s*(${uniqueTierNames.join("|")})`, "i");
2920
+ const bbTierMatch = tierPattern.exec(content);
2921
+ const rawTierValue = bbTierMatch ? bbTierMatch[1].toLowerCase() : null;
2922
+ const normalizedTier = rawTierValue ? LEGACY_TIER_MAP[rawTierValue] ?? (TIER_HIERARCHY.includes(rawTierValue) ? rawTierValue : null) : null;
2923
+ const skipDockerEnforcement = normalizedTier !== null && normalizedTier !== "environment-provable";
2924
+ const bbRawEnforcement = checkBlackBoxEnforcement(content);
2925
+ const bbEnforcement = skipDockerEnforcement ? { ...bbRawEnforcement, blackBoxPass: true } : bbRawEnforcement;
2901
2926
  function buildResult(base) {
2902
2927
  const basePassed = base.pending === 0 && base.verified > 0;
2903
2928
  return {
@@ -3716,6 +3741,8 @@ function closeBeadsIssue(storyId, dir) {
3716
3741
 
3717
3742
  // src/modules/verify/parser.ts
3718
3743
  import { existsSync as existsSync17, readFileSync as readFileSync13 } from "fs";
3744
+
3745
+ // src/modules/verify/parser-keywords.ts
3719
3746
  var UI_KEYWORDS = [
3720
3747
  "agent-browser",
3721
3748
  "screenshot",
@@ -3752,6 +3779,40 @@ var ESCALATE_KEYWORDS = [
3752
3779
  "visual inspection by human",
3753
3780
  "paid external service"
3754
3781
  ];
3782
+ var RUNTIME_PROVABLE_KEYWORDS = [
3783
+ "cli command",
3784
+ "api endpoint",
3785
+ "http",
3786
+ "server",
3787
+ "output shows",
3788
+ "exit code",
3789
+ "binary",
3790
+ "runs and produces",
3791
+ "cli outputs",
3792
+ "when run"
3793
+ ];
3794
+ var ENVIRONMENT_PROVABLE_KEYWORDS = [
3795
+ "docker",
3796
+ "container",
3797
+ "observability",
3798
+ "telemetry",
3799
+ "database",
3800
+ "queue",
3801
+ "distributed",
3802
+ "multi-service",
3803
+ "end-to-end",
3804
+ "victorialogs"
3805
+ ];
3806
+ var ESCALATE_TIER_KEYWORDS = [
3807
+ "physical hardware",
3808
+ "human visual",
3809
+ "paid service",
3810
+ "gpu",
3811
+ "manual inspection",
3812
+ "physical display"
3813
+ ];
3814
+
3815
+ // src/modules/verify/parser.ts
3755
3816
  function classifyVerifiability(description) {
3756
3817
  const lower = description.toLowerCase();
3757
3818
  for (const kw of INTEGRATION_KEYWORDS) {
@@ -3766,10 +3827,27 @@ function classifyStrategy(description) {
3766
3827
  }
3767
3828
  return "docker";
3768
3829
  }
3769
- var VERIFICATION_TAG_PATTERN = /<!--\s*verification:\s*(cli-verifiable|integration-required)\s*-->/;
3830
+ function classifyTier(description) {
3831
+ const lower = description.toLowerCase();
3832
+ for (const kw of ESCALATE_TIER_KEYWORDS) {
3833
+ if (lower.includes(kw)) return "escalate";
3834
+ }
3835
+ for (const kw of ENVIRONMENT_PROVABLE_KEYWORDS) {
3836
+ if (lower.includes(kw)) return "environment-provable";
3837
+ }
3838
+ for (const kw of RUNTIME_PROVABLE_KEYWORDS) {
3839
+ if (lower.includes(kw)) return "runtime-provable";
3840
+ }
3841
+ return "test-provable";
3842
+ }
3843
+ var VERIFICATION_TAG_PATTERN = /<!--\s*verification:\s*(cli-verifiable|integration-required|unit-testable|black-box|test-provable|runtime-provable|environment-provable|escalate)\s*-->/;
3770
3844
  function parseVerificationTag(text) {
3771
3845
  const match = VERIFICATION_TAG_PATTERN.exec(text);
3772
- return match ? match[1] : null;
3846
+ if (!match) return null;
3847
+ const raw = match[1];
3848
+ const mapped = LEGACY_TIER_MAP[raw] ?? raw;
3849
+ if (!TIER_HIERARCHY.includes(mapped)) return null;
3850
+ return mapped;
3773
3851
  }
3774
3852
  function classifyAC(description) {
3775
3853
  const lower = description.toLowerCase();
@@ -3821,14 +3899,16 @@ function parseStoryACs(storyFilePath) {
3821
3899
  const description = currentDesc.join(" ").trim();
3822
3900
  if (description) {
3823
3901
  const tag = parseVerificationTag(description);
3824
- const verifiability = tag ?? classifyVerifiability(description);
3902
+ const tier = tag ?? classifyTier(description);
3903
+ const verifiability = classifyVerifiability(description);
3825
3904
  const strategy = classifyStrategy(description);
3826
3905
  acs.push({
3827
3906
  id: currentId,
3828
3907
  description,
3829
3908
  type: classifyAC(description),
3830
3909
  verifiability,
3831
- strategy
3910
+ strategy,
3911
+ tier
3832
3912
  });
3833
3913
  } else {
3834
3914
  warn(`Skipping malformed AC #${currentId}: empty description`);
@@ -5418,14 +5498,16 @@ function runValidationCycle() {
5418
5498
  // src/modules/verify/env.ts
5419
5499
  import { execFileSync as execFileSync5 } from "child_process";
5420
5500
  import { existsSync as existsSync21, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync17, writeFileSync as writeFileSync12, cpSync, rmSync, statSync as statSync5 } from "fs";
5421
- import { join as join19, basename } from "path";
5501
+ import { join as join20, basename } from "path";
5422
5502
  import { createHash } from "crypto";
5423
5503
 
5424
5504
  // src/modules/verify/dockerfile-generator.ts
5505
+ import { join as join19 } from "path";
5425
5506
  function generateVerifyDockerfile(projectDir) {
5426
5507
  const detections = detectStacks(projectDir);
5427
5508
  const sections = [];
5428
5509
  sections.push("FROM ubuntu:22.04");
5510
+ sections.push("ENV DEBIAN_FRONTEND=noninteractive");
5429
5511
  sections.push("");
5430
5512
  sections.push("# Common tools");
5431
5513
  sections.push(
@@ -5440,7 +5522,8 @@ function generateVerifyDockerfile(projectDir) {
5440
5522
  for (const detection of detections) {
5441
5523
  const provider = getStackProvider(detection.stack);
5442
5524
  if (!provider) continue;
5443
- const section = provider.getVerifyDockerfileSection(projectDir);
5525
+ const resolvedDir = detection.dir === "." ? projectDir : join19(projectDir, detection.dir);
5526
+ const section = provider.getVerifyDockerfileSection(resolvedDir);
5444
5527
  if (section) {
5445
5528
  sections.push(section);
5446
5529
  sections.push("");
@@ -5468,7 +5551,7 @@ function isValidStoryKey(storyKey) {
5468
5551
  return /^[a-zA-Z0-9_-]+$/.test(storyKey);
5469
5552
  }
5470
5553
  function computeDistHash(projectDir) {
5471
- const distDir = join19(projectDir, "dist");
5554
+ const distDir = join20(projectDir, "dist");
5472
5555
  if (!existsSync21(distDir)) return null;
5473
5556
  const hash = createHash("sha256");
5474
5557
  const files = collectFiles(distDir).sort();
@@ -5481,7 +5564,7 @@ function computeDistHash(projectDir) {
5481
5564
  function collectFiles(dir) {
5482
5565
  const results = [];
5483
5566
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
5484
- const fullPath = join19(dir, entry.name);
5567
+ const fullPath = join20(dir, entry.name);
5485
5568
  if (entry.isDirectory()) {
5486
5569
  results.push(...collectFiles(fullPath));
5487
5570
  } else {
@@ -5517,7 +5600,7 @@ function detectProjectType(projectDir) {
5517
5600
  const rootDetection = allStacks.find((s) => s.dir === ".");
5518
5601
  const stack = rootDetection ? rootDetection.stack : null;
5519
5602
  if (stack && STACK_TO_PROJECT_TYPE[stack]) return STACK_TO_PROJECT_TYPE[stack];
5520
- if (existsSync21(join19(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
5603
+ if (existsSync21(join20(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
5521
5604
  return "generic";
5522
5605
  }
5523
5606
  function buildVerifyImage(options = {}) {
@@ -5561,18 +5644,18 @@ function buildNodeImage(projectDir) {
5561
5644
  const lastLine = packOutput.split("\n").pop()?.trim();
5562
5645
  if (!lastLine) throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
5563
5646
  const tarballName = basename(lastLine);
5564
- const tarballPath = join19("/tmp", tarballName);
5565
- const buildContext = join19("/tmp", `codeharness-verify-build-${Date.now()}`);
5647
+ const tarballPath = join20("/tmp", tarballName);
5648
+ const buildContext = join20("/tmp", `codeharness-verify-build-${Date.now()}`);
5566
5649
  mkdirSync6(buildContext, { recursive: true });
5567
5650
  try {
5568
- cpSync(tarballPath, join19(buildContext, tarballName));
5651
+ cpSync(tarballPath, join20(buildContext, tarballName));
5569
5652
  const dockerfile = generateVerifyDockerfile(projectDir) + `
5570
5653
  # Install project from tarball
5571
5654
  ARG TARBALL=package.tgz
5572
5655
  COPY \${TARBALL} /tmp/\${TARBALL}
5573
5656
  RUN npm install -g /tmp/\${TARBALL} && rm /tmp/\${TARBALL}
5574
5657
  `;
5575
- writeFileSync12(join19(buildContext, "Dockerfile"), dockerfile);
5658
+ writeFileSync12(join20(buildContext, "Dockerfile"), dockerfile);
5576
5659
  execFileSync5("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${tarballName}`, "."], {
5577
5660
  cwd: buildContext,
5578
5661
  stdio: "pipe",
@@ -5584,22 +5667,22 @@ RUN npm install -g /tmp/\${TARBALL} && rm /tmp/\${TARBALL}
5584
5667
  }
5585
5668
  }
5586
5669
  function buildPythonImage(projectDir) {
5587
- const distDir = join19(projectDir, "dist");
5670
+ const distDir = join20(projectDir, "dist");
5588
5671
  const distFiles = readdirSync6(distDir).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".whl"));
5589
5672
  if (distFiles.length === 0) {
5590
5673
  throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
5591
5674
  }
5592
5675
  const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
5593
- const buildContext = join19("/tmp", `codeharness-verify-build-${Date.now()}`);
5676
+ const buildContext = join20("/tmp", `codeharness-verify-build-${Date.now()}`);
5594
5677
  mkdirSync6(buildContext, { recursive: true });
5595
5678
  try {
5596
- cpSync(join19(distDir, distFile), join19(buildContext, distFile));
5679
+ cpSync(join20(distDir, distFile), join20(buildContext, distFile));
5597
5680
  const dockerfile = generateVerifyDockerfile(projectDir) + `
5598
5681
  # Install project from distribution
5599
5682
  COPY ${distFile} /tmp/${distFile}
5600
5683
  RUN pip install --break-system-packages /tmp/${distFile} && rm /tmp/${distFile}
5601
5684
  `;
5602
- writeFileSync12(join19(buildContext, "Dockerfile"), dockerfile);
5685
+ writeFileSync12(join20(buildContext, "Dockerfile"), dockerfile);
5603
5686
  execFileSync5("docker", ["build", "-t", IMAGE_TAG, "."], {
5604
5687
  cwd: buildContext,
5605
5688
  stdio: "pipe",
@@ -5614,19 +5697,19 @@ function prepareVerifyWorkspace(storyKey, projectDir) {
5614
5697
  if (!isValidStoryKey(storyKey)) {
5615
5698
  throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
5616
5699
  }
5617
- const storyFile = join19(root, STORY_DIR, `${storyKey}.md`);
5700
+ const storyFile = join20(root, STORY_DIR, `${storyKey}.md`);
5618
5701
  if (!existsSync21(storyFile)) throw new Error(`Story file not found: ${storyFile}`);
5619
5702
  const workspace = `${TEMP_PREFIX}${storyKey}`;
5620
5703
  if (existsSync21(workspace)) rmSync(workspace, { recursive: true, force: true });
5621
5704
  mkdirSync6(workspace, { recursive: true });
5622
- cpSync(storyFile, join19(workspace, "story.md"));
5623
- const readmePath = join19(root, "README.md");
5624
- if (existsSync21(readmePath)) cpSync(readmePath, join19(workspace, "README.md"));
5625
- const docsDir = join19(root, "docs");
5705
+ cpSync(storyFile, join20(workspace, "story.md"));
5706
+ const readmePath = join20(root, "README.md");
5707
+ if (existsSync21(readmePath)) cpSync(readmePath, join20(workspace, "README.md"));
5708
+ const docsDir = join20(root, "docs");
5626
5709
  if (existsSync21(docsDir) && statSync5(docsDir).isDirectory()) {
5627
- cpSync(docsDir, join19(workspace, "docs"), { recursive: true });
5710
+ cpSync(docsDir, join20(workspace, "docs"), { recursive: true });
5628
5711
  }
5629
- mkdirSync6(join19(workspace, "verification"), { recursive: true });
5712
+ mkdirSync6(join20(workspace, "verification"), { recursive: true });
5630
5713
  return workspace;
5631
5714
  }
5632
5715
  function checkVerifyEnv() {
@@ -5679,18 +5762,18 @@ function cleanupVerifyEnv(storyKey) {
5679
5762
  }
5680
5763
  }
5681
5764
  function buildPluginImage(projectDir) {
5682
- const buildContext = join19("/tmp", `codeharness-verify-build-${Date.now()}`);
5765
+ const buildContext = join20("/tmp", `codeharness-verify-build-${Date.now()}`);
5683
5766
  mkdirSync6(buildContext, { recursive: true });
5684
5767
  try {
5685
- const pluginDir = join19(projectDir, ".claude-plugin");
5686
- cpSync(pluginDir, join19(buildContext, ".claude-plugin"), { recursive: true });
5768
+ const pluginDir = join20(projectDir, ".claude-plugin");
5769
+ cpSync(pluginDir, join20(buildContext, ".claude-plugin"), { recursive: true });
5687
5770
  for (const dir of ["commands", "hooks", "knowledge", "skills"]) {
5688
- const src = join19(projectDir, dir);
5771
+ const src = join20(projectDir, dir);
5689
5772
  if (existsSync21(src) && statSync5(src).isDirectory()) {
5690
- cpSync(src, join19(buildContext, dir), { recursive: true });
5773
+ cpSync(src, join20(buildContext, dir), { recursive: true });
5691
5774
  }
5692
5775
  }
5693
- writeFileSync12(join19(buildContext, "Dockerfile"), generateVerifyDockerfile(projectDir));
5776
+ writeFileSync12(join20(buildContext, "Dockerfile"), generateVerifyDockerfile(projectDir));
5694
5777
  execFileSync5("docker", ["build", "-t", IMAGE_TAG, "."], {
5695
5778
  cwd: buildContext,
5696
5779
  stdio: "pipe",
@@ -5701,10 +5784,10 @@ function buildPluginImage(projectDir) {
5701
5784
  }
5702
5785
  }
5703
5786
  function buildSimpleImage(projectDir, timeout = 12e4) {
5704
- const buildContext = join19("/tmp", `codeharness-verify-build-${Date.now()}`);
5787
+ const buildContext = join20("/tmp", `codeharness-verify-build-${Date.now()}`);
5705
5788
  mkdirSync6(buildContext, { recursive: true });
5706
5789
  try {
5707
- writeFileSync12(join19(buildContext, "Dockerfile"), generateVerifyDockerfile(projectDir));
5790
+ writeFileSync12(join20(buildContext, "Dockerfile"), generateVerifyDockerfile(projectDir));
5708
5791
  execFileSync5("docker", ["build", "-t", IMAGE_TAG, "."], {
5709
5792
  cwd: buildContext,
5710
5793
  stdio: "pipe",
@@ -5786,7 +5869,7 @@ function verifyRetro(opts, isJson, root) {
5786
5869
  return;
5787
5870
  }
5788
5871
  const retroFile = `epic-${epicNum}-retrospective.md`;
5789
- const retroPath = join20(root, STORY_DIR2, retroFile);
5872
+ const retroPath = join21(root, STORY_DIR2, retroFile);
5790
5873
  if (!existsSync22(retroPath)) {
5791
5874
  if (isJson) {
5792
5875
  jsonOutput({ status: "fail", epic: epicNum, retroFile, message: `${retroFile} not found` });
@@ -5804,7 +5887,7 @@ function verifyRetro(opts, isJson, root) {
5804
5887
  warn(`Failed to update sprint status: ${message}`);
5805
5888
  }
5806
5889
  if (isJson) {
5807
- jsonOutput({ status: "ok", epic: epicNum, retroFile: join20(STORY_DIR2, retroFile) });
5890
+ jsonOutput({ status: "ok", epic: epicNum, retroFile: join21(STORY_DIR2, retroFile) });
5808
5891
  } else {
5809
5892
  ok(`Epic ${epicNum} retrospective: marked done`);
5810
5893
  }
@@ -5815,7 +5898,7 @@ function verifyStory(storyId, isJson, root) {
5815
5898
  process.exitCode = 1;
5816
5899
  return;
5817
5900
  }
5818
- const readmePath = join20(root, "README.md");
5901
+ const readmePath = join21(root, "README.md");
5819
5902
  if (!existsSync22(readmePath)) {
5820
5903
  if (isJson) {
5821
5904
  jsonOutput({ status: "fail", message: "No README.md found \u2014 verification requires user documentation" });
@@ -5825,7 +5908,7 @@ function verifyStory(storyId, isJson, root) {
5825
5908
  process.exitCode = 1;
5826
5909
  return;
5827
5910
  }
5828
- const storyFilePath = join20(root, STORY_DIR2, `${storyId}.md`);
5911
+ const storyFilePath = join21(root, STORY_DIR2, `${storyId}.md`);
5829
5912
  if (!existsSync22(storyFilePath)) {
5830
5913
  fail(`Story file not found: ${storyFilePath}`, { json: isJson });
5831
5914
  process.exitCode = 1;
@@ -5866,7 +5949,7 @@ function verifyStory(storyId, isJson, root) {
5866
5949
  return;
5867
5950
  }
5868
5951
  const storyTitle = extractStoryTitle(storyFilePath);
5869
- const expectedProofPath = join20(root, "verification", `${storyId}-proof.md`);
5952
+ const expectedProofPath = join21(root, "verification", `${storyId}-proof.md`);
5870
5953
  const proofPath = existsSync22(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
5871
5954
  const proofQuality = validateProofQuality(proofPath);
5872
5955
  if (!proofQuality.passed) {
@@ -5996,8 +6079,15 @@ var ELK_ENDPOINTS = {
5996
6079
  function getDefaultEndpointsForBackend(backend) {
5997
6080
  return backend === "elk" ? ELK_ENDPOINTS : DEFAULT_ENDPOINTS;
5998
6081
  }
5999
- function buildScopedEndpoints(endpoints, serviceName) {
6082
+ function buildScopedEndpoints(endpoints, serviceName, backend) {
6000
6083
  const encoded = encodeURIComponent(serviceName);
6084
+ if (backend === "elk") {
6085
+ return {
6086
+ logs: `${endpoints.logs}/_search?q=${encodeURIComponent(`service_name:${serviceName}`)}&size=100`,
6087
+ metrics: `${endpoints.metrics}/_search?q=${encodeURIComponent(`service_name:${serviceName}`)}&size=100`,
6088
+ traces: `${endpoints.traces}/_search?q=${encodeURIComponent(`trace_id:* AND service_name:${serviceName}`)}&size=20`
6089
+ };
6090
+ }
6001
6091
  return {
6002
6092
  logs: `${endpoints.logs}/select/logsql/query?query=${encodeURIComponent(`service_name:${serviceName}`)}`,
6003
6093
  metrics: `${endpoints.metrics}/api/v1/query?query=${encodeURIComponent(`{service_name="${serviceName}"}`)}`,
@@ -6031,12 +6121,12 @@ function resolveEndpoints(state) {
6031
6121
 
6032
6122
  // src/lib/onboard-checks.ts
6033
6123
  import { existsSync as existsSync26 } from "fs";
6034
- import { join as join23, dirname as dirname5 } from "path";
6124
+ import { join as join24, dirname as dirname5 } from "path";
6035
6125
  import { fileURLToPath } from "url";
6036
6126
 
6037
6127
  // src/lib/coverage/parser.ts
6038
6128
  import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
6039
- import { join as join21 } from "path";
6129
+ import { join as join22 } from "path";
6040
6130
  function parseTestCounts(output) {
6041
6131
  const vitestMatch = /Tests\s+(\d+)\s+passed(?:\s*\|\s*(\d+)\s+failed)?/i.exec(output);
6042
6132
  if (vitestMatch) {
@@ -6100,7 +6190,7 @@ function parseVitestCoverage(dir) {
6100
6190
  }
6101
6191
  }
6102
6192
  function parsePythonCoverage(dir) {
6103
- const reportPath = join21(dir, "coverage.json");
6193
+ const reportPath = join22(dir, "coverage.json");
6104
6194
  if (!existsSync23(reportPath)) {
6105
6195
  warn("Coverage report not found at coverage.json");
6106
6196
  return 0;
@@ -6114,7 +6204,7 @@ function parsePythonCoverage(dir) {
6114
6204
  }
6115
6205
  }
6116
6206
  function parseTarpaulinCoverage(dir) {
6117
- const reportPath = join21(dir, "coverage", "tarpaulin-report.json");
6207
+ const reportPath = join22(dir, "coverage", "tarpaulin-report.json");
6118
6208
  if (!existsSync23(reportPath)) {
6119
6209
  warn("Tarpaulin report not found at coverage/tarpaulin-report.json");
6120
6210
  return 0;
@@ -6129,8 +6219,8 @@ function parseTarpaulinCoverage(dir) {
6129
6219
  }
6130
6220
  function findCoverageSummary(dir) {
6131
6221
  const candidates = [
6132
- join21(dir, "coverage", "coverage-summary.json"),
6133
- join21(dir, "src", "coverage", "coverage-summary.json")
6222
+ join22(dir, "coverage", "coverage-summary.json"),
6223
+ join22(dir, "src", "coverage", "coverage-summary.json")
6134
6224
  ];
6135
6225
  for (const p of candidates) {
6136
6226
  if (existsSync23(p)) return p;
@@ -6141,7 +6231,7 @@ function findCoverageSummary(dir) {
6141
6231
  // src/lib/coverage/runner.ts
6142
6232
  import { execSync as execSync6 } from "child_process";
6143
6233
  import { existsSync as existsSync24, readFileSync as readFileSync21 } from "fs";
6144
- import { join as join22 } from "path";
6234
+ import { join as join23 } from "path";
6145
6235
  function detectCoverageTool(dir) {
6146
6236
  const baseDir = dir ?? process.cwd();
6147
6237
  const stateHint = getStateToolHint(baseDir);
@@ -6174,7 +6264,7 @@ function detectRustCoverageTool(dir) {
6174
6264
  warn("cargo-tarpaulin not installed \u2014 coverage detection unavailable");
6175
6265
  return { tool: "unknown", runCommand: "", reportFormat: "" };
6176
6266
  }
6177
- const cargoPath = join22(dir, "Cargo.toml");
6267
+ const cargoPath = join23(dir, "Cargo.toml");
6178
6268
  let isWorkspace = false;
6179
6269
  try {
6180
6270
  const cargoContent = readFileSync21(cargoPath, "utf-8");
@@ -6197,8 +6287,8 @@ function getStateToolHint(dir) {
6197
6287
  }
6198
6288
  }
6199
6289
  function detectNodeCoverageTool(dir, stateHint) {
6200
- const hasVitestConfig = existsSync24(join22(dir, "vitest.config.ts")) || existsSync24(join22(dir, "vitest.config.js"));
6201
- const pkgPath = join22(dir, "package.json");
6290
+ const hasVitestConfig = existsSync24(join23(dir, "vitest.config.ts")) || existsSync24(join23(dir, "vitest.config.js"));
6291
+ const pkgPath = join23(dir, "package.json");
6202
6292
  let hasVitestCoverageV8 = false;
6203
6293
  let hasVitestCoverageIstanbul = false;
6204
6294
  let hasC8 = false;
@@ -6259,7 +6349,7 @@ function getNodeTestCommand(scripts, runner) {
6259
6349
  return "npm test";
6260
6350
  }
6261
6351
  function detectPythonCoverageTool(dir) {
6262
- const reqPath = join22(dir, "requirements.txt");
6352
+ const reqPath = join23(dir, "requirements.txt");
6263
6353
  if (existsSync24(reqPath)) {
6264
6354
  try {
6265
6355
  const content = readFileSync21(reqPath, "utf-8");
@@ -6273,7 +6363,7 @@ function detectPythonCoverageTool(dir) {
6273
6363
  } catch {
6274
6364
  }
6275
6365
  }
6276
- const pyprojectPath = join22(dir, "pyproject.toml");
6366
+ const pyprojectPath = join23(dir, "pyproject.toml");
6277
6367
  if (existsSync24(pyprojectPath)) {
6278
6368
  try {
6279
6369
  const content = readFileSync21(pyprojectPath, "utf-8");
@@ -6472,7 +6562,7 @@ function checkBmadInstalled(dir) {
6472
6562
  function checkHooksRegistered(dir) {
6473
6563
  const __filename = fileURLToPath(import.meta.url);
6474
6564
  const __dirname = dirname5(__filename);
6475
- const hooksPath = join23(__dirname, "..", "..", "hooks", "hooks.json");
6565
+ const hooksPath = join24(__dirname, "..", "..", "hooks", "hooks.json");
6476
6566
  return { ok: existsSync26(hooksPath) };
6477
6567
  }
6478
6568
  function runPreconditions(dir) {
@@ -6603,7 +6693,7 @@ function handleFullStatus(isJson) {
6603
6693
  const serviceName = state.otlp?.service_name;
6604
6694
  if (serviceName) {
6605
6695
  const endpoints = resolveEndpoints(state);
6606
- const scoped = buildScopedEndpoints(endpoints, serviceName);
6696
+ const scoped = buildScopedEndpoints(endpoints, serviceName, state.otlp?.backend);
6607
6697
  console.log(` Scoped: logs=${scoped.logs} metrics=${scoped.metrics} traces=${scoped.traces}`);
6608
6698
  }
6609
6699
  printBeadsSummary();
@@ -6671,7 +6761,7 @@ function handleFullStatusJson(state) {
6671
6761
  }
6672
6762
  const endpoints = resolveEndpoints(state);
6673
6763
  const serviceName = state.otlp?.service_name;
6674
- const scoped_endpoints = serviceName ? buildScopedEndpoints(endpoints, serviceName) : void 0;
6764
+ const scoped_endpoints = serviceName ? buildScopedEndpoints(endpoints, serviceName, state.otlp?.backend) : void 0;
6675
6765
  const beads = getBeadsData();
6676
6766
  const onboarding = getOnboardingProgressData();
6677
6767
  const sprint = getSprintReportData();
@@ -7112,7 +7202,7 @@ function registerStatusCommand(program) {
7112
7202
 
7113
7203
  // src/modules/audit/dimensions.ts
7114
7204
  import { existsSync as existsSync27, readdirSync as readdirSync7 } from "fs";
7115
- import { join as join24 } from "path";
7205
+ import { join as join25 } from "path";
7116
7206
  function gap(dimension, description, suggestedFix) {
7117
7207
  return { dimension, description, suggestedFix };
7118
7208
  }
@@ -7224,15 +7314,15 @@ function checkDocumentation(projectDir) {
7224
7314
  function checkVerification(projectDir) {
7225
7315
  try {
7226
7316
  const gaps = [];
7227
- const sprintPath = join24(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
7317
+ const sprintPath = join25(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
7228
7318
  if (!existsSync27(sprintPath)) return dimOk("verification", "warn", "no sprint data", [gap("verification", "No sprint-status.yaml found", "Run sprint planning to create sprint status")]);
7229
- const vDir = join24(projectDir, "verification");
7319
+ const vDir = join25(projectDir, "verification");
7230
7320
  let proofCount = 0, totalChecked = 0;
7231
7321
  if (existsSync27(vDir)) {
7232
7322
  for (const file of readdirSafe(vDir)) {
7233
7323
  if (!file.endsWith("-proof.md")) continue;
7234
7324
  totalChecked++;
7235
- const r = parseProof(join24(vDir, file));
7325
+ const r = parseProof(join25(vDir, file));
7236
7326
  if (isOk(r) && r.data.passed) {
7237
7327
  proofCount++;
7238
7328
  } else {
@@ -7310,7 +7400,7 @@ function formatAuditJson(result) {
7310
7400
 
7311
7401
  // src/modules/audit/fix-generator.ts
7312
7402
  import { existsSync as existsSync28, writeFileSync as writeFileSync13, mkdirSync as mkdirSync7 } from "fs";
7313
- import { join as join25, dirname as dirname6 } from "path";
7403
+ import { join as join26, dirname as dirname6 } from "path";
7314
7404
  function buildStoryKey(gap2, index) {
7315
7405
  const safeDimension = gap2.dimension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
7316
7406
  return `audit-fix-${safeDimension}-${index}`;
@@ -7342,7 +7432,7 @@ function generateFixStories(auditResult) {
7342
7432
  const stories = [];
7343
7433
  let created = 0;
7344
7434
  let skipped = 0;
7345
- const artifactsDir = join25(
7435
+ const artifactsDir = join26(
7346
7436
  process.cwd(),
7347
7437
  "_bmad-output",
7348
7438
  "implementation-artifacts"
@@ -7351,7 +7441,7 @@ function generateFixStories(auditResult) {
7351
7441
  for (let i = 0; i < dimension.gaps.length; i++) {
7352
7442
  const gap2 = dimension.gaps[i];
7353
7443
  const key = buildStoryKey(gap2, i + 1);
7354
- const filePath = join25(artifactsDir, `${key}.md`);
7444
+ const filePath = join26(artifactsDir, `${key}.md`);
7355
7445
  if (existsSync28(filePath)) {
7356
7446
  stories.push({
7357
7447
  key,
@@ -7542,7 +7632,7 @@ function registerOnboardCommand(program) {
7542
7632
 
7543
7633
  // src/commands/teardown.ts
7544
7634
  import { existsSync as existsSync29, unlinkSync as unlinkSync3, readFileSync as readFileSync23, writeFileSync as writeFileSync14, rmSync as rmSync2 } from "fs";
7545
- import { join as join26 } from "path";
7635
+ import { join as join27 } from "path";
7546
7636
  function buildDefaultResult() {
7547
7637
  return {
7548
7638
  status: "ok",
@@ -7588,7 +7678,7 @@ function registerTeardownCommand(program) {
7588
7678
  } else if (otlpMode === "remote-routed") {
7589
7679
  if (!options.keepDocker) {
7590
7680
  try {
7591
- const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-GG765ZJT.js");
7681
+ const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-VHOP56YP.js");
7592
7682
  stopCollectorOnly2();
7593
7683
  result.docker.stopped = true;
7594
7684
  if (!isJson) {
@@ -7620,7 +7710,7 @@ function registerTeardownCommand(program) {
7620
7710
  info("Shared stack: kept running (other projects may use it)");
7621
7711
  }
7622
7712
  } else if (isLegacyStack) {
7623
- const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-GG765ZJT.js");
7713
+ const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-VHOP56YP.js");
7624
7714
  let stackRunning = false;
7625
7715
  try {
7626
7716
  stackRunning = isStackRunning2(composeFile);
@@ -7645,7 +7735,7 @@ function registerTeardownCommand(program) {
7645
7735
  info("Docker stack: not running, skipping");
7646
7736
  }
7647
7737
  }
7648
- const composeFilePath = join26(projectDir, composeFile);
7738
+ const composeFilePath = join27(projectDir, composeFile);
7649
7739
  if (existsSync29(composeFilePath)) {
7650
7740
  unlinkSync3(composeFilePath);
7651
7741
  result.removed.push(composeFile);
@@ -7653,7 +7743,7 @@ function registerTeardownCommand(program) {
7653
7743
  ok(`Removed: ${composeFile}`);
7654
7744
  }
7655
7745
  }
7656
- const otelConfigPath = join26(projectDir, "otel-collector-config.yaml");
7746
+ const otelConfigPath = join27(projectDir, "otel-collector-config.yaml");
7657
7747
  if (existsSync29(otelConfigPath)) {
7658
7748
  unlinkSync3(otelConfigPath);
7659
7749
  result.removed.push("otel-collector-config.yaml");
@@ -7664,7 +7754,7 @@ function registerTeardownCommand(program) {
7664
7754
  }
7665
7755
  let patchesRemoved = 0;
7666
7756
  for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
7667
- const filePath = join26(projectDir, "_bmad", relativePath);
7757
+ const filePath = join27(projectDir, "_bmad", relativePath);
7668
7758
  if (!existsSync29(filePath)) {
7669
7759
  continue;
7670
7760
  }
@@ -7686,7 +7776,7 @@ function registerTeardownCommand(program) {
7686
7776
  }
7687
7777
  const stacks = state.stacks ?? (state.stack ? [state.stack] : []);
7688
7778
  if (state.otlp?.enabled && stacks.includes("nodejs")) {
7689
- const pkgPath = join26(projectDir, "package.json");
7779
+ const pkgPath = join27(projectDir, "package.json");
7690
7780
  if (existsSync29(pkgPath)) {
7691
7781
  try {
7692
7782
  const raw = readFileSync23(pkgPath, "utf-8");
@@ -7729,7 +7819,7 @@ function registerTeardownCommand(program) {
7729
7819
  }
7730
7820
  }
7731
7821
  }
7732
- const harnessDir = join26(projectDir, ".harness");
7822
+ const harnessDir = join27(projectDir, ".harness");
7733
7823
  if (existsSync29(harnessDir)) {
7734
7824
  rmSync2(harnessDir, { recursive: true, force: true });
7735
7825
  result.removed.push(".harness/");
@@ -8488,7 +8578,7 @@ function registerQueryCommand(program) {
8488
8578
 
8489
8579
  // src/commands/retro-import.ts
8490
8580
  import { existsSync as existsSync30, readFileSync as readFileSync24 } from "fs";
8491
- import { join as join27 } from "path";
8581
+ import { join as join28 } from "path";
8492
8582
 
8493
8583
  // src/lib/retro-parser.ts
8494
8584
  var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
@@ -8657,7 +8747,7 @@ function registerRetroImportCommand(program) {
8657
8747
  return;
8658
8748
  }
8659
8749
  const retroFile = `epic-${epicNum}-retrospective.md`;
8660
- const retroPath = join27(root, STORY_DIR3, retroFile);
8750
+ const retroPath = join28(root, STORY_DIR3, retroFile);
8661
8751
  if (!existsSync30(retroPath)) {
8662
8752
  fail(`Retro file not found: ${retroFile}`, { json: isJson });
8663
8753
  process.exitCode = 1;
@@ -9046,10 +9136,10 @@ function registerVerifyEnvCommand(program) {
9046
9136
  }
9047
9137
 
9048
9138
  // src/commands/retry.ts
9049
- import { join as join29 } from "path";
9139
+ import { join as join30 } from "path";
9050
9140
 
9051
9141
  // src/lib/retry-state.ts
9052
- import { join as join28 } from "path";
9142
+ import { join as join29 } from "path";
9053
9143
  function mutateState(mutator) {
9054
9144
  const result = getSprintState2();
9055
9145
  if (!result.success) return;
@@ -9110,7 +9200,7 @@ function registerRetryCommand(program) {
9110
9200
  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) => {
9111
9201
  const opts = cmd.optsWithGlobals();
9112
9202
  const isJson = opts.json === true;
9113
- const dir = join29(process.cwd(), RALPH_SUBDIR);
9203
+ const dir = join30(process.cwd(), RALPH_SUBDIR);
9114
9204
  if (opts.story && !isValidStoryKey3(opts.story)) {
9115
9205
  if (isJson) {
9116
9206
  jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
@@ -9520,7 +9610,7 @@ function registerAuditCommand(program) {
9520
9610
 
9521
9611
  // src/commands/stats.ts
9522
9612
  import { existsSync as existsSync31, readdirSync as readdirSync8, readFileSync as readFileSync25, writeFileSync as writeFileSync15 } from "fs";
9523
- import { join as join30 } from "path";
9613
+ import { join as join31 } from "path";
9524
9614
  var RATES = {
9525
9615
  input: 15,
9526
9616
  output: 75,
@@ -9605,8 +9695,8 @@ function parseLogFile(filePath, report) {
9605
9695
  }
9606
9696
  }
9607
9697
  function generateReport3(projectDir) {
9608
- const logsDir = join30(projectDir, "ralph", "logs");
9609
- const logFiles = readdirSync8(logsDir).filter((f) => f.startsWith("claude_output_") && f.endsWith(".log")).sort().map((f) => join30(logsDir, f));
9698
+ const logsDir = join31(projectDir, "ralph", "logs");
9699
+ const logFiles = readdirSync8(logsDir).filter((f) => f.startsWith("claude_output_") && f.endsWith(".log")).sort().map((f) => join31(logsDir, f));
9610
9700
  const report = {
9611
9701
  byPhase: /* @__PURE__ */ new Map(),
9612
9702
  byStory: /* @__PURE__ */ new Map(),
@@ -9705,7 +9795,7 @@ function registerStatsCommand(program) {
9705
9795
  const globalOpts = cmd.optsWithGlobals();
9706
9796
  const isJson = !!globalOpts.json;
9707
9797
  const projectDir = process.cwd();
9708
- const logsDir = join30(projectDir, "ralph", "logs");
9798
+ const logsDir = join31(projectDir, "ralph", "logs");
9709
9799
  if (!existsSync31(logsDir)) {
9710
9800
  fail("No ralph/logs/ directory found \u2014 run codeharness run first");
9711
9801
  process.exitCode = 1;
@@ -9719,7 +9809,7 @@ function registerStatsCommand(program) {
9719
9809
  const formatted = formatReport2(report);
9720
9810
  console.log(formatted);
9721
9811
  if (options.save) {
9722
- const outPath = join30(projectDir, "_bmad-output", "implementation-artifacts", "cost-report.md");
9812
+ const outPath = join31(projectDir, "_bmad-output", "implementation-artifacts", "cost-report.md");
9723
9813
  writeFileSync15(outPath, formatted, "utf-8");
9724
9814
  ok(`Report saved to ${outPath}`);
9725
9815
  }
@@ -9727,7 +9817,7 @@ function registerStatsCommand(program) {
9727
9817
  }
9728
9818
 
9729
9819
  // src/index.ts
9730
- var VERSION = true ? "0.26.3" : "0.0.0-dev";
9820
+ var VERSION = true ? "0.26.5" : "0.0.0-dev";
9731
9821
  function createProgram() {
9732
9822
  const program = new Command();
9733
9823
  program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeharness",
3
- "version": "0.26.3",
3
+ "version": "0.26.5",
4
4
  "type": "module",
5
5
  "description": "CLI for codeharness — makes autonomous coding agents produce software that actually works",
6
6
  "bin": {
package/patches/AGENTS.md CHANGED
@@ -12,7 +12,7 @@ prevent recurrence of observed failures.
12
12
  patches/
13
13
  dev/enforcement.md — Dev agent guardrails
14
14
  review/enforcement.md — Review gates (proof quality, coverage)
15
- verify/story-verification.md — Black-box proof requirements
15
+ verify/story-verification.md — Tier-appropriate proof requirements
16
16
  sprint/planning.md — Sprint planning pre-checks
17
17
  retro/enforcement.md — Retrospective quality metrics
18
18
  ```
@@ -4,7 +4,7 @@ Dev agents repeatedly shipped code without reading module conventions (AGENTS.md
4
4
  skipped observability checks, and produced features that could not be verified
5
5
  from outside the source tree. This patch enforces architecture awareness,
6
6
  observability validation, documentation hygiene, test coverage gates, and
7
- black-box thinking — all operational failures observed in prior sprints.
7
+ verification tier awareness — all operational failures observed in prior sprints.
8
8
  (FR33, FR34, NFR20)
9
9
 
10
10
  ## Codeharness Development Enforcement
@@ -35,14 +35,23 @@ After running tests, verify telemetry is flowing:
35
35
  - Coverage gate: 100% of new/changed code
36
36
  - Run `npm test` / `pytest` and verify no regressions
37
37
 
38
- ### Black-Box Thinking
38
+ ### Verification Tier Awareness
39
39
 
40
- Write code that can be verified from the outside. Ask yourself:
41
- - Can a user exercise this feature from the CLI alone?
42
- - Is the behavior documented in README.md?
43
- - Would a verifier with NO source access be able to tell if this works?
40
+ Write code that can be verified at the appropriate tier. The four verification tiers determine what evidence is needed to prove an AC works:
44
41
 
45
- If the answer is "no", the feature has a testability gap fix the CLI/docs, not the verification process.
42
+ - **`test-provable`** Code must be testable via `npm test` / `npm run build`. Ensure functions have test coverage, outputs are greppable, and build artifacts are inspectable. No running app required.
43
+ - **`runtime-provable`** — Code must be exercisable via CLI or local server. Ensure the binary/CLI produces verifiable stdout, exit codes, or HTTP responses without needing Docker.
44
+ - **`environment-provable`** — Code must work in a Docker verification environment. Ensure the Dockerfile is current, services start correctly, and `docker exec` can exercise the feature. Observability queries should return expected log/trace events.
45
+ - **`escalate`** — Reserved for ACs that genuinely cannot be automated (physical hardware, paid external APIs). This is rare — exhaust all automated approaches first.
46
+
47
+ Ask yourself:
48
+ - What tier is this story tagged with?
49
+ - Does my implementation produce the evidence that tier requires?
50
+ - If `test-provable`: are my functions testable and my outputs greppable?
51
+ - If `runtime-provable`: can I run the CLI/server and verify output locally?
52
+ - If `environment-provable`: does `docker exec` work? Are logs flowing to the observability stack?
53
+
54
+ If the answer is "no", the feature has a testability gap — fix the code to be verifiable at the appropriate tier.
46
55
 
47
56
  ### Dockerfile Maintenance
48
57
 
@@ -11,7 +11,7 @@ quality trends, and mandatory concrete action items with owners.
11
11
 
12
12
  ### Verification Effectiveness
13
13
 
14
- - How many ACs were caught by black-box verification vs slipped through?
14
+ - How many ACs were caught by tier-appropriate verification vs slipped through?
15
15
  - Were there false positives (proof said PASS but feature was broken)?
16
16
  - Were there false negatives (proof said FAIL but feature actually works)?
17
17
  - Time spent on verification — is it proportional to value?
@@ -20,7 +20,7 @@ quality trends, and mandatory concrete action items with owners.
20
20
 
21
21
  - Did the verifier hang on permissions? (check for `--allowedTools` issues)
22
22
  - Did stories get stuck in verify→dev loops? (check `attempts` counter)
23
- - Were stories incorrectly flagged as `integration-required`?
23
+ - Were stories assigned the wrong verification tier?
24
24
  - Did the verify parser correctly detect `[FAIL]` verdicts?
25
25
 
26
26
  ### Documentation Health
@@ -1,9 +1,9 @@
1
1
  ## WHY
2
2
 
3
3
  Review agents approved stories without verifying proof documents existed or
4
- checking that evidence was black-box (not source-grep). Stories passed review
4
+ checking that evidence matched the story's verification tier. Stories passed review
5
5
  with fabricated output and missing coverage data. This patch enforces proof
6
- existence, black-box evidence quality, and coverage delta reporting as hard
6
+ existence, tier-appropriate evidence quality, and coverage delta reporting as hard
7
7
  gates before a story can leave review.
8
8
  (FR33, FR34, NFR20)
9
9
 
@@ -18,13 +18,34 @@ gates before a story can leave review.
18
18
 
19
19
  ### Proof Quality Checks
20
20
 
21
- The proof must pass black-box enforcement:
21
+ The proof must pass tier-appropriate evidence enforcement. The required evidence depends on the story's verification tier:
22
+
23
+ #### `test-provable` stories
24
+ - Evidence comes from build output, test results, and grep/read of code or generated artifacts
25
+ - `npm test` / `npm run build` output is the primary evidence
26
+ - Source-level assertions (grep against `src/`) are acceptable — this IS the verification method for this tier
27
+ - `docker exec` evidence is NOT required
28
+ - Each AC section must show actual test output or build results
29
+
30
+ #### `runtime-provable` stories
31
+ - Evidence comes from running the actual binary, CLI, or server
32
+ - Process execution output (stdout, stderr, exit codes) is the primary evidence
33
+ - HTTP responses from a locally running server are acceptable
34
+ - `docker exec` evidence is NOT required
35
+ - Each AC section must show actual command execution and output
36
+
37
+ #### `environment-provable` stories
22
38
  - Commands run via `docker exec` (not direct host access)
23
39
  - Less than 50% of evidence commands are `grep` against `src/`
24
40
  - Each AC section has at least one `docker exec`, `docker ps/logs`, or observability query
25
41
  - `[FAIL]` verdicts outside code blocks cause the proof to fail
26
42
  - `[ESCALATE]` is acceptable only when all automated approaches are exhausted
27
43
 
44
+ #### `escalate` stories
45
+ - Human judgment is required — automated evidence may be partial or absent
46
+ - Proof document must explain why automation is not possible
47
+ - `[ESCALATE]` verdict is expected and acceptable
48
+
28
49
  ### Observability
29
50
 
30
51
  Run `semgrep scan --config patches/observability/ --config patches/error-handling/ --json` against changed files and report gaps.
@@ -1,35 +1,49 @@
1
1
  ## WHY
2
2
 
3
3
  Stories were marked "done" with no proof artifact, or with proofs that only
4
- grepped source code instead of exercising the feature from the user's
5
- perspective. This patch mandates black-box proof documents, docker exec evidence,
4
+ grepped source code instead of exercising the feature at the appropriate
5
+ verification tier. This patch mandates tier-appropriate proof documents,
6
6
  verification tags per AC, and test coverage targets — preventing regressions
7
- from being hidden behind source-level assertions.
7
+ from being hidden behind inadequate evidence.
8
8
  (FR33, FR36, NFR20)
9
9
 
10
10
  ## Verification Requirements
11
11
 
12
- Every story must produce a **black-box proof** evidence that the feature works from the user's perspective, NOT from reading source code.
12
+ Every story must produce a **proof document** with evidence appropriate to its verification tier.
13
13
 
14
14
  ### Proof Standard
15
15
 
16
16
  - Proof document at `verification/<story-key>-proof.md`
17
- - Each AC gets a `## AC N:` section with `docker exec` commands and captured output
18
- - Evidence must come from running the installed CLI/tool, not from grepping source
17
+ - Each AC gets a `## AC N:` section with tier-appropriate evidence and captured output
19
18
  - `[FAIL]` = AC failed with evidence showing what went wrong
20
19
  - `[ESCALATE]` = AC genuinely cannot be automated (last resort — try everything first)
21
20
 
21
+ **Tier-dependent evidence rules:**
22
+
23
+ - **`test-provable`** — Evidence comes from build + test output + grep/read of code or artifacts. Run `npm test` or `npm run build`, capture results. Source-level assertions are the primary verification method. No running app or Docker required.
24
+ - **`runtime-provable`** — Evidence comes from running the actual binary/server and interacting with it. Start the process, make requests or run commands, capture stdout/stderr/exit codes. No Docker stack required.
25
+ - **`environment-provable`** — Evidence comes from `docker exec` commands and observability queries. Full Docker verification environment required. Each AC section needs at least one `docker exec`, `docker ps/logs`, or observability query. Evidence must come from running the installed CLI/tool in Docker, not from grepping source.
26
+ - **`escalate`** — Human judgment required. Document why automation is not possible. `[ESCALATE]` verdict is expected.
27
+
22
28
  ### Verification Tags
23
29
 
24
- For each AC, append a tag indicating verification approach:
25
- - `<!-- verification: cli-verifiable -->` — default. Can be verified via CLI commands in a Docker container.
26
- - `<!-- verification: integration-required -->` — requires external systems not available in the test environment (e.g., paid third-party APIs, physical hardware). This is rare — most things including workflows, agent sessions, and multi-step processes CAN be verified in Docker.
30
+ For each AC, append a tag indicating its verification tier:
31
+ - `<!-- verification: test-provable -->` — Can be verified by building and running tests. Evidence: build output, test results, grep/read of code. No running app needed.
32
+ - `<!-- verification: runtime-provable -->` — Requires running the actual binary/CLI/server. Evidence: process output, HTTP responses, exit codes. No Docker stack needed.
33
+ - `<!-- verification: environment-provable -->` — Requires full Docker environment with observability. Evidence: `docker exec` commands, VictoriaLogs queries, multi-service interaction.
34
+ - `<!-- verification: escalate -->` — Cannot be automated. Requires human judgment, physical hardware, or paid external services.
35
+
36
+ **Decision criteria:**
37
+ 1. Can you prove it with `npm test` or `npm run build` alone? → `test-provable`
38
+ 2. Do you need to run the actual binary/server locally? → `runtime-provable`
39
+ 3. Do you need Docker, external services, or observability? → `environment-provable`
40
+ 4. Have you exhausted all automated approaches? → `escalate`
27
41
 
28
- **Do not over-tag.** Workflows, sprint planning, user sessions, slash commands, and agent behavior are all verifiable via `docker exec ... claude --print`. Only tag `integration-required` when there is genuinely no automated path.
42
+ **Do not over-tag.** Most stories are `test-provable` or `runtime-provable`. Only use `environment-provable` when Docker infrastructure is genuinely needed. Only use `escalate` as a last resort.
29
43
 
30
44
  ### Observability Evidence
31
45
 
32
- After each `docker exec` command, query the observability backend for log events from the last 30 seconds.
46
+ After each `docker exec` command (applicable to `environment-provable` stories), query the observability backend for log events from the last 30 seconds.
33
47
  Use the configured VictoriaLogs endpoint (default: `http://localhost:9428`):
34
48
 
35
49
  ```bash
package/ralph/ralph.sh CHANGED
@@ -279,13 +279,10 @@ check_sprint_complete() {
279
279
  local done_count=0
280
280
  local flagged_count=0
281
281
 
282
- # Load flagged stories for comparison
283
- local -A flagged_map
282
+ # Load flagged stories into a newline-separated string for lookup
283
+ local flagged_list=""
284
284
  if [[ -f "$FLAGGED_STORIES_FILE" ]]; then
285
- while IFS= read -r flagged_key; do
286
- flagged_key=$(echo "$flagged_key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
287
- [[ -n "$flagged_key" ]] && flagged_map["$flagged_key"]=1
288
- done < "$FLAGGED_STORIES_FILE"
285
+ flagged_list=$(sed 's/^[[:space:]]*//;s/[[:space:]]*$//' "$FLAGGED_STORIES_FILE" | grep -v '^$')
289
286
  fi
290
287
 
291
288
  while IFS=: read -r key value; do
@@ -301,7 +298,7 @@ check_sprint_complete() {
301
298
  total=$((total + 1))
302
299
  if [[ "$value" == "done" ]]; then
303
300
  done_count=$((done_count + 1))
304
- elif [[ -n "${flagged_map[$key]+x}" ]]; then
301
+ elif [[ -n "$flagged_list" ]] && echo "$flagged_list" | grep -qxF "$key"; then
305
302
  # Retry-exhausted/flagged stories count as "effectively done"
306
303
  # — no autonomous work can be done on them
307
304
  flagged_count=$((flagged_count + 1))
@@ -341,7 +338,7 @@ get_task_counts() {
341
338
 
342
339
  if [[ "$key" =~ ^[0-9]+-[0-9]+- ]]; then
343
340
  total=$((total + 1))
344
- if [[ "$value" == "done" ]]; then
341
+ if [[ "$value" == "done" || "$value" == "failed" ]]; then
345
342
  completed=$((completed + 1))
346
343
  fi
347
344
  fi
@@ -442,6 +439,14 @@ flag_story() {
442
439
  if ! is_story_flagged "$story_key"; then
443
440
  echo "$story_key" >> "$FLAGGED_STORIES_FILE"
444
441
  fi
442
+
443
+ # Also update sprint-status.yaml to 'failed' so reconciliation picks it up
444
+ # and sprint-state.json stays consistent (prevents flagged stories stuck at 'review')
445
+ local sprint_yaml="${SPRINT_STATUS_FILE:-}"
446
+ if [[ -n "$sprint_yaml" && -f "$sprint_yaml" ]]; then
447
+ sed -i.bak "s/^ ${story_key}: .*/ ${story_key}: failed/" "$sprint_yaml" 2>/dev/null
448
+ rm -f "${sprint_yaml}.bak" 2>/dev/null
449
+ fi
445
450
  }
446
451
 
447
452
  # Get list of flagged stories (newline-separated).