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