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-
|
|
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-
|
|
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
|
|
2673
|
-
import { join as
|
|
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 =
|
|
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
|
-
|
|
2698
|
-
|
|
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
|
|
2777
|
-
import { join as
|
|
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 (!
|
|
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 (
|
|
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 =
|
|
2805
|
-
const verdictDir =
|
|
2806
|
-
if (
|
|
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
|
-
|
|
2810
|
-
|
|
2709
|
+
mkdirSync3(storyFilesDir, { recursive: true });
|
|
2710
|
+
mkdirSync3(verdictDir, { recursive: true });
|
|
2811
2711
|
for (const filePath of options.storyFiles) {
|
|
2812
|
-
if (!
|
|
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 =
|
|
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 (
|
|
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
|
|
3110
|
-
import { join as
|
|
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 =
|
|
3144
|
-
|
|
3145
|
-
appendFileSync(
|
|
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-
|
|
3168
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
3281
|
-
"
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
"
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
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
|
-
|
|
3294
|
-
const
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
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
|
|
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,
|
|
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,
|
|
3332
|
+
const sessionId = resolveSessionId(task.session, sessionKey, workflowState);
|
|
3371
3333
|
const driverName = task.driver ?? "claude-code";
|
|
3372
3334
|
const driver = getDriver(driverName);
|
|
3373
|
-
const
|
|
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 ===
|
|
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,
|
|
3409
|
-
const coverageDedup = buildCoverageDeduplicationContext(
|
|
3410
|
-
|
|
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
|
-
|
|
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 (
|
|
3445
|
-
|
|
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
|
|
3452
|
-
activeToolName =
|
|
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
|
|
3474
|
-
resultSessionId =
|
|
3475
|
-
cost =
|
|
3476
|
-
if (
|
|
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 =
|
|
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
|
|
3545
|
-
warn(`workflow-
|
|
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
|
-
|
|
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
|
|
3574
|
-
const
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
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
|
|
3596
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3621
|
-
|
|
3622
|
-
const
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
const
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
const
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
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,
|
|
3568
|
+
const engineError = isEngineError(err) ? err : handleDispatchError(err, taskName, item.key);
|
|
3849
3569
|
errors.push(engineError);
|
|
3850
|
-
currentState = recordErrorInState(currentState, taskName,
|
|
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
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
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
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3919
|
-
const
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
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:
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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.
|
|
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");
|