codeharness 0.35.7 → 0.36.2

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-374J3J3A.js";
44
44
 
45
45
  // src/index.ts
46
46
  import { Command } from "commander";
@@ -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, createActor } from "xstate";
2510
2511
  import { readFileSync as readFileSync13, existsSync as existsSync15, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "fs";
2511
2512
  import { join as join12 } from "path";
2512
- import { parse as parse5 } from "yaml";
2513
2513
 
2514
2514
  // src/lib/agent-dispatch.ts
2515
2515
  import { query } from "@anthropic-ai/claude-agent-sdk";
@@ -2568,109 +2568,9 @@ function resolveModel(task, agent, driver) {
2568
2568
  return driver.defaultModel;
2569
2569
  }
2570
2570
 
2571
- // src/lib/workflow-state.ts
2572
- import { existsSync as existsSync11, mkdirSync as mkdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
2573
- import { join as join8 } from "path";
2574
- import { parse as parse4, stringify } from "yaml";
2575
- var STATE_DIR = ".codeharness";
2576
- var STATE_FILE = "workflow-state.yaml";
2577
- function getDefaultWorkflowState() {
2578
- return {
2579
- workflow_name: "",
2580
- started: "",
2581
- iteration: 0,
2582
- phase: "idle",
2583
- tasks_completed: [],
2584
- evaluator_scores: [],
2585
- circuit_breaker: {
2586
- triggered: false,
2587
- reason: null,
2588
- score_history: []
2589
- },
2590
- trace_ids: []
2591
- };
2592
- }
2593
- function writeWorkflowState(state, dir) {
2594
- const baseDir = dir ?? process.cwd();
2595
- const stateDir = join8(baseDir, STATE_DIR);
2596
- mkdirSync2(stateDir, { recursive: true });
2597
- const yamlContent = stringify(state, { nullStr: "null" });
2598
- writeFileSync6(join8(stateDir, STATE_FILE), yamlContent, "utf-8");
2599
- }
2600
- function readWorkflowState(dir) {
2601
- const baseDir = dir ?? process.cwd();
2602
- const filePath = join8(baseDir, STATE_DIR, STATE_FILE);
2603
- if (!existsSync11(filePath)) {
2604
- return getDefaultWorkflowState();
2605
- }
2606
- let raw;
2607
- try {
2608
- raw = readFileSync10(filePath, "utf-8");
2609
- } catch {
2610
- warn("workflow-state.yaml could not be read \u2014 returning default state");
2611
- return getDefaultWorkflowState();
2612
- }
2613
- let parsed;
2614
- try {
2615
- parsed = parse4(raw);
2616
- } catch {
2617
- warn("workflow-state.yaml contains invalid YAML \u2014 returning default state");
2618
- return getDefaultWorkflowState();
2619
- }
2620
- if (!isValidWorkflowState(parsed)) {
2621
- warn("workflow-state.yaml has invalid shape \u2014 returning default state");
2622
- return getDefaultWorkflowState();
2623
- }
2624
- return parsed;
2625
- }
2626
- function isValidWorkflowState(value) {
2627
- if (!value || typeof value !== "object") return false;
2628
- const s = value;
2629
- if (typeof s.workflow_name !== "string") return false;
2630
- if (typeof s.started !== "string") return false;
2631
- if (typeof s.iteration !== "number") return false;
2632
- if (typeof s.phase !== "string") return false;
2633
- if (!Array.isArray(s.tasks_completed)) return false;
2634
- if (!Array.isArray(s.evaluator_scores)) return false;
2635
- if (!s.circuit_breaker || typeof s.circuit_breaker !== "object") return false;
2636
- if (s.trace_ids !== void 0) {
2637
- if (!Array.isArray(s.trace_ids)) return false;
2638
- for (const id of s.trace_ids) {
2639
- if (typeof id !== "string") return false;
2640
- }
2641
- }
2642
- for (const t of s.tasks_completed) {
2643
- if (!t || typeof t !== "object") return false;
2644
- const tc = t;
2645
- if (typeof tc.task_name !== "string") return false;
2646
- if (typeof tc.story_key !== "string") return false;
2647
- if (typeof tc.completed_at !== "string") return false;
2648
- if (tc.session_id !== void 0 && typeof tc.session_id !== "string") return false;
2649
- if (tc.error !== void 0 && typeof tc.error !== "boolean") return false;
2650
- }
2651
- for (const e of s.evaluator_scores) {
2652
- if (!e || typeof e !== "object") return false;
2653
- const es = e;
2654
- if (typeof es.iteration !== "number") return false;
2655
- if (typeof es.passed !== "number") return false;
2656
- if (typeof es.failed !== "number") return false;
2657
- if (typeof es.unknown !== "number") return false;
2658
- if (typeof es.total !== "number") return false;
2659
- if (typeof es.timestamp !== "string") return false;
2660
- }
2661
- const cb = s.circuit_breaker;
2662
- if (typeof cb.triggered !== "boolean") return false;
2663
- if (cb.reason !== null && typeof cb.reason !== "string") return false;
2664
- if (!Array.isArray(cb.score_history)) return false;
2665
- for (const score of cb.score_history) {
2666
- if (typeof score !== "number") return false;
2667
- }
2668
- return true;
2669
- }
2670
-
2671
2571
  // src/lib/agents/output-contract.ts
2672
- import { writeFileSync as writeFileSync7, readFileSync as readFileSync11, renameSync as renameSync2, mkdirSync as mkdirSync3, existsSync as existsSync12 } from "fs";
2673
- import { join as join9, resolve as resolve4 } from "path";
2572
+ import { writeFileSync as writeFileSync6, readFileSync as readFileSync10, renameSync as renameSync2, mkdirSync as mkdirSync2, existsSync as existsSync11 } from "fs";
2573
+ import { join as join8, resolve as resolve4 } from "path";
2674
2574
  function assertSafeComponent(value, label) {
2675
2575
  if (!value || value.trim().length === 0) {
2676
2576
  throw new Error(`${label} must be a non-empty string`);
@@ -2682,7 +2582,7 @@ function assertSafeComponent(value, label) {
2682
2582
  function contractFilePath(taskName, storyId, contractDir) {
2683
2583
  assertSafeComponent(taskName, "taskName");
2684
2584
  assertSafeComponent(storyId, "storyId");
2685
- const filePath = join9(contractDir, `${taskName}-${storyId}.json`);
2585
+ const filePath = join8(contractDir, `${taskName}-${storyId}.json`);
2686
2586
  const resolvedDir = resolve4(contractDir);
2687
2587
  const resolvedFile = resolve4(filePath);
2688
2588
  if (!resolvedFile.startsWith(resolvedDir)) {
@@ -2694,8 +2594,8 @@ function writeOutputContract(contract, contractDir) {
2694
2594
  const finalPath = contractFilePath(contract.taskName, contract.storyId, contractDir);
2695
2595
  const tmpPath2 = finalPath + ".tmp";
2696
2596
  try {
2697
- mkdirSync3(contractDir, { recursive: true });
2698
- writeFileSync7(tmpPath2, JSON.stringify(contract, null, 2) + "\n", "utf-8");
2597
+ mkdirSync2(contractDir, { recursive: true });
2598
+ writeFileSync6(tmpPath2, JSON.stringify(contract, null, 2) + "\n", "utf-8");
2699
2599
  renameSync2(tmpPath2, finalPath);
2700
2600
  } catch (err) {
2701
2601
  const message = err instanceof Error ? err.message : String(err);
@@ -2773,8 +2673,8 @@ ${context}`;
2773
2673
  }
2774
2674
 
2775
2675
  // src/lib/source-isolation.ts
2776
- import { mkdirSync as mkdirSync4, copyFileSync, existsSync as existsSync13, rmSync } from "fs";
2777
- import { join as join10, basename } from "path";
2676
+ import { mkdirSync as mkdirSync3, copyFileSync, existsSync as existsSync12, rmSync } from "fs";
2677
+ import { join as join9, basename } from "path";
2778
2678
  function sanitizeRunId(runId) {
2779
2679
  let sanitized = runId.replace(/[^a-zA-Z0-9._-]/g, "_");
2780
2680
  sanitized = sanitized.replace(/^\.+/, "");
@@ -2784,7 +2684,7 @@ function sanitizeRunId(runId) {
2784
2684
  return sanitized;
2785
2685
  }
2786
2686
  function deduplicateFilename(dir, name) {
2787
- if (!existsSync13(join10(dir, name))) {
2687
+ if (!existsSync12(join9(dir, name))) {
2788
2688
  return name;
2789
2689
  }
2790
2690
  const dotIdx = name.lastIndexOf(".");
@@ -2795,26 +2695,26 @@ function deduplicateFilename(dir, name) {
2795
2695
  do {
2796
2696
  candidate = `${stem}-${counter}${ext}`;
2797
2697
  counter++;
2798
- } while (existsSync13(join10(dir, candidate)));
2698
+ } while (existsSync12(join9(dir, candidate)));
2799
2699
  return candidate;
2800
2700
  }
2801
2701
  async function createIsolatedWorkspace(options) {
2802
2702
  const safeRunId = sanitizeRunId(options.runId);
2803
2703
  const dir = `/tmp/codeharness-verify-${safeRunId}`;
2804
- const storyFilesDir = join10(dir, "story-files");
2805
- const verdictDir = join10(dir, "verdict");
2806
- if (existsSync13(dir)) {
2704
+ const storyFilesDir = join9(dir, "story-files");
2705
+ const verdictDir = join9(dir, "verdict");
2706
+ if (existsSync12(dir)) {
2807
2707
  rmSync(dir, { recursive: true, force: true });
2808
2708
  }
2809
- mkdirSync4(storyFilesDir, { recursive: true });
2810
- mkdirSync4(verdictDir, { recursive: true });
2709
+ mkdirSync3(storyFilesDir, { recursive: true });
2710
+ mkdirSync3(verdictDir, { recursive: true });
2811
2711
  for (const filePath of options.storyFiles) {
2812
- if (!existsSync13(filePath)) {
2712
+ if (!existsSync12(filePath)) {
2813
2713
  warn(`Source isolation: story file not found, skipping: ${filePath}`);
2814
2714
  continue;
2815
2715
  }
2816
2716
  const name = deduplicateFilename(storyFilesDir, basename(filePath));
2817
- const dest = join10(storyFilesDir, name);
2717
+ const dest = join9(storyFilesDir, name);
2818
2718
  copyFileSync(filePath, dest);
2819
2719
  }
2820
2720
  const workspace = {
@@ -2825,7 +2725,7 @@ async function createIsolatedWorkspace(options) {
2825
2725
  return { cwd: dir };
2826
2726
  },
2827
2727
  async cleanup() {
2828
- if (existsSync13(dir)) {
2728
+ if (existsSync12(dir)) {
2829
2729
  rmSync(dir, { recursive: true, force: true });
2830
2730
  }
2831
2731
  }
@@ -3106,8 +3006,8 @@ function evaluateProgress(scores) {
3106
3006
  }
3107
3007
 
3108
3008
  // src/lib/telemetry-writer.ts
3109
- import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync12 } from "fs";
3110
- import { join as join11 } from "path";
3009
+ import { appendFileSync, existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync11 } from "fs";
3010
+ import { join as join10 } from "path";
3111
3011
  var TELEMETRY_DIR = ".codeharness";
3112
3012
  var TELEMETRY_FILE = "telemetry.jsonl";
3113
3013
  function extractEpicId(storyKey) {
@@ -3140,9 +3040,9 @@ async function writeTelemetryEntry(ctx) {
3140
3040
  } : null,
3141
3041
  errors: []
3142
3042
  };
3143
- const dir = join11(ctx.projectDir, TELEMETRY_DIR);
3144
- mkdirSync5(dir, { recursive: true });
3145
- appendFileSync(join11(dir, TELEMETRY_FILE), JSON.stringify(entry) + "\n");
3043
+ const dir = join10(ctx.projectDir, TELEMETRY_DIR);
3044
+ mkdirSync4(dir, { recursive: true });
3045
+ appendFileSync(join10(dir, TELEMETRY_FILE), JSON.stringify(entry) + "\n");
3146
3046
  return { success: true, output: `telemetry: entry written for ${ctx.storyKey}` };
3147
3047
  }
3148
3048
 
@@ -3159,15 +3059,138 @@ function listNullTasks() {
3159
3059
  }
3160
3060
  registerNullTask("telemetry", writeTelemetryEntry);
3161
3061
 
3062
+ // src/lib/workflow-machine.ts
3063
+ import { parse as parse5 } from "yaml";
3064
+
3162
3065
  // src/lib/evaluator.ts
3163
3066
  function formatCoverageContextMessage(coverage, target) {
3164
3067
  return `Coverage already verified by engine: ${coverage}% (target: ${target}%). No re-run needed.`;
3165
3068
  }
3166
3069
 
3167
- // src/lib/workflow-engine.ts
3168
- var PER_RUN_SENTINEL = "__run__";
3070
+ // src/lib/workflow-state.ts
3071
+ import { existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
3072
+ import { join as join11 } from "path";
3073
+ import { parse as parse4, stringify } from "yaml";
3074
+ var STATE_DIR = ".codeharness";
3075
+ var STATE_FILE = "workflow-state.yaml";
3076
+ function getDefaultWorkflowState() {
3077
+ return {
3078
+ workflow_name: "",
3079
+ started: "",
3080
+ iteration: 0,
3081
+ phase: "idle",
3082
+ tasks_completed: [],
3083
+ evaluator_scores: [],
3084
+ circuit_breaker: {
3085
+ triggered: false,
3086
+ reason: null,
3087
+ score_history: []
3088
+ },
3089
+ trace_ids: []
3090
+ };
3091
+ }
3092
+ function writeWorkflowState(state, dir) {
3093
+ const baseDir = dir ?? process.cwd();
3094
+ const stateDir = join11(baseDir, STATE_DIR);
3095
+ mkdirSync5(stateDir, { recursive: true });
3096
+ const yamlContent = stringify(state, { nullStr: "null" });
3097
+ writeFileSync7(join11(stateDir, STATE_FILE), yamlContent, "utf-8");
3098
+ }
3099
+ function readWorkflowState(dir) {
3100
+ const baseDir = dir ?? process.cwd();
3101
+ const filePath = join11(baseDir, STATE_DIR, STATE_FILE);
3102
+ if (!existsSync14(filePath)) {
3103
+ return getDefaultWorkflowState();
3104
+ }
3105
+ let raw;
3106
+ try {
3107
+ raw = readFileSync12(filePath, "utf-8");
3108
+ } catch {
3109
+ warn("workflow-state.yaml could not be read \u2014 returning default state");
3110
+ return getDefaultWorkflowState();
3111
+ }
3112
+ let parsed;
3113
+ try {
3114
+ parsed = parse4(raw);
3115
+ } catch {
3116
+ warn("workflow-state.yaml contains invalid YAML \u2014 returning default state");
3117
+ return getDefaultWorkflowState();
3118
+ }
3119
+ if (!isValidWorkflowState(parsed)) {
3120
+ warn("workflow-state.yaml has invalid shape \u2014 returning default state");
3121
+ return getDefaultWorkflowState();
3122
+ }
3123
+ return parsed;
3124
+ }
3125
+ function isValidWorkflowState(value) {
3126
+ if (!value || typeof value !== "object") return false;
3127
+ const s = value;
3128
+ if (typeof s.workflow_name !== "string") return false;
3129
+ if (typeof s.started !== "string") return false;
3130
+ if (typeof s.iteration !== "number") return false;
3131
+ if (typeof s.phase !== "string") return false;
3132
+ if (!Array.isArray(s.tasks_completed)) return false;
3133
+ if (!Array.isArray(s.evaluator_scores)) return false;
3134
+ if (!s.circuit_breaker || typeof s.circuit_breaker !== "object") return false;
3135
+ if (s.trace_ids !== void 0) {
3136
+ if (!Array.isArray(s.trace_ids)) return false;
3137
+ for (const id of s.trace_ids) {
3138
+ if (typeof id !== "string") return false;
3139
+ }
3140
+ }
3141
+ for (const t of s.tasks_completed) {
3142
+ if (!t || typeof t !== "object") return false;
3143
+ const tc = t;
3144
+ if (typeof tc.task_name !== "string") return false;
3145
+ if (typeof tc.story_key !== "string") return false;
3146
+ if (typeof tc.completed_at !== "string") return false;
3147
+ if (tc.session_id !== void 0 && typeof tc.session_id !== "string") return false;
3148
+ if (tc.error !== void 0 && typeof tc.error !== "boolean") return false;
3149
+ }
3150
+ for (const e of s.evaluator_scores) {
3151
+ if (!e || typeof e !== "object") return false;
3152
+ const es = e;
3153
+ if (typeof es.iteration !== "number") return false;
3154
+ if (typeof es.passed !== "number") return false;
3155
+ if (typeof es.failed !== "number") return false;
3156
+ if (typeof es.unknown !== "number") return false;
3157
+ if (typeof es.total !== "number") return false;
3158
+ if (typeof es.timestamp !== "string") return false;
3159
+ }
3160
+ const cb = s.circuit_breaker;
3161
+ if (typeof cb.triggered !== "boolean") return false;
3162
+ if (cb.reason !== null && typeof cb.reason !== "string") return false;
3163
+ if (!Array.isArray(cb.score_history)) return false;
3164
+ for (const score of cb.score_history) {
3165
+ if (typeof score !== "number") return false;
3166
+ }
3167
+ return true;
3168
+ }
3169
+
3170
+ // src/lib/workflow-machine.ts
3169
3171
  var HALT_ERROR_CODES = /* @__PURE__ */ new Set(["RATE_LIMIT", "NETWORK", "SDK_INIT"]);
3170
3172
  var DEFAULT_MAX_ITERATIONS = 5;
3173
+ var HEALTH_CHECK_TIMEOUT_MS = 5e3;
3174
+ var FILE_WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
3175
+ "Write",
3176
+ "Edit",
3177
+ "write_to_file",
3178
+ "edit_file",
3179
+ "write",
3180
+ "edit",
3181
+ "WriteFile",
3182
+ "EditFile"
3183
+ ]);
3184
+ var TASK_PROMPTS = {
3185
+ "create-story": (key) => `Create the story spec for ${key}. Read the epic definitions and architecture docs. Write a complete story file with acceptance criteria, tasks, and dev notes. CRITICAL: Every AC must be testable by a blind QA agent using ONLY a user guide + browser/API/CLI access. No AC should reference source code, internal data structures, or implementation details like O(1) complexity. Each AC must describe observable behavior that can be verified through UI interaction (agent-browser), API calls (curl), CLI commands (docker exec), or log inspection (docker logs). Wrap output in <story-spec>...</story-spec> tags.`,
3186
+ "implement": (key) => `Implement story ${key}`,
3187
+ "check": (key) => `Run automated checks for story ${key}. Execute the project's test suite, linter, and coverage tool. Include <verdict>pass</verdict> or <verdict>fail</verdict> in your response.`,
3188
+ "review": (key) => `Review the implementation of story ${key}. Check for correctness, security issues, architecture violations, and AC coverage. Include <verdict>pass</verdict> or <verdict>fail</verdict> in your response. If fail, include <issues>...</issues>.`,
3189
+ "document": (key) => `Write user documentation for story ${key}. Describe what was built and how to use it from a user's perspective. No source code. Wrap documentation in <user-docs>...</user-docs> tags.`,
3190
+ "deploy": () => `Provision the Docker environment for this project. Check for docker-compose.yml, start containers, verify health. Wrap report in <deploy-report>...</deploy-report> tags with status, containers, URLs, credentials, health.`,
3191
+ "verify": () => `Verify the epic's stories using the user docs and deploy info in ./story-files/. For each AC, derive verification steps, run commands, observe output. Include <verdict>pass</verdict> or <verdict>fail</verdict>. Include <evidence ac="N" status="pass|fail|unknown">...</evidence> per AC. Include <quality-scores>...</quality-scores>.`,
3192
+ "retro": () => `Run a retrospective for this epic. Analyze what worked, what failed, patterns, and action items for next epic.`
3193
+ };
3171
3194
  function buildCoverageDeduplicationContext(contract, projectDir) {
3172
3195
  if (!contract?.testResults) return null;
3173
3196
  const { coverage } = contract.testResults;
@@ -3187,16 +3210,14 @@ function propagateVerifyFlags(taskName, contract, projectDir) {
3187
3210
  const { failed, coverage } = contract.testResults;
3188
3211
  try {
3189
3212
  const { state, body } = readStateWithBody(projectDir);
3190
- if (failed === 0) {
3191
- state.session_flags.tests_passed = true;
3192
- }
3213
+ if (failed === 0) state.session_flags.tests_passed = true;
3193
3214
  if (coverage !== null && coverage !== void 0 && coverage >= state.coverage.target) {
3194
3215
  state.session_flags.coverage_met = true;
3195
3216
  }
3196
3217
  writeState(state, projectDir, body);
3197
3218
  } catch (err) {
3198
3219
  const msg = err instanceof Error ? err.message : String(err);
3199
- warn(`workflow-engine: flag propagation failed for ${taskName}: ${msg}`);
3220
+ warn(`workflow-machine: flag propagation failed for ${taskName}: ${msg}`);
3200
3221
  }
3201
3222
  }
3202
3223
  function isTaskCompleted(state, taskName, storyKey) {
@@ -3217,14 +3238,14 @@ function loadWorkItems(sprintStatusPath, issuesPath2) {
3217
3238
  try {
3218
3239
  raw = readFileSync13(sprintStatusPath, "utf-8");
3219
3240
  } catch {
3220
- warn(`workflow-engine: could not read sprint-status.yaml at ${sprintStatusPath}`);
3241
+ warn(`workflow-machine: could not read sprint-status.yaml at ${sprintStatusPath}`);
3221
3242
  return items;
3222
3243
  }
3223
3244
  let parsed;
3224
3245
  try {
3225
3246
  parsed = parse5(raw);
3226
3247
  } catch {
3227
- warn(`workflow-engine: invalid YAML in sprint-status.yaml at ${sprintStatusPath}`);
3248
+ warn(`workflow-machine: invalid YAML in sprint-status.yaml at ${sprintStatusPath}`);
3228
3249
  return items;
3229
3250
  }
3230
3251
  if (parsed && typeof parsed === "object") {
@@ -3246,14 +3267,14 @@ function loadWorkItems(sprintStatusPath, issuesPath2) {
3246
3267
  try {
3247
3268
  raw = readFileSync13(issuesPath2, "utf-8");
3248
3269
  } catch {
3249
- warn(`workflow-engine: could not read issues.yaml at ${issuesPath2}`);
3270
+ warn(`workflow-machine: could not read issues.yaml at ${issuesPath2}`);
3250
3271
  return items;
3251
3272
  }
3252
3273
  let parsed;
3253
3274
  try {
3254
3275
  parsed = parse5(raw);
3255
3276
  } catch {
3256
- warn(`workflow-engine: invalid YAML in issues.yaml at ${issuesPath2}`);
3277
+ warn(`workflow-machine: invalid YAML in issues.yaml at ${issuesPath2}`);
3257
3278
  return items;
3258
3279
  }
3259
3280
  if (parsed && typeof parsed === "object") {
@@ -3264,11 +3285,7 @@ function loadWorkItems(sprintStatusPath, issuesPath2) {
3264
3285
  if (issue && typeof issue === "object") {
3265
3286
  const status = issue.status;
3266
3287
  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
- });
3288
+ items.push({ key: issue.id, title: issue.title, source: "issues" });
3272
3289
  }
3273
3290
  }
3274
3291
  }
@@ -3277,101 +3294,45 @@ function loadWorkItems(sprintStatusPath, issuesPath2) {
3277
3294
  }
3278
3295
  return items;
3279
3296
  }
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;
3297
+ function buildRetryPrompt(storyKey, findings) {
3298
+ const failedFindings = findings.filter((f) => f.status === "fail" || f.status === "unknown");
3299
+ if (failedFindings.length === 0) return `Implement story ${storyKey}`;
3300
+ const formatted = failedFindings.map((f) => {
3301
+ let entry = `AC #${f.ac} (${f.status.toUpperCase()}): ${f.description}`;
3302
+ if (f.evidence?.reasoning) entry += `
3303
+ Evidence: ${f.evidence.reasoning}`;
3304
+ return entry;
3305
+ }).join("\n\n");
3306
+ return `Retry story ${storyKey}. Previous evaluator findings:
3307
+
3308
+ ${formatted}
3309
+
3310
+ Focus on fixing the failed criteria above.`;
3292
3311
  }
3293
- async function executeNullTask(task, taskName, storyKey, state, config, previousOutputContract, accumulatedCostUsd) {
3294
- const projectDir = config.projectDir ?? process.cwd();
3295
- const handler = getNullTask(taskName);
3296
- if (!handler) {
3297
- 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;
3306
- }
3307
- const startMs = Date.now();
3308
- const workflowStartMs = state.started ? new Date(state.started).getTime() : startMs;
3309
- const ctx = {
3310
- storyKey,
3311
- taskName,
3312
- cost: accumulatedCostUsd ?? 0,
3313
- durationMs: startMs - workflowStartMs,
3314
- outputContract: previousOutputContract ?? null,
3315
- projectDir
3316
- };
3317
- let result;
3318
- try {
3319
- 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;
3328
- }
3329
- const durationMs = Date.now() - startMs;
3330
- 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;
3338
- }
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
- };
3348
- const contract = {
3349
- version: 1,
3350
- taskName,
3351
- storyId: storyKey,
3352
- driver: "engine",
3353
- model: "null",
3354
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3355
- cost_usd: 0,
3356
- duration_ms: durationMs,
3357
- changedFiles: [],
3358
- testResults: null,
3359
- output: result.output ?? "",
3360
- acceptanceCriteria: []
3361
- };
3362
- writeWorkflowState(updatedState, projectDir);
3363
- return { updatedState, output: result.output ?? "", contract };
3312
+ function buildAllUnknownVerdict(workItems, reasoning) {
3313
+ const findings = workItems.map((_, index) => ({
3314
+ ac: index + 1,
3315
+ description: `AC #${index + 1}`,
3316
+ status: "unknown",
3317
+ evidence: { commands_run: [], output_observed: "", reasoning }
3318
+ }));
3319
+ return { verdict: "fail", score: { passed: 0, failed: 0, unknown: findings.length, total: findings.length }, findings };
3320
+ }
3321
+ function getFailedItems(verdict, allItems) {
3322
+ if (!verdict) return allItems;
3323
+ if (verdict.verdict === "pass") return [];
3324
+ return allItems;
3364
3325
  }
3365
- async function dispatchTaskWithResult(task, taskName, storyKey, definition, state, config, customPrompt, previousOutputContract, storyFiles) {
3326
+ async function dispatchTaskCore(input) {
3327
+ const { task, taskName, storyKey, definition, config, workflowState, previousContract, onStreamEvent, storyFiles, customPrompt, accumulatedCostUsd } = input;
3366
3328
  const projectDir = config.projectDir ?? process.cwd();
3367
- const traceId = generateTraceId(config.runId, state.iteration, taskName);
3329
+ const traceId = generateTraceId(config.runId, workflowState.iteration, taskName);
3368
3330
  const tracePrompt = formatTracePrompt(traceId);
3369
3331
  const sessionKey = { taskName, storyKey };
3370
- const sessionId = resolveSessionId(task.session, sessionKey, state);
3332
+ const sessionId = resolveSessionId(task.session, sessionKey, workflowState);
3371
3333
  const driverName = task.driver ?? "claude-code";
3372
3334
  const driver = getDriver(driverName);
3373
- const agentAsModelSource = { model: definition.model };
3374
- const model = resolveModel(task, agentAsModelSource, driver);
3335
+ const model = resolveModel(task, { model: definition.model }, driver);
3375
3336
  let cwd;
3376
3337
  let workspace = null;
3377
3338
  if (task.source_access === false) {
@@ -3384,37 +3345,20 @@ async function dispatchTaskWithResult(task, taskName, storyKey, definition, stat
3384
3345
  } else {
3385
3346
  cwd = projectDir;
3386
3347
  }
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
- };
3348
+ const isEpicSentinel = storyKey.startsWith("__epic_") || storyKey === "__run__";
3398
3349
  let basePrompt;
3399
3350
  if (customPrompt) {
3400
3351
  basePrompt = customPrompt;
3401
- } else if (isEpicSentinel && TASK_PROMPTS[taskName]) {
3402
- basePrompt = TASK_PROMPTS[taskName](storyKey);
3403
3352
  } else if (TASK_PROMPTS[taskName]) {
3404
3353
  basePrompt = TASK_PROMPTS[taskName](storyKey);
3405
3354
  } else {
3406
3355
  basePrompt = `Execute task "${taskName}" for story ${storyKey}`;
3407
3356
  }
3408
- let prompt = buildPromptWithContractContext(basePrompt, previousOutputContract ?? null);
3409
- const coverageDedup = buildCoverageDeduplicationContext(
3410
- previousOutputContract ?? null,
3411
- projectDir
3412
- );
3413
- if (coverageDedup) {
3414
- prompt = `${prompt}
3357
+ let prompt = buildPromptWithContractContext(basePrompt, previousContract);
3358
+ const coverageDedup = buildCoverageDeduplicationContext(previousContract, projectDir);
3359
+ if (coverageDedup) prompt = `${prompt}
3415
3360
 
3416
3361
  ${coverageDedup}`;
3417
- }
3418
3362
  const dispatchOpts = {
3419
3363
  prompt,
3420
3364
  model,
@@ -3422,15 +3366,11 @@ ${coverageDedup}`;
3422
3366
  sourceAccess: task.source_access !== false,
3423
3367
  ...sessionId ? { sessionId } : {},
3424
3368
  ...tracePrompt ? { appendSystemPrompt: tracePrompt } : {},
3425
- ...task.plugins ?? definition.plugins ? { plugins: task.plugins ?? definition.plugins } : {},
3426
- ...task.max_budget_usd != null ? { timeout: task.max_budget_usd } : {}
3369
+ ...task.plugins ?? definition.plugins ? { plugins: task.plugins ?? definition.plugins } : {}
3370
+ // Note: max_budget_usd is not mapped to timeout they have different semantics
3427
3371
  };
3428
3372
  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
- }
3373
+ if (emit) emit({ type: "dispatch-start", taskName, storyKey, driverName, model });
3434
3374
  let output = "";
3435
3375
  let resultSessionId = "";
3436
3376
  let cost = 0;
@@ -3441,28 +3381,21 @@ ${coverageDedup}`;
3441
3381
  const startMs = Date.now();
3442
3382
  try {
3443
3383
  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
- }
3384
+ if (onStreamEvent) onStreamEvent(event, driverName);
3385
+ if (emit) emit({ type: "stream-event", taskName, storyKey, driverName, streamEvent: event });
3386
+ if (event.type === "text") output += event.text;
3450
3387
  if (event.type === "tool-start") {
3451
- const toolStart = event;
3452
- activeToolName = toolStart.name;
3388
+ const ts = event;
3389
+ activeToolName = ts.name;
3453
3390
  activeToolInput = "";
3454
3391
  }
3455
- if (event.type === "tool-input") {
3456
- activeToolInput += event.partial;
3457
- }
3392
+ if (event.type === "tool-input") activeToolInput += event.partial;
3458
3393
  if (event.type === "tool-complete") {
3459
3394
  if (activeToolName && FILE_WRITE_TOOL_NAMES.has(activeToolName)) {
3460
3395
  try {
3461
3396
  const parsed = JSON.parse(activeToolInput);
3462
3397
  const filePath = parsed.file_path ?? parsed.path ?? parsed.filePath;
3463
- if (filePath && typeof filePath === "string") {
3464
- changedFiles.push(filePath);
3465
- }
3398
+ if (filePath && typeof filePath === "string") changedFiles.push(filePath);
3466
3399
  } catch {
3467
3400
  }
3468
3401
  }
@@ -3470,26 +3403,17 @@ ${coverageDedup}`;
3470
3403
  activeToolInput = "";
3471
3404
  }
3472
3405
  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
- }
3406
+ const r = event;
3407
+ resultSessionId = r.sessionId;
3408
+ cost = r.cost;
3409
+ if (r.error) errorEvent = { error: r.error, errorCategory: r.errorCategory };
3479
3410
  }
3480
3411
  }
3481
3412
  } finally {
3482
- if (workspace) {
3483
- await workspace.cleanup();
3484
- }
3413
+ if (workspace) await workspace.cleanup();
3485
3414
  }
3486
3415
  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
- }
3416
+ if (emit) emit({ type: "dispatch-end", taskName, storyKey, driverName, elapsedMs, costUsd: cost });
3493
3417
  if (errorEvent) {
3494
3418
  const categoryToCode = {
3495
3419
  RATE_LIMIT: "RATE_LIMIT",
@@ -3500,14 +3424,9 @@ ${coverageDedup}`;
3500
3424
  UNKNOWN: "UNKNOWN"
3501
3425
  };
3502
3426
  const code = categoryToCode[errorEvent.errorCategory ?? "UNKNOWN"] ?? "UNKNOWN";
3503
- throw new DispatchError(
3504
- errorEvent.error,
3505
- code,
3506
- definition.name,
3507
- errorEvent
3508
- );
3427
+ throw new DispatchError(errorEvent.error, code, definition.name, errorEvent);
3509
3428
  }
3510
- let updatedState = state;
3429
+ let updatedState = workflowState;
3511
3430
  if (resultSessionId) {
3512
3431
  updatedState = recordSessionId(sessionKey, resultSessionId, updatedState);
3513
3432
  } else {
@@ -3516,10 +3435,7 @@ ${coverageDedup}`;
3516
3435
  story_key: storyKey,
3517
3436
  completed_at: (/* @__PURE__ */ new Date()).toISOString()
3518
3437
  };
3519
- updatedState = {
3520
- ...updatedState,
3521
- tasks_completed: [...updatedState.tasks_completed, checkpoint]
3522
- };
3438
+ updatedState = { ...updatedState, tasks_completed: [...updatedState.tasks_completed, checkpoint] };
3523
3439
  }
3524
3440
  updatedState = recordTraceId(traceId, updatedState);
3525
3441
  const durationMs = Date.now() - startMs;
@@ -3541,342 +3457,278 @@ ${coverageDedup}`;
3541
3457
  };
3542
3458
  writeOutputContract(contract, join12(projectDir, ".codeharness", "contracts"));
3543
3459
  } 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}`);
3460
+ const msg = err instanceof Error ? err.message : String(err);
3461
+ warn(`workflow-machine: failed to write output contract for ${taskName}/${storyKey}: ${msg}`);
3546
3462
  contract = null;
3547
3463
  }
3548
3464
  writeWorkflowState(updatedState, projectDir);
3549
- return { updatedState, output, contract };
3550
- }
3551
- function buildRetryPrompt(storyKey, findings) {
3552
- const failedFindings = findings.filter(
3553
- (f) => f.status === "fail" || f.status === "unknown"
3554
- );
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}`;
3564
- }
3565
- return entry;
3566
- }).join("\n\n");
3567
- return `Retry story ${storyKey}. Previous evaluator findings:
3568
-
3569
- ${formattedFindings}
3570
-
3571
- Focus on fixing the failed criteria above.`;
3465
+ propagateVerifyFlags(taskName, contract, projectDir);
3466
+ return { output, cost, changedFiles, sessionId: resultSessionId, contract, updatedState };
3572
3467
  }
3573
- function buildAllUnknownVerdict(workItems, reasoning) {
3574
- const findings = workItems.map((_, index) => ({
3575
- ac: index + 1,
3576
- description: `AC #${index + 1}`,
3577
- status: "unknown",
3578
- evidence: {
3579
- commands_run: [],
3580
- output_observed: "",
3581
- reasoning
3582
- }
3583
- }));
3584
- return {
3585
- verdict: "fail",
3586
- score: {
3587
- passed: 0,
3588
- failed: 0,
3589
- unknown: findings.length,
3590
- total: findings.length
3591
- },
3592
- findings
3468
+ async function nullTaskCore(input) {
3469
+ const { task, taskName, storyKey, config, workflowState, previousContract, accumulatedCostUsd } = input;
3470
+ const projectDir = config.projectDir ?? process.cwd();
3471
+ const handler = getNullTask(taskName);
3472
+ if (!handler) {
3473
+ const registered = listNullTasks();
3474
+ throw { taskName, storyKey, code: "NULL_TASK_NOT_FOUND", message: `No null task handler registered for "${taskName}". Registered: ${registered.join(", ") || "(none)"}` };
3475
+ }
3476
+ const startMs = Date.now();
3477
+ const workflowStartMs = workflowState.started ? new Date(workflowState.started).getTime() : startMs;
3478
+ const ctx = {
3479
+ storyKey,
3480
+ taskName,
3481
+ cost: accumulatedCostUsd,
3482
+ durationMs: startMs - workflowStartMs,
3483
+ outputContract: previousContract,
3484
+ projectDir
3485
+ };
3486
+ let result;
3487
+ try {
3488
+ result = await handler(ctx);
3489
+ } catch (err) {
3490
+ throw { taskName, storyKey, code: "NULL_TASK_HANDLER_ERROR", message: `Null task handler "${taskName}" threw: ${err instanceof Error ? err.message : String(err)}` };
3491
+ }
3492
+ if (!result.success) {
3493
+ throw { taskName, storyKey, code: "NULL_TASK_FAILED", message: `Null task handler "${taskName}" returned success=false${result.output ? `: ${result.output}` : ""}` };
3494
+ }
3495
+ const checkpoint = { task_name: taskName, story_key: storyKey, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
3496
+ const updatedState = { ...workflowState, tasks_completed: [...workflowState.tasks_completed, checkpoint] };
3497
+ const durationMs = Date.now() - startMs;
3498
+ const contract = {
3499
+ version: 1,
3500
+ taskName,
3501
+ storyId: storyKey,
3502
+ driver: "engine",
3503
+ model: "null",
3504
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3505
+ cost_usd: 0,
3506
+ duration_ms: durationMs,
3507
+ changedFiles: [],
3508
+ testResults: null,
3509
+ output: result.output ?? "",
3510
+ acceptanceCriteria: []
3593
3511
  };
3512
+ writeWorkflowState(updatedState, projectDir);
3513
+ return { output: result.output ?? "", cost: 0, changedFiles: [], sessionId: "", contract, updatedState };
3594
3514
  }
3595
- function getFailedItems(verdict, allItems) {
3596
- if (!verdict) return allItems;
3597
- if (verdict.verdict === "pass") return [];
3598
- return allItems;
3515
+ function isLoopBlock(step) {
3516
+ return typeof step === "object" && step !== null && "loop" in step;
3599
3517
  }
3600
- async function executeLoopBlock(loopBlock, state, config, workItems, initialContract, storyFlowTasks) {
3518
+ var dispatchActor = fromPromise(async ({ input }) => {
3519
+ return dispatchTaskCore(input);
3520
+ });
3521
+ var nullTaskDispatchActor = fromPromise(async ({ input }) => {
3522
+ return nullTaskCore(input);
3523
+ });
3524
+ var loopIterationActor = fromPromise(async ({ input }) => {
3525
+ const { loopBlock, config, workItems, storyFlowTasks, onStreamEvent, maxIterations } = input;
3526
+ let { currentState, errors, tasksCompleted, lastContract, lastVerdict, accumulatedCostUsd } = input;
3601
3527
  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
- }
3528
+ const RUN_SENTINEL = "__run__";
3612
3529
  const lastAgentTaskInLoop = (() => {
3613
3530
  for (let i = loopBlock.loop.length - 1; i >= 0; i--) {
3614
3531
  const tn = loopBlock.loop[i];
3615
3532
  const t = config.workflow.tasks[tn];
3616
3533
  if (t && t.agent !== null) return tn;
3617
- }
3618
- return loopBlock.loop[loopBlock.loop.length - 1];
3619
- })();
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));
3627
- }
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);
3636
- }
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`);
3642
- continue;
3643
- }
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);
3670
- }
3671
- }
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;
3676
- }
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);
3695
- }
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);
3732
- 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
- }
3846
- writeWorkflowState(currentState, projectDir);
3534
+ }
3535
+ return loopBlock.loop[loopBlock.loop.length - 1];
3536
+ })();
3537
+ const nextIteration = currentState.iteration + 1;
3538
+ const allCurrentIterationDone = currentState.iteration > 0 && loopBlock.loop.every((tn) => {
3539
+ const t = config.workflow.tasks[tn];
3540
+ if (!t) return true;
3541
+ if (storyFlowTasks?.has(tn)) return workItems.every((item) => isLoopTaskCompleted(currentState, tn, item.key, currentState.iteration));
3542
+ return isLoopTaskCompleted(currentState, tn, RUN_SENTINEL, currentState.iteration);
3543
+ });
3544
+ if (currentState.iteration === 0 || allCurrentIterationDone) {
3545
+ currentState = { ...currentState, iteration: nextIteration };
3546
+ writeWorkflowState(currentState, projectDir);
3547
+ }
3548
+ let haltedInLoop = false;
3549
+ for (const taskName of loopBlock.loop) {
3550
+ const task = config.workflow.tasks[taskName];
3551
+ if (!task) {
3552
+ warn(`workflow-machine: task "${taskName}" not found in workflow tasks, skipping`);
3553
+ continue;
3554
+ }
3555
+ if (task.agent === null) {
3556
+ const items2 = storyFlowTasks?.has(taskName) ? lastVerdict ? getFailedItems(lastVerdict, workItems) : workItems : [{ key: RUN_SENTINEL, source: "sprint" }];
3557
+ for (const item of items2) {
3558
+ if (isLoopTaskCompleted(currentState, taskName, item.key, currentState.iteration)) {
3559
+ warn(`workflow-machine: skipping completed task ${taskName} for ${item.key}`);
3560
+ continue;
3561
+ }
3562
+ try {
3563
+ const nr = await nullTaskCore({ task, taskName, storyKey: item.key, config, workflowState: currentState, previousContract: lastContract, accumulatedCostUsd });
3564
+ currentState = nr.updatedState;
3565
+ lastContract = nr.contract;
3566
+ tasksCompleted++;
3847
3567
  } catch (err) {
3848
- const engineError = handleDispatchError(err, taskName, PER_RUN_SENTINEL);
3568
+ const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, item.key);
3849
3569
  errors.push(engineError);
3850
- currentState = recordErrorInState(currentState, taskName, PER_RUN_SENTINEL, engineError);
3570
+ currentState = recordErrorInState(currentState, taskName, item.key, engineError);
3851
3571
  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;
3858
3572
  }
3859
3573
  }
3574
+ continue;
3860
3575
  }
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 };
3576
+ const definition = config.agents[task.agent];
3577
+ if (!definition) {
3578
+ warn(`workflow-machine: agent "${task.agent}" not found for task "${taskName}", skipping`);
3579
+ continue;
3871
3580
  }
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 };
3581
+ const items = storyFlowTasks?.has(taskName) ? lastVerdict ? getFailedItems(lastVerdict, workItems) : workItems : [{ key: RUN_SENTINEL, source: "sprint" }];
3582
+ for (const item of items) {
3583
+ if (isLoopTaskCompleted(currentState, taskName, item.key, currentState.iteration)) {
3584
+ warn(`workflow-machine: skipping completed task ${taskName} for ${item.key}`);
3585
+ continue;
3586
+ }
3587
+ const prompt = lastVerdict ? buildRetryPrompt(item.key, lastVerdict.findings) : void 0;
3588
+ try {
3589
+ const dr = await dispatchTaskCore({ task, taskName, storyKey: item.key, definition, config, workflowState: currentState, previousContract: lastContract, onStreamEvent, customPrompt: prompt });
3590
+ currentState = dr.updatedState;
3591
+ lastContract = dr.contract;
3592
+ accumulatedCostUsd += dr.contract?.cost_usd ?? 0;
3593
+ tasksCompleted++;
3594
+ if (taskName === lastAgentTaskInLoop && !storyFlowTasks?.has(taskName)) {
3595
+ let verdict = null;
3596
+ try {
3597
+ verdict = parseVerdict(dr.output);
3598
+ } catch (parseErr) {
3599
+ if (parseErr instanceof VerdictParseError && parseErr.retryable) {
3600
+ try {
3601
+ const retryResult = await dispatchTaskCore({ task, taskName, storyKey: item.key, definition, config, workflowState: currentState, previousContract: lastContract, onStreamEvent });
3602
+ currentState = retryResult.updatedState;
3603
+ lastContract = retryResult.contract;
3604
+ tasksCompleted++;
3605
+ verdict = parseVerdict(retryResult.output);
3606
+ } catch {
3607
+ verdict = buildAllUnknownVerdict(workItems, "Evaluator failed after retry");
3608
+ }
3609
+ }
3610
+ }
3611
+ if (!verdict) {
3612
+ const tagged = parseVerdictTag(dr.output);
3613
+ if (tagged) verdict = { verdict: tagged.verdict, score: { passed: tagged.verdict === "pass" ? 1 : 0, failed: tagged.verdict === "fail" ? 1 : 0, unknown: 0, total: 1 }, findings: [] };
3614
+ }
3615
+ lastVerdict = verdict;
3616
+ if (verdict) {
3617
+ 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() };
3618
+ currentState = { ...currentState, evaluator_scores: [...currentState.evaluator_scores, score] };
3619
+ }
3620
+ const cbDecision = evaluateProgress(currentState.evaluator_scores);
3621
+ if (cbDecision.halt) {
3622
+ currentState = { ...currentState, circuit_breaker: { triggered: true, reason: cbDecision.reason, score_history: cbDecision.scoreHistory } };
3623
+ writeWorkflowState(currentState, projectDir);
3624
+ }
3625
+ writeWorkflowState(currentState, projectDir);
3626
+ }
3627
+ } catch (err) {
3628
+ const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, item.key);
3629
+ errors.push(engineError);
3630
+ currentState = recordErrorInState(currentState, taskName, item.key, engineError);
3631
+ writeWorkflowState(currentState, projectDir);
3632
+ if (err instanceof DispatchError && HALT_ERROR_CODES.has(err.code)) {
3633
+ haltedInLoop = true;
3634
+ break;
3635
+ }
3636
+ }
3876
3637
  }
3877
- }
3638
+ if (haltedInLoop) break;
3639
+ }
3640
+ return { ...input, currentState, errors, tasksCompleted, halted: haltedInLoop, lastContract, lastVerdict, accumulatedCostUsd };
3641
+ });
3642
+ var loopMachine = setup({
3643
+ types: {},
3644
+ actors: { loopIterationActor },
3645
+ guards: {
3646
+ halted: ({ context }) => context.halted,
3647
+ verdictPass: ({ context }) => context.lastVerdict?.verdict === "pass",
3648
+ maxIterations: ({ context }) => context.currentState.iteration >= context.maxIterations,
3649
+ circuitBreaker: ({ context }) => context.currentState.circuit_breaker.triggered
3650
+ }
3651
+ }).createMachine({
3652
+ id: "loopBlock",
3653
+ context: ({ input }) => input,
3654
+ initial: "checkEmpty",
3655
+ states: {
3656
+ checkEmpty: {
3657
+ always: [
3658
+ { guard: ({ context }) => context.loopBlock.loop.length === 0, target: "done" },
3659
+ { target: "iterating" }
3660
+ ]
3661
+ },
3662
+ iterating: {
3663
+ invoke: {
3664
+ src: "loopIterationActor",
3665
+ input: ({ context }) => context,
3666
+ onDone: {
3667
+ target: "checkTermination",
3668
+ actions: assign(({ event }) => event.output)
3669
+ },
3670
+ onError: { target: "halted" }
3671
+ }
3672
+ },
3673
+ checkTermination: {
3674
+ always: [
3675
+ { guard: "halted", target: "halted" },
3676
+ { guard: "verdictPass", target: "done" },
3677
+ { guard: "maxIterations", target: "maxIterationsReached" },
3678
+ { guard: "circuitBreaker", target: "circuitBreakerTriggered" },
3679
+ { target: "iterating" }
3680
+ ]
3681
+ },
3682
+ done: { type: "final" },
3683
+ halted: { type: "final" },
3684
+ maxIterationsReached: {
3685
+ type: "final",
3686
+ entry: [
3687
+ assign(({ context }) => ({ ...context, currentState: { ...context.currentState, phase: "max-iterations" } })),
3688
+ ({ context }) => {
3689
+ const projectDir = context.config.projectDir ?? process.cwd();
3690
+ writeWorkflowState(context.currentState, projectDir);
3691
+ }
3692
+ ]
3693
+ },
3694
+ circuitBreakerTriggered: {
3695
+ type: "final",
3696
+ entry: [
3697
+ assign(({ context }) => ({ ...context, currentState: { ...context.currentState, phase: "circuit-breaker" } })),
3698
+ ({ context }) => {
3699
+ const projectDir = context.config.projectDir ?? process.cwd();
3700
+ writeWorkflowState(context.currentState, projectDir);
3701
+ }
3702
+ ]
3703
+ }
3704
+ }
3705
+ });
3706
+ async function executeLoopBlock(loopBlock, state, config, workItems, initialContract, storyFlowTasks, onStreamEvent) {
3707
+ const input = {
3708
+ loopBlock,
3709
+ config,
3710
+ workItems,
3711
+ storyFlowTasks,
3712
+ onStreamEvent,
3713
+ maxIterations: config.maxIterations ?? DEFAULT_MAX_ITERATIONS,
3714
+ currentState: state,
3715
+ errors: [],
3716
+ tasksCompleted: 0,
3717
+ halted: false,
3718
+ lastContract: initialContract ?? null,
3719
+ lastVerdict: null,
3720
+ accumulatedCostUsd: 0
3721
+ };
3722
+ const actor = createActor(loopMachine, { input });
3723
+ return new Promise((resolve7) => {
3724
+ actor.subscribe({ complete: () => {
3725
+ const snap = actor.getSnapshot();
3726
+ const ctx = snap.context;
3727
+ resolve7({ state: ctx.currentState, errors: ctx.errors, tasksCompleted: ctx.tasksCompleted, halted: ctx.halted, lastContract: ctx.lastContract });
3728
+ } });
3729
+ actor.start();
3730
+ });
3878
3731
  }
3879
- var HEALTH_CHECK_TIMEOUT_MS = 5e3;
3880
3732
  async function checkDriverHealth(workflow, timeoutMs) {
3881
3733
  const driverNames = /* @__PURE__ */ new Set();
3882
3734
  for (const task of Object.values(workflow.tasks)) {
@@ -3884,9 +3736,7 @@ async function checkDriverHealth(workflow, timeoutMs) {
3884
3736
  driverNames.add(task.driver ?? "claude-code");
3885
3737
  }
3886
3738
  const drivers = /* @__PURE__ */ new Map();
3887
- for (const name of driverNames) {
3888
- drivers.set(name, getDriver(name));
3889
- }
3739
+ for (const name of driverNames) drivers.set(name, getDriver(name));
3890
3740
  const responded = /* @__PURE__ */ new Set();
3891
3741
  const healthChecks = Promise.all(
3892
3742
  [...drivers.entries()].map(async ([name, driver]) => {
@@ -3903,78 +3753,378 @@ async function checkDriverHealth(workflow, timeoutMs) {
3903
3753
  const result = await Promise.race([healthChecks, timeoutPromise]);
3904
3754
  if (result === "timeout") {
3905
3755
  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
- );
3756
+ throw new Error(`Driver health check timed out after ${effectiveTimeout}ms. Drivers: ${(pending.length > 0 ? pending : [...driverNames]).join(", ")}`);
3910
3757
  }
3911
3758
  clearTimeout(timer);
3912
3759
  const failures = result.filter((r) => !r.health.available);
3913
3760
  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}`);
