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