codeharness 0.21.1 → 0.22.1
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 +1231 -124
- package/dist/modules/observability/index.d.ts +101 -29
- package/dist/modules/observability/index.js +240 -7
- 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.
|
|
1944
|
+
var HARNESS_VERSION = true ? "0.22.1" : "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,51 @@ 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 parts = [];
|
|
2878
|
+
if (groups["done"]?.length) {
|
|
2879
|
+
parts.push(`Done: ${fmt(groups["done"], "done")}`);
|
|
2880
|
+
}
|
|
2881
|
+
if (groups["in-progress"]?.length) {
|
|
2882
|
+
parts.push(`This: ${fmt(groups["in-progress"], "in-progress")}`);
|
|
2883
|
+
}
|
|
2884
|
+
if (groups["pending"]?.length) {
|
|
2885
|
+
parts.push(`Next: ${fmt(groups["pending"], "pending")}`);
|
|
2886
|
+
}
|
|
2887
|
+
if (groups["failed"]?.length) {
|
|
2888
|
+
parts.push(`Failed: ${fmt(groups["failed"], "failed")}`);
|
|
2889
|
+
}
|
|
2890
|
+
if (groups["blocked"]?.length) {
|
|
2891
|
+
parts.push(`Blocked: ${fmt(groups["blocked"], "blocked")}`);
|
|
2892
|
+
}
|
|
2893
|
+
return /* @__PURE__ */ jsx(Text, { children: parts.join(" | ") });
|
|
2894
|
+
}
|
|
2895
|
+
var MESSAGE_PREFIX = {
|
|
2896
|
+
ok: "[OK]",
|
|
2897
|
+
warn: "[WARN]",
|
|
2898
|
+
fail: "[FAIL]"
|
|
2899
|
+
};
|
|
2900
|
+
function StoryMessages({ messages }) {
|
|
2901
|
+
if (messages.length === 0) return null;
|
|
2902
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: messages.map((msg, i) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2903
|
+
/* @__PURE__ */ jsx(Text, { children: `${MESSAGE_PREFIX[msg.type]} Story ${msg.key}: ${msg.message}` }),
|
|
2904
|
+
msg.details?.map((d, j) => /* @__PURE__ */ jsx(Text, { children: ` \u2514 ${d}` }, j))
|
|
2905
|
+
] }, i)) });
|
|
2906
|
+
}
|
|
2861
2907
|
function RetryNotice({ info: info2 }) {
|
|
2862
2908
|
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
2863
2909
|
"\u23F3 API retry ",
|
|
@@ -2872,6 +2918,8 @@ function App({
|
|
|
2872
2918
|
}) {
|
|
2873
2919
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2874
2920
|
/* @__PURE__ */ jsx(Header, { info: state.sprintInfo }),
|
|
2921
|
+
/* @__PURE__ */ jsx(StoryBreakdown, { stories: state.stories }),
|
|
2922
|
+
/* @__PURE__ */ jsx(StoryMessages, { messages: state.messages }),
|
|
2875
2923
|
/* @__PURE__ */ jsx(CompletedTools, { tools: state.completedTools }),
|
|
2876
2924
|
state.activeTool && /* @__PURE__ */ jsx(ActiveTool, { name: state.activeTool.name }),
|
|
2877
2925
|
state.lastThought && /* @__PURE__ */ jsx(LastThought, { text: state.lastThought }),
|
|
@@ -2886,6 +2934,10 @@ var noopHandle = {
|
|
|
2886
2934
|
},
|
|
2887
2935
|
updateSprintState() {
|
|
2888
2936
|
},
|
|
2937
|
+
updateStories() {
|
|
2938
|
+
},
|
|
2939
|
+
addMessage() {
|
|
2940
|
+
},
|
|
2889
2941
|
cleanup() {
|
|
2890
2942
|
}
|
|
2891
2943
|
};
|
|
@@ -2896,6 +2948,8 @@ function startRenderer(options) {
|
|
|
2896
2948
|
}
|
|
2897
2949
|
let state = {
|
|
2898
2950
|
sprintInfo: options?.sprintState ?? null,
|
|
2951
|
+
stories: [],
|
|
2952
|
+
messages: [],
|
|
2899
2953
|
completedTools: [],
|
|
2900
2954
|
activeTool: null,
|
|
2901
2955
|
activeToolArgs: "",
|
|
@@ -2978,7 +3032,17 @@ function startRenderer(options) {
|
|
|
2978
3032
|
state.sprintInfo = sprintState ?? null;
|
|
2979
3033
|
rerender();
|
|
2980
3034
|
}
|
|
2981
|
-
|
|
3035
|
+
function updateStories(stories) {
|
|
3036
|
+
if (cleaned) return;
|
|
3037
|
+
state.stories = [...stories];
|
|
3038
|
+
rerender();
|
|
3039
|
+
}
|
|
3040
|
+
function addMessage(msg) {
|
|
3041
|
+
if (cleaned) return;
|
|
3042
|
+
state.messages = [...state.messages, msg];
|
|
3043
|
+
rerender();
|
|
3044
|
+
}
|
|
3045
|
+
return { update, updateSprintState, updateStories, addMessage, cleanup };
|
|
2982
3046
|
}
|
|
2983
3047
|
|
|
2984
3048
|
// src/modules/sprint/state.ts
|
|
@@ -3864,21 +3928,8 @@ function clearRunProgress2() {
|
|
|
3864
3928
|
return clearRunProgress();
|
|
3865
3929
|
}
|
|
3866
3930
|
|
|
3867
|
-
// src/
|
|
3868
|
-
var SPRINT_STATUS_REL = "_bmad-output/implementation-artifacts/sprint-status.yaml";
|
|
3931
|
+
// src/lib/run-helpers.ts
|
|
3869
3932
|
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
3933
|
function countStories(statuses) {
|
|
3883
3934
|
let total = 0;
|
|
3884
3935
|
let ready = 0;
|
|
@@ -3919,6 +3970,108 @@ function buildSpawnArgs(opts) {
|
|
|
3919
3970
|
}
|
|
3920
3971
|
return args;
|
|
3921
3972
|
}
|
|
3973
|
+
function formatElapsed(ms) {
|
|
3974
|
+
const totalMinutes = Math.max(0, Math.floor(ms / 6e4));
|
|
3975
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
3976
|
+
const minutes = totalMinutes % 60;
|
|
3977
|
+
if (hours > 0) {
|
|
3978
|
+
return `${hours}h${minutes}m`;
|
|
3979
|
+
}
|
|
3980
|
+
return `${totalMinutes}m`;
|
|
3981
|
+
}
|
|
3982
|
+
function mapSprintStatus(status) {
|
|
3983
|
+
switch (status) {
|
|
3984
|
+
case "done":
|
|
3985
|
+
return "done";
|
|
3986
|
+
case "in-progress":
|
|
3987
|
+
case "review":
|
|
3988
|
+
case "verifying":
|
|
3989
|
+
return "in-progress";
|
|
3990
|
+
case "backlog":
|
|
3991
|
+
case "ready-for-dev":
|
|
3992
|
+
return "pending";
|
|
3993
|
+
case "failed":
|
|
3994
|
+
return "failed";
|
|
3995
|
+
case "blocked":
|
|
3996
|
+
case "exhausted":
|
|
3997
|
+
return "blocked";
|
|
3998
|
+
default:
|
|
3999
|
+
return "pending";
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
function mapSprintStatuses(statuses) {
|
|
4003
|
+
const entries = [];
|
|
4004
|
+
for (const [key, status] of Object.entries(statuses)) {
|
|
4005
|
+
if (!STORY_KEY_PATTERN.test(key)) continue;
|
|
4006
|
+
if (status === "optional") continue;
|
|
4007
|
+
entries.push({ key, status: mapSprintStatus(status) });
|
|
4008
|
+
}
|
|
4009
|
+
return entries;
|
|
4010
|
+
}
|
|
4011
|
+
var ANSI_ESCAPE = /\x1b\[[0-9;]*m/g;
|
|
4012
|
+
var TIMESTAMP_PREFIX = /^\[[\d-]+\s[\d:]+\]\s*/;
|
|
4013
|
+
var SUCCESS_STORY = /\[SUCCESS\]\s+Story\s+([\w-]+):\s+DONE(.*)/;
|
|
4014
|
+
var WARN_STORY_RETRY = /\[WARN\]\s+Story\s+([\w-]+)\s+exceeded retry limit/;
|
|
4015
|
+
var WARN_STORY_RETRYING = /\[WARN\]\s+Story\s+([\w-]+)\s+.*retry\s+(\d+)\/(\d+)/;
|
|
4016
|
+
var ERROR_LINE = /\[ERROR\]\s+(.+)/;
|
|
4017
|
+
function parseRalphMessage(rawLine) {
|
|
4018
|
+
const clean = rawLine.replace(ANSI_ESCAPE, "").replace(TIMESTAMP_PREFIX, "").trim();
|
|
4019
|
+
if (clean.length === 0) return null;
|
|
4020
|
+
const success = SUCCESS_STORY.exec(clean);
|
|
4021
|
+
if (success) {
|
|
4022
|
+
const key = success[1];
|
|
4023
|
+
const rest = success[2].trim().replace(/^—\s*/, "");
|
|
4024
|
+
return {
|
|
4025
|
+
type: "ok",
|
|
4026
|
+
key,
|
|
4027
|
+
message: rest ? `DONE \u2014 ${rest}` : "DONE"
|
|
4028
|
+
};
|
|
4029
|
+
}
|
|
4030
|
+
const retryExceeded = WARN_STORY_RETRY.exec(clean);
|
|
4031
|
+
if (retryExceeded) {
|
|
4032
|
+
return {
|
|
4033
|
+
type: "fail",
|
|
4034
|
+
key: retryExceeded[1],
|
|
4035
|
+
message: "exceeded retry limit"
|
|
4036
|
+
};
|
|
4037
|
+
}
|
|
4038
|
+
const retrying = WARN_STORY_RETRYING.exec(clean);
|
|
4039
|
+
if (retrying) {
|
|
4040
|
+
return {
|
|
4041
|
+
type: "warn",
|
|
4042
|
+
key: retrying[1],
|
|
4043
|
+
message: `retry ${retrying[2]}/${retrying[3]}`
|
|
4044
|
+
};
|
|
4045
|
+
}
|
|
4046
|
+
const errorMatch = ERROR_LINE.exec(clean);
|
|
4047
|
+
if (errorMatch) {
|
|
4048
|
+
const keyMatch = errorMatch[1].match(/Story\s+([\w-]+)/);
|
|
4049
|
+
if (keyMatch) {
|
|
4050
|
+
return {
|
|
4051
|
+
type: "fail",
|
|
4052
|
+
key: keyMatch[1],
|
|
4053
|
+
message: errorMatch[1].trim()
|
|
4054
|
+
};
|
|
4055
|
+
}
|
|
4056
|
+
return null;
|
|
4057
|
+
}
|
|
4058
|
+
return null;
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
// src/commands/run.ts
|
|
4062
|
+
var SPRINT_STATUS_REL = "_bmad-output/implementation-artifacts/sprint-status.yaml";
|
|
4063
|
+
function resolveRalphPath() {
|
|
4064
|
+
const currentFile = fileURLToPath2(import.meta.url);
|
|
4065
|
+
const currentDir = dirname4(currentFile);
|
|
4066
|
+
let root = dirname4(currentDir);
|
|
4067
|
+
if (root.endsWith("/src") || root.endsWith("\\src")) {
|
|
4068
|
+
root = dirname4(root);
|
|
4069
|
+
}
|
|
4070
|
+
return join13(root, "ralph", "ralph.sh");
|
|
4071
|
+
}
|
|
4072
|
+
function resolvePluginDir() {
|
|
4073
|
+
return join13(process.cwd(), ".claude");
|
|
4074
|
+
}
|
|
3922
4075
|
function registerRunCommand(program) {
|
|
3923
4076
|
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
4077
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -3999,6 +4152,7 @@ function registerRunCommand(program) {
|
|
|
3999
4152
|
const quiet = options.quiet;
|
|
4000
4153
|
const rendererHandle = startRenderer({ quiet });
|
|
4001
4154
|
let sprintStateInterval = null;
|
|
4155
|
+
const sessionStartTime = Date.now();
|
|
4002
4156
|
try {
|
|
4003
4157
|
const initialState = getSprintState2();
|
|
4004
4158
|
if (initialState.success) {
|
|
@@ -4007,17 +4161,23 @@ function registerRunCommand(program) {
|
|
|
4007
4161
|
storyKey: s.run.currentStory ?? "",
|
|
4008
4162
|
phase: s.run.currentPhase ?? "",
|
|
4009
4163
|
done: s.sprint.done,
|
|
4010
|
-
total: s.sprint.total
|
|
4164
|
+
total: s.sprint.total,
|
|
4165
|
+
elapsed: formatElapsed(Date.now() - sessionStartTime)
|
|
4011
4166
|
};
|
|
4012
4167
|
rendererHandle.updateSprintState(sprintInfo);
|
|
4013
4168
|
}
|
|
4169
|
+
const initialStatuses = readSprintStatus(projectDir);
|
|
4170
|
+
const initialStories = mapSprintStatuses(initialStatuses);
|
|
4171
|
+
if (initialStories.length > 0) {
|
|
4172
|
+
rendererHandle.updateStories(initialStories);
|
|
4173
|
+
}
|
|
4014
4174
|
const child = spawn("bash", args, {
|
|
4015
4175
|
stdio: quiet ? "ignore" : ["inherit", "pipe", "pipe"],
|
|
4016
4176
|
cwd: projectDir,
|
|
4017
4177
|
env
|
|
4018
4178
|
});
|
|
4019
4179
|
if (!quiet && child.stdout && child.stderr) {
|
|
4020
|
-
const makeLineHandler = () => {
|
|
4180
|
+
const makeLineHandler = (opts) => {
|
|
4021
4181
|
let partial = "";
|
|
4022
4182
|
const decoder = new StringDecoder("utf8");
|
|
4023
4183
|
return (data) => {
|
|
@@ -4030,11 +4190,17 @@ function registerRunCommand(program) {
|
|
|
4030
4190
|
if (event) {
|
|
4031
4191
|
rendererHandle.update(event);
|
|
4032
4192
|
}
|
|
4193
|
+
if (opts?.parseRalph) {
|
|
4194
|
+
const msg = parseRalphMessage(line);
|
|
4195
|
+
if (msg) {
|
|
4196
|
+
rendererHandle.addMessage(msg);
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4033
4199
|
}
|
|
4034
4200
|
};
|
|
4035
4201
|
};
|
|
4036
4202
|
child.stdout.on("data", makeLineHandler());
|
|
4037
|
-
child.stderr.on("data", makeLineHandler());
|
|
4203
|
+
child.stderr.on("data", makeLineHandler({ parseRalph: true }));
|
|
4038
4204
|
sprintStateInterval = setInterval(() => {
|
|
4039
4205
|
try {
|
|
4040
4206
|
const stateResult = getSprintState2();
|
|
@@ -4044,10 +4210,14 @@ function registerRunCommand(program) {
|
|
|
4044
4210
|
storyKey: s.run.currentStory ?? "",
|
|
4045
4211
|
phase: s.run.currentPhase ?? "",
|
|
4046
4212
|
done: s.sprint.done,
|
|
4047
|
-
total: s.sprint.total
|
|
4213
|
+
total: s.sprint.total,
|
|
4214
|
+
elapsed: formatElapsed(Date.now() - sessionStartTime)
|
|
4048
4215
|
};
|
|
4049
4216
|
rendererHandle.updateSprintState(sprintInfo);
|
|
4050
4217
|
}
|
|
4218
|
+
const currentStatuses = readSprintStatus(projectDir);
|
|
4219
|
+
const storyEntries = mapSprintStatuses(currentStatuses);
|
|
4220
|
+
rendererHandle.updateStories(storyEntries);
|
|
4051
4221
|
} catch {
|
|
4052
4222
|
}
|
|
4053
4223
|
}, 5e3);
|
|
@@ -4121,7 +4291,7 @@ function registerRunCommand(program) {
|
|
|
4121
4291
|
|
|
4122
4292
|
// src/commands/verify.ts
|
|
4123
4293
|
import { existsSync as existsSync25, readFileSync as readFileSync23 } from "fs";
|
|
4124
|
-
import { join as
|
|
4294
|
+
import { join as join22 } from "path";
|
|
4125
4295
|
|
|
4126
4296
|
// src/modules/verify/index.ts
|
|
4127
4297
|
import { readFileSync as readFileSync22 } from "fs";
|
|
@@ -5174,15 +5344,414 @@ function parseObservabilityGaps(proofContent) {
|
|
|
5174
5344
|
// src/modules/observability/analyzer.ts
|
|
5175
5345
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
5176
5346
|
import { join as join16 } from "path";
|
|
5347
|
+
var DEFAULT_RULES_DIR = "patches/observability/";
|
|
5348
|
+
var DEFAULT_TIMEOUT = 6e4;
|
|
5349
|
+
var FUNCTION_NO_LOG_RULE = "function-no-debug-log";
|
|
5350
|
+
var CATCH_WITHOUT_LOGGING_RULE = "catch-without-logging";
|
|
5351
|
+
var ERROR_PATH_NO_LOG_RULE = "error-path-no-log";
|
|
5352
|
+
function matchesRule(gapType, ruleName) {
|
|
5353
|
+
return gapType === ruleName || gapType.endsWith(`.${ruleName}`);
|
|
5354
|
+
}
|
|
5355
|
+
function analyze(projectDir, config) {
|
|
5356
|
+
if (!projectDir || typeof projectDir !== "string") {
|
|
5357
|
+
return fail2("projectDir is required and must be a non-empty string");
|
|
5358
|
+
}
|
|
5359
|
+
const tool = config?.tool ?? "semgrep";
|
|
5360
|
+
if (tool !== "semgrep") {
|
|
5361
|
+
return fail2(`Unsupported analyzer tool: ${tool}`);
|
|
5362
|
+
}
|
|
5363
|
+
if (!checkSemgrepInstalled()) {
|
|
5364
|
+
return ok2({
|
|
5365
|
+
tool: "semgrep",
|
|
5366
|
+
gaps: [],
|
|
5367
|
+
summary: {
|
|
5368
|
+
totalFunctions: 0,
|
|
5369
|
+
functionsWithLogs: 0,
|
|
5370
|
+
errorHandlersWithoutLogs: 0,
|
|
5371
|
+
coveragePercent: 0,
|
|
5372
|
+
levelDistribution: {}
|
|
5373
|
+
},
|
|
5374
|
+
skipped: true,
|
|
5375
|
+
skipReason: "static analysis skipped -- install semgrep"
|
|
5376
|
+
});
|
|
5377
|
+
}
|
|
5378
|
+
const rulesDir = config?.rulesDir ?? DEFAULT_RULES_DIR;
|
|
5379
|
+
const timeout = config?.timeout ?? DEFAULT_TIMEOUT;
|
|
5380
|
+
const fullRulesDir = join16(projectDir, rulesDir);
|
|
5381
|
+
const rawResult = runSemgrep(projectDir, fullRulesDir, timeout);
|
|
5382
|
+
if (!rawResult.success) {
|
|
5383
|
+
return fail2(rawResult.error);
|
|
5384
|
+
}
|
|
5385
|
+
const gaps = parseSemgrepOutput(rawResult.data);
|
|
5386
|
+
const summaryOpts = config?.totalFunctions != null ? { totalFunctions: config.totalFunctions } : void 0;
|
|
5387
|
+
const summary = computeSummary(gaps, summaryOpts);
|
|
5388
|
+
return ok2({
|
|
5389
|
+
tool: "semgrep",
|
|
5390
|
+
gaps,
|
|
5391
|
+
summary
|
|
5392
|
+
});
|
|
5393
|
+
}
|
|
5394
|
+
function checkSemgrepInstalled() {
|
|
5395
|
+
try {
|
|
5396
|
+
execFileSync8("semgrep", ["--version"], {
|
|
5397
|
+
encoding: "utf-8",
|
|
5398
|
+
timeout: 5e3,
|
|
5399
|
+
stdio: "pipe"
|
|
5400
|
+
});
|
|
5401
|
+
return true;
|
|
5402
|
+
} catch {
|
|
5403
|
+
return false;
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
5406
|
+
function runSemgrep(projectDir, rulesDir, timeout = DEFAULT_TIMEOUT) {
|
|
5407
|
+
try {
|
|
5408
|
+
const stdout = execFileSync8(
|
|
5409
|
+
"semgrep",
|
|
5410
|
+
["scan", "--config", rulesDir, "--json", projectDir],
|
|
5411
|
+
{ encoding: "utf-8", timeout, stdio: ["pipe", "pipe", "pipe"] }
|
|
5412
|
+
);
|
|
5413
|
+
const parsed = JSON.parse(stdout);
|
|
5414
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.results)) {
|
|
5415
|
+
return fail2("Semgrep scan returned invalid JSON: missing results array");
|
|
5416
|
+
}
|
|
5417
|
+
return ok2(parsed);
|
|
5418
|
+
} catch (error) {
|
|
5419
|
+
return fail2(`Semgrep scan failed: ${String(error)}`);
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
function parseSemgrepOutput(raw) {
|
|
5423
|
+
if (!raw.results || !Array.isArray(raw.results)) {
|
|
5424
|
+
return [];
|
|
5425
|
+
}
|
|
5426
|
+
return raw.results.map((r) => ({
|
|
5427
|
+
file: r.path,
|
|
5428
|
+
line: r.start.line,
|
|
5429
|
+
type: r.check_id,
|
|
5430
|
+
description: r.extra.message,
|
|
5431
|
+
severity: normalizeSeverity(r.extra.severity)
|
|
5432
|
+
}));
|
|
5433
|
+
}
|
|
5434
|
+
function computeSummary(gaps, opts) {
|
|
5435
|
+
const functionsWithoutLogs = gaps.filter(
|
|
5436
|
+
(g) => matchesRule(g.type, FUNCTION_NO_LOG_RULE)
|
|
5437
|
+
).length;
|
|
5438
|
+
const errorHandlersWithoutLogs = gaps.filter(
|
|
5439
|
+
(g) => matchesRule(g.type, CATCH_WITHOUT_LOGGING_RULE) || matchesRule(g.type, ERROR_PATH_NO_LOG_RULE)
|
|
5440
|
+
).length;
|
|
5441
|
+
const totalFunctions = opts?.totalFunctions ?? functionsWithoutLogs;
|
|
5442
|
+
const functionsWithLogs = totalFunctions - functionsWithoutLogs;
|
|
5443
|
+
const coveragePercent = totalFunctions === 0 ? 100 : Math.round(functionsWithLogs / totalFunctions * 100 * 100) / 100;
|
|
5444
|
+
const levelDistribution = {};
|
|
5445
|
+
for (const gap2 of gaps) {
|
|
5446
|
+
levelDistribution[gap2.severity] = (levelDistribution[gap2.severity] ?? 0) + 1;
|
|
5447
|
+
}
|
|
5448
|
+
return {
|
|
5449
|
+
totalFunctions,
|
|
5450
|
+
functionsWithLogs,
|
|
5451
|
+
errorHandlersWithoutLogs,
|
|
5452
|
+
coveragePercent,
|
|
5453
|
+
levelDistribution
|
|
5454
|
+
};
|
|
5455
|
+
}
|
|
5456
|
+
function normalizeSeverity(severity) {
|
|
5457
|
+
const lower = severity.toLowerCase();
|
|
5458
|
+
if (lower === "error") return "error";
|
|
5459
|
+
if (lower === "warning") return "warning";
|
|
5460
|
+
return "info";
|
|
5461
|
+
}
|
|
5177
5462
|
|
|
5178
5463
|
// src/modules/observability/coverage.ts
|
|
5179
5464
|
import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, renameSync as renameSync2, existsSync as existsSync21 } from "fs";
|
|
5180
5465
|
import { join as join17 } from "path";
|
|
5466
|
+
var STATE_FILE2 = "sprint-state.json";
|
|
5467
|
+
var DEFAULT_STATIC_TARGET = 80;
|
|
5468
|
+
function defaultCoverageState() {
|
|
5469
|
+
return {
|
|
5470
|
+
static: {
|
|
5471
|
+
coveragePercent: 0,
|
|
5472
|
+
lastScanTimestamp: "",
|
|
5473
|
+
history: []
|
|
5474
|
+
},
|
|
5475
|
+
targets: {
|
|
5476
|
+
staticTarget: DEFAULT_STATIC_TARGET
|
|
5477
|
+
}
|
|
5478
|
+
};
|
|
5479
|
+
}
|
|
5480
|
+
function readStateFile(projectDir) {
|
|
5481
|
+
const fp = join17(projectDir, STATE_FILE2);
|
|
5482
|
+
if (!existsSync21(fp)) {
|
|
5483
|
+
return ok2({});
|
|
5484
|
+
}
|
|
5485
|
+
try {
|
|
5486
|
+
const raw = readFileSync18(fp, "utf-8");
|
|
5487
|
+
const parsed = JSON.parse(raw);
|
|
5488
|
+
return ok2(parsed);
|
|
5489
|
+
} catch (err) {
|
|
5490
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5491
|
+
return fail2(`Failed to read ${STATE_FILE2}: ${msg}`);
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
function readCoverageState(projectDir) {
|
|
5495
|
+
if (!projectDir || typeof projectDir !== "string") {
|
|
5496
|
+
return fail2("projectDir is required and must be a non-empty string");
|
|
5497
|
+
}
|
|
5498
|
+
const stateResult = readStateFile(projectDir);
|
|
5499
|
+
if (!stateResult.success) {
|
|
5500
|
+
return fail2(stateResult.error);
|
|
5501
|
+
}
|
|
5502
|
+
return ok2(extractCoverageState(stateResult.data));
|
|
5503
|
+
}
|
|
5504
|
+
function extractCoverageState(state) {
|
|
5505
|
+
const obs = state.observability;
|
|
5506
|
+
if (!obs) {
|
|
5507
|
+
return defaultCoverageState();
|
|
5508
|
+
}
|
|
5509
|
+
const staticSection = obs.static;
|
|
5510
|
+
const targets = obs.targets;
|
|
5511
|
+
const runtimeSection = obs.runtime;
|
|
5512
|
+
const runtime = runtimeSection && typeof runtimeSection.coveragePercent === "number" ? {
|
|
5513
|
+
coveragePercent: runtimeSection.coveragePercent,
|
|
5514
|
+
lastValidationTimestamp: typeof runtimeSection.lastValidationTimestamp === "string" ? runtimeSection.lastValidationTimestamp : "",
|
|
5515
|
+
modulesWithTelemetry: typeof runtimeSection.modulesWithTelemetry === "number" ? runtimeSection.modulesWithTelemetry : 0,
|
|
5516
|
+
totalModules: typeof runtimeSection.totalModules === "number" ? runtimeSection.totalModules : 0,
|
|
5517
|
+
telemetryDetected: typeof runtimeSection.telemetryDetected === "boolean" ? runtimeSection.telemetryDetected : false
|
|
5518
|
+
} : void 0;
|
|
5519
|
+
const parsedGaps = parseGapArray(staticSection?.gaps);
|
|
5520
|
+
const result = {
|
|
5521
|
+
static: {
|
|
5522
|
+
coveragePercent: typeof staticSection?.coveragePercent === "number" ? staticSection.coveragePercent : 0,
|
|
5523
|
+
lastScanTimestamp: typeof staticSection?.lastScanTimestamp === "string" ? staticSection.lastScanTimestamp : "",
|
|
5524
|
+
history: Array.isArray(staticSection?.history) ? staticSection.history.filter(
|
|
5525
|
+
(entry) => typeof entry === "object" && entry !== null && typeof entry.coveragePercent === "number" && typeof entry.timestamp === "string"
|
|
5526
|
+
) : [],
|
|
5527
|
+
...parsedGaps.length > 0 ? { gaps: parsedGaps } : {}
|
|
5528
|
+
},
|
|
5529
|
+
targets: {
|
|
5530
|
+
staticTarget: typeof targets?.staticTarget === "number" ? targets.staticTarget : DEFAULT_STATIC_TARGET,
|
|
5531
|
+
...typeof targets?.runtimeTarget === "number" ? { runtimeTarget: targets.runtimeTarget } : {}
|
|
5532
|
+
},
|
|
5533
|
+
...runtime ? { runtime } : {}
|
|
5534
|
+
};
|
|
5535
|
+
return result;
|
|
5536
|
+
}
|
|
5537
|
+
function parseGapArray(raw) {
|
|
5538
|
+
if (!Array.isArray(raw)) return [];
|
|
5539
|
+
return raw.filter((g) => {
|
|
5540
|
+
if (typeof g !== "object" || g === null) return false;
|
|
5541
|
+
const r = g;
|
|
5542
|
+
return typeof r.file === "string" && typeof r.line === "number" && typeof r.type === "string" && typeof r.description === "string";
|
|
5543
|
+
}).map((g) => ({
|
|
5544
|
+
file: g.file,
|
|
5545
|
+
line: g.line,
|
|
5546
|
+
type: g.type,
|
|
5547
|
+
description: g.description,
|
|
5548
|
+
severity: g.severity === "error" || g.severity === "warning" ? g.severity : "info"
|
|
5549
|
+
}));
|
|
5550
|
+
}
|
|
5181
5551
|
|
|
5182
5552
|
// src/modules/observability/runtime-coverage.ts
|
|
5183
5553
|
import { readFileSync as readFileSync19, writeFileSync as writeFileSync13, renameSync as renameSync3, existsSync as existsSync22 } from "fs";
|
|
5184
5554
|
import { join as join18 } from "path";
|
|
5185
5555
|
|
|
5556
|
+
// src/modules/observability/coverage-gate.ts
|
|
5557
|
+
var DEFAULT_STATIC_TARGET2 = 80;
|
|
5558
|
+
var DEFAULT_RUNTIME_TARGET = 60;
|
|
5559
|
+
function checkObservabilityCoverageGate(projectDir, overrides) {
|
|
5560
|
+
if (!projectDir || typeof projectDir !== "string") {
|
|
5561
|
+
return fail2("projectDir is required and must be a non-empty string");
|
|
5562
|
+
}
|
|
5563
|
+
const stateResult = readCoverageState(projectDir);
|
|
5564
|
+
if (!stateResult.success) {
|
|
5565
|
+
return fail2(stateResult.error);
|
|
5566
|
+
}
|
|
5567
|
+
const state = stateResult.data;
|
|
5568
|
+
const staticTarget = overrides?.staticTarget ?? state.targets.staticTarget ?? DEFAULT_STATIC_TARGET2;
|
|
5569
|
+
const runtimeTarget = overrides?.runtimeTarget ?? state.targets.runtimeTarget ?? DEFAULT_RUNTIME_TARGET;
|
|
5570
|
+
const staticCurrent = state.static.coveragePercent;
|
|
5571
|
+
const staticMet = staticCurrent >= staticTarget;
|
|
5572
|
+
const staticResult = {
|
|
5573
|
+
met: staticMet,
|
|
5574
|
+
current: staticCurrent,
|
|
5575
|
+
target: staticTarget,
|
|
5576
|
+
gap: staticMet ? 0 : staticTarget - staticCurrent
|
|
5577
|
+
};
|
|
5578
|
+
let runtimeResult = null;
|
|
5579
|
+
if (state.runtime) {
|
|
5580
|
+
const runtimeCurrent = state.runtime.coveragePercent;
|
|
5581
|
+
const runtimeMet = runtimeCurrent >= runtimeTarget;
|
|
5582
|
+
runtimeResult = {
|
|
5583
|
+
met: runtimeMet,
|
|
5584
|
+
current: runtimeCurrent,
|
|
5585
|
+
target: runtimeTarget,
|
|
5586
|
+
gap: runtimeMet ? 0 : runtimeTarget - runtimeCurrent
|
|
5587
|
+
};
|
|
5588
|
+
}
|
|
5589
|
+
const passed = staticResult.met && (runtimeResult === null || runtimeResult.met);
|
|
5590
|
+
const gapSummary = state.static.gaps ? [...state.static.gaps] : [];
|
|
5591
|
+
return ok2({ passed, staticResult, runtimeResult, gapSummary });
|
|
5592
|
+
}
|
|
5593
|
+
|
|
5594
|
+
// src/modules/observability/runtime-validator.ts
|
|
5595
|
+
import { execSync as execSync3 } from "child_process";
|
|
5596
|
+
import { readdirSync as readdirSync4, statSync as statSync2 } from "fs";
|
|
5597
|
+
import { join as join19 } from "path";
|
|
5598
|
+
var DEFAULT_CONFIG = {
|
|
5599
|
+
testCommand: "npm test",
|
|
5600
|
+
otlpEndpoint: "http://localhost:4318",
|
|
5601
|
+
queryEndpoint: "http://localhost:9428",
|
|
5602
|
+
timeoutMs: 12e4
|
|
5603
|
+
};
|
|
5604
|
+
var HEALTH_TIMEOUT_MS = 3e3;
|
|
5605
|
+
var QUERY_TIMEOUT_MS = 3e4;
|
|
5606
|
+
async function validateRuntime(projectDir, config) {
|
|
5607
|
+
if (!projectDir || typeof projectDir !== "string") {
|
|
5608
|
+
return fail2("projectDir is required and must be a non-empty string");
|
|
5609
|
+
}
|
|
5610
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
5611
|
+
if (/[;&|`$(){}!#]/.test(cfg.testCommand)) {
|
|
5612
|
+
return fail2(`testCommand contains disallowed shell metacharacters: ${cfg.testCommand}`);
|
|
5613
|
+
}
|
|
5614
|
+
const healthy = await checkBackendHealth(cfg.queryEndpoint);
|
|
5615
|
+
if (!healthy) {
|
|
5616
|
+
const modules2 = discoverModules(projectDir);
|
|
5617
|
+
const entries2 = modules2.map((m) => ({
|
|
5618
|
+
moduleName: m,
|
|
5619
|
+
telemetryDetected: false,
|
|
5620
|
+
eventCount: 0
|
|
5621
|
+
}));
|
|
5622
|
+
return ok2({
|
|
5623
|
+
entries: entries2,
|
|
5624
|
+
totalModules: modules2.length,
|
|
5625
|
+
modulesWithTelemetry: 0,
|
|
5626
|
+
coveragePercent: 0,
|
|
5627
|
+
skipped: true,
|
|
5628
|
+
skipReason: "runtime validation skipped -- observability stack not available"
|
|
5629
|
+
});
|
|
5630
|
+
}
|
|
5631
|
+
const startTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
5632
|
+
try {
|
|
5633
|
+
execSync3(cfg.testCommand, {
|
|
5634
|
+
cwd: projectDir,
|
|
5635
|
+
timeout: cfg.timeoutMs,
|
|
5636
|
+
env: {
|
|
5637
|
+
...process.env,
|
|
5638
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: cfg.otlpEndpoint
|
|
5639
|
+
},
|
|
5640
|
+
stdio: "pipe"
|
|
5641
|
+
});
|
|
5642
|
+
} catch (err) {
|
|
5643
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5644
|
+
return fail2(`Test command failed: ${msg}`);
|
|
5645
|
+
}
|
|
5646
|
+
const endTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
5647
|
+
const eventsResult = await queryTelemetryEvents(cfg.queryEndpoint, startTime, endTime);
|
|
5648
|
+
if (!eventsResult.success) {
|
|
5649
|
+
return fail2(eventsResult.error);
|
|
5650
|
+
}
|
|
5651
|
+
const modules = discoverModules(projectDir);
|
|
5652
|
+
const entries = mapEventsToModules(eventsResult.data, projectDir, modules);
|
|
5653
|
+
const modulesWithTelemetry = entries.filter((e) => e.telemetryDetected).length;
|
|
5654
|
+
const totalModules = entries.length;
|
|
5655
|
+
const coveragePercent = totalModules === 0 ? 0 : modulesWithTelemetry / totalModules * 100;
|
|
5656
|
+
return ok2({
|
|
5657
|
+
entries,
|
|
5658
|
+
totalModules,
|
|
5659
|
+
modulesWithTelemetry,
|
|
5660
|
+
coveragePercent,
|
|
5661
|
+
skipped: false
|
|
5662
|
+
});
|
|
5663
|
+
}
|
|
5664
|
+
async function checkBackendHealth(queryEndpoint) {
|
|
5665
|
+
try {
|
|
5666
|
+
const response = await fetch(`${queryEndpoint}/health`, {
|
|
5667
|
+
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
|
|
5668
|
+
});
|
|
5669
|
+
return response.ok;
|
|
5670
|
+
} catch {
|
|
5671
|
+
return false;
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
5674
|
+
async function queryTelemetryEvents(queryEndpoint, startTime, endTime) {
|
|
5675
|
+
let url;
|
|
5676
|
+
try {
|
|
5677
|
+
url = new URL("/select/logsql/query", queryEndpoint);
|
|
5678
|
+
} catch {
|
|
5679
|
+
return fail2(`Invalid queryEndpoint URL: ${queryEndpoint}`);
|
|
5680
|
+
}
|
|
5681
|
+
url.searchParams.set("query", "*");
|
|
5682
|
+
url.searchParams.set("start", startTime);
|
|
5683
|
+
url.searchParams.set("end", endTime);
|
|
5684
|
+
url.searchParams.set("limit", "1000");
|
|
5685
|
+
try {
|
|
5686
|
+
const response = await fetch(url.toString(), {
|
|
5687
|
+
signal: AbortSignal.timeout(QUERY_TIMEOUT_MS)
|
|
5688
|
+
});
|
|
5689
|
+
if (!response.ok) {
|
|
5690
|
+
return fail2(`VictoriaLogs returned ${response.status}`);
|
|
5691
|
+
}
|
|
5692
|
+
const text = await response.text();
|
|
5693
|
+
const events = parseLogEvents(text);
|
|
5694
|
+
return ok2(events);
|
|
5695
|
+
} catch (err) {
|
|
5696
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5697
|
+
return fail2(`Failed to query telemetry events: ${msg}`);
|
|
5698
|
+
}
|
|
5699
|
+
}
|
|
5700
|
+
function mapEventsToModules(events, projectDir, modules) {
|
|
5701
|
+
void projectDir;
|
|
5702
|
+
const moduleList = modules ?? [];
|
|
5703
|
+
const moduleCounts = /* @__PURE__ */ new Map();
|
|
5704
|
+
for (const mod of moduleList) {
|
|
5705
|
+
moduleCounts.set(mod, 0);
|
|
5706
|
+
}
|
|
5707
|
+
for (const event of events) {
|
|
5708
|
+
for (const mod of moduleList) {
|
|
5709
|
+
if (event.source.includes(mod) || event.message.includes(mod)) {
|
|
5710
|
+
moduleCounts.set(mod, (moduleCounts.get(mod) ?? 0) + 1);
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
return moduleList.map((mod) => {
|
|
5715
|
+
const count = moduleCounts.get(mod) ?? 0;
|
|
5716
|
+
return {
|
|
5717
|
+
moduleName: mod,
|
|
5718
|
+
telemetryDetected: count > 0,
|
|
5719
|
+
eventCount: count
|
|
5720
|
+
};
|
|
5721
|
+
});
|
|
5722
|
+
}
|
|
5723
|
+
function discoverModules(projectDir) {
|
|
5724
|
+
const srcDir = join19(projectDir, "src");
|
|
5725
|
+
try {
|
|
5726
|
+
return readdirSync4(srcDir).filter((name) => {
|
|
5727
|
+
try {
|
|
5728
|
+
return statSync2(join19(srcDir, name)).isDirectory();
|
|
5729
|
+
} catch {
|
|
5730
|
+
return false;
|
|
5731
|
+
}
|
|
5732
|
+
});
|
|
5733
|
+
} catch {
|
|
5734
|
+
return [];
|
|
5735
|
+
}
|
|
5736
|
+
}
|
|
5737
|
+
function parseLogEvents(text) {
|
|
5738
|
+
if (!text.trim()) return [];
|
|
5739
|
+
const lines = text.trim().split("\n");
|
|
5740
|
+
const events = [];
|
|
5741
|
+
for (const line of lines) {
|
|
5742
|
+
try {
|
|
5743
|
+
const raw = JSON.parse(line);
|
|
5744
|
+
events.push({
|
|
5745
|
+
timestamp: String(raw._time ?? raw.timestamp ?? ""),
|
|
5746
|
+
message: String(raw._msg ?? raw.message ?? ""),
|
|
5747
|
+
source: String(raw.source ?? raw._source ?? raw.service ?? "")
|
|
5748
|
+
});
|
|
5749
|
+
} catch {
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
return events;
|
|
5753
|
+
}
|
|
5754
|
+
|
|
5186
5755
|
// src/modules/verify/browser.ts
|
|
5187
5756
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
5188
5757
|
import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
|
|
@@ -5822,9 +6391,9 @@ function getACById(id) {
|
|
|
5822
6391
|
}
|
|
5823
6392
|
|
|
5824
6393
|
// src/modules/verify/validation-runner.ts
|
|
5825
|
-
import { execSync as
|
|
6394
|
+
import { execSync as execSync4 } from "child_process";
|
|
5826
6395
|
import { writeFileSync as writeFileSync14, mkdirSync as mkdirSync7 } from "fs";
|
|
5827
|
-
import { join as
|
|
6396
|
+
import { join as join20, dirname as dirname5 } from "path";
|
|
5828
6397
|
var MAX_VALIDATION_ATTEMPTS = 10;
|
|
5829
6398
|
var AC_COMMAND_TIMEOUT_MS = 3e4;
|
|
5830
6399
|
var VAL_KEY_PREFIX = "val-";
|
|
@@ -5896,7 +6465,7 @@ function executeValidationAC(ac) {
|
|
|
5896
6465
|
}
|
|
5897
6466
|
const startTime = Date.now();
|
|
5898
6467
|
try {
|
|
5899
|
-
const output =
|
|
6468
|
+
const output = execSync4(ac.command, {
|
|
5900
6469
|
timeout: AC_COMMAND_TIMEOUT_MS,
|
|
5901
6470
|
encoding: "utf-8",
|
|
5902
6471
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5933,7 +6502,7 @@ function executeValidationAC(ac) {
|
|
|
5933
6502
|
function createFixStory(ac, error) {
|
|
5934
6503
|
try {
|
|
5935
6504
|
const storyKey = `val-fix-${ac.id}`;
|
|
5936
|
-
const storyPath =
|
|
6505
|
+
const storyPath = join20(
|
|
5937
6506
|
process.cwd(),
|
|
5938
6507
|
"_bmad-output",
|
|
5939
6508
|
"implementation-artifacts",
|
|
@@ -6036,7 +6605,7 @@ function processValidationResult(acId, result) {
|
|
|
6036
6605
|
}
|
|
6037
6606
|
|
|
6038
6607
|
// src/modules/dev/orchestrator.ts
|
|
6039
|
-
import { execFileSync as execFileSync10, execSync as
|
|
6608
|
+
import { execFileSync as execFileSync10, execSync as execSync5 } from "child_process";
|
|
6040
6609
|
var DEFAULT_TIMEOUT_MS = 15e5;
|
|
6041
6610
|
var MAX_OUTPUT_LINES = 200;
|
|
6042
6611
|
var GIT_TIMEOUT_MS2 = 5e3;
|
|
@@ -6049,17 +6618,17 @@ function truncateOutput(output, maxLines) {
|
|
|
6049
6618
|
}
|
|
6050
6619
|
function captureFilesChanged() {
|
|
6051
6620
|
try {
|
|
6052
|
-
const unstaged =
|
|
6621
|
+
const unstaged = execSync5("git diff --name-only", {
|
|
6053
6622
|
timeout: GIT_TIMEOUT_MS2,
|
|
6054
6623
|
encoding: "utf-8",
|
|
6055
6624
|
stdio: ["pipe", "pipe", "pipe"]
|
|
6056
6625
|
}).trim();
|
|
6057
|
-
const staged =
|
|
6626
|
+
const staged = execSync5("git diff --cached --name-only", {
|
|
6058
6627
|
timeout: GIT_TIMEOUT_MS2,
|
|
6059
6628
|
encoding: "utf-8",
|
|
6060
6629
|
stdio: ["pipe", "pipe", "pipe"]
|
|
6061
6630
|
}).trim();
|
|
6062
|
-
const untracked =
|
|
6631
|
+
const untracked = execSync5("git ls-files --others --exclude-standard", {
|
|
6063
6632
|
timeout: GIT_TIMEOUT_MS2,
|
|
6064
6633
|
encoding: "utf-8",
|
|
6065
6634
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6303,8 +6872,8 @@ function runValidationCycle() {
|
|
|
6303
6872
|
|
|
6304
6873
|
// src/modules/verify/env.ts
|
|
6305
6874
|
import { execFileSync as execFileSync11 } from "child_process";
|
|
6306
|
-
import { existsSync as existsSync24, mkdirSync as mkdirSync8, readdirSync as
|
|
6307
|
-
import { join as
|
|
6875
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync8, readdirSync as readdirSync5, readFileSync as readFileSync21, cpSync, rmSync, statSync as statSync3 } from "fs";
|
|
6876
|
+
import { join as join21, basename as basename4 } from "path";
|
|
6308
6877
|
import { createHash } from "crypto";
|
|
6309
6878
|
var IMAGE_TAG = "codeharness-verify";
|
|
6310
6879
|
var STORY_DIR = "_bmad-output/implementation-artifacts";
|
|
@@ -6317,7 +6886,7 @@ function isValidStoryKey(storyKey) {
|
|
|
6317
6886
|
return /^[a-zA-Z0-9_-]+$/.test(storyKey);
|
|
6318
6887
|
}
|
|
6319
6888
|
function computeDistHash(projectDir) {
|
|
6320
|
-
const distDir =
|
|
6889
|
+
const distDir = join21(projectDir, "dist");
|
|
6321
6890
|
if (!existsSync24(distDir)) return null;
|
|
6322
6891
|
const hash = createHash("sha256");
|
|
6323
6892
|
const files = collectFiles(distDir).sort();
|
|
@@ -6329,8 +6898,8 @@ function computeDistHash(projectDir) {
|
|
|
6329
6898
|
}
|
|
6330
6899
|
function collectFiles(dir) {
|
|
6331
6900
|
const results = [];
|
|
6332
|
-
for (const entry of
|
|
6333
|
-
const fullPath =
|
|
6901
|
+
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
6902
|
+
const fullPath = join21(dir, entry.name);
|
|
6334
6903
|
if (entry.isDirectory()) {
|
|
6335
6904
|
results.push(...collectFiles(fullPath));
|
|
6336
6905
|
} else {
|
|
@@ -6360,7 +6929,7 @@ function detectProjectType(projectDir) {
|
|
|
6360
6929
|
const stack = detectStack(projectDir);
|
|
6361
6930
|
if (stack === "nodejs") return "nodejs";
|
|
6362
6931
|
if (stack === "python") return "python";
|
|
6363
|
-
if (existsSync24(
|
|
6932
|
+
if (existsSync24(join21(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
|
|
6364
6933
|
return "generic";
|
|
6365
6934
|
}
|
|
6366
6935
|
function buildVerifyImage(options = {}) {
|
|
@@ -6404,12 +6973,12 @@ function buildNodeImage(projectDir) {
|
|
|
6404
6973
|
const lastLine = packOutput.split("\n").pop()?.trim();
|
|
6405
6974
|
if (!lastLine) throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
|
|
6406
6975
|
const tarballName = basename4(lastLine);
|
|
6407
|
-
const tarballPath =
|
|
6408
|
-
const buildContext =
|
|
6976
|
+
const tarballPath = join21("/tmp", tarballName);
|
|
6977
|
+
const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
6409
6978
|
mkdirSync8(buildContext, { recursive: true });
|
|
6410
6979
|
try {
|
|
6411
|
-
cpSync(tarballPath,
|
|
6412
|
-
cpSync(resolveDockerfileTemplate(projectDir),
|
|
6980
|
+
cpSync(tarballPath, join21(buildContext, tarballName));
|
|
6981
|
+
cpSync(resolveDockerfileTemplate(projectDir), join21(buildContext, "Dockerfile"));
|
|
6413
6982
|
execFileSync11("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${tarballName}`, "."], {
|
|
6414
6983
|
cwd: buildContext,
|
|
6415
6984
|
stdio: "pipe",
|
|
@@ -6421,17 +6990,17 @@ function buildNodeImage(projectDir) {
|
|
|
6421
6990
|
}
|
|
6422
6991
|
}
|
|
6423
6992
|
function buildPythonImage(projectDir) {
|
|
6424
|
-
const distDir =
|
|
6425
|
-
const distFiles =
|
|
6993
|
+
const distDir = join21(projectDir, "dist");
|
|
6994
|
+
const distFiles = readdirSync5(distDir).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".whl"));
|
|
6426
6995
|
if (distFiles.length === 0) {
|
|
6427
6996
|
throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
|
|
6428
6997
|
}
|
|
6429
6998
|
const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
|
|
6430
|
-
const buildContext =
|
|
6999
|
+
const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
6431
7000
|
mkdirSync8(buildContext, { recursive: true });
|
|
6432
7001
|
try {
|
|
6433
|
-
cpSync(
|
|
6434
|
-
cpSync(resolveDockerfileTemplate(projectDir),
|
|
7002
|
+
cpSync(join21(distDir, distFile), join21(buildContext, distFile));
|
|
7003
|
+
cpSync(resolveDockerfileTemplate(projectDir), join21(buildContext, "Dockerfile"));
|
|
6435
7004
|
execFileSync11("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${distFile}`, "."], {
|
|
6436
7005
|
cwd: buildContext,
|
|
6437
7006
|
stdio: "pipe",
|
|
@@ -6446,19 +7015,19 @@ function prepareVerifyWorkspace(storyKey, projectDir) {
|
|
|
6446
7015
|
if (!isValidStoryKey(storyKey)) {
|
|
6447
7016
|
throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
|
|
6448
7017
|
}
|
|
6449
|
-
const storyFile =
|
|
7018
|
+
const storyFile = join21(root, STORY_DIR, `${storyKey}.md`);
|
|
6450
7019
|
if (!existsSync24(storyFile)) throw new Error(`Story file not found: ${storyFile}`);
|
|
6451
7020
|
const workspace = `${TEMP_PREFIX}${storyKey}`;
|
|
6452
7021
|
if (existsSync24(workspace)) rmSync(workspace, { recursive: true, force: true });
|
|
6453
7022
|
mkdirSync8(workspace, { recursive: true });
|
|
6454
|
-
cpSync(storyFile,
|
|
6455
|
-
const readmePath =
|
|
6456
|
-
if (existsSync24(readmePath)) cpSync(readmePath,
|
|
6457
|
-
const docsDir =
|
|
6458
|
-
if (existsSync24(docsDir) &&
|
|
6459
|
-
cpSync(docsDir,
|
|
6460
|
-
}
|
|
6461
|
-
mkdirSync8(
|
|
7023
|
+
cpSync(storyFile, join21(workspace, "story.md"));
|
|
7024
|
+
const readmePath = join21(root, "README.md");
|
|
7025
|
+
if (existsSync24(readmePath)) cpSync(readmePath, join21(workspace, "README.md"));
|
|
7026
|
+
const docsDir = join21(root, "docs");
|
|
7027
|
+
if (existsSync24(docsDir) && statSync3(docsDir).isDirectory()) {
|
|
7028
|
+
cpSync(docsDir, join21(workspace, "docs"), { recursive: true });
|
|
7029
|
+
}
|
|
7030
|
+
mkdirSync8(join21(workspace, "verification"), { recursive: true });
|
|
6462
7031
|
return workspace;
|
|
6463
7032
|
}
|
|
6464
7033
|
function checkVerifyEnv() {
|
|
@@ -6511,18 +7080,18 @@ function cleanupVerifyEnv(storyKey) {
|
|
|
6511
7080
|
}
|
|
6512
7081
|
}
|
|
6513
7082
|
function buildPluginImage(projectDir) {
|
|
6514
|
-
const buildContext =
|
|
7083
|
+
const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
6515
7084
|
mkdirSync8(buildContext, { recursive: true });
|
|
6516
7085
|
try {
|
|
6517
|
-
const pluginDir =
|
|
6518
|
-
cpSync(pluginDir,
|
|
7086
|
+
const pluginDir = join21(projectDir, ".claude-plugin");
|
|
7087
|
+
cpSync(pluginDir, join21(buildContext, ".claude-plugin"), { recursive: true });
|
|
6519
7088
|
for (const dir of ["commands", "hooks", "knowledge", "skills"]) {
|
|
6520
|
-
const src =
|
|
6521
|
-
if (existsSync24(src) &&
|
|
6522
|
-
cpSync(src,
|
|
7089
|
+
const src = join21(projectDir, dir);
|
|
7090
|
+
if (existsSync24(src) && statSync3(src).isDirectory()) {
|
|
7091
|
+
cpSync(src, join21(buildContext, dir), { recursive: true });
|
|
6523
7092
|
}
|
|
6524
7093
|
}
|
|
6525
|
-
cpSync(resolveDockerfileTemplate(projectDir, "generic"),
|
|
7094
|
+
cpSync(resolveDockerfileTemplate(projectDir, "generic"), join21(buildContext, "Dockerfile"));
|
|
6526
7095
|
execFileSync11("docker", ["build", "-t", IMAGE_TAG, "."], {
|
|
6527
7096
|
cwd: buildContext,
|
|
6528
7097
|
stdio: "pipe",
|
|
@@ -6533,10 +7102,10 @@ function buildPluginImage(projectDir) {
|
|
|
6533
7102
|
}
|
|
6534
7103
|
}
|
|
6535
7104
|
function buildGenericImage(projectDir) {
|
|
6536
|
-
const buildContext =
|
|
7105
|
+
const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
6537
7106
|
mkdirSync8(buildContext, { recursive: true });
|
|
6538
7107
|
try {
|
|
6539
|
-
cpSync(resolveDockerfileTemplate(projectDir, "generic"),
|
|
7108
|
+
cpSync(resolveDockerfileTemplate(projectDir, "generic"), join21(buildContext, "Dockerfile"));
|
|
6540
7109
|
execFileSync11("docker", ["build", "-t", IMAGE_TAG, "."], {
|
|
6541
7110
|
cwd: buildContext,
|
|
6542
7111
|
stdio: "pipe",
|
|
@@ -6548,10 +7117,10 @@ function buildGenericImage(projectDir) {
|
|
|
6548
7117
|
}
|
|
6549
7118
|
function resolveDockerfileTemplate(projectDir, variant) {
|
|
6550
7119
|
const filename = variant === "generic" ? "Dockerfile.verify.generic" : "Dockerfile.verify";
|
|
6551
|
-
const local =
|
|
7120
|
+
const local = join21(projectDir, "templates", filename);
|
|
6552
7121
|
if (existsSync24(local)) return local;
|
|
6553
7122
|
const pkgDir = new URL("../../", import.meta.url).pathname;
|
|
6554
|
-
const pkg =
|
|
7123
|
+
const pkg = join21(pkgDir, "templates", filename);
|
|
6555
7124
|
if (existsSync24(pkg)) return pkg;
|
|
6556
7125
|
throw new Error(`${filename} not found. Ensure templates/${filename} exists.`);
|
|
6557
7126
|
}
|
|
@@ -6580,6 +7149,17 @@ function getImageSize(tag) {
|
|
|
6580
7149
|
}
|
|
6581
7150
|
}
|
|
6582
7151
|
|
|
7152
|
+
// src/modules/verify/index.ts
|
|
7153
|
+
function parseProof(path) {
|
|
7154
|
+
try {
|
|
7155
|
+
const quality = validateProofQuality(path);
|
|
7156
|
+
return ok2(quality);
|
|
7157
|
+
} catch (err) {
|
|
7158
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7159
|
+
return fail2(message);
|
|
7160
|
+
}
|
|
7161
|
+
}
|
|
7162
|
+
|
|
6583
7163
|
// src/commands/verify.ts
|
|
6584
7164
|
var STORY_DIR2 = "_bmad-output/implementation-artifacts";
|
|
6585
7165
|
function isValidStoryId(storyId) {
|
|
@@ -6616,7 +7196,7 @@ function verifyRetro(opts, isJson, root) {
|
|
|
6616
7196
|
return;
|
|
6617
7197
|
}
|
|
6618
7198
|
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
6619
|
-
const retroPath =
|
|
7199
|
+
const retroPath = join22(root, STORY_DIR2, retroFile);
|
|
6620
7200
|
if (!existsSync25(retroPath)) {
|
|
6621
7201
|
if (isJson) {
|
|
6622
7202
|
jsonOutput({ status: "fail", epic: epicNum, retroFile, message: `${retroFile} not found` });
|
|
@@ -6634,7 +7214,7 @@ function verifyRetro(opts, isJson, root) {
|
|
|
6634
7214
|
warn(`Failed to update sprint status: ${message}`);
|
|
6635
7215
|
}
|
|
6636
7216
|
if (isJson) {
|
|
6637
|
-
jsonOutput({ status: "ok", epic: epicNum, retroFile:
|
|
7217
|
+
jsonOutput({ status: "ok", epic: epicNum, retroFile: join22(STORY_DIR2, retroFile) });
|
|
6638
7218
|
} else {
|
|
6639
7219
|
ok(`Epic ${epicNum} retrospective: marked done`);
|
|
6640
7220
|
}
|
|
@@ -6645,7 +7225,7 @@ function verifyStory(storyId, isJson, root) {
|
|
|
6645
7225
|
process.exitCode = 1;
|
|
6646
7226
|
return;
|
|
6647
7227
|
}
|
|
6648
|
-
const readmePath =
|
|
7228
|
+
const readmePath = join22(root, "README.md");
|
|
6649
7229
|
if (!existsSync25(readmePath)) {
|
|
6650
7230
|
if (isJson) {
|
|
6651
7231
|
jsonOutput({ status: "fail", message: "No README.md found \u2014 verification requires user documentation" });
|
|
@@ -6655,7 +7235,7 @@ function verifyStory(storyId, isJson, root) {
|
|
|
6655
7235
|
process.exitCode = 1;
|
|
6656
7236
|
return;
|
|
6657
7237
|
}
|
|
6658
|
-
const storyFilePath =
|
|
7238
|
+
const storyFilePath = join22(root, STORY_DIR2, `${storyId}.md`);
|
|
6659
7239
|
if (!existsSync25(storyFilePath)) {
|
|
6660
7240
|
fail(`Story file not found: ${storyFilePath}`, { json: isJson });
|
|
6661
7241
|
process.exitCode = 1;
|
|
@@ -6696,7 +7276,7 @@ function verifyStory(storyId, isJson, root) {
|
|
|
6696
7276
|
return;
|
|
6697
7277
|
}
|
|
6698
7278
|
const storyTitle = extractStoryTitle(storyFilePath);
|
|
6699
|
-
const expectedProofPath =
|
|
7279
|
+
const expectedProofPath = join22(root, "verification", `${storyId}-proof.md`);
|
|
6700
7280
|
const proofPath = existsSync25(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
|
|
6701
7281
|
const proofQuality = validateProofQuality(proofPath);
|
|
6702
7282
|
if (!proofQuality.passed) {
|
|
@@ -6812,13 +7392,13 @@ function extractStoryTitle(filePath) {
|
|
|
6812
7392
|
|
|
6813
7393
|
// src/lib/onboard-checks.ts
|
|
6814
7394
|
import { existsSync as existsSync27 } from "fs";
|
|
6815
|
-
import { join as
|
|
7395
|
+
import { join as join24, dirname as dirname6 } from "path";
|
|
6816
7396
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6817
7397
|
|
|
6818
7398
|
// src/lib/coverage.ts
|
|
6819
|
-
import { execSync as
|
|
7399
|
+
import { execSync as execSync6 } from "child_process";
|
|
6820
7400
|
import { existsSync as existsSync26, readFileSync as readFileSync24 } from "fs";
|
|
6821
|
-
import { join as
|
|
7401
|
+
import { join as join23 } from "path";
|
|
6822
7402
|
function detectCoverageTool(dir) {
|
|
6823
7403
|
const baseDir = dir ?? process.cwd();
|
|
6824
7404
|
const stateHint = getStateToolHint(baseDir);
|
|
@@ -6841,8 +7421,8 @@ function getStateToolHint(dir) {
|
|
|
6841
7421
|
}
|
|
6842
7422
|
}
|
|
6843
7423
|
function detectNodeCoverageTool(dir, stateHint) {
|
|
6844
|
-
const hasVitestConfig = existsSync26(
|
|
6845
|
-
const pkgPath =
|
|
7424
|
+
const hasVitestConfig = existsSync26(join23(dir, "vitest.config.ts")) || existsSync26(join23(dir, "vitest.config.js"));
|
|
7425
|
+
const pkgPath = join23(dir, "package.json");
|
|
6846
7426
|
let hasVitestCoverageV8 = false;
|
|
6847
7427
|
let hasVitestCoverageIstanbul = false;
|
|
6848
7428
|
let hasC8 = false;
|
|
@@ -6903,7 +7483,7 @@ function getNodeTestCommand(scripts, runner) {
|
|
|
6903
7483
|
return "npm test";
|
|
6904
7484
|
}
|
|
6905
7485
|
function detectPythonCoverageTool(dir) {
|
|
6906
|
-
const reqPath =
|
|
7486
|
+
const reqPath = join23(dir, "requirements.txt");
|
|
6907
7487
|
if (existsSync26(reqPath)) {
|
|
6908
7488
|
try {
|
|
6909
7489
|
const content = readFileSync24(reqPath, "utf-8");
|
|
@@ -6917,7 +7497,7 @@ function detectPythonCoverageTool(dir) {
|
|
|
6917
7497
|
} catch {
|
|
6918
7498
|
}
|
|
6919
7499
|
}
|
|
6920
|
-
const pyprojectPath =
|
|
7500
|
+
const pyprojectPath = join23(dir, "pyproject.toml");
|
|
6921
7501
|
if (existsSync26(pyprojectPath)) {
|
|
6922
7502
|
try {
|
|
6923
7503
|
const content = readFileSync24(pyprojectPath, "utf-8");
|
|
@@ -6950,7 +7530,7 @@ function runCoverage(dir) {
|
|
|
6950
7530
|
let rawOutput = "";
|
|
6951
7531
|
let testsPassed = true;
|
|
6952
7532
|
try {
|
|
6953
|
-
rawOutput =
|
|
7533
|
+
rawOutput = execSync6(toolInfo.runCommand, {
|
|
6954
7534
|
cwd: baseDir,
|
|
6955
7535
|
encoding: "utf-8",
|
|
6956
7536
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -7010,7 +7590,7 @@ function parseVitestCoverage(dir) {
|
|
|
7010
7590
|
}
|
|
7011
7591
|
}
|
|
7012
7592
|
function parsePythonCoverage(dir) {
|
|
7013
|
-
const reportPath =
|
|
7593
|
+
const reportPath = join23(dir, "coverage.json");
|
|
7014
7594
|
if (!existsSync26(reportPath)) {
|
|
7015
7595
|
warn("Coverage report not found at coverage.json");
|
|
7016
7596
|
return 0;
|
|
@@ -7147,8 +7727,8 @@ function checkPerFileCoverage(floor, dir) {
|
|
|
7147
7727
|
}
|
|
7148
7728
|
function findCoverageSummary(dir) {
|
|
7149
7729
|
const candidates = [
|
|
7150
|
-
|
|
7151
|
-
|
|
7730
|
+
join23(dir, "coverage", "coverage-summary.json"),
|
|
7731
|
+
join23(dir, "src", "coverage", "coverage-summary.json")
|
|
7152
7732
|
];
|
|
7153
7733
|
for (const p of candidates) {
|
|
7154
7734
|
if (existsSync26(p)) return p;
|
|
@@ -7184,7 +7764,7 @@ function checkBmadInstalled(dir) {
|
|
|
7184
7764
|
function checkHooksRegistered(dir) {
|
|
7185
7765
|
const __filename = fileURLToPath3(import.meta.url);
|
|
7186
7766
|
const __dirname2 = dirname6(__filename);
|
|
7187
|
-
const hooksPath =
|
|
7767
|
+
const hooksPath = join24(__dirname2, "..", "..", "hooks", "hooks.json");
|
|
7188
7768
|
return { ok: existsSync27(hooksPath) };
|
|
7189
7769
|
}
|
|
7190
7770
|
function runPreconditions(dir) {
|
|
@@ -7225,7 +7805,7 @@ function findVerificationGaps(dir) {
|
|
|
7225
7805
|
for (const [key, status] of Object.entries(statuses)) {
|
|
7226
7806
|
if (status !== "done") continue;
|
|
7227
7807
|
if (!STORY_KEY_PATTERN2.test(key)) continue;
|
|
7228
|
-
const proofPath =
|
|
7808
|
+
const proofPath = join24(root, "verification", `${key}-proof.md`);
|
|
7229
7809
|
if (!existsSync27(proofPath)) {
|
|
7230
7810
|
unverified.push(key);
|
|
7231
7811
|
}
|
|
@@ -7938,16 +8518,16 @@ function getBeadsData() {
|
|
|
7938
8518
|
}
|
|
7939
8519
|
|
|
7940
8520
|
// src/commands/onboard.ts
|
|
7941
|
-
import { join as
|
|
8521
|
+
import { join as join28 } from "path";
|
|
7942
8522
|
|
|
7943
8523
|
// src/lib/scanner.ts
|
|
7944
8524
|
import {
|
|
7945
8525
|
existsSync as existsSync28,
|
|
7946
|
-
readdirSync as
|
|
8526
|
+
readdirSync as readdirSync6,
|
|
7947
8527
|
readFileSync as readFileSync25,
|
|
7948
|
-
statSync as
|
|
8528
|
+
statSync as statSync4
|
|
7949
8529
|
} from "fs";
|
|
7950
|
-
import { join as
|
|
8530
|
+
import { join as join25, relative as relative2 } from "path";
|
|
7951
8531
|
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
|
|
7952
8532
|
var DEFAULT_MIN_MODULE_SIZE = 3;
|
|
7953
8533
|
function getExtension2(filename) {
|
|
@@ -7965,17 +8545,17 @@ function countSourceFiles(dir) {
|
|
|
7965
8545
|
function walk(current) {
|
|
7966
8546
|
let entries;
|
|
7967
8547
|
try {
|
|
7968
|
-
entries =
|
|
8548
|
+
entries = readdirSync6(current);
|
|
7969
8549
|
} catch {
|
|
7970
8550
|
return;
|
|
7971
8551
|
}
|
|
7972
8552
|
for (const entry of entries) {
|
|
7973
8553
|
if (isSkippedDir(entry)) continue;
|
|
7974
8554
|
if (entry.startsWith(".") && current !== dir) continue;
|
|
7975
|
-
const fullPath =
|
|
8555
|
+
const fullPath = join25(current, entry);
|
|
7976
8556
|
let stat;
|
|
7977
8557
|
try {
|
|
7978
|
-
stat =
|
|
8558
|
+
stat = statSync4(fullPath);
|
|
7979
8559
|
} catch {
|
|
7980
8560
|
continue;
|
|
7981
8561
|
}
|
|
@@ -7995,22 +8575,22 @@ function countSourceFiles(dir) {
|
|
|
7995
8575
|
return count;
|
|
7996
8576
|
}
|
|
7997
8577
|
function countModuleFiles(modulePath, rootDir) {
|
|
7998
|
-
const fullModulePath =
|
|
8578
|
+
const fullModulePath = join25(rootDir, modulePath);
|
|
7999
8579
|
let sourceFiles = 0;
|
|
8000
8580
|
let testFiles = 0;
|
|
8001
8581
|
function walk(current) {
|
|
8002
8582
|
let entries;
|
|
8003
8583
|
try {
|
|
8004
|
-
entries =
|
|
8584
|
+
entries = readdirSync6(current);
|
|
8005
8585
|
} catch {
|
|
8006
8586
|
return;
|
|
8007
8587
|
}
|
|
8008
8588
|
for (const entry of entries) {
|
|
8009
8589
|
if (isSkippedDir(entry)) continue;
|
|
8010
|
-
const fullPath =
|
|
8590
|
+
const fullPath = join25(current, entry);
|
|
8011
8591
|
let stat;
|
|
8012
8592
|
try {
|
|
8013
|
-
stat =
|
|
8593
|
+
stat = statSync4(fullPath);
|
|
8014
8594
|
} catch {
|
|
8015
8595
|
continue;
|
|
8016
8596
|
}
|
|
@@ -8032,7 +8612,7 @@ function countModuleFiles(modulePath, rootDir) {
|
|
|
8032
8612
|
return { sourceFiles, testFiles };
|
|
8033
8613
|
}
|
|
8034
8614
|
function detectArtifacts(dir) {
|
|
8035
|
-
const bmadPath =
|
|
8615
|
+
const bmadPath = join25(dir, "_bmad");
|
|
8036
8616
|
const hasBmad = existsSync28(bmadPath);
|
|
8037
8617
|
return {
|
|
8038
8618
|
hasBmad,
|
|
@@ -8115,7 +8695,7 @@ function readPerFileCoverage(dir, format) {
|
|
|
8115
8695
|
return null;
|
|
8116
8696
|
}
|
|
8117
8697
|
function readVitestPerFileCoverage(dir) {
|
|
8118
|
-
const reportPath =
|
|
8698
|
+
const reportPath = join25(dir, "coverage", "coverage-summary.json");
|
|
8119
8699
|
if (!existsSync28(reportPath)) return null;
|
|
8120
8700
|
try {
|
|
8121
8701
|
const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
|
|
@@ -8130,7 +8710,7 @@ function readVitestPerFileCoverage(dir) {
|
|
|
8130
8710
|
}
|
|
8131
8711
|
}
|
|
8132
8712
|
function readPythonPerFileCoverage(dir) {
|
|
8133
|
-
const reportPath =
|
|
8713
|
+
const reportPath = join25(dir, "coverage.json");
|
|
8134
8714
|
if (!existsSync28(reportPath)) return null;
|
|
8135
8715
|
try {
|
|
8136
8716
|
const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
|
|
@@ -8149,12 +8729,12 @@ function auditDocumentation(dir) {
|
|
|
8149
8729
|
const root = dir ?? process.cwd();
|
|
8150
8730
|
const documents = [];
|
|
8151
8731
|
for (const docName of AUDIT_DOCUMENTS) {
|
|
8152
|
-
const docPath =
|
|
8732
|
+
const docPath = join25(root, docName);
|
|
8153
8733
|
if (!existsSync28(docPath)) {
|
|
8154
8734
|
documents.push({ name: docName, grade: "missing", path: null });
|
|
8155
8735
|
continue;
|
|
8156
8736
|
}
|
|
8157
|
-
const srcDir =
|
|
8737
|
+
const srcDir = join25(root, "src");
|
|
8158
8738
|
const codeDir = existsSync28(srcDir) ? srcDir : root;
|
|
8159
8739
|
const stale = isDocStale(docPath, codeDir);
|
|
8160
8740
|
documents.push({
|
|
@@ -8163,10 +8743,10 @@ function auditDocumentation(dir) {
|
|
|
8163
8743
|
path: docName
|
|
8164
8744
|
});
|
|
8165
8745
|
}
|
|
8166
|
-
const docsDir =
|
|
8746
|
+
const docsDir = join25(root, "docs");
|
|
8167
8747
|
if (existsSync28(docsDir)) {
|
|
8168
8748
|
try {
|
|
8169
|
-
const stat =
|
|
8749
|
+
const stat = statSync4(docsDir);
|
|
8170
8750
|
if (stat.isDirectory()) {
|
|
8171
8751
|
documents.push({ name: "docs/", grade: "present", path: "docs/" });
|
|
8172
8752
|
}
|
|
@@ -8176,9 +8756,9 @@ function auditDocumentation(dir) {
|
|
|
8176
8756
|
} else {
|
|
8177
8757
|
documents.push({ name: "docs/", grade: "missing", path: null });
|
|
8178
8758
|
}
|
|
8179
|
-
const indexPath =
|
|
8759
|
+
const indexPath = join25(root, "docs", "index.md");
|
|
8180
8760
|
if (existsSync28(indexPath)) {
|
|
8181
|
-
const srcDir =
|
|
8761
|
+
const srcDir = join25(root, "src");
|
|
8182
8762
|
const indexCodeDir = existsSync28(srcDir) ? srcDir : root;
|
|
8183
8763
|
const indexStale = isDocStale(indexPath, indexCodeDir);
|
|
8184
8764
|
documents.push({
|
|
@@ -8197,7 +8777,7 @@ function auditDocumentation(dir) {
|
|
|
8197
8777
|
// src/lib/epic-generator.ts
|
|
8198
8778
|
import { createInterface } from "readline";
|
|
8199
8779
|
import { existsSync as existsSync29, mkdirSync as mkdirSync9, writeFileSync as writeFileSync15 } from "fs";
|
|
8200
|
-
import { dirname as dirname7, join as
|
|
8780
|
+
import { dirname as dirname7, join as join26 } from "path";
|
|
8201
8781
|
var PRIORITY_BY_TYPE = {
|
|
8202
8782
|
observability: 1,
|
|
8203
8783
|
coverage: 2,
|
|
@@ -8235,7 +8815,7 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
|
|
|
8235
8815
|
storyNum++;
|
|
8236
8816
|
}
|
|
8237
8817
|
for (const mod of scan.modules) {
|
|
8238
|
-
const agentsPath =
|
|
8818
|
+
const agentsPath = join26(root, mod.path, "AGENTS.md");
|
|
8239
8819
|
if (!existsSync29(agentsPath)) {
|
|
8240
8820
|
stories.push({
|
|
8241
8821
|
key: `0.${storyNum}`,
|
|
@@ -8435,23 +9015,23 @@ function getGapIdFromTitle(title) {
|
|
|
8435
9015
|
|
|
8436
9016
|
// src/lib/scan-cache.ts
|
|
8437
9017
|
import { existsSync as existsSync30, mkdirSync as mkdirSync10, readFileSync as readFileSync26, writeFileSync as writeFileSync16 } from "fs";
|
|
8438
|
-
import { join as
|
|
9018
|
+
import { join as join27 } from "path";
|
|
8439
9019
|
var CACHE_DIR = ".harness";
|
|
8440
9020
|
var CACHE_FILE = "last-onboard-scan.json";
|
|
8441
9021
|
var DEFAULT_MAX_AGE_MS = 864e5;
|
|
8442
9022
|
function saveScanCache(entry, dir) {
|
|
8443
9023
|
try {
|
|
8444
9024
|
const root = dir ?? process.cwd();
|
|
8445
|
-
const cacheDir =
|
|
9025
|
+
const cacheDir = join27(root, CACHE_DIR);
|
|
8446
9026
|
mkdirSync10(cacheDir, { recursive: true });
|
|
8447
|
-
const cachePath =
|
|
9027
|
+
const cachePath = join27(cacheDir, CACHE_FILE);
|
|
8448
9028
|
writeFileSync16(cachePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
8449
9029
|
} catch {
|
|
8450
9030
|
}
|
|
8451
9031
|
}
|
|
8452
9032
|
function loadScanCache(dir) {
|
|
8453
9033
|
const root = dir ?? process.cwd();
|
|
8454
|
-
const cachePath =
|
|
9034
|
+
const cachePath = join27(root, CACHE_DIR, CACHE_FILE);
|
|
8455
9035
|
if (!existsSync30(cachePath)) {
|
|
8456
9036
|
return null;
|
|
8457
9037
|
}
|
|
@@ -8629,7 +9209,7 @@ function registerOnboardCommand(program) {
|
|
|
8629
9209
|
}
|
|
8630
9210
|
coverage = lastCoverageResult ?? runCoverageAnalysis(scan);
|
|
8631
9211
|
audit = lastAuditResult ?? runAudit();
|
|
8632
|
-
const epicPath =
|
|
9212
|
+
const epicPath = join28(process.cwd(), "ralph", "onboarding-epic.md");
|
|
8633
9213
|
const epic = generateOnboardingEpic(scan, coverage, audit);
|
|
8634
9214
|
mergeExtendedGaps(epic);
|
|
8635
9215
|
if (!isFull) {
|
|
@@ -8702,7 +9282,7 @@ function registerOnboardCommand(program) {
|
|
|
8702
9282
|
coverage,
|
|
8703
9283
|
audit
|
|
8704
9284
|
});
|
|
8705
|
-
const epicPath =
|
|
9285
|
+
const epicPath = join28(process.cwd(), "ralph", "onboarding-epic.md");
|
|
8706
9286
|
const epic = generateOnboardingEpic(scan, coverage, audit);
|
|
8707
9287
|
mergeExtendedGaps(epic);
|
|
8708
9288
|
if (!isFull) {
|
|
@@ -8811,7 +9391,7 @@ function printEpicOutput(epic) {
|
|
|
8811
9391
|
|
|
8812
9392
|
// src/commands/teardown.ts
|
|
8813
9393
|
import { existsSync as existsSync31, unlinkSync as unlinkSync2, readFileSync as readFileSync27, writeFileSync as writeFileSync17, rmSync as rmSync2 } from "fs";
|
|
8814
|
-
import { join as
|
|
9394
|
+
import { join as join29 } from "path";
|
|
8815
9395
|
function buildDefaultResult() {
|
|
8816
9396
|
return {
|
|
8817
9397
|
status: "ok",
|
|
@@ -8914,7 +9494,7 @@ function registerTeardownCommand(program) {
|
|
|
8914
9494
|
info("Docker stack: not running, skipping");
|
|
8915
9495
|
}
|
|
8916
9496
|
}
|
|
8917
|
-
const composeFilePath =
|
|
9497
|
+
const composeFilePath = join29(projectDir, composeFile);
|
|
8918
9498
|
if (existsSync31(composeFilePath)) {
|
|
8919
9499
|
unlinkSync2(composeFilePath);
|
|
8920
9500
|
result.removed.push(composeFile);
|
|
@@ -8922,7 +9502,7 @@ function registerTeardownCommand(program) {
|
|
|
8922
9502
|
ok(`Removed: ${composeFile}`);
|
|
8923
9503
|
}
|
|
8924
9504
|
}
|
|
8925
|
-
const otelConfigPath =
|
|
9505
|
+
const otelConfigPath = join29(projectDir, "otel-collector-config.yaml");
|
|
8926
9506
|
if (existsSync31(otelConfigPath)) {
|
|
8927
9507
|
unlinkSync2(otelConfigPath);
|
|
8928
9508
|
result.removed.push("otel-collector-config.yaml");
|
|
@@ -8933,7 +9513,7 @@ function registerTeardownCommand(program) {
|
|
|
8933
9513
|
}
|
|
8934
9514
|
let patchesRemoved = 0;
|
|
8935
9515
|
for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
|
|
8936
|
-
const filePath =
|
|
9516
|
+
const filePath = join29(projectDir, "_bmad", relativePath);
|
|
8937
9517
|
if (!existsSync31(filePath)) {
|
|
8938
9518
|
continue;
|
|
8939
9519
|
}
|
|
@@ -8954,7 +9534,7 @@ function registerTeardownCommand(program) {
|
|
|
8954
9534
|
}
|
|
8955
9535
|
}
|
|
8956
9536
|
if (state.otlp?.enabled && state.stack === "nodejs") {
|
|
8957
|
-
const pkgPath =
|
|
9537
|
+
const pkgPath = join29(projectDir, "package.json");
|
|
8958
9538
|
if (existsSync31(pkgPath)) {
|
|
8959
9539
|
try {
|
|
8960
9540
|
const raw = readFileSync27(pkgPath, "utf-8");
|
|
@@ -8997,7 +9577,7 @@ function registerTeardownCommand(program) {
|
|
|
8997
9577
|
}
|
|
8998
9578
|
}
|
|
8999
9579
|
}
|
|
9000
|
-
const harnessDir =
|
|
9580
|
+
const harnessDir = join29(projectDir, ".harness");
|
|
9001
9581
|
if (existsSync31(harnessDir)) {
|
|
9002
9582
|
rmSync2(harnessDir, { recursive: true, force: true });
|
|
9003
9583
|
result.removed.push(".harness/");
|
|
@@ -9751,7 +10331,7 @@ function registerQueryCommand(program) {
|
|
|
9751
10331
|
|
|
9752
10332
|
// src/commands/retro-import.ts
|
|
9753
10333
|
import { existsSync as existsSync32, readFileSync as readFileSync28 } from "fs";
|
|
9754
|
-
import { join as
|
|
10334
|
+
import { join as join30 } from "path";
|
|
9755
10335
|
|
|
9756
10336
|
// src/lib/retro-parser.ts
|
|
9757
10337
|
var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
|
|
@@ -9920,7 +10500,7 @@ function registerRetroImportCommand(program) {
|
|
|
9920
10500
|
return;
|
|
9921
10501
|
}
|
|
9922
10502
|
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
9923
|
-
const retroPath =
|
|
10503
|
+
const retroPath = join30(root, STORY_DIR3, retroFile);
|
|
9924
10504
|
if (!existsSync32(retroPath)) {
|
|
9925
10505
|
fail(`Retro file not found: ${retroFile}`, { json: isJson });
|
|
9926
10506
|
process.exitCode = 1;
|
|
@@ -10309,19 +10889,19 @@ function registerVerifyEnvCommand(program) {
|
|
|
10309
10889
|
}
|
|
10310
10890
|
|
|
10311
10891
|
// src/commands/retry.ts
|
|
10312
|
-
import { join as
|
|
10892
|
+
import { join as join32 } from "path";
|
|
10313
10893
|
|
|
10314
10894
|
// src/lib/retry-state.ts
|
|
10315
10895
|
import { existsSync as existsSync33, readFileSync as readFileSync29, writeFileSync as writeFileSync18 } from "fs";
|
|
10316
|
-
import { join as
|
|
10896
|
+
import { join as join31 } from "path";
|
|
10317
10897
|
var RETRIES_FILE = ".story_retries";
|
|
10318
10898
|
var FLAGGED_FILE = ".flagged_stories";
|
|
10319
10899
|
var LINE_PATTERN = /^([^=]+)=(\d+)$/;
|
|
10320
10900
|
function retriesPath(dir) {
|
|
10321
|
-
return
|
|
10901
|
+
return join31(dir, RETRIES_FILE);
|
|
10322
10902
|
}
|
|
10323
10903
|
function flaggedPath(dir) {
|
|
10324
|
-
return
|
|
10904
|
+
return join31(dir, FLAGGED_FILE);
|
|
10325
10905
|
}
|
|
10326
10906
|
function readRetries(dir) {
|
|
10327
10907
|
const filePath = retriesPath(dir);
|
|
@@ -10393,7 +10973,7 @@ function registerRetryCommand(program) {
|
|
|
10393
10973
|
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) => {
|
|
10394
10974
|
const opts = cmd.optsWithGlobals();
|
|
10395
10975
|
const isJson = opts.json === true;
|
|
10396
|
-
const dir =
|
|
10976
|
+
const dir = join32(process.cwd(), RALPH_SUBDIR);
|
|
10397
10977
|
if (opts.story && !isValidStoryKey3(opts.story)) {
|
|
10398
10978
|
if (isJson) {
|
|
10399
10979
|
jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
|
|
@@ -10693,8 +11273,533 @@ function registerProgressCommand(program) {
|
|
|
10693
11273
|
});
|
|
10694
11274
|
}
|
|
10695
11275
|
|
|
11276
|
+
// src/commands/observability-gate.ts
|
|
11277
|
+
function registerObservabilityGateCommand(program) {
|
|
11278
|
+
program.command("observability-gate").description("Check observability coverage against targets (commit gate)").option("--json", "Machine-readable JSON output").option("--min-static <percent>", "Override static coverage target").option("--min-runtime <percent>", "Override runtime coverage target").action((opts, cmd) => {
|
|
11279
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
11280
|
+
const isJson = opts.json === true || globalOpts.json === true;
|
|
11281
|
+
const root = process.cwd();
|
|
11282
|
+
const overrides = {};
|
|
11283
|
+
if (opts.minStatic !== void 0) {
|
|
11284
|
+
const parsed = parseInt(opts.minStatic, 10);
|
|
11285
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 100) {
|
|
11286
|
+
if (isJson) {
|
|
11287
|
+
jsonOutput({ status: "error", message: "--min-static must be a number between 0 and 100" });
|
|
11288
|
+
} else {
|
|
11289
|
+
fail("--min-static must be a number between 0 and 100");
|
|
11290
|
+
}
|
|
11291
|
+
process.exitCode = 1;
|
|
11292
|
+
return;
|
|
11293
|
+
}
|
|
11294
|
+
overrides.staticTarget = parsed;
|
|
11295
|
+
}
|
|
11296
|
+
if (opts.minRuntime !== void 0) {
|
|
11297
|
+
const parsed = parseInt(opts.minRuntime, 10);
|
|
11298
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 100) {
|
|
11299
|
+
if (isJson) {
|
|
11300
|
+
jsonOutput({ status: "error", message: "--min-runtime must be a number between 0 and 100" });
|
|
11301
|
+
} else {
|
|
11302
|
+
fail("--min-runtime must be a number between 0 and 100");
|
|
11303
|
+
}
|
|
11304
|
+
process.exitCode = 1;
|
|
11305
|
+
return;
|
|
11306
|
+
}
|
|
11307
|
+
overrides.runtimeTarget = parsed;
|
|
11308
|
+
}
|
|
11309
|
+
const result = checkObservabilityCoverageGate(root, overrides);
|
|
11310
|
+
if (!result.success) {
|
|
11311
|
+
if (isJson) {
|
|
11312
|
+
jsonOutput({ status: "error", message: result.error });
|
|
11313
|
+
} else {
|
|
11314
|
+
fail(`Observability gate error: ${result.error}`);
|
|
11315
|
+
}
|
|
11316
|
+
process.exitCode = 1;
|
|
11317
|
+
return;
|
|
11318
|
+
}
|
|
11319
|
+
const gate = result.data;
|
|
11320
|
+
if (isJson) {
|
|
11321
|
+
jsonOutput({
|
|
11322
|
+
status: gate.passed ? "pass" : "fail",
|
|
11323
|
+
passed: gate.passed,
|
|
11324
|
+
static: {
|
|
11325
|
+
current: gate.staticResult.current,
|
|
11326
|
+
target: gate.staticResult.target,
|
|
11327
|
+
met: gate.staticResult.met,
|
|
11328
|
+
gap: gate.staticResult.gap
|
|
11329
|
+
},
|
|
11330
|
+
runtime: gate.runtimeResult ? {
|
|
11331
|
+
current: gate.runtimeResult.current,
|
|
11332
|
+
target: gate.runtimeResult.target,
|
|
11333
|
+
met: gate.runtimeResult.met,
|
|
11334
|
+
gap: gate.runtimeResult.gap
|
|
11335
|
+
} : null,
|
|
11336
|
+
gaps: gate.gapSummary.map((g) => ({
|
|
11337
|
+
file: g.file,
|
|
11338
|
+
line: g.line,
|
|
11339
|
+
type: g.type,
|
|
11340
|
+
description: g.description
|
|
11341
|
+
}))
|
|
11342
|
+
});
|
|
11343
|
+
} else {
|
|
11344
|
+
const staticLine = `Static: ${gate.staticResult.current}% / ${gate.staticResult.target}% target`;
|
|
11345
|
+
if (gate.passed) {
|
|
11346
|
+
ok(`Observability gate passed. ${staticLine}`);
|
|
11347
|
+
if (gate.runtimeResult) {
|
|
11348
|
+
ok(`Runtime: ${gate.runtimeResult.current}% / ${gate.runtimeResult.target}% target`);
|
|
11349
|
+
}
|
|
11350
|
+
} else {
|
|
11351
|
+
fail(`Observability gate failed. ${staticLine}`);
|
|
11352
|
+
if (gate.runtimeResult && !gate.runtimeResult.met) {
|
|
11353
|
+
fail(`Runtime: ${gate.runtimeResult.current}% / ${gate.runtimeResult.target}% target`);
|
|
11354
|
+
}
|
|
11355
|
+
if (gate.gapSummary.length > 0) {
|
|
11356
|
+
fail("Gaps:");
|
|
11357
|
+
const shown = gate.gapSummary.slice(0, 5);
|
|
11358
|
+
for (const g of shown) {
|
|
11359
|
+
fail(` ${g.file}:${g.line} \u2014 ${g.description}`);
|
|
11360
|
+
}
|
|
11361
|
+
if (gate.gapSummary.length > 5) {
|
|
11362
|
+
fail(` ... and ${gate.gapSummary.length - 5} more.`);
|
|
11363
|
+
}
|
|
11364
|
+
}
|
|
11365
|
+
fail("Add logging to flagged functions. Run: codeharness observability-gate for details.");
|
|
11366
|
+
}
|
|
11367
|
+
}
|
|
11368
|
+
if (!gate.passed) {
|
|
11369
|
+
process.exitCode = 1;
|
|
11370
|
+
}
|
|
11371
|
+
});
|
|
11372
|
+
}
|
|
11373
|
+
|
|
11374
|
+
// src/modules/audit/dimensions.ts
|
|
11375
|
+
import { existsSync as existsSync34, readFileSync as readFileSync30, readdirSync as readdirSync7 } from "fs";
|
|
11376
|
+
import { join as join33 } from "path";
|
|
11377
|
+
function gap(dimension, description, suggestedFix) {
|
|
11378
|
+
return { dimension, description, suggestedFix };
|
|
11379
|
+
}
|
|
11380
|
+
function dimOk(name, status, metric, gaps = []) {
|
|
11381
|
+
return ok2({ name, status, metric, gaps });
|
|
11382
|
+
}
|
|
11383
|
+
function dimCatch(name, err) {
|
|
11384
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11385
|
+
return dimOk(name, "warn", "error", [gap(name, `${name} check failed: ${msg}`, `Check ${name} configuration`)]);
|
|
11386
|
+
}
|
|
11387
|
+
function worstStatus(...statuses) {
|
|
11388
|
+
if (statuses.includes("fail")) return "fail";
|
|
11389
|
+
if (statuses.includes("warn")) return "warn";
|
|
11390
|
+
return "pass";
|
|
11391
|
+
}
|
|
11392
|
+
async function checkObservability(projectDir) {
|
|
11393
|
+
try {
|
|
11394
|
+
const gaps = [];
|
|
11395
|
+
let sStatus = "pass", sMetric = "";
|
|
11396
|
+
const sr = analyze(projectDir);
|
|
11397
|
+
if (isOk(sr)) {
|
|
11398
|
+
const d = sr.data;
|
|
11399
|
+
if (d.skipped) {
|
|
11400
|
+
sStatus = "warn";
|
|
11401
|
+
sMetric = `static: skipped (${d.skipReason ?? "unknown"})`;
|
|
11402
|
+
gaps.push(gap("observability", `Static analysis skipped: ${d.skipReason ?? "Semgrep not installed"}`, "Install Semgrep: pip install semgrep"));
|
|
11403
|
+
} else {
|
|
11404
|
+
const n = d.gaps.length;
|
|
11405
|
+
sMetric = `static: ${n} gap${n !== 1 ? "s" : ""}`;
|
|
11406
|
+
if (n > 0) {
|
|
11407
|
+
sStatus = "warn";
|
|
11408
|
+
for (const g of d.gaps) gaps.push(gap("observability", `${g.file}:${g.line} \u2014 ${g.message}`, g.fix ?? "Add observability instrumentation"));
|
|
11409
|
+
}
|
|
11410
|
+
}
|
|
11411
|
+
} else {
|
|
11412
|
+
sStatus = "warn";
|
|
11413
|
+
sMetric = "static: skipped (analysis failed)";
|
|
11414
|
+
gaps.push(gap("observability", `Static analysis failed: ${sr.error}`, "Check Semgrep installation and rules configuration"));
|
|
11415
|
+
}
|
|
11416
|
+
let rStatus = "pass", rMetric = "";
|
|
11417
|
+
try {
|
|
11418
|
+
const rr = await validateRuntime(projectDir);
|
|
11419
|
+
if (isOk(rr)) {
|
|
11420
|
+
const d = rr.data;
|
|
11421
|
+
if (d.skipped) {
|
|
11422
|
+
rStatus = "warn";
|
|
11423
|
+
rMetric = `runtime: skipped (${d.skipReason ?? "unknown"})`;
|
|
11424
|
+
gaps.push(gap("observability", `Runtime validation skipped: ${d.skipReason ?? "backend unreachable"}`, "Start the observability stack: codeharness stack up"));
|
|
11425
|
+
} else {
|
|
11426
|
+
rMetric = `runtime: ${d.coveragePercent}%`;
|
|
11427
|
+
if (d.coveragePercent < 50) {
|
|
11428
|
+
rStatus = "warn";
|
|
11429
|
+
gaps.push(gap("observability", `Runtime coverage low: ${d.coveragePercent}%`, "Add telemetry instrumentation to more modules"));
|
|
11430
|
+
}
|
|
11431
|
+
}
|
|
11432
|
+
} else {
|
|
11433
|
+
rStatus = "warn";
|
|
11434
|
+
rMetric = "runtime: skipped (validation failed)";
|
|
11435
|
+
gaps.push(gap("observability", `Runtime validation failed: ${rr.error}`, "Ensure observability backend is running"));
|
|
11436
|
+
}
|
|
11437
|
+
} catch {
|
|
11438
|
+
rStatus = "warn";
|
|
11439
|
+
rMetric = "runtime: skipped (error)";
|
|
11440
|
+
gaps.push(gap("observability", "Runtime validation threw an unexpected error", "Check observability stack health"));
|
|
11441
|
+
}
|
|
11442
|
+
return dimOk("observability", worstStatus(sStatus, rStatus), `${sMetric}, ${rMetric}`, gaps);
|
|
11443
|
+
} catch (err) {
|
|
11444
|
+
return dimCatch("observability", err);
|
|
11445
|
+
}
|
|
11446
|
+
}
|
|
11447
|
+
function checkTesting(projectDir) {
|
|
11448
|
+
try {
|
|
11449
|
+
const r = checkOnlyCoverage(projectDir);
|
|
11450
|
+
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")]);
|
|
11451
|
+
const pct = r.coveragePercent;
|
|
11452
|
+
const gaps = [];
|
|
11453
|
+
let status = "pass";
|
|
11454
|
+
if (pct < 50) {
|
|
11455
|
+
status = "fail";
|
|
11456
|
+
gaps.push(gap("testing", `Test coverage critically low: ${pct}%`, "Add unit tests to increase coverage above 50%"));
|
|
11457
|
+
} else if (pct < 80) {
|
|
11458
|
+
status = "warn";
|
|
11459
|
+
gaps.push(gap("testing", `Test coverage below target: ${pct}%`, "Add tests to reach 80% coverage target"));
|
|
11460
|
+
}
|
|
11461
|
+
return dimOk("testing", status, `${pct}%`, gaps);
|
|
11462
|
+
} catch (err) {
|
|
11463
|
+
return dimCatch("testing", err);
|
|
11464
|
+
}
|
|
11465
|
+
}
|
|
11466
|
+
function checkDocumentation(projectDir) {
|
|
11467
|
+
try {
|
|
11468
|
+
const report = scanDocHealth(projectDir);
|
|
11469
|
+
const gaps = [];
|
|
11470
|
+
const { fresh, stale, missing } = report.summary;
|
|
11471
|
+
let status = "pass";
|
|
11472
|
+
if (missing > 0) {
|
|
11473
|
+
status = "fail";
|
|
11474
|
+
for (const doc of report.documents) if (doc.grade === "missing") gaps.push(gap("documentation", `Missing: ${doc.path} \u2014 ${doc.reason}`, `Create ${doc.path}`));
|
|
11475
|
+
}
|
|
11476
|
+
if (stale > 0) {
|
|
11477
|
+
if (status !== "fail") status = "warn";
|
|
11478
|
+
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`));
|
|
11479
|
+
}
|
|
11480
|
+
return dimOk("documentation", status, `${fresh} fresh, ${stale} stale, ${missing} missing`, gaps);
|
|
11481
|
+
} catch (err) {
|
|
11482
|
+
return dimCatch("documentation", err);
|
|
11483
|
+
}
|
|
11484
|
+
}
|
|
11485
|
+
function checkVerification(projectDir) {
|
|
11486
|
+
try {
|
|
11487
|
+
const gaps = [];
|
|
11488
|
+
const sprintPath = join33(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
|
|
11489
|
+
if (!existsSync34(sprintPath)) return dimOk("verification", "warn", "no sprint data", [gap("verification", "No sprint-status.yaml found", "Run sprint planning to create sprint status")]);
|
|
11490
|
+
const vDir = join33(projectDir, "verification");
|
|
11491
|
+
let proofCount = 0, totalChecked = 0;
|
|
11492
|
+
if (existsSync34(vDir)) {
|
|
11493
|
+
for (const file of readdirSafe(vDir)) {
|
|
11494
|
+
if (!file.endsWith("-proof.md")) continue;
|
|
11495
|
+
totalChecked++;
|
|
11496
|
+
const r = parseProof(join33(vDir, file));
|
|
11497
|
+
if (isOk(r) && r.data.passed) {
|
|
11498
|
+
proofCount++;
|
|
11499
|
+
} else {
|
|
11500
|
+
gaps.push(gap("verification", `Story ${file.replace("-proof.md", "")} proof incomplete or failing`, `Run codeharness verify ${file.replace("-proof.md", "")}`));
|
|
11501
|
+
}
|
|
11502
|
+
}
|
|
11503
|
+
}
|
|
11504
|
+
let status = "pass";
|
|
11505
|
+
if (totalChecked === 0) {
|
|
11506
|
+
status = "warn";
|
|
11507
|
+
gaps.push(gap("verification", "No verification proofs found", "Run codeharness verify for completed stories"));
|
|
11508
|
+
} else if (proofCount < totalChecked) {
|
|
11509
|
+
status = "warn";
|
|
11510
|
+
}
|
|
11511
|
+
return dimOk("verification", status, totalChecked > 0 ? `${proofCount}/${totalChecked} verified` : "no proofs", gaps);
|
|
11512
|
+
} catch (err) {
|
|
11513
|
+
return dimCatch("verification", err);
|
|
11514
|
+
}
|
|
11515
|
+
}
|
|
11516
|
+
function checkInfrastructure(projectDir) {
|
|
11517
|
+
try {
|
|
11518
|
+
const dfPath = join33(projectDir, "Dockerfile");
|
|
11519
|
+
if (!existsSync34(dfPath)) return dimOk("infrastructure", "fail", "no Dockerfile", [gap("infrastructure", "No Dockerfile found", "Create a Dockerfile for containerized deployment")]);
|
|
11520
|
+
let content;
|
|
11521
|
+
try {
|
|
11522
|
+
content = readFileSync30(dfPath, "utf-8");
|
|
11523
|
+
} catch {
|
|
11524
|
+
return dimOk("infrastructure", "warn", "Dockerfile unreadable", [gap("infrastructure", "Dockerfile exists but could not be read", "Check Dockerfile permissions")]);
|
|
11525
|
+
}
|
|
11526
|
+
const fromLines = content.split("\n").filter((l) => /^\s*FROM\s+/i.test(l));
|
|
11527
|
+
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")]);
|
|
11528
|
+
const gaps = [];
|
|
11529
|
+
let hasUnpinned = false;
|
|
11530
|
+
for (const line of fromLines) {
|
|
11531
|
+
const ref = line.replace(/^\s*FROM\s+/i, "").split(/\s+/)[0];
|
|
11532
|
+
if (ref.endsWith(":latest")) {
|
|
11533
|
+
hasUnpinned = true;
|
|
11534
|
+
gaps.push(gap("infrastructure", `Unpinned base image: ${ref}`, `Pin ${ref} to a specific version tag`));
|
|
11535
|
+
} else if (!ref.includes(":") && !ref.includes("@")) {
|
|
11536
|
+
hasUnpinned = true;
|
|
11537
|
+
gaps.push(gap("infrastructure", `Unpinned base image (no tag): ${ref}`, `Pin ${ref} to a specific version tag (e.g., ${ref}:22-slim)`));
|
|
11538
|
+
}
|
|
11539
|
+
}
|
|
11540
|
+
const status = hasUnpinned ? "warn" : "pass";
|
|
11541
|
+
const metric = hasUnpinned ? `Dockerfile exists (${gaps.length} issue${gaps.length !== 1 ? "s" : ""})` : "Dockerfile valid";
|
|
11542
|
+
return dimOk("infrastructure", status, metric, gaps);
|
|
11543
|
+
} catch (err) {
|
|
11544
|
+
return dimCatch("infrastructure", err);
|
|
11545
|
+
}
|
|
11546
|
+
}
|
|
11547
|
+
function readdirSafe(dir) {
|
|
11548
|
+
try {
|
|
11549
|
+
return readdirSync7(dir);
|
|
11550
|
+
} catch {
|
|
11551
|
+
return [];
|
|
11552
|
+
}
|
|
11553
|
+
}
|
|
11554
|
+
|
|
11555
|
+
// src/modules/audit/report.ts
|
|
11556
|
+
var STATUS_PREFIX = {
|
|
11557
|
+
pass: "[OK]",
|
|
11558
|
+
fail: "[FAIL]",
|
|
11559
|
+
warn: "[WARN]"
|
|
11560
|
+
};
|
|
11561
|
+
function formatAuditHuman(result) {
|
|
11562
|
+
const lines = [];
|
|
11563
|
+
for (const dimension of Object.values(result.dimensions)) {
|
|
11564
|
+
const prefix = STATUS_PREFIX[dimension.status] ?? "[WARN]";
|
|
11565
|
+
lines.push(`${prefix} ${dimension.name}: ${dimension.metric}`);
|
|
11566
|
+
for (const gap2 of dimension.gaps) {
|
|
11567
|
+
lines.push(` [WARN] ${gap2.description} -- fix: ${gap2.suggestedFix}`);
|
|
11568
|
+
}
|
|
11569
|
+
}
|
|
11570
|
+
const overallPrefix = STATUS_PREFIX[result.overallStatus] ?? "[WARN]";
|
|
11571
|
+
lines.push("");
|
|
11572
|
+
lines.push(
|
|
11573
|
+
`${overallPrefix} Audit complete: ${result.gapCount} gap${result.gapCount !== 1 ? "s" : ""} found (${result.durationMs}ms)`
|
|
11574
|
+
);
|
|
11575
|
+
return lines;
|
|
11576
|
+
}
|
|
11577
|
+
function formatAuditJson(result) {
|
|
11578
|
+
return result;
|
|
11579
|
+
}
|
|
11580
|
+
|
|
11581
|
+
// src/modules/audit/fix-generator.ts
|
|
11582
|
+
import { existsSync as existsSync35, writeFileSync as writeFileSync19, mkdirSync as mkdirSync11 } from "fs";
|
|
11583
|
+
import { join as join34, dirname as dirname8 } from "path";
|
|
11584
|
+
function buildStoryKey(gap2, index) {
|
|
11585
|
+
const safeDimension = gap2.dimension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
11586
|
+
return `audit-fix-${safeDimension}-${index}`;
|
|
11587
|
+
}
|
|
11588
|
+
function buildStoryMarkdown(gap2, key) {
|
|
11589
|
+
return [
|
|
11590
|
+
`# Fix: ${gap2.dimension} \u2014 ${gap2.description}`,
|
|
11591
|
+
"",
|
|
11592
|
+
"Status: backlog",
|
|
11593
|
+
"",
|
|
11594
|
+
"## Story",
|
|
11595
|
+
"",
|
|
11596
|
+
`As an operator, I need ${gap2.description} fixed so that audit compliance improves.`,
|
|
11597
|
+
"",
|
|
11598
|
+
"## Acceptance Criteria",
|
|
11599
|
+
"",
|
|
11600
|
+
`1. **Given** ${gap2.description}, **When** the fix is applied, **Then** ${gap2.suggestedFix}.`,
|
|
11601
|
+
"",
|
|
11602
|
+
"## Dev Notes",
|
|
11603
|
+
"",
|
|
11604
|
+
"This is an auto-generated fix story created by `codeharness audit --fix`.",
|
|
11605
|
+
`**Audit Gap:** ${gap2.dimension}: ${gap2.description}`,
|
|
11606
|
+
`**Suggested Fix:** ${gap2.suggestedFix}`,
|
|
11607
|
+
""
|
|
11608
|
+
].join("\n");
|
|
11609
|
+
}
|
|
11610
|
+
function generateFixStories(auditResult) {
|
|
11611
|
+
try {
|
|
11612
|
+
const stories = [];
|
|
11613
|
+
let created = 0;
|
|
11614
|
+
let skipped = 0;
|
|
11615
|
+
const artifactsDir = join34(
|
|
11616
|
+
process.cwd(),
|
|
11617
|
+
"_bmad-output",
|
|
11618
|
+
"implementation-artifacts"
|
|
11619
|
+
);
|
|
11620
|
+
for (const dimension of Object.values(auditResult.dimensions)) {
|
|
11621
|
+
for (let i = 0; i < dimension.gaps.length; i++) {
|
|
11622
|
+
const gap2 = dimension.gaps[i];
|
|
11623
|
+
const key = buildStoryKey(gap2, i + 1);
|
|
11624
|
+
const filePath = join34(artifactsDir, `${key}.md`);
|
|
11625
|
+
if (existsSync35(filePath)) {
|
|
11626
|
+
stories.push({
|
|
11627
|
+
key,
|
|
11628
|
+
filePath,
|
|
11629
|
+
gap: gap2,
|
|
11630
|
+
skipped: true,
|
|
11631
|
+
skipReason: "Story file already exists"
|
|
11632
|
+
});
|
|
11633
|
+
skipped++;
|
|
11634
|
+
continue;
|
|
11635
|
+
}
|
|
11636
|
+
const markdown = buildStoryMarkdown(gap2, key);
|
|
11637
|
+
mkdirSync11(dirname8(filePath), { recursive: true });
|
|
11638
|
+
writeFileSync19(filePath, markdown, "utf-8");
|
|
11639
|
+
stories.push({ key, filePath, gap: gap2, skipped: false });
|
|
11640
|
+
created++;
|
|
11641
|
+
}
|
|
11642
|
+
}
|
|
11643
|
+
return ok2({ stories, created, skipped });
|
|
11644
|
+
} catch (err) {
|
|
11645
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11646
|
+
return fail2(`Failed to generate fix stories: ${msg}`);
|
|
11647
|
+
}
|
|
11648
|
+
}
|
|
11649
|
+
function addFixStoriesToState(stories) {
|
|
11650
|
+
const newStories = stories.filter((s) => !s.skipped);
|
|
11651
|
+
if (newStories.length === 0) {
|
|
11652
|
+
return ok2(void 0);
|
|
11653
|
+
}
|
|
11654
|
+
const stateResult = getSprintState2();
|
|
11655
|
+
if (!stateResult.success) {
|
|
11656
|
+
return fail2(stateResult.error);
|
|
11657
|
+
}
|
|
11658
|
+
const current = stateResult.data;
|
|
11659
|
+
const updatedStories = { ...current.stories };
|
|
11660
|
+
for (const story of newStories) {
|
|
11661
|
+
updatedStories[story.key] = {
|
|
11662
|
+
status: "backlog",
|
|
11663
|
+
attempts: 0,
|
|
11664
|
+
lastAttempt: null,
|
|
11665
|
+
lastError: null,
|
|
11666
|
+
proofPath: null,
|
|
11667
|
+
acResults: null
|
|
11668
|
+
};
|
|
11669
|
+
}
|
|
11670
|
+
const updatedSprint = computeSprintCounts2(updatedStories);
|
|
11671
|
+
return writeStateAtomic2({
|
|
11672
|
+
...current,
|
|
11673
|
+
sprint: updatedSprint,
|
|
11674
|
+
stories: updatedStories
|
|
11675
|
+
});
|
|
11676
|
+
}
|
|
11677
|
+
|
|
11678
|
+
// src/modules/audit/index.ts
|
|
11679
|
+
async function runAudit2(projectDir) {
|
|
11680
|
+
const start = performance.now();
|
|
11681
|
+
const [
|
|
11682
|
+
obsResult,
|
|
11683
|
+
testResult,
|
|
11684
|
+
docResult,
|
|
11685
|
+
verifyResult,
|
|
11686
|
+
infraResult
|
|
11687
|
+
] = await Promise.all([
|
|
11688
|
+
checkObservability(projectDir),
|
|
11689
|
+
Promise.resolve(checkTesting(projectDir)),
|
|
11690
|
+
Promise.resolve(checkDocumentation(projectDir)),
|
|
11691
|
+
Promise.resolve(checkVerification(projectDir)),
|
|
11692
|
+
Promise.resolve(checkInfrastructure(projectDir))
|
|
11693
|
+
]);
|
|
11694
|
+
const dimensions = {};
|
|
11695
|
+
const allResults = [obsResult, testResult, docResult, verifyResult, infraResult];
|
|
11696
|
+
for (const result of allResults) {
|
|
11697
|
+
if (result.success) {
|
|
11698
|
+
dimensions[result.data.name] = result.data;
|
|
11699
|
+
}
|
|
11700
|
+
}
|
|
11701
|
+
const statuses = Object.values(dimensions).map((d) => d.status);
|
|
11702
|
+
const overallStatus = computeOverallStatus(statuses);
|
|
11703
|
+
const gapCount = Object.values(dimensions).reduce((sum, d) => sum + d.gaps.length, 0);
|
|
11704
|
+
const durationMs = Math.round(performance.now() - start);
|
|
11705
|
+
return ok2({ dimensions, overallStatus, gapCount, durationMs });
|
|
11706
|
+
}
|
|
11707
|
+
function computeOverallStatus(statuses) {
|
|
11708
|
+
if (statuses.includes("fail")) return "fail";
|
|
11709
|
+
if (statuses.includes("warn")) return "warn";
|
|
11710
|
+
return "pass";
|
|
11711
|
+
}
|
|
11712
|
+
|
|
11713
|
+
// src/commands/audit.ts
|
|
11714
|
+
function registerAuditCommand(program) {
|
|
11715
|
+
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) => {
|
|
11716
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
11717
|
+
const isJson = opts.json === true || globalOpts.json === true;
|
|
11718
|
+
const isFix = opts.fix === true;
|
|
11719
|
+
const preconditions = runPreconditions();
|
|
11720
|
+
if (!preconditions.canProceed) {
|
|
11721
|
+
if (isJson) {
|
|
11722
|
+
jsonOutput({
|
|
11723
|
+
status: "fail",
|
|
11724
|
+
message: "Harness not initialized -- run codeharness init first"
|
|
11725
|
+
});
|
|
11726
|
+
} else {
|
|
11727
|
+
fail("Harness not initialized -- run codeharness init first");
|
|
11728
|
+
}
|
|
11729
|
+
process.exitCode = 1;
|
|
11730
|
+
return;
|
|
11731
|
+
}
|
|
11732
|
+
const result = await runAudit2(process.cwd());
|
|
11733
|
+
if (!result.success) {
|
|
11734
|
+
if (isJson) {
|
|
11735
|
+
jsonOutput({ status: "fail", message: result.error });
|
|
11736
|
+
} else {
|
|
11737
|
+
fail(result.error);
|
|
11738
|
+
}
|
|
11739
|
+
process.exitCode = 1;
|
|
11740
|
+
return;
|
|
11741
|
+
}
|
|
11742
|
+
let fixStories;
|
|
11743
|
+
let fixStateError;
|
|
11744
|
+
if (isFix) {
|
|
11745
|
+
if (result.data.gapCount === 0) {
|
|
11746
|
+
if (!isJson) {
|
|
11747
|
+
ok("No gaps found -- nothing to fix");
|
|
11748
|
+
}
|
|
11749
|
+
} else {
|
|
11750
|
+
const fixResult = generateFixStories(result.data);
|
|
11751
|
+
fixStories = fixResult;
|
|
11752
|
+
if (fixResult.success) {
|
|
11753
|
+
const stateResult = addFixStoriesToState(fixResult.data.stories);
|
|
11754
|
+
if (!stateResult.success) {
|
|
11755
|
+
fixStateError = stateResult.error;
|
|
11756
|
+
if (!isJson) {
|
|
11757
|
+
fail(`Failed to update sprint state: ${stateResult.error}`);
|
|
11758
|
+
}
|
|
11759
|
+
}
|
|
11760
|
+
if (!isJson) {
|
|
11761
|
+
info(`Generated ${fixResult.data.created} fix stories (${fixResult.data.skipped} skipped)`);
|
|
11762
|
+
}
|
|
11763
|
+
} else if (!isJson) {
|
|
11764
|
+
fail(fixResult.error);
|
|
11765
|
+
}
|
|
11766
|
+
}
|
|
11767
|
+
}
|
|
11768
|
+
if (isJson) {
|
|
11769
|
+
const jsonData = formatAuditJson(result.data);
|
|
11770
|
+
if (isFix) {
|
|
11771
|
+
if (result.data.gapCount === 0) {
|
|
11772
|
+
jsonData.fixStories = [];
|
|
11773
|
+
} else if (fixStories && fixStories.success) {
|
|
11774
|
+
jsonData.fixStories = fixStories.data.stories.map((s) => ({
|
|
11775
|
+
key: s.key,
|
|
11776
|
+
filePath: s.filePath,
|
|
11777
|
+
gap: s.gap,
|
|
11778
|
+
...s.skipped ? { skipped: true } : {}
|
|
11779
|
+
}));
|
|
11780
|
+
if (fixStateError) {
|
|
11781
|
+
jsonData.fixStateError = fixStateError;
|
|
11782
|
+
}
|
|
11783
|
+
} else if (fixStories && !fixStories.success) {
|
|
11784
|
+
jsonData.fixStories = [];
|
|
11785
|
+
jsonData.fixError = fixStories.error;
|
|
11786
|
+
}
|
|
11787
|
+
}
|
|
11788
|
+
jsonOutput(jsonData);
|
|
11789
|
+
} else if (!isFix || result.data.gapCount > 0) {
|
|
11790
|
+
const lines = formatAuditHuman(result.data);
|
|
11791
|
+
for (const line of lines) {
|
|
11792
|
+
console.log(line);
|
|
11793
|
+
}
|
|
11794
|
+
}
|
|
11795
|
+
if (result.data.overallStatus === "fail") {
|
|
11796
|
+
process.exitCode = 1;
|
|
11797
|
+
}
|
|
11798
|
+
});
|
|
11799
|
+
}
|
|
11800
|
+
|
|
10696
11801
|
// src/index.ts
|
|
10697
|
-
var VERSION = true ? "0.
|
|
11802
|
+
var VERSION = true ? "0.22.1" : "0.0.0-dev";
|
|
10698
11803
|
function createProgram() {
|
|
10699
11804
|
const program = new Command();
|
|
10700
11805
|
program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
|
|
@@ -10719,6 +11824,8 @@ function createProgram() {
|
|
|
10719
11824
|
registerValidateStateCommand(program);
|
|
10720
11825
|
registerValidateCommand(program);
|
|
10721
11826
|
registerProgressCommand(program);
|
|
11827
|
+
registerObservabilityGateCommand(program);
|
|
11828
|
+
registerAuditCommand(program);
|
|
10722
11829
|
return program;
|
|
10723
11830
|
}
|
|
10724
11831
|
if (!process.env["VITEST"]) {
|