codeharness 0.22.0 → 0.22.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1018 -124
- package/dist/modules/observability/index.d.ts +59 -1
- package/dist/modules/observability/index.js +166 -1
- package/package.json +1 -1
- package/ralph/ralph.sh +6 -1
package/dist/index.js
CHANGED
|
@@ -1941,7 +1941,7 @@ async function scaffoldDocs(opts) {
|
|
|
1941
1941
|
}
|
|
1942
1942
|
|
|
1943
1943
|
// src/modules/infra/init-project.ts
|
|
1944
|
-
var HARNESS_VERSION = true ? "0.22.
|
|
1944
|
+
var HARNESS_VERSION = true ? "0.22.2" : "0.0.0-dev";
|
|
1945
1945
|
function failResult(opts, error) {
|
|
1946
1946
|
return {
|
|
1947
1947
|
status: "fail",
|
|
@@ -2808,6 +2808,7 @@ function Header({ info: info2 }) {
|
|
|
2808
2808
|
info2.storyKey,
|
|
2809
2809
|
" \u2014 ",
|
|
2810
2810
|
info2.phase,
|
|
2811
|
+
info2.elapsed ? ` | ${info2.elapsed}` : "",
|
|
2811
2812
|
" | Sprint: ",
|
|
2812
2813
|
info2.done,
|
|
2813
2814
|
"/",
|
|
@@ -2858,6 +2859,60 @@ function truncateToWidth(text, maxWidth) {
|
|
|
2858
2859
|
}
|
|
2859
2860
|
return result;
|
|
2860
2861
|
}
|
|
2862
|
+
var STATUS_SYMBOLS = {
|
|
2863
|
+
"done": "\u2713",
|
|
2864
|
+
"in-progress": "\u25C6",
|
|
2865
|
+
"pending": "\u25CB",
|
|
2866
|
+
"failed": "\u2717",
|
|
2867
|
+
"blocked": "\u2715"
|
|
2868
|
+
};
|
|
2869
|
+
function StoryBreakdown({ stories }) {
|
|
2870
|
+
if (stories.length === 0) return null;
|
|
2871
|
+
const groups = {};
|
|
2872
|
+
for (const s of stories) {
|
|
2873
|
+
if (!groups[s.status]) groups[s.status] = [];
|
|
2874
|
+
groups[s.status].push(s.key);
|
|
2875
|
+
}
|
|
2876
|
+
const fmt = (keys, status) => keys.map((k) => `${k} ${STATUS_SYMBOLS[status]}`).join(" ");
|
|
2877
|
+
const shortKey = (key) => {
|
|
2878
|
+
const m = key.match(/^(\d+-\d+)/);
|
|
2879
|
+
return m ? m[1] : key;
|
|
2880
|
+
};
|
|
2881
|
+
const fmtShort = (keys, status) => keys.map((k) => `${shortKey(k)} ${STATUS_SYMBOLS[status]}`).join(" ");
|
|
2882
|
+
const parts = [];
|
|
2883
|
+
if (groups["done"]?.length) {
|
|
2884
|
+
parts.push(`Done: ${groups["done"].length} \u2713`);
|
|
2885
|
+
}
|
|
2886
|
+
if (groups["in-progress"]?.length) {
|
|
2887
|
+
parts.push(`This: ${fmt(groups["in-progress"], "in-progress")}`);
|
|
2888
|
+
}
|
|
2889
|
+
if (groups["pending"]?.length) {
|
|
2890
|
+
const shown = groups["pending"].slice(0, 3);
|
|
2891
|
+
const rest = groups["pending"].length - shown.length;
|
|
2892
|
+
let s = `Next: ${fmtShort(shown, "pending")}`;
|
|
2893
|
+
if (rest > 0) s += ` +${rest}`;
|
|
2894
|
+
parts.push(s);
|
|
2895
|
+
}
|
|
2896
|
+
if (groups["failed"]?.length) {
|
|
2897
|
+
parts.push(`Failed: ${fmtShort(groups["failed"], "failed")}`);
|
|
2898
|
+
}
|
|
2899
|
+
if (groups["blocked"]?.length) {
|
|
2900
|
+
parts.push(`Blocked: ${fmtShort(groups["blocked"], "blocked")}`);
|
|
2901
|
+
}
|
|
2902
|
+
return /* @__PURE__ */ jsx(Text, { children: parts.join(" | ") });
|
|
2903
|
+
}
|
|
2904
|
+
var MESSAGE_PREFIX = {
|
|
2905
|
+
ok: "[OK]",
|
|
2906
|
+
warn: "[WARN]",
|
|
2907
|
+
fail: "[FAIL]"
|
|
2908
|
+
};
|
|
2909
|
+
function StoryMessages({ messages }) {
|
|
2910
|
+
if (messages.length === 0) return null;
|
|
2911
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: messages.map((msg, i) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2912
|
+
/* @__PURE__ */ jsx(Text, { children: `${MESSAGE_PREFIX[msg.type]} Story ${msg.key}: ${msg.message}` }),
|
|
2913
|
+
msg.details?.map((d, j) => /* @__PURE__ */ jsx(Text, { children: ` \u2514 ${d}` }, j))
|
|
2914
|
+
] }, i)) });
|
|
2915
|
+
}
|
|
2861
2916
|
function RetryNotice({ info: info2 }) {
|
|
2862
2917
|
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
2863
2918
|
"\u23F3 API retry ",
|
|
@@ -2872,6 +2927,8 @@ function App({
|
|
|
2872
2927
|
}) {
|
|
2873
2928
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2874
2929
|
/* @__PURE__ */ jsx(Header, { info: state.sprintInfo }),
|
|
2930
|
+
/* @__PURE__ */ jsx(StoryBreakdown, { stories: state.stories }),
|
|
2931
|
+
/* @__PURE__ */ jsx(StoryMessages, { messages: state.messages }),
|
|
2875
2932
|
/* @__PURE__ */ jsx(CompletedTools, { tools: state.completedTools }),
|
|
2876
2933
|
state.activeTool && /* @__PURE__ */ jsx(ActiveTool, { name: state.activeTool.name }),
|
|
2877
2934
|
state.lastThought && /* @__PURE__ */ jsx(LastThought, { text: state.lastThought }),
|
|
@@ -2886,6 +2943,10 @@ var noopHandle = {
|
|
|
2886
2943
|
},
|
|
2887
2944
|
updateSprintState() {
|
|
2888
2945
|
},
|
|
2946
|
+
updateStories() {
|
|
2947
|
+
},
|
|
2948
|
+
addMessage() {
|
|
2949
|
+
},
|
|
2889
2950
|
cleanup() {
|
|
2890
2951
|
}
|
|
2891
2952
|
};
|
|
@@ -2896,6 +2957,8 @@ function startRenderer(options) {
|
|
|
2896
2957
|
}
|
|
2897
2958
|
let state = {
|
|
2898
2959
|
sprintInfo: options?.sprintState ?? null,
|
|
2960
|
+
stories: [],
|
|
2961
|
+
messages: [],
|
|
2899
2962
|
completedTools: [],
|
|
2900
2963
|
activeTool: null,
|
|
2901
2964
|
activeToolArgs: "",
|
|
@@ -2978,7 +3041,17 @@ function startRenderer(options) {
|
|
|
2978
3041
|
state.sprintInfo = sprintState ?? null;
|
|
2979
3042
|
rerender();
|
|
2980
3043
|
}
|
|
2981
|
-
|
|
3044
|
+
function updateStories(stories) {
|
|
3045
|
+
if (cleaned) return;
|
|
3046
|
+
state.stories = [...stories];
|
|
3047
|
+
rerender();
|
|
3048
|
+
}
|
|
3049
|
+
function addMessage(msg) {
|
|
3050
|
+
if (cleaned) return;
|
|
3051
|
+
state.messages = [...state.messages, msg];
|
|
3052
|
+
rerender();
|
|
3053
|
+
}
|
|
3054
|
+
return { update, updateSprintState, updateStories, addMessage, cleanup };
|
|
2982
3055
|
}
|
|
2983
3056
|
|
|
2984
3057
|
// src/modules/sprint/state.ts
|
|
@@ -3864,21 +3937,8 @@ function clearRunProgress2() {
|
|
|
3864
3937
|
return clearRunProgress();
|
|
3865
3938
|
}
|
|
3866
3939
|
|
|
3867
|
-
// src/
|
|
3868
|
-
var SPRINT_STATUS_REL = "_bmad-output/implementation-artifacts/sprint-status.yaml";
|
|
3940
|
+
// src/lib/run-helpers.ts
|
|
3869
3941
|
var STORY_KEY_PATTERN = /^\d+-\d+-/;
|
|
3870
|
-
function resolveRalphPath() {
|
|
3871
|
-
const currentFile = fileURLToPath2(import.meta.url);
|
|
3872
|
-
const currentDir = dirname4(currentFile);
|
|
3873
|
-
let root = dirname4(currentDir);
|
|
3874
|
-
if (root.endsWith("/src") || root.endsWith("\\src")) {
|
|
3875
|
-
root = dirname4(root);
|
|
3876
|
-
}
|
|
3877
|
-
return join13(root, "ralph", "ralph.sh");
|
|
3878
|
-
}
|
|
3879
|
-
function resolvePluginDir() {
|
|
3880
|
-
return join13(process.cwd(), ".claude");
|
|
3881
|
-
}
|
|
3882
3942
|
function countStories(statuses) {
|
|
3883
3943
|
let total = 0;
|
|
3884
3944
|
let ready = 0;
|
|
@@ -3919,6 +3979,108 @@ function buildSpawnArgs(opts) {
|
|
|
3919
3979
|
}
|
|
3920
3980
|
return args;
|
|
3921
3981
|
}
|
|
3982
|
+
function formatElapsed(ms) {
|
|
3983
|
+
const totalMinutes = Math.max(0, Math.floor(ms / 6e4));
|
|
3984
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
3985
|
+
const minutes = totalMinutes % 60;
|
|
3986
|
+
if (hours > 0) {
|
|
3987
|
+
return `${hours}h${minutes}m`;
|
|
3988
|
+
}
|
|
3989
|
+
return `${totalMinutes}m`;
|
|
3990
|
+
}
|
|
3991
|
+
function mapSprintStatus(status) {
|
|
3992
|
+
switch (status) {
|
|
3993
|
+
case "done":
|
|
3994
|
+
return "done";
|
|
3995
|
+
case "in-progress":
|
|
3996
|
+
case "review":
|
|
3997
|
+
case "verifying":
|
|
3998
|
+
return "in-progress";
|
|
3999
|
+
case "backlog":
|
|
4000
|
+
case "ready-for-dev":
|
|
4001
|
+
return "pending";
|
|
4002
|
+
case "failed":
|
|
4003
|
+
return "failed";
|
|
4004
|
+
case "blocked":
|
|
4005
|
+
case "exhausted":
|
|
4006
|
+
return "blocked";
|
|
4007
|
+
default:
|
|
4008
|
+
return "pending";
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
function mapSprintStatuses(statuses) {
|
|
4012
|
+
const entries = [];
|
|
4013
|
+
for (const [key, status] of Object.entries(statuses)) {
|
|
4014
|
+
if (!STORY_KEY_PATTERN.test(key)) continue;
|
|
4015
|
+
if (status === "optional") continue;
|
|
4016
|
+
entries.push({ key, status: mapSprintStatus(status) });
|
|
4017
|
+
}
|
|
4018
|
+
return entries;
|
|
4019
|
+
}
|
|
4020
|
+
var ANSI_ESCAPE = /\x1b\[[0-9;]*m/g;
|
|
4021
|
+
var TIMESTAMP_PREFIX = /^\[[\d-]+\s[\d:]+\]\s*/;
|
|
4022
|
+
var SUCCESS_STORY = /\[SUCCESS\]\s+Story\s+([\w-]+):\s+DONE(.*)/;
|
|
4023
|
+
var WARN_STORY_RETRY = /\[WARN\]\s+Story\s+([\w-]+)\s+exceeded retry limit/;
|
|
4024
|
+
var WARN_STORY_RETRYING = /\[WARN\]\s+Story\s+([\w-]+)\s+.*retry\s+(\d+)\/(\d+)/;
|
|
4025
|
+
var ERROR_LINE = /\[ERROR\]\s+(.+)/;
|
|
4026
|
+
function parseRalphMessage(rawLine) {
|
|
4027
|
+
const clean = rawLine.replace(ANSI_ESCAPE, "").replace(TIMESTAMP_PREFIX, "").trim();
|
|
4028
|
+
if (clean.length === 0) return null;
|
|
4029
|
+
const success = SUCCESS_STORY.exec(clean);
|
|
4030
|
+
if (success) {
|
|
4031
|
+
const key = success[1];
|
|
4032
|
+
const rest = success[2].trim().replace(/^—\s*/, "");
|
|
4033
|
+
return {
|
|
4034
|
+
type: "ok",
|
|
4035
|
+
key,
|
|
4036
|
+
message: rest ? `DONE \u2014 ${rest}` : "DONE"
|
|
4037
|
+
};
|
|
4038
|
+
}
|
|
4039
|
+
const retryExceeded = WARN_STORY_RETRY.exec(clean);
|
|
4040
|
+
if (retryExceeded) {
|
|
4041
|
+
return {
|
|
4042
|
+
type: "fail",
|
|
4043
|
+
key: retryExceeded[1],
|
|
4044
|
+
message: "exceeded retry limit"
|
|
4045
|
+
};
|
|
4046
|
+
}
|
|
4047
|
+
const retrying = WARN_STORY_RETRYING.exec(clean);
|
|
4048
|
+
if (retrying) {
|
|
4049
|
+
return {
|
|
4050
|
+
type: "warn",
|
|
4051
|
+
key: retrying[1],
|
|
4052
|
+
message: `retry ${retrying[2]}/${retrying[3]}`
|
|
4053
|
+
};
|
|
4054
|
+
}
|
|
4055
|
+
const errorMatch = ERROR_LINE.exec(clean);
|
|
4056
|
+
if (errorMatch) {
|
|
4057
|
+
const keyMatch = errorMatch[1].match(/Story\s+([\w-]+)/);
|
|
4058
|
+
if (keyMatch) {
|
|
4059
|
+
return {
|
|
4060
|
+
type: "fail",
|
|
4061
|
+
key: keyMatch[1],
|
|
4062
|
+
message: errorMatch[1].trim()
|
|
4063
|
+
};
|
|
4064
|
+
}
|
|
4065
|
+
return null;
|
|
4066
|
+
}
|
|
4067
|
+
return null;
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
// src/commands/run.ts
|
|
4071
|
+
var SPRINT_STATUS_REL = "_bmad-output/implementation-artifacts/sprint-status.yaml";
|
|
4072
|
+
function resolveRalphPath() {
|
|
4073
|
+
const currentFile = fileURLToPath2(import.meta.url);
|
|
4074
|
+
const currentDir = dirname4(currentFile);
|
|
4075
|
+
let root = dirname4(currentDir);
|
|
4076
|
+
if (root.endsWith("/src") || root.endsWith("\\src")) {
|
|
4077
|
+
root = dirname4(root);
|
|
4078
|
+
}
|
|
4079
|
+
return join13(root, "ralph", "ralph.sh");
|
|
4080
|
+
}
|
|
4081
|
+
function resolvePluginDir() {
|
|
4082
|
+
return join13(process.cwd(), ".claude");
|
|
4083
|
+
}
|
|
3922
4084
|
function registerRunCommand(program) {
|
|
3923
4085
|
program.command("run").description("Execute the autonomous coding loop").option("--max-iterations <n>", "Maximum loop iterations", "50").option("--timeout <seconds>", "Total loop timeout in seconds", "43200").option("--iteration-timeout <minutes>", "Per-iteration timeout in minutes", "30").option("--quiet", "Suppress terminal output (background mode)", false).option("--calls <n>", "Max API calls per hour", "100").option("--max-story-retries <n>", "Max retries per story before flagging", "10").option("--reset", "Clear retry counters, flagged stories, and circuit breaker before starting", false).action(async (options, cmd) => {
|
|
3924
4086
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -3999,6 +4161,7 @@ function registerRunCommand(program) {
|
|
|
3999
4161
|
const quiet = options.quiet;
|
|
4000
4162
|
const rendererHandle = startRenderer({ quiet });
|
|
4001
4163
|
let sprintStateInterval = null;
|
|
4164
|
+
const sessionStartTime = Date.now();
|
|
4002
4165
|
try {
|
|
4003
4166
|
const initialState = getSprintState2();
|
|
4004
4167
|
if (initialState.success) {
|
|
@@ -4007,17 +4170,23 @@ function registerRunCommand(program) {
|
|
|
4007
4170
|
storyKey: s.run.currentStory ?? "",
|
|
4008
4171
|
phase: s.run.currentPhase ?? "",
|
|
4009
4172
|
done: s.sprint.done,
|
|
4010
|
-
total: s.sprint.total
|
|
4173
|
+
total: s.sprint.total,
|
|
4174
|
+
elapsed: formatElapsed(Date.now() - sessionStartTime)
|
|
4011
4175
|
};
|
|
4012
4176
|
rendererHandle.updateSprintState(sprintInfo);
|
|
4013
4177
|
}
|
|
4178
|
+
const initialStatuses = readSprintStatus(projectDir);
|
|
4179
|
+
const initialStories = mapSprintStatuses(initialStatuses);
|
|
4180
|
+
if (initialStories.length > 0) {
|
|
4181
|
+
rendererHandle.updateStories(initialStories);
|
|
4182
|
+
}
|
|
4014
4183
|
const child = spawn("bash", args, {
|
|
4015
4184
|
stdio: quiet ? "ignore" : ["inherit", "pipe", "pipe"],
|
|
4016
4185
|
cwd: projectDir,
|
|
4017
4186
|
env
|
|
4018
4187
|
});
|
|
4019
4188
|
if (!quiet && child.stdout && child.stderr) {
|
|
4020
|
-
const makeLineHandler = () => {
|
|
4189
|
+
const makeLineHandler = (opts) => {
|
|
4021
4190
|
let partial = "";
|
|
4022
4191
|
const decoder = new StringDecoder("utf8");
|
|
4023
4192
|
return (data) => {
|
|
@@ -4030,11 +4199,17 @@ function registerRunCommand(program) {
|
|
|
4030
4199
|
if (event) {
|
|
4031
4200
|
rendererHandle.update(event);
|
|
4032
4201
|
}
|
|
4202
|
+
if (opts?.parseRalph) {
|
|
4203
|
+
const msg = parseRalphMessage(line);
|
|
4204
|
+
if (msg) {
|
|
4205
|
+
rendererHandle.addMessage(msg);
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4033
4208
|
}
|
|
4034
4209
|
};
|
|
4035
4210
|
};
|
|
4036
4211
|
child.stdout.on("data", makeLineHandler());
|
|
4037
|
-
child.stderr.on("data", makeLineHandler());
|
|
4212
|
+
child.stderr.on("data", makeLineHandler({ parseRalph: true }));
|
|
4038
4213
|
sprintStateInterval = setInterval(() => {
|
|
4039
4214
|
try {
|
|
4040
4215
|
const stateResult = getSprintState2();
|
|
@@ -4044,10 +4219,14 @@ function registerRunCommand(program) {
|
|
|
4044
4219
|
storyKey: s.run.currentStory ?? "",
|
|
4045
4220
|
phase: s.run.currentPhase ?? "",
|
|
4046
4221
|
done: s.sprint.done,
|
|
4047
|
-
total: s.sprint.total
|
|
4222
|
+
total: s.sprint.total,
|
|
4223
|
+
elapsed: formatElapsed(Date.now() - sessionStartTime)
|
|
4048
4224
|
};
|
|
4049
4225
|
rendererHandle.updateSprintState(sprintInfo);
|
|
4050
4226
|
}
|
|
4227
|
+
const currentStatuses = readSprintStatus(projectDir);
|
|
4228
|
+
const storyEntries = mapSprintStatuses(currentStatuses);
|
|
4229
|
+
rendererHandle.updateStories(storyEntries);
|
|
4051
4230
|
} catch {
|
|
4052
4231
|
}
|
|
4053
4232
|
}, 5e3);
|
|
@@ -4121,7 +4300,7 @@ function registerRunCommand(program) {
|
|
|
4121
4300
|
|
|
4122
4301
|
// src/commands/verify.ts
|
|
4123
4302
|
import { existsSync as existsSync25, readFileSync as readFileSync23 } from "fs";
|
|
4124
|
-
import { join as
|
|
4303
|
+
import { join as join22 } from "path";
|
|
4125
4304
|
|
|
4126
4305
|
// src/modules/verify/index.ts
|
|
4127
4306
|
import { readFileSync as readFileSync22 } from "fs";
|
|
@@ -5174,6 +5353,121 @@ function parseObservabilityGaps(proofContent) {
|
|
|
5174
5353
|
// src/modules/observability/analyzer.ts
|
|
5175
5354
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
5176
5355
|
import { join as join16 } from "path";
|
|
5356
|
+
var DEFAULT_RULES_DIR = "patches/observability/";
|
|
5357
|
+
var DEFAULT_TIMEOUT = 6e4;
|
|
5358
|
+
var FUNCTION_NO_LOG_RULE = "function-no-debug-log";
|
|
5359
|
+
var CATCH_WITHOUT_LOGGING_RULE = "catch-without-logging";
|
|
5360
|
+
var ERROR_PATH_NO_LOG_RULE = "error-path-no-log";
|
|
5361
|
+
function matchesRule(gapType, ruleName) {
|
|
5362
|
+
return gapType === ruleName || gapType.endsWith(`.${ruleName}`);
|
|
5363
|
+
}
|
|
5364
|
+
function analyze(projectDir, config) {
|
|
5365
|
+
if (!projectDir || typeof projectDir !== "string") {
|
|
5366
|
+
return fail2("projectDir is required and must be a non-empty string");
|
|
5367
|
+
}
|
|
5368
|
+
const tool = config?.tool ?? "semgrep";
|
|
5369
|
+
if (tool !== "semgrep") {
|
|
5370
|
+
return fail2(`Unsupported analyzer tool: ${tool}`);
|
|
5371
|
+
}
|
|
5372
|
+
if (!checkSemgrepInstalled()) {
|
|
5373
|
+
return ok2({
|
|
5374
|
+
tool: "semgrep",
|
|
5375
|
+
gaps: [],
|
|
5376
|
+
summary: {
|
|
5377
|
+
totalFunctions: 0,
|
|
5378
|
+
functionsWithLogs: 0,
|
|
5379
|
+
errorHandlersWithoutLogs: 0,
|
|
5380
|
+
coveragePercent: 0,
|
|
5381
|
+
levelDistribution: {}
|
|
5382
|
+
},
|
|
5383
|
+
skipped: true,
|
|
5384
|
+
skipReason: "static analysis skipped -- install semgrep"
|
|
5385
|
+
});
|
|
5386
|
+
}
|
|
5387
|
+
const rulesDir = config?.rulesDir ?? DEFAULT_RULES_DIR;
|
|
5388
|
+
const timeout = config?.timeout ?? DEFAULT_TIMEOUT;
|
|
5389
|
+
const fullRulesDir = join16(projectDir, rulesDir);
|
|
5390
|
+
const rawResult = runSemgrep(projectDir, fullRulesDir, timeout);
|
|
5391
|
+
if (!rawResult.success) {
|
|
5392
|
+
return fail2(rawResult.error);
|
|
5393
|
+
}
|
|
5394
|
+
const gaps = parseSemgrepOutput(rawResult.data);
|
|
5395
|
+
const summaryOpts = config?.totalFunctions != null ? { totalFunctions: config.totalFunctions } : void 0;
|
|
5396
|
+
const summary = computeSummary(gaps, summaryOpts);
|
|
5397
|
+
return ok2({
|
|
5398
|
+
tool: "semgrep",
|
|
5399
|
+
gaps,
|
|
5400
|
+
summary
|
|
5401
|
+
});
|
|
5402
|
+
}
|
|
5403
|
+
function checkSemgrepInstalled() {
|
|
5404
|
+
try {
|
|
5405
|
+
execFileSync8("semgrep", ["--version"], {
|
|
5406
|
+
encoding: "utf-8",
|
|
5407
|
+
timeout: 5e3,
|
|
5408
|
+
stdio: "pipe"
|
|
5409
|
+
});
|
|
5410
|
+
return true;
|
|
5411
|
+
} catch {
|
|
5412
|
+
return false;
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
function runSemgrep(projectDir, rulesDir, timeout = DEFAULT_TIMEOUT) {
|
|
5416
|
+
try {
|
|
5417
|
+
const stdout = execFileSync8(
|
|
5418
|
+
"semgrep",
|
|
5419
|
+
["scan", "--config", rulesDir, "--json", projectDir],
|
|
5420
|
+
{ encoding: "utf-8", timeout, stdio: ["pipe", "pipe", "pipe"] }
|
|
5421
|
+
);
|
|
5422
|
+
const parsed = JSON.parse(stdout);
|
|
5423
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.results)) {
|
|
5424
|
+
return fail2("Semgrep scan returned invalid JSON: missing results array");
|
|
5425
|
+
}
|
|
5426
|
+
return ok2(parsed);
|
|
5427
|
+
} catch (error) {
|
|
5428
|
+
return fail2(`Semgrep scan failed: ${String(error)}`);
|
|
5429
|
+
}
|
|
5430
|
+
}
|
|
5431
|
+
function parseSemgrepOutput(raw) {
|
|
5432
|
+
if (!raw.results || !Array.isArray(raw.results)) {
|
|
5433
|
+
return [];
|
|
5434
|
+
}
|
|
5435
|
+
return raw.results.map((r) => ({
|
|
5436
|
+
file: r.path,
|
|
5437
|
+
line: r.start.line,
|
|
5438
|
+
type: r.check_id,
|
|
5439
|
+
description: r.extra.message,
|
|
5440
|
+
severity: normalizeSeverity(r.extra.severity)
|
|
5441
|
+
}));
|
|
5442
|
+
}
|
|
5443
|
+
function computeSummary(gaps, opts) {
|
|
5444
|
+
const functionsWithoutLogs = gaps.filter(
|
|
5445
|
+
(g) => matchesRule(g.type, FUNCTION_NO_LOG_RULE)
|
|
5446
|
+
).length;
|
|
5447
|
+
const errorHandlersWithoutLogs = gaps.filter(
|
|
5448
|
+
(g) => matchesRule(g.type, CATCH_WITHOUT_LOGGING_RULE) || matchesRule(g.type, ERROR_PATH_NO_LOG_RULE)
|
|
5449
|
+
).length;
|
|
5450
|
+
const totalFunctions = opts?.totalFunctions ?? functionsWithoutLogs;
|
|
5451
|
+
const functionsWithLogs = totalFunctions - functionsWithoutLogs;
|
|
5452
|
+
const coveragePercent = totalFunctions === 0 ? 100 : Math.round(functionsWithLogs / totalFunctions * 100 * 100) / 100;
|
|
5453
|
+
const levelDistribution = {};
|
|
5454
|
+
for (const gap2 of gaps) {
|
|
5455
|
+
levelDistribution[gap2.severity] = (levelDistribution[gap2.severity] ?? 0) + 1;
|
|
5456
|
+
}
|
|
5457
|
+
return {
|
|
5458
|
+
totalFunctions,
|
|
5459
|
+
functionsWithLogs,
|
|
5460
|
+
errorHandlersWithoutLogs,
|
|
5461
|
+
coveragePercent,
|
|
5462
|
+
levelDistribution
|
|
5463
|
+
};
|
|
5464
|
+
}
|
|
5465
|
+
function normalizeSeverity(severity) {
|
|
5466
|
+
const lower = severity.toLowerCase();
|
|
5467
|
+
if (lower === "error") return "error";
|
|
5468
|
+
if (lower === "warning") return "warning";
|
|
5469
|
+
return "info";
|
|
5470
|
+
}
|
|
5177
5471
|
|
|
5178
5472
|
// src/modules/observability/coverage.ts
|
|
5179
5473
|
import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, renameSync as renameSync2, existsSync as existsSync21 } from "fs";
|
|
@@ -5306,6 +5600,167 @@ function checkObservabilityCoverageGate(projectDir, overrides) {
|
|
|
5306
5600
|
return ok2({ passed, staticResult, runtimeResult, gapSummary });
|
|
5307
5601
|
}
|
|
5308
5602
|
|
|
5603
|
+
// src/modules/observability/runtime-validator.ts
|
|
5604
|
+
import { execSync as execSync3 } from "child_process";
|
|
5605
|
+
import { readdirSync as readdirSync4, statSync as statSync2 } from "fs";
|
|
5606
|
+
import { join as join19 } from "path";
|
|
5607
|
+
var DEFAULT_CONFIG = {
|
|
5608
|
+
testCommand: "npm test",
|
|
5609
|
+
otlpEndpoint: "http://localhost:4318",
|
|
5610
|
+
queryEndpoint: "http://localhost:9428",
|
|
5611
|
+
timeoutMs: 12e4
|
|
5612
|
+
};
|
|
5613
|
+
var HEALTH_TIMEOUT_MS = 3e3;
|
|
5614
|
+
var QUERY_TIMEOUT_MS = 3e4;
|
|
5615
|
+
async function validateRuntime(projectDir, config) {
|
|
5616
|
+
if (!projectDir || typeof projectDir !== "string") {
|
|
5617
|
+
return fail2("projectDir is required and must be a non-empty string");
|
|
5618
|
+
}
|
|
5619
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
5620
|
+
if (/[;&|`$(){}!#]/.test(cfg.testCommand)) {
|
|
5621
|
+
return fail2(`testCommand contains disallowed shell metacharacters: ${cfg.testCommand}`);
|
|
5622
|
+
}
|
|
5623
|
+
const healthy = await checkBackendHealth(cfg.queryEndpoint);
|
|
5624
|
+
if (!healthy) {
|
|
5625
|
+
const modules2 = discoverModules(projectDir);
|
|
5626
|
+
const entries2 = modules2.map((m) => ({
|
|
5627
|
+
moduleName: m,
|
|
5628
|
+
telemetryDetected: false,
|
|
5629
|
+
eventCount: 0
|
|
5630
|
+
}));
|
|
5631
|
+
return ok2({
|
|
5632
|
+
entries: entries2,
|
|
5633
|
+
totalModules: modules2.length,
|
|
5634
|
+
modulesWithTelemetry: 0,
|
|
5635
|
+
coveragePercent: 0,
|
|
5636
|
+
skipped: true,
|
|
5637
|
+
skipReason: "runtime validation skipped -- observability stack not available"
|
|
5638
|
+
});
|
|
5639
|
+
}
|
|
5640
|
+
const startTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
5641
|
+
try {
|
|
5642
|
+
execSync3(cfg.testCommand, {
|
|
5643
|
+
cwd: projectDir,
|
|
5644
|
+
timeout: cfg.timeoutMs,
|
|
5645
|
+
env: {
|
|
5646
|
+
...process.env,
|
|
5647
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: cfg.otlpEndpoint
|
|
5648
|
+
},
|
|
5649
|
+
stdio: "pipe"
|
|
5650
|
+
});
|
|
5651
|
+
} catch (err) {
|
|
5652
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5653
|
+
return fail2(`Test command failed: ${msg}`);
|
|
5654
|
+
}
|
|
5655
|
+
const endTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
5656
|
+
const eventsResult = await queryTelemetryEvents(cfg.queryEndpoint, startTime, endTime);
|
|
5657
|
+
if (!eventsResult.success) {
|
|
5658
|
+
return fail2(eventsResult.error);
|
|
5659
|
+
}
|
|
5660
|
+
const modules = discoverModules(projectDir);
|
|
5661
|
+
const entries = mapEventsToModules(eventsResult.data, projectDir, modules);
|
|
5662
|
+
const modulesWithTelemetry = entries.filter((e) => e.telemetryDetected).length;
|
|
5663
|
+
const totalModules = entries.length;
|
|
5664
|
+
const coveragePercent = totalModules === 0 ? 0 : modulesWithTelemetry / totalModules * 100;
|
|
5665
|
+
return ok2({
|
|
5666
|
+
entries,
|
|
5667
|
+
totalModules,
|
|
5668
|
+
modulesWithTelemetry,
|
|
5669
|
+
coveragePercent,
|
|
5670
|
+
skipped: false
|
|
5671
|
+
});
|
|
5672
|
+
}
|
|
5673
|
+
async function checkBackendHealth(queryEndpoint) {
|
|
5674
|
+
try {
|
|
5675
|
+
const response = await fetch(`${queryEndpoint}/health`, {
|
|
5676
|
+
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
|
|
5677
|
+
});
|
|
5678
|
+
return response.ok;
|
|
5679
|
+
} catch {
|
|
5680
|
+
return false;
|
|
5681
|
+
}
|
|
5682
|
+
}
|
|
5683
|
+
async function queryTelemetryEvents(queryEndpoint, startTime, endTime) {
|
|
5684
|
+
let url;
|
|
5685
|
+
try {
|
|
5686
|
+
url = new URL("/select/logsql/query", queryEndpoint);
|
|
5687
|
+
} catch {
|
|
5688
|
+
return fail2(`Invalid queryEndpoint URL: ${queryEndpoint}`);
|
|
5689
|
+
}
|
|
5690
|
+
url.searchParams.set("query", "*");
|
|
5691
|
+
url.searchParams.set("start", startTime);
|
|
5692
|
+
url.searchParams.set("end", endTime);
|
|
5693
|
+
url.searchParams.set("limit", "1000");
|
|
5694
|
+
try {
|
|
5695
|
+
const response = await fetch(url.toString(), {
|
|
5696
|
+
signal: AbortSignal.timeout(QUERY_TIMEOUT_MS)
|
|
5697
|
+
});
|
|
5698
|
+
if (!response.ok) {
|
|
5699
|
+
return fail2(`VictoriaLogs returned ${response.status}`);
|
|
5700
|
+
}
|
|
5701
|
+
const text = await response.text();
|
|
5702
|
+
const events = parseLogEvents(text);
|
|
5703
|
+
return ok2(events);
|
|
5704
|
+
} catch (err) {
|
|
5705
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5706
|
+
return fail2(`Failed to query telemetry events: ${msg}`);
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5709
|
+
function mapEventsToModules(events, projectDir, modules) {
|
|
5710
|
+
void projectDir;
|
|
5711
|
+
const moduleList = modules ?? [];
|
|
5712
|
+
const moduleCounts = /* @__PURE__ */ new Map();
|
|
5713
|
+
for (const mod of moduleList) {
|
|
5714
|
+
moduleCounts.set(mod, 0);
|
|
5715
|
+
}
|
|
5716
|
+
for (const event of events) {
|
|
5717
|
+
for (const mod of moduleList) {
|
|
5718
|
+
if (event.source.includes(mod) || event.message.includes(mod)) {
|
|
5719
|
+
moduleCounts.set(mod, (moduleCounts.get(mod) ?? 0) + 1);
|
|
5720
|
+
}
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
return moduleList.map((mod) => {
|
|
5724
|
+
const count = moduleCounts.get(mod) ?? 0;
|
|
5725
|
+
return {
|
|
5726
|
+
moduleName: mod,
|
|
5727
|
+
telemetryDetected: count > 0,
|
|
5728
|
+
eventCount: count
|
|
5729
|
+
};
|
|
5730
|
+
});
|
|
5731
|
+
}
|
|
5732
|
+
function discoverModules(projectDir) {
|
|
5733
|
+
const srcDir = join19(projectDir, "src");
|
|
5734
|
+
try {
|
|
5735
|
+
return readdirSync4(srcDir).filter((name) => {
|
|
5736
|
+
try {
|
|
5737
|
+
return statSync2(join19(srcDir, name)).isDirectory();
|
|
5738
|
+
} catch {
|
|
5739
|
+
return false;
|
|
5740
|
+
}
|
|
5741
|
+
});
|
|
5742
|
+
} catch {
|
|
5743
|
+
return [];
|
|
5744
|
+
}
|
|
5745
|
+
}
|
|
5746
|
+
function parseLogEvents(text) {
|
|
5747
|
+
if (!text.trim()) return [];
|
|
5748
|
+
const lines = text.trim().split("\n");
|
|
5749
|
+
const events = [];
|
|
5750
|
+
for (const line of lines) {
|
|
5751
|
+
try {
|
|
5752
|
+
const raw = JSON.parse(line);
|
|
5753
|
+
events.push({
|
|
5754
|
+
timestamp: String(raw._time ?? raw.timestamp ?? ""),
|
|
5755
|
+
message: String(raw._msg ?? raw.message ?? ""),
|
|
5756
|
+
source: String(raw.source ?? raw._source ?? raw.service ?? "")
|
|
5757
|
+
});
|
|
5758
|
+
} catch {
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
5761
|
+
return events;
|
|
5762
|
+
}
|
|
5763
|
+
|
|
5309
5764
|
// src/modules/verify/browser.ts
|
|
5310
5765
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
5311
5766
|
import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
|
|
@@ -5945,9 +6400,9 @@ function getACById(id) {
|
|
|
5945
6400
|
}
|
|
5946
6401
|
|
|
5947
6402
|
// src/modules/verify/validation-runner.ts
|
|
5948
|
-
import { execSync as
|
|
6403
|
+
import { execSync as execSync4 } from "child_process";
|
|
5949
6404
|
import { writeFileSync as writeFileSync14, mkdirSync as mkdirSync7 } from "fs";
|
|
5950
|
-
import { join as
|
|
6405
|
+
import { join as join20, dirname as dirname5 } from "path";
|
|
5951
6406
|
var MAX_VALIDATION_ATTEMPTS = 10;
|
|
5952
6407
|
var AC_COMMAND_TIMEOUT_MS = 3e4;
|
|
5953
6408
|
var VAL_KEY_PREFIX = "val-";
|
|
@@ -6019,7 +6474,7 @@ function executeValidationAC(ac) {
|
|
|
6019
6474
|
}
|
|
6020
6475
|
const startTime = Date.now();
|
|
6021
6476
|
try {
|
|
6022
|
-
const output =
|
|
6477
|
+
const output = execSync4(ac.command, {
|
|
6023
6478
|
timeout: AC_COMMAND_TIMEOUT_MS,
|
|
6024
6479
|
encoding: "utf-8",
|
|
6025
6480
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6056,7 +6511,7 @@ function executeValidationAC(ac) {
|
|
|
6056
6511
|
function createFixStory(ac, error) {
|
|
6057
6512
|
try {
|
|
6058
6513
|
const storyKey = `val-fix-${ac.id}`;
|
|
6059
|
-
const storyPath =
|
|
6514
|
+
const storyPath = join20(
|
|
6060
6515
|
process.cwd(),
|
|
6061
6516
|
"_bmad-output",
|
|
6062
6517
|
"implementation-artifacts",
|
|
@@ -6159,7 +6614,7 @@ function processValidationResult(acId, result) {
|
|
|
6159
6614
|
}
|
|
6160
6615
|
|
|
6161
6616
|
// src/modules/dev/orchestrator.ts
|
|
6162
|
-
import { execFileSync as execFileSync10, execSync as
|
|
6617
|
+
import { execFileSync as execFileSync10, execSync as execSync5 } from "child_process";
|
|
6163
6618
|
var DEFAULT_TIMEOUT_MS = 15e5;
|
|
6164
6619
|
var MAX_OUTPUT_LINES = 200;
|
|
6165
6620
|
var GIT_TIMEOUT_MS2 = 5e3;
|
|
@@ -6172,17 +6627,17 @@ function truncateOutput(output, maxLines) {
|
|
|
6172
6627
|
}
|
|
6173
6628
|
function captureFilesChanged() {
|
|
6174
6629
|
try {
|
|
6175
|
-
const unstaged =
|
|
6630
|
+
const unstaged = execSync5("git diff --name-only", {
|
|
6176
6631
|
timeout: GIT_TIMEOUT_MS2,
|
|
6177
6632
|
encoding: "utf-8",
|
|
6178
6633
|
stdio: ["pipe", "pipe", "pipe"]
|
|
6179
6634
|
}).trim();
|
|
6180
|
-
const staged =
|
|
6635
|
+
const staged = execSync5("git diff --cached --name-only", {
|
|
6181
6636
|
timeout: GIT_TIMEOUT_MS2,
|
|
6182
6637
|
encoding: "utf-8",
|
|
6183
6638
|
stdio: ["pipe", "pipe", "pipe"]
|
|
6184
6639
|
}).trim();
|
|
6185
|
-
const untracked =
|
|
6640
|
+
const untracked = execSync5("git ls-files --others --exclude-standard", {
|
|
6186
6641
|
timeout: GIT_TIMEOUT_MS2,
|
|
6187
6642
|
encoding: "utf-8",
|
|
6188
6643
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6426,8 +6881,8 @@ function runValidationCycle() {
|
|
|
6426
6881
|
|
|
6427
6882
|
// src/modules/verify/env.ts
|
|
6428
6883
|
import { execFileSync as execFileSync11 } from "child_process";
|
|
6429
|
-
import { existsSync as existsSync24, mkdirSync as mkdirSync8, readdirSync as
|
|
6430
|
-
import { join as
|
|
6884
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync8, readdirSync as readdirSync5, readFileSync as readFileSync21, cpSync, rmSync, statSync as statSync3 } from "fs";
|
|
6885
|
+
import { join as join21, basename as basename4 } from "path";
|
|
6431
6886
|
import { createHash } from "crypto";
|
|
6432
6887
|
var IMAGE_TAG = "codeharness-verify";
|
|
6433
6888
|
var STORY_DIR = "_bmad-output/implementation-artifacts";
|
|
@@ -6440,7 +6895,7 @@ function isValidStoryKey(storyKey) {
|
|
|
6440
6895
|
return /^[a-zA-Z0-9_-]+$/.test(storyKey);
|
|
6441
6896
|
}
|
|
6442
6897
|
function computeDistHash(projectDir) {
|
|
6443
|
-
const distDir =
|
|
6898
|
+
const distDir = join21(projectDir, "dist");
|
|
6444
6899
|
if (!existsSync24(distDir)) return null;
|
|
6445
6900
|
const hash = createHash("sha256");
|
|
6446
6901
|
const files = collectFiles(distDir).sort();
|
|
@@ -6452,8 +6907,8 @@ function computeDistHash(projectDir) {
|
|
|
6452
6907
|
}
|
|
6453
6908
|
function collectFiles(dir) {
|
|
6454
6909
|
const results = [];
|
|
6455
|
-
for (const entry of
|
|
6456
|
-
const fullPath =
|
|
6910
|
+
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
6911
|
+
const fullPath = join21(dir, entry.name);
|
|
6457
6912
|
if (entry.isDirectory()) {
|
|
6458
6913
|
results.push(...collectFiles(fullPath));
|
|
6459
6914
|
} else {
|
|
@@ -6483,7 +6938,7 @@ function detectProjectType(projectDir) {
|
|
|
6483
6938
|
const stack = detectStack(projectDir);
|
|
6484
6939
|
if (stack === "nodejs") return "nodejs";
|
|
6485
6940
|
if (stack === "python") return "python";
|
|
6486
|
-
if (existsSync24(
|
|
6941
|
+
if (existsSync24(join21(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
|
|
6487
6942
|
return "generic";
|
|
6488
6943
|
}
|
|
6489
6944
|
function buildVerifyImage(options = {}) {
|
|
@@ -6527,12 +6982,12 @@ function buildNodeImage(projectDir) {
|
|
|
6527
6982
|
const lastLine = packOutput.split("\n").pop()?.trim();
|
|
6528
6983
|
if (!lastLine) throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
|
|
6529
6984
|
const tarballName = basename4(lastLine);
|
|
6530
|
-
const tarballPath =
|
|
6531
|
-
const buildContext =
|
|
6985
|
+
const tarballPath = join21("/tmp", tarballName);
|
|
6986
|
+
const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
6532
6987
|
mkdirSync8(buildContext, { recursive: true });
|
|
6533
6988
|
try {
|
|
6534
|
-
cpSync(tarballPath,
|
|
6535
|
-
cpSync(resolveDockerfileTemplate(projectDir),
|
|
6989
|
+
cpSync(tarballPath, join21(buildContext, tarballName));
|
|
6990
|
+
cpSync(resolveDockerfileTemplate(projectDir), join21(buildContext, "Dockerfile"));
|
|
6536
6991
|
execFileSync11("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${tarballName}`, "."], {
|
|
6537
6992
|
cwd: buildContext,
|
|
6538
6993
|
stdio: "pipe",
|
|
@@ -6544,17 +6999,17 @@ function buildNodeImage(projectDir) {
|
|
|
6544
6999
|
}
|
|
6545
7000
|
}
|
|
6546
7001
|
function buildPythonImage(projectDir) {
|
|
6547
|
-
const distDir =
|
|
6548
|
-
const distFiles =
|
|
7002
|
+
const distDir = join21(projectDir, "dist");
|
|
7003
|
+
const distFiles = readdirSync5(distDir).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".whl"));
|
|
6549
7004
|
if (distFiles.length === 0) {
|
|
6550
7005
|
throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
|
|
6551
7006
|
}
|
|
6552
7007
|
const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
|
|
6553
|
-
const buildContext =
|
|
7008
|
+
const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
6554
7009
|
mkdirSync8(buildContext, { recursive: true });
|
|
6555
7010
|
try {
|
|
6556
|
-
cpSync(
|
|
6557
|
-
cpSync(resolveDockerfileTemplate(projectDir),
|
|
7011
|
+
cpSync(join21(distDir, distFile), join21(buildContext, distFile));
|
|
7012
|
+
cpSync(resolveDockerfileTemplate(projectDir), join21(buildContext, "Dockerfile"));
|
|
6558
7013
|
execFileSync11("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${distFile}`, "."], {
|
|
6559
7014
|
cwd: buildContext,
|
|
6560
7015
|
stdio: "pipe",
|
|
@@ -6569,19 +7024,19 @@ function prepareVerifyWorkspace(storyKey, projectDir) {
|
|
|
6569
7024
|
if (!isValidStoryKey(storyKey)) {
|
|
6570
7025
|
throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
|
|
6571
7026
|
}
|
|
6572
|
-
const storyFile =
|
|
7027
|
+
const storyFile = join21(root, STORY_DIR, `${storyKey}.md`);
|
|
6573
7028
|
if (!existsSync24(storyFile)) throw new Error(`Story file not found: ${storyFile}`);
|
|
6574
7029
|
const workspace = `${TEMP_PREFIX}${storyKey}`;
|
|
6575
7030
|
if (existsSync24(workspace)) rmSync(workspace, { recursive: true, force: true });
|
|
6576
7031
|
mkdirSync8(workspace, { recursive: true });
|
|
6577
|
-
cpSync(storyFile,
|
|
6578
|
-
const readmePath =
|
|
6579
|
-
if (existsSync24(readmePath)) cpSync(readmePath,
|
|
6580
|
-
const docsDir =
|
|
6581
|
-
if (existsSync24(docsDir) &&
|
|
6582
|
-
cpSync(docsDir,
|
|
6583
|
-
}
|
|
6584
|
-
mkdirSync8(
|
|
7032
|
+
cpSync(storyFile, join21(workspace, "story.md"));
|
|
7033
|
+
const readmePath = join21(root, "README.md");
|
|
7034
|
+
if (existsSync24(readmePath)) cpSync(readmePath, join21(workspace, "README.md"));
|
|
7035
|
+
const docsDir = join21(root, "docs");
|
|
7036
|
+
if (existsSync24(docsDir) && statSync3(docsDir).isDirectory()) {
|
|
7037
|
+
cpSync(docsDir, join21(workspace, "docs"), { recursive: true });
|
|
7038
|
+
}
|
|
7039
|
+
mkdirSync8(join21(workspace, "verification"), { recursive: true });
|
|
6585
7040
|
return workspace;
|
|
6586
7041
|
}
|
|
6587
7042
|
function checkVerifyEnv() {
|
|
@@ -6634,18 +7089,18 @@ function cleanupVerifyEnv(storyKey) {
|
|
|
6634
7089
|
}
|
|
6635
7090
|
}
|
|
6636
7091
|
function buildPluginImage(projectDir) {
|
|
6637
|
-
const buildContext =
|
|
7092
|
+
const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
6638
7093
|
mkdirSync8(buildContext, { recursive: true });
|
|
6639
7094
|
try {
|
|
6640
|
-
const pluginDir =
|
|
6641
|
-
cpSync(pluginDir,
|
|
7095
|
+
const pluginDir = join21(projectDir, ".claude-plugin");
|
|
7096
|
+
cpSync(pluginDir, join21(buildContext, ".claude-plugin"), { recursive: true });
|
|
6642
7097
|
for (const dir of ["commands", "hooks", "knowledge", "skills"]) {
|
|
6643
|
-
const src =
|
|
6644
|
-
if (existsSync24(src) &&
|
|
6645
|
-
cpSync(src,
|
|
7098
|
+
const src = join21(projectDir, dir);
|
|
7099
|
+
if (existsSync24(src) && statSync3(src).isDirectory()) {
|
|
7100
|
+
cpSync(src, join21(buildContext, dir), { recursive: true });
|
|
6646
7101
|
}
|
|
6647
7102
|
}
|
|
6648
|
-
cpSync(resolveDockerfileTemplate(projectDir, "generic"),
|
|
7103
|
+
cpSync(resolveDockerfileTemplate(projectDir, "generic"), join21(buildContext, "Dockerfile"));
|
|
6649
7104
|
execFileSync11("docker", ["build", "-t", IMAGE_TAG, "."], {
|
|
6650
7105
|
cwd: buildContext,
|
|
6651
7106
|
stdio: "pipe",
|
|
@@ -6656,10 +7111,10 @@ function buildPluginImage(projectDir) {
|
|
|
6656
7111
|
}
|
|
6657
7112
|
}
|
|
6658
7113
|
function buildGenericImage(projectDir) {
|
|
6659
|
-
const buildContext =
|
|
7114
|
+
const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
6660
7115
|
mkdirSync8(buildContext, { recursive: true });
|
|
6661
7116
|
try {
|
|
6662
|
-
cpSync(resolveDockerfileTemplate(projectDir, "generic"),
|
|
7117
|
+
cpSync(resolveDockerfileTemplate(projectDir, "generic"), join21(buildContext, "Dockerfile"));
|
|
6663
7118
|
execFileSync11("docker", ["build", "-t", IMAGE_TAG, "."], {
|
|
6664
7119
|
cwd: buildContext,
|
|
6665
7120
|
stdio: "pipe",
|
|
@@ -6671,10 +7126,10 @@ function buildGenericImage(projectDir) {
|
|
|
6671
7126
|
}
|
|
6672
7127
|
function resolveDockerfileTemplate(projectDir, variant) {
|
|
6673
7128
|
const filename = variant === "generic" ? "Dockerfile.verify.generic" : "Dockerfile.verify";
|
|
6674
|
-
const local =
|
|
7129
|
+
const local = join21(projectDir, "templates", filename);
|
|
6675
7130
|
if (existsSync24(local)) return local;
|
|
6676
7131
|
const pkgDir = new URL("../../", import.meta.url).pathname;
|
|
6677
|
-
const pkg =
|
|
7132
|
+
const pkg = join21(pkgDir, "templates", filename);
|
|
6678
7133
|
if (existsSync24(pkg)) return pkg;
|
|
6679
7134
|
throw new Error(`${filename} not found. Ensure templates/${filename} exists.`);
|
|
6680
7135
|
}
|
|
@@ -6703,6 +7158,17 @@ function getImageSize(tag) {
|
|
|
6703
7158
|
}
|
|
6704
7159
|
}
|
|
6705
7160
|
|
|
7161
|
+
// src/modules/verify/index.ts
|
|
7162
|
+
function parseProof(path) {
|
|
7163
|
+
try {
|
|
7164
|
+
const quality = validateProofQuality(path);
|
|
7165
|
+
return ok2(quality);
|
|
7166
|
+
} catch (err) {
|
|
7167
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7168
|
+
return fail2(message);
|
|
7169
|
+
}
|
|
7170
|
+
}
|
|
7171
|
+
|
|
6706
7172
|
// src/commands/verify.ts
|
|
6707
7173
|
var STORY_DIR2 = "_bmad-output/implementation-artifacts";
|
|
6708
7174
|
function isValidStoryId(storyId) {
|
|
@@ -6739,7 +7205,7 @@ function verifyRetro(opts, isJson, root) {
|
|
|
6739
7205
|
return;
|
|
6740
7206
|
}
|
|
6741
7207
|
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
6742
|
-
const retroPath =
|
|
7208
|
+
const retroPath = join22(root, STORY_DIR2, retroFile);
|
|
6743
7209
|
if (!existsSync25(retroPath)) {
|
|
6744
7210
|
if (isJson) {
|
|
6745
7211
|
jsonOutput({ status: "fail", epic: epicNum, retroFile, message: `${retroFile} not found` });
|
|
@@ -6757,7 +7223,7 @@ function verifyRetro(opts, isJson, root) {
|
|
|
6757
7223
|
warn(`Failed to update sprint status: ${message}`);
|
|
6758
7224
|
}
|
|
6759
7225
|
if (isJson) {
|
|
6760
|
-
jsonOutput({ status: "ok", epic: epicNum, retroFile:
|
|
7226
|
+
jsonOutput({ status: "ok", epic: epicNum, retroFile: join22(STORY_DIR2, retroFile) });
|
|
6761
7227
|
} else {
|
|
6762
7228
|
ok(`Epic ${epicNum} retrospective: marked done`);
|
|
6763
7229
|
}
|
|
@@ -6768,7 +7234,7 @@ function verifyStory(storyId, isJson, root) {
|
|
|
6768
7234
|
process.exitCode = 1;
|
|
6769
7235
|
return;
|
|
6770
7236
|
}
|
|
6771
|
-
const readmePath =
|
|
7237
|
+
const readmePath = join22(root, "README.md");
|
|
6772
7238
|
if (!existsSync25(readmePath)) {
|
|
6773
7239
|
if (isJson) {
|
|
6774
7240
|
jsonOutput({ status: "fail", message: "No README.md found \u2014 verification requires user documentation" });
|
|
@@ -6778,7 +7244,7 @@ function verifyStory(storyId, isJson, root) {
|
|
|
6778
7244
|
process.exitCode = 1;
|
|
6779
7245
|
return;
|
|
6780
7246
|
}
|
|
6781
|
-
const storyFilePath =
|
|
7247
|
+
const storyFilePath = join22(root, STORY_DIR2, `${storyId}.md`);
|
|
6782
7248
|
if (!existsSync25(storyFilePath)) {
|
|
6783
7249
|
fail(`Story file not found: ${storyFilePath}`, { json: isJson });
|
|
6784
7250
|
process.exitCode = 1;
|
|
@@ -6819,7 +7285,7 @@ function verifyStory(storyId, isJson, root) {
|
|
|
6819
7285
|
return;
|
|
6820
7286
|
}
|
|
6821
7287
|
const storyTitle = extractStoryTitle(storyFilePath);
|
|
6822
|
-
const expectedProofPath =
|
|
7288
|
+
const expectedProofPath = join22(root, "verification", `${storyId}-proof.md`);
|
|
6823
7289
|
const proofPath = existsSync25(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
|
|
6824
7290
|
const proofQuality = validateProofQuality(proofPath);
|
|
6825
7291
|
if (!proofQuality.passed) {
|
|
@@ -6935,13 +7401,13 @@ function extractStoryTitle(filePath) {
|
|
|
6935
7401
|
|
|
6936
7402
|
// src/lib/onboard-checks.ts
|
|
6937
7403
|
import { existsSync as existsSync27 } from "fs";
|
|
6938
|
-
import { join as
|
|
7404
|
+
import { join as join24, dirname as dirname6 } from "path";
|
|
6939
7405
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6940
7406
|
|
|
6941
7407
|
// src/lib/coverage.ts
|
|
6942
|
-
import { execSync as
|
|
7408
|
+
import { execSync as execSync6 } from "child_process";
|
|
6943
7409
|
import { existsSync as existsSync26, readFileSync as readFileSync24 } from "fs";
|
|
6944
|
-
import { join as
|
|
7410
|
+
import { join as join23 } from "path";
|
|
6945
7411
|
function detectCoverageTool(dir) {
|
|
6946
7412
|
const baseDir = dir ?? process.cwd();
|
|
6947
7413
|
const stateHint = getStateToolHint(baseDir);
|
|
@@ -6964,8 +7430,8 @@ function getStateToolHint(dir) {
|
|
|
6964
7430
|
}
|
|
6965
7431
|
}
|
|
6966
7432
|
function detectNodeCoverageTool(dir, stateHint) {
|
|
6967
|
-
const hasVitestConfig = existsSync26(
|
|
6968
|
-
const pkgPath =
|
|
7433
|
+
const hasVitestConfig = existsSync26(join23(dir, "vitest.config.ts")) || existsSync26(join23(dir, "vitest.config.js"));
|
|
7434
|
+
const pkgPath = join23(dir, "package.json");
|
|
6969
7435
|
let hasVitestCoverageV8 = false;
|
|
6970
7436
|
let hasVitestCoverageIstanbul = false;
|
|
6971
7437
|
let hasC8 = false;
|
|
@@ -7026,7 +7492,7 @@ function getNodeTestCommand(scripts, runner) {
|
|
|
7026
7492
|
return "npm test";
|
|
7027
7493
|
}
|
|
7028
7494
|
function detectPythonCoverageTool(dir) {
|
|
7029
|
-
const reqPath =
|
|
7495
|
+
const reqPath = join23(dir, "requirements.txt");
|
|
7030
7496
|
if (existsSync26(reqPath)) {
|
|
7031
7497
|
try {
|
|
7032
7498
|
const content = readFileSync24(reqPath, "utf-8");
|
|
@@ -7040,7 +7506,7 @@ function detectPythonCoverageTool(dir) {
|
|
|
7040
7506
|
} catch {
|
|
7041
7507
|
}
|
|
7042
7508
|
}
|
|
7043
|
-
const pyprojectPath =
|
|
7509
|
+
const pyprojectPath = join23(dir, "pyproject.toml");
|
|
7044
7510
|
if (existsSync26(pyprojectPath)) {
|
|
7045
7511
|
try {
|
|
7046
7512
|
const content = readFileSync24(pyprojectPath, "utf-8");
|
|
@@ -7073,7 +7539,7 @@ function runCoverage(dir) {
|
|
|
7073
7539
|
let rawOutput = "";
|
|
7074
7540
|
let testsPassed = true;
|
|
7075
7541
|
try {
|
|
7076
|
-
rawOutput =
|
|
7542
|
+
rawOutput = execSync6(toolInfo.runCommand, {
|
|
7077
7543
|
cwd: baseDir,
|
|
7078
7544
|
encoding: "utf-8",
|
|
7079
7545
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -7133,7 +7599,7 @@ function parseVitestCoverage(dir) {
|
|
|
7133
7599
|
}
|
|
7134
7600
|
}
|
|
7135
7601
|
function parsePythonCoverage(dir) {
|
|
7136
|
-
const reportPath =
|
|
7602
|
+
const reportPath = join23(dir, "coverage.json");
|
|
7137
7603
|
if (!existsSync26(reportPath)) {
|
|
7138
7604
|
warn("Coverage report not found at coverage.json");
|
|
7139
7605
|
return 0;
|
|
@@ -7270,8 +7736,8 @@ function checkPerFileCoverage(floor, dir) {
|
|
|
7270
7736
|
}
|
|
7271
7737
|
function findCoverageSummary(dir) {
|
|
7272
7738
|
const candidates = [
|
|
7273
|
-
|
|
7274
|
-
|
|
7739
|
+
join23(dir, "coverage", "coverage-summary.json"),
|
|
7740
|
+
join23(dir, "src", "coverage", "coverage-summary.json")
|
|
7275
7741
|
];
|
|
7276
7742
|
for (const p of candidates) {
|
|
7277
7743
|
if (existsSync26(p)) return p;
|
|
@@ -7307,7 +7773,7 @@ function checkBmadInstalled(dir) {
|
|
|
7307
7773
|
function checkHooksRegistered(dir) {
|
|
7308
7774
|
const __filename = fileURLToPath3(import.meta.url);
|
|
7309
7775
|
const __dirname2 = dirname6(__filename);
|
|
7310
|
-
const hooksPath =
|
|
7776
|
+
const hooksPath = join24(__dirname2, "..", "..", "hooks", "hooks.json");
|
|
7311
7777
|
return { ok: existsSync27(hooksPath) };
|
|
7312
7778
|
}
|
|
7313
7779
|
function runPreconditions(dir) {
|
|
@@ -7348,7 +7814,7 @@ function findVerificationGaps(dir) {
|
|
|
7348
7814
|
for (const [key, status] of Object.entries(statuses)) {
|
|
7349
7815
|
if (status !== "done") continue;
|
|
7350
7816
|
if (!STORY_KEY_PATTERN2.test(key)) continue;
|
|
7351
|
-
const proofPath =
|
|
7817
|
+
const proofPath = join24(root, "verification", `${key}-proof.md`);
|
|
7352
7818
|
if (!existsSync27(proofPath)) {
|
|
7353
7819
|
unverified.push(key);
|
|
7354
7820
|
}
|
|
@@ -8061,16 +8527,16 @@ function getBeadsData() {
|
|
|
8061
8527
|
}
|
|
8062
8528
|
|
|
8063
8529
|
// src/commands/onboard.ts
|
|
8064
|
-
import { join as
|
|
8530
|
+
import { join as join28 } from "path";
|
|
8065
8531
|
|
|
8066
8532
|
// src/lib/scanner.ts
|
|
8067
8533
|
import {
|
|
8068
8534
|
existsSync as existsSync28,
|
|
8069
|
-
readdirSync as
|
|
8535
|
+
readdirSync as readdirSync6,
|
|
8070
8536
|
readFileSync as readFileSync25,
|
|
8071
|
-
statSync as
|
|
8537
|
+
statSync as statSync4
|
|
8072
8538
|
} from "fs";
|
|
8073
|
-
import { join as
|
|
8539
|
+
import { join as join25, relative as relative2 } from "path";
|
|
8074
8540
|
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
|
|
8075
8541
|
var DEFAULT_MIN_MODULE_SIZE = 3;
|
|
8076
8542
|
function getExtension2(filename) {
|
|
@@ -8088,17 +8554,17 @@ function countSourceFiles(dir) {
|
|
|
8088
8554
|
function walk(current) {
|
|
8089
8555
|
let entries;
|
|
8090
8556
|
try {
|
|
8091
|
-
entries =
|
|
8557
|
+
entries = readdirSync6(current);
|
|
8092
8558
|
} catch {
|
|
8093
8559
|
return;
|
|
8094
8560
|
}
|
|
8095
8561
|
for (const entry of entries) {
|
|
8096
8562
|
if (isSkippedDir(entry)) continue;
|
|
8097
8563
|
if (entry.startsWith(".") && current !== dir) continue;
|
|
8098
|
-
const fullPath =
|
|
8564
|
+
const fullPath = join25(current, entry);
|
|
8099
8565
|
let stat;
|
|
8100
8566
|
try {
|
|
8101
|
-
stat =
|
|
8567
|
+
stat = statSync4(fullPath);
|
|
8102
8568
|
} catch {
|
|
8103
8569
|
continue;
|
|
8104
8570
|
}
|
|
@@ -8118,22 +8584,22 @@ function countSourceFiles(dir) {
|
|
|
8118
8584
|
return count;
|
|
8119
8585
|
}
|
|
8120
8586
|
function countModuleFiles(modulePath, rootDir) {
|
|
8121
|
-
const fullModulePath =
|
|
8587
|
+
const fullModulePath = join25(rootDir, modulePath);
|
|
8122
8588
|
let sourceFiles = 0;
|
|
8123
8589
|
let testFiles = 0;
|
|
8124
8590
|
function walk(current) {
|
|
8125
8591
|
let entries;
|
|
8126
8592
|
try {
|
|
8127
|
-
entries =
|
|
8593
|
+
entries = readdirSync6(current);
|
|
8128
8594
|
} catch {
|
|
8129
8595
|
return;
|
|
8130
8596
|
}
|
|
8131
8597
|
for (const entry of entries) {
|
|
8132
8598
|
if (isSkippedDir(entry)) continue;
|
|
8133
|
-
const fullPath =
|
|
8599
|
+
const fullPath = join25(current, entry);
|
|
8134
8600
|
let stat;
|
|
8135
8601
|
try {
|
|
8136
|
-
stat =
|
|
8602
|
+
stat = statSync4(fullPath);
|
|
8137
8603
|
} catch {
|
|
8138
8604
|
continue;
|
|
8139
8605
|
}
|
|
@@ -8155,7 +8621,7 @@ function countModuleFiles(modulePath, rootDir) {
|
|
|
8155
8621
|
return { sourceFiles, testFiles };
|
|
8156
8622
|
}
|
|
8157
8623
|
function detectArtifacts(dir) {
|
|
8158
|
-
const bmadPath =
|
|
8624
|
+
const bmadPath = join25(dir, "_bmad");
|
|
8159
8625
|
const hasBmad = existsSync28(bmadPath);
|
|
8160
8626
|
return {
|
|
8161
8627
|
hasBmad,
|
|
@@ -8238,7 +8704,7 @@ function readPerFileCoverage(dir, format) {
|
|
|
8238
8704
|
return null;
|
|
8239
8705
|
}
|
|
8240
8706
|
function readVitestPerFileCoverage(dir) {
|
|
8241
|
-
const reportPath =
|
|
8707
|
+
const reportPath = join25(dir, "coverage", "coverage-summary.json");
|
|
8242
8708
|
if (!existsSync28(reportPath)) return null;
|
|
8243
8709
|
try {
|
|
8244
8710
|
const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
|
|
@@ -8253,7 +8719,7 @@ function readVitestPerFileCoverage(dir) {
|
|
|
8253
8719
|
}
|
|
8254
8720
|
}
|
|
8255
8721
|
function readPythonPerFileCoverage(dir) {
|
|
8256
|
-
const reportPath =
|
|
8722
|
+
const reportPath = join25(dir, "coverage.json");
|
|
8257
8723
|
if (!existsSync28(reportPath)) return null;
|
|
8258
8724
|
try {
|
|
8259
8725
|
const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
|
|
@@ -8272,12 +8738,12 @@ function auditDocumentation(dir) {
|
|
|
8272
8738
|
const root = dir ?? process.cwd();
|
|
8273
8739
|
const documents = [];
|
|
8274
8740
|
for (const docName of AUDIT_DOCUMENTS) {
|
|
8275
|
-
const docPath =
|
|
8741
|
+
const docPath = join25(root, docName);
|
|
8276
8742
|
if (!existsSync28(docPath)) {
|
|
8277
8743
|
documents.push({ name: docName, grade: "missing", path: null });
|
|
8278
8744
|
continue;
|
|
8279
8745
|
}
|
|
8280
|
-
const srcDir =
|
|
8746
|
+
const srcDir = join25(root, "src");
|
|
8281
8747
|
const codeDir = existsSync28(srcDir) ? srcDir : root;
|
|
8282
8748
|
const stale = isDocStale(docPath, codeDir);
|
|
8283
8749
|
documents.push({
|
|
@@ -8286,10 +8752,10 @@ function auditDocumentation(dir) {
|
|
|
8286
8752
|
path: docName
|
|
8287
8753
|
});
|
|
8288
8754
|
}
|
|
8289
|
-
const docsDir =
|
|
8755
|
+
const docsDir = join25(root, "docs");
|
|
8290
8756
|
if (existsSync28(docsDir)) {
|
|
8291
8757
|
try {
|
|
8292
|
-
const stat =
|
|
8758
|
+
const stat = statSync4(docsDir);
|
|
8293
8759
|
if (stat.isDirectory()) {
|
|
8294
8760
|
documents.push({ name: "docs/", grade: "present", path: "docs/" });
|
|
8295
8761
|
}
|
|
@@ -8299,9 +8765,9 @@ function auditDocumentation(dir) {
|
|
|
8299
8765
|
} else {
|
|
8300
8766
|
documents.push({ name: "docs/", grade: "missing", path: null });
|
|
8301
8767
|
}
|
|
8302
|
-
const indexPath =
|
|
8768
|
+
const indexPath = join25(root, "docs", "index.md");
|
|
8303
8769
|
if (existsSync28(indexPath)) {
|
|
8304
|
-
const srcDir =
|
|
8770
|
+
const srcDir = join25(root, "src");
|
|
8305
8771
|
const indexCodeDir = existsSync28(srcDir) ? srcDir : root;
|
|
8306
8772
|
const indexStale = isDocStale(indexPath, indexCodeDir);
|
|
8307
8773
|
documents.push({
|
|
@@ -8320,7 +8786,7 @@ function auditDocumentation(dir) {
|
|
|
8320
8786
|
// src/lib/epic-generator.ts
|
|
8321
8787
|
import { createInterface } from "readline";
|
|
8322
8788
|
import { existsSync as existsSync29, mkdirSync as mkdirSync9, writeFileSync as writeFileSync15 } from "fs";
|
|
8323
|
-
import { dirname as dirname7, join as
|
|
8789
|
+
import { dirname as dirname7, join as join26 } from "path";
|
|
8324
8790
|
var PRIORITY_BY_TYPE = {
|
|
8325
8791
|
observability: 1,
|
|
8326
8792
|
coverage: 2,
|
|
@@ -8358,7 +8824,7 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
|
|
|
8358
8824
|
storyNum++;
|
|
8359
8825
|
}
|
|
8360
8826
|
for (const mod of scan.modules) {
|
|
8361
|
-
const agentsPath =
|
|
8827
|
+
const agentsPath = join26(root, mod.path, "AGENTS.md");
|
|
8362
8828
|
if (!existsSync29(agentsPath)) {
|
|
8363
8829
|
stories.push({
|
|
8364
8830
|
key: `0.${storyNum}`,
|
|
@@ -8558,23 +9024,23 @@ function getGapIdFromTitle(title) {
|
|
|
8558
9024
|
|
|
8559
9025
|
// src/lib/scan-cache.ts
|
|
8560
9026
|
import { existsSync as existsSync30, mkdirSync as mkdirSync10, readFileSync as readFileSync26, writeFileSync as writeFileSync16 } from "fs";
|
|
8561
|
-
import { join as
|
|
9027
|
+
import { join as join27 } from "path";
|
|
8562
9028
|
var CACHE_DIR = ".harness";
|
|
8563
9029
|
var CACHE_FILE = "last-onboard-scan.json";
|
|
8564
9030
|
var DEFAULT_MAX_AGE_MS = 864e5;
|
|
8565
9031
|
function saveScanCache(entry, dir) {
|
|
8566
9032
|
try {
|
|
8567
9033
|
const root = dir ?? process.cwd();
|
|
8568
|
-
const cacheDir =
|
|
9034
|
+
const cacheDir = join27(root, CACHE_DIR);
|
|
8569
9035
|
mkdirSync10(cacheDir, { recursive: true });
|
|
8570
|
-
const cachePath =
|
|
9036
|
+
const cachePath = join27(cacheDir, CACHE_FILE);
|
|
8571
9037
|
writeFileSync16(cachePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
8572
9038
|
} catch {
|
|
8573
9039
|
}
|
|
8574
9040
|
}
|
|
8575
9041
|
function loadScanCache(dir) {
|
|
8576
9042
|
const root = dir ?? process.cwd();
|
|
8577
|
-
const cachePath =
|
|
9043
|
+
const cachePath = join27(root, CACHE_DIR, CACHE_FILE);
|
|
8578
9044
|
if (!existsSync30(cachePath)) {
|
|
8579
9045
|
return null;
|
|
8580
9046
|
}
|
|
@@ -8752,7 +9218,7 @@ function registerOnboardCommand(program) {
|
|
|
8752
9218
|
}
|
|
8753
9219
|
coverage = lastCoverageResult ?? runCoverageAnalysis(scan);
|
|
8754
9220
|
audit = lastAuditResult ?? runAudit();
|
|
8755
|
-
const epicPath =
|
|
9221
|
+
const epicPath = join28(process.cwd(), "ralph", "onboarding-epic.md");
|
|
8756
9222
|
const epic = generateOnboardingEpic(scan, coverage, audit);
|
|
8757
9223
|
mergeExtendedGaps(epic);
|
|
8758
9224
|
if (!isFull) {
|
|
@@ -8825,7 +9291,7 @@ function registerOnboardCommand(program) {
|
|
|
8825
9291
|
coverage,
|
|
8826
9292
|
audit
|
|
8827
9293
|
});
|
|
8828
|
-
const epicPath =
|
|
9294
|
+
const epicPath = join28(process.cwd(), "ralph", "onboarding-epic.md");
|
|
8829
9295
|
const epic = generateOnboardingEpic(scan, coverage, audit);
|
|
8830
9296
|
mergeExtendedGaps(epic);
|
|
8831
9297
|
if (!isFull) {
|
|
@@ -8934,7 +9400,7 @@ function printEpicOutput(epic) {
|
|
|
8934
9400
|
|
|
8935
9401
|
// src/commands/teardown.ts
|
|
8936
9402
|
import { existsSync as existsSync31, unlinkSync as unlinkSync2, readFileSync as readFileSync27, writeFileSync as writeFileSync17, rmSync as rmSync2 } from "fs";
|
|
8937
|
-
import { join as
|
|
9403
|
+
import { join as join29 } from "path";
|
|
8938
9404
|
function buildDefaultResult() {
|
|
8939
9405
|
return {
|
|
8940
9406
|
status: "ok",
|
|
@@ -9037,7 +9503,7 @@ function registerTeardownCommand(program) {
|
|
|
9037
9503
|
info("Docker stack: not running, skipping");
|
|
9038
9504
|
}
|
|
9039
9505
|
}
|
|
9040
|
-
const composeFilePath =
|
|
9506
|
+
const composeFilePath = join29(projectDir, composeFile);
|
|
9041
9507
|
if (existsSync31(composeFilePath)) {
|
|
9042
9508
|
unlinkSync2(composeFilePath);
|
|
9043
9509
|
result.removed.push(composeFile);
|
|
@@ -9045,7 +9511,7 @@ function registerTeardownCommand(program) {
|
|
|
9045
9511
|
ok(`Removed: ${composeFile}`);
|
|
9046
9512
|
}
|
|
9047
9513
|
}
|
|
9048
|
-
const otelConfigPath =
|
|
9514
|
+
const otelConfigPath = join29(projectDir, "otel-collector-config.yaml");
|
|
9049
9515
|
if (existsSync31(otelConfigPath)) {
|
|
9050
9516
|
unlinkSync2(otelConfigPath);
|
|
9051
9517
|
result.removed.push("otel-collector-config.yaml");
|
|
@@ -9056,7 +9522,7 @@ function registerTeardownCommand(program) {
|
|
|
9056
9522
|
}
|
|
9057
9523
|
let patchesRemoved = 0;
|
|
9058
9524
|
for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
|
|
9059
|
-
const filePath =
|
|
9525
|
+
const filePath = join29(projectDir, "_bmad", relativePath);
|
|
9060
9526
|
if (!existsSync31(filePath)) {
|
|
9061
9527
|
continue;
|
|
9062
9528
|
}
|
|
@@ -9077,7 +9543,7 @@ function registerTeardownCommand(program) {
|
|
|
9077
9543
|
}
|
|
9078
9544
|
}
|
|
9079
9545
|
if (state.otlp?.enabled && state.stack === "nodejs") {
|
|
9080
|
-
const pkgPath =
|
|
9546
|
+
const pkgPath = join29(projectDir, "package.json");
|
|
9081
9547
|
if (existsSync31(pkgPath)) {
|
|
9082
9548
|
try {
|
|
9083
9549
|
const raw = readFileSync27(pkgPath, "utf-8");
|
|
@@ -9120,7 +9586,7 @@ function registerTeardownCommand(program) {
|
|
|
9120
9586
|
}
|
|
9121
9587
|
}
|
|
9122
9588
|
}
|
|
9123
|
-
const harnessDir =
|
|
9589
|
+
const harnessDir = join29(projectDir, ".harness");
|
|
9124
9590
|
if (existsSync31(harnessDir)) {
|
|
9125
9591
|
rmSync2(harnessDir, { recursive: true, force: true });
|
|
9126
9592
|
result.removed.push(".harness/");
|
|
@@ -9874,7 +10340,7 @@ function registerQueryCommand(program) {
|
|
|
9874
10340
|
|
|
9875
10341
|
// src/commands/retro-import.ts
|
|
9876
10342
|
import { existsSync as existsSync32, readFileSync as readFileSync28 } from "fs";
|
|
9877
|
-
import { join as
|
|
10343
|
+
import { join as join30 } from "path";
|
|
9878
10344
|
|
|
9879
10345
|
// src/lib/retro-parser.ts
|
|
9880
10346
|
var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
|
|
@@ -10043,7 +10509,7 @@ function registerRetroImportCommand(program) {
|
|
|
10043
10509
|
return;
|
|
10044
10510
|
}
|
|
10045
10511
|
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
10046
|
-
const retroPath =
|
|
10512
|
+
const retroPath = join30(root, STORY_DIR3, retroFile);
|
|
10047
10513
|
if (!existsSync32(retroPath)) {
|
|
10048
10514
|
fail(`Retro file not found: ${retroFile}`, { json: isJson });
|
|
10049
10515
|
process.exitCode = 1;
|
|
@@ -10432,19 +10898,19 @@ function registerVerifyEnvCommand(program) {
|
|
|
10432
10898
|
}
|
|
10433
10899
|
|
|
10434
10900
|
// src/commands/retry.ts
|
|
10435
|
-
import { join as
|
|
10901
|
+
import { join as join32 } from "path";
|
|
10436
10902
|
|
|
10437
10903
|
// src/lib/retry-state.ts
|
|
10438
10904
|
import { existsSync as existsSync33, readFileSync as readFileSync29, writeFileSync as writeFileSync18 } from "fs";
|
|
10439
|
-
import { join as
|
|
10905
|
+
import { join as join31 } from "path";
|
|
10440
10906
|
var RETRIES_FILE = ".story_retries";
|
|
10441
10907
|
var FLAGGED_FILE = ".flagged_stories";
|
|
10442
10908
|
var LINE_PATTERN = /^([^=]+)=(\d+)$/;
|
|
10443
10909
|
function retriesPath(dir) {
|
|
10444
|
-
return
|
|
10910
|
+
return join31(dir, RETRIES_FILE);
|
|
10445
10911
|
}
|
|
10446
10912
|
function flaggedPath(dir) {
|
|
10447
|
-
return
|
|
10913
|
+
return join31(dir, FLAGGED_FILE);
|
|
10448
10914
|
}
|
|
10449
10915
|
function readRetries(dir) {
|
|
10450
10916
|
const filePath = retriesPath(dir);
|
|
@@ -10516,7 +10982,7 @@ function registerRetryCommand(program) {
|
|
|
10516
10982
|
program.command("retry").description("Manage retry state for stories").option("--reset", "Clear retry counters and flagged stories").option("--story <key>", "Target a specific story key (used with --reset or --status)").option("--status", "Show retry status for all stories").action((_options, cmd) => {
|
|
10517
10983
|
const opts = cmd.optsWithGlobals();
|
|
10518
10984
|
const isJson = opts.json === true;
|
|
10519
|
-
const dir =
|
|
10985
|
+
const dir = join32(process.cwd(), RALPH_SUBDIR);
|
|
10520
10986
|
if (opts.story && !isValidStoryKey3(opts.story)) {
|
|
10521
10987
|
if (isJson) {
|
|
10522
10988
|
jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
|
|
@@ -10914,8 +11380,435 @@ function registerObservabilityGateCommand(program) {
|
|
|
10914
11380
|
});
|
|
10915
11381
|
}
|
|
10916
11382
|
|
|
11383
|
+
// src/modules/audit/dimensions.ts
|
|
11384
|
+
import { existsSync as existsSync34, readFileSync as readFileSync30, readdirSync as readdirSync7 } from "fs";
|
|
11385
|
+
import { join as join33 } from "path";
|
|
11386
|
+
function gap(dimension, description, suggestedFix) {
|
|
11387
|
+
return { dimension, description, suggestedFix };
|
|
11388
|
+
}
|
|
11389
|
+
function dimOk(name, status, metric, gaps = []) {
|
|
11390
|
+
return ok2({ name, status, metric, gaps });
|
|
11391
|
+
}
|
|
11392
|
+
function dimCatch(name, err) {
|
|
11393
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11394
|
+
return dimOk(name, "warn", "error", [gap(name, `${name} check failed: ${msg}`, `Check ${name} configuration`)]);
|
|
11395
|
+
}
|
|
11396
|
+
function worstStatus(...statuses) {
|
|
11397
|
+
if (statuses.includes("fail")) return "fail";
|
|
11398
|
+
if (statuses.includes("warn")) return "warn";
|
|
11399
|
+
return "pass";
|
|
11400
|
+
}
|
|
11401
|
+
async function checkObservability(projectDir) {
|
|
11402
|
+
try {
|
|
11403
|
+
const gaps = [];
|
|
11404
|
+
let sStatus = "pass", sMetric = "";
|
|
11405
|
+
const sr = analyze(projectDir);
|
|
11406
|
+
if (isOk(sr)) {
|
|
11407
|
+
const d = sr.data;
|
|
11408
|
+
if (d.skipped) {
|
|
11409
|
+
sStatus = "warn";
|
|
11410
|
+
sMetric = `static: skipped (${d.skipReason ?? "unknown"})`;
|
|
11411
|
+
gaps.push(gap("observability", `Static analysis skipped: ${d.skipReason ?? "Semgrep not installed"}`, "Install Semgrep: pip install semgrep"));
|
|
11412
|
+
} else {
|
|
11413
|
+
const n = d.gaps.length;
|
|
11414
|
+
sMetric = `static: ${n} gap${n !== 1 ? "s" : ""}`;
|
|
11415
|
+
if (n > 0) {
|
|
11416
|
+
sStatus = "warn";
|
|
11417
|
+
for (const g of d.gaps) gaps.push(gap("observability", `${g.file}:${g.line} \u2014 ${g.message}`, g.fix ?? "Add observability instrumentation"));
|
|
11418
|
+
}
|
|
11419
|
+
}
|
|
11420
|
+
} else {
|
|
11421
|
+
sStatus = "warn";
|
|
11422
|
+
sMetric = "static: skipped (analysis failed)";
|
|
11423
|
+
gaps.push(gap("observability", `Static analysis failed: ${sr.error}`, "Check Semgrep installation and rules configuration"));
|
|
11424
|
+
}
|
|
11425
|
+
let rStatus = "pass", rMetric = "";
|
|
11426
|
+
try {
|
|
11427
|
+
const rr = await validateRuntime(projectDir);
|
|
11428
|
+
if (isOk(rr)) {
|
|
11429
|
+
const d = rr.data;
|
|
11430
|
+
if (d.skipped) {
|
|
11431
|
+
rStatus = "warn";
|
|
11432
|
+
rMetric = `runtime: skipped (${d.skipReason ?? "unknown"})`;
|
|
11433
|
+
gaps.push(gap("observability", `Runtime validation skipped: ${d.skipReason ?? "backend unreachable"}`, "Start the observability stack: codeharness stack up"));
|
|
11434
|
+
} else {
|
|
11435
|
+
rMetric = `runtime: ${d.coveragePercent}%`;
|
|
11436
|
+
if (d.coveragePercent < 50) {
|
|
11437
|
+
rStatus = "warn";
|
|
11438
|
+
gaps.push(gap("observability", `Runtime coverage low: ${d.coveragePercent}%`, "Add telemetry instrumentation to more modules"));
|
|
11439
|
+
}
|
|
11440
|
+
}
|
|
11441
|
+
} else {
|
|
11442
|
+
rStatus = "warn";
|
|
11443
|
+
rMetric = "runtime: skipped (validation failed)";
|
|
11444
|
+
gaps.push(gap("observability", `Runtime validation failed: ${rr.error}`, "Ensure observability backend is running"));
|
|
11445
|
+
}
|
|
11446
|
+
} catch {
|
|
11447
|
+
rStatus = "warn";
|
|
11448
|
+
rMetric = "runtime: skipped (error)";
|
|
11449
|
+
gaps.push(gap("observability", "Runtime validation threw an unexpected error", "Check observability stack health"));
|
|
11450
|
+
}
|
|
11451
|
+
return dimOk("observability", worstStatus(sStatus, rStatus), `${sMetric}, ${rMetric}`, gaps);
|
|
11452
|
+
} catch (err) {
|
|
11453
|
+
return dimCatch("observability", err);
|
|
11454
|
+
}
|
|
11455
|
+
}
|
|
11456
|
+
function checkTesting(projectDir) {
|
|
11457
|
+
try {
|
|
11458
|
+
const r = checkOnlyCoverage(projectDir);
|
|
11459
|
+
if (!r.success) return dimOk("testing", "warn", "no coverage data", [gap("testing", "No coverage tool detected or coverage data unavailable", "Run tests with coverage: npm run test:coverage")]);
|
|
11460
|
+
const pct = r.coveragePercent;
|
|
11461
|
+
const gaps = [];
|
|
11462
|
+
let status = "pass";
|
|
11463
|
+
if (pct < 50) {
|
|
11464
|
+
status = "fail";
|
|
11465
|
+
gaps.push(gap("testing", `Test coverage critically low: ${pct}%`, "Add unit tests to increase coverage above 50%"));
|
|
11466
|
+
} else if (pct < 80) {
|
|
11467
|
+
status = "warn";
|
|
11468
|
+
gaps.push(gap("testing", `Test coverage below target: ${pct}%`, "Add tests to reach 80% coverage target"));
|
|
11469
|
+
}
|
|
11470
|
+
return dimOk("testing", status, `${pct}%`, gaps);
|
|
11471
|
+
} catch (err) {
|
|
11472
|
+
return dimCatch("testing", err);
|
|
11473
|
+
}
|
|
11474
|
+
}
|
|
11475
|
+
function checkDocumentation(projectDir) {
|
|
11476
|
+
try {
|
|
11477
|
+
const report = scanDocHealth(projectDir);
|
|
11478
|
+
const gaps = [];
|
|
11479
|
+
const { fresh, stale, missing } = report.summary;
|
|
11480
|
+
let status = "pass";
|
|
11481
|
+
if (missing > 0) {
|
|
11482
|
+
status = "fail";
|
|
11483
|
+
for (const doc of report.documents) if (doc.grade === "missing") gaps.push(gap("documentation", `Missing: ${doc.path} \u2014 ${doc.reason}`, `Create ${doc.path}`));
|
|
11484
|
+
}
|
|
11485
|
+
if (stale > 0) {
|
|
11486
|
+
if (status !== "fail") status = "warn";
|
|
11487
|
+
for (const doc of report.documents) if (doc.grade === "stale") gaps.push(gap("documentation", `Stale: ${doc.path} \u2014 ${doc.reason}`, `Update ${doc.path} to reflect current code`));
|
|
11488
|
+
}
|
|
11489
|
+
return dimOk("documentation", status, `${fresh} fresh, ${stale} stale, ${missing} missing`, gaps);
|
|
11490
|
+
} catch (err) {
|
|
11491
|
+
return dimCatch("documentation", err);
|
|
11492
|
+
}
|
|
11493
|
+
}
|
|
11494
|
+
function checkVerification(projectDir) {
|
|
11495
|
+
try {
|
|
11496
|
+
const gaps = [];
|
|
11497
|
+
const sprintPath = join33(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
|
|
11498
|
+
if (!existsSync34(sprintPath)) return dimOk("verification", "warn", "no sprint data", [gap("verification", "No sprint-status.yaml found", "Run sprint planning to create sprint status")]);
|
|
11499
|
+
const vDir = join33(projectDir, "verification");
|
|
11500
|
+
let proofCount = 0, totalChecked = 0;
|
|
11501
|
+
if (existsSync34(vDir)) {
|
|
11502
|
+
for (const file of readdirSafe(vDir)) {
|
|
11503
|
+
if (!file.endsWith("-proof.md")) continue;
|
|
11504
|
+
totalChecked++;
|
|
11505
|
+
const r = parseProof(join33(vDir, file));
|
|
11506
|
+
if (isOk(r) && r.data.passed) {
|
|
11507
|
+
proofCount++;
|
|
11508
|
+
} else {
|
|
11509
|
+
gaps.push(gap("verification", `Story ${file.replace("-proof.md", "")} proof incomplete or failing`, `Run codeharness verify ${file.replace("-proof.md", "")}`));
|
|
11510
|
+
}
|
|
11511
|
+
}
|
|
11512
|
+
}
|
|
11513
|
+
let status = "pass";
|
|
11514
|
+
if (totalChecked === 0) {
|
|
11515
|
+
status = "warn";
|
|
11516
|
+
gaps.push(gap("verification", "No verification proofs found", "Run codeharness verify for completed stories"));
|
|
11517
|
+
} else if (proofCount < totalChecked) {
|
|
11518
|
+
status = "warn";
|
|
11519
|
+
}
|
|
11520
|
+
return dimOk("verification", status, totalChecked > 0 ? `${proofCount}/${totalChecked} verified` : "no proofs", gaps);
|
|
11521
|
+
} catch (err) {
|
|
11522
|
+
return dimCatch("verification", err);
|
|
11523
|
+
}
|
|
11524
|
+
}
|
|
11525
|
+
function checkInfrastructure(projectDir) {
|
|
11526
|
+
try {
|
|
11527
|
+
const dfPath = join33(projectDir, "Dockerfile");
|
|
11528
|
+
if (!existsSync34(dfPath)) return dimOk("infrastructure", "fail", "no Dockerfile", [gap("infrastructure", "No Dockerfile found", "Create a Dockerfile for containerized deployment")]);
|
|
11529
|
+
let content;
|
|
11530
|
+
try {
|
|
11531
|
+
content = readFileSync30(dfPath, "utf-8");
|
|
11532
|
+
} catch {
|
|
11533
|
+
return dimOk("infrastructure", "warn", "Dockerfile unreadable", [gap("infrastructure", "Dockerfile exists but could not be read", "Check Dockerfile permissions")]);
|
|
11534
|
+
}
|
|
11535
|
+
const fromLines = content.split("\n").filter((l) => /^\s*FROM\s+/i.test(l));
|
|
11536
|
+
if (fromLines.length === 0) return dimOk("infrastructure", "fail", "invalid Dockerfile", [gap("infrastructure", "Dockerfile has no FROM instruction", "Add a FROM instruction with a pinned base image")]);
|
|
11537
|
+
const gaps = [];
|
|
11538
|
+
let hasUnpinned = false;
|
|
11539
|
+
for (const line of fromLines) {
|
|
11540
|
+
const ref = line.replace(/^\s*FROM\s+/i, "").split(/\s+/)[0];
|
|
11541
|
+
if (ref.endsWith(":latest")) {
|
|
11542
|
+
hasUnpinned = true;
|
|
11543
|
+
gaps.push(gap("infrastructure", `Unpinned base image: ${ref}`, `Pin ${ref} to a specific version tag`));
|
|
11544
|
+
} else if (!ref.includes(":") && !ref.includes("@")) {
|
|
11545
|
+
hasUnpinned = true;
|
|
11546
|
+
gaps.push(gap("infrastructure", `Unpinned base image (no tag): ${ref}`, `Pin ${ref} to a specific version tag (e.g., ${ref}:22-slim)`));
|
|
11547
|
+
}
|
|
11548
|
+
}
|
|
11549
|
+
const status = hasUnpinned ? "warn" : "pass";
|
|
11550
|
+
const metric = hasUnpinned ? `Dockerfile exists (${gaps.length} issue${gaps.length !== 1 ? "s" : ""})` : "Dockerfile valid";
|
|
11551
|
+
return dimOk("infrastructure", status, metric, gaps);
|
|
11552
|
+
} catch (err) {
|
|
11553
|
+
return dimCatch("infrastructure", err);
|
|
11554
|
+
}
|
|
11555
|
+
}
|
|
11556
|
+
function readdirSafe(dir) {
|
|
11557
|
+
try {
|
|
11558
|
+
return readdirSync7(dir);
|
|
11559
|
+
} catch {
|
|
11560
|
+
return [];
|
|
11561
|
+
}
|
|
11562
|
+
}
|
|
11563
|
+
|
|
11564
|
+
// src/modules/audit/report.ts
|
|
11565
|
+
var STATUS_PREFIX = {
|
|
11566
|
+
pass: "[OK]",
|
|
11567
|
+
fail: "[FAIL]",
|
|
11568
|
+
warn: "[WARN]"
|
|
11569
|
+
};
|
|
11570
|
+
function formatAuditHuman(result) {
|
|
11571
|
+
const lines = [];
|
|
11572
|
+
for (const dimension of Object.values(result.dimensions)) {
|
|
11573
|
+
const prefix = STATUS_PREFIX[dimension.status] ?? "[WARN]";
|
|
11574
|
+
lines.push(`${prefix} ${dimension.name}: ${dimension.metric}`);
|
|
11575
|
+
for (const gap2 of dimension.gaps) {
|
|
11576
|
+
lines.push(` [WARN] ${gap2.description} -- fix: ${gap2.suggestedFix}`);
|
|
11577
|
+
}
|
|
11578
|
+
}
|
|
11579
|
+
const overallPrefix = STATUS_PREFIX[result.overallStatus] ?? "[WARN]";
|
|
11580
|
+
lines.push("");
|
|
11581
|
+
lines.push(
|
|
11582
|
+
`${overallPrefix} Audit complete: ${result.gapCount} gap${result.gapCount !== 1 ? "s" : ""} found (${result.durationMs}ms)`
|
|
11583
|
+
);
|
|
11584
|
+
return lines;
|
|
11585
|
+
}
|
|
11586
|
+
function formatAuditJson(result) {
|
|
11587
|
+
return result;
|
|
11588
|
+
}
|
|
11589
|
+
|
|
11590
|
+
// src/modules/audit/fix-generator.ts
|
|
11591
|
+
import { existsSync as existsSync35, writeFileSync as writeFileSync19, mkdirSync as mkdirSync11 } from "fs";
|
|
11592
|
+
import { join as join34, dirname as dirname8 } from "path";
|
|
11593
|
+
function buildStoryKey(gap2, index) {
|
|
11594
|
+
const safeDimension = gap2.dimension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
11595
|
+
return `audit-fix-${safeDimension}-${index}`;
|
|
11596
|
+
}
|
|
11597
|
+
function buildStoryMarkdown(gap2, key) {
|
|
11598
|
+
return [
|
|
11599
|
+
`# Fix: ${gap2.dimension} \u2014 ${gap2.description}`,
|
|
11600
|
+
"",
|
|
11601
|
+
"Status: backlog",
|
|
11602
|
+
"",
|
|
11603
|
+
"## Story",
|
|
11604
|
+
"",
|
|
11605
|
+
`As an operator, I need ${gap2.description} fixed so that audit compliance improves.`,
|
|
11606
|
+
"",
|
|
11607
|
+
"## Acceptance Criteria",
|
|
11608
|
+
"",
|
|
11609
|
+
`1. **Given** ${gap2.description}, **When** the fix is applied, **Then** ${gap2.suggestedFix}.`,
|
|
11610
|
+
"",
|
|
11611
|
+
"## Dev Notes",
|
|
11612
|
+
"",
|
|
11613
|
+
"This is an auto-generated fix story created by `codeharness audit --fix`.",
|
|
11614
|
+
`**Audit Gap:** ${gap2.dimension}: ${gap2.description}`,
|
|
11615
|
+
`**Suggested Fix:** ${gap2.suggestedFix}`,
|
|
11616
|
+
""
|
|
11617
|
+
].join("\n");
|
|
11618
|
+
}
|
|
11619
|
+
function generateFixStories(auditResult) {
|
|
11620
|
+
try {
|
|
11621
|
+
const stories = [];
|
|
11622
|
+
let created = 0;
|
|
11623
|
+
let skipped = 0;
|
|
11624
|
+
const artifactsDir = join34(
|
|
11625
|
+
process.cwd(),
|
|
11626
|
+
"_bmad-output",
|
|
11627
|
+
"implementation-artifacts"
|
|
11628
|
+
);
|
|
11629
|
+
for (const dimension of Object.values(auditResult.dimensions)) {
|
|
11630
|
+
for (let i = 0; i < dimension.gaps.length; i++) {
|
|
11631
|
+
const gap2 = dimension.gaps[i];
|
|
11632
|
+
const key = buildStoryKey(gap2, i + 1);
|
|
11633
|
+
const filePath = join34(artifactsDir, `${key}.md`);
|
|
11634
|
+
if (existsSync35(filePath)) {
|
|
11635
|
+
stories.push({
|
|
11636
|
+
key,
|
|
11637
|
+
filePath,
|
|
11638
|
+
gap: gap2,
|
|
11639
|
+
skipped: true,
|
|
11640
|
+
skipReason: "Story file already exists"
|
|
11641
|
+
});
|
|
11642
|
+
skipped++;
|
|
11643
|
+
continue;
|
|
11644
|
+
}
|
|
11645
|
+
const markdown = buildStoryMarkdown(gap2, key);
|
|
11646
|
+
mkdirSync11(dirname8(filePath), { recursive: true });
|
|
11647
|
+
writeFileSync19(filePath, markdown, "utf-8");
|
|
11648
|
+
stories.push({ key, filePath, gap: gap2, skipped: false });
|
|
11649
|
+
created++;
|
|
11650
|
+
}
|
|
11651
|
+
}
|
|
11652
|
+
return ok2({ stories, created, skipped });
|
|
11653
|
+
} catch (err) {
|
|
11654
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11655
|
+
return fail2(`Failed to generate fix stories: ${msg}`);
|
|
11656
|
+
}
|
|
11657
|
+
}
|
|
11658
|
+
function addFixStoriesToState(stories) {
|
|
11659
|
+
const newStories = stories.filter((s) => !s.skipped);
|
|
11660
|
+
if (newStories.length === 0) {
|
|
11661
|
+
return ok2(void 0);
|
|
11662
|
+
}
|
|
11663
|
+
const stateResult = getSprintState2();
|
|
11664
|
+
if (!stateResult.success) {
|
|
11665
|
+
return fail2(stateResult.error);
|
|
11666
|
+
}
|
|
11667
|
+
const current = stateResult.data;
|
|
11668
|
+
const updatedStories = { ...current.stories };
|
|
11669
|
+
for (const story of newStories) {
|
|
11670
|
+
updatedStories[story.key] = {
|
|
11671
|
+
status: "backlog",
|
|
11672
|
+
attempts: 0,
|
|
11673
|
+
lastAttempt: null,
|
|
11674
|
+
lastError: null,
|
|
11675
|
+
proofPath: null,
|
|
11676
|
+
acResults: null
|
|
11677
|
+
};
|
|
11678
|
+
}
|
|
11679
|
+
const updatedSprint = computeSprintCounts2(updatedStories);
|
|
11680
|
+
return writeStateAtomic2({
|
|
11681
|
+
...current,
|
|
11682
|
+
sprint: updatedSprint,
|
|
11683
|
+
stories: updatedStories
|
|
11684
|
+
});
|
|
11685
|
+
}
|
|
11686
|
+
|
|
11687
|
+
// src/modules/audit/index.ts
|
|
11688
|
+
async function runAudit2(projectDir) {
|
|
11689
|
+
const start = performance.now();
|
|
11690
|
+
const [
|
|
11691
|
+
obsResult,
|
|
11692
|
+
testResult,
|
|
11693
|
+
docResult,
|
|
11694
|
+
verifyResult,
|
|
11695
|
+
infraResult
|
|
11696
|
+
] = await Promise.all([
|
|
11697
|
+
checkObservability(projectDir),
|
|
11698
|
+
Promise.resolve(checkTesting(projectDir)),
|
|
11699
|
+
Promise.resolve(checkDocumentation(projectDir)),
|
|
11700
|
+
Promise.resolve(checkVerification(projectDir)),
|
|
11701
|
+
Promise.resolve(checkInfrastructure(projectDir))
|
|
11702
|
+
]);
|
|
11703
|
+
const dimensions = {};
|
|
11704
|
+
const allResults = [obsResult, testResult, docResult, verifyResult, infraResult];
|
|
11705
|
+
for (const result of allResults) {
|
|
11706
|
+
if (result.success) {
|
|
11707
|
+
dimensions[result.data.name] = result.data;
|
|
11708
|
+
}
|
|
11709
|
+
}
|
|
11710
|
+
const statuses = Object.values(dimensions).map((d) => d.status);
|
|
11711
|
+
const overallStatus = computeOverallStatus(statuses);
|
|
11712
|
+
const gapCount = Object.values(dimensions).reduce((sum, d) => sum + d.gaps.length, 0);
|
|
11713
|
+
const durationMs = Math.round(performance.now() - start);
|
|
11714
|
+
return ok2({ dimensions, overallStatus, gapCount, durationMs });
|
|
11715
|
+
}
|
|
11716
|
+
function computeOverallStatus(statuses) {
|
|
11717
|
+
if (statuses.includes("fail")) return "fail";
|
|
11718
|
+
if (statuses.includes("warn")) return "warn";
|
|
11719
|
+
return "pass";
|
|
11720
|
+
}
|
|
11721
|
+
|
|
11722
|
+
// src/commands/audit.ts
|
|
11723
|
+
function registerAuditCommand(program) {
|
|
11724
|
+
program.command("audit").description("Check all compliance dimensions and report project health").option("--json", "Output in machine-readable JSON format").option("--fix", "Generate fix stories for every gap found").action(async (opts, cmd) => {
|
|
11725
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
11726
|
+
const isJson = opts.json === true || globalOpts.json === true;
|
|
11727
|
+
const isFix = opts.fix === true;
|
|
11728
|
+
const preconditions = runPreconditions();
|
|
11729
|
+
if (!preconditions.canProceed) {
|
|
11730
|
+
if (isJson) {
|
|
11731
|
+
jsonOutput({
|
|
11732
|
+
status: "fail",
|
|
11733
|
+
message: "Harness not initialized -- run codeharness init first"
|
|
11734
|
+
});
|
|
11735
|
+
} else {
|
|
11736
|
+
fail("Harness not initialized -- run codeharness init first");
|
|
11737
|
+
}
|
|
11738
|
+
process.exitCode = 1;
|
|
11739
|
+
return;
|
|
11740
|
+
}
|
|
11741
|
+
const result = await runAudit2(process.cwd());
|
|
11742
|
+
if (!result.success) {
|
|
11743
|
+
if (isJson) {
|
|
11744
|
+
jsonOutput({ status: "fail", message: result.error });
|
|
11745
|
+
} else {
|
|
11746
|
+
fail(result.error);
|
|
11747
|
+
}
|
|
11748
|
+
process.exitCode = 1;
|
|
11749
|
+
return;
|
|
11750
|
+
}
|
|
11751
|
+
let fixStories;
|
|
11752
|
+
let fixStateError;
|
|
11753
|
+
if (isFix) {
|
|
11754
|
+
if (result.data.gapCount === 0) {
|
|
11755
|
+
if (!isJson) {
|
|
11756
|
+
ok("No gaps found -- nothing to fix");
|
|
11757
|
+
}
|
|
11758
|
+
} else {
|
|
11759
|
+
const fixResult = generateFixStories(result.data);
|
|
11760
|
+
fixStories = fixResult;
|
|
11761
|
+
if (fixResult.success) {
|
|
11762
|
+
const stateResult = addFixStoriesToState(fixResult.data.stories);
|
|
11763
|
+
if (!stateResult.success) {
|
|
11764
|
+
fixStateError = stateResult.error;
|
|
11765
|
+
if (!isJson) {
|
|
11766
|
+
fail(`Failed to update sprint state: ${stateResult.error}`);
|
|
11767
|
+
}
|
|
11768
|
+
}
|
|
11769
|
+
if (!isJson) {
|
|
11770
|
+
info(`Generated ${fixResult.data.created} fix stories (${fixResult.data.skipped} skipped)`);
|
|
11771
|
+
}
|
|
11772
|
+
} else if (!isJson) {
|
|
11773
|
+
fail(fixResult.error);
|
|
11774
|
+
}
|
|
11775
|
+
}
|
|
11776
|
+
}
|
|
11777
|
+
if (isJson) {
|
|
11778
|
+
const jsonData = formatAuditJson(result.data);
|
|
11779
|
+
if (isFix) {
|
|
11780
|
+
if (result.data.gapCount === 0) {
|
|
11781
|
+
jsonData.fixStories = [];
|
|
11782
|
+
} else if (fixStories && fixStories.success) {
|
|
11783
|
+
jsonData.fixStories = fixStories.data.stories.map((s) => ({
|
|
11784
|
+
key: s.key,
|
|
11785
|
+
filePath: s.filePath,
|
|
11786
|
+
gap: s.gap,
|
|
11787
|
+
...s.skipped ? { skipped: true } : {}
|
|
11788
|
+
}));
|
|
11789
|
+
if (fixStateError) {
|
|
11790
|
+
jsonData.fixStateError = fixStateError;
|
|
11791
|
+
}
|
|
11792
|
+
} else if (fixStories && !fixStories.success) {
|
|
11793
|
+
jsonData.fixStories = [];
|
|
11794
|
+
jsonData.fixError = fixStories.error;
|
|
11795
|
+
}
|
|
11796
|
+
}
|
|
11797
|
+
jsonOutput(jsonData);
|
|
11798
|
+
} else if (!isFix || result.data.gapCount > 0) {
|
|
11799
|
+
const lines = formatAuditHuman(result.data);
|
|
11800
|
+
for (const line of lines) {
|
|
11801
|
+
console.log(line);
|
|
11802
|
+
}
|
|
11803
|
+
}
|
|
11804
|
+
if (result.data.overallStatus === "fail") {
|
|
11805
|
+
process.exitCode = 1;
|
|
11806
|
+
}
|
|
11807
|
+
});
|
|
11808
|
+
}
|
|
11809
|
+
|
|
10917
11810
|
// src/index.ts
|
|
10918
|
-
var VERSION = true ? "0.22.
|
|
11811
|
+
var VERSION = true ? "0.22.2" : "0.0.0-dev";
|
|
10919
11812
|
function createProgram() {
|
|
10920
11813
|
const program = new Command();
|
|
10921
11814
|
program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
|
|
@@ -10941,6 +11834,7 @@ function createProgram() {
|
|
|
10941
11834
|
registerValidateCommand(program);
|
|
10942
11835
|
registerProgressCommand(program);
|
|
10943
11836
|
registerObservabilityGateCommand(program);
|
|
11837
|
+
registerAuditCommand(program);
|
|
10944
11838
|
return program;
|
|
10945
11839
|
}
|
|
10946
11840
|
if (!process.env["VITEST"]) {
|