codeharness 0.35.7 → 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-WNIM6AUG.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";
@@ -2506,10 +2506,10 @@ function resolveWorkflow(options) {
2506
2506
  return validateAndResolve(merged);
2507
2507
  }
2508
2508
 
2509
- // src/lib/workflow-engine.ts
2509
+ // src/lib/workflow-machine.ts
2510
+ import { setup, assign, fromPromise as fromPromise2, createActor } from "xstate";
2510
2511
  import { readFileSync as readFileSync13, existsSync as existsSync15, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "fs";
2511
- import { join as join12 } from "path";
2512
- import { parse as parse5 } from "yaml";
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,24 +2550,213 @@ 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;
2553
+ // src/lib/verdict-parser.ts
2554
+ import Ajv2 from "ajv";
2555
+
2556
+ // src/schemas/verdict.schema.json
2557
+ var verdict_schema_default = {
2558
+ $schema: "http://json-schema.org/draft-07/schema#",
2559
+ $id: "https://codeharness.dev/schemas/verdict.schema.json",
2560
+ title: "EvaluatorVerdict",
2561
+ description: "Schema for evaluator verdict output (AD5)",
2562
+ type: "object",
2563
+ required: ["verdict", "score", "findings"],
2564
+ additionalProperties: true,
2565
+ properties: {
2566
+ verdict: {
2567
+ type: "string",
2568
+ enum: ["pass", "fail"]
2569
+ },
2570
+ score: {
2571
+ type: "object",
2572
+ required: ["passed", "failed", "unknown", "total"],
2573
+ additionalProperties: true,
2574
+ properties: {
2575
+ passed: {
2576
+ type: "integer",
2577
+ minimum: 0
2578
+ },
2579
+ failed: {
2580
+ type: "integer",
2581
+ minimum: 0
2582
+ },
2583
+ unknown: {
2584
+ type: "integer",
2585
+ minimum: 0
2586
+ },
2587
+ total: {
2588
+ type: "integer",
2589
+ minimum: 0
2590
+ }
2591
+ }
2592
+ },
2593
+ findings: {
2594
+ type: "array",
2595
+ items: {
2596
+ type: "object",
2597
+ required: ["ac", "description", "status", "evidence"],
2598
+ additionalProperties: true,
2599
+ properties: {
2600
+ ac: {
2601
+ type: "integer"
2602
+ },
2603
+ description: {
2604
+ type: "string"
2605
+ },
2606
+ status: {
2607
+ type: "string",
2608
+ enum: ["pass", "fail", "unknown"]
2609
+ },
2610
+ evidence: {
2611
+ type: "object",
2612
+ required: ["commands_run", "output_observed", "reasoning"],
2613
+ additionalProperties: true,
2614
+ properties: {
2615
+ commands_run: {
2616
+ type: "array",
2617
+ items: {
2618
+ type: "string"
2619
+ }
2620
+ },
2621
+ output_observed: {
2622
+ type: "string"
2623
+ },
2624
+ reasoning: {
2625
+ type: "string"
2626
+ }
2627
+ }
2628
+ }
2629
+ }
2630
+ }
2631
+ },
2632
+ evaluator_trace_id: {
2633
+ type: "string"
2634
+ },
2635
+ duration_seconds: {
2636
+ type: "number"
2637
+ }
2559
2638
  }
2560
- if (agentModel) {
2561
- return agentModel;
2639
+ };
2640
+
2641
+ // src/lib/verdict-parser.ts
2642
+ var VerdictParseError = class _VerdictParseError extends Error {
2643
+ retryable;
2644
+ rawOutput;
2645
+ validationErrors;
2646
+ constructor(message, retryable, rawOutput, validationErrors) {
2647
+ super(message);
2648
+ Object.setPrototypeOf(this, _VerdictParseError.prototype);
2649
+ this.name = "VerdictParseError";
2650
+ this.retryable = retryable;
2651
+ this.rawOutput = rawOutput;
2652
+ this.validationErrors = validationErrors;
2562
2653
  }
2563
- if (!driver.defaultModel?.trim()) {
2564
- throw new Error(
2565
- "Driver has no default model: driver.defaultModel must be a non-empty string"
2654
+ };
2655
+ var ajv2 = new Ajv2({ allErrors: true });
2656
+ var validateSchema = ajv2.compile(verdict_schema_default);
2657
+ function validateVerdict(data) {
2658
+ const valid = validateSchema(data);
2659
+ if (valid) {
2660
+ const verdict = JSON.parse(JSON.stringify(data));
2661
+ return { valid: true, verdict };
2662
+ }
2663
+ const errors = (validateSchema.errors ?? []).map((err) => {
2664
+ const path = err.instancePath || "/";
2665
+ return `${path}: ${err.message ?? "unknown error"}`;
2666
+ });
2667
+ return { valid: false, errors };
2668
+ }
2669
+ function parseVerdict(output) {
2670
+ let parsed;
2671
+ try {
2672
+ parsed = JSON.parse(output);
2673
+ } catch {
2674
+ throw new VerdictParseError(
2675
+ "Failed to parse verdict: invalid JSON",
2676
+ true,
2677
+ output
2566
2678
  );
2567
2679
  }
2568
- return driver.defaultModel;
2680
+ const result = validateVerdict(parsed);
2681
+ if (!result.valid) {
2682
+ throw new VerdictParseError(
2683
+ `Failed to parse verdict: schema validation failed`,
2684
+ true,
2685
+ output,
2686
+ result.errors
2687
+ );
2688
+ }
2689
+ const verdict = result.verdict;
2690
+ let passDowngraded = false;
2691
+ for (const finding of verdict.findings) {
2692
+ if (finding.status === "pass" && (!finding.evidence.commands_run || finding.evidence.commands_run.length === 0)) {
2693
+ finding.status = "unknown";
2694
+ finding.evidence.reasoning += " [Downgraded from PASS: no commands_run evidence provided]";
2695
+ passDowngraded = true;
2696
+ }
2697
+ }
2698
+ if (passDowngraded) {
2699
+ let passed = 0;
2700
+ let failed = 0;
2701
+ let unknown = 0;
2702
+ for (const finding of verdict.findings) {
2703
+ if (finding.status === "pass") passed++;
2704
+ else if (finding.status === "fail") failed++;
2705
+ else unknown++;
2706
+ }
2707
+ verdict.score = {
2708
+ passed,
2709
+ failed,
2710
+ unknown,
2711
+ total: verdict.findings.length
2712
+ };
2713
+ if (passed === 0) {
2714
+ verdict.verdict = "fail";
2715
+ }
2716
+ }
2717
+ return verdict;
2718
+ }
2719
+ function parseVerdictTag(output) {
2720
+ const match = /<verdict>(pass|fail)<\/verdict>/i.exec(output);
2721
+ if (!match) return null;
2722
+ const verdict = match[1].toLowerCase();
2723
+ const issuesMatch = /<issues>([\s\S]*?)<\/issues>/i.exec(output);
2724
+ return {
2725
+ verdict,
2726
+ ...issuesMatch ? { issues: issuesMatch[1].trim() } : {}
2727
+ };
2728
+ }
2729
+ function extractTag(output, tag) {
2730
+ const pattern = new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`, "i");
2731
+ const match = pattern.exec(output);
2732
+ return match ? match[1].trim() : null;
2733
+ }
2734
+
2735
+ // src/lib/circuit-breaker.ts
2736
+ function evaluateProgress(scores) {
2737
+ if (scores.length < 2) {
2738
+ return { halt: false };
2739
+ }
2740
+ const scoreHistory = scores.map((s) => s.passed);
2741
+ const latest = scoreHistory[scoreHistory.length - 1];
2742
+ const previous = scoreHistory[scoreHistory.length - 2];
2743
+ if (latest > previous) {
2744
+ return { halt: false };
2745
+ }
2746
+ const latestScore = scores[scores.length - 1];
2747
+ const failCount = latestScore.total - latestScore.passed;
2748
+ const remainingFailures = Array.from({ length: failCount }, (_, i) => i + 1);
2749
+ return {
2750
+ halt: true,
2751
+ reason: "score-stagnation",
2752
+ remainingFailures,
2753
+ scoreHistory
2754
+ };
2569
2755
  }
2570
2756
 
2757
+ // src/lib/workflow-machine.ts
2758
+ import { parse as parse5 } from "yaml";
2759
+
2571
2760
  // src/lib/workflow-state.ts
2572
2761
  import { existsSync as existsSync11, mkdirSync as mkdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
2573
2762
  import { join as join8 } from "path";
@@ -2668,6 +2857,28 @@ function isValidWorkflowState(value) {
2668
2857
  return true;
2669
2858
  }
2670
2859
 
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
+
2671
2882
  // src/lib/agents/output-contract.ts
2672
2883
  import { writeFileSync as writeFileSync7, readFileSync as readFileSync11, renameSync as renameSync2, mkdirSync as mkdirSync3, existsSync as existsSync12 } from "fs";
2673
2884
  import { join as join9, resolve as resolve4 } from "path";
@@ -2901,210 +3112,6 @@ function getLastSessionId(state, taskName, storyKey) {
2901
3112
  return void 0;
2902
3113
  }
2903
3114
 
2904
- // src/lib/verdict-parser.ts
2905
- import Ajv2 from "ajv";
2906
-
2907
- // src/schemas/verdict.schema.json
2908
- var verdict_schema_default = {
2909
- $schema: "http://json-schema.org/draft-07/schema#",
2910
- $id: "https://codeharness.dev/schemas/verdict.schema.json",
2911
- title: "EvaluatorVerdict",
2912
- description: "Schema for evaluator verdict output (AD5)",
2913
- type: "object",
2914
- required: ["verdict", "score", "findings"],
2915
- additionalProperties: true,
2916
- properties: {
2917
- verdict: {
2918
- type: "string",
2919
- enum: ["pass", "fail"]
2920
- },
2921
- score: {
2922
- type: "object",
2923
- required: ["passed", "failed", "unknown", "total"],
2924
- additionalProperties: true,
2925
- properties: {
2926
- passed: {
2927
- type: "integer",
2928
- minimum: 0
2929
- },
2930
- failed: {
2931
- type: "integer",
2932
- minimum: 0
2933
- },
2934
- unknown: {
2935
- type: "integer",
2936
- minimum: 0
2937
- },
2938
- total: {
2939
- type: "integer",
2940
- minimum: 0
2941
- }
2942
- }
2943
- },
2944
- findings: {
2945
- type: "array",
2946
- items: {
2947
- type: "object",
2948
- required: ["ac", "description", "status", "evidence"],
2949
- additionalProperties: true,
2950
- properties: {
2951
- ac: {
2952
- type: "integer"
2953
- },
2954
- description: {
2955
- type: "string"
2956
- },
2957
- status: {
2958
- type: "string",
2959
- enum: ["pass", "fail", "unknown"]
2960
- },
2961
- evidence: {
2962
- type: "object",
2963
- required: ["commands_run", "output_observed", "reasoning"],
2964
- additionalProperties: true,
2965
- properties: {
2966
- commands_run: {
2967
- type: "array",
2968
- items: {
2969
- type: "string"
2970
- }
2971
- },
2972
- output_observed: {
2973
- type: "string"
2974
- },
2975
- reasoning: {
2976
- type: "string"
2977
- }
2978
- }
2979
- }
2980
- }
2981
- }
2982
- },
2983
- evaluator_trace_id: {
2984
- type: "string"
2985
- },
2986
- duration_seconds: {
2987
- type: "number"
2988
- }
2989
- }
2990
- };
2991
-
2992
- // src/lib/verdict-parser.ts
2993
- var VerdictParseError = class _VerdictParseError extends Error {
2994
- retryable;
2995
- rawOutput;
2996
- validationErrors;
2997
- constructor(message, retryable, rawOutput, validationErrors) {
2998
- super(message);
2999
- Object.setPrototypeOf(this, _VerdictParseError.prototype);
3000
- this.name = "VerdictParseError";
3001
- this.retryable = retryable;
3002
- this.rawOutput = rawOutput;
3003
- this.validationErrors = validationErrors;
3004
- }
3005
- };
3006
- var ajv2 = new Ajv2({ allErrors: true });
3007
- var validateSchema = ajv2.compile(verdict_schema_default);
3008
- function validateVerdict(data) {
3009
- const valid = validateSchema(data);
3010
- if (valid) {
3011
- const verdict = JSON.parse(JSON.stringify(data));
3012
- return { valid: true, verdict };
3013
- }
3014
- const errors = (validateSchema.errors ?? []).map((err) => {
3015
- const path = err.instancePath || "/";
3016
- return `${path}: ${err.message ?? "unknown error"}`;
3017
- });
3018
- return { valid: false, errors };
3019
- }
3020
- function parseVerdict(output) {
3021
- let parsed;
3022
- try {
3023
- parsed = JSON.parse(output);
3024
- } catch {
3025
- throw new VerdictParseError(
3026
- "Failed to parse verdict: invalid JSON",
3027
- true,
3028
- output
3029
- );
3030
- }
3031
- const result = validateVerdict(parsed);
3032
- if (!result.valid) {
3033
- throw new VerdictParseError(
3034
- `Failed to parse verdict: schema validation failed`,
3035
- true,
3036
- output,
3037
- result.errors
3038
- );
3039
- }
3040
- const verdict = result.verdict;
3041
- let passDowngraded = false;
3042
- for (const finding of verdict.findings) {
3043
- if (finding.status === "pass" && (!finding.evidence.commands_run || finding.evidence.commands_run.length === 0)) {
3044
- finding.status = "unknown";
3045
- finding.evidence.reasoning += " [Downgraded from PASS: no commands_run evidence provided]";
3046
- passDowngraded = true;
3047
- }
3048
- }
3049
- if (passDowngraded) {
3050
- let passed = 0;
3051
- let failed = 0;
3052
- let unknown = 0;
3053
- for (const finding of verdict.findings) {
3054
- if (finding.status === "pass") passed++;
3055
- else if (finding.status === "fail") failed++;
3056
- else unknown++;
3057
- }
3058
- verdict.score = {
3059
- passed,
3060
- failed,
3061
- unknown,
3062
- total: verdict.findings.length
3063
- };
3064
- if (passed === 0) {
3065
- verdict.verdict = "fail";
3066
- }
3067
- }
3068
- return verdict;
3069
- }
3070
- function parseVerdictTag(output) {
3071
- const match = /<verdict>(pass|fail)<\/verdict>/i.exec(output);
3072
- if (!match) return null;
3073
- const verdict = match[1].toLowerCase();
3074
- const issuesMatch = /<issues>([\s\S]*?)<\/issues>/i.exec(output);
3075
- return {
3076
- verdict,
3077
- ...issuesMatch ? { issues: issuesMatch[1].trim() } : {}
3078
- };
3079
- }
3080
- function extractTag(output, tag) {
3081
- const pattern = new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`, "i");
3082
- const match = pattern.exec(output);
3083
- return match ? match[1].trim() : null;
3084
- }
3085
-
3086
- // src/lib/circuit-breaker.ts
3087
- function evaluateProgress(scores) {
3088
- if (scores.length < 2) {
3089
- return { halt: false };
3090
- }
3091
- const scoreHistory = scores.map((s) => s.passed);
3092
- const latest = scoreHistory[scoreHistory.length - 1];
3093
- const previous = scoreHistory[scoreHistory.length - 2];
3094
- if (latest > previous) {
3095
- return { halt: false };
3096
- }
3097
- const latestScore = scores[scores.length - 1];
3098
- const failCount = latestScore.total - latestScore.passed;
3099
- const remainingFailures = Array.from({ length: failCount }, (_, i) => i + 1);
3100
- return {
3101
- halt: true,
3102
- reason: "score-stagnation",
3103
- remainingFailures,
3104
- scoreHistory
3105
- };
3106
- }
3107
-
3108
3115
  // src/lib/telemetry-writer.ts
3109
3116
  import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync12 } from "fs";
3110
3117
  import { join as join11 } from "path";
@@ -3164,10 +3171,27 @@ function formatCoverageContextMessage(coverage, target) {
3164
3171
  return `Coverage already verified by engine: ${coverage}% (target: ${target}%). No re-run needed.`;
3165
3172
  }
3166
3173
 
3167
- // src/lib/workflow-engine.ts
3168
- var PER_RUN_SENTINEL = "__run__";
3169
- var HALT_ERROR_CODES = /* @__PURE__ */ new Set(["RATE_LIMIT", "NETWORK", "SDK_INIT"]);
3170
- var DEFAULT_MAX_ITERATIONS = 5;
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
+ };
3185
+ var FILE_WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
3186
+ "Write",
3187
+ "Edit",
3188
+ "write_to_file",
3189
+ "edit_file",
3190
+ "write",
3191
+ "edit",
3192
+ "WriteFile",
3193
+ "EditFile"
3194
+ ]);
3171
3195
  function buildCoverageDeduplicationContext(contract, projectDir) {
3172
3196
  if (!contract?.testResults) return null;
3173
3197
  const { coverage } = contract.testResults;
@@ -3181,170 +3205,36 @@ function buildCoverageDeduplicationContext(contract, projectDir) {
3181
3205
  return null;
3182
3206
  }
3183
3207
  }
3184
- function propagateVerifyFlags(taskName, contract, projectDir) {
3185
- if (taskName !== "implement") return;
3186
- if (!contract?.testResults) return;
3187
- const { failed, coverage } = contract.testResults;
3188
- try {
3189
- const { state, body } = readStateWithBody(projectDir);
3190
- if (failed === 0) {
3191
- state.session_flags.tests_passed = true;
3192
- }
3193
- if (coverage !== null && coverage !== void 0 && coverage >= state.coverage.target) {
3194
- state.session_flags.coverage_met = true;
3195
- }
3196
- writeState(state, projectDir, body);
3197
- } catch (err) {
3198
- const msg = err instanceof Error ? err.message : String(err);
3199
- warn(`workflow-engine: flag propagation failed for ${taskName}: ${msg}`);
3200
- }
3201
- }
3202
- function isTaskCompleted(state, taskName, storyKey) {
3203
- return state.tasks_completed.some(
3204
- (cp) => cp.task_name === taskName && cp.story_key === storyKey && !cp.error
3205
- );
3206
- }
3207
- function isLoopTaskCompleted(state, taskName, storyKey, iteration) {
3208
- const count = state.tasks_completed.filter(
3209
- (cp) => cp.task_name === taskName && cp.story_key === storyKey && !cp.error
3210
- ).length;
3211
- return count >= iteration;
3212
- }
3213
- function loadWorkItems(sprintStatusPath, issuesPath2) {
3214
- const items = [];
3215
- if (existsSync15(sprintStatusPath)) {
3216
- let raw;
3217
- try {
3218
- raw = readFileSync13(sprintStatusPath, "utf-8");
3219
- } catch {
3220
- warn(`workflow-engine: could not read sprint-status.yaml at ${sprintStatusPath}`);
3221
- return items;
3222
- }
3223
- let parsed;
3224
- try {
3225
- parsed = parse5(raw);
3226
- } catch {
3227
- warn(`workflow-engine: invalid YAML in sprint-status.yaml at ${sprintStatusPath}`);
3228
- return items;
3229
- }
3230
- if (parsed && typeof parsed === "object") {
3231
- const data = parsed;
3232
- const devStatus = data.development_status;
3233
- if (devStatus && typeof devStatus === "object") {
3234
- for (const [key, status] of Object.entries(devStatus)) {
3235
- if (key.startsWith("epic-")) continue;
3236
- if (key.endsWith("-retrospective")) continue;
3237
- if (status === "backlog" || status === "ready-for-dev" || status === "in-progress") {
3238
- items.push({ key, source: "sprint" });
3239
- }
3240
- }
3241
- }
3242
- }
3243
- }
3244
- if (issuesPath2 && existsSync15(issuesPath2)) {
3245
- let raw;
3246
- try {
3247
- raw = readFileSync13(issuesPath2, "utf-8");
3248
- } catch {
3249
- warn(`workflow-engine: could not read issues.yaml at ${issuesPath2}`);
3250
- return items;
3251
- }
3252
- let parsed;
3253
- try {
3254
- parsed = parse5(raw);
3255
- } catch {
3256
- warn(`workflow-engine: invalid YAML in issues.yaml at ${issuesPath2}`);
3257
- return items;
3258
- }
3259
- if (parsed && typeof parsed === "object") {
3260
- const data = parsed;
3261
- const issuesList = data.issues;
3262
- if (Array.isArray(issuesList)) {
3263
- for (const issue of issuesList) {
3264
- if (issue && typeof issue === "object") {
3265
- const status = issue.status;
3266
- if (status === "backlog" || status === "ready-for-dev" || status === "in-progress") {
3267
- items.push({
3268
- key: issue.id,
3269
- title: issue.title,
3270
- source: "issues"
3271
- });
3272
- }
3273
- }
3274
- }
3275
- }
3276
- }
3277
- }
3278
- return items;
3279
- }
3280
- var FILE_WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
3281
- "Write",
3282
- "Edit",
3283
- "write_to_file",
3284
- "edit_file",
3285
- "write",
3286
- "edit",
3287
- "WriteFile",
3288
- "EditFile"
3289
- ]);
3290
- function isLoopBlock(step) {
3291
- return typeof step === "object" && step !== null && "loop" in step;
3292
- }
3293
- async function executeNullTask(task, taskName, storyKey, state, config, previousOutputContract, accumulatedCostUsd) {
3208
+ async function nullTaskCore(input) {
3209
+ const { task: _task, taskName, storyKey, config, workflowState, previousContract, accumulatedCostUsd } = input;
3294
3210
  const projectDir = config.projectDir ?? process.cwd();
3295
3211
  const handler = getNullTask(taskName);
3296
3212
  if (!handler) {
3297
3213
  const registered = listNullTasks();
3298
- const registeredList = registered.length > 0 ? registered.join(", ") : "(none)";
3299
- const error = {
3300
- taskName,
3301
- storyKey,
3302
- code: "NULL_TASK_NOT_FOUND",
3303
- message: `No null task handler registered for "${taskName}". Registered handlers: ${registeredList}`
3304
- };
3305
- throw error;
3214
+ throw { taskName, storyKey, code: "NULL_TASK_NOT_FOUND", message: `No null task handler registered for "${taskName}". Registered: ${registered.join(", ") || "(none)"}` };
3306
3215
  }
3307
3216
  const startMs = Date.now();
3308
- const workflowStartMs = state.started ? new Date(state.started).getTime() : startMs;
3217
+ const workflowStartMs = workflowState.started ? new Date(workflowState.started).getTime() : startMs;
3309
3218
  const ctx = {
3310
3219
  storyKey,
3311
3220
  taskName,
3312
- cost: accumulatedCostUsd ?? 0,
3221
+ cost: accumulatedCostUsd,
3313
3222
  durationMs: startMs - workflowStartMs,
3314
- outputContract: previousOutputContract ?? null,
3223
+ outputContract: previousContract,
3315
3224
  projectDir
3316
3225
  };
3317
3226
  let result;
3318
3227
  try {
3319
3228
  result = await handler(ctx);
3320
- } catch (handlerErr) {
3321
- const error = {
3322
- taskName,
3323
- storyKey,
3324
- code: "NULL_TASK_HANDLER_ERROR",
3325
- message: `Null task handler "${taskName}" threw: ${handlerErr instanceof Error ? handlerErr.message : String(handlerErr)}`
3326
- };
3327
- throw error;
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)}` };
3328
3231
  }
3329
- const durationMs = Date.now() - startMs;
3330
3232
  if (!result.success) {
3331
- const error = {
3332
- taskName,
3333
- storyKey,
3334
- code: "NULL_TASK_FAILED",
3335
- message: `Null task handler "${taskName}" returned success=false${result.output ? `: ${result.output}` : ""}`
3336
- };
3337
- throw error;
3233
+ throw { taskName, storyKey, code: "NULL_TASK_FAILED", message: `Null task handler "${taskName}" returned success=false${result.output ? `: ${result.output}` : ""}` };
3338
3234
  }
3339
- const checkpoint = {
3340
- task_name: taskName,
3341
- story_key: storyKey,
3342
- completed_at: (/* @__PURE__ */ new Date()).toISOString()
3343
- };
3344
- let updatedState = {
3345
- ...state,
3346
- tasks_completed: [...state.tasks_completed, checkpoint]
3347
- };
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;
3348
3238
  const contract = {
3349
3239
  version: 1,
3350
3240
  taskName,
@@ -3360,18 +3250,34 @@ async function executeNullTask(task, taskName, storyKey, state, config, previous
3360
3250
  acceptanceCriteria: []
3361
3251
  };
3362
3252
  writeWorkflowState(updatedState, projectDir);
3363
- return { updatedState, output: result.output ?? "", contract };
3253
+ return { output: result.output ?? "", cost: 0, changedFiles: [], sessionId: "", contract, updatedState };
3254
+ }
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
+ }
3364
3270
  }
3365
- async function dispatchTaskWithResult(task, taskName, storyKey, definition, state, config, customPrompt, previousOutputContract, storyFiles) {
3271
+ async function dispatchTaskCore(input) {
3272
+ const { task, taskName, storyKey, definition, config, workflowState, previousContract, onStreamEvent, storyFiles, customPrompt } = input;
3366
3273
  const projectDir = config.projectDir ?? process.cwd();
3367
- const traceId = generateTraceId(config.runId, state.iteration, taskName);
3274
+ const traceId = generateTraceId(config.runId, workflowState.iteration, taskName);
3368
3275
  const tracePrompt = formatTracePrompt(traceId);
3369
3276
  const sessionKey = { taskName, storyKey };
3370
- const sessionId = resolveSessionId(task.session, sessionKey, state);
3277
+ const sessionId = resolveSessionId(task.session, sessionKey, workflowState);
3371
3278
  const driverName = task.driver ?? "claude-code";
3372
3279
  const driver = getDriver(driverName);
3373
- const agentAsModelSource = { model: definition.model };
3374
- const model = resolveModel(task, agentAsModelSource, driver);
3280
+ const model = resolveModel(task, { model: definition.model }, driver);
3375
3281
  let cwd;
3376
3282
  let workspace = null;
3377
3283
  if (task.source_access === false) {
@@ -3384,37 +3290,19 @@ async function dispatchTaskWithResult(task, taskName, storyKey, definition, stat
3384
3290
  } else {
3385
3291
  cwd = projectDir;
3386
3292
  }
3387
- const isEpicSentinel = storyKey.startsWith("__epic_") || storyKey === PER_RUN_SENTINEL;
3388
- const TASK_PROMPTS = {
3389
- "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.`,
3390
- "implement": (key) => `Implement story ${key}`,
3391
- "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.`,
3392
- "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>.`,
3393
- "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.`,
3394
- "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.`,
3395
- "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>.`,
3396
- "retro": () => `Run a retrospective for this epic. Analyze what worked, what failed, patterns, and action items for next epic.`
3397
- };
3398
3293
  let basePrompt;
3399
3294
  if (customPrompt) {
3400
3295
  basePrompt = customPrompt;
3401
- } else if (isEpicSentinel && TASK_PROMPTS[taskName]) {
3402
- basePrompt = TASK_PROMPTS[taskName](storyKey);
3403
3296
  } else if (TASK_PROMPTS[taskName]) {
3404
3297
  basePrompt = TASK_PROMPTS[taskName](storyKey);
3405
3298
  } else {
3406
3299
  basePrompt = `Execute task "${taskName}" for story ${storyKey}`;
3407
3300
  }
3408
- let prompt = buildPromptWithContractContext(basePrompt, previousOutputContract ?? null);
3409
- const coverageDedup = buildCoverageDeduplicationContext(
3410
- previousOutputContract ?? null,
3411
- projectDir
3412
- );
3413
- if (coverageDedup) {
3414
- prompt = `${prompt}
3301
+ let prompt = buildPromptWithContractContext(basePrompt, previousContract);
3302
+ const coverageDedup = buildCoverageDeduplicationContext(previousContract, projectDir);
3303
+ if (coverageDedup) prompt = `${prompt}
3415
3304
 
3416
3305
  ${coverageDedup}`;
3417
- }
3418
3306
  const dispatchOpts = {
3419
3307
  prompt,
3420
3308
  model,
@@ -3422,15 +3310,10 @@ ${coverageDedup}`;
3422
3310
  sourceAccess: task.source_access !== false,
3423
3311
  ...sessionId ? { sessionId } : {},
3424
3312
  ...tracePrompt ? { appendSystemPrompt: tracePrompt } : {},
3425
- ...task.plugins ?? definition.plugins ? { plugins: task.plugins ?? definition.plugins } : {},
3426
- ...task.max_budget_usd != null ? { timeout: task.max_budget_usd } : {}
3313
+ ...task.plugins ?? definition.plugins ? { plugins: task.plugins ?? definition.plugins } : {}
3427
3314
  };
3428
3315
  const emit = config.onEvent;
3429
- if (emit) {
3430
- emit({ type: "dispatch-start", taskName, storyKey, driverName, model });
3431
- } else {
3432
- info(`[${taskName}] ${storyKey} \u2014 dispatching via ${driverName} (model: ${model})...`);
3433
- }
3316
+ if (emit) emit({ type: "dispatch-start", taskName, storyKey, driverName, model });
3434
3317
  let output = "";
3435
3318
  let resultSessionId = "";
3436
3319
  let cost = 0;
@@ -3441,28 +3324,21 @@ ${coverageDedup}`;
3441
3324
  const startMs = Date.now();
3442
3325
  try {
3443
3326
  for await (const event of driver.dispatch(dispatchOpts)) {
3444
- if (emit) {
3445
- emit({ type: "stream-event", taskName, storyKey, driverName, streamEvent: event });
3446
- }
3447
- if (event.type === "text") {
3448
- output += event.text;
3449
- }
3327
+ if (onStreamEvent) onStreamEvent(event, driverName);
3328
+ if (emit) emit({ type: "stream-event", taskName, storyKey, driverName, streamEvent: event });
3329
+ if (event.type === "text") output += event.text;
3450
3330
  if (event.type === "tool-start") {
3451
- const toolStart = event;
3452
- activeToolName = toolStart.name;
3331
+ const ts = event;
3332
+ activeToolName = ts.name;
3453
3333
  activeToolInput = "";
3454
3334
  }
3455
- if (event.type === "tool-input") {
3456
- activeToolInput += event.partial;
3457
- }
3335
+ if (event.type === "tool-input") activeToolInput += event.partial;
3458
3336
  if (event.type === "tool-complete") {
3459
3337
  if (activeToolName && FILE_WRITE_TOOL_NAMES.has(activeToolName)) {
3460
3338
  try {
3461
3339
  const parsed = JSON.parse(activeToolInput);
3462
3340
  const filePath = parsed.file_path ?? parsed.path ?? parsed.filePath;
3463
- if (filePath && typeof filePath === "string") {
3464
- changedFiles.push(filePath);
3465
- }
3341
+ if (filePath && typeof filePath === "string") changedFiles.push(filePath);
3466
3342
  } catch {
3467
3343
  }
3468
3344
  }
@@ -3470,26 +3346,17 @@ ${coverageDedup}`;
3470
3346
  activeToolInput = "";
3471
3347
  }
3472
3348
  if (event.type === "result") {
3473
- const resultEvt = event;
3474
- resultSessionId = resultEvt.sessionId;
3475
- cost = resultEvt.cost;
3476
- if (resultEvt.error) {
3477
- errorEvent = { error: resultEvt.error, errorCategory: resultEvt.errorCategory };
3478
- }
3349
+ const r = event;
3350
+ resultSessionId = r.sessionId;
3351
+ cost = r.cost;
3352
+ if (r.error) errorEvent = { error: r.error, errorCategory: r.errorCategory };
3479
3353
  }
3480
3354
  }
3481
3355
  } finally {
3482
- if (workspace) {
3483
- await workspace.cleanup();
3484
- }
3356
+ if (workspace) await workspace.cleanup();
3485
3357
  }
3486
3358
  const elapsedMs = Date.now() - startMs;
3487
- if (emit) {
3488
- emit({ type: "dispatch-end", taskName, storyKey, driverName, elapsedMs, costUsd: cost });
3489
- } else {
3490
- const elapsed = (elapsedMs / 1e3).toFixed(1);
3491
- info(`[${taskName}] ${storyKey} \u2014 done (${elapsed}s, cost: $${cost.toFixed(4)})`);
3492
- }
3359
+ if (emit) emit({ type: "dispatch-end", taskName, storyKey, driverName, elapsedMs, costUsd: cost });
3493
3360
  if (errorEvent) {
3494
3361
  const categoryToCode = {
3495
3362
  RATE_LIMIT: "RATE_LIMIT",
@@ -3500,26 +3367,14 @@ ${coverageDedup}`;
3500
3367
  UNKNOWN: "UNKNOWN"
3501
3368
  };
3502
3369
  const code = categoryToCode[errorEvent.errorCategory ?? "UNKNOWN"] ?? "UNKNOWN";
3503
- throw new DispatchError(
3504
- errorEvent.error,
3505
- code,
3506
- definition.name,
3507
- errorEvent
3508
- );
3370
+ throw new DispatchError(errorEvent.error, code, definition.name, errorEvent);
3509
3371
  }
3510
- let updatedState = state;
3372
+ let updatedState = workflowState;
3511
3373
  if (resultSessionId) {
3512
3374
  updatedState = recordSessionId(sessionKey, resultSessionId, updatedState);
3513
3375
  } else {
3514
- const checkpoint = {
3515
- task_name: taskName,
3516
- story_key: storyKey,
3517
- completed_at: (/* @__PURE__ */ new Date()).toISOString()
3518
- };
3519
- updatedState = {
3520
- ...updatedState,
3521
- tasks_completed: [...updatedState.tasks_completed, checkpoint]
3522
- };
3376
+ const checkpoint = { task_name: taskName, story_key: storyKey, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
3377
+ updatedState = { ...updatedState, tasks_completed: [...updatedState.tasks_completed, checkpoint] };
3523
3378
  }
3524
3379
  updatedState = recordTraceId(traceId, updatedState);
3525
3380
  const durationMs = Date.now() - startMs;
@@ -3541,32 +3396,109 @@ ${coverageDedup}`;
3541
3396
  };
3542
3397
  writeOutputContract(contract, join12(projectDir, ".codeharness", "contracts"));
3543
3398
  } catch (err) {
3544
- const message = err instanceof Error ? err.message : String(err);
3545
- warn(`workflow-engine: failed to write output contract for ${taskName}/${storyKey}: ${message}`);
3399
+ const msg = err instanceof Error ? err.message : String(err);
3400
+ warn(`workflow-actors: failed to write output contract for ${taskName}/${storyKey}: ${msg}`);
3546
3401
  contract = null;
3547
3402
  }
3548
3403
  writeWorkflowState(updatedState, projectDir);
3549
- return { updatedState, output, contract };
3404
+ propagateVerifyFlags(taskName, contract, projectDir);
3405
+ return { output, cost, changedFiles, sessionId: resultSessionId, contract, updatedState };
3550
3406
  }
3551
- function buildRetryPrompt(storyKey, findings) {
3552
- const failedFindings = findings.filter(
3553
- (f) => f.status === "fail" || f.status === "unknown"
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
3554
3419
  );
3555
- if (failedFindings.length === 0) {
3556
- return `Implement story ${storyKey}`;
3557
- }
3558
- const formattedFindings = failedFindings.map((f) => {
3559
- const status = f.status.toUpperCase();
3560
- let entry = `AC #${f.ac} (${status}): ${f.description}`;
3561
- if (f.evidence?.reasoning) {
3562
- entry += `
3563
- Evidence: ${f.evidence.reasoning}`;
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;
3564
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
+ }
3457
+ }
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
+ }
3487
+ }
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}`;
3565
3497
  return entry;
3566
3498
  }).join("\n\n");
3567
3499
  return `Retry story ${storyKey}. Previous evaluator findings:
3568
3500
 
3569
- ${formattedFindings}
3501
+ ${formatted}
3570
3502
 
3571
3503
  Focus on fixing the failed criteria above.`;
3572
3504
  }
@@ -3575,40 +3507,23 @@ function buildAllUnknownVerdict(workItems, reasoning) {
3575
3507
  ac: index + 1,
3576
3508
  description: `AC #${index + 1}`,
3577
3509
  status: "unknown",
3578
- evidence: {
3579
- commands_run: [],
3580
- output_observed: "",
3581
- reasoning
3582
- }
3510
+ evidence: { commands_run: [], output_observed: "", reasoning }
3583
3511
  }));
3584
- return {
3585
- verdict: "fail",
3586
- score: {
3587
- passed: 0,
3588
- failed: 0,
3589
- unknown: findings.length,
3590
- total: findings.length
3591
- },
3592
- findings
3593
- };
3512
+ return { verdict: "fail", score: { passed: 0, failed: 0, unknown: findings.length, total: findings.length }, findings };
3594
3513
  }
3595
3514
  function getFailedItems(verdict, allItems) {
3596
3515
  if (!verdict) return allItems;
3597
3516
  if (verdict.verdict === "pass") return [];
3598
3517
  return allItems;
3599
3518
  }
3600
- async function executeLoopBlock(loopBlock, state, config, workItems, initialContract, storyFlowTasks) {
3519
+ function isLoopBlock(step) {
3520
+ return typeof step === "object" && step !== null && "loop" in step;
3521
+ }
3522
+ var loopIterationActor = fromPromise2(async ({ input }) => {
3523
+ const { loopBlock, config, workItems, storyFlowTasks, onStreamEvent, maxIterations } = input;
3524
+ let { currentState, errors, tasksCompleted, lastContract, lastVerdict, accumulatedCostUsd } = input;
3601
3525
  const projectDir = config.projectDir ?? process.cwd();
3602
- const maxIterations = config.maxIterations ?? DEFAULT_MAX_ITERATIONS;
3603
- const errors = [];
3604
- let tasksCompleted = 0;
3605
- let currentState = state;
3606
- let lastVerdict = null;
3607
- let lastOutputContract = initialContract ?? null;
3608
- let accumulatedCostUsd = 0;
3609
- if (loopBlock.loop.length === 0) {
3610
- return { state: currentState, errors, tasksCompleted, halted: false, lastContract: lastOutputContract };
3611
- }
3526
+ const RUN_SENTINEL = "__run__";
3612
3527
  const lastAgentTaskInLoop = (() => {
3613
3528
  for (let i = loopBlock.loop.length - 1; i >= 0; i--) {
3614
3529
  const tn = loopBlock.loop[i];
@@ -3617,266 +3532,201 @@ async function executeLoopBlock(loopBlock, state, config, workItems, initialCont
3617
3532
  }
3618
3533
  return loopBlock.loop[loopBlock.loop.length - 1];
3619
3534
  })();
3620
- while (true) {
3621
- const nextIteration = currentState.iteration + 1;
3622
- const allCurrentIterationDone = currentState.iteration > 0 && loopBlock.loop.every((tn) => {
3623
- const t = config.workflow.tasks[tn];
3624
- if (!t) return true;
3625
- if (storyFlowTasks?.has(tn)) {
3626
- return workItems.every((item) => isLoopTaskCompleted(currentState, tn, item.key, currentState.iteration));
3535
+ const nextIteration = currentState.iteration + 1;
3536
+ const allCurrentIterationDone = currentState.iteration > 0 && loopBlock.loop.every((tn) => {
3537
+ const t = config.workflow.tasks[tn];
3538
+ if (!t) return true;
3539
+ if (storyFlowTasks?.has(tn)) return workItems.every((item) => isLoopTaskCompleted(currentState, tn, item.key, currentState.iteration));
3540
+ return isLoopTaskCompleted(currentState, tn, RUN_SENTINEL, currentState.iteration);
3541
+ });
3542
+ if (currentState.iteration === 0 || allCurrentIterationDone) {
3543
+ currentState = { ...currentState, iteration: nextIteration };
3544
+ writeWorkflowState(currentState, projectDir);
3545
+ }
3546
+ let haltedInLoop = false;
3547
+ for (const taskName of loopBlock.loop) {
3548
+ const task = config.workflow.tasks[taskName];
3549
+ if (!task) {
3550
+ warn(`workflow-machine: task "${taskName}" not found in workflow tasks, skipping`);
3551
+ continue;
3552
+ }
3553
+ if (task.agent === null) {
3554
+ const items2 = storyFlowTasks?.has(taskName) ? lastVerdict ? getFailedItems(lastVerdict, workItems) : workItems : [{ key: RUN_SENTINEL, source: "sprint" }];
3555
+ for (const item of items2) {
3556
+ if (isLoopTaskCompleted(currentState, taskName, item.key, currentState.iteration)) {
3557
+ warn(`workflow-machine: skipping completed task ${taskName} for ${item.key}`);
3558
+ continue;
3559
+ }
3560
+ try {
3561
+ const nr = await nullTaskCore({ task, taskName, storyKey: item.key, config, workflowState: currentState, previousContract: lastContract, accumulatedCostUsd });
3562
+ currentState = nr.updatedState;
3563
+ lastContract = nr.contract;
3564
+ tasksCompleted++;
3565
+ } catch (err) {
3566
+ const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, item.key);
3567
+ errors.push(engineError);
3568
+ currentState = recordErrorInState(currentState, taskName, item.key, engineError);
3569
+ writeWorkflowState(currentState, projectDir);
3570
+ }
3627
3571
  }
3628
- return isLoopTaskCompleted(currentState, tn, PER_RUN_SENTINEL, currentState.iteration);
3629
- });
3630
- if (currentState.iteration === 0 || allCurrentIterationDone) {
3631
- currentState = {
3632
- ...currentState,
3633
- iteration: nextIteration
3634
- };
3635
- writeWorkflowState(currentState, projectDir);
3572
+ continue;
3573
+ }
3574
+ const definition = config.agents[task.agent];
3575
+ if (!definition) {
3576
+ warn(`workflow-machine: agent "${task.agent}" not found for task "${taskName}", skipping`);
3577
+ continue;
3636
3578
  }
3637
- let haltedInLoop = false;
3638
- for (const taskName of loopBlock.loop) {
3639
- const task = config.workflow.tasks[taskName];
3640
- if (!task) {
3641
- warn(`workflow-engine: task "${taskName}" not found in workflow tasks, skipping`);
3579
+ const items = storyFlowTasks?.has(taskName) ? lastVerdict ? getFailedItems(lastVerdict, workItems) : workItems : [{ key: RUN_SENTINEL, source: "sprint" }];
3580
+ for (const item of items) {
3581
+ if (isLoopTaskCompleted(currentState, taskName, item.key, currentState.iteration)) {
3582
+ warn(`workflow-machine: skipping completed task ${taskName} for ${item.key}`);
3642
3583
  continue;
3643
3584
  }
3644
- if (task.agent === null) {
3645
- if (storyFlowTasks?.has(taskName)) {
3646
- const itemsToProcess = lastVerdict ? getFailedItems(lastVerdict, workItems) : workItems;
3647
- for (const item of itemsToProcess) {
3648
- if (isLoopTaskCompleted(currentState, taskName, item.key, currentState.iteration)) {
3649
- warn(`workflow-engine: skipping completed task ${taskName} for ${item.key}`);
3650
- continue;
3651
- }
3652
- try {
3653
- const nullResult = await executeNullTask(
3654
- task,
3655
- taskName,
3656
- item.key,
3657
- currentState,
3658
- config,
3659
- lastOutputContract ?? void 0,
3660
- accumulatedCostUsd
3661
- );
3662
- currentState = nullResult.updatedState;
3663
- lastOutputContract = nullResult.contract;
3664
- tasksCompleted++;
3665
- } catch (err) {
3666
- const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, item.key);
3667
- errors.push(engineError);
3668
- currentState = recordErrorInState(currentState, taskName, item.key, engineError);
3669
- writeWorkflowState(currentState, projectDir);
3585
+ const prompt = lastVerdict ? buildRetryPrompt(item.key, lastVerdict.findings) : void 0;
3586
+ try {
3587
+ const dr = await dispatchTaskCore({ task, taskName, storyKey: item.key, definition, config, workflowState: currentState, previousContract: lastContract, onStreamEvent, customPrompt: prompt });
3588
+ currentState = dr.updatedState;
3589
+ lastContract = dr.contract;
3590
+ accumulatedCostUsd += dr.contract?.cost_usd ?? 0;
3591
+ tasksCompleted++;
3592
+ if (taskName === lastAgentTaskInLoop && !storyFlowTasks?.has(taskName)) {
3593
+ let verdict = null;
3594
+ try {
3595
+ verdict = parseVerdict(dr.output);
3596
+ } catch (parseErr) {
3597
+ if (parseErr instanceof VerdictParseError && parseErr.retryable) {
3598
+ try {
3599
+ const retryResult = await dispatchTaskCore({ task, taskName, storyKey: item.key, definition, config, workflowState: currentState, previousContract: lastContract, onStreamEvent });
3600
+ currentState = retryResult.updatedState;
3601
+ lastContract = retryResult.contract;
3602
+ tasksCompleted++;
3603
+ verdict = parseVerdict(retryResult.output);
3604
+ } catch {
3605
+ verdict = buildAllUnknownVerdict(workItems, "Evaluator failed after retry");
3606
+ }
3670
3607
  }
3671
3608
  }
3672
- } else {
3673
- if (isLoopTaskCompleted(currentState, taskName, PER_RUN_SENTINEL, currentState.iteration)) {
3674
- warn(`workflow-engine: skipping completed task ${taskName} for ${PER_RUN_SENTINEL}`);
3675
- continue;
3609
+ if (!verdict) {
3610
+ const tagged = parseVerdictTag(dr.output);
3611
+ if (tagged) verdict = { verdict: tagged.verdict, score: { passed: tagged.verdict === "pass" ? 1 : 0, failed: tagged.verdict === "fail" ? 1 : 0, unknown: 0, total: 1 }, findings: [] };
3676
3612
  }
3677
- try {
3678
- const nullResult = await executeNullTask(
3679
- task,
3680
- taskName,
3681
- PER_RUN_SENTINEL,
3682
- currentState,
3683
- config,
3684
- lastOutputContract ?? void 0,
3685
- accumulatedCostUsd
3686
- );
3687
- currentState = nullResult.updatedState;
3688
- lastOutputContract = nullResult.contract;
3689
- tasksCompleted++;
3690
- } catch (err) {
3691
- const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, PER_RUN_SENTINEL);
3692
- errors.push(engineError);
3693
- currentState = recordErrorInState(currentState, taskName, PER_RUN_SENTINEL, engineError);
3694
- writeWorkflowState(currentState, projectDir);
3613
+ lastVerdict = verdict;
3614
+ if (verdict) {
3615
+ const score = { iteration: currentState.iteration, passed: verdict.score.passed, failed: verdict.score.failed, unknown: verdict.score.unknown, total: verdict.score.total, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
3616
+ currentState = { ...currentState, evaluator_scores: [...currentState.evaluator_scores, score] };
3695
3617
  }
3696
- }
3697
- continue;
3698
- }
3699
- const definition = config.agents[task.agent];
3700
- if (!definition) {
3701
- warn(`workflow-engine: agent "${task.agent}" not found for task "${taskName}", skipping`);
3702
- continue;
3703
- }
3704
- if (storyFlowTasks?.has(taskName)) {
3705
- const itemsToRetry = lastVerdict ? getFailedItems(lastVerdict, workItems) : workItems;
3706
- for (const item of itemsToRetry) {
3707
- if (isLoopTaskCompleted(currentState, taskName, item.key, currentState.iteration)) {
3708
- warn(`workflow-engine: skipping completed task ${taskName} for ${item.key}`);
3709
- continue;
3710
- }
3711
- const prompt = lastVerdict ? buildRetryPrompt(item.key, lastVerdict.findings) : void 0;
3712
- try {
3713
- const dispatchResult = await dispatchTaskWithResult(
3714
- task,
3715
- taskName,
3716
- item.key,
3717
- definition,
3718
- currentState,
3719
- config,
3720
- prompt,
3721
- lastOutputContract ?? void 0
3722
- );
3723
- currentState = dispatchResult.updatedState;
3724
- lastOutputContract = dispatchResult.contract;
3725
- propagateVerifyFlags(taskName, dispatchResult.contract, projectDir);
3726
- accumulatedCostUsd += dispatchResult.contract?.cost_usd ?? 0;
3727
- tasksCompleted++;
3728
- } catch (err) {
3729
- const engineError = handleDispatchError(err, taskName, item.key);
3730
- errors.push(engineError);
3731
- currentState = recordErrorInState(currentState, taskName, item.key, engineError);
3618
+ const cbDecision = evaluateProgress(currentState.evaluator_scores);
3619
+ if (cbDecision.halt) {
3620
+ currentState = { ...currentState, circuit_breaker: { triggered: true, reason: cbDecision.reason, score_history: cbDecision.scoreHistory } };
3732
3621
  writeWorkflowState(currentState, projectDir);
3733
- if (err instanceof DispatchError && HALT_ERROR_CODES.has(err.code)) {
3734
- haltedInLoop = true;
3735
- break;
3736
- }
3737
- continue;
3738
- }
3739
- }
3740
- if (haltedInLoop) break;
3741
- } else {
3742
- if (isLoopTaskCompleted(currentState, taskName, PER_RUN_SENTINEL, currentState.iteration)) {
3743
- warn(`workflow-engine: skipping completed task ${taskName} for ${PER_RUN_SENTINEL}`);
3744
- continue;
3745
- }
3746
- try {
3747
- const dispatchResult = await dispatchTaskWithResult(
3748
- task,
3749
- taskName,
3750
- PER_RUN_SENTINEL,
3751
- definition,
3752
- currentState,
3753
- config,
3754
- void 0,
3755
- lastOutputContract ?? void 0
3756
- );
3757
- currentState = dispatchResult.updatedState;
3758
- lastOutputContract = dispatchResult.contract;
3759
- propagateVerifyFlags(taskName, dispatchResult.contract, projectDir);
3760
- accumulatedCostUsd += dispatchResult.contract?.cost_usd ?? 0;
3761
- tasksCompleted++;
3762
- const isLastTaskInLoop = taskName === lastAgentTaskInLoop;
3763
- if (isLastTaskInLoop) {
3764
- let verdict = null;
3765
- try {
3766
- verdict = parseVerdict(dispatchResult.output);
3767
- } catch (parseErr) {
3768
- if (parseErr instanceof VerdictParseError && parseErr.retryable) {
3769
- warn(`workflow-engine: verdict parse failed, retrying evaluator for ${taskName}`);
3770
- try {
3771
- const retryResult = await dispatchTaskWithResult(
3772
- task,
3773
- taskName,
3774
- PER_RUN_SENTINEL,
3775
- definition,
3776
- currentState,
3777
- config,
3778
- void 0,
3779
- lastOutputContract ?? void 0
3780
- );
3781
- currentState = retryResult.updatedState;
3782
- lastOutputContract = retryResult.contract;
3783
- propagateVerifyFlags(taskName, retryResult.contract, projectDir);
3784
- tasksCompleted++;
3785
- verdict = parseVerdict(retryResult.output);
3786
- } catch {
3787
- verdict = buildAllUnknownVerdict(
3788
- workItems,
3789
- "Evaluator failed to produce valid JSON after retry"
3790
- );
3791
- }
3792
- }
3793
- }
3794
- if (!verdict) {
3795
- const tagged = parseVerdictTag(dispatchResult.output);
3796
- if (tagged) {
3797
- verdict = {
3798
- verdict: tagged.verdict,
3799
- score: { passed: tagged.verdict === "pass" ? 1 : 0, failed: tagged.verdict === "fail" ? 1 : 0, unknown: 0, total: 1 },
3800
- findings: []
3801
- };
3802
- }
3803
- }
3804
- lastVerdict = verdict;
3805
- if (verdict) {
3806
- const score = {
3807
- iteration: currentState.iteration,
3808
- passed: verdict.score.passed,
3809
- failed: verdict.score.failed,
3810
- unknown: verdict.score.unknown,
3811
- total: verdict.score.total,
3812
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
3813
- };
3814
- currentState = {
3815
- ...currentState,
3816
- evaluator_scores: [...currentState.evaluator_scores, score]
3817
- };
3818
- } else {
3819
- const totalItems = workItems.length;
3820
- const score = {
3821
- iteration: currentState.iteration,
3822
- passed: 0,
3823
- failed: 0,
3824
- unknown: totalItems,
3825
- total: totalItems,
3826
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
3827
- };
3828
- currentState = {
3829
- ...currentState,
3830
- evaluator_scores: [...currentState.evaluator_scores, score]
3831
- };
3832
- }
3833
- const cbDecision = evaluateProgress(currentState.evaluator_scores);
3834
- if (cbDecision.halt) {
3835
- currentState = {
3836
- ...currentState,
3837
- circuit_breaker: {
3838
- triggered: true,
3839
- reason: cbDecision.reason,
3840
- score_history: cbDecision.scoreHistory
3841
- }
3842
- };
3843
- writeWorkflowState(currentState, projectDir);
3844
- }
3845
3622
  }
3846
3623
  writeWorkflowState(currentState, projectDir);
3847
- } catch (err) {
3848
- const engineError = handleDispatchError(err, taskName, PER_RUN_SENTINEL);
3849
- errors.push(engineError);
3850
- currentState = recordErrorInState(currentState, taskName, PER_RUN_SENTINEL, engineError);
3851
- writeWorkflowState(currentState, projectDir);
3852
- lastVerdict = null;
3853
- if (err instanceof DispatchError && HALT_ERROR_CODES.has(err.code)) {
3854
- haltedInLoop = true;
3855
- break;
3856
- }
3857
- continue;
3624
+ }
3625
+ } catch (err) {
3626
+ const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, item.key);
3627
+ errors.push(engineError);
3628
+ currentState = recordErrorInState(currentState, taskName, item.key, engineError);
3629
+ writeWorkflowState(currentState, projectDir);
3630
+ if (err instanceof DispatchError && HALT_ERROR_CODES.has(err.code)) {
3631
+ haltedInLoop = true;
3632
+ break;
3858
3633
  }
3859
3634
  }
3860
3635
  }
3861
- if (haltedInLoop) {
3862
- return { state: currentState, errors, tasksCompleted, halted: true, lastContract: lastOutputContract };
3863
- }
3864
- if (lastVerdict?.verdict === "pass") {
3865
- return { state: currentState, errors, tasksCompleted, halted: false, lastContract: lastOutputContract };
3866
- }
3867
- if (currentState.iteration >= maxIterations) {
3868
- currentState = { ...currentState, phase: "max-iterations" };
3869
- writeWorkflowState(currentState, projectDir);
3870
- return { state: currentState, errors, tasksCompleted, halted: false, lastContract: lastOutputContract };
3871
- }
3872
- if (currentState.circuit_breaker.triggered) {
3873
- currentState = { ...currentState, phase: "circuit-breaker" };
3874
- writeWorkflowState(currentState, projectDir);
3875
- return { state: currentState, errors, tasksCompleted, halted: false, lastContract: lastOutputContract };
3876
- }
3877
- }
3636
+ if (haltedInLoop) break;
3637
+ }
3638
+ return { ...input, currentState, errors, tasksCompleted, halted: haltedInLoop, lastContract, lastVerdict, accumulatedCostUsd };
3639
+ });
3640
+ var loopMachine = setup({
3641
+ types: {},
3642
+ actors: { loopIterationActor },
3643
+ guards: {
3644
+ halted: ({ context }) => context.halted,
3645
+ verdictPass: ({ context }) => context.lastVerdict?.verdict === "pass",
3646
+ maxIterations: ({ context }) => context.currentState.iteration >= context.maxIterations,
3647
+ circuitBreaker: ({ context }) => context.currentState.circuit_breaker.triggered
3648
+ }
3649
+ }).createMachine({
3650
+ id: "loopBlock",
3651
+ context: ({ input }) => input,
3652
+ initial: "checkEmpty",
3653
+ states: {
3654
+ checkEmpty: {
3655
+ always: [
3656
+ { guard: ({ context }) => context.loopBlock.loop.length === 0, target: "done" },
3657
+ { target: "iterating" }
3658
+ ]
3659
+ },
3660
+ iterating: {
3661
+ invoke: {
3662
+ src: "loopIterationActor",
3663
+ input: ({ context }) => context,
3664
+ onDone: {
3665
+ target: "checkTermination",
3666
+ actions: assign(({ event }) => event.output)
3667
+ },
3668
+ onError: { target: "halted" }
3669
+ }
3670
+ },
3671
+ checkTermination: {
3672
+ always: [
3673
+ { guard: "halted", target: "halted" },
3674
+ { guard: "verdictPass", target: "done" },
3675
+ { guard: "maxIterations", target: "maxIterationsReached" },
3676
+ { guard: "circuitBreaker", target: "circuitBreakerTriggered" },
3677
+ { target: "iterating" }
3678
+ ]
3679
+ },
3680
+ done: { type: "final" },
3681
+ halted: { type: "final" },
3682
+ maxIterationsReached: {
3683
+ type: "final",
3684
+ entry: [
3685
+ assign(({ context }) => ({ ...context, currentState: { ...context.currentState, phase: "max-iterations" } })),
3686
+ ({ context }) => {
3687
+ const projectDir = context.config.projectDir ?? process.cwd();
3688
+ writeWorkflowState(context.currentState, projectDir);
3689
+ }
3690
+ ]
3691
+ },
3692
+ circuitBreakerTriggered: {
3693
+ type: "final",
3694
+ entry: [
3695
+ assign(({ context }) => ({ ...context, currentState: { ...context.currentState, phase: "circuit-breaker" } })),
3696
+ ({ context }) => {
3697
+ const projectDir = context.config.projectDir ?? process.cwd();
3698
+ writeWorkflowState(context.currentState, projectDir);
3699
+ }
3700
+ ]
3701
+ }
3702
+ }
3703
+ });
3704
+ async function executeLoopBlock(loopBlock, state, config, workItems, initialContract, storyFlowTasks, onStreamEvent) {
3705
+ const input = {
3706
+ loopBlock,
3707
+ config,
3708
+ workItems,
3709
+ storyFlowTasks,
3710
+ onStreamEvent,
3711
+ maxIterations: config.maxIterations ?? DEFAULT_MAX_ITERATIONS,
3712
+ currentState: state,
3713
+ errors: [],
3714
+ tasksCompleted: 0,
3715
+ halted: false,
3716
+ lastContract: initialContract ?? null,
3717
+ lastVerdict: null,
3718
+ accumulatedCostUsd: 0
3719
+ };
3720
+ const actor = createActor(loopMachine, { input });
3721
+ return new Promise((resolve7) => {
3722
+ actor.subscribe({ complete: () => {
3723
+ const snap = actor.getSnapshot();
3724
+ const ctx = snap.context;
3725
+ resolve7({ state: ctx.currentState, errors: ctx.errors, tasksCompleted: ctx.tasksCompleted, halted: ctx.halted, lastContract: ctx.lastContract });
3726
+ } });
3727
+ actor.start();
3728
+ });
3878
3729
  }
3879
- var HEALTH_CHECK_TIMEOUT_MS = 5e3;
3880
3730
  async function checkDriverHealth(workflow, timeoutMs) {
3881
3731
  const driverNames = /* @__PURE__ */ new Set();
3882
3732
  for (const task of Object.values(workflow.tasks)) {
@@ -3884,9 +3734,7 @@ async function checkDriverHealth(workflow, timeoutMs) {
3884
3734
  driverNames.add(task.driver ?? "claude-code");
3885
3735
  }
3886
3736
  const drivers = /* @__PURE__ */ new Map();
3887
- for (const name of driverNames) {
3888
- drivers.set(name, getDriver(name));
3889
- }
3737
+ for (const name of driverNames) drivers.set(name, getDriver(name));
3890
3738
  const responded = /* @__PURE__ */ new Set();
3891
3739
  const healthChecks = Promise.all(
3892
3740
  [...drivers.entries()].map(async ([name, driver]) => {
@@ -3903,78 +3751,378 @@ async function checkDriverHealth(workflow, timeoutMs) {
3903
3751
  const result = await Promise.race([healthChecks, timeoutPromise]);
3904
3752
  if (result === "timeout") {
3905
3753
  const pending = [...driverNames].filter((n) => !responded.has(n));
3906
- const names = pending.length > 0 ? pending.join(", ") : [...driverNames].join(", ");
3907
- throw new Error(
3908
- `Driver health check timed out after ${effectiveTimeout}ms. Drivers that did not respond: ${names}`
3909
- );
3754
+ throw new Error(`Driver health check timed out after ${effectiveTimeout}ms. Drivers: ${(pending.length > 0 ? pending : [...driverNames]).join(", ")}`);
3910
3755
  }
3911
3756
  clearTimeout(timer);
3912
3757
  const failures = result.filter((r) => !r.health.available);
3913
3758
  if (failures.length > 0) {
3914
- const details = failures.map((f) => `${f.name}: ${f.health.error ?? "unavailable"}`).join("; ");
3915
- throw new Error(`Driver health check failed: ${details}`);
3759
+ throw new Error(`Driver health check failed: ${failures.map((f) => `${f.name}: ${f.health.error ?? "unavailable"}`).join("; ")}`);
3916
3760
  }
3917
3761
  }
3918
- async function executeWorkflow(config) {
3919
- const startMs = Date.now();
3762
+ function collectGuideFiles(epicItems, epicSentinel, projectDir) {
3763
+ const guidesDir = join13(projectDir, ".codeharness", "verify-guides");
3764
+ const guideFiles = [];
3765
+ try {
3766
+ mkdirSync6(guidesDir, { recursive: true });
3767
+ } catch {
3768
+ return guideFiles;
3769
+ }
3770
+ for (const item of epicItems) {
3771
+ try {
3772
+ const contractPath = join13(projectDir, ".codeharness", "contracts", `document-${item.key}.json`);
3773
+ if (existsSync15(contractPath)) {
3774
+ const contractData = JSON.parse(readFileSync13(contractPath, "utf-8"));
3775
+ const docs = contractData.output ? extractTag(contractData.output, "user-docs") ?? contractData.output : null;
3776
+ if (docs) {
3777
+ const guidePath = join13(guidesDir, `${item.key}-guide.md`);
3778
+ writeFileSync8(guidePath, docs, "utf-8");
3779
+ guideFiles.push(guidePath);
3780
+ }
3781
+ }
3782
+ } catch {
3783
+ }
3784
+ }
3785
+ try {
3786
+ const deployContractPath = join13(projectDir, ".codeharness", "contracts", `deploy-${epicSentinel}.json`);
3787
+ if (existsSync15(deployContractPath)) {
3788
+ const deployData = JSON.parse(readFileSync13(deployContractPath, "utf-8"));
3789
+ const report = deployData.output ? extractTag(deployData.output, "deploy-report") ?? deployData.output : null;
3790
+ if (report) {
3791
+ const deployPath = join13(guidesDir, "deploy-info.md");
3792
+ writeFileSync8(deployPath, report, "utf-8");
3793
+ guideFiles.push(deployPath);
3794
+ }
3795
+ }
3796
+ } catch {
3797
+ }
3798
+ return guideFiles;
3799
+ }
3800
+ function cleanupGuideFiles(projectDir) {
3801
+ const guidesDir = join13(projectDir, ".codeharness", "verify-guides");
3802
+ try {
3803
+ rmSync2(guidesDir, { recursive: true, force: true });
3804
+ } catch {
3805
+ }
3806
+ }
3807
+ var storyFlowActor = fromPromise2(async ({ input }) => {
3808
+ const { item, config, storyFlowTasks } = input;
3809
+ let { workflowState: state, lastContract, accumulatedCostUsd } = input;
3920
3810
  const projectDir = config.projectDir ?? process.cwd();
3921
3811
  const errors = [];
3922
3812
  let tasksCompleted = 0;
3923
- const processedStories = /* @__PURE__ */ new Set();
3813
+ let halted = false;
3814
+ for (const storyStep of config.workflow.storyFlow) {
3815
+ if (halted || config.abortSignal?.aborted) {
3816
+ halted = true;
3817
+ break;
3818
+ }
3819
+ if (isLoopBlock(storyStep)) {
3820
+ const loopResult = await executeLoopBlock(storyStep, state, config, [item], lastContract, storyFlowTasks);
3821
+ state = loopResult.state;
3822
+ errors.push(...loopResult.errors);
3823
+ tasksCompleted += loopResult.tasksCompleted;
3824
+ lastContract = loopResult.lastContract;
3825
+ if (loopResult.halted || state.phase === "max-iterations" || state.phase === "circuit-breaker") {
3826
+ halted = true;
3827
+ break;
3828
+ }
3829
+ continue;
3830
+ }
3831
+ if (typeof storyStep !== "string") continue;
3832
+ const taskName = storyStep;
3833
+ const task = config.workflow.tasks[taskName];
3834
+ if (!task) {
3835
+ warn(`workflow-machine: task "${taskName}" not found in workflow tasks, skipping`);
3836
+ continue;
3837
+ }
3838
+ if (task.agent === null) {
3839
+ if (isTaskCompleted(state, taskName, item.key)) continue;
3840
+ try {
3841
+ const nr = await nullTaskCore({ task, taskName, storyKey: item.key, config, workflowState: state, previousContract: lastContract, accumulatedCostUsd });
3842
+ state = nr.updatedState;
3843
+ lastContract = nr.contract;
3844
+ tasksCompleted++;
3845
+ } catch (err) {
3846
+ const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, item.key);
3847
+ errors.push(engineError);
3848
+ state = recordErrorInState(state, taskName, item.key, engineError);
3849
+ writeWorkflowState(state, projectDir);
3850
+ break;
3851
+ }
3852
+ continue;
3853
+ }
3854
+ const definition = config.agents[task.agent];
3855
+ if (!definition) {
3856
+ warn(`workflow-machine: agent "${task.agent}" not found for task "${taskName}", skipping`);
3857
+ continue;
3858
+ }
3859
+ if (isTaskCompleted(state, taskName, item.key)) continue;
3860
+ try {
3861
+ const dr = await dispatchTaskCore({ task, taskName, storyKey: item.key, definition, config, workflowState: state, previousContract: lastContract });
3862
+ state = dr.updatedState;
3863
+ lastContract = dr.contract;
3864
+ accumulatedCostUsd += dr.contract?.cost_usd ?? 0;
3865
+ tasksCompleted++;
3866
+ } catch (err) {
3867
+ const engineError = handleDispatchError(err, taskName, item.key);
3868
+ errors.push(engineError);
3869
+ if (config.onEvent) config.onEvent({ type: "dispatch-error", taskName, storyKey: item.key, error: { code: engineError.code, message: engineError.message } });
3870
+ state = recordErrorInState(state, taskName, item.key, engineError);
3871
+ writeWorkflowState(state, projectDir);
3872
+ if (err instanceof DispatchError && HALT_ERROR_CODES.has(err.code)) halted = true;
3873
+ break;
3874
+ }
3875
+ }
3876
+ if (!halted && config.onEvent) {
3877
+ config.onEvent({ type: "story-done", taskName: "story_flow", storyKey: item.key });
3878
+ }
3879
+ return { workflowState: state, errors, tasksCompleted, lastContract, accumulatedCostUsd, halted };
3880
+ });
3881
+ var epicStepActor = fromPromise2(async ({ input }) => {
3882
+ const { epicId, epicItems, config, storyFlowTasks } = input;
3883
+ let { workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted, currentStepIndex } = input;
3884
+ const projectDir = config.projectDir ?? process.cwd();
3885
+ const step = config.workflow.epicFlow[currentStepIndex];
3886
+ if (!step || halted || config.abortSignal?.aborted) {
3887
+ if (config.abortSignal?.aborted) {
3888
+ state = { ...state, phase: "interrupted" };
3889
+ writeWorkflowState(state, projectDir);
3890
+ }
3891
+ return { ...input, workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted: true, currentStepIndex };
3892
+ }
3893
+ if (step === "story_flow") {
3894
+ for (const item of epicItems) {
3895
+ if (halted || config.abortSignal?.aborted) {
3896
+ halted = true;
3897
+ break;
3898
+ }
3899
+ storiesProcessed.add(item.key);
3900
+ const storyResult = await new Promise((resolve7, reject) => {
3901
+ const a = createActor(storyFlowActor, { input: { item, config, workflowState: state, lastContract, accumulatedCostUsd, storyFlowTasks } });
3902
+ a.subscribe({ complete: () => resolve7(a.getSnapshot().output), error: reject });
3903
+ a.start();
3904
+ });
3905
+ state = storyResult.workflowState;
3906
+ errors.push(...storyResult.errors);
3907
+ tasksCompleted += storyResult.tasksCompleted;
3908
+ lastContract = storyResult.lastContract;
3909
+ accumulatedCostUsd = storyResult.accumulatedCostUsd;
3910
+ if (storyResult.halted) {
3911
+ halted = true;
3912
+ break;
3913
+ }
3914
+ }
3915
+ return { ...input, workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted, currentStepIndex: currentStepIndex + 1 };
3916
+ }
3917
+ if (isLoopBlock(step)) {
3918
+ const loopResult = await executeLoopBlock(step, state, config, epicItems, lastContract, storyFlowTasks);
3919
+ state = loopResult.state;
3920
+ errors.push(...loopResult.errors);
3921
+ tasksCompleted += loopResult.tasksCompleted;
3922
+ lastContract = loopResult.lastContract;
3923
+ for (const item of epicItems) storiesProcessed.add(item.key);
3924
+ if (loopResult.halted || state.phase === "max-iterations" || state.phase === "circuit-breaker") halted = true;
3925
+ return { ...input, workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted, currentStepIndex: currentStepIndex + 1 };
3926
+ }
3927
+ const taskName = step;
3928
+ const task = config.workflow.tasks[taskName];
3929
+ if (!task) {
3930
+ warn(`workflow-machine: task "${taskName}" not found in workflow tasks, skipping`);
3931
+ return { ...input, currentStepIndex: currentStepIndex + 1 };
3932
+ }
3933
+ const epicSentinel = `__epic_${epicId}__`;
3934
+ if (task.agent === null) {
3935
+ if (!isTaskCompleted(state, taskName, epicSentinel)) {
3936
+ try {
3937
+ const nr = await nullTaskCore({ task, taskName, storyKey: epicSentinel, config, workflowState: state, previousContract: lastContract, accumulatedCostUsd });
3938
+ state = nr.updatedState;
3939
+ lastContract = nr.contract;
3940
+ tasksCompleted++;
3941
+ } catch (err) {
3942
+ const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, epicSentinel);
3943
+ errors.push(engineError);
3944
+ state = recordErrorInState(state, taskName, epicSentinel, engineError);
3945
+ writeWorkflowState(state, projectDir);
3946
+ }
3947
+ }
3948
+ return { ...input, workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted, currentStepIndex: currentStepIndex + 1 };
3949
+ }
3950
+ const definition = config.agents[task.agent];
3951
+ if (!definition) {
3952
+ warn(`workflow-machine: agent "${task.agent}" not found for task "${taskName}", skipping`);
3953
+ return { ...input, currentStepIndex: currentStepIndex + 1 };
3954
+ }
3955
+ if (isTaskCompleted(state, taskName, epicSentinel)) {
3956
+ return { ...input, currentStepIndex: currentStepIndex + 1 };
3957
+ }
3958
+ let guideFiles = [];
3959
+ if (task.source_access === false) guideFiles = collectGuideFiles(epicItems, epicSentinel, projectDir);
3960
+ try {
3961
+ const dr = await dispatchTaskCore({ task, taskName, storyKey: epicSentinel, definition, config, workflowState: state, previousContract: lastContract, storyFiles: guideFiles });
3962
+ state = dr.updatedState;
3963
+ lastContract = dr.contract;
3964
+ accumulatedCostUsd += dr.contract?.cost_usd ?? 0;
3965
+ tasksCompleted++;
3966
+ } catch (err) {
3967
+ const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, epicSentinel);
3968
+ errors.push(engineError);
3969
+ if (config.onEvent) config.onEvent({ type: "dispatch-error", taskName, storyKey: epicSentinel, error: { code: engineError.code, message: engineError.message } });
3970
+ state = recordErrorInState(state, taskName, epicSentinel, engineError);
3971
+ writeWorkflowState(state, projectDir);
3972
+ if (err instanceof DispatchError && HALT_ERROR_CODES.has(err.code)) halted = true;
3973
+ } finally {
3974
+ if (guideFiles.length > 0) cleanupGuideFiles(projectDir);
3975
+ }
3976
+ return { ...input, workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted, currentStepIndex: currentStepIndex + 1 };
3977
+ });
3978
+ var epicMachine = setup({
3979
+ types: {},
3980
+ actors: { epicStepActor },
3981
+ guards: {
3982
+ epicDone: ({ context }) => context.halted || context.currentStepIndex >= context.config.workflow.epicFlow.length
3983
+ }
3984
+ }).createMachine({
3985
+ id: "epic",
3986
+ context: ({ input }) => input,
3987
+ initial: "processingStep",
3988
+ states: {
3989
+ processingStep: {
3990
+ invoke: {
3991
+ src: "epicStepActor",
3992
+ input: ({ context }) => context,
3993
+ onDone: {
3994
+ target: "checkNext",
3995
+ actions: assign(({ event }) => event.output)
3996
+ },
3997
+ onError: {
3998
+ target: "done",
3999
+ actions: assign(({ context, event }) => {
4000
+ const msg = event.error instanceof Error ? event.error.message : String(event.error);
4001
+ return { ...context, errors: [...context.errors, { taskName: "__epic_actor__", storyKey: context.epicId, code: "ACTOR_ERROR", message: msg }], halted: true };
4002
+ })
4003
+ }
4004
+ }
4005
+ },
4006
+ checkNext: {
4007
+ always: [
4008
+ { guard: "epicDone", target: "done" },
4009
+ { target: "processingStep" }
4010
+ ]
4011
+ },
4012
+ done: { type: "final" }
4013
+ }
4014
+ });
4015
+ var runEpicActor = fromPromise2(async ({ input }) => {
4016
+ const { config, storyFlowTasks, epicEntries, currentEpicIndex } = input;
4017
+ let { workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted } = input;
4018
+ if (currentEpicIndex >= epicEntries.length || halted || config.abortSignal?.aborted) {
4019
+ if (config.abortSignal?.aborted) {
4020
+ const projectDir = config.projectDir ?? process.cwd();
4021
+ state = { ...state, phase: "interrupted" };
4022
+ writeWorkflowState(state, projectDir);
4023
+ }
4024
+ return { ...input, workflowState: state, halted: true };
4025
+ }
4026
+ const [epicId, epicItems] = epicEntries[currentEpicIndex];
4027
+ if (config.onEvent) {
4028
+ config.onEvent({ type: "dispatch-start", taskName: "story_flow", storyKey: `__epic_${epicId}__` });
4029
+ }
4030
+ const epicInput = {
4031
+ epicId,
4032
+ epicItems,
4033
+ config,
4034
+ storyFlowTasks,
4035
+ currentStoryIndex: 0,
4036
+ workflowState: state,
4037
+ errors: [],
4038
+ tasksCompleted: 0,
4039
+ storiesProcessed: /* @__PURE__ */ new Set(),
4040
+ lastContract,
4041
+ accumulatedCostUsd,
4042
+ halted: false,
4043
+ currentStepIndex: 0
4044
+ };
4045
+ const epicResult = await new Promise((resolve7) => {
4046
+ const actor = createActor(epicMachine, { input: epicInput });
4047
+ actor.subscribe({ complete: () => resolve7(actor.getSnapshot().context) });
4048
+ actor.start();
4049
+ });
4050
+ state = epicResult.workflowState;
4051
+ errors.push(...epicResult.errors);
4052
+ tasksCompleted += epicResult.tasksCompleted;
4053
+ for (const key of epicResult.storiesProcessed) storiesProcessed.add(key);
4054
+ lastContract = epicResult.lastContract;
4055
+ accumulatedCostUsd = epicResult.accumulatedCostUsd;
4056
+ halted = epicResult.halted;
4057
+ return { ...input, workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted, currentEpicIndex: currentEpicIndex + 1 };
4058
+ });
4059
+ var runMachine = setup({
4060
+ types: {},
4061
+ actors: { runEpicActor },
4062
+ guards: {
4063
+ allEpicsDone: ({ context }) => context.halted || context.currentEpicIndex >= context.epicEntries.length
4064
+ }
4065
+ }).createMachine({
4066
+ id: "run",
4067
+ context: ({ input }) => input,
4068
+ initial: "processingEpic",
4069
+ states: {
4070
+ processingEpic: {
4071
+ invoke: {
4072
+ src: "runEpicActor",
4073
+ input: ({ context }) => context,
4074
+ onDone: {
4075
+ target: "checkNext",
4076
+ actions: assign(({ event }) => event.output)
4077
+ },
4078
+ onError: {
4079
+ target: "allDone",
4080
+ actions: assign(({ context, event }) => {
4081
+ const msg = event.error instanceof Error ? event.error.message : String(event.error);
4082
+ return { ...context, errors: [...context.errors, { taskName: "__run_actor__", storyKey: "__run__", code: "ACTOR_ERROR", message: msg }], halted: true };
4083
+ })
4084
+ }
4085
+ }
4086
+ },
4087
+ checkNext: {
4088
+ always: [
4089
+ { guard: "allEpicsDone", target: "allDone" },
4090
+ { target: "processingEpic" }
4091
+ ]
4092
+ },
4093
+ allDone: { type: "final" }
4094
+ }
4095
+ });
4096
+ async function runWorkflowActor(config) {
4097
+ const startMs = Date.now();
4098
+ const projectDir = config.projectDir ?? process.cwd();
3924
4099
  let state = readWorkflowState(projectDir);
3925
4100
  if (state.phase === "completed") {
3926
- return {
3927
- success: true,
3928
- tasksCompleted: 0,
3929
- storiesProcessed: 0,
3930
- errors: [],
3931
- durationMs: 0
3932
- };
4101
+ return { success: true, tasksCompleted: 0, storiesProcessed: 0, errors: [], durationMs: 0 };
3933
4102
  }
3934
4103
  if (state.phase === "error" || state.phase === "failed") {
3935
4104
  const errorCount = state.tasks_completed.filter((t) => t.error).length;
3936
- if (!config.onEvent) info(`Resuming from ${state.phase} state \u2014 ${errorCount} previous error(s), retrying failed tasks`);
4105
+ if (!config.onEvent) info(`Resuming from ${state.phase} state \u2014 ${errorCount} previous error(s)`);
3937
4106
  }
3938
- state = {
3939
- ...state,
3940
- phase: "executing",
3941
- started: state.started || (/* @__PURE__ */ new Date()).toISOString(),
3942
- workflow_name: config.workflow.storyFlow.filter((s) => typeof s === "string").join(" -> ")
3943
- };
4107
+ state = { ...state, phase: "executing", started: state.started || (/* @__PURE__ */ new Date()).toISOString(), workflow_name: config.workflow.storyFlow.filter((s) => typeof s === "string").join(" -> ") };
3944
4108
  writeWorkflowState(state, projectDir);
3945
4109
  try {
3946
4110
  await checkDriverHealth(config.workflow);
3947
4111
  } catch (err) {
3948
4112
  const message = err instanceof Error ? err.message : String(err);
3949
4113
  state = { ...state, phase: "failed" };
3950
- const engineError = {
3951
- taskName: "__health_check__",
3952
- storyKey: "__health_check__",
3953
- code: "HEALTH_CHECK",
3954
- message
3955
- };
3956
- errors.push(engineError);
4114
+ const errors2 = [{ taskName: "__health_check__", storyKey: "__health_check__", code: "HEALTH_CHECK", message }];
3957
4115
  writeWorkflowState(state, projectDir);
3958
- return {
3959
- success: false,
3960
- tasksCompleted: 0,
3961
- storiesProcessed: 0,
3962
- errors,
3963
- durationMs: Date.now() - startMs
3964
- };
4116
+ return { success: false, tasksCompleted: 0, storiesProcessed: 0, errors: errors2, durationMs: Date.now() - startMs };
3965
4117
  }
3966
4118
  const capWarnings = checkCapabilityConflicts(config.workflow);
3967
- for (const cw of capWarnings) {
3968
- warn(cw.message);
3969
- }
4119
+ for (const cw of capWarnings) warn(cw.message);
3970
4120
  const workItems = loadWorkItems(config.sprintStatusPath, config.issuesPath);
3971
4121
  const storyFlowTasks = /* @__PURE__ */ new Set();
3972
4122
  for (const step of config.workflow.storyFlow) {
3973
4123
  if (typeof step === "string") storyFlowTasks.add(step);
3974
4124
  if (typeof step === "object" && "loop" in step) {
3975
- for (const loopTask of step.loop) {
3976
- storyFlowTasks.add(loopTask);
3977
- }
4125
+ for (const lt of step.loop) storyFlowTasks.add(lt);
3978
4126
  }
3979
4127
  }
3980
4128
  const epicGroups = /* @__PURE__ */ new Map();
@@ -3983,196 +4131,28 @@ async function executeWorkflow(config) {
3983
4131
  if (!epicGroups.has(epicId)) epicGroups.set(epicId, []);
3984
4132
  epicGroups.get(epicId).push(item);
3985
4133
  }
3986
- let halted = false;
3987
- let lastOutputContract = null;
3988
- let accumulatedCostUsd = 0;
3989
- for (const [epicId, epicItems] of epicGroups) {
3990
- if (halted) break;
3991
- if (config.abortSignal?.aborted) {
3992
- if (!config.onEvent) info("Execution interrupted \u2014 saving state");
3993
- state = { ...state, phase: "interrupted" };
3994
- writeWorkflowState(state, projectDir);
3995
- halted = true;
3996
- break;
3997
- }
3998
- if (config.onEvent) {
3999
- config.onEvent({ type: "dispatch-start", taskName: "story_flow", storyKey: `__epic_${epicId}__` });
4000
- } else {
4001
- info(`[epic-${epicId}] Starting epic with ${epicItems.length} stories`);
4002
- }
4003
- for (const step of config.workflow.epicFlow) {
4004
- if (halted) break;
4005
- if (config.abortSignal?.aborted) {
4006
- state = { ...state, phase: "interrupted" };
4007
- writeWorkflowState(state, projectDir);
4008
- halted = true;
4009
- break;
4010
- }
4011
- if (step === "story_flow") {
4012
- for (const item of epicItems) {
4013
- if (halted || config.abortSignal?.aborted) {
4014
- if (config.abortSignal?.aborted) {
4015
- state = { ...state, phase: "interrupted" };
4016
- writeWorkflowState(state, projectDir);
4017
- }
4018
- halted = true;
4019
- break;
4020
- }
4021
- processedStories.add(item.key);
4022
- for (const storyStep of config.workflow.storyFlow) {
4023
- if (halted || config.abortSignal?.aborted) {
4024
- halted = true;
4025
- break;
4026
- }
4027
- if (isLoopBlock(storyStep)) {
4028
- const loopResult = await executeLoopBlock(
4029
- storyStep,
4030
- state,
4031
- config,
4032
- [item],
4033
- lastOutputContract,
4034
- storyFlowTasks
4035
- );
4036
- state = loopResult.state;
4037
- errors.push(...loopResult.errors);
4038
- tasksCompleted += loopResult.tasksCompleted;
4039
- lastOutputContract = loopResult.lastContract;
4040
- if (loopResult.halted || state.phase === "max-iterations" || state.phase === "circuit-breaker") {
4041
- halted = true;
4042
- break;
4043
- }
4044
- continue;
4045
- }
4046
- if (typeof storyStep !== "string") continue;
4047
- const taskName2 = storyStep;
4048
- const task2 = config.workflow.tasks[taskName2];
4049
- if (!task2) {
4050
- warn(`workflow-engine: task "${taskName2}" not found, skipping`);
4051
- continue;
4052
- }
4053
- if (task2.agent === null) continue;
4054
- const definition2 = config.agents[task2.agent];
4055
- if (!definition2) {
4056
- warn(`workflow-engine: agent "${task2.agent}" not found for "${taskName2}"`);
4057
- continue;
4058
- }
4059
- if (isTaskCompleted(state, taskName2, item.key)) continue;
4060
- try {
4061
- const dr = await dispatchTaskWithResult(task2, taskName2, item.key, definition2, state, config, void 0, lastOutputContract ?? void 0);
4062
- state = dr.updatedState;
4063
- lastOutputContract = dr.contract;
4064
- propagateVerifyFlags(taskName2, dr.contract, projectDir);
4065
- accumulatedCostUsd += dr.contract?.cost_usd ?? 0;
4066
- tasksCompleted++;
4067
- } catch (err) {
4068
- const engineError = handleDispatchError(err, taskName2, item.key);
4069
- errors.push(engineError);
4070
- if (config.onEvent) {
4071
- config.onEvent({ type: "dispatch-error", taskName: taskName2, storyKey: item.key, error: { code: engineError.code, message: engineError.message } });
4072
- } else {
4073
- warn(`[${taskName2}] ${item.key} \u2014 ERROR: [${engineError.code}] ${engineError.message}`);
4074
- }
4075
- state = recordErrorInState(state, taskName2, item.key, engineError);
4076
- writeWorkflowState(state, projectDir);
4077
- if (err instanceof DispatchError && HALT_ERROR_CODES.has(err.code)) {
4078
- halted = true;
4079
- }
4080
- break;
4081
- }
4082
- }
4083
- }
4084
- continue;
4085
- }
4086
- if (isLoopBlock(step)) {
4087
- const loopResult = await executeLoopBlock(step, state, config, epicItems, lastOutputContract, storyFlowTasks);
4088
- state = loopResult.state;
4089
- errors.push(...loopResult.errors);
4090
- tasksCompleted += loopResult.tasksCompleted;
4091
- lastOutputContract = loopResult.lastContract;
4092
- for (const item of epicItems) processedStories.add(item.key);
4093
- if (loopResult.halted || state.phase === "max-iterations" || state.phase === "circuit-breaker") {
4094
- halted = true;
4095
- }
4096
- continue;
4097
- }
4098
- const taskName = step;
4099
- const task = config.workflow.tasks[taskName];
4100
- if (!task) {
4101
- warn(`workflow-engine: task "${taskName}" not found, skipping`);
4102
- continue;
4103
- }
4104
- if (task.agent === null) continue;
4105
- const definition = config.agents[task.agent];
4106
- if (!definition) {
4107
- warn(`workflow-engine: agent "${task.agent}" not found for "${taskName}"`);
4108
- continue;
4109
- }
4110
- const epicSentinel = `__epic_${epicId}__`;
4111
- if (isTaskCompleted(state, taskName, epicSentinel)) continue;
4112
- let guideFiles = [];
4113
- if (task.source_access === false) {
4114
- const guidesDir = join12(projectDir, ".codeharness", "verify-guides");
4115
- try {
4116
- mkdirSync6(guidesDir, { recursive: true });
4117
- for (const item of epicItems) {
4118
- const contractPath = join12(projectDir, ".codeharness", "contracts", `document-${item.key}.json`);
4119
- if (existsSync15(contractPath)) {
4120
- const contractData = JSON.parse(readFileSync13(contractPath, "utf-8"));
4121
- const docs = contractData.output ? extractTag(contractData.output, "user-docs") ?? contractData.output : null;
4122
- if (docs) {
4123
- const guidePath = join12(guidesDir, `${item.key}-guide.md`);
4124
- writeFileSync8(guidePath, docs, "utf-8");
4125
- guideFiles.push(guidePath);
4126
- }
4127
- }
4128
- }
4129
- const deployContractPath = join12(projectDir, ".codeharness", "contracts", `deploy-${epicSentinel}.json`);
4130
- if (existsSync15(deployContractPath)) {
4131
- const deployData = JSON.parse(readFileSync13(deployContractPath, "utf-8"));
4132
- const report = deployData.output ? extractTag(deployData.output, "deploy-report") ?? deployData.output : null;
4133
- if (report) {
4134
- const deployPath = join12(guidesDir, "deploy-info.md");
4135
- writeFileSync8(deployPath, report, "utf-8");
4136
- guideFiles.push(deployPath);
4137
- }
4138
- }
4139
- } catch {
4140
- }
4141
- }
4142
- try {
4143
- const dr = await dispatchTaskWithResult(task, taskName, epicSentinel, definition, state, config, void 0, lastOutputContract ?? void 0, guideFiles);
4144
- state = dr.updatedState;
4145
- lastOutputContract = dr.contract;
4146
- propagateVerifyFlags(taskName, dr.contract, projectDir);
4147
- accumulatedCostUsd += dr.contract?.cost_usd ?? 0;
4148
- tasksCompleted++;
4149
- } catch (err) {
4150
- const engineError = handleDispatchError(err, taskName, epicSentinel);
4151
- errors.push(engineError);
4152
- if (config.onEvent) {
4153
- config.onEvent({ type: "dispatch-error", taskName, storyKey: epicSentinel, error: { code: engineError.code, message: engineError.message } });
4154
- } else {
4155
- warn(`[${taskName}] epic-${epicId} \u2014 ERROR: [${engineError.code}] ${engineError.message}`);
4156
- }
4157
- state = recordErrorInState(state, taskName, epicSentinel, engineError);
4158
- writeWorkflowState(state, projectDir);
4159
- if (err instanceof DispatchError && HALT_ERROR_CODES.has(err.code)) {
4160
- halted = true;
4161
- }
4162
- } finally {
4163
- if (guideFiles.length > 0) {
4164
- const guidesDir = join12(projectDir, ".codeharness", "verify-guides");
4165
- try {
4166
- rmSync2(guidesDir, { recursive: true, force: true });
4167
- } catch {
4168
- }
4169
- }
4170
- }
4171
- }
4172
- if (!halted) {
4173
- if (!config.onEvent) info(`[epic-${epicId}] Epic completed`);
4174
- }
4175
- }
4134
+ const runInput = {
4135
+ config,
4136
+ storyFlowTasks,
4137
+ epicEntries: [...epicGroups.entries()],
4138
+ currentEpicIndex: 0,
4139
+ workflowState: state,
4140
+ errors: [],
4141
+ tasksCompleted: 0,
4142
+ storiesProcessed: /* @__PURE__ */ new Set(),
4143
+ lastContract: null,
4144
+ accumulatedCostUsd: 0,
4145
+ halted: false
4146
+ };
4147
+ const finalContext = await new Promise((resolve7) => {
4148
+ const actor = createActor(runMachine, { input: runInput });
4149
+ actor.subscribe({ complete: () => resolve7(actor.getSnapshot().context) });
4150
+ actor.start();
4151
+ });
4152
+ state = finalContext.workflowState;
4153
+ const errors = finalContext.errors;
4154
+ const tasksCompleted = finalContext.tasksCompleted;
4155
+ const storiesProcessed = finalContext.storiesProcessed;
4176
4156
  if (state.phase === "interrupted") {
4177
4157
  } else if (errors.length === 0 && state.phase !== "max-iterations" && state.phase !== "circuit-breaker") {
4178
4158
  state = { ...state, phase: "completed" };
@@ -4182,7 +4162,7 @@ async function executeWorkflow(config) {
4182
4162
  return {
4183
4163
  success: errors.length === 0 && !loopTerminated && state.phase !== "interrupted",
4184
4164
  tasksCompleted,
4185
- storiesProcessed: processedStories.size,
4165
+ storiesProcessed: storiesProcessed.size,
4186
4166
  errors,
4187
4167
  durationMs: Date.now() - startMs
4188
4168
  };
@@ -4196,11 +4176,7 @@ function recordErrorInState(state, taskName, storyKey, error) {
4196
4176
  error_message: error.message,
4197
4177
  error_code: error.code
4198
4178
  };
4199
- return {
4200
- ...state,
4201
- phase: "error",
4202
- tasks_completed: [...state.tasks_completed, errorCheckpoint]
4203
- };
4179
+ return { ...state, phase: "error", tasks_completed: [...state.tasks_completed, errorCheckpoint] };
4204
4180
  }
4205
4181
  function isEngineError(err) {
4206
4182
  if (!err || typeof err !== "object") return false;
@@ -4208,32 +4184,20 @@ function isEngineError(err) {
4208
4184
  return typeof e.taskName === "string" && typeof e.storyKey === "string" && typeof e.code === "string" && typeof e.message === "string";
4209
4185
  }
4210
4186
  function handleDispatchError(err, taskName, storyKey) {
4211
- if (err instanceof DispatchError) {
4212
- return {
4213
- taskName,
4214
- storyKey,
4215
- code: err.code,
4216
- message: err.message
4217
- };
4218
- }
4187
+ if (err instanceof DispatchError) return { taskName, storyKey, code: err.code, message: err.message };
4219
4188
  const message = err instanceof Error ? err.message : String(err);
4220
- return {
4221
- taskName,
4222
- storyKey,
4223
- code: "UNKNOWN",
4224
- message
4225
- };
4189
+ return { taskName, storyKey, code: "UNKNOWN", message };
4226
4190
  }
4227
4191
 
4228
4192
  // src/lib/worktree-manager.ts
4229
4193
  import { execSync as execSync2 } from "child_process";
4230
4194
  import { existsSync as existsSync16, readFileSync as readFileSync14, statSync } from "fs";
4231
- import { join as join14 } from "path";
4195
+ import { join as join15 } from "path";
4232
4196
 
4233
4197
  // src/lib/cross-worktree-validator.ts
4234
4198
  import { exec } from "child_process";
4235
4199
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync7 } from "fs";
4236
- import { join as join13 } from "path";
4200
+ import { join as join14 } from "path";
4237
4201
  import { promisify } from "util";
4238
4202
  var execAsync = promisify(exec);
4239
4203
  var MAX_BUFFER = 10 * 1024 * 1024;
@@ -4267,9 +4231,9 @@ function writeMergeTelemetry(opts, result) {
4267
4231
  testResults: result.testResults,
4268
4232
  errors: result.valid ? [] : ["Test suite failed after merge"]
4269
4233
  };
4270
- const dir = join13(opts.cwd, TELEMETRY_DIR2);
4234
+ const dir = join14(opts.cwd, TELEMETRY_DIR2);
4271
4235
  mkdirSync7(dir, { recursive: true });
4272
- appendFileSync2(join13(dir, TELEMETRY_FILE2), JSON.stringify(entry) + "\n");
4236
+ appendFileSync2(join14(dir, TELEMETRY_FILE2), JSON.stringify(entry) + "\n");
4273
4237
  } catch {
4274
4238
  }
4275
4239
  }
@@ -4750,7 +4714,7 @@ var WorktreeManager = class {
4750
4714
  * Check if a worktree is orphaned (no active codeharness process).
4751
4715
  */
4752
4716
  isOrphaned(wt) {
4753
- const laneStatePath = join14(wt.path, ".codeharness", "lane-state.json");
4717
+ const laneStatePath = join15(wt.path, ".codeharness", "lane-state.json");
4754
4718
  if (!existsSync16(laneStatePath)) {
4755
4719
  return true;
4756
4720
  }
@@ -6103,7 +6067,7 @@ function startRenderer(options) {
6103
6067
 
6104
6068
  // src/commands/run.ts
6105
6069
  function resolvePluginDir() {
6106
- return join15(process.cwd(), ".claude");
6070
+ return join16(process.cwd(), ".claude");
6107
6071
  }
6108
6072
  function extractEpicId2(storyKey) {
6109
6073
  const match = storyKey.match(/^(\d+)-/);
@@ -6195,8 +6159,8 @@ function registerRunCommand(program) {
6195
6159
  process.exitCode = 1;
6196
6160
  return;
6197
6161
  }
6198
- const projectWorkflowPath = join15(projectDir, ".codeharness", "workflows", "default.yaml");
6199
- 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");
6200
6164
  const workflowPath = existsSync17(projectWorkflowPath) ? projectWorkflowPath : templateWorkflowPath;
6201
6165
  try {
6202
6166
  parsedWorkflow = parseWorkflow(workflowPath);
@@ -6417,16 +6381,6 @@ function registerRunCommand(program) {
6417
6381
  key: event.storyKey.replace("__epic_", "Epic ").replace("__", ""),
6418
6382
  message: `verification complete (cost: $${(event.costUsd ?? 0).toFixed(2)})`
6419
6383
  });
6420
- const epicId = event.storyKey.replace("__epic_", "").replace("__", "");
6421
- for (let i = 0; i < storyEntries.length; i++) {
6422
- const se = storyEntries[i];
6423
- if (se.status === "in-progress" && se.key.startsWith(`${epicId}-`)) {
6424
- storiesDone++;
6425
- updateStoryStatus2(se.key, "done");
6426
- storyEntries[i] = { ...se, status: "done" };
6427
- }
6428
- }
6429
- renderer.updateStories([...storyEntries]);
6430
6384
  }
6431
6385
  }
6432
6386
  if (event.type === "dispatch-error") {
@@ -6447,12 +6401,25 @@ function registerRunCommand(program) {
6447
6401
  renderer.updateStories([...storyEntries]);
6448
6402
  }
6449
6403
  }
6404
+ if (event.type === "story-done") {
6405
+ storiesDone++;
6406
+ updateStoryStatus2(event.storyKey, "done");
6407
+ const idx = storyEntries.findIndex((s) => s.key === event.storyKey);
6408
+ if (idx >= 0) {
6409
+ storyEntries[idx] = { ...storyEntries[idx], status: "done" };
6410
+ renderer.updateStories([...storyEntries]);
6411
+ }
6412
+ const epicId = extractEpicId2(event.storyKey);
6413
+ if (epicData[epicId]) {
6414
+ epicData[epicId].storiesDone = (epicData[epicId].storiesDone ?? 0) + 1;
6415
+ }
6416
+ }
6450
6417
  };
6451
6418
  const config = {
6452
6419
  workflow: parsedWorkflow,
6453
6420
  agents,
6454
- sprintStatusPath: join15(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml"),
6455
- issuesPath: join15(projectDir, ".codeharness", "issues.yaml"),
6421
+ sprintStatusPath: join16(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml"),
6422
+ issuesPath: join16(projectDir, ".codeharness", "issues.yaml"),
6456
6423
  runId: `run-${Date.now()}`,
6457
6424
  projectDir,
6458
6425
  abortSignal: abortController.signal,
@@ -6498,7 +6465,7 @@ function registerRunCommand(program) {
6498
6465
  ...config,
6499
6466
  projectDir: worktreePath
6500
6467
  };
6501
- return executeWorkflow(epicConfig);
6468
+ return runWorkflowActor(epicConfig);
6502
6469
  };
6503
6470
  const poolResult = await pool.startPool(epics, executeFn);
6504
6471
  const remainingWorktrees = worktreeManager.listWorktrees();
@@ -6534,7 +6501,7 @@ function registerRunCommand(program) {
6534
6501
  }
6535
6502
  } else {
6536
6503
  try {
6537
- const result = await executeWorkflow(config);
6504
+ const result = await runWorkflowActor(config);
6538
6505
  clearInterval(headerRefresh);
6539
6506
  process.removeListener("SIGINT", onInterrupt);
6540
6507
  process.removeListener("SIGTERM", onInterrupt);
@@ -6564,7 +6531,7 @@ function registerRunCommand(program) {
6564
6531
 
6565
6532
  // src/commands/verify.ts
6566
6533
  import { existsSync as existsSync28, readFileSync as readFileSync25 } from "fs";
6567
- import { join as join28 } from "path";
6534
+ import { join as join29 } from "path";
6568
6535
 
6569
6536
  // src/modules/verify/index.ts
6570
6537
  import { readFileSync as readFileSync24 } from "fs";
@@ -6801,11 +6768,11 @@ function validateProofQuality(proofPath) {
6801
6768
  // src/modules/verify/orchestrator.ts
6802
6769
  import { execFileSync } from "child_process";
6803
6770
  import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
6804
- import { join as join20 } from "path";
6771
+ import { join as join21 } from "path";
6805
6772
 
6806
6773
  // src/lib/doc-health/types.ts
6807
6774
  import { readdirSync as readdirSync3, statSync as statSync2 } from "fs";
6808
- import { join as join16 } from "path";
6775
+ import { join as join17 } from "path";
6809
6776
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
6810
6777
  function getExtension(filename) {
6811
6778
  const dot = filename.lastIndexOf(".");
@@ -6826,7 +6793,7 @@ function getNewestSourceMtime(dir) {
6826
6793
  const dirName = current.split("/").pop() ?? "";
6827
6794
  if (dirName === "node_modules" || dirName === ".git") return;
6828
6795
  for (const entry of entries) {
6829
- const fullPath = join16(current, entry);
6796
+ const fullPath = join17(current, entry);
6830
6797
  let stat;
6831
6798
  try {
6832
6799
  stat = statSync2(fullPath);
@@ -6858,7 +6825,7 @@ import {
6858
6825
  readdirSync as readdirSync5,
6859
6826
  statSync as statSync4
6860
6827
  } from "fs";
6861
- import { join as join18, relative as relative2 } from "path";
6828
+ import { join as join19, relative as relative2 } from "path";
6862
6829
 
6863
6830
  // src/lib/doc-health/staleness.ts
6864
6831
  import { execSync as execSync3 } from "child_process";
@@ -6868,7 +6835,7 @@ import {
6868
6835
  readdirSync as readdirSync4,
6869
6836
  statSync as statSync3
6870
6837
  } from "fs";
6871
- import { join as join17, relative } from "path";
6838
+ import { join as join18, relative } from "path";
6872
6839
  var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
6873
6840
  var DO_NOT_EDIT_HEADER = "<!-- DO NOT EDIT MANUALLY";
6874
6841
  function getSourceFilesInModule(modulePath) {
@@ -6883,7 +6850,7 @@ function getSourceFilesInModule(modulePath) {
6883
6850
  const dirName = current.split("/").pop() ?? "";
6884
6851
  if (dirName === "node_modules" || dirName === ".git" || dirName === "__tests__" || dirName === "dist" || dirName === "coverage" || dirName.startsWith(".") && current !== modulePath) return;
6885
6852
  for (const entry of entries) {
6886
- const fullPath = join17(current, entry);
6853
+ const fullPath = join18(current, entry);
6887
6854
  let stat;
6888
6855
  try {
6889
6856
  stat = statSync3(fullPath);
@@ -6929,10 +6896,10 @@ function checkAgentsMdCompleteness(agentsPath, modulePath) {
6929
6896
  }
6930
6897
  function checkAgentsMdForModule(modulePath, dir) {
6931
6898
  const root = dir ?? process.cwd();
6932
- const fullModulePath = join17(root, modulePath);
6933
- let agentsPath = join17(fullModulePath, "AGENTS.md");
6899
+ const fullModulePath = join18(root, modulePath);
6900
+ let agentsPath = join18(fullModulePath, "AGENTS.md");
6934
6901
  if (!existsSync19(agentsPath)) {
6935
- agentsPath = join17(root, "AGENTS.md");
6902
+ agentsPath = join18(root, "AGENTS.md");
6936
6903
  }
6937
6904
  if (!existsSync19(agentsPath)) {
6938
6905
  return {
@@ -6997,14 +6964,14 @@ function checkStoryDocFreshness(storyId, dir) {
6997
6964
  for (const mod of modulesToCheck) {
6998
6965
  const result = checkAgentsMdForModule(mod, root);
6999
6966
  documents.push(result);
7000
- const moduleAgentsPath = join17(root, mod, "AGENTS.md");
7001
- 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");
7002
6969
  if (existsSync19(actualAgentsPath)) {
7003
6970
  checkAgentsMdLineCountInternal(actualAgentsPath, result.path, documents);
7004
6971
  }
7005
6972
  }
7006
6973
  if (modulesToCheck.length === 0) {
7007
- const rootAgentsPath = join17(root, "AGENTS.md");
6974
+ const rootAgentsPath = join18(root, "AGENTS.md");
7008
6975
  if (existsSync19(rootAgentsPath)) {
7009
6976
  documents.push({
7010
6977
  path: "AGENTS.md",
@@ -7079,7 +7046,7 @@ function findModules(dir, threshold) {
7079
7046
  let sourceCount = 0;
7080
7047
  const subdirs = [];
7081
7048
  for (const entry of entries) {
7082
- const fullPath = join18(current, entry);
7049
+ const fullPath = join19(current, entry);
7083
7050
  let stat;
7084
7051
  try {
7085
7052
  stat = statSync4(fullPath);
@@ -7113,7 +7080,7 @@ function scanDocHealth(dir) {
7113
7080
  const root = dir ?? process.cwd();
7114
7081
  const documents = [];
7115
7082
  const modules = findModules(root);
7116
- const rootAgentsPath = join18(root, "AGENTS.md");
7083
+ const rootAgentsPath = join19(root, "AGENTS.md");
7117
7084
  if (existsSync20(rootAgentsPath)) {
7118
7085
  if (modules.length > 0) {
7119
7086
  const docMtime = statSync4(rootAgentsPath).mtime;
@@ -7121,8 +7088,8 @@ function scanDocHealth(dir) {
7121
7088
  let staleModule = "";
7122
7089
  let newestCode = null;
7123
7090
  for (const mod of modules) {
7124
- const fullModPath = join18(root, mod);
7125
- const modAgentsPath = join18(fullModPath, "AGENTS.md");
7091
+ const fullModPath = join19(root, mod);
7092
+ const modAgentsPath = join19(fullModPath, "AGENTS.md");
7126
7093
  if (existsSync20(modAgentsPath)) continue;
7127
7094
  const { missing } = checkAgentsMdCompleteness(rootAgentsPath, fullModPath);
7128
7095
  if (missing.length > 0 && staleModule === "") {
@@ -7171,7 +7138,7 @@ function scanDocHealth(dir) {
7171
7138
  });
7172
7139
  }
7173
7140
  for (const mod of modules) {
7174
- const modAgentsPath = join18(root, mod, "AGENTS.md");
7141
+ const modAgentsPath = join19(root, mod, "AGENTS.md");
7175
7142
  if (existsSync20(modAgentsPath)) {
7176
7143
  const result = checkAgentsMdForModule(mod, root);
7177
7144
  if (result.path !== "AGENTS.md") {
@@ -7180,7 +7147,7 @@ function scanDocHealth(dir) {
7180
7147
  }
7181
7148
  }
7182
7149
  }
7183
- const indexPath = join18(root, "docs", "index.md");
7150
+ const indexPath = join19(root, "docs", "index.md");
7184
7151
  if (existsSync20(indexPath)) {
7185
7152
  const content = readFileSync17(indexPath, "utf-8");
7186
7153
  const hasAbsolutePaths = /https?:\/\/|file:\/\//i.test(content);
@@ -7192,11 +7159,11 @@ function scanDocHealth(dir) {
7192
7159
  reason: hasAbsolutePaths ? "Contains absolute URLs (may violate NFR25)" : "Uses relative paths"
7193
7160
  });
7194
7161
  }
7195
- const activeDir = join18(root, "docs", "exec-plans", "active");
7162
+ const activeDir = join19(root, "docs", "exec-plans", "active");
7196
7163
  if (existsSync20(activeDir)) {
7197
7164
  const files = readdirSync5(activeDir).filter((f) => f.endsWith(".md"));
7198
7165
  for (const file of files) {
7199
- const filePath = join18(activeDir, file);
7166
+ const filePath = join19(activeDir, file);
7200
7167
  documents.push({
7201
7168
  path: `docs/exec-plans/active/${file}`,
7202
7169
  grade: "fresh",
@@ -7207,11 +7174,11 @@ function scanDocHealth(dir) {
7207
7174
  }
7208
7175
  }
7209
7176
  for (const subdir of ["quality", "generated"]) {
7210
- const dirPath = join18(root, "docs", subdir);
7177
+ const dirPath = join19(root, "docs", subdir);
7211
7178
  if (!existsSync20(dirPath)) continue;
7212
7179
  const files = readdirSync5(dirPath).filter((f) => !f.startsWith("."));
7213
7180
  for (const file of files) {
7214
- const filePath = join18(dirPath, file);
7181
+ const filePath = join19(dirPath, file);
7215
7182
  let stat;
7216
7183
  try {
7217
7184
  stat = statSync4(filePath);
@@ -7267,7 +7234,7 @@ import {
7267
7234
  unlinkSync as unlinkSync2,
7268
7235
  writeFileSync as writeFileSync9
7269
7236
  } from "fs";
7270
- import { join as join19 } from "path";
7237
+ import { join as join20 } from "path";
7271
7238
  function printDocHealthOutput(report) {
7272
7239
  for (const doc of report.documents) {
7273
7240
  switch (doc.grade) {
@@ -7288,7 +7255,7 @@ function printDocHealthOutput(report) {
7288
7255
  }
7289
7256
  function completeExecPlan(storyId, dir) {
7290
7257
  const root = dir ?? process.cwd();
7291
- const activePath = join19(root, "docs", "exec-plans", "active", `${storyId}.md`);
7258
+ const activePath = join20(root, "docs", "exec-plans", "active", `${storyId}.md`);
7292
7259
  if (!existsSync21(activePath)) {
7293
7260
  return null;
7294
7261
  }
@@ -7300,9 +7267,9 @@ function completeExecPlan(storyId, dir) {
7300
7267
  `$1
7301
7268
  Completed: ${timestamp}`
7302
7269
  );
7303
- const completedDir = join19(root, "docs", "exec-plans", "completed");
7270
+ const completedDir = join20(root, "docs", "exec-plans", "completed");
7304
7271
  mkdirSync8(completedDir, { recursive: true });
7305
- const completedPath = join19(completedDir, `${storyId}.md`);
7272
+ const completedPath = join20(completedDir, `${storyId}.md`);
7306
7273
  writeFileSync9(completedPath, content, "utf-8");
7307
7274
  try {
7308
7275
  unlinkSync2(activePath);
@@ -7344,9 +7311,9 @@ function checkPreconditions(dir, storyId) {
7344
7311
  }
7345
7312
  function createProofDocument(storyId, _storyTitle, _acs, dir) {
7346
7313
  const root = dir ?? process.cwd();
7347
- const verificationDir = join20(root, "verification");
7314
+ const verificationDir = join21(root, "verification");
7348
7315
  mkdirSync9(verificationDir, { recursive: true });
7349
- const proofPath = join20(verificationDir, `${storyId}-proof.md`);
7316
+ const proofPath = join21(verificationDir, `${storyId}-proof.md`);
7350
7317
  writeFileSync10(proofPath, `# ${storyId} \u2014 Proof
7351
7318
 
7352
7319
  Pending: blind evaluator (Epic 6)
@@ -7518,7 +7485,7 @@ function parseObservabilityGaps(proofContent) {
7518
7485
 
7519
7486
  // src/modules/observability/analyzer.ts
7520
7487
  import { execFileSync as execFileSync2 } from "child_process";
7521
- import { join as join21 } from "path";
7488
+ import { join as join22 } from "path";
7522
7489
  var DEFAULT_RULES_DIR = "patches/observability/";
7523
7490
  var ADDITIONAL_RULES_DIRS = ["patches/error-handling/"];
7524
7491
  var DEFAULT_TIMEOUT = 6e4;
@@ -7553,8 +7520,8 @@ function analyze(projectDir, config) {
7553
7520
  }
7554
7521
  const rulesDir = config?.rulesDir ?? DEFAULT_RULES_DIR;
7555
7522
  const timeout = config?.timeout ?? DEFAULT_TIMEOUT;
7556
- const fullRulesDir = join21(projectDir, rulesDir);
7557
- 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));
7558
7525
  const rawResult = runSemgrep(projectDir, fullRulesDir, timeout, additionalDirs);
7559
7526
  if (!rawResult.success) {
7560
7527
  return fail2(rawResult.error);
@@ -7643,7 +7610,7 @@ function normalizeSeverity(severity) {
7643
7610
 
7644
7611
  // src/modules/observability/coverage.ts
7645
7612
  import { readFileSync as readFileSync20, writeFileSync as writeFileSync11, renameSync as renameSync3, existsSync as existsSync24 } from "fs";
7646
- import { join as join22 } from "path";
7613
+ import { join as join23 } from "path";
7647
7614
  var STATE_FILE2 = "sprint-state.json";
7648
7615
  var DEFAULT_STATIC_TARGET = 80;
7649
7616
  function defaultCoverageState() {
@@ -7659,7 +7626,7 @@ function defaultCoverageState() {
7659
7626
  };
7660
7627
  }
7661
7628
  function readStateFile(projectDir) {
7662
- const fp = join22(projectDir, STATE_FILE2);
7629
+ const fp = join23(projectDir, STATE_FILE2);
7663
7630
  if (!existsSync24(fp)) {
7664
7631
  return ok2({});
7665
7632
  }
@@ -7732,7 +7699,7 @@ function parseGapArray(raw) {
7732
7699
 
7733
7700
  // src/modules/observability/runtime-coverage.ts
7734
7701
  import { readFileSync as readFileSync21, writeFileSync as writeFileSync12, renameSync as renameSync4, existsSync as existsSync25 } from "fs";
7735
- import { join as join23 } from "path";
7702
+ import { join as join24 } from "path";
7736
7703
 
7737
7704
  // src/modules/observability/coverage-gate.ts
7738
7705
  var DEFAULT_STATIC_TARGET2 = 80;
@@ -7775,7 +7742,7 @@ function checkObservabilityCoverageGate(projectDir, overrides) {
7775
7742
  // src/modules/observability/runtime-validator.ts
7776
7743
  import { execSync as execSync4 } from "child_process";
7777
7744
  import { readdirSync as readdirSync6, statSync as statSync5 } from "fs";
7778
- import { join as join24 } from "path";
7745
+ import { join as join25 } from "path";
7779
7746
  var DEFAULT_CONFIG = {
7780
7747
  testCommand: "npm test",
7781
7748
  otlpEndpoint: "http://localhost:4318",
@@ -7902,11 +7869,11 @@ function mapEventsToModules(events, projectDir, modules) {
7902
7869
  });
7903
7870
  }
7904
7871
  function discoverModules(projectDir) {
7905
- const srcDir = join24(projectDir, "src");
7872
+ const srcDir = join25(projectDir, "src");
7906
7873
  try {
7907
7874
  return readdirSync6(srcDir).filter((name) => {
7908
7875
  try {
7909
- return statSync5(join24(srcDir, name)).isDirectory();
7876
+ return statSync5(join25(srcDir, name)).isDirectory();
7910
7877
  } catch {
7911
7878
  return false;
7912
7879
  }
@@ -8574,7 +8541,7 @@ function getACById(id) {
8574
8541
  // src/modules/verify/validation-runner.ts
8575
8542
  import { execSync as execSync5 } from "child_process";
8576
8543
  import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync10 } from "fs";
8577
- import { join as join25, dirname as dirname3 } from "path";
8544
+ import { join as join26, dirname as dirname3 } from "path";
8578
8545
  var MAX_VALIDATION_ATTEMPTS = 10;
8579
8546
  var AC_COMMAND_TIMEOUT_MS = 3e4;
8580
8547
  var VAL_KEY_PREFIX = "val-";
@@ -8683,7 +8650,7 @@ function executeValidationAC(ac) {
8683
8650
  function createFixStory(ac, error) {
8684
8651
  try {
8685
8652
  const storyKey = `val-fix-${ac.id}`;
8686
- const storyPath = join25(
8653
+ const storyPath = join26(
8687
8654
  process.cwd(),
8688
8655
  "_bmad-output",
8689
8656
  "implementation-artifacts",
@@ -9054,11 +9021,11 @@ function runValidationCycle() {
9054
9021
  // src/modules/verify/env.ts
9055
9022
  import { execFileSync as execFileSync5 } from "child_process";
9056
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";
9057
- import { join as join27, basename as basename2 } from "path";
9024
+ import { join as join28, basename as basename2 } from "path";
9058
9025
  import { createHash } from "crypto";
9059
9026
 
9060
9027
  // src/modules/verify/dockerfile-generator.ts
9061
- import { join as join26 } from "path";
9028
+ import { join as join27 } from "path";
9062
9029
  function generateVerifyDockerfile(projectDir) {
9063
9030
  const detections = detectStacks(projectDir);
9064
9031
  const sections = [];
@@ -9078,7 +9045,7 @@ function generateVerifyDockerfile(projectDir) {
9078
9045
  for (const detection of detections) {
9079
9046
  const provider = getStackProvider(detection.stack);
9080
9047
  if (!provider) continue;
9081
- const resolvedDir = detection.dir === "." ? projectDir : join26(projectDir, detection.dir);
9048
+ const resolvedDir = detection.dir === "." ? projectDir : join27(projectDir, detection.dir);
9082
9049
  const section = provider.getVerifyDockerfileSection(resolvedDir);
9083
9050
  if (section) {
9084
9051
  sections.push(section);
@@ -9107,7 +9074,7 @@ function isValidStoryKey(storyKey) {
9107
9074
  return /^[a-zA-Z0-9_-]+$/.test(storyKey);
9108
9075
  }
9109
9076
  function computeDistHash(projectDir) {
9110
- const distDir = join27(projectDir, "dist");
9077
+ const distDir = join28(projectDir, "dist");
9111
9078
  if (!existsSync27(distDir)) return null;
9112
9079
  const hash = createHash("sha256");
9113
9080
  const files = collectFiles(distDir).sort();
@@ -9120,7 +9087,7 @@ function computeDistHash(projectDir) {
9120
9087
  function collectFiles(dir) {
9121
9088
  const results = [];
9122
9089
  for (const entry of readdirSync7(dir, { withFileTypes: true })) {
9123
- const fullPath = join27(dir, entry.name);
9090
+ const fullPath = join28(dir, entry.name);
9124
9091
  if (entry.isDirectory()) {
9125
9092
  results.push(...collectFiles(fullPath));
9126
9093
  } else {
@@ -9156,7 +9123,7 @@ function detectProjectType(projectDir) {
9156
9123
  const rootDetection = allStacks.find((s) => s.dir === ".");
9157
9124
  const stack = rootDetection ? rootDetection.stack : null;
9158
9125
  if (stack && STACK_TO_PROJECT_TYPE[stack]) return STACK_TO_PROJECT_TYPE[stack];
9159
- if (existsSync27(join27(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
9126
+ if (existsSync27(join28(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
9160
9127
  return "generic";
9161
9128
  }
9162
9129
  function buildVerifyImage(options = {}) {
@@ -9200,18 +9167,18 @@ function buildNodeImage(projectDir) {
9200
9167
  const lastLine = packOutput.split("\n").pop()?.trim();
9201
9168
  if (!lastLine) throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
9202
9169
  const tarballName = basename2(lastLine);
9203
- const tarballPath = join27("/tmp", tarballName);
9204
- 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()}`);
9205
9172
  mkdirSync11(buildContext, { recursive: true });
9206
9173
  try {
9207
- cpSync(tarballPath, join27(buildContext, tarballName));
9174
+ cpSync(tarballPath, join28(buildContext, tarballName));
9208
9175
  const dockerfile = generateVerifyDockerfile(projectDir) + `
9209
9176
  # Install project from tarball
9210
9177
  ARG TARBALL=package.tgz
9211
9178
  COPY \${TARBALL} /tmp/\${TARBALL}
9212
9179
  RUN npm install -g /tmp/\${TARBALL} && rm /tmp/\${TARBALL}
9213
9180
  `;
9214
- writeFileSync14(join27(buildContext, "Dockerfile"), dockerfile);
9181
+ writeFileSync14(join28(buildContext, "Dockerfile"), dockerfile);
9215
9182
  execFileSync5("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${tarballName}`, "."], {
9216
9183
  cwd: buildContext,
9217
9184
  stdio: "pipe",
@@ -9223,22 +9190,22 @@ RUN npm install -g /tmp/\${TARBALL} && rm /tmp/\${TARBALL}
9223
9190
  }
9224
9191
  }
9225
9192
  function buildPythonImage(projectDir) {
9226
- const distDir = join27(projectDir, "dist");
9193
+ const distDir = join28(projectDir, "dist");
9227
9194
  const distFiles = readdirSync7(distDir).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".whl"));
9228
9195
  if (distFiles.length === 0) {
9229
9196
  throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
9230
9197
  }
9231
9198
  const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
9232
- const buildContext = join27("/tmp", `codeharness-verify-build-${Date.now()}`);
9199
+ const buildContext = join28("/tmp", `codeharness-verify-build-${Date.now()}`);
9233
9200
  mkdirSync11(buildContext, { recursive: true });
9234
9201
  try {
9235
- cpSync(join27(distDir, distFile), join27(buildContext, distFile));
9202
+ cpSync(join28(distDir, distFile), join28(buildContext, distFile));
9236
9203
  const dockerfile = generateVerifyDockerfile(projectDir) + `
9237
9204
  # Install project from distribution
9238
9205
  COPY ${distFile} /tmp/${distFile}
9239
9206
  RUN pip install --break-system-packages /tmp/${distFile} && rm /tmp/${distFile}
9240
9207
  `;
9241
- writeFileSync14(join27(buildContext, "Dockerfile"), dockerfile);
9208
+ writeFileSync14(join28(buildContext, "Dockerfile"), dockerfile);
9242
9209
  execFileSync5("docker", ["build", "-t", IMAGE_TAG, "."], {
9243
9210
  cwd: buildContext,
9244
9211
  stdio: "pipe",
@@ -9253,19 +9220,19 @@ function prepareVerifyWorkspace(storyKey, projectDir) {
9253
9220
  if (!isValidStoryKey(storyKey)) {
9254
9221
  throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
9255
9222
  }
9256
- const storyFile = join27(root, STORY_DIR, `${storyKey}.md`);
9223
+ const storyFile = join28(root, STORY_DIR, `${storyKey}.md`);
9257
9224
  if (!existsSync27(storyFile)) throw new Error(`Story file not found: ${storyFile}`);
9258
9225
  const workspace = `${TEMP_PREFIX}${storyKey}`;
9259
9226
  if (existsSync27(workspace)) rmSync3(workspace, { recursive: true, force: true });
9260
9227
  mkdirSync11(workspace, { recursive: true });
9261
- cpSync(storyFile, join27(workspace, "story.md"));
9262
- const readmePath = join27(root, "README.md");
9263
- if (existsSync27(readmePath)) cpSync(readmePath, join27(workspace, "README.md"));
9264
- 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");
9265
9232
  if (existsSync27(docsDir) && statSync6(docsDir).isDirectory()) {
9266
- cpSync(docsDir, join27(workspace, "docs"), { recursive: true });
9233
+ cpSync(docsDir, join28(workspace, "docs"), { recursive: true });
9267
9234
  }
9268
- mkdirSync11(join27(workspace, "verification"), { recursive: true });
9235
+ mkdirSync11(join28(workspace, "verification"), { recursive: true });
9269
9236
  return workspace;
9270
9237
  }
9271
9238
  function checkVerifyEnv() {
@@ -9318,18 +9285,18 @@ function cleanupVerifyEnv(storyKey) {
9318
9285
  }
9319
9286
  }
9320
9287
  function buildPluginImage(projectDir) {
9321
- const buildContext = join27("/tmp", `codeharness-verify-build-${Date.now()}`);
9288
+ const buildContext = join28("/tmp", `codeharness-verify-build-${Date.now()}`);
9322
9289
  mkdirSync11(buildContext, { recursive: true });
9323
9290
  try {
9324
- const pluginDir = join27(projectDir, ".claude-plugin");
9325
- cpSync(pluginDir, join27(buildContext, ".claude-plugin"), { recursive: true });
9291
+ const pluginDir = join28(projectDir, ".claude-plugin");
9292
+ cpSync(pluginDir, join28(buildContext, ".claude-plugin"), { recursive: true });
9326
9293
  for (const dir of ["commands", "hooks", "knowledge", "skills"]) {
9327
- const src = join27(projectDir, dir);
9294
+ const src = join28(projectDir, dir);
9328
9295
  if (existsSync27(src) && statSync6(src).isDirectory()) {
9329
- cpSync(src, join27(buildContext, dir), { recursive: true });
9296
+ cpSync(src, join28(buildContext, dir), { recursive: true });
9330
9297
  }
9331
9298
  }
9332
- writeFileSync14(join27(buildContext, "Dockerfile"), generateVerifyDockerfile(projectDir));
9299
+ writeFileSync14(join28(buildContext, "Dockerfile"), generateVerifyDockerfile(projectDir));
9333
9300
  execFileSync5("docker", ["build", "-t", IMAGE_TAG, "."], {
9334
9301
  cwd: buildContext,
9335
9302
  stdio: "pipe",
@@ -9340,10 +9307,10 @@ function buildPluginImage(projectDir) {
9340
9307
  }
9341
9308
  }
9342
9309
  function buildSimpleImage(projectDir, timeout = 12e4) {
9343
- const buildContext = join27("/tmp", `codeharness-verify-build-${Date.now()}`);
9310
+ const buildContext = join28("/tmp", `codeharness-verify-build-${Date.now()}`);
9344
9311
  mkdirSync11(buildContext, { recursive: true });
9345
9312
  try {
9346
- writeFileSync14(join27(buildContext, "Dockerfile"), generateVerifyDockerfile(projectDir));
9313
+ writeFileSync14(join28(buildContext, "Dockerfile"), generateVerifyDockerfile(projectDir));
9347
9314
  execFileSync5("docker", ["build", "-t", IMAGE_TAG, "."], {
9348
9315
  cwd: buildContext,
9349
9316
  stdio: "pipe",
@@ -9425,7 +9392,7 @@ function verifyRetro(opts, isJson, root) {
9425
9392
  return;
9426
9393
  }
9427
9394
  const retroFile = `epic-${epicNum}-retrospective.md`;
9428
- const retroPath = join28(root, STORY_DIR2, retroFile);
9395
+ const retroPath = join29(root, STORY_DIR2, retroFile);
9429
9396
  if (!existsSync28(retroPath)) {
9430
9397
  if (isJson) {
9431
9398
  jsonOutput({ status: "fail", epic: epicNum, retroFile, message: `${retroFile} not found` });
@@ -9443,7 +9410,7 @@ function verifyRetro(opts, isJson, root) {
9443
9410
  warn(`Failed to update sprint status: ${message}`);
9444
9411
  }
9445
9412
  if (isJson) {
9446
- jsonOutput({ status: "ok", epic: epicNum, retroFile: join28(STORY_DIR2, retroFile) });
9413
+ jsonOutput({ status: "ok", epic: epicNum, retroFile: join29(STORY_DIR2, retroFile) });
9447
9414
  } else {
9448
9415
  ok(`Epic ${epicNum} retrospective: marked done`);
9449
9416
  }
@@ -9454,7 +9421,7 @@ function verifyStory(storyId, isJson, root) {
9454
9421
  process.exitCode = 1;
9455
9422
  return;
9456
9423
  }
9457
- const readmePath = join28(root, "README.md");
9424
+ const readmePath = join29(root, "README.md");
9458
9425
  if (!existsSync28(readmePath)) {
9459
9426
  if (isJson) {
9460
9427
  jsonOutput({ status: "fail", message: "No README.md found \u2014 verification requires user documentation" });
@@ -9464,7 +9431,7 @@ function verifyStory(storyId, isJson, root) {
9464
9431
  process.exitCode = 1;
9465
9432
  return;
9466
9433
  }
9467
- const storyFilePath = join28(root, STORY_DIR2, `${storyId}.md`);
9434
+ const storyFilePath = join29(root, STORY_DIR2, `${storyId}.md`);
9468
9435
  if (!existsSync28(storyFilePath)) {
9469
9436
  fail(`Story file not found: ${storyFilePath}`, { json: isJson });
9470
9437
  process.exitCode = 1;
@@ -9505,7 +9472,7 @@ function verifyStory(storyId, isJson, root) {
9505
9472
  return;
9506
9473
  }
9507
9474
  const storyTitle = extractStoryTitle(storyFilePath);
9508
- const expectedProofPath = join28(root, "verification", `${storyId}-proof.md`);
9475
+ const expectedProofPath = join29(root, "verification", `${storyId}-proof.md`);
9509
9476
  const proofPath = existsSync28(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
9510
9477
  const proofQuality = validateProofQuality(proofPath);
9511
9478
  if (!proofQuality.passed) {
@@ -9677,11 +9644,11 @@ function resolveEndpoints(state) {
9677
9644
 
9678
9645
  // src/lib/onboard-checks.ts
9679
9646
  import { existsSync as existsSync32 } from "fs";
9680
- import { join as join31 } from "path";
9647
+ import { join as join32 } from "path";
9681
9648
 
9682
9649
  // src/lib/coverage/parser.ts
9683
9650
  import { existsSync as existsSync29, readFileSync as readFileSync26 } from "fs";
9684
- import { join as join29 } from "path";
9651
+ import { join as join30 } from "path";
9685
9652
  function parseTestCounts(output) {
9686
9653
  const vitestMatch = /Tests\s+(\d+)\s+passed(?:\s*\|\s*(\d+)\s+failed)?/i.exec(output);
9687
9654
  if (vitestMatch) {
@@ -9745,7 +9712,7 @@ function parseVitestCoverage(dir) {
9745
9712
  }
9746
9713
  }
9747
9714
  function parsePythonCoverage(dir) {
9748
- const reportPath = join29(dir, "coverage.json");
9715
+ const reportPath = join30(dir, "coverage.json");
9749
9716
  if (!existsSync29(reportPath)) {
9750
9717
  warn("Coverage report not found at coverage.json");
9751
9718
  return 0;
@@ -9759,7 +9726,7 @@ function parsePythonCoverage(dir) {
9759
9726
  }
9760
9727
  }
9761
9728
  function parseTarpaulinCoverage(dir) {
9762
- const reportPath = join29(dir, "coverage", "tarpaulin-report.json");
9729
+ const reportPath = join30(dir, "coverage", "tarpaulin-report.json");
9763
9730
  if (!existsSync29(reportPath)) {
9764
9731
  warn("Tarpaulin report not found at coverage/tarpaulin-report.json");
9765
9732
  return 0;
@@ -9774,8 +9741,8 @@ function parseTarpaulinCoverage(dir) {
9774
9741
  }
9775
9742
  function findCoverageSummary(dir) {
9776
9743
  const candidates = [
9777
- join29(dir, "coverage", "coverage-summary.json"),
9778
- join29(dir, "src", "coverage", "coverage-summary.json")
9744
+ join30(dir, "coverage", "coverage-summary.json"),
9745
+ join30(dir, "src", "coverage", "coverage-summary.json")
9779
9746
  ];
9780
9747
  for (const p of candidates) {
9781
9748
  if (existsSync29(p)) return p;
@@ -9786,7 +9753,7 @@ function findCoverageSummary(dir) {
9786
9753
  // src/lib/coverage/runner.ts
9787
9754
  import { execSync as execSync7 } from "child_process";
9788
9755
  import { existsSync as existsSync30, readFileSync as readFileSync27 } from "fs";
9789
- import { join as join30 } from "path";
9756
+ import { join as join31 } from "path";
9790
9757
  function detectCoverageTool(dir) {
9791
9758
  const baseDir = dir ?? process.cwd();
9792
9759
  const stateHint = getStateToolHint(baseDir);
@@ -9819,7 +9786,7 @@ function detectRustCoverageTool(dir) {
9819
9786
  warn("cargo-tarpaulin not installed \u2014 coverage detection unavailable");
9820
9787
  return { tool: "unknown", runCommand: "", reportFormat: "" };
9821
9788
  }
9822
- const cargoPath = join30(dir, "Cargo.toml");
9789
+ const cargoPath = join31(dir, "Cargo.toml");
9823
9790
  let isWorkspace = false;
9824
9791
  try {
9825
9792
  const cargoContent = readFileSync27(cargoPath, "utf-8");
@@ -9842,8 +9809,8 @@ function getStateToolHint(dir) {
9842
9809
  }
9843
9810
  }
9844
9811
  function detectNodeCoverageTool(dir, stateHint) {
9845
- const hasVitestConfig = existsSync30(join30(dir, "vitest.config.ts")) || existsSync30(join30(dir, "vitest.config.js"));
9846
- 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");
9847
9814
  let hasVitestCoverageV8 = false;
9848
9815
  let hasVitestCoverageIstanbul = false;
9849
9816
  let hasC8 = false;
@@ -9904,7 +9871,7 @@ function getNodeTestCommand(scripts, runner) {
9904
9871
  return "npm test";
9905
9872
  }
9906
9873
  function detectPythonCoverageTool(dir) {
9907
- const reqPath = join30(dir, "requirements.txt");
9874
+ const reqPath = join31(dir, "requirements.txt");
9908
9875
  if (existsSync30(reqPath)) {
9909
9876
  try {
9910
9877
  const content = readFileSync27(reqPath, "utf-8");
@@ -9918,7 +9885,7 @@ function detectPythonCoverageTool(dir) {
9918
9885
  } catch {
9919
9886
  }
9920
9887
  }
9921
- const pyprojectPath = join30(dir, "pyproject.toml");
9888
+ const pyprojectPath = join31(dir, "pyproject.toml");
9922
9889
  if (existsSync30(pyprojectPath)) {
9923
9890
  try {
9924
9891
  const content = readFileSync27(pyprojectPath, "utf-8");
@@ -10751,7 +10718,7 @@ function registerStatusCommand(program) {
10751
10718
 
10752
10719
  // src/modules/audit/dimensions.ts
10753
10720
  import { existsSync as existsSync33, readdirSync as readdirSync8 } from "fs";
10754
- import { join as join32 } from "path";
10721
+ import { join as join33 } from "path";
10755
10722
  function gap(dimension, description, suggestedFix) {
10756
10723
  return { dimension, description, suggestedFix };
10757
10724
  }
@@ -10863,15 +10830,15 @@ function checkDocumentation(projectDir) {
10863
10830
  function checkVerification(projectDir) {
10864
10831
  try {
10865
10832
  const gaps = [];
10866
- const sprintPath = join32(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
10833
+ const sprintPath = join33(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
10867
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")]);
10868
- const vDir = join32(projectDir, "verification");
10835
+ const vDir = join33(projectDir, "verification");
10869
10836
  let proofCount = 0, totalChecked = 0;
10870
10837
  if (existsSync33(vDir)) {
10871
10838
  for (const file of readdirSafe(vDir)) {
10872
10839
  if (!file.endsWith("-proof.md")) continue;
10873
10840
  totalChecked++;
10874
- const r = parseProof(join32(vDir, file));
10841
+ const r = parseProof(join33(vDir, file));
10875
10842
  if (isOk(r) && r.data.passed) {
10876
10843
  proofCount++;
10877
10844
  } else {
@@ -10949,7 +10916,7 @@ function formatAuditJson(result) {
10949
10916
 
10950
10917
  // src/modules/audit/fix-generator.ts
10951
10918
  import { existsSync as existsSync34, writeFileSync as writeFileSync15, mkdirSync as mkdirSync12 } from "fs";
10952
- import { join as join33, dirname as dirname5 } from "path";
10919
+ import { join as join34, dirname as dirname5 } from "path";
10953
10920
  function buildStoryKey(gap2, index) {
10954
10921
  const safeDimension = gap2.dimension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
10955
10922
  return `audit-fix-${safeDimension}-${index}`;
@@ -10981,7 +10948,7 @@ function generateFixStories(auditResult) {
10981
10948
  const stories = [];
10982
10949
  let created = 0;
10983
10950
  let skipped = 0;
10984
- const artifactsDir = join33(
10951
+ const artifactsDir = join34(
10985
10952
  process.cwd(),
10986
10953
  "_bmad-output",
10987
10954
  "implementation-artifacts"
@@ -10990,7 +10957,7 @@ function generateFixStories(auditResult) {
10990
10957
  for (let i = 0; i < dimension.gaps.length; i++) {
10991
10958
  const gap2 = dimension.gaps[i];
10992
10959
  const key = buildStoryKey(gap2, i + 1);
10993
- const filePath = join33(artifactsDir, `${key}.md`);
10960
+ const filePath = join34(artifactsDir, `${key}.md`);
10994
10961
  if (existsSync34(filePath)) {
10995
10962
  stories.push({
10996
10963
  key,
@@ -11181,7 +11148,7 @@ function registerOnboardCommand(program) {
11181
11148
 
11182
11149
  // src/commands/teardown.ts
11183
11150
  import { existsSync as existsSync35, unlinkSync as unlinkSync3, readFileSync as readFileSync29, writeFileSync as writeFileSync16, rmSync as rmSync4 } from "fs";
11184
- import { join as join34 } from "path";
11151
+ import { join as join35 } from "path";
11185
11152
  function buildDefaultResult() {
11186
11153
  return {
11187
11154
  status: "ok",
@@ -11227,7 +11194,7 @@ function registerTeardownCommand(program) {
11227
11194
  } else if (otlpMode === "remote-routed") {
11228
11195
  if (!options.keepDocker) {
11229
11196
  try {
11230
- const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-MXONF2RK.js");
11197
+ const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-Z6B3GBST.js");
11231
11198
  stopCollectorOnly2();
11232
11199
  result.docker.stopped = true;
11233
11200
  if (!isJson) {
@@ -11259,7 +11226,7 @@ function registerTeardownCommand(program) {
11259
11226
  info("Shared stack: kept running (other projects may use it)");
11260
11227
  }
11261
11228
  } else if (isLegacyStack) {
11262
- const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-MXONF2RK.js");
11229
+ const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-Z6B3GBST.js");
11263
11230
  let stackRunning = false;
11264
11231
  try {
11265
11232
  stackRunning = isStackRunning2(composeFile);
@@ -11284,7 +11251,7 @@ function registerTeardownCommand(program) {
11284
11251
  info("Docker stack: not running, skipping");
11285
11252
  }
11286
11253
  }
11287
- const composeFilePath = join34(projectDir, composeFile);
11254
+ const composeFilePath = join35(projectDir, composeFile);
11288
11255
  if (existsSync35(composeFilePath)) {
11289
11256
  unlinkSync3(composeFilePath);
11290
11257
  result.removed.push(composeFile);
@@ -11292,7 +11259,7 @@ function registerTeardownCommand(program) {
11292
11259
  ok(`Removed: ${composeFile}`);
11293
11260
  }
11294
11261
  }
11295
- const otelConfigPath = join34(projectDir, "otel-collector-config.yaml");
11262
+ const otelConfigPath = join35(projectDir, "otel-collector-config.yaml");
11296
11263
  if (existsSync35(otelConfigPath)) {
11297
11264
  unlinkSync3(otelConfigPath);
11298
11265
  result.removed.push("otel-collector-config.yaml");
@@ -11312,7 +11279,7 @@ function registerTeardownCommand(program) {
11312
11279
  }
11313
11280
  const stacks = state.stacks ?? (state.stack ? [state.stack] : []);
11314
11281
  if (state.otlp?.enabled && stacks.includes("nodejs")) {
11315
- const pkgPath = join34(projectDir, "package.json");
11282
+ const pkgPath = join35(projectDir, "package.json");
11316
11283
  if (existsSync35(pkgPath)) {
11317
11284
  try {
11318
11285
  const raw = readFileSync29(pkgPath, "utf-8");
@@ -11355,7 +11322,7 @@ function registerTeardownCommand(program) {
11355
11322
  }
11356
11323
  }
11357
11324
  }
11358
- const harnessDir = join34(projectDir, ".harness");
11325
+ const harnessDir = join35(projectDir, ".harness");
11359
11326
  if (existsSync35(harnessDir)) {
11360
11327
  rmSync4(harnessDir, { recursive: true, force: true });
11361
11328
  result.removed.push(".harness/");
@@ -12046,7 +12013,7 @@ function registerQueryCommand(program) {
12046
12013
 
12047
12014
  // src/commands/retro-import.ts
12048
12015
  import { existsSync as existsSync37, readFileSync as readFileSync31 } from "fs";
12049
- import { join as join36 } from "path";
12016
+ import { join as join37 } from "path";
12050
12017
 
12051
12018
  // src/lib/retro-parser.ts
12052
12019
  var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
@@ -12164,7 +12131,7 @@ function isDuplicate(newItem, existingTitles, threshold = 0.8) {
12164
12131
 
12165
12132
  // src/lib/issue-tracker.ts
12166
12133
  import { existsSync as existsSync36, readFileSync as readFileSync30, writeFileSync as writeFileSync17, mkdirSync as mkdirSync13 } from "fs";
12167
- import { join as join35 } from "path";
12134
+ import { join as join36 } from "path";
12168
12135
  import { parse as parse6, stringify as stringify3 } from "yaml";
12169
12136
  var VALID_PRIORITIES = /* @__PURE__ */ new Set([
12170
12137
  "low",
@@ -12172,9 +12139,9 @@ var VALID_PRIORITIES = /* @__PURE__ */ new Set([
12172
12139
  "high",
12173
12140
  "critical"
12174
12141
  ]);
12175
- var ISSUES_REL_PATH = join35(".codeharness", "issues.yaml");
12142
+ var ISSUES_REL_PATH = join36(".codeharness", "issues.yaml");
12176
12143
  function issuesPath(dir) {
12177
- return join35(dir, ISSUES_REL_PATH);
12144
+ return join36(dir, ISSUES_REL_PATH);
12178
12145
  }
12179
12146
  function readIssues(dir = process.cwd()) {
12180
12147
  const filePath = issuesPath(dir);
@@ -12190,7 +12157,7 @@ function readIssues(dir = process.cwd()) {
12190
12157
  }
12191
12158
  function writeIssues(data, dir = process.cwd()) {
12192
12159
  const filePath = issuesPath(dir);
12193
- const dirPath = join35(dir, ".codeharness");
12160
+ const dirPath = join36(dir, ".codeharness");
12194
12161
  if (!existsSync36(dirPath)) {
12195
12162
  mkdirSync13(dirPath, { recursive: true });
12196
12163
  }
@@ -12351,7 +12318,7 @@ function registerRetroImportCommand(program) {
12351
12318
  return;
12352
12319
  }
12353
12320
  const retroFile = `epic-${epicNum}-retrospective.md`;
12354
- const retroPath = join36(root, STORY_DIR3, retroFile);
12321
+ const retroPath = join37(root, STORY_DIR3, retroFile);
12355
12322
  if (!existsSync37(retroPath)) {
12356
12323
  fail(`Retro file not found: ${retroFile}`, { json: isJson });
12357
12324
  process.exitCode = 1;
@@ -12852,7 +12819,7 @@ function registerValidateStateCommand(program) {
12852
12819
 
12853
12820
  // src/commands/validate-schema.ts
12854
12821
  import { readdirSync as readdirSync9, existsSync as existsSync38 } from "fs";
12855
- import { join as join37, resolve as resolve6 } from "path";
12822
+ import { join as join38, resolve as resolve6 } from "path";
12856
12823
  function renderSchemaResult(result, isJson) {
12857
12824
  if (isJson) {
12858
12825
  jsonOutput(result);
@@ -12872,7 +12839,7 @@ function renderSchemaResult(result, isJson) {
12872
12839
  process.exitCode = result.status === "pass" ? 0 : 1;
12873
12840
  }
12874
12841
  function runSchemaValidation(projectDir) {
12875
- const workflowsDir = join37(projectDir, ".codeharness", "workflows");
12842
+ const workflowsDir = join38(projectDir, ".codeharness", "workflows");
12876
12843
  if (!existsSync38(workflowsDir)) {
12877
12844
  return {
12878
12845
  status: "fail",
@@ -13181,7 +13148,7 @@ function registerAuditCommand(program) {
13181
13148
 
13182
13149
  // src/commands/stats.ts
13183
13150
  import { existsSync as existsSync39, readdirSync as readdirSync10, readFileSync as readFileSync32, writeFileSync as writeFileSync18 } from "fs";
13184
- import { join as join38 } from "path";
13151
+ import { join as join39 } from "path";
13185
13152
  var RATES = {
13186
13153
  input: 15,
13187
13154
  output: 75,
@@ -13266,10 +13233,10 @@ function parseLogFile(filePath, report) {
13266
13233
  }
13267
13234
  }
13268
13235
  function generateReport3(projectDir, logsDir) {
13269
- const ralphLogs = join38(projectDir, "ralph", "logs");
13270
- const sessionLogs = join38(projectDir, "session-logs");
13236
+ const ralphLogs = join39(projectDir, "ralph", "logs");
13237
+ const sessionLogs = join39(projectDir, "session-logs");
13271
13238
  const resolvedLogsDir = logsDir ?? (existsSync39(ralphLogs) ? ralphLogs : sessionLogs);
13272
- 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));
13273
13240
  const report = {
13274
13241
  byPhase: /* @__PURE__ */ new Map(),
13275
13242
  byStory: /* @__PURE__ */ new Map(),
@@ -13370,10 +13337,10 @@ function registerStatsCommand(program) {
13370
13337
  const projectDir = process.cwd();
13371
13338
  let logsDir;
13372
13339
  if (options.logsDir) {
13373
- logsDir = join38(projectDir, options.logsDir);
13340
+ logsDir = join39(projectDir, options.logsDir);
13374
13341
  } else {
13375
- const ralphLogs = join38(projectDir, "ralph", "logs");
13376
- const sessionLogs = join38(projectDir, "session-logs");
13342
+ const ralphLogs = join39(projectDir, "ralph", "logs");
13343
+ const sessionLogs = join39(projectDir, "session-logs");
13377
13344
  logsDir = existsSync39(ralphLogs) ? ralphLogs : sessionLogs;
13378
13345
  }
13379
13346
  if (!existsSync39(logsDir)) {
@@ -13389,7 +13356,7 @@ function registerStatsCommand(program) {
13389
13356
  const formatted = formatReport2(report);
13390
13357
  console.log(formatted);
13391
13358
  if (options.save) {
13392
- const outPath = join38(projectDir, "_bmad-output", "implementation-artifacts", "cost-report.md");
13359
+ const outPath = join39(projectDir, "_bmad-output", "implementation-artifacts", "cost-report.md");
13393
13360
  writeFileSync18(outPath, formatted, "utf-8");
13394
13361
  ok(`Report saved to ${outPath}`);
13395
13362
  }
@@ -14246,7 +14213,7 @@ function registerDriversCommand(program) {
14246
14213
  }
14247
14214
 
14248
14215
  // src/index.ts
14249
- var VERSION = true ? "0.35.7" : "0.0.0-dev";
14216
+ var VERSION = true ? "0.36.3" : "0.0.0-dev";
14250
14217
  function createProgram() {
14251
14218
  const program = new Command();
14252
14219
  program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");