3761
+ throw new Error(`Driver health check failed: ${failures.map((f) => `${f.name}: ${f.health.error ?? "unavailable"}`).join("; ")}`);
3916
3762
  }
3917
3763
  }
3918
- async function executeWorkflow(config) {
3919
- const startMs = Date.now();
3764
+ function collectGuideFiles(epicItems, epicSentinel, projectDir) {
3765
+ const guidesDir = join12(projectDir, ".codeharness", "verify-guides");
3766
+ const guideFiles = [];
3767
+ try {
3768
+ mkdirSync6(guidesDir, { recursive: true });
3769
+ } catch {
3770
+ return guideFiles;
3771
+ }
3772
+ for (const item of epicItems) {
3773
+ try {
3774
+ const contractPath = join12(projectDir, ".codeharness", "contracts", `document-${item.key}.json`);
3775
+ if (existsSync15(contractPath)) {
3776
+ const contractData = JSON.parse(readFileSync13(contractPath, "utf-8"));
3777
+ const docs = contractData.output ? extractTag(contractData.output, "user-docs") ?? contractData.output : null;
3778
+ if (docs) {
3779
+ const guidePath = join12(guidesDir, `${item.key}-guide.md`);
3780
+ writeFileSync8(guidePath, docs, "utf-8");
3781
+ guideFiles.push(guidePath);
3782
+ }
3783
+ }
3784
+ } catch {
3785
+ }
3786
+ }
3787
+ try {
3788
+ const deployContractPath = join12(projectDir, ".codeharness", "contracts", `deploy-${epicSentinel}.json`);
3789
+ if (existsSync15(deployContractPath)) {
3790
+ const deployData = JSON.parse(readFileSync13(deployContractPath, "utf-8"));
3791
+ const report = deployData.output ? extractTag(deployData.output, "deploy-report") ?? deployData.output : null;
3792
+ if (report) {
3793
+ const deployPath = join12(guidesDir, "deploy-info.md");
3794
+ writeFileSync8(deployPath, report, "utf-8");
3795
+ guideFiles.push(deployPath);
3796
+ }
3797
+ }
3798
+ } catch {
3799
+ }
3800
+ return guideFiles;
3801
+ }
3802
+ function cleanupGuideFiles(projectDir) {
3803
+ const guidesDir = join12(projectDir, ".codeharness", "verify-guides");
3804
+ try {
3805
+ rmSync2(guidesDir, { recursive: true, force: true });
3806
+ } catch {
3807
+ }
3808
+ }
3809
+ var storyFlowActor = fromPromise(async ({ input }) => {
3810
+ const { item, config, storyFlowTasks } = input;
3811
+ let { workflowState: state, lastContract, accumulatedCostUsd } = input;
3920
3812
  const projectDir = config.projectDir ?? process.cwd();
3921
3813
  const errors = [];
3922
3814
  let tasksCompleted = 0;
3923
- const processedStories = /* @__PURE__ */ new Set();
3815
+ let halted = false;
3816
+ for (const storyStep of config.workflow.storyFlow) {
3817
+ if (halted || config.abortSignal?.aborted) {
3818
+ halted = true;
3819
+ break;
3820
+ }
3821
+ if (isLoopBlock(storyStep)) {
3822
+ const loopResult = await executeLoopBlock(storyStep, state, config, [item], lastContract, storyFlowTasks);
3823
+ state = loopResult.state;
3824
+ errors.push(...loopResult.errors);
3825
+ tasksCompleted += loopResult.tasksCompleted;
3826
+ lastContract = loopResult.lastContract;
3827
+ if (loopResult.halted || state.phase === "max-iterations" || state.phase === "circuit-breaker") {
3828
+ halted = true;
3829
+ break;
3830
+ }
3831
+ continue;
3832
+ }
3833
+ if (typeof storyStep !== "string") continue;
3834
+ const taskName = storyStep;
3835
+ const task = config.workflow.tasks[taskName];
3836
+ if (!task) {
3837
+ warn(`workflow-machine: task "${taskName}" not found in workflow tasks, skipping`);
3838
+ continue;
3839
+ }
3840
+ if (task.agent === null) {
3841
+ if (isTaskCompleted(state, taskName, item.key)) continue;
3842
+ try {
3843
+ const nr = await nullTaskCore({ task, taskName, storyKey: item.key, config, workflowState: state, previousContract: lastContract, accumulatedCostUsd });
3844
+ state = nr.updatedState;
3845
+ lastContract = nr.contract;
3846
+ tasksCompleted++;
3847
+ } catch (err) {
3848
+ const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, item.key);
3849
+ errors.push(engineError);
3850
+ state = recordErrorInState(state, taskName, item.key, engineError);
3851
+ writeWorkflowState(state, projectDir);
3852
+ break;
3853
+ }
3854
+ continue;
3855
+ }
3856
+ const definition = config.agents[task.agent];
3857
+ if (!definition) {
3858
+ warn(`workflow-machine: agent "${task.agent}" not found for task "${taskName}", skipping`);
3859
+ continue;
3860
+ }
3861
+ if (isTaskCompleted(state, taskName, item.key)) continue;
3862
+ try {
3863
+ const dr = await dispatchTaskCore({ task, taskName, storyKey: item.key, definition, config, workflowState: state, previousContract: lastContract });
3864
+ state = dr.updatedState;
3865
+ lastContract = dr.contract;
3866
+ accumulatedCostUsd += dr.contract?.cost_usd ?? 0;
3867
+ tasksCompleted++;
3868
+ } catch (err) {
3869
+ const engineError = handleDispatchError(err, taskName, item.key);
3870
+ errors.push(engineError);
3871
+ if (config.onEvent) config.onEvent({ type: "dispatch-error", taskName, storyKey: item.key, error: { code: engineError.code, message: engineError.message } });
3872
+ state = recordErrorInState(state, taskName, item.key, engineError);
3873
+ writeWorkflowState(state, projectDir);
3874
+ if (err instanceof DispatchError && HALT_ERROR_CODES.has(err.code)) halted = true;
3875
+ break;
3876
+ }
3877
+ }
3878
+ if (!halted && config.onEvent) {
3879
+ config.onEvent({ type: "story-done", taskName: "story_flow", storyKey: item.key });
3880
+ }
3881
+ return { workflowState: state, errors, tasksCompleted, lastContract, accumulatedCostUsd, halted };
3882
+ });
3883
+ var epicStepActor = fromPromise(async ({ input }) => {
3884
+ const { epicId, epicItems, config, storyFlowTasks } = input;
3885
+ let { workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted, currentStepIndex } = input;
3886
+ const projectDir = config.projectDir ?? process.cwd();
3887
+ const step = config.workflow.epicFlow[currentStepIndex];
3888
+ if (!step || halted || config.abortSignal?.aborted) {
3889
+ if (config.abortSignal?.aborted) {
3890
+ state = { ...state, phase: "interrupted" };
3891
+ writeWorkflowState(state, projectDir);
3892
+ }
3893
+ return { ...input, workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted: true, currentStepIndex };
3894
+ }
3895
+ if (step === "story_flow") {
3896
+ for (const item of epicItems) {
3897
+ if (halted || config.abortSignal?.aborted) {
3898
+ halted = true;
3899
+ break;
3900
+ }
3901
+ storiesProcessed.add(item.key);
3902
+ const storyResult = await new Promise((resolve7, reject) => {
3903
+ const a = createActor(storyFlowActor, { input: { item, config, workflowState: state, lastContract, accumulatedCostUsd, storyFlowTasks } });
3904
+ a.subscribe({ complete: () => resolve7(a.getSnapshot().output), error: reject });
3905
+ a.start();
3906
+ });
3907
+ state = storyResult.workflowState;
3908
+ errors.push(...storyResult.errors);
3909
+ tasksCompleted += storyResult.tasksCompleted;
3910
+ lastContract = storyResult.lastContract;
3911
+ accumulatedCostUsd = storyResult.accumulatedCostUsd;
3912
+ if (storyResult.halted) {
3913
+ halted = true;
3914
+ break;
3915
+ }
3916
+ }
3917
+ return { ...input, workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted, currentStepIndex: currentStepIndex + 1 };
3918
+ }
3919
+ if (isLoopBlock(step)) {
3920
+ const loopResult = await executeLoopBlock(step, state, config, epicItems, lastContract, storyFlowTasks);
3921
+ state = loopResult.state;
3922
+ errors.push(...loopResult.errors);
3923
+ tasksCompleted += loopResult.tasksCompleted;
3924
+ lastContract = loopResult.lastContract;
3925
+ for (const item of epicItems) storiesProcessed.add(item.key);
3926
+ if (loopResult.halted || state.phase === "max-iterations" || state.phase === "circuit-breaker") halted = true;
3927
+ return { ...input, workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted, currentStepIndex: currentStepIndex + 1 };
3928
+ }
3929
+ const taskName = step;
3930
+ const task = config.workflow.tasks[taskName];
3931
+ if (!task) {
3932
+ warn(`workflow-machine: task "${taskName}" not found in workflow tasks, skipping`);
3933
+ return { ...input, currentStepIndex: currentStepIndex + 1 };
3934
+ }
3935
+ const epicSentinel = `__epic_${epicId}__`;
3936
+ if (task.agent === null) {
3937
+ if (!isTaskCompleted(state, taskName, epicSentinel)) {
3938
+ try {
3939
+ const nr = await nullTaskCore({ task, taskName, storyKey: epicSentinel, config, workflowState: state, previousContract: lastContract, accumulatedCostUsd });
3940
+ state = nr.updatedState;
3941
+ lastContract = nr.contract;
3942
+ tasksCompleted++;
3943
+ } catch (err) {
3944
+ const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, epicSentinel);
3945
+ errors.push(engineError);
3946
+ state = recordErrorInState(state, taskName, epicSentinel, engineError);
3947
+ writeWorkflowState(state, projectDir);
3948
+ }
3949
+ }
3950
+ return { ...input, workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted, currentStepIndex: currentStepIndex + 1 };
3951
+ }
3952
+ const definition = config.agents[task.agent];
3953
+ if (!definition) {
3954
+ warn(`workflow-machine: agent "${task.agent}" not found for task "${taskName}", skipping`);
3955
+ return { ...input, currentStepIndex: currentStepIndex + 1 };
3956
+ }
3957
+ if (isTaskCompleted(state, taskName, epicSentinel)) {
3958
+ return { ...input, currentStepIndex: currentStepIndex + 1 };
3959
+ }
3960
+ let guideFiles = [];
3961
+ if (task.source_access === false) guideFiles = collectGuideFiles(epicItems, epicSentinel, projectDir);
3962
+ try {
3963
+ const dr = await dispatchTaskCore({ task, taskName, storyKey: epicSentinel, definition, config, workflowState: state, previousContract: lastContract, storyFiles: guideFiles });
3964
+ state = dr.updatedState;
3965
+ lastContract = dr.contract;
3966
+ accumulatedCostUsd += dr.contract?.cost_usd ?? 0;
3967
+ tasksCompleted++;
3968
+ } catch (err) {
3969
+ const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, epicSentinel);
3970
+ errors.push(engineError);
3971
+ if (config.onEvent) config.onEvent({ type: "dispatch-error", taskName, storyKey: epicSentinel, error: { code: engineError.code, message: engineError.message } });
3972
+ state = recordErrorInState(state, taskName, epicSentinel, engineError);
3973
+ writeWorkflowState(state, projectDir);
3974
+ if (err instanceof DispatchError && HALT_ERROR_CODES.has(err.code)) halted = true;
3975
+ } finally {
3976
+ if (guideFiles.length > 0) cleanupGuideFiles(projectDir);
3977
+ }
3978
+ return { ...input, workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted, currentStepIndex: currentStepIndex + 1 };
3979
+ });
3980
+ var epicMachine = setup({
3981
+ types: {},
3982
+ actors: { epicStepActor },
3983
+ guards: {
3984
+ epicDone: ({ context }) => context.halted || context.currentStepIndex >= context.config.workflow.epicFlow.length
3985
+ }
3986
+ }).createMachine({
3987
+ id: "epic",
3988
+ context: ({ input }) => input,
3989
+ initial: "processingStep",
3990
+ states: {
3991
+ processingStep: {
3992
+ invoke: {
3993
+ src: "epicStepActor",
3994
+ input: ({ context }) => context,
3995
+ onDone: {
3996
+ target: "checkNext",
3997
+ actions: assign(({ event }) => event.output)
3998
+ },
3999
+ onError: {
4000
+ target: "done",
4001
+ actions: assign(({ context, event }) => {
4002
+ const msg = event.error instanceof Error ? event.error.message : String(event.error);
4003
+ return { ...context, errors: [...context.errors, { taskName: "__epic_actor__", storyKey: context.epicId, code: "ACTOR_ERROR", message: msg }], halted: true };
4004
+ })
4005
+ }
4006
+ }
4007
+ },
4008
+ checkNext: {
4009
+ always: [
4010
+ { guard: "epicDone", target: "done" },
4011
+ { target: "processingStep" }
4012
+ ]
4013
+ },
4014
+ done: { type: "final" }
4015
+ }
4016
+ });
4017
+ var runEpicActor = fromPromise(async ({ input }) => {
4018
+ const { config, storyFlowTasks, epicEntries, currentEpicIndex } = input;
4019
+ let { workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted } = input;
4020
+ if (currentEpicIndex >= epicEntries.length || halted || config.abortSignal?.aborted) {
4021
+ if (config.abortSignal?.aborted) {
4022
+ const projectDir = config.projectDir ?? process.cwd();
4023
+ state = { ...state, phase: "interrupted" };
4024
+ writeWorkflowState(state, projectDir);
4025
+ }
4026
+ return { ...input, workflowState: state, halted: true };
4027
+ }
4028
+ const [epicId, epicItems] = epicEntries[currentEpicIndex];
4029
+ if (config.onEvent) {
4030
+ config.onEvent({ type: "dispatch-start", taskName: "story_flow", storyKey: `__epic_${epicId}__` });
4031
+ }
4032
+ const epicInput = {
4033
+ epicId,
4034
+ epicItems,
4035
+ config,
4036
+ storyFlowTasks,
4037
+ currentStoryIndex: 0,
4038
+ workflowState: state,
4039
+ errors: [],
4040
+ tasksCompleted: 0,
4041
+ storiesProcessed: /* @__PURE__ */ new Set(),
4042
+ lastContract,
4043
+ accumulatedCostUsd,
4044
+ halted: false,
4045
+ currentStepIndex: 0
4046
+ };
4047
+ const epicResult = await new Promise((resolve7) => {
4048
+ const actor = createActor(epicMachine, { input: epicInput });
4049
+ actor.subscribe({ complete: () => resolve7(actor.getSnapshot().context) });
4050
+ actor.start();
4051
+ });
4052
+ state = epicResult.workflowState;
4053
+ errors.push(...epicResult.errors);
4054
+ tasksCompleted += epicResult.tasksCompleted;
4055
+ for (const key of epicResult.storiesProcessed) storiesProcessed.add(key);
4056
+ lastContract = epicResult.lastContract;
4057
+ accumulatedCostUsd = epicResult.accumulatedCostUsd;
4058
+ halted = epicResult.halted;
4059
+ return { ...input, workflowState: state, errors, tasksCompleted, storiesProcessed, lastContract, accumulatedCostUsd, halted, currentEpicIndex: currentEpicIndex + 1 };
4060
+ });
4061
+ var runMachine = setup({
4062
+ types: {},
4063
+ actors: { runEpicActor },
4064
+ guards: {
4065
+ allEpicsDone: ({ context }) => context.halted || context.currentEpicIndex >= context.epicEntries.length
4066
+ }
4067
+ }).createMachine({
4068
+ id: "run",
4069
+ context: ({ input }) => input,
4070
+ initial: "processingEpic",
4071
+ states: {
4072
+ processingEpic: {
4073
+ invoke: {
4074
+ src: "runEpicActor",
4075
+ input: ({ context }) => context,
4076
+ onDone: {
4077
+ target: "checkNext",
4078
+ actions: assign(({ event }) => event.output)
4079
+ },
4080
+ onError: {
4081
+ target: "allDone",
4082
+ actions: assign(({ context, event }) => {
4083
+ const msg = event.error instanceof Error ? event.error.message : String(event.error);
4084
+ return { ...context, errors: [...context.errors, { taskName: "__run_actor__", storyKey: "__run__", code: "ACTOR_ERROR", message: msg }], halted: true };
4085
+ })
4086
+ }
4087
+ }
4088
+ },
4089
+ checkNext: {
4090
+ always: [
4091
+ { guard: "allEpicsDone", target: "allDone" },
4092
+ { target: "processingEpic" }
4093
+ ]
4094
+ },
4095
+ allDone: { type: "final" }
4096
+ }
4097
+ });
4098
+ async function runWorkflowActor(config) {
4099
+ const startMs = Date.now();
4100
+ const projectDir = config.projectDir ?? process.cwd();
3924
4101
  let state = readWorkflowState(projectDir);
3925
4102
  if (state.phase === "completed") {
3926
- return {
3927
- success: true,
3928
- tasksCompleted: 0,
3929
- storiesProcessed: 0,
3930
- errors: [],
3931
- durationMs: 0
3932
- };
4103
+ return { success: true, tasksCompleted: 0, storiesProcessed: 0, errors: [], durationMs: 0 };
3933
4104
  }
3934
4105
  if (state.phase === "error" || state.phase === "failed") {
3935
4106
  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`);
4107
+ if (!config.onEvent) info(`Resuming from ${state.phase} state \u2014 ${errorCount} previous error(s)`);
3937
4108
  }
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
- };
4109
+ state = { ...state, phase: "executing", started: state.started || (/* @__PURE__ */ new Date()).toISOString(), workflow_name: config.workflow.storyFlow.filter((s) => typeof s === "string").join(" -> ") };
3944
4110
  writeWorkflowState(state, projectDir);
3945
4111
  try {
3946
4112
  await checkDriverHealth(config.workflow);
3947
4113
  } catch (err) {
3948
4114
  const message = err instanceof Error ? err.message : String(err);
3949
4115
  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);
4116
+ const errors2 = [{ taskName: "__health_check__", storyKey: "__health_check__", code: "HEALTH_CHECK", message }];
3957
4117
  writeWorkflowState(state, projectDir);
3958
- return {
3959
- success: false,
3960
- tasksCompleted: 0,
3961
- storiesProcessed: 0,
3962
- errors,
3963
- durationMs: Date.now() - startMs
3964
- };
4118
+ return { success: false, tasksCompleted: 0, storiesProcessed: 0, errors: errors2, durationMs: Date.now() - startMs };
3965
4119
  }
3966
4120
  const capWarnings = checkCapabilityConflicts(config.workflow);
3967
- for (const cw of capWarnings) {
3968
- warn(cw.message);
3969
- }
4121
+ for (const cw of capWarnings) warn(cw.message);
3970
4122
  const workItems = loadWorkItems(config.sprintStatusPath, config.issuesPath);
3971
4123
  const storyFlowTasks = /* @__PURE__ */ new Set();
3972
4124
  for (const step of config.workflow.storyFlow) {
3973
4125
  if (typeof step === "string") storyFlowTasks.add(step);
3974
4126
  if (typeof step === "object" && "loop" in step) {
3975
- for (const loopTask of step.loop) {
3976
- storyFlowTasks.add(loopTask);
3977
- }
4127
+ for (const lt of step.loop) storyFlowTasks.add(lt);
3978
4128
  }
3979
4129
  }
3980
4130
  const epicGroups = /* @__PURE__ */ new Map();
@@ -3983,196 +4133,28 @@ async function executeWorkflow(config) {
3983
4133
  if (!epicGroups.has(epicId)) epicGroups.set(epicId, []);
3984
4134
  epicGroups.get(epicId).push(item);
3985
4135
  }
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
- }
4136
+ const runInput = {
4137
+ config,
4138
+ storyFlowTasks,
4139
+ epicEntries: [...epicGroups.entries()],
4140
+ currentEpicIndex: 0,
4141
+ workflowState: state,
4142
+ errors: [],
4143
+ tasksCompleted: 0,
4144
+ storiesProcessed: /* @__PURE__ */ new Set(),
4145
+ lastContract: null,
4146
+ accumulatedCostUsd: 0,
4147
+ halted: false
4148
+ };
4149
+ const finalContext = await new Promise((resolve7) => {
4150
+ const actor = createActor(runMachine, { input: runInput });
4151
+ actor.subscribe({ complete: () => resolve7(actor.getSnapshot().context) });
4152
+ actor.start();
4153
+ });
4154
+ state = finalContext.workflowState;
4155
+ const errors = finalContext.errors;
4156
+ const tasksCompleted = finalContext.tasksCompleted;
4157
+ const storiesProcessed = finalContext.storiesProcessed;
4176
4158
  if (state.phase === "interrupted") {
4177
4159
  } else if (errors.length === 0 && state.phase !== "max-iterations" && state.phase !== "circuit-breaker") {
4178
4160
  state = { ...state, phase: "completed" };
@@ -4182,7 +4164,7 @@ async function executeWorkflow(config) {
4182
4164
  return {
4183
4165
  success: errors.length === 0 && !loopTerminated && state.phase !== "interrupted",
4184
4166
  tasksCompleted,
4185
- storiesProcessed: processedStories.size,
4167
+ storiesProcessed: storiesProcessed.size,
4186
4168
  errors,
4187
4169
  durationMs: Date.now() - startMs
4188
4170
  };
@@ -4196,11 +4178,7 @@ function recordErrorInState(state, taskName, storyKey, error) {
4196
4178
  error_message: error.message,
4197
4179
  error_code: error.code
4198
4180
  };
4199
- return {
4200
- ...state,
4201
- phase: "error",
4202
- tasks_completed: [...state.tasks_completed, errorCheckpoint]
4203
- };
4181
+ return { ...state, phase: "error", tasks_completed: [...state.tasks_completed, errorCheckpoint] };
4204
4182
  }
4205
4183
  function isEngineError(err) {
4206
4184
  if (!err || typeof err !== "object") return false;
@@ -4208,21 +4186,9 @@ function isEngineError(err) {
4208
4186
  return typeof e.taskName === "string" && typeof e.storyKey === "string" && typeof e.code === "string" && typeof e.message === "string";
4209
4187
  }
4210
4188
  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
- }
4189
+ if (err instanceof DispatchError) return { taskName, storyKey, code: err.code, message: err.message };
4219
4190
  const message = err instanceof Error ? err.message : String(err);
4220
- return {
4221
- taskName,
4222
- storyKey,
4223
- code: "UNKNOWN",
4224
- message
4225
- };
4191
+ return { taskName, storyKey, code: "UNKNOWN", message };
4226
4192
  }
4227
4193
 
4228
4194
  // src/lib/worktree-manager.ts
@@ -6417,16 +6383,6 @@ function registerRunCommand(program) {
6417
6383
  key: event.storyKey.replace("__epic_", "Epic ").replace("__", ""),
6418
6384
  message: `verification complete (cost: $${(event.costUsd ?? 0).toFixed(2)})`
6419
6385
  });
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
6386
  }
6431
6387
  }
6432
6388
  if (event.type === "dispatch-error") {
@@ -6447,6 +6403,19 @@ function registerRunCommand(program) {
6447
6403
  renderer.updateStories([...storyEntries]);
6448
6404
  }
6449
6405
  }
6406
+ if (event.type === "story-done") {
6407
+ storiesDone++;
6408
+ updateStoryStatus2(event.storyKey, "done");
6409
+ const idx = storyEntries.findIndex((s) => s.key === event.storyKey);
6410
+ if (idx >= 0) {
6411
+ storyEntries[idx] = { ...storyEntries[idx], status: "done" };
6412
+ renderer.updateStories([...storyEntries]);
6413
+ }
6414
+ const epicId = extractEpicId2(event.storyKey);
6415
+ if (epicData[epicId]) {
6416
+ epicData[epicId].storiesDone = (epicData[epicId].storiesDone ?? 0) + 1;
6417
+ }
6418
+ }
6450
6419
  };
6451
6420
  const config = {
6452
6421
  workflow: parsedWorkflow,
@@ -6498,7 +6467,7 @@ function registerRunCommand(program) {
6498
6467
  ...config,
6499
6468
  projectDir: worktreePath
6500
6469
  };
6501
- return executeWorkflow(epicConfig);
6470
+ return runWorkflowActor(epicConfig);
6502
6471
  };
6503
6472
  const poolResult = await pool.startPool(epics, executeFn);
6504
6473
  const remainingWorktrees = worktreeManager.listWorktrees();
@@ -6534,7 +6503,7 @@ function registerRunCommand(program) {
6534
6503
  }
6535
6504
  } else {
6536
6505
  try {
6537
- const result = await executeWorkflow(config);
6506
+ const result = await runWorkflowActor(config);
6538
6507
  clearInterval(headerRefresh);
6539
6508
  process.removeListener("SIGINT", onInterrupt);
6540
6509
  process.removeListener("SIGTERM", onInterrupt);
@@ -11227,7 +11196,7 @@ function registerTeardownCommand(program) {
11227
11196
  } else if (otlpMode === "remote-routed") {
11228
11197
  if (!options.keepDocker) {
11229
11198
  try {
11230
- const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-MXONF2RK.js");
11199
+ const { stopCollectorOnly: stopCollectorOnly2 } = await import("./docker-SV6TB753.js");
11231
11200
  stopCollectorOnly2();
11232
11201
  result.docker.stopped = true;
11233
11202
  if (!isJson) {
@@ -11259,7 +11228,7 @@ function registerTeardownCommand(program) {
11259
11228
  info("Shared stack: kept running (other projects may use it)");
11260
11229
  }
11261
11230
  } else if (isLegacyStack) {
11262
- const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-MXONF2RK.js");
11231
+ const { isStackRunning: isStackRunning2, stopStack } = await import("./docker-SV6TB753.js");
11263
11232
  let stackRunning = false;
11264
11233
  try {
11265
11234
  stackRunning = isStackRunning2(composeFile);
@@ -14246,7 +14215,7 @@ function registerDriversCommand(program) {
14246
14215
  }
14247
14216
 
14248
14217
  // src/index.ts
14249
- var VERSION = true ? "0.35.7" : "0.0.0-dev";
14218
+ var VERSION = true ? "0.36.2" : "0.0.0-dev";
14250
14219
  function createProgram() {
14251
14220
  const program = new Command();
14252
14221
  program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");