codeharness 0.36.2 → 0.36.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  validateDockerfile,
41
41
  warn,
42
42
  writeState
43
- } from "./chunk-374J3J3A.js";
43
+ } from "./chunk-2FHBRGG7.js";
44
44
 
45
45
  // src/index.ts
46
46
  import { Command } from "commander";
@@ -149,7 +149,7 @@ function registerBridgeCommand(program) {
149
149
 
150
150
  // src/commands/run.ts
151
151
  import { existsSync as existsSync17 } from "fs";
152
- import { join as join15 } from "path";
152
+ import { join as join16 } from "path";
153
153
 
154
154
  // src/modules/sprint/state.ts
155
155
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync, existsSync as existsSync5, unlinkSync } from "fs";
@@ -2507,9 +2507,9 @@ function resolveWorkflow(options) {
2507
2507
  }
2508
2508
 
2509
2509
  // src/lib/workflow-machine.ts
2510
- import { setup, assign, fromPromise, createActor } from "xstate";
2510
+ import { setup, assign, fromPromise as fromPromise2, createActor } from "xstate";
2511
2511
  import { readFileSync as readFileSync13, existsSync as existsSync15, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "fs";
2512
- import { join as join12 } from "path";
2512
+ import { join as join13 } from "path";
2513
2513
 
2514
2514
  // src/lib/agent-dispatch.ts
2515
2515
  import { query } from "@anthropic-ai/claude-agent-sdk";
@@ -2550,257 +2550,6 @@ function checkCapabilityConflicts(workflow) {
2550
2550
  return warnings;
2551
2551
  }
2552
2552
 
2553
- // src/lib/agents/model-resolver.ts
2554
- function resolveModel(task, agent, driver) {
2555
- const taskModel = task.model?.trim() || void 0;
2556
- const agentModel = agent.model?.trim() || void 0;
2557
- if (taskModel) {
2558
- return taskModel;
2559
- }
2560
- if (agentModel) {
2561
- return agentModel;
2562
- }
2563
- if (!driver.defaultModel?.trim()) {
2564
- throw new Error(
2565
- "Driver has no default model: driver.defaultModel must be a non-empty string"
2566
- );
2567
- }
2568
- return driver.defaultModel;
2569
- }
2570
-
2571
- // src/lib/agents/output-contract.ts
2572
- import { writeFileSync as writeFileSync6, readFileSync as readFileSync10, renameSync as renameSync2, mkdirSync as mkdirSync2, existsSync as existsSync11 } from "fs";
2573
- import { join as join8, resolve as resolve4 } from "path";
2574
- function assertSafeComponent(value, label) {
2575
- if (!value || value.trim().length === 0) {
2576
- throw new Error(`${label} must be a non-empty string`);
2577
- }
2578
- if (value.includes("/") || value.includes("\\") || value.includes("..")) {
2579
- throw new Error(`${label} contains invalid path characters: ${value}`);
2580
- }
2581
- }
2582
- function contractFilePath(taskName, storyId, contractDir) {
2583
- assertSafeComponent(taskName, "taskName");
2584
- assertSafeComponent(storyId, "storyId");
2585
- const filePath = join8(contractDir, `${taskName}-${storyId}.json`);
2586
- const resolvedDir = resolve4(contractDir);
2587
- const resolvedFile = resolve4(filePath);
2588
- if (!resolvedFile.startsWith(resolvedDir)) {
2589
- throw new Error(`Path traversal detected: ${filePath} escapes ${contractDir}`);
2590
- }
2591
- return filePath;
2592
- }
2593
- function writeOutputContract(contract, contractDir) {
2594
- const finalPath = contractFilePath(contract.taskName, contract.storyId, contractDir);
2595
- const tmpPath2 = finalPath + ".tmp";
2596
- try {
2597
- mkdirSync2(contractDir, { recursive: true });
2598
- writeFileSync6(tmpPath2, JSON.stringify(contract, null, 2) + "\n", "utf-8");
2599
- renameSync2(tmpPath2, finalPath);
2600
- } catch (err) {
2601
- const message = err instanceof Error ? err.message : String(err);
2602
- throw new Error(`Failed to write output contract to ${finalPath}: ${message}`, { cause: err });
2603
- }
2604
- }
2605
- var OUTPUT_TRUNCATE_LIMIT = 2e3;
2606
- function formatContractAsPromptContext(contract) {
2607
- const sections = [];
2608
- const costStr = contract.cost_usd != null ? `$${contract.cost_usd.toFixed(2)}` : "N/A";
2609
- const durationStr = `${(contract.duration_ms / 1e3).toFixed(1)}s`;
2610
- sections.push(
2611
- `### Context from Previous Task
2612
- - **Task:** ${contract.taskName}
2613
- - **Driver:** ${contract.driver}
2614
- - **Model:** ${contract.model}
2615
- - **Cost:** ${costStr}
2616
- - **Duration:** ${durationStr}
2617
- - **Timestamp:** ${contract.timestamp}`
2618
- );
2619
- if (contract.changedFiles.length > 0) {
2620
- const fileList = contract.changedFiles.map((f) => `- ${f}`).join("\n");
2621
- sections.push(`### Changed Files
2622
- ${fileList}`);
2623
- } else {
2624
- sections.push(`### Changed Files
2625
- None`);
2626
- }
2627
- if (contract.testResults) {
2628
- const tr = contract.testResults;
2629
- const coverageStr = tr.coverage != null ? `${tr.coverage}%` : "N/A";
2630
- sections.push(
2631
- `### Test Results
2632
- - **Passed:** ${tr.passed}
2633
- - **Failed:** ${tr.failed}
2634
- - **Coverage:** ${coverageStr}`
2635
- );
2636
- } else {
2637
- sections.push(`### Test Results
2638
- No test results available`);
2639
- }
2640
- let outputText = contract.output;
2641
- if (outputText.length > OUTPUT_TRUNCATE_LIMIT) {
2642
- outputText = outputText.slice(0, OUTPUT_TRUNCATE_LIMIT) + " [truncated]";
2643
- }
2644
- if (outputText.length > 0) {
2645
- sections.push(`### Output Summary
2646
- ${outputText}`);
2647
- } else {
2648
- sections.push(`### Output Summary
2649
- None`);
2650
- }
2651
- if (contract.acceptanceCriteria.length > 0) {
2652
- const acList = contract.acceptanceCriteria.map((ac) => `- **${ac.id}** (${ac.status}): ${ac.description}`).join("\n");
2653
- sections.push(`### Acceptance Criteria
2654
- ${acList}`);
2655
- } else {
2656
- sections.push(`### Acceptance Criteria
2657
- None`);
2658
- }
2659
- return sections.join("\n\n");
2660
- }
2661
- function buildPromptWithContractContext(basePrompt, previousContract) {
2662
- if (!previousContract) {
2663
- return basePrompt;
2664
- }
2665
- const context = formatContractAsPromptContext(previousContract);
2666
- return `${basePrompt}
2667
-
2668
- ---
2669
-
2670
- ## Previous Task Context
2671
-
2672
- ${context}`;
2673
- }
2674
-
2675
- // src/lib/source-isolation.ts
2676
- import { mkdirSync as mkdirSync3, copyFileSync, existsSync as existsSync12, rmSync } from "fs";
2677
- import { join as join9, basename } from "path";
2678
- function sanitizeRunId(runId) {
2679
- let sanitized = runId.replace(/[^a-zA-Z0-9._-]/g, "_");
2680
- sanitized = sanitized.replace(/^\.+/, "");
2681
- if (sanitized.length === 0) {
2682
- throw new Error("Source isolation: runId is empty after sanitization");
2683
- }
2684
- return sanitized;
2685
- }
2686
- function deduplicateFilename(dir, name) {
2687
- if (!existsSync12(join9(dir, name))) {
2688
- return name;
2689
- }
2690
- const dotIdx = name.lastIndexOf(".");
2691
- const stem = dotIdx > 0 ? name.slice(0, dotIdx) : name;
2692
- const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
2693
- let counter = 1;
2694
- let candidate;
2695
- do {
2696
- candidate = `${stem}-${counter}${ext}`;
2697
- counter++;
2698
- } while (existsSync12(join9(dir, candidate)));
2699
- return candidate;
2700
- }
2701
- async function createIsolatedWorkspace(options) {
2702
- const safeRunId = sanitizeRunId(options.runId);
2703
- const dir = `/tmp/codeharness-verify-${safeRunId}`;
2704
- const storyFilesDir = join9(dir, "story-files");
2705
- const verdictDir = join9(dir, "verdict");
2706
- if (existsSync12(dir)) {
2707
- rmSync(dir, { recursive: true, force: true });
2708
- }
2709
- mkdirSync3(storyFilesDir, { recursive: true });
2710
- mkdirSync3(verdictDir, { recursive: true });
2711
- for (const filePath of options.storyFiles) {
2712
- if (!existsSync12(filePath)) {
2713
- warn(`Source isolation: story file not found, skipping: ${filePath}`);
2714
- continue;
2715
- }
2716
- const name = deduplicateFilename(storyFilesDir, basename(filePath));
2717
- const dest = join9(storyFilesDir, name);
2718
- copyFileSync(filePath, dest);
2719
- }
2720
- const workspace = {
2721
- dir,
2722
- storyFilesDir,
2723
- verdictDir,
2724
- toDispatchOptions() {
2725
- return { cwd: dir };
2726
- },
2727
- async cleanup() {
2728
- if (existsSync12(dir)) {
2729
- rmSync(dir, { recursive: true, force: true });
2730
- }
2731
- }
2732
- };
2733
- return workspace;
2734
- }
2735
-
2736
- // src/lib/trace-id.ts
2737
- function sanitizeSegment(segment) {
2738
- return segment.replace(/[^a-zA-Z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
2739
- }
2740
- var MAX_SEGMENT_LENGTH = 128;
2741
- function generateTraceId(runId, iteration, taskName) {
2742
- if (!Number.isInteger(iteration) || iteration < 0) {
2743
- throw new Error(
2744
- `generateTraceId: iteration must be a non-negative integer, got ${iteration}`
2745
- );
2746
- }
2747
- const safeRunId = sanitizeSegment(runId).slice(0, MAX_SEGMENT_LENGTH);
2748
- const safeTask = sanitizeSegment(taskName).slice(0, MAX_SEGMENT_LENGTH);
2749
- return `ch-${safeRunId}-${iteration}-${safeTask}`;
2750
- }
2751
- function formatTracePrompt(traceId) {
2752
- if (!traceId) {
2753
- throw new Error("formatTracePrompt: traceId must be a non-empty string");
2754
- }
2755
- return `[TRACE] trace_id=${traceId}
2756
- Include this trace ID in all log output, metric labels, and trace spans for correlation.`;
2757
- }
2758
- function recordTraceId(traceId, state) {
2759
- if (!traceId) {
2760
- throw new Error("recordTraceId: traceId must be a non-empty string");
2761
- }
2762
- return {
2763
- ...state,
2764
- trace_ids: [...state.trace_ids ?? [], traceId]
2765
- };
2766
- }
2767
-
2768
- // src/lib/session-manager.ts
2769
- function resolveSessionId(boundary, key, state) {
2770
- if (boundary === "fresh") {
2771
- return void 0;
2772
- }
2773
- if (boundary !== "continue") {
2774
- return void 0;
2775
- }
2776
- return getLastSessionId(state, key.taskName, key.storyKey);
2777
- }
2778
- function recordSessionId(key, sessionId, state) {
2779
- if (!sessionId) {
2780
- throw new Error("recordSessionId: sessionId must be a non-empty string");
2781
- }
2782
- const checkpoint = {
2783
- task_name: key.taskName,
2784
- story_key: key.storyKey,
2785
- completed_at: (/* @__PURE__ */ new Date()).toISOString(),
2786
- session_id: sessionId
2787
- };
2788
- return {
2789
- ...state,
2790
- tasks_completed: [...state.tasks_completed, checkpoint]
2791
- };
2792
- }
2793
- function getLastSessionId(state, taskName, storyKey) {
2794
- const tasks = state.tasks_completed;
2795
- for (let i = tasks.length - 1; i >= 0; i--) {
2796
- const cp = tasks[i];
2797
- if (cp.task_name === taskName && cp.story_key === storyKey && cp.session_id !== void 0) {
2798
- return cp.session_id;
2799
- }
2800
- }
2801
- return void 0;
2802
- }
2803
-
2804
2553
  // src/lib/verdict-parser.ts
2805
2554
  import Ajv2 from "ajv";
2806
2555
 
@@ -3005,71 +2754,12 @@ function evaluateProgress(scores) {
3005
2754
  };
3006
2755
  }
3007
2756
 
3008
- // src/lib/telemetry-writer.ts
3009
- import { appendFileSync, existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync11 } from "fs";
3010
- import { join as join10 } from "path";
3011
- var TELEMETRY_DIR = ".codeharness";
3012
- var TELEMETRY_FILE = "telemetry.jsonl";
3013
- function extractEpicId(storyKey) {
3014
- if (storyKey === "__run__") return "unknown";
3015
- const dash = storyKey.indexOf("-");
3016
- if (dash === -1) return storyKey;
3017
- return storyKey.slice(0, dash);
3018
- }
3019
- async function writeTelemetryEntry(ctx) {
3020
- const epicId = extractEpicId(ctx.storyKey);
3021
- const contract = ctx.outputContract;
3022
- const entry = {
3023
- version: 1,
3024
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3025
- storyKey: ctx.storyKey,
3026
- epicId,
3027
- duration_ms: ctx.durationMs,
3028
- cost_usd: ctx.cost ?? null,
3029
- attempts: null,
3030
- acResults: contract?.acceptanceCriteria ? contract.acceptanceCriteria.map((ac) => ({
3031
- id: ac.id,
3032
- description: ac.description,
3033
- status: ac.status
3034
- })) : null,
3035
- filesChanged: contract?.changedFiles ? [...contract.changedFiles] : [],
3036
- testResults: contract?.testResults ? {
3037
- passed: contract.testResults.passed,
3038
- failed: contract.testResults.failed,
3039
- coverage: contract.testResults.coverage
3040
- } : null,
3041
- errors: []
3042
- };
3043
- const dir = join10(ctx.projectDir, TELEMETRY_DIR);
3044
- mkdirSync4(dir, { recursive: true });
3045
- appendFileSync(join10(dir, TELEMETRY_FILE), JSON.stringify(entry) + "\n");
3046
- return { success: true, output: `telemetry: entry written for ${ctx.storyKey}` };
3047
- }
3048
-
3049
- // src/lib/null-task-registry.ts
3050
- var registry2 = /* @__PURE__ */ new Map();
3051
- function registerNullTask(name, handler) {
3052
- registry2.set(name, handler);
3053
- }
3054
- function getNullTask(name) {
3055
- return registry2.get(name);
3056
- }
3057
- function listNullTasks() {
3058
- return [...registry2.keys()];
3059
- }
3060
- registerNullTask("telemetry", writeTelemetryEntry);
3061
-
3062
2757
  // src/lib/workflow-machine.ts
3063
2758
  import { parse as parse5 } from "yaml";
3064
2759
 
3065
- // src/lib/evaluator.ts
3066
- function formatCoverageContextMessage(coverage, target) {
3067
- return `Coverage already verified by engine: ${coverage}% (target: ${target}%). No re-run needed.`;
3068
- }
3069
-
3070
2760
  // src/lib/workflow-state.ts
3071
- import { existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
3072
- import { join as join11 } from "path";
2761
+ import { existsSync as existsSync11, mkdirSync as mkdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
2762
+ import { join as join8 } from "path";
3073
2763
  import { parse as parse4, stringify } from "yaml";
3074
2764
  var STATE_DIR = ".codeharness";
3075
2765
  var STATE_FILE = "workflow-state.yaml";
@@ -3091,20 +2781,20 @@ function getDefaultWorkflowState() {
3091
2781
  }
3092
2782
  function writeWorkflowState(state, dir) {
3093
2783
  const baseDir = dir ?? process.cwd();
3094
- const stateDir = join11(baseDir, STATE_DIR);
3095
- mkdirSync5(stateDir, { recursive: true });
2784
+ const stateDir = join8(baseDir, STATE_DIR);
2785
+ mkdirSync2(stateDir, { recursive: true });
3096
2786
  const yamlContent = stringify(state, { nullStr: "null" });
3097
- writeFileSync7(join11(stateDir, STATE_FILE), yamlContent, "utf-8");
2787
+ writeFileSync6(join8(stateDir, STATE_FILE), yamlContent, "utf-8");
3098
2788
  }
3099
2789
  function readWorkflowState(dir) {
3100
2790
  const baseDir = dir ?? process.cwd();
3101
- const filePath = join11(baseDir, STATE_DIR, STATE_FILE);
3102
- if (!existsSync14(filePath)) {
2791
+ const filePath = join8(baseDir, STATE_DIR, STATE_FILE);
2792
+ if (!existsSync11(filePath)) {
3103
2793
  return getDefaultWorkflowState();
3104
2794
  }
3105
2795
  let raw;
3106
2796
  try {
3107
- raw = readFileSync12(filePath, "utf-8");
2797
+ raw = readFileSync10(filePath, "utf-8");
3108
2798
  } catch {
3109
2799
  warn("workflow-state.yaml could not be read \u2014 returning default state");
3110
2800
  return getDefaultWorkflowState();
@@ -3167,10 +2857,331 @@ function isValidWorkflowState(value) {
3167
2857
  return true;
3168
2858
  }
3169
2859
 
3170
- // src/lib/workflow-machine.ts
3171
- var HALT_ERROR_CODES = /* @__PURE__ */ new Set(["RATE_LIMIT", "NETWORK", "SDK_INIT"]);
3172
- var DEFAULT_MAX_ITERATIONS = 5;
3173
- var HEALTH_CHECK_TIMEOUT_MS = 5e3;
2860
+ // src/lib/workflow-actors.ts
2861
+ import { fromPromise } from "xstate";
2862
+ import { join as join12 } from "path";
2863
+
2864
+ // src/lib/agents/model-resolver.ts
2865
+ function resolveModel(task, agent, driver) {
2866
+ const taskModel = task.model?.trim() || void 0;
2867
+ const agentModel = agent.model?.trim() || void 0;
2868
+ if (taskModel) {
2869
+ return taskModel;
2870
+ }
2871
+ if (agentModel) {
2872
+ return agentModel;
2873
+ }
2874
+ if (!driver.defaultModel?.trim()) {
2875
+ throw new Error(
2876
+ "Driver has no default model: driver.defaultModel must be a non-empty string"
2877
+ );
2878
+ }
2879
+ return driver.defaultModel;
2880
+ }
2881
+
2882
+ // src/lib/agents/output-contract.ts
2883
+ import { writeFileSync as writeFileSync7, readFileSync as readFileSync11, renameSync as renameSync2, mkdirSync as mkdirSync3, existsSync as existsSync12 } from "fs";
2884
+ import { join as join9, resolve as resolve4 } from "path";
2885
+ function assertSafeComponent(value, label) {
2886
+ if (!value || value.trim().length === 0) {
2887
+ throw new Error(`${label} must be a non-empty string`);
2888
+ }
2889
+ if (value.includes("/") || value.includes("\\") || value.includes("..")) {
2890
+ throw new Error(`${label} contains invalid path characters: ${value}`);
2891
+ }
2892
+ }
2893
+ function contractFilePath(taskName, storyId, contractDir) {
2894
+ assertSafeComponent(taskName, "taskName");
2895
+ assertSafeComponent(storyId, "storyId");
2896
+ const filePath = join9(contractDir, `${taskName}-${storyId}.json`);
2897
+ const resolvedDir = resolve4(contractDir);
2898
+ const resolvedFile = resolve4(filePath);
2899
+ if (!resolvedFile.startsWith(resolvedDir)) {
2900
+ throw new Error(`Path traversal detected: ${filePath} escapes ${contractDir}`);
2901
+ }
2902
+ return filePath;
2903
+ }
2904
+ function writeOutputContract(contract, contractDir) {
2905
+ const finalPath = contractFilePath(contract.taskName, contract.storyId, contractDir);
2906
+ const tmpPath2 = finalPath + ".tmp";
2907
+ try {
2908
+ mkdirSync3(contractDir, { recursive: true });
2909
+ writeFileSync7(tmpPath2, JSON.stringify(contract, null, 2) + "\n", "utf-8");
2910
+ renameSync2(tmpPath2, finalPath);
2911
+ } catch (err) {
2912
+ const message = err instanceof Error ? err.message : String(err);
2913
+ throw new Error(`Failed to write output contract to ${finalPath}: ${message}`, { cause: err });
2914
+ }
2915
+ }
2916
+ var OUTPUT_TRUNCATE_LIMIT = 2e3;
2917
+ function formatContractAsPromptContext(contract) {
2918
+ const sections = [];
2919
+ const costStr = contract.cost_usd != null ? `$${contract.cost_usd.toFixed(2)}` : "N/A";
2920
+ const durationStr = `${(contract.duration_ms / 1e3).toFixed(1)}s`;
2921
+ sections.push(
2922
+ `### Context from Previous Task
2923
+ - **Task:** ${contract.taskName}
2924
+ - **Driver:** ${contract.driver}
2925
+ - **Model:** ${contract.model}
2926
+ - **Cost:** ${costStr}
2927
+ - **Duration:** ${durationStr}
2928
+ - **Timestamp:** ${contract.timestamp}`
2929
+ );
2930
+ if (contract.changedFiles.length > 0) {
2931
+ const fileList = contract.changedFiles.map((f) => `- ${f}`).join("\n");
2932
+ sections.push(`### Changed Files
2933
+ ${fileList}`);
2934
+ } else {
2935
+ sections.push(`### Changed Files
2936
+ None`);
2937
+ }
2938
+ if (contract.testResults) {
2939
+ const tr = contract.testResults;
2940
+ const coverageStr = tr.coverage != null ? `${tr.coverage}%` : "N/A";
2941
+ sections.push(
2942
+ `### Test Results
2943
+ - **Passed:** ${tr.passed}
2944
+ - **Failed:** ${tr.failed}
2945
+ - **Coverage:** ${coverageStr}`
2946
+ );
2947
+ } else {
2948
+ sections.push(`### Test Results
2949
+ No test results available`);
2950
+ }
2951
+ let outputText = contract.output;
2952
+ if (outputText.length > OUTPUT_TRUNCATE_LIMIT) {
2953
+ outputText = outputText.slice(0, OUTPUT_TRUNCATE_LIMIT) + " [truncated]";
2954
+ }
2955
+ if (outputText.length > 0) {
2956
+ sections.push(`### Output Summary
2957
+ ${outputText}`);
2958
+ } else {
2959
+ sections.push(`### Output Summary
2960
+ None`);
2961
+ }
2962
+ if (contract.acceptanceCriteria.length > 0) {
2963
+ const acList = contract.acceptanceCriteria.map((ac) => `- **${ac.id}** (${ac.status}): ${ac.description}`).join("\n");
2964
+ sections.push(`### Acceptance Criteria
2965
+ ${acList}`);
2966
+ } else {
2967
+ sections.push(`### Acceptance Criteria
2968
+ None`);
2969
+ }
2970
+ return sections.join("\n\n");
2971
+ }
2972
+ function buildPromptWithContractContext(basePrompt, previousContract) {
2973
+ if (!previousContract) {
2974
+ return basePrompt;
2975
+ }
2976
+ const context = formatContractAsPromptContext(previousContract);
2977
+ return `${basePrompt}
2978
+
2979
+ ---
2980
+
2981
+ ## Previous Task Context
2982
+
2983
+ ${context}`;
2984
+ }
2985
+
2986
+ // src/lib/source-isolation.ts
2987
+ import { mkdirSync as mkdirSync4, copyFileSync, existsSync as existsSync13, rmSync } from "fs";
2988
+ import { join as join10, basename } from "path";
2989
+ function sanitizeRunId(runId) {
2990
+ let sanitized = runId.replace(/[^a-zA-Z0-9._-]/g, "_");
2991
+ sanitized = sanitized.replace(/^\.+/, "");
2992
+ if (sanitized.length === 0) {
2993
+ throw new Error("Source isolation: runId is empty after sanitization");
2994
+ }
2995
+ return sanitized;
2996
+ }
2997
+ function deduplicateFilename(dir, name) {
2998
+ if (!existsSync13(join10(dir, name))) {
2999
+ return name;
3000
+ }
3001
+ const dotIdx = name.lastIndexOf(".");
3002
+ const stem = dotIdx > 0 ? name.slice(0, dotIdx) : name;
3003
+ const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
3004
+ let counter = 1;
3005
+ let candidate;
3006
+ do {
3007
+ candidate = `${stem}-${counter}${ext}`;
3008
+ counter++;
3009
+ } while (existsSync13(join10(dir, candidate)));
3010
+ return candidate;
3011
+ }
3012
+ async function createIsolatedWorkspace(options) {
3013
+ const safeRunId = sanitizeRunId(options.runId);
3014
+ const dir = `/tmp/codeharness-verify-${safeRunId}`;
3015
+ const storyFilesDir = join10(dir, "story-files");
3016
+ const verdictDir = join10(dir, "verdict");
3017
+ if (existsSync13(dir)) {
3018
+ rmSync(dir, { recursive: true, force: true });
3019
+ }
3020
+ mkdirSync4(storyFilesDir, { recursive: true });
3021
+ mkdirSync4(verdictDir, { recursive: true });
3022
+ for (const filePath of options.storyFiles) {
3023
+ if (!existsSync13(filePath)) {
3024
+ warn(`Source isolation: story file not found, skipping: ${filePath}`);
3025
+ continue;
3026
+ }
3027
+ const name = deduplicateFilename(storyFilesDir, basename(filePath));
3028
+ const dest = join10(storyFilesDir, name);
3029
+ copyFileSync(filePath, dest);
3030
+ }
3031
+ const workspace = {
3032
+ dir,
3033
+ storyFilesDir,
3034
+ verdictDir,
3035
+ toDispatchOptions() {
3036
+ return { cwd: dir };
3037
+ },
3038
+ async cleanup() {
3039
+ if (existsSync13(dir)) {
3040
+ rmSync(dir, { recursive: true, force: true });
3041
+ }
3042
+ }
3043
+ };
3044
+ return workspace;
3045
+ }
3046
+
3047
+ // src/lib/trace-id.ts
3048
+ function sanitizeSegment(segment) {
3049
+ return segment.replace(/[^a-zA-Z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
3050
+ }
3051
+ var MAX_SEGMENT_LENGTH = 128;
3052
+ function generateTraceId(runId, iteration, taskName) {
3053
+ if (!Number.isInteger(iteration) || iteration < 0) {
3054
+ throw new Error(
3055
+ `generateTraceId: iteration must be a non-negative integer, got ${iteration}`
3056
+ );
3057
+ }
3058
+ const safeRunId = sanitizeSegment(runId).slice(0, MAX_SEGMENT_LENGTH);
3059
+ const safeTask = sanitizeSegment(taskName).slice(0, MAX_SEGMENT_LENGTH);
3060
+ return `ch-${safeRunId}-${iteration}-${safeTask}`;
3061
+ }
3062
+ function formatTracePrompt(traceId) {
3063
+ if (!traceId) {
3064
+ throw new Error("formatTracePrompt: traceId must be a non-empty string");
3065
+ }
3066
+ return `[TRACE] trace_id=${traceId}
3067
+ Include this trace ID in all log output, metric labels, and trace spans for correlation.`;
3068
+ }
3069
+ function recordTraceId(traceId, state) {
3070
+ if (!traceId) {
3071
+ throw new Error("recordTraceId: traceId must be a non-empty string");
3072
+ }
3073
+ return {
3074
+ ...state,
3075
+ trace_ids: [...state.trace_ids ?? [], traceId]
3076
+ };
3077
+ }
3078
+
3079
+ // src/lib/session-manager.ts
3080
+ function resolveSessionId(boundary, key, state) {
3081
+ if (boundary === "fresh") {
3082
+ return void 0;
3083
+ }
3084
+ if (boundary !== "continue") {
3085
+ return void 0;
3086
+ }
3087
+ return getLastSessionId(state, key.taskName, key.storyKey);
3088
+ }
3089
+ function recordSessionId(key, sessionId, state) {
3090
+ if (!sessionId) {
3091
+ throw new Error("recordSessionId: sessionId must be a non-empty string");
3092
+ }
3093
+ const checkpoint = {
3094
+ task_name: key.taskName,
3095
+ story_key: key.storyKey,
3096
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
3097
+ session_id: sessionId
3098
+ };
3099
+ return {
3100
+ ...state,
3101
+ tasks_completed: [...state.tasks_completed, checkpoint]
3102
+ };
3103
+ }
3104
+ function getLastSessionId(state, taskName, storyKey) {
3105
+ const tasks = state.tasks_completed;
3106
+ for (let i = tasks.length - 1; i >= 0; i--) {
3107
+ const cp = tasks[i];
3108
+ if (cp.task_name === taskName && cp.story_key === storyKey && cp.session_id !== void 0) {
3109
+ return cp.session_id;
3110
+ }
3111
+ }
3112
+ return void 0;
3113
+ }
3114
+
3115
+ // src/lib/telemetry-writer.ts
3116
+ import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync12 } from "fs";
3117
+ import { join as join11 } from "path";
3118
+ var TELEMETRY_DIR = ".codeharness";
3119
+ var TELEMETRY_FILE = "telemetry.jsonl";
3120
+ function extractEpicId(storyKey) {
3121
+ if (storyKey === "__run__") return "unknown";
3122
+ const dash = storyKey.indexOf("-");
3123
+ if (dash === -1) return storyKey;
3124
+ return storyKey.slice(0, dash);
3125
+ }
3126
+ async function writeTelemetryEntry(ctx) {
3127
+ const epicId = extractEpicId(ctx.storyKey);
3128
+ const contract = ctx.outputContract;
3129
+ const entry = {
3130
+ version: 1,
3131
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3132
+ storyKey: ctx.storyKey,
3133
+ epicId,
3134
+ duration_ms: ctx.durationMs,
3135
+ cost_usd: ctx.cost ?? null,
3136
+ attempts: null,
3137
+ acResults: contract?.acceptanceCriteria ? contract.acceptanceCriteria.map((ac) => ({
3138
+ id: ac.id,
3139
+ description: ac.description,
3140
+ status: ac.status
3141
+ })) : null,
3142
+ filesChanged: contract?.changedFiles ? [...contract.changedFiles] : [],
3143
+ testResults: contract?.testResults ? {
3144
+ passed: contract.testResults.passed,
3145
+ failed: contract.testResults.failed,
3146
+ coverage: contract.testResults.coverage
3147
+ } : null,
3148
+ errors: []
3149
+ };
3150
+ const dir = join11(ctx.projectDir, TELEMETRY_DIR);
3151
+ mkdirSync5(dir, { recursive: true });
3152
+ appendFileSync(join11(dir, TELEMETRY_FILE), JSON.stringify(entry) + "\n");
3153
+ return { success: true, output: `telemetry: entry written for ${ctx.storyKey}` };
3154
+ }
3155
+
3156
+ // src/lib/null-task-registry.ts
3157
+ var registry2 = /* @__PURE__ */ new Map();
3158
+ function registerNullTask(name, handler) {
3159
+ registry2.set(name, handler);
3160
+ }
3161
+ function getNullTask(name) {
3162
+ return registry2.get(name);
3163
+ }
3164
+ function listNullTasks() {
3165
+ return [...registry2.keys()];
3166
+ }
3167
+ registerNullTask("telemetry", writeTelemetryEntry);
3168
+
3169
+ // src/lib/evaluator.ts
3170
+ function formatCoverageContextMessage(coverage, target) {
3171
+ return `Coverage already verified by engine: ${coverage}% (target: ${target}%). No re-run needed.`;
3172
+ }
3173
+
3174
+ // src/lib/workflow-actors.ts
3175
+ var TASK_PROMPTS = {
3176
+ "create-story": (key) => `Create the story spec for ${key}. Read the epic definitions and architecture docs. Write a complete story file with acceptance criteria, tasks, and dev notes. CRITICAL: Every AC must be testable by a blind QA agent using ONLY a user guide + browser/API/CLI access. No AC should reference source code, internal data structures, or implementation details like O(1) complexity. Each AC must describe observable behavior that can be verified through UI interaction (agent-browser), API calls (curl), CLI commands (docker exec), or log inspection (docker logs). Wrap output in <story-spec>...</story-spec> tags.`,
3177
+ "implement": (key) => `Implement story ${key}`,
3178
+ "check": (key) => `Run automated checks for story ${key}. Run ONLY these commands: 1) \`npx vitest run\` (unit tests), 2) \`npm run lint\` (linter). Do NOT run \`npm test\` (BATS integration tests) \u2014 those require the installed CLI binary and Docker, which are not available in the sandbox. If vitest and lint both pass, include <verdict>pass</verdict>. If either fails, include <verdict>fail</verdict> with the actual error output. Only report failures from YOUR test run, not from previous sessions.`,
3179
+ "review": (key) => `Review the implementation of story ${key}. Check for correctness, security issues, architecture violations, and AC coverage. Include <verdict>pass</verdict> or <verdict>fail</verdict> in your response. If fail, include <issues>...</issues>.`,
3180
+ "document": (key) => `Write user documentation for story ${key}. Describe what was built and how to use it from a user's perspective. No source code. Wrap documentation in <user-docs>...</user-docs> tags.`,
3181
+ "deploy": () => `Provision the Docker environment for this project. Check for docker-compose.yml, start containers, verify health. Wrap report in <deploy-report>...</deploy-report> tags with status, containers, URLs, credentials, health.`,
3182
+ "verify": () => `Verify the epic's stories using the user docs and deploy info in ./story-files/. For each AC, derive verification steps, run commands, observe output. Include <verdict>pass</verdict> or <verdict>fail</verdict>. Include <evidence ac="N" status="pass|fail|unknown">...</evidence> per AC. Include <quality-scores>...</quality-scores>.`,
3183
+ "retro": () => `Run a retrospective for this epic. Analyze what worked, what failed, patterns, and action items for next epic.`
3184
+ };
3174
3185
  var FILE_WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
3175
3186
  "Write",
3176
3187
  "Edit",
@@ -3181,16 +3192,6 @@ var FILE_WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
3181
3192
  "WriteFile",
3182
3193
  "EditFile"
3183
3194
  ]);
3184
- var TASK_PROMPTS = {
3185
- "create-story": (key) => `Create the story spec for ${key}. Read the epic definitions and architecture docs. Write a complete story file with acceptance criteria, tasks, and dev notes. CRITICAL: Every AC must be testable by a blind QA agent using ONLY a user guide + browser/API/CLI access. No AC should reference source code, internal data structures, or implementation details like O(1) complexity. Each AC must describe observable behavior that can be verified through UI interaction (agent-browser), API calls (curl), CLI commands (docker exec), or log inspection (docker logs). Wrap output in <story-spec>...</story-spec> tags.`,
3186
- "implement": (key) => `Implement story ${key}`,
3187
- "check": (key) => `Run automated checks for story ${key}. Execute the project's test suite, linter, and coverage tool. Include <verdict>pass</verdict> or <verdict>fail</verdict> in your response.`,
3188
- "review": (key) => `Review the implementation of story ${key}. Check for correctness, security issues, architecture violations, and AC coverage. Include <verdict>pass</verdict> or <verdict>fail</verdict> in your response. If fail, include <issues>...</issues>.`,
3189
- "document": (key) => `Write user documentation for story ${key}. Describe what was built and how to use it from a user's perspective. No source code. Wrap documentation in <user-docs>...</user-docs> tags.`,
3190
- "deploy": () => `Provision the Docker environment for this project. Check for docker-compose.yml, start containers, verify health. Wrap report in <deploy-report>...</deploy-report> tags with status, containers, URLs, credentials, health.`,
3191
- "verify": () => `Verify the epic's stories using the user docs and deploy info in ./story-files/. For each AC, derive verification steps, run commands, observe output. Include <verdict>pass</verdict> or <verdict>fail</verdict>. Include <evidence ac="N" status="pass|fail|unknown">...</evidence> per AC. Include <quality-scores>...</quality-scores>.`,
3192
- "retro": () => `Run a retrospective for this epic. Analyze what worked, what failed, patterns, and action items for next epic.`
3193
- };
3194
3195
  function buildCoverageDeduplicationContext(contract, projectDir) {
3195
3196
  if (!contract?.testResults) return null;
3196
3197
  const { coverage } = contract.testResults;
@@ -3204,127 +3205,71 @@ function buildCoverageDeduplicationContext(contract, projectDir) {
3204
3205
  return null;
3205
3206
  }
3206
3207
  }
3207
- function propagateVerifyFlags(taskName, contract, projectDir) {
3208
- if (taskName !== "implement") return;
3209
- if (!contract?.testResults) return;
3210
- const { failed, coverage } = contract.testResults;
3211
- try {
3212
- const { state, body } = readStateWithBody(projectDir);
3213
- if (failed === 0) state.session_flags.tests_passed = true;
3214
- if (coverage !== null && coverage !== void 0 && coverage >= state.coverage.target) {
3215
- state.session_flags.coverage_met = true;
3216
- }
3217
- writeState(state, projectDir, body);
3218
- } catch (err) {
3219
- const msg = err instanceof Error ? err.message : String(err);
3220
- warn(`workflow-machine: flag propagation failed for ${taskName}: ${msg}`);
3221
- }
3222
- }
3223
- function isTaskCompleted(state, taskName, storyKey) {
3224
- return state.tasks_completed.some(
3225
- (cp) => cp.task_name === taskName && cp.story_key === storyKey && !cp.error
3226
- );
3227
- }
3228
- function isLoopTaskCompleted(state, taskName, storyKey, iteration) {
3229
- const count = state.tasks_completed.filter(
3230
- (cp) => cp.task_name === taskName && cp.story_key === storyKey && !cp.error
3231
- ).length;
3232
- return count >= iteration;
3233
- }
3234
- function loadWorkItems(sprintStatusPath, issuesPath2) {
3235
- const items = [];
3236
- if (existsSync15(sprintStatusPath)) {
3237
- let raw;
3238
- try {
3239
- raw = readFileSync13(sprintStatusPath, "utf-8");
3240
- } catch {
3241
- warn(`workflow-machine: could not read sprint-status.yaml at ${sprintStatusPath}`);
3242
- return items;
3243
- }
3244
- let parsed;
3245
- try {
3246
- parsed = parse5(raw);
3247
- } catch {
3248
- warn(`workflow-machine: invalid YAML in sprint-status.yaml at ${sprintStatusPath}`);
3249
- return items;
3250
- }
3251
- if (parsed && typeof parsed === "object") {
3252
- const data = parsed;
3253
- const devStatus = data.development_status;
3254
- if (devStatus && typeof devStatus === "object") {
3255
- for (const [key, status] of Object.entries(devStatus)) {
3256
- if (key.startsWith("epic-")) continue;
3257
- if (key.endsWith("-retrospective")) continue;
3258
- if (status === "backlog" || status === "ready-for-dev" || status === "in-progress") {
3259
- items.push({ key, source: "sprint" });
3260
- }
3261
- }
3262
- }
3263
- }
3208
+ async function nullTaskCore(input) {
3209
+ const { task: _task, taskName, storyKey, config, workflowState, previousContract, accumulatedCostUsd } = input;
3210
+ const projectDir = config.projectDir ?? process.cwd();
3211
+ const handler = getNullTask(taskName);
3212
+ if (!handler) {
3213
+ const registered = listNullTasks();
3214
+ throw { taskName, storyKey, code: "NULL_TASK_NOT_FOUND", message: `No null task handler registered for "${taskName}". Registered: ${registered.join(", ") || "(none)"}` };
3264
3215
  }
3265
- if (issuesPath2 && existsSync15(issuesPath2)) {
3266
- let raw;
3267
- try {
3268
- raw = readFileSync13(issuesPath2, "utf-8");
3269
- } catch {
3270
- warn(`workflow-machine: could not read issues.yaml at ${issuesPath2}`);
3271
- return items;
3272
- }
3273
- let parsed;
3274
- try {
3275
- parsed = parse5(raw);
3276
- } catch {
3277
- warn(`workflow-machine: invalid YAML in issues.yaml at ${issuesPath2}`);
3278
- return items;
3279
- }
3280
- if (parsed && typeof parsed === "object") {
3281
- const data = parsed;
3282
- const issuesList = data.issues;
3283
- if (Array.isArray(issuesList)) {
3284
- for (const issue of issuesList) {
3285
- if (issue && typeof issue === "object") {
3286
- const status = issue.status;
3287
- if (status === "backlog" || status === "ready-for-dev" || status === "in-progress") {
3288
- items.push({ key: issue.id, title: issue.title, source: "issues" });
3289
- }
3290
- }
3291
- }
3292
- }
3293
- }
3216
+ const startMs = Date.now();
3217
+ const workflowStartMs = workflowState.started ? new Date(workflowState.started).getTime() : startMs;
3218
+ const ctx = {
3219
+ storyKey,
3220
+ taskName,
3221
+ cost: accumulatedCostUsd,
3222
+ durationMs: startMs - workflowStartMs,
3223
+ outputContract: previousContract,
3224
+ projectDir
3225
+ };
3226
+ let result;
3227
+ try {
3228
+ result = await handler(ctx);
3229
+ } catch (err) {
3230
+ throw { taskName, storyKey, code: "NULL_TASK_HANDLER_ERROR", message: `Null task handler "${taskName}" threw: ${err instanceof Error ? err.message : String(err)}` };
3294
3231
  }
3295
- return items;
3296
- }
3297
- function buildRetryPrompt(storyKey, findings) {
3298
- const failedFindings = findings.filter((f) => f.status === "fail" || f.status === "unknown");
3299
- if (failedFindings.length === 0) return `Implement story ${storyKey}`;
3300
- const formatted = failedFindings.map((f) => {
3301
- let entry = `AC #${f.ac} (${f.status.toUpperCase()}): ${f.description}`;
3302
- if (f.evidence?.reasoning) entry += `
3303
- Evidence: ${f.evidence.reasoning}`;
3304
- return entry;
3305
- }).join("\n\n");
3306
- return `Retry story ${storyKey}. Previous evaluator findings:
3307
-
3308
- ${formatted}
3309
-
3310
- Focus on fixing the failed criteria above.`;
3311
- }
3312
- function buildAllUnknownVerdict(workItems, reasoning) {
3313
- const findings = workItems.map((_, index) => ({
3314
- ac: index + 1,
3315
- description: `AC #${index + 1}`,
3316
- status: "unknown",
3317
- evidence: { commands_run: [], output_observed: "", reasoning }
3318
- }));
3319
- return { verdict: "fail", score: { passed: 0, failed: 0, unknown: findings.length, total: findings.length }, findings };
3232
+ if (!result.success) {
3233
+ throw { taskName, storyKey, code: "NULL_TASK_FAILED", message: `Null task handler "${taskName}" returned success=false${result.output ? `: ${result.output}` : ""}` };
3234
+ }
3235
+ const checkpoint = { task_name: taskName, story_key: storyKey, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
3236
+ const updatedState = { ...workflowState, tasks_completed: [...workflowState.tasks_completed, checkpoint] };
3237
+ const durationMs = Date.now() - startMs;
3238
+ const contract = {
3239
+ version: 1,
3240
+ taskName,
3241
+ storyId: storyKey,
3242
+ driver: "engine",
3243
+ model: "null",
3244
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3245
+ cost_usd: 0,
3246
+ duration_ms: durationMs,
3247
+ changedFiles: [],
3248
+ testResults: null,
3249
+ output: result.output ?? "",
3250
+ acceptanceCriteria: []
3251
+ };
3252
+ writeWorkflowState(updatedState, projectDir);
3253
+ return { output: result.output ?? "", cost: 0, changedFiles: [], sessionId: "", contract, updatedState };
3320
3254
  }
3321
- function getFailedItems(verdict, allItems) {
3322
- if (!verdict) return allItems;
3323
- if (verdict.verdict === "pass") return [];
3324
- return allItems;
3255
+ function propagateVerifyFlags(taskName, contract, projectDir) {
3256
+ if (taskName !== "implement") return;
3257
+ if (!contract?.testResults) return;
3258
+ const { failed, coverage } = contract.testResults;
3259
+ try {
3260
+ const { state, body } = readStateWithBody(projectDir);
3261
+ if (failed === 0) state.session_flags.tests_passed = true;
3262
+ if (coverage !== null && coverage !== void 0 && coverage >= state.coverage.target) {
3263
+ state.session_flags.coverage_met = true;
3264
+ }
3265
+ writeState(state, projectDir, body);
3266
+ } catch (err) {
3267
+ const msg = err instanceof Error ? err.message : String(err);
3268
+ warn(`workflow-actors: flag propagation failed for ${taskName}: ${msg}`);
3269
+ }
3325
3270
  }
3326
3271
  async function dispatchTaskCore(input) {
3327
- const { task, taskName, storyKey, definition, config, workflowState, previousContract, onStreamEvent, storyFiles, customPrompt, accumulatedCostUsd } = input;
3272
+ const { task, taskName, storyKey, definition, config, workflowState, previousContract, onStreamEvent, storyFiles, customPrompt } = input;
3328
3273
  const projectDir = config.projectDir ?? process.cwd();
3329
3274
  const traceId = generateTraceId(config.runId, workflowState.iteration, taskName);
3330
3275
  const tracePrompt = formatTracePrompt(traceId);
@@ -3345,7 +3290,6 @@ async function dispatchTaskCore(input) {
3345
3290
  } else {
3346
3291
  cwd = projectDir;
3347
3292
  }
3348
- const isEpicSentinel = storyKey.startsWith("__epic_") || storyKey === "__run__";
3349
3293
  let basePrompt;
3350
3294
  if (customPrompt) {
3351
3295
  basePrompt = customPrompt;
@@ -3367,7 +3311,6 @@ ${coverageDedup}`;
3367
3311
  ...sessionId ? { sessionId } : {},
3368
3312
  ...tracePrompt ? { appendSystemPrompt: tracePrompt } : {},
3369
3313
  ...task.plugins ?? definition.plugins ? { plugins: task.plugins ?? definition.plugins } : {}
3370
- // Note: max_budget_usd is not mapped to timeout — they have different semantics
3371
3314
  };
3372
3315
  const emit = config.onEvent;
3373
3316
  if (emit) emit({ type: "dispatch-start", taskName, storyKey, driverName, model });
@@ -3430,11 +3373,7 @@ ${coverageDedup}`;
3430
3373
  if (resultSessionId) {
3431
3374
  updatedState = recordSessionId(sessionKey, resultSessionId, updatedState);
3432
3375
  } else {
3433
- const checkpoint = {
3434
- task_name: taskName,
3435
- story_key: storyKey,
3436
- completed_at: (/* @__PURE__ */ new Date()).toISOString()
3437
- };
3376
+ const checkpoint = { task_name: taskName, story_key: storyKey, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
3438
3377
  updatedState = { ...updatedState, tasks_completed: [...updatedState.tasks_completed, checkpoint] };
3439
3378
  }
3440
3379
  updatedState = recordTraceId(traceId, updatedState);
@@ -3458,70 +3397,129 @@ ${coverageDedup}`;
3458
3397
  writeOutputContract(contract, join12(projectDir, ".codeharness", "contracts"));
3459
3398
  } catch (err) {
3460
3399
  const msg = err instanceof Error ? err.message : String(err);
3461
- warn(`workflow-machine: failed to write output contract for ${taskName}/${storyKey}: ${msg}`);
3400
+ warn(`workflow-actors: failed to write output contract for ${taskName}/${storyKey}: ${msg}`);
3462
3401
  contract = null;
3463
3402
  }
3464
3403
  writeWorkflowState(updatedState, projectDir);
3465
3404
  propagateVerifyFlags(taskName, contract, projectDir);
3466
3405
  return { output, cost, changedFiles, sessionId: resultSessionId, contract, updatedState };
3467
3406
  }
3468
- async function nullTaskCore(input) {
3469
- const { task, taskName, storyKey, config, workflowState, previousContract, accumulatedCostUsd } = input;
3470
- const projectDir = config.projectDir ?? process.cwd();
3471
- const handler = getNullTask(taskName);
3472
- if (!handler) {
3473
- const registered = listNullTasks();
3474
- throw { taskName, storyKey, code: "NULL_TASK_NOT_FOUND", message: `No null task handler registered for "${taskName}". Registered: ${registered.join(", ") || "(none)"}` };
3475
- }
3476
- const startMs = Date.now();
3477
- const workflowStartMs = workflowState.started ? new Date(workflowState.started).getTime() : startMs;
3478
- const ctx = {
3479
- storyKey,
3480
- taskName,
3481
- cost: accumulatedCostUsd,
3482
- durationMs: startMs - workflowStartMs,
3483
- outputContract: previousContract,
3484
- projectDir
3485
- };
3486
- let result;
3487
- try {
3488
- result = await handler(ctx);
3489
- } catch (err) {
3490
- throw { taskName, storyKey, code: "NULL_TASK_HANDLER_ERROR", message: `Null task handler "${taskName}" threw: ${err instanceof Error ? err.message : String(err)}` };
3407
+ var dispatchActor = fromPromise(async ({ input }) => dispatchTaskCore(input));
3408
+ var nullTaskDispatchActor = fromPromise(async ({ input }) => {
3409
+ return nullTaskCore(input);
3410
+ });
3411
+
3412
+ // src/lib/workflow-machine.ts
3413
+ var HALT_ERROR_CODES = /* @__PURE__ */ new Set(["RATE_LIMIT", "NETWORK", "SDK_INIT"]);
3414
+ var DEFAULT_MAX_ITERATIONS = 5;
3415
+ var HEALTH_CHECK_TIMEOUT_MS = 5e3;
3416
+ function isTaskCompleted(state, taskName, storyKey) {
3417
+ return state.tasks_completed.some(
3418
+ (cp) => cp.task_name === taskName && cp.story_key === storyKey && !cp.error
3419
+ );
3420
+ }
3421
+ function isLoopTaskCompleted(state, taskName, storyKey, iteration) {
3422
+ const count = state.tasks_completed.filter(
3423
+ (cp) => cp.task_name === taskName && cp.story_key === storyKey && !cp.error
3424
+ ).length;
3425
+ return count >= iteration;
3426
+ }
3427
+ function loadWorkItems(sprintStatusPath, issuesPath2) {
3428
+ const items = [];
3429
+ if (existsSync15(sprintStatusPath)) {
3430
+ let raw;
3431
+ try {
3432
+ raw = readFileSync13(sprintStatusPath, "utf-8");
3433
+ } catch {
3434
+ warn(`workflow-machine: could not read sprint-status.yaml at ${sprintStatusPath}`);
3435
+ return items;
3436
+ }
3437
+ let parsed;
3438
+ try {
3439
+ parsed = parse5(raw);
3440
+ } catch {
3441
+ warn(`workflow-machine: invalid YAML in sprint-status.yaml at ${sprintStatusPath}`);
3442
+ return items;
3443
+ }
3444
+ if (parsed && typeof parsed === "object") {
3445
+ const data = parsed;
3446
+ const devStatus = data.development_status;
3447
+ if (devStatus && typeof devStatus === "object") {
3448
+ for (const [key, status] of Object.entries(devStatus)) {
3449
+ if (key.startsWith("epic-")) continue;
3450
+ if (key.endsWith("-retrospective")) continue;
3451
+ if (status === "backlog" || status === "ready-for-dev" || status === "in-progress") {
3452
+ items.push({ key, source: "sprint" });
3453
+ }
3454
+ }
3455
+ }
3456
+ }
3491
3457
  }
3492
- if (!result.success) {
3493
- throw { taskName, storyKey, code: "NULL_TASK_FAILED", message: `Null task handler "${taskName}" returned success=false${result.output ? `: ${result.output}` : ""}` };
3458
+ if (issuesPath2 && existsSync15(issuesPath2)) {
3459
+ let raw;
3460
+ try {
3461
+ raw = readFileSync13(issuesPath2, "utf-8");
3462
+ } catch {
3463
+ warn(`workflow-machine: could not read issues.yaml at ${issuesPath2}`);
3464
+ return items;
3465
+ }
3466
+ let parsed;
3467
+ try {
3468
+ parsed = parse5(raw);
3469
+ } catch {
3470
+ warn(`workflow-machine: invalid YAML in issues.yaml at ${issuesPath2}`);
3471
+ return items;
3472
+ }
3473
+ if (parsed && typeof parsed === "object") {
3474
+ const data = parsed;
3475
+ const issuesList = data.issues;
3476
+ if (Array.isArray(issuesList)) {
3477
+ for (const issue of issuesList) {
3478
+ if (issue && typeof issue === "object") {
3479
+ const status = issue.status;
3480
+ if (status === "backlog" || status === "ready-for-dev" || status === "in-progress") {
3481
+ items.push({ key: issue.id, title: issue.title, source: "issues" });
3482
+ }
3483
+ }
3484
+ }
3485
+ }
3486
+ }
3494
3487
  }
3495
- const checkpoint = { task_name: taskName, story_key: storyKey, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
3496
- const updatedState = { ...workflowState, tasks_completed: [...workflowState.tasks_completed, checkpoint] };
3497
- const durationMs = Date.now() - startMs;
3498
- const contract = {
3499
- version: 1,
3500
- taskName,
3501
- storyId: storyKey,
3502
- driver: "engine",
3503
- model: "null",
3504
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3505
- cost_usd: 0,
3506
- duration_ms: durationMs,
3507
- changedFiles: [],
3508
- testResults: null,
3509
- output: result.output ?? "",
3510
- acceptanceCriteria: []
3511
- };
3512
- writeWorkflowState(updatedState, projectDir);
3513
- return { output: result.output ?? "", cost: 0, changedFiles: [], sessionId: "", contract, updatedState };
3488
+ return items;
3489
+ }
3490
+ function buildRetryPrompt(storyKey, findings) {
3491
+ const failedFindings = findings.filter((f) => f.status === "fail" || f.status === "unknown");
3492
+ if (failedFindings.length === 0) return `Implement story ${storyKey}`;
3493
+ const formatted = failedFindings.map((f) => {
3494
+ let entry = `AC #${f.ac} (${f.status.toUpperCase()}): ${f.description}`;
3495
+ if (f.evidence?.reasoning) entry += `
3496
+ Evidence: ${f.evidence.reasoning}`;
3497
+ return entry;
3498
+ }).join("\n\n");
3499
+ return `Retry story ${storyKey}. Previous evaluator findings:
3500
+
3501
+ ${formatted}
3502
+
3503
+ Focus on fixing the failed criteria above.`;
3504
+ }
3505
+ function buildAllUnknownVerdict(workItems, reasoning) {
3506
+ const findings = workItems.map((_, index) => ({
3507
+ ac: index + 1,
3508
+ description: `AC #${index + 1}`,
3509
+ status: "unknown",
3510
+ evidence: { commands_run: [], output_observed: "", reasoning }
3511
+ }));
3512
+ return { verdict: "fail", score: { passed: 0, failed: 0, unknown: findings.length, total: findings.length }, findings };
3513
+ }
3514
+ function getFailedItems(verdict, allItems) {
3515
+ if (!verdict) return allItems;
3516
+ if (verdict.verdict === "pass") return [];
3517
+ return allItems;
3514
3518
  }
3515
3519
  function isLoopBlock(step) {
3516
3520
  return typeof step === "object" && step !== null && "loop" in step;
3517
3521
  }
3518
- var dispatchActor = fromPromise(async ({ input }) => {
3519
- return dispatchTaskCore(input);
3520
- });
3521
- var nullTaskDispatchActor = fromPromise(async ({ input }) => {
3522
- return nullTaskCore(input);
3523
- });
3524
- var loopIterationActor = fromPromise(async ({ input }) => {
3522
+ var loopIterationActor = fromPromise2(async ({ input }) => {
3525
3523
  const { loopBlock, config, workItems, storyFlowTasks, onStreamEvent, maxIterations } = input;
3526
3524
  let { currentState, errors, tasksCompleted, lastContract, lastVerdict, accumulatedCostUsd } = input;
3527
3525
  const projectDir = config.projectDir ?? process.cwd();
@@ -3762,7 +3760,7 @@ async function checkDriverHealth(workflow, timeoutMs) {
3762
3760
  }
3763
3761
  }
3764
3762
  function collectGuideFiles(epicItems, epicSentinel, projectDir) {
3765
- const guidesDir = join12(projectDir, ".codeharness", "verify-guides");
3763
+ const guidesDir = join13(projectDir, ".codeharness", "verify-guides");
3766
3764
  const guideFiles = [];
3767
3765
  try {
3768
3766
  mkdirSync6(guidesDir, { recursive: true });
@@ -3771,12 +3769,12 @@ function collectGuideFiles(epicItems, epicSentinel, projectDir) {
3771
3769
  }
3772
3770
  for (const item of epicItems) {
3773
3771
  try {
3774
- const contractPath = join12(projectDir, ".codeharness", "contracts", `document-${item.key}.json`);
3772
+ const contractPath = join13(projectDir, ".codeharness", "contracts", `document-${item.key}.json`);
3775
3773
  if (existsSync15(contractPath)) {
3776
3774
  const contractData = JSON.parse(readFileSync13(contractPath, "utf-8"));
3777
3775
  const docs = contractData.output ? extractTag(contractData.output, "user-docs") ?? contractData.output : null;
3778
3776
  if (docs) {
3779
- const guidePath = join12(guidesDir, `${item.key}-guide.md`);
3777
+ const guidePath = join13(guidesDir, `${item.key}-guide.md`);
3780
3778
  writeFileSync8(guidePath, docs, "utf-8");
3781
3779
  guideFiles.push(guidePath);
3782
3780
  }
@@ -3785,12 +3783,12 @@ function collectGuideFiles(epicItems, epicSentinel, projectDir) {
3785
3783
  }
3786
3784
  }
3787
3785
  try {
3788
- const deployContractPath = join12(projectDir, ".codeharness", "contracts", `deploy-${epicSentinel}.json`);
3786
+ const deployContractPath = join13(projectDir, ".codeharness", "contracts", `deploy-${epicSentinel}.json`);
3789
3787
  if (existsSync15(deployContractPath)) {
3790
3788
  const deployData = JSON.parse(readFileSync13(deployContractPath, "utf-8"));
3791
3789
  const report = deployData.output ? extractTag(deployData.output, "deploy-report") ?? deployData.output : null;
3792
3790
  if (report) {
3793
- const deployPath = join12(guidesDir, "deploy-info.md");
3791
+ const deployPath = join13(guidesDir, "deploy-info.md");
3794
3792
  writeFileSync8(deployPath, report, "utf-8");
3795
3793
  guideFiles.push(deployPath);
3796
3794
  }
@@ -3800,13 +3798,13 @@ function collectGuideFiles(epicItems, epicSentinel, projectDir) {
3800
3798
  return guideFiles;
3801
3799
  }
3802
3800
  function cleanupGuideFiles(projectDir) {
3803
- const guidesDir = join12(projectDir, ".codeharness", "verify-guides");
3801
+ const guidesDir = join13(projectDir, ".codeharness", "verify-guides");
3804
3802
  try {
3805
3803
  rmSync2(guidesDir, { recursive: true, force: true });
3806
3804
  } catch {
3807
3805
  }
3808
3806
  }
3809
- var storyFlowActor = fromPromise(async ({ input }) => {
3807
+ var storyFlowActor = fromPromise2(async ({ input }) => {
3810
3808
  const { item, config, storyFlowTasks } = input;
3811
3809
  let { workflowState: state, lastContract, accumulatedCostUsd } = input;
3812
3810
  const projectDir = config.projectDir ?? process.cwd();
@@ -3880,7 +3878,7 @@ var storyFlowActor = fromPromise(async ({ input }) => {
3880
3878
  }
3881
3879
  return { workflowState: state, errors, tasksCompleted, lastContract, accumulatedCostUsd, halted };
3882
3880
  });
3883
- var epicStepActor = fromPromise(async ({ input }) => {
3881
+ var epicStepActor = fromPromise2(async ({ input }) => {
3884
3882
  const { epicId, epicItems, config, storyFlowTasks } = input;
3885
3883
  let { workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted, currentStepIndex } = input;
3886
3884
  const projectDir = config.projectDir ?? process.cwd();
@@ -4014,7 +4012,7 @@ var epicMachine = setup({
4014
4012
  done: { type: "final" }
4015
4013
  }
4016
4014
  });
4017
- var runEpicActor = fromPromise(async ({ input }) => {
4015
+ var runEpicActor = fromPromise2(async ({ input }) => {
4018
4016
  const { config, storyFlowTasks, epicEntries, currentEpicIndex } = input;
4019
4017
  let { workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted } = input;
4020
4018
  if (currentEpicIndex >= epicEntries.length || halted || config.abortSignal?.aborted) {
@@ -4194,12 +4192,12 @@ function handleDispatchError(err, taskName, storyKey) {
4194
4192
  // src/lib/worktree-manager.ts
4195
4193
  import { execSync as execSync2 } from "child_process";
4196
4194
  import { existsSync as existsSync16, readFileSync as readFileSync14, statSync } from "fs";
4197
- import { join as join14 } from "path";
4195
+ import { join as join15 } from "path";
4198
4196
 
4199
4197
  // src/lib/cross-worktree-validator.ts
4200
4198
  import { exec } from "child_process";
4201
4199
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync7 } from "fs";
4202
- import { join as join13 } from "path";
4200
+ import { join as join14 } from "path";
4203
4201
  import { promisify } from "util";
4204
4202
  var execAsync = promisify(exec);
4205
4203
  var MAX_BUFFER = 10 * 1024 * 1024;
@@ -4233,9 +4231,9 @@ function writeMergeTelemetry(opts, result) {
4233
4231
  testResults: result.testResults,
4234
4232
  errors: result.valid ? [] : ["Test suite failed after merge"]
4235
4233
  };
4236
- const dir = join13(opts.cwd, TELEMETRY_DIR2);
4234
+ const dir = join14(opts.cwd, TELEMETRY_DIR2);
4237
4235
  mkdirSync7(dir, { recursive: true });
4238
- appendFileSync2(join13(dir, TELEMETRY_FILE2), JSON.stringify(entry) + "\n");
4236
+ appendFileSync2(join14(dir, TELEMETRY_FILE2), JSON.stringify(entry) + "\n");
4239
4237
  } catch {
4240
4238
  }
4241
4239
  }
@@ -4716,7 +4714,7 @@ var WorktreeManager = class {
4716
4714
  * Check if a worktree is orphaned (no active codeharness process).
4717
4715
  */
4718
4716
  isOrphaned(wt) {
4719
- const laneStatePath = join14(wt.path, ".codeharness", "lane-state.json");
4717
+ const laneStatePath = join15(wt.path, ".codeharness", "lane-state.json");
4720
4718
  if (!existsSync16(laneStatePath)) {
4721
4719
  return true;
4722
4720
  }
@@ -6069,7 +6067,7 @@ function startRenderer(options) {
6069
6067
 
6070
6068
  // src/commands/run.ts
6071
6069
  function resolvePluginDir() {
6072
- return join15(process.cwd(), ".claude");
6070
+ return join16(process.cwd(), ".claude");
6073
6071
  }
6074
6072
  function extractEpicId2(storyKey) {
6075
6073
  const match = storyKey.match(/^(\d+)-/);
@@ -6161,8 +6159,8 @@ function registerRunCommand(program) {
6161
6159
  process.exitCode = 1;
6162
6160
  return;
6163
6161
  }
6164
- const projectWorkflowPath = join15(projectDir, ".codeharness", "workflows", "default.yaml");
6165
- const templateWorkflowPath = join15(projectDir, "templates", "workflows", "default.yaml");
6162
+ const projectWorkflowPath = join16(projectDir, ".codeharness", "workflows", "default.yaml");
6163
+ const templateWorkflowPath = join16(projectDir, "templates", "workflows", "default.yaml");
6166
6164
  const workflowPath = existsSync17(projectWorkflowPath) ? projectWorkflowPath : templateWorkflowPath;
6167
6165
  try {
6168
6166
  parsedWorkflow = parseWorkflow(workflowPath);
@@ -6420,8 +6418,8 @@ function registerRunCommand(program) {
6420
6418
  const config = {
6421
6419
  workflow: parsedWorkflow,
6422
6420
  agents,
6423
- sprintStatusPath: join15(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml"),
6424
- issuesPath: join15(projectDir, ".codeharness", "issues.yaml"),
6421
+ sprintStatusPath: join16(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml"),
6422
+ issuesPath: join16(projectDir, ".codeharness", "issues.yaml"),
6425
6423
  runId: `run-${Date.now()}`,
6426
6424
  projectDir,
6427
6425
  abortSignal: abortController.signal,
@@ -6533,7 +6531,7 @@ function registerRunCommand(program) {
6533
6531
 
6534
6532
  // src/commands/verify.ts
6535
6533
  import { existsSync as existsSync28, readFileSync as readFileSync25 } from "fs";
6536
- import { join as join28 } from "path";
6534
+ import { join as join29 } from "path";
6537
6535
 
6538
6536
  // src/modules/verify/index.ts
6539
6537
  import { readFileSync as readFileSync24 } from "fs";
@@ -6770,11 +6768,11 @@ function validateProofQuality(proofPath) {
6770
6768
  // src/modules/verify/orchestrator.ts
6771
6769
  import { execFileSync } from "child_process";
6772
6770
  import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
6773
- import { join as join20 } from "path";
6771
+ import { join as join21 } from "path";
6774
6772
 
6775
6773
  // src/lib/doc-health/types.ts
6776
6774
  import { readdirSync as readdirSync3, statSync as statSync2 } from "fs";
6777
- import { join as join16 } from "path";
6775
+ import { join as join17 } from "path";
6778
6776
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
6779
6777
  function getExtension(filename) {
6780
6778
  const dot = filename.lastIndexOf(".");
@@ -6795,7 +6793,7 @@ function getNewestSourceMtime(dir) {
6795
6793
  const dirName = current.split("/").pop() ?? "";
6796
6794
  if (dirName === "node_modules" || dirName === ".git") return;
6797
6795
  for (const entry of entries) {
6798
- const fullPath = join16(current, entry);
6796
+ const fullPath = join17(current, entry);
6799
6797
  let stat;
6800
6798
  try {
6801
6799
  stat = statSync2(fullPath);
@@ -6827,7 +6825,7 @@ import {
6827
6825
  readdirSync as readdirSync5,
6828
6826
  statSync as statSync4
6829
6827
  } from "fs";
6830
- import { join as join18, relative as relative2 } from "path";
6828
+ import { join as join19, relative as relative2 } from "path";
6831
6829
 
6832
6830
  // src/lib/doc-health/staleness.ts
6833
6831
  import { execSync as execSync3 } from "child_process";
@@ -6837,7 +6835,7 @@ import {
6837
6835
  readdirSync as readdirSync4,
6838
6836
  statSync as statSync3
6839
6837
  } from "fs";
6840
- import { join as join17, relative } from "path";
6838
+ import { join as join18, relative } from "path";
6841
6839
  var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
6842
6840
  var DO_NOT_EDIT_HEADER = "<!-- DO NOT EDIT MANUALLY";
6843
6841
  function getSourceFilesInModule(modulePath) {
@@ -6852,7 +6850,7 @@ function getSourceFilesInModule(modulePath) {
6852
6850
  const dirName = current.split("/").pop() ?? "";
6853
6851
  if (dirName === "node_modules" || dirName === ".git" || dirName === "__tests__" || dirName === "dist" || dirName === "coverage" || dirName.startsWith(".") && current !== modulePath) return;
6854
6852
  for (const entry of entries) {
6855
- const fullPath = join17(current, entry);
6853
+ const fullPath = join18(current, entry);
6856
6854
  let stat;
6857
6855
  try {
6858
6856
  stat = statSync3(fullPath);
@@ -6898,10 +6896,10 @@ function checkAgentsMdCompleteness(agentsPath, modulePath) {
6898
6896
  }
6899
6897
  function checkAgentsMdForModule(modulePath, dir) {
6900
6898
  const root = dir ?? process.cwd();
6901
- const fullModulePath = join17(root, modulePath);
6902
- let agentsPath = join17(fullModulePath, "AGENTS.md");
6899
+ const fullModulePath = join18(root, modulePath);
6900
+ let agentsPath = join18(fullModulePath, "AGENTS.md");
6903
6901
  if (!existsSync19(agentsPath)) {
6904
- agentsPath = join17(root, "AGENTS.md");
6902
+ agentsPath = join18(root, "AGENTS.md");
6905
6903
  }
6906
6904
  if (!existsSync19(agentsPath)) {
6907
6905
  return {
@@ -6966,14 +6964,14 @@ function checkStoryDocFreshness(storyId, dir) {
6966
6964
  for (const mod of modulesToCheck) {
6967
6965
  const result = checkAgentsMdForModule(mod, root);
6968
6966
  documents.push(result);
6969
- const moduleAgentsPath = join17(root, mod, "AGENTS.md");
6970
- const actualAgentsPath = existsSync19(moduleAgentsPath) ? moduleAgentsPath : join17(root, "AGENTS.md");
6967
+ const moduleAgentsPath = join18(root, mod, "AGENTS.md");
6968
+ const actualAgentsPath = existsSync19(moduleAgentsPath) ? moduleAgentsPath : join18(root, "AGENTS.md");
6971
6969
  if (existsSync19(actualAgentsPath)) {
6972
6970
  checkAgentsMdLineCountInternal(actualAgentsPath, result.path, documents);
6973
6971
  }
6974
6972
  }
6975
6973
  if (modulesToCheck.length === 0) {
6976
- const rootAgentsPath = join17(root, "AGENTS.md");
6974
+ const rootAgentsPath = join18(root, "AGENTS.md");
6977
6975
  if (existsSync19(rootAgentsPath)) {
6978
6976
  documents.push({
6979
6977
  path: "AGENTS.md",
@@ -7048,7 +7046,7 @@ function findModules(dir, threshold) {
7048
7046
  let sourceCount = 0;
7049
7047
  const subdirs = [];
7050
7048
  for (const entry of entries) {
7051
- const fullPath = join18(current, entry);
7049
+ const fullPath = join19(current, entry);
7052
7050
  let stat;
7053
7051
  try {
7054
7052
  stat = statSync4(fullPath);
@@ -7082,7 +7080,7 @@ function scanDocHealth(dir) {
7082
7080
  const root = dir ?? process.cwd();
7083
7081
  const documents = [];
7084
7082
  const modules = findModules(root);
7085
- const rootAgentsPath = join18(root, "AGENTS.md");
7083
+ const rootAgentsPath = join19(root, "AGENTS.md");
7086
7084
  if (existsSync20(rootAgentsPath)) {
7087
7085
  if (modules.length > 0) {
7088
7086
  const docMtime = statSync4(rootAgentsPath).mtime;
@@ -7090,8 +7088,8 @@ function scanDocHealth(dir) {
7090
7088
  let staleModule = "";
7091
7089
  let newestCode = null;
7092
7090
  for (const mod of modules) {
7093
- const fullModPath = join18(root, mod);
7094
- const modAgentsPath = join18(fullModPath, "AGENTS.md");
7091
+ const fullModPath = join19(root, mod);
7092
+ const modAgentsPath = join19(fullModPath, "AGENTS.md");
7095
7093
  if (existsSync20(modAgentsPath)) continue;
7096
7094
  const { missing } = checkAgentsMdCompleteness(rootAgentsPath, fullModPath);
7097
7095
  if (missing.length > 0 && staleModule === "") {
@@ -7140,7 +7138,7 @@ function scanDocHealth(dir) {
7140
7138
  });
7141
7139
  }
7142
7140
  for (const mod of modules) {
7143
- const modAgentsPath = join18(root, mod, "AGENTS.md");
7141
+ const modAgentsPath = join19(root, mod, "AGENTS.md");
7144
7142
  if (existsSync20(modAgentsPath)) {
7145
7143
  const result = checkAgentsMdForModule(mod, root);
7146
7144
  if (result.path !== "AGENTS.md") {
@@ -7149,7 +7147,7 @@ function scanDocHealth(dir) {
7149
7147
  }
7150
7148
  }
7151
7149
  }
7152
- const indexPath = join18(root, "docs", "index.md");
7150
+ const indexPath = join19(root, "docs", "index.md");
7153
7151
  if (existsSync20(indexPath)) {
7154
7152
  const content = readFileSync17(indexPath, "utf-8");
7155
7153
  const hasAbsolutePaths = /https?:\/\/|file:\/\//i.test(content);
@@ -7161,11 +7159,11 @@ function scanDocHealth(dir) {
7161
7159
  reason: hasAbsolutePaths ? "Contains absolute URLs (may violate NFR25)" : "Uses relative paths"
7162
7160
  });
7163
7161
  }
7164
- const activeDir = join18(root, "docs", "exec-plans", "active");
7162
+ const activeDir = join19(root, "docs", "exec-plans", "active");
7165
7163
  if (existsSync20(activeDir)) {
7166
7164
  const files = readdirSync5(activeDir).filter((f) => f.endsWith(".md"));
7167
7165
  for (const file of files) {
7168
- const filePath = join18(activeDir, file);
7166
+ const filePath = join19(activeDir, file);
7169
7167
  documents.push({
7170
7168
  path: `docs/exec-plans/active/${file}`,
7171
7169
  grade: "fresh",
@@ -7176,11 +7174,11 @@ function scanDocHealth(dir) {
7176
7174
  }
7177
7175
  }
7178
7176
  for (const subdir of ["quality", "generated"]) {
7179
- const dirPath = join18(root, "docs", subdir);
7177
+ const dirPath = join19(root, "docs", subdir);
7180
7178
  if (!existsSync20(dirPath)) continue;
7181
7179
  const files = readdirSync5(dirPath).filter((f) => !f.startsWith("."));
7182
7180
  for (const file of files) {
7183
- const filePath = join18(dirPath, file);
7181
+ const filePath = join19(dirPath, file);
7184
7182
  let stat;
7185
7183
  try {
7186
7184
  stat = statSync4(filePath);
@@ -7236,7 +7234,7 @@ import {
7236
7234
  unlinkSync as unlinkSync2,
7237
7235
  writeFileSync as writeFileSync9
7238
7236
  } from "fs";
7239
- import { join as join19 } from "path";
7237
+ import { join as join20 } from "path";
7240
7238
  function printDocHealthOutput(report) {
7241
7239
  for (const doc of report.documents) {
7242
7240
  switch (doc.grade) {
@@ -7257,7 +7255,7 @@ function printDocHealthOutput(report) {
7257
7255
  }
7258
7256
  function completeExecPlan(storyId, dir) {
7259
7257
  const root = dir ?? process.cwd();
7260
- const activePath = join19(root, "docs", "exec-plans", "active", `${storyId}.md`);
7258
+ const activePath = join20(root, "docs", "exec-plans", "active", `${storyId}.md`);
7261
7259
  if (!existsSync21(activePath)) {
7262
7260
  return null;
7263
7261
  }
@@ -7269,9 +7267,9 @@ function completeExecPlan(storyId, dir) {
7269
7267
  `$1
7270
7268
  Completed: ${timestamp}`
7271
7269
  );
7272
- const completedDir = join19(root, "docs", "exec-plans", "completed");
7270
+ const completedDir = join20(root, "docs", "exec-plans", "completed");
7273
7271
  mkdirSync8(completedDir, { recursive: true });
7274
- const completedPath = join19(completedDir, `${storyId}.md`);
7272
+ const completedPath = join20(completedDir, `${storyId}.md`);
7275
7273
  writeFileSync9(completedPath, content, "utf-8");
7276
7274
  try {
7277
7275
  unlinkSync2(activePath);
@@ -7313,9 +7311,9 @@ function checkPreconditions(dir, storyId) {
7313
7311
  }
7314
7312
  function createProofDocument(storyId, _storyTitle, _acs, dir) {
7315
7313
  const root = dir ?? process.cwd();
7316
- const verificationDir = join20(root, "verification");
7314
+ const verificationDir = join21(root, "verification");
7317
7315
  mkdirSync9(verificationDir, { recursive: true });
7318
- const proofPath = join20(verificationDir, `${storyId}-proof.md`);
7316
+ const proofPath = join21(verificationDir, `${storyId}-proof.md`);
7319
7317
  writeFileSync10(proofPath, `# ${storyId} \u2014 Proof
7320
7318
 
7321
7319
  Pending: blind evaluator (Epic 6)
@@ -7487,7 +7485,7 @@ function parseObservabilityGaps(proofContent) {
7487
7485
 
7488
7486
  // src/modules/observability/analyzer.ts
7489
7487
  import { execFileSync as execFileSync2 } from "child_process";
7490
- import { join as join21 } from "path";
7488
+ import { join as join22 } from "path";
7491
7489
  var DEFAULT_RULES_DIR = "patches/observability/";
7492
7490
  var ADDITIONAL_RULES_DIRS = ["patches/error-handling/"];
7493
7491
  var DEFAULT_TIMEOUT = 6e4;
@@ -7522,8 +7520,8 @@ function analyze(projectDir, config) {
7522
7520
  }
7523
7521
  const rulesDir = config?.rulesDir ?? DEFAULT_RULES_DIR;
7524
7522
  const timeout = config?.timeout ?? DEFAULT_TIMEOUT;
7525
- const fullRulesDir = join21(projectDir, rulesDir);
7526
- const additionalDirs = (config?.additionalRulesDirs ?? ADDITIONAL_RULES_DIRS).map((d) => join21(projectDir, d));
7523
+ const fullRulesDir = join22(projectDir, rulesDir);
7524
+ const additionalDirs = (config?.additionalRulesDirs ?? ADDITIONAL_RULES_DIRS).map((d) => join22(projectDir, d));
7527
7525
  const rawResult = runSemgrep(projectDir, fullRulesDir, timeout, additionalDirs);
7528
7526
  if (!rawResult.success) {
7529
7527
  return fail2(rawResult.error);
@@ -7612,7 +7610,7 @@ function normalizeSeverity(severity) {
7612
7610
 
7613
7611
  // src/modules/observability/coverage.ts
7614
7612
  import { readFileSync as readFileSync20, writeFileSync as writeFileSync11, renameSync as renameSync3, existsSync as existsSync24 } from "fs";
7615
- import { join as join22 } from "path";
7613
+ import { join as join23 } from "path";
7616
7614
  var STATE_FILE2 = "sprint-state.json";
7617
7615
  var DEFAULT_STATIC_TARGET = 80;
7618
7616
  function defaultCoverageState() {
@@ -7628,7 +7626,7 @@ function defaultCoverageState() {
7628
7626
  };
7629
7627
  }
7630
7628
  function readStateFile(projectDir) {
7631
- const fp = join22(projectDir, STATE_FILE2);
7629
+ const fp = join23(projectDir, STATE_FILE2);
7632
7630
  if (!existsSync24(fp)) {
7633
7631
  return ok2({});
7634
7632
  }
@@ -7701,7 +7699,7 @@ function parseGapArray(raw) {
7701
7699
 
7702
7700
  // src/modules/observability/runtime-coverage.ts
7703
7701
  import { readFileSync as readFileSync21, writeFileSync as writeFileSync12, renameSync as renameSync4, existsSync as existsSync25 } from "fs";
7704
- import { join as join23 } from "path";
7702
+ import { join as join24 } from "path";
7705
7703
 
7706
7704
  // src/modules/observability/coverage-gate.ts
7707
7705
  var DEFAULT_STATIC_TARGET2 = 80;
@@ -7744,7 +7742,7 @@ function checkObservabilityCoverageGate(projectDir, overrides) {
7744
7742
  // src/modules/observability/runtime-validator.ts
7745
7743
  import { execSync as execSync4 } from "child_process";
7746
7744
  import { readdirSync as readdirSync6, statSync as statSync5 } from "fs";
7747
- import { join as join24 } from "path";
7745
+ import { join as join25 } from "path";
7748
7746
  var DEFAULT_CONFIG = {
7749
7747
  testCommand: "npm test",
7750
7748
  otlpEndpoint: "http://localhost:4318",
@@ -7871,11 +7869,11 @@ function mapEventsToModules(events, projectDir, modules) {
7871
7869
  });
7872
7870
  }
7873
7871
  function discoverModules(projectDir) {
7874
- const srcDir = join24(projectDir, "src");
7872
+ const srcDir = join25(projectDir, "src");
7875
7873
  try {
7876
7874
  return readdirSync6(srcDir).filter((name) => {
7877
7875
  try {
7878
- return statSync5(join24(srcDir, name)).isDirectory();
7876
+ return statSync5(join25(srcDir, name)).isDirectory();
7879
7877
  } catch {
7880
7878
  return false;
7881
7879
  }
@@ -8543,7 +8541,7 @@ function getACById(id) {
8543
8541
  // src/modules/verify/validation-runner.ts
8544
8542
  import { execSync as execSync5 } from "child_process";
8545
8543
  import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync10 } from "fs";
8546
- import { join as join25, dirname as dirname3 } from "path";
8544
+ import { join as join26, dirname as dirname3 } from "path";
8547
8545
  var MAX_VALIDATION_ATTEMPTS = 10;
8548
8546
  var AC_COMMAND_TIMEOUT_MS = 3e4;
8549
8547
  var VAL_KEY_PREFIX = "val-";
@@ -8652,7 +8650,7 @@ function executeValidationAC(ac) {
8652
8650
  function createFixStory(ac, error) {
8653
8651
  try {
8654
8652
  const storyKey = `val-fix-${ac.id}`;
8655
- const storyPath = join25(
8653
+ const storyPath = join26(
8656
8654
  process.cwd(),
8657
8655
  "_bmad-output",
8658
8656
  "implementation-artifacts",
@@ -9023,11 +9021,11 @@ function runValidationCycle() {
9023
9021
  // src/modules/verify/env.ts
9024
9022
  import { execFileSync as execFileSync5 } from "child_process";
9025
9023
  import { existsSync as existsSync27, mkdirSync as mkdirSync11, readdirSync as readdirSync7, readFileSync as readFileSync23, writeFileSync as writeFileSync14, cpSync, rmSync as rmSync3, statSync as statSync6 } from "fs";
9026
- import { join as join27, basename as basename2 } from "path";
9024
+ import { join as join28, basename as basename2 } from "path";
9027
9025
  import { createHash } from "crypto";
9028
9026
 
9029
9027
  // src/modules/verify/dockerfile-generator.ts
9030
- import { join as join26 } from "path";
9028
+ import { join as join27 } from "path";
9031
9029
  function generateVerifyDockerfile(projectDir) {
9032
9030
  const detections = detectStacks(projectDir);
9033
9031
  const sections = [];
@@ -9047,7 +9045,7 @@ function generateVerifyDockerfile(projectDir) {
9047
9045
  for (const detection of detections) {
9048
9046
  const provider = getStackProvider(detection.stack);
9049
9047
  if (!provider) continue;
9050
- const resolvedDir = detection.dir === "." ? projectDir : join26(projectDir, detection.dir);
9048
+ const resolvedDir = detection.dir === "." ? projectDir : join27(projectDir, detection.dir);
9051
9049
  const section = provider.getVerifyDockerfileSection(resolvedDir);
9052
9050
  if (section) {
9053
9051
  sections.push(section);
@@ -9076,7 +9074,7 @@ function isValidStoryKey(storyKey) {
9076
9074
  return /^[a-zA-Z0-9_-]+$/.test(storyKey);
9077
9075
  }
9078
9076
  function computeDistHash(projectDir) {
9079
- const distDir = join27(projectDir, "dist");
9077
+ const distDir = join28(projectDir, "dist");
9080
9078
  if (!existsSync27(distDir)) return null;
9081
9079
  const hash = createHash("sha256");
9082
9080
  const files = collectFiles(distDir).sort();
@@ -9089,7 +9087,7 @@ function computeDistHash(projectDir) {
9089
9087
  function collectFiles(dir) {
9090
9088
  const results = [];
9091
9089
  for (const entry of readdirSync7(dir, { withFileTypes: true })) {
9092
- const fullPath = join27(dir, entry.name);
9090
+ const fullPath = join28(dir, entry.name);
9093
9091
  if (entry.isDirectory()) {
9094
9092
  results.push(...collectFiles(fullPath));
9095
9093
  } else {
@@ -9125,7 +9123,7 @@ function detectProjectType(projectDir) {
9125
9123
  const rootDetection = allStacks.find((s) => s.dir === ".");
9126
9124
  const stack = rootDetection ? rootDetection.stack : null;
9127
9125
  if (stack && STACK_TO_PROJECT_TYPE[stack]) return STACK_TO_PROJECT_TYPE[stack];
9128
- if (existsSync27(join27(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
9126
+ if (existsSync27(join28(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
9129
9127
  return "generic";
9130
9128
  }
9131
9129
  function buildVerifyImage(options = {}) {
@@ -9169,18 +9167,18 @@ function buildNodeImage(projectDir) {
9169
9167
  const lastLine = packOutput.split("\n").pop()?.trim();
9170
9168
  if (!lastLine) throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
9171
9169
  const tarballName = basename2(lastLine);
9172
- const tarballPath = join27("/tmp", tarballName);
9173
- const buildContext = join27("/tmp", `codeharness-verify-build-${Date.now()}`);
9170
+ const tarballPath = join28("/tmp", tarballName);
9171
+ const buildContext = join28("/tmp", `codeharness-verify-build-${Date.now()}`);
9174
9172
  mkdirSync11(buildContext, { recursive: true });
9175
9173
  try {
9176
- cpSync(tarballPath, join27(buildContext, tarballName));
9174
+ cpSync(tarballPath, join28(buildContext, tarballName));
9177
9175
  const dockerfile = generateVerifyDockerfile(projectDir) + `
9178
9176
  # Install project from tarball
9179
9177
  ARG TARBALL=package.tgz
9180
9178
  COPY \${TARBALL} /tmp/\${TARBALL}
9181
9179
  RUN npm install -g /tmp/\${TARBALL} && rm /tmp/\${TARBALL}
9182
9180
  `;
9183
- writeFileSync14(join27(buildContext, "Dockerfile"), dockerfile);
9181
+ writeFileSync14(join28(buildContext, "Dockerfile"), dockerfile);
9184
9182
  execFileSync5("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${tarballName}`, "."], {
9185
9183
  cwd: buildContext,
9186
9184
  stdio: "pipe",
@@ -9192,22 +9190,22 @@ RUN npm install -g /tmp/\${TARBALL} && rm /tmp/\${TARBALL}
9192
9190
  }
9193
9191
  }
9194
9192
  function buildPythonImage(projectDir) {
9195
- const distDir = join27(projectDir, "dist");
9193
+ const distDir = join28(projectDir, "dist");
9196
9194
  const distFiles = readdirSync7(distDir).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".whl"));
9197
9195
  if (distFiles.length === 0) {
9198
9196
  throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
9199
9197
  }
9200
9198
  const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
9201
- const buildContext = join27("/tmp", `codeharness-verify-build-${Date.now()}`);
9199
+ const buildContext = join28("/tmp", `codeharness-verify-build-${Date.now()}`);
9202
9200
  mkdirSync11(buildContext, { recursive: true });
9203
9201
  try {
9204
- cpSync(join27(distDir, distFile), join27(buildContext, distFile));
9202
+ cpSync(join28(distDir, distFile), join28(buildContext, distFile));
9205
9203
  const dockerfile = generateVerifyDockerfile(projectDir) + `
9206
9204
  # Install project from distribution
9207
9205
  COPY ${distFile} /tmp/${distFile}
9208
9206
  RUN pip install --break-system-packages /tmp/${distFile} && rm /tmp/${distFile}
9209
9207
  `;
9210
- writeFileSync14(join27(buildContext, "Dockerfile"), dockerfile);
9208
+ writeFileSync14(join28(buildContext, "Dockerfile"), dockerfile);
9211
9209
  execFileSync5("docker", ["build", "-t", IMAGE_TAG, "."], {
9212
9210
  cwd: buildContext,
9213
9211
  stdio: "pipe",
@@ -9222,19 +9220,19 @@ function prepareVerifyWorkspace(storyKey, projectDir) {
9222
9220
  if (!isValidStoryKey(storyKey)) {
9223
9221
  throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
9224
9222
  }
9225
- const storyFile = join27(root, STORY_DIR, `${storyKey}.md`);
9223
+ const storyFile = join28(root, STORY_DIR, `${storyKey}.md`);
9226
9224
  if (!existsSync27(storyFile)) throw new Error(`Story file not found: ${storyFile}`);
9227
9225
  const workspace = `${TEMP_PREFIX}${storyKey}`;
9228
9226
  if (existsSync27(workspace)) rmSync3(workspace, { recursive: true, force: true });
9229
9227
  mkdirSync11(workspace, { recursive: true });
9230
- cpSync(storyFile, join27(workspace, "story.md"));
9231
- const readmePath = join27(root, "README.md");
9232
- if (existsSync27(readmePath)) cpSync(readmePath, join27(workspace, "README.md"));
9233
- const docsDir = join27(root, "docs");
9228
+ cpSync(storyFile, join28(workspace, "story.md"));
9229
+ const readmePath = join28(root, "README.md");
9230
+ if (existsSync27(readmePath)) cpSync(readmePath, join28(workspace, "README.md"));
9231
+ const docsDir = join28(root, "docs");
9234
9232
  if (existsSync27(docsDir) && statSync6(docsDir).isDirectory()) {
9235
- cpSync(docsDir, join27(workspace, "docs"), { recursive: true });
9233
+ cpSync(docsDir, join28(workspace, "docs"), { recursive: true });
9236
9234
  }
9237
- mkdirSync11(join27(workspace, "verification"), { recursive: true });
9235
+ mkdirSync11(join28(workspace, "verification"), { recursive: true });
9238
9236
  return workspace;
9239
9237
  }
9240
9238
  function checkVerifyEnv() {
@@ -9287,18 +9285,18 @@ function cleanupVerifyEnv(storyKey) {
9287
9285
  }
9288
9286
  }
9289
9287
  function buildPluginImage(projectDir) {
9290
- const buildContext = join27("/tmp", `codeharness-verify-build-${Date.now()}`);
9288
+ const buildContext = join28("/tmp", `codeharness-verify-build-${Date.now()}`);
9291
9289
  mkdirSync11(buildContext, { recursive: true });
9292
9290
  try {
9293
- const pluginDir = join27(projectDir, ".claude-plugin");
9294
- cpSync(pluginDir, join27(buildContext, ".claude-plugin"), { recursive: true });
9291
+ const pluginDir = join28(projectDir, ".claude-plugin");
9292
+ cpSync(pluginDir, join28(buildContext, ".claude-plugin"), { recursive: true });
9295
9293
  for (const dir of ["commands", "hooks", "knowledge", "skills"]) {
9296
- const src = join27(projectDir, dir);
9294
+ const src = join28(projectDir, dir);
9297
9295
  if (existsSync27(src) && statSync6(src).isDirectory()) {
9298
- cpSync(src, join27(buildContext, dir), { recursive: true });
9296
+ cpSync(src, join28(buildContext, dir), { recursive: true });
9299
9297
  }
9300
9298
  }
9301
- writeFileSync14(join27(buildContext, "Dockerfile"), generateVerifyDockerfile(projectDir));
9299
+ writeFileSync14(join28(buildContext, "Dockerfile"), generateVerifyDockerfile(projectDir));
9302
9300
  execFileSync5("docker", ["build", "-t", IMAGE_TAG, "."], {
9303
9301
  cwd: buildContext,
9304
9302
  stdio: "pipe",
@@ -9309,10 +9307,10 @@ function buildPluginImage(projectDir) {
9309
9307
  }
9310
9308
  }
9311
9309
  function buildSimpleImage(projectDir, timeout = 12e4) {
9312
- const buildContext = join27("/tmp", `codeharness-verify-build-${Date.now()}`);
9310
+ const buildContext = join28("/tmp", `codeharness-verify-build-${Date.now()}`);
9313
9311
  mkdirSync11(buildContext, { recursive: true });
9314
9312
  try {
9315
- writeFileSync14(join27(buildContext, "Dockerfile"), generateVerifyDockerfile(projectDir));
9313
+ writeFileSync14(join28(buildContext, "Dockerfile"), generateVerifyDockerfile(projectDir));
9316
9314
  execFileSync5("docker", ["build", "-t", IMAGE_TAG, "."], {
9317
9315
  cwd: buildContext,
9318
9316
  stdio: "pipe",
@@ -9394,7 +9392,7 @@ function verifyRetro(opts, isJson, root) {
9394
9392
  return;
9395
9393
  }
9396
9394
  const retroFile = `epic-${epicNum}-retrospective.md`;
9397
- const retroPath = join28(root, STORY_DIR2, retroFile);
9395
+ const retroPath = join29(root, STORY_DIR2, retroFile);
9398
9396
  if (!existsSync28(retroPath)) {
9399
9397
  if (isJson) {
9400
9398
  jsonOutput({ status: "fail", epic: epicNum, retroFile, message: `${retroFile} not found` });
@@ -9412,7 +9410,7 @@ function verifyRetro(opts, isJson, root) {
9412
9410
  warn(`Failed to update sprint status: ${message}`);
9413
9411
  }
9414
9412
  if (isJson) {
9415
- jsonOutput({ status: "ok", epic: epicNum, retroFile: join28(STORY_DIR2, retroFile) });
9413
+ jsonOutput({ status: "ok", epic: epicNum, retroFile: join29(STORY_DIR2, retroFile) });
9416
9414
  } else {
9417
9415
  ok(`Epic ${epicNum} retrospective: marked done`);
9418
9416
  }
@@ -9423,7 +9421,7 @@ function verifyStory(storyId, isJson, root) {
9423
9421
  process.exitCode = 1;
9424
9422
  return;
9425
9423
  }
9426
- const readmePath = join28(root, "README.md");
9424
+ const readmePath = join29(root, "README.md");
9427
9425
  if (!existsSync28(readmePath)) {
9428
9426
  if (isJson) {
9429
9427
  jsonOutput({ status: "fail", message: "No README.md found \u2014 verification requires user documentation" });
@@ -9433,7 +9431,7 @@ function verifyStory(storyId, isJson, root) {
9433
9431
  process.exitCode = 1;
9434
9432
  return;
9435
9433
  }
9436
- const storyFilePath = join28(root, STORY_DIR2, `${storyId}.md`);
9434
+ const storyFilePath = join29(root, STORY_DIR2, `${storyId}.md`);
9437
9435
  if (!existsSync28(storyFilePath)) {
9438
9436
  fail(`Story file not found: ${storyFilePath}`, { json: isJson });
9439
9437
  process.exitCode = 1;
@@ -9474,7 +9472,7 @@ function verifyStory(storyId, isJson, root) {
9474
9472
  return;
9475
9473
  }
9476
9474
  const storyTitle = extractStoryTitle(storyFilePath);
9477
- const expectedProofPath = join28(root, "verification", `${storyId}-proof.md`);
9475
+ const expectedProofPath = join29(root, "verification", `${storyId}-proof.md`);
9478
9476
  const proofPath = existsSync28(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
9479
9477
  const proofQuality = validateProofQuality(proofPath);
9480
9478
  if (!proofQuality.passed) {
@@ -9646,11 +9644,11 @@ function resolveEndpoints(state) {
9646
9644
 
9647
9645
  // src/lib/onboard-checks.ts
9648
9646
  import { existsSync as existsSync32 } from "fs";
9649
- import { join as join31 } from "path";
9647
+ import { join as join32 } from "path";
9650
9648
 
9651
9649
  // src/lib/coverage/parser.ts
9652
9650
  import { existsSync as existsSync29, readFileSync as readFileSync26 } from "fs";
9653
- import { join as join29 } from "path";
9651
+ import { join as join30 } from "path";
9654
9652
  function parseTestCounts(output) {
9655
9653
  const vitestMatch = /Tests\s+(\d+)\s+passed(?:\s*\|\s*(\d+)\s+failed)?/i.exec(output);
9656
9654
  if (vitestMatch) {
@@ -9714,7 +9712,7 @@ function parseVitestCoverage(dir) {
9714
9712
  }
9715
9713
  }
9716
9714
  function parsePythonCoverage(dir) {
9717
- const reportPath = join29(dir, "coverage.json");
9715
+ const reportPath = join30(dir, "coverage.json");
9718
9716
  if (!existsSync29(reportPath)) {
9719
9717
  warn("Coverage report not found at coverage.json");
9720
9718
  return 0;
@@ -9728,7 +9726,7 @@ function parsePythonCoverage(dir) {
9728
9726
  }
9729
9727
  }
9730
9728
  function parseTarpaulinCoverage(dir) {
9731
- const reportPath = join29(dir, "coverage", "tarpaulin-report.json");
9729
+ const reportPath = join30(dir, "coverage", "tarpaulin-report.json");
9732
9730
  if (!existsSync29(reportPath)) {
9733
9731
  warn("Tarpaulin report not found at coverage/tarpaulin-report.json");
9734
9732
  return 0;
@@ -9743,8 +9741,8 @@ function parseTarpaulinCoverage(dir) {
9743
9741
  }
9744
9742
  function findCoverageSummary(dir) {
9745
9743
  const candidates = [
9746
- join29(dir, "coverage", "coverage-summary.json"),
9747
- join29(dir, "src", "coverage", "coverage-summary.json")
9744
+ join30(dir, "coverage", "coverage-summary.json"),
9745
+ join30(dir, "src", "coverage", "coverage-summary.json")
9748
9746
  ];
9749
9747
  for (const p of candidates) {
9750
9748
  if (existsSync29(p)) return p;
@@ -9755,7 +9753,7 @@ function findCoverageSummary(dir) {
9755
9753
  // src/lib/coverage/runner.ts
9756
9754
  import { execSync as execSync7 } from "child_process";
9757
9755
  import { existsSync as existsSync30, readFileSync as readFileSync27 } from "fs";
9758
- import { join as join30 } from "path";
9756
+ import { join as join31 } from "path";
9759
9757
  function detectCoverageTool(dir) {
9760
9758
  const baseDir = dir ?? process.cwd();
9761
9759
  const stateHint = getStateToolHint(baseDir);
@@ -9788,7 +9786,7 @@ function detectRustCoverageTool(dir) {
9788
9786
  warn("cargo-tarpaulin not installed \u2014 coverage detection unavailable");
9789
9787
  return { tool: "unknown", runCommand: "", reportFormat: "" };
9790
9788
  }
9791
- const cargoPath = join30(dir, "Cargo.toml");
9789
+ const cargoPath = join31(dir, "Cargo.toml");
9792
9790
  let isWorkspace = false;
9793
9791
  try {
9794
9792
  const cargoContent = readFileSync27(cargoPath, "utf-8");
@@ -9811,8 +9809,8 @@ function getStateToolHint(dir) {
9811
9809
  }
9812
9810
  }
9813
9811
  function detectNodeCoverageTool(dir, stateHint) {
9814
- const hasVitestConfig = existsSync30(join30(dir, "vitest.config.ts")) || existsSync30(join30(dir, "vitest.config.js"));
9815
- const pkgPath = join30(dir, "package.json");
9812
+ const hasVitestConfig = existsSync30(join31(dir, "vitest.config.ts")) || existsSync30(join31(dir, "vitest.config.js"));
9813
+ const pkgPath = join31(dir, "package.json");
9816
9814
  let hasVitestCoverageV8 = false;
9817
9815
  let hasVitestCoverageIstanbul = false;
9818
9816
  let hasC8 = false;
@@ -9873,7 +9871,7 @@ function getNodeTestCommand(scripts, runner) {
9873
9871
  return "npm test";
9874
9872
  }
9875
9873
  function detectPythonCoverageTool(dir) {
9876
- const reqPath = join30(dir, "requirements.txt");
9874
+ const reqPath = join31(dir, "requirements.txt");
9877
9875
  if (existsSync30(reqPath)) {
9878
9876
  try {
9879
9877
  const content = readFileSync27(reqPath, "utf-8");
@@ -9887,7 +9885,7 @@ function detectPythonCoverageTool(dir) {
9887
9885
  } catch {
9888
9886
  }
9889
9887
  }
9890
- const pyprojectPath = join30(dir, "pyproject.toml");
9888
+ const pyprojectPath = join31(dir, "pyproject.toml");
9891
9889
  if (existsSync30(pyprojectPath)) {
9892
9890
  try {
9893
9891
  const content = readFileSync27(pyprojectPath, "utf-8");
@@ -10720,7 +10718,7 @@ function registerStatusCommand(program) {
10720
10718
 
10721
10719
  // src/modules/audit/dimensions.ts
10722
10720
  import { existsSync as existsSync33, readdirSync as readdirSync8 } from "fs";
10723
- import { join as join32 } from "path";
10721
+ import { join as join33 } from "path";
10724
10722
  function gap(dimension, description, suggestedFix) {
10725
10723
  return { dimension, description, suggestedFix };
10726
10724
  }
@@ -10832,15 +10830,15 @@ function checkDocumentation(projectDir) {
10832
10830
  function checkVerification(projectDir) {
10833
10831
  try {
10834
10832
  const gaps = [];
10835
- const sprintPath = join32(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
10833
+ const sprintPath = join33(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
10836
10834
  if (!existsSync33(sprintPath)) return dimOk("verification", "warn", "no sprint data", [gap("verification", "No sprint-status.yaml found", "Run sprint planning to create sprint status")]);
10837
- const vDir = join32(projectDir, "verification");
10835
+ const vDir = join33(projectDir, "verification");
10838
10836
  let proofCount = 0, totalChecked = 0;
10839
10837
  if (existsSync33(vDir)) {
10840
10838
  for (const file of readdirSafe(vDir)) {
10841
10839
  if (!file.endsWith("-proof.md")) continue;
10842
10840
  totalChecked++;
10843
- const r = parseProof(join32(vDir, file));
10841
+ const r = parseProof(join33(vDir, file));
10844
10842
  if (isOk(r) && r.data.passed) {
10845
10843
  proofCount++;
10846
10844
  } else {
@@ -10918,7 +10916,7 @@ function formatAuditJson(result) {
10918
10916
 
10919
10917
  // src/modules/audit/fix-generator.ts
10920
10918
  import { existsSync as existsSync34, writeFileSync as writeFileSync15, mkdirSync as mkdirSync12 } from "fs";
10921
- import { join as join33, dirname as dirname5 } from "path";
10919
+ import { join as join34, dirname as dirname5 } from "path";
10922
10920
  function buildStoryKey(gap2, index) {
10923
10921
  const safeDimension = gap2.dimension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
10924
10922
  return `audit-fix-${safeDimension}-${index}`;
@@ -10950,7 +10948,7 @@ function generateFixStories(auditResult) {
10950
10948
  const stories = [];
10951
10949
  let created = 0;
10952
10950
  let skipped = 0;
10953
- const artifactsDir = join33(
10951
+ const artifactsDir = join34(
10954
10952
  process.cwd(),
10955
10953
  "_bmad-output",
10956
10954
  "implementation-artifacts"
@@ -10959,7 +10957,7 @@ function generateFixStories(auditResult) {
10959
10957
  for (let i = 0; i < dimension.gaps.length; i++) {
10960
10958
  const gap2 = dimension.gaps[i];
10961
10959
  const key = buildStoryKey(gap2, i + 1);
10962
- const filePath = join33(artifactsDir, `${key}.md`);
10960
+ const filePath = join34(artifactsDir, `${key}.md`);
10963
10961
  if (existsSync34(filePath)) {
10964
10962
  stories.push({
10965
10963
  key,
@@ -11150,7 +11148,7 @@ function registerOnboardCommand(program) {
11150
11148
 
11151
11149
  // src/commands/teardown.ts
11152
11150
  import { existsSync as existsSync35, unlinkSync as unlinkSync3, readFileSync as readFileSync29, writeFileSync as writeFileSync16, rmSync as rmSync4 } from "fs";
11153
- import { join as join34 } from "path";
11151
+ import { join as join35 } from "path";
11154
11152
  function buildDefaultResult() {
11155
11153
  return {
11156
11154
  status: "ok",
@@ -11196,7 +11194,7 @@ function registerTeardownCommand(program) {
11196
11194
  } else if (otlpMode === "remote-routed") {
11197
11195
  if (!options.keepDocker) {
11198
11196
  try {
11199
- const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-SV6TB753.js");
11197
+ const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-Z6B3GBST.js");
11200
11198
  stopCollectorOnly2();
11201
11199
  result.docker.stopped = true;
11202
11200
  if (!isJson) {
@@ -11228,7 +11226,7 @@ function registerTeardownCommand(program) {
11228
11226
  info("Shared stack: kept running (other projects may use it)");
11229
11227
  }
11230
11228
  } else if (isLegacyStack) {
11231
- const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-SV6TB753.js");
11229
+ const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-Z6B3GBST.js");
11232
11230
  let stackRunning = false;
11233
11231
  try {
11234
11232
  stackRunning = isStackRunning2(composeFile);
@@ -11253,7 +11251,7 @@ function registerTeardownCommand(program) {
11253
11251
  info("Docker stack: not running, skipping");
11254
11252
  }
11255
11253
  }
11256
- const composeFilePath = join34(projectDir, composeFile);
11254
+ const composeFilePath = join35(projectDir, composeFile);
11257
11255
  if (existsSync35(composeFilePath)) {
11258
11256
  unlinkSync3(composeFilePath);
11259
11257
  result.removed.push(composeFile);
@@ -11261,7 +11259,7 @@ function registerTeardownCommand(program) {
11261
11259
  ok(`Removed: ${composeFile}`);
11262
11260
  }
11263
11261
  }
11264
- const otelConfigPath = join34(projectDir, "otel-collector-config.yaml");
11262
+ const otelConfigPath = join35(projectDir, "otel-collector-config.yaml");
11265
11263
  if (existsSync35(otelConfigPath)) {
11266
11264
  unlinkSync3(otelConfigPath);
11267
11265
  result.removed.push("otel-collector-config.yaml");
@@ -11281,7 +11279,7 @@ function registerTeardownCommand(program) {
11281
11279
  }
11282
11280
  const stacks = state.stacks ?? (state.stack ? [state.stack] : []);
11283
11281
  if (state.otlp?.enabled && stacks.includes("nodejs")) {
11284
- const pkgPath = join34(projectDir, "package.json");
11282
+ const pkgPath = join35(projectDir, "package.json");
11285
11283
  if (existsSync35(pkgPath)) {
11286
11284
  try {
11287
11285
  const raw = readFileSync29(pkgPath, "utf-8");
@@ -11324,7 +11322,7 @@ function registerTeardownCommand(program) {
11324
11322
  }
11325
11323
  }
11326
11324
  }
11327
- const harnessDir = join34(projectDir, ".harness");
11325
+ const harnessDir = join35(projectDir, ".harness");
11328
11326
  if (existsSync35(harnessDir)) {
11329
11327
  rmSync4(harnessDir, { recursive: true, force: true });
11330
11328
  result.removed.push(".harness/");
@@ -12015,7 +12013,7 @@ function registerQueryCommand(program) {
12015
12013
 
12016
12014
  // src/commands/retro-import.ts
12017
12015
  import { existsSync as existsSync37, readFileSync as readFileSync31 } from "fs";
12018
- import { join as join36 } from "path";
12016
+ import { join as join37 } from "path";
12019
12017
 
12020
12018
  // src/lib/retro-parser.ts
12021
12019
  var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
@@ -12133,7 +12131,7 @@ function isDuplicate(newItem, existingTitles, threshold = 0.8) {
12133
12131
 
12134
12132
  // src/lib/issue-tracker.ts
12135
12133
  import { existsSync as existsSync36, readFileSync as readFileSync30, writeFileSync as writeFileSync17, mkdirSync as mkdirSync13 } from "fs";
12136
- import { join as join35 } from "path";
12134
+ import { join as join36 } from "path";
12137
12135
  import { parse as parse6, stringify as stringify3 } from "yaml";
12138
12136
  var VALID_PRIORITIES = /* @__PURE__ */ new Set([
12139
12137
  "low",
@@ -12141,9 +12139,9 @@ var VALID_PRIORITIES = /* @__PURE__ */ new Set([
12141
12139
  "high",
12142
12140
  "critical"
12143
12141
  ]);
12144
- var ISSUES_REL_PATH = join35(".codeharness", "issues.yaml");
12142
+ var ISSUES_REL_PATH = join36(".codeharness", "issues.yaml");
12145
12143
  function issuesPath(dir) {
12146
- return join35(dir, ISSUES_REL_PATH);
12144
+ return join36(dir, ISSUES_REL_PATH);
12147
12145
  }
12148
12146
  function readIssues(dir = process.cwd()) {
12149
12147
  const filePath = issuesPath(dir);
@@ -12159,7 +12157,7 @@ function readIssues(dir = process.cwd()) {
12159
12157
  }
12160
12158
  function writeIssues(data, dir = process.cwd()) {
12161
12159
  const filePath = issuesPath(dir);
12162
- const dirPath = join35(dir, ".codeharness");
12160
+ const dirPath = join36(dir, ".codeharness");
12163
12161
  if (!existsSync36(dirPath)) {
12164
12162
  mkdirSync13(dirPath, { recursive: true });
12165
12163
  }
@@ -12320,7 +12318,7 @@ function registerRetroImportCommand(program) {
12320
12318
  return;
12321
12319
  }
12322
12320
  const retroFile = `epic-${epicNum}-retrospective.md`;
12323
- const retroPath = join36(root, STORY_DIR3, retroFile);
12321
+ const retroPath = join37(root, STORY_DIR3, retroFile);
12324
12322
  if (!existsSync37(retroPath)) {
12325
12323
  fail(`Retro file not found: ${retroFile}`, { json: isJson });
12326
12324
  process.exitCode = 1;
@@ -12821,7 +12819,7 @@ function registerValidateStateCommand(program) {
12821
12819
 
12822
12820
  // src/commands/validate-schema.ts
12823
12821
  import { readdirSync as readdirSync9, existsSync as existsSync38 } from "fs";
12824
- import { join as join37, resolve as resolve6 } from "path";
12822
+ import { join as join38, resolve as resolve6 } from "path";
12825
12823
  function renderSchemaResult(result, isJson) {
12826
12824
  if (isJson) {
12827
12825
  jsonOutput(result);
@@ -12841,7 +12839,7 @@ function renderSchemaResult(result, isJson) {
12841
12839
  process.exitCode = result.status === "pass" ? 0 : 1;
12842
12840
  }
12843
12841
  function runSchemaValidation(projectDir) {
12844
- const workflowsDir = join37(projectDir, ".codeharness", "workflows");
12842
+ const workflowsDir = join38(projectDir, ".codeharness", "workflows");
12845
12843
  if (!existsSync38(workflowsDir)) {
12846
12844
  return {
12847
12845
  status: "fail",
@@ -13150,7 +13148,7 @@ function registerAuditCommand(program) {
13150
13148
 
13151
13149
  // src/commands/stats.ts
13152
13150
  import { existsSync as existsSync39, readdirSync as readdirSync10, readFileSync as readFileSync32, writeFileSync as writeFileSync18 } from "fs";
13153
- import { join as join38 } from "path";
13151
+ import { join as join39 } from "path";
13154
13152
  var RATES = {
13155
13153
  input: 15,
13156
13154
  output: 75,
@@ -13235,10 +13233,10 @@ function parseLogFile(filePath, report) {
13235
13233
  }
13236
13234
  }
13237
13235
  function generateReport3(projectDir, logsDir) {
13238
- const ralphLogs = join38(projectDir, "ralph", "logs");
13239
- const sessionLogs = join38(projectDir, "session-logs");
13236
+ const ralphLogs = join39(projectDir, "ralph", "logs");
13237
+ const sessionLogs = join39(projectDir, "session-logs");
13240
13238
  const resolvedLogsDir = logsDir ?? (existsSync39(ralphLogs) ? ralphLogs : sessionLogs);
13241
- const logFiles = readdirSync10(resolvedLogsDir).filter((f) => f.endsWith(".log")).sort().map((f) => join38(resolvedLogsDir, f));
13239
+ const logFiles = readdirSync10(resolvedLogsDir).filter((f) => f.endsWith(".log")).sort().map((f) => join39(resolvedLogsDir, f));
13242
13240
  const report = {
13243
13241
  byPhase: /* @__PURE__ */ new Map(),
13244
13242
  byStory: /* @__PURE__ */ new Map(),
@@ -13339,10 +13337,10 @@ function registerStatsCommand(program) {
13339
13337
  const projectDir = process.cwd();
13340
13338
  let logsDir;
13341
13339
  if (options.logsDir) {
13342
- logsDir = join38(projectDir, options.logsDir);
13340
+ logsDir = join39(projectDir, options.logsDir);
13343
13341
  } else {
13344
- const ralphLogs = join38(projectDir, "ralph", "logs");
13345
- const sessionLogs = join38(projectDir, "session-logs");
13342
+ const ralphLogs = join39(projectDir, "ralph", "logs");
13343
+ const sessionLogs = join39(projectDir, "session-logs");
13346
13344
  logsDir = existsSync39(ralphLogs) ? ralphLogs : sessionLogs;
13347
13345
  }
13348
13346
  if (!existsSync39(logsDir)) {
@@ -13358,7 +13356,7 @@ function registerStatsCommand(program) {
13358
13356
  const formatted = formatReport2(report);
13359
13357
  console.log(formatted);
13360
13358
  if (options.save) {
13361
- const outPath = join38(projectDir, "_bmad-output", "implementation-artifacts", "cost-report.md");
13359
+ const outPath = join39(projectDir, "_bmad-output", "implementation-artifacts", "cost-report.md");
13362
13360
  writeFileSync18(outPath, formatted, "utf-8");
13363
13361
  ok(`Report saved to ${outPath}`);
13364
13362
  }
@@ -14215,7 +14213,7 @@ function registerDriversCommand(program) {
14215
14213
  }
14216
14214
 
14217
14215
  // src/index.ts
14218
- var VERSION = true ? "0.36.2" : "0.0.0-dev";
14216
+ var VERSION = true ? "0.36.3" : "0.0.0-dev";
14219
14217
  function createProgram() {
14220
14218
  const program = new Command();
14221
14219
  program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");