codeharness 0.22.2 → 0.23.0
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 +691 -1707
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
isCollectorRunning,
|
|
9
9
|
isDockerAvailable,
|
|
10
10
|
isSharedStackRunning,
|
|
11
|
-
isStackRunning,
|
|
12
11
|
startCollectorOnly,
|
|
13
12
|
startSharedStack,
|
|
14
13
|
stopCollectorOnly,
|
|
@@ -1941,7 +1940,7 @@ async function scaffoldDocs(opts) {
|
|
|
1941
1940
|
}
|
|
1942
1941
|
|
|
1943
1942
|
// src/modules/infra/init-project.ts
|
|
1944
|
-
var HARNESS_VERSION = true ? "0.
|
|
1943
|
+
var HARNESS_VERSION = true ? "0.23.0" : "0.0.0-dev";
|
|
1945
1944
|
function failResult(opts, error) {
|
|
1946
1945
|
return {
|
|
1947
1946
|
status: "fail",
|
|
@@ -2347,45 +2346,6 @@ function updateSprintStatus(storyKey, newStatus, dir) {
|
|
|
2347
2346
|
function escapeRegExp(s) {
|
|
2348
2347
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2349
2348
|
}
|
|
2350
|
-
function nextEpicNumber(statuses) {
|
|
2351
|
-
let max = -1;
|
|
2352
|
-
for (const key of Object.keys(statuses)) {
|
|
2353
|
-
const match = key.match(/^epic-(\d+)$/);
|
|
2354
|
-
if (match) {
|
|
2355
|
-
const n = parseInt(match[1], 10);
|
|
2356
|
-
if (n > max) max = n;
|
|
2357
|
-
}
|
|
2358
|
-
}
|
|
2359
|
-
return max + 1;
|
|
2360
|
-
}
|
|
2361
|
-
function slugify(title) {
|
|
2362
|
-
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
2363
|
-
}
|
|
2364
|
-
function appendOnboardingEpicToSprint(stories, dir) {
|
|
2365
|
-
const root = dir ?? process.cwd();
|
|
2366
|
-
const filePath = join8(root, SPRINT_STATUS_PATH);
|
|
2367
|
-
if (!existsSync10(filePath)) {
|
|
2368
|
-
warn(`sprint-status.yaml not found at ${filePath}, cannot append onboarding epic`);
|
|
2369
|
-
return { epicNumber: -1, storyKeys: [] };
|
|
2370
|
-
}
|
|
2371
|
-
const statuses = readSprintStatus(dir);
|
|
2372
|
-
const epicNum = nextEpicNumber(statuses);
|
|
2373
|
-
const storyKeys = [];
|
|
2374
|
-
const lines = [""];
|
|
2375
|
-
lines.push(` epic-${epicNum}: backlog`);
|
|
2376
|
-
for (let i = 0; i < stories.length; i++) {
|
|
2377
|
-
const slug = slugify(stories[i].title);
|
|
2378
|
-
const storyKey = `${epicNum}-${i + 1}-${slug}`;
|
|
2379
|
-
storyKeys.push(storyKey);
|
|
2380
|
-
lines.push(` ${storyKey}: backlog`);
|
|
2381
|
-
}
|
|
2382
|
-
lines.push(` epic-${epicNum}-retrospective: optional`);
|
|
2383
|
-
lines.push("");
|
|
2384
|
-
const content = readFileSync8(filePath, "utf-8");
|
|
2385
|
-
const updated = content.trimEnd() + "\n" + lines.join("\n");
|
|
2386
|
-
writeFileSync5(filePath, updated, "utf-8");
|
|
2387
|
-
return { epicNumber: epicNum, storyKeys };
|
|
2388
|
-
}
|
|
2389
2349
|
function storyKeyFromPath(filePath) {
|
|
2390
2350
|
const base = filePath.split("/").pop() ?? filePath;
|
|
2391
2351
|
return base.replace(/\.md$/, "");
|
|
@@ -2798,123 +2758,114 @@ function parseResultEvent(parsed) {
|
|
|
2798
2758
|
import { render as inkRender } from "ink";
|
|
2799
2759
|
|
|
2800
2760
|
// src/lib/ink-components.tsx
|
|
2801
|
-
import { Text, Box } from "ink";
|
|
2761
|
+
import { Text, Box, Static } from "ink";
|
|
2802
2762
|
import { Spinner } from "@inkjs/ui";
|
|
2803
2763
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2804
2764
|
function Header({ info: info2 }) {
|
|
2805
2765
|
if (!info2) return null;
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
"
|
|
2815
|
-
|
|
2766
|
+
const pct = info2.total > 0 ? Math.round(info2.done / info2.total * 100) : 0;
|
|
2767
|
+
return /* @__PURE__ */ jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
2768
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "\u25C6 " }),
|
|
2769
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: info2.storyKey || "(waiting)" }),
|
|
2770
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2014 " }),
|
|
2771
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", children: info2.phase || "..." }),
|
|
2772
|
+
info2.elapsed && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u2502 ${info2.elapsed}` }),
|
|
2773
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 Sprint: " }),
|
|
2774
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: info2.done }),
|
|
2775
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "/" }),
|
|
2776
|
+
/* @__PURE__ */ jsx(Text, { children: String(info2.total) }),
|
|
2777
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` (${pct}%)` })
|
|
2778
|
+
] });
|
|
2779
|
+
}
|
|
2780
|
+
function shortKey(key) {
|
|
2781
|
+
const m = key.match(/^(\d+-\d+)/);
|
|
2782
|
+
return m ? m[1] : key;
|
|
2783
|
+
}
|
|
2784
|
+
function StoryBreakdown({ stories }) {
|
|
2785
|
+
if (stories.length === 0) return null;
|
|
2786
|
+
const groups = {};
|
|
2787
|
+
for (const s of stories) {
|
|
2788
|
+
if (!groups[s.status]) groups[s.status] = [];
|
|
2789
|
+
groups[s.status].push(s.key);
|
|
2790
|
+
}
|
|
2791
|
+
return /* @__PURE__ */ jsxs(Box, { paddingX: 1, gap: 2, children: [
|
|
2792
|
+
groups["done"]?.length && /* @__PURE__ */ jsxs(Text, { children: [
|
|
2793
|
+
/* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
2794
|
+
groups["done"].length,
|
|
2795
|
+
" \u2713"
|
|
2796
|
+
] }),
|
|
2797
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " done" })
|
|
2798
|
+
] }),
|
|
2799
|
+
groups["in-progress"]?.map((k) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
2800
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u25C6 " }),
|
|
2801
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: k })
|
|
2802
|
+
] }, k)),
|
|
2803
|
+
groups["pending"]?.length && /* @__PURE__ */ jsxs(Text, { children: [
|
|
2804
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "next: " }),
|
|
2805
|
+
/* @__PURE__ */ jsx(Text, { children: groups["pending"].slice(0, 3).map((k) => shortKey(k)).join(" ") }),
|
|
2806
|
+
groups["pending"].length > 3 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` +${groups["pending"].length - 3}` })
|
|
2807
|
+
] }),
|
|
2808
|
+
groups["failed"]?.map((k) => /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsxs(Text, { color: "red", children: [
|
|
2809
|
+
"\u2717 ",
|
|
2810
|
+
shortKey(k)
|
|
2811
|
+
] }) }, k)),
|
|
2812
|
+
groups["blocked"]?.map((k) => /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
2813
|
+
"\u2715 ",
|
|
2814
|
+
shortKey(k)
|
|
2815
|
+
] }) }, k))
|
|
2816
|
+
] });
|
|
2817
|
+
}
|
|
2818
|
+
var MESSAGE_STYLE = {
|
|
2819
|
+
ok: { prefix: "[OK]", color: "green" },
|
|
2820
|
+
warn: { prefix: "[WARN]", color: "yellow" },
|
|
2821
|
+
fail: { prefix: "[FAIL]", color: "red" }
|
|
2822
|
+
};
|
|
2823
|
+
function StoryMessageLine({ msg }) {
|
|
2824
|
+
const style = MESSAGE_STYLE[msg.type];
|
|
2825
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2826
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
2827
|
+
/* @__PURE__ */ jsx(Text, { color: style.color, bold: true, children: style.prefix }),
|
|
2828
|
+
/* @__PURE__ */ jsx(Text, { children: ` Story ${msg.key}: ${msg.message}` })
|
|
2829
|
+
] }),
|
|
2830
|
+
msg.details?.map((d, j) => /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u2514 ${d}` }, j))
|
|
2816
2831
|
] });
|
|
2817
2832
|
}
|
|
2818
2833
|
function CompletedTool({ entry }) {
|
|
2819
|
-
const argsSummary = entry.args.length > 60 ? entry.args.slice(0, 60) + "
|
|
2820
|
-
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
2821
|
-
"\u2713
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2834
|
+
const argsSummary = entry.args.length > 60 ? entry.args.slice(0, 60) + "\u2026" : entry.args;
|
|
2835
|
+
return /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
2836
|
+
/* @__PURE__ */ jsx(Text, { color: "green", children: "\u2713 " }),
|
|
2837
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "[" }),
|
|
2838
|
+
/* @__PURE__ */ jsx(Text, { children: entry.name }),
|
|
2839
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "] " }),
|
|
2840
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: argsSummary })
|
|
2825
2841
|
] });
|
|
2826
2842
|
}
|
|
2843
|
+
var VISIBLE_COMPLETED_TOOLS = 5;
|
|
2827
2844
|
function CompletedTools({ tools }) {
|
|
2828
|
-
|
|
2845
|
+
const visible = tools.slice(-VISIBLE_COMPLETED_TOOLS);
|
|
2846
|
+
const hidden = tools.length - visible.length;
|
|
2847
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2848
|
+
hidden > 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u2026 ${hidden} earlier tools` }),
|
|
2849
|
+
visible.map((entry, i) => /* @__PURE__ */ jsx(CompletedTool, { entry }, i))
|
|
2850
|
+
] });
|
|
2829
2851
|
}
|
|
2830
2852
|
function ActiveTool({ name }) {
|
|
2831
2853
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
2832
|
-
/* @__PURE__ */
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
] }),
|
|
2854
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u26A1 " }),
|
|
2855
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "[" }),
|
|
2856
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: name }),
|
|
2857
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "] " }),
|
|
2837
2858
|
/* @__PURE__ */ jsx(Spinner, { label: "" })
|
|
2838
2859
|
] });
|
|
2839
2860
|
}
|
|
2840
2861
|
function LastThought({ text }) {
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
"\u{1F4AD} ",
|
|
2845
|
-
truncated
|
|
2862
|
+
return /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
2863
|
+
/* @__PURE__ */ jsx(Text, { children: "\u{1F4AD} " }),
|
|
2864
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: text })
|
|
2846
2865
|
] });
|
|
2847
2866
|
}
|
|
2848
|
-
function truncateToWidth(text, maxWidth) {
|
|
2849
|
-
let width = 0;
|
|
2850
|
-
let result = "";
|
|
2851
|
-
for (const char of text) {
|
|
2852
|
-
const cp = char.codePointAt(0);
|
|
2853
|
-
const charWidth = cp <= 126 ? 1 : 2;
|
|
2854
|
-
if (width + charWidth > maxWidth) {
|
|
2855
|
-
return result;
|
|
2856
|
-
}
|
|
2857
|
-
width += charWidth;
|
|
2858
|
-
result += char;
|
|
2859
|
-
}
|
|
2860
|
-
return result;
|
|
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
|
-
}
|
|
2916
2867
|
function RetryNotice({ info: info2 }) {
|
|
2917
|
-
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
2868
|
+
return /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
2918
2869
|
"\u23F3 API retry ",
|
|
2919
2870
|
info2.attempt,
|
|
2920
2871
|
" (waiting ",
|
|
@@ -2926,13 +2877,15 @@ function App({
|
|
|
2926
2877
|
state
|
|
2927
2878
|
}) {
|
|
2928
2879
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2880
|
+
/* @__PURE__ */ jsx(Static, { items: state.messages, children: (msg, i) => /* @__PURE__ */ jsx(StoryMessageLine, { msg }, i) }),
|
|
2929
2881
|
/* @__PURE__ */ jsx(Header, { info: state.sprintInfo }),
|
|
2930
2882
|
/* @__PURE__ */ jsx(StoryBreakdown, { stories: state.stories }),
|
|
2931
|
-
/* @__PURE__ */
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2883
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [
|
|
2884
|
+
/* @__PURE__ */ jsx(CompletedTools, { tools: state.completedTools }),
|
|
2885
|
+
state.activeTool && /* @__PURE__ */ jsx(ActiveTool, { name: state.activeTool.name }),
|
|
2886
|
+
state.lastThought && /* @__PURE__ */ jsx(LastThought, { text: state.lastThought }),
|
|
2887
|
+
state.retryInfo && /* @__PURE__ */ jsx(RetryNotice, { info: state.retryInfo })
|
|
2888
|
+
] })
|
|
2936
2889
|
] });
|
|
2937
2890
|
}
|
|
2938
2891
|
|
|
@@ -2968,7 +2921,11 @@ function startRenderer(options) {
|
|
|
2968
2921
|
let cleaned = false;
|
|
2969
2922
|
const inkInstance = inkRender(/* @__PURE__ */ jsx2(App, { state }), {
|
|
2970
2923
|
exitOnCtrlC: false,
|
|
2971
|
-
patchConsole: false
|
|
2924
|
+
patchConsole: false,
|
|
2925
|
+
incrementalRendering: true,
|
|
2926
|
+
// Only redraw changed lines (v6.5+)
|
|
2927
|
+
maxFps: 15
|
|
2928
|
+
// Dashboard doesn't need 30fps
|
|
2972
2929
|
});
|
|
2973
2930
|
function rerender() {
|
|
2974
2931
|
if (!cleaned) {
|
|
@@ -3068,11 +3025,11 @@ var OLD_FILES = {
|
|
|
3068
3025
|
sprintStatusYaml: "_bmad-output/implementation-artifacts/sprint-status.yaml",
|
|
3069
3026
|
sessionIssues: "_bmad-output/implementation-artifacts/.session-issues.md"
|
|
3070
3027
|
};
|
|
3071
|
-
function resolve(
|
|
3072
|
-
return join9(process.cwd(),
|
|
3028
|
+
function resolve(relative2) {
|
|
3029
|
+
return join9(process.cwd(), relative2);
|
|
3073
3030
|
}
|
|
3074
|
-
function readIfExists(
|
|
3075
|
-
const p = resolve(
|
|
3031
|
+
function readIfExists(relative2) {
|
|
3032
|
+
const p = resolve(relative2);
|
|
3076
3033
|
if (!existsSync11(p)) return null;
|
|
3077
3034
|
try {
|
|
3078
3035
|
return readFileSync9(p, "utf-8");
|
|
@@ -4610,14 +4567,6 @@ function getExtension(filename) {
|
|
|
4610
4567
|
function isTestFile(filename) {
|
|
4611
4568
|
return filename.includes(".test.") || filename.includes(".spec.") || filename.includes("__tests__") || filename.startsWith("test_");
|
|
4612
4569
|
}
|
|
4613
|
-
function isDocStale(docPath, codeDir) {
|
|
4614
|
-
if (!existsSync18(docPath)) return true;
|
|
4615
|
-
if (!existsSync18(codeDir)) return false;
|
|
4616
|
-
const docMtime = statSync(docPath).mtime;
|
|
4617
|
-
const newestCode = getNewestSourceMtime(codeDir);
|
|
4618
|
-
if (newestCode === null) return false;
|
|
4619
|
-
return newestCode.getTime() > docMtime.getTime();
|
|
4620
|
-
}
|
|
4621
4570
|
function getNewestSourceMtime(dir) {
|
|
4622
4571
|
let newest = null;
|
|
4623
4572
|
function walk(current) {
|
|
@@ -7721,9 +7670,9 @@ function checkPerFileCoverage(floor, dir) {
|
|
|
7721
7670
|
const funcs = data.functions?.pct ?? 0;
|
|
7722
7671
|
const lines = data.lines?.pct ?? 0;
|
|
7723
7672
|
if (stmts < floor) {
|
|
7724
|
-
const
|
|
7673
|
+
const relative2 = key.startsWith(baseDir) ? key.slice(baseDir.length + 1) : key;
|
|
7725
7674
|
violations.push({
|
|
7726
|
-
file:
|
|
7675
|
+
file: relative2,
|
|
7727
7676
|
statements: stmts,
|
|
7728
7677
|
branches,
|
|
7729
7678
|
functions: funcs,
|
|
@@ -7806,85 +7755,6 @@ function runPreconditions(dir) {
|
|
|
7806
7755
|
hooks: hooksCheck.ok
|
|
7807
7756
|
};
|
|
7808
7757
|
}
|
|
7809
|
-
var STORY_KEY_PATTERN2 = /^\d+-\d+-/;
|
|
7810
|
-
function findVerificationGaps(dir) {
|
|
7811
|
-
const statuses = readSprintStatus(dir);
|
|
7812
|
-
const root = dir ?? process.cwd();
|
|
7813
|
-
const unverified = [];
|
|
7814
|
-
for (const [key, status] of Object.entries(statuses)) {
|
|
7815
|
-
if (status !== "done") continue;
|
|
7816
|
-
if (!STORY_KEY_PATTERN2.test(key)) continue;
|
|
7817
|
-
const proofPath = join24(root, "verification", `${key}-proof.md`);
|
|
7818
|
-
if (!existsSync27(proofPath)) {
|
|
7819
|
-
unverified.push(key);
|
|
7820
|
-
}
|
|
7821
|
-
}
|
|
7822
|
-
return [];
|
|
7823
|
-
}
|
|
7824
|
-
function findPerFileCoverageGaps(floor, dir) {
|
|
7825
|
-
const result = checkPerFileCoverage(floor, dir);
|
|
7826
|
-
const stories = [];
|
|
7827
|
-
let counter = 1;
|
|
7828
|
-
for (const violation of result.violations) {
|
|
7829
|
-
stories.push({
|
|
7830
|
-
key: `0.fc${counter}`,
|
|
7831
|
-
title: `Add test coverage for ${violation.file}`,
|
|
7832
|
-
type: "coverage",
|
|
7833
|
-
module: violation.file,
|
|
7834
|
-
acceptanceCriteria: [
|
|
7835
|
-
`**Given** ${violation.file} has ${violation.statements}% statement coverage (below ${floor}% floor)
|
|
7836
|
-
**When** the agent writes tests
|
|
7837
|
-
**Then** ${violation.file} reaches at least ${floor}% statement coverage`
|
|
7838
|
-
]
|
|
7839
|
-
});
|
|
7840
|
-
counter++;
|
|
7841
|
-
}
|
|
7842
|
-
return stories;
|
|
7843
|
-
}
|
|
7844
|
-
function findObservabilityGaps(dir) {
|
|
7845
|
-
let state;
|
|
7846
|
-
try {
|
|
7847
|
-
state = readState(dir);
|
|
7848
|
-
} catch {
|
|
7849
|
-
return [];
|
|
7850
|
-
}
|
|
7851
|
-
const stories = [];
|
|
7852
|
-
if (!state.otlp?.enabled) {
|
|
7853
|
-
stories.push({
|
|
7854
|
-
key: "0.o1",
|
|
7855
|
-
title: "Configure OTLP instrumentation",
|
|
7856
|
-
type: "observability",
|
|
7857
|
-
module: "otlp-config",
|
|
7858
|
-
acceptanceCriteria: [
|
|
7859
|
-
"**Given** observability is enabled but OTLP is not configured\n**When** onboard runs\n**Then** OTLP instrumentation must be configured with endpoint and service name"
|
|
7860
|
-
]
|
|
7861
|
-
});
|
|
7862
|
-
}
|
|
7863
|
-
if (state.docker?.compose_file) {
|
|
7864
|
-
if (!isStackRunning(state.docker.compose_file)) {
|
|
7865
|
-
stories.push({
|
|
7866
|
-
key: "0.o2",
|
|
7867
|
-
title: "Start Docker observability stack",
|
|
7868
|
-
type: "observability",
|
|
7869
|
-
module: "docker-stack",
|
|
7870
|
-
acceptanceCriteria: [
|
|
7871
|
-
"**Given** observability is enabled but Docker stack is not running\n**When** onboard runs\n**Then** Docker observability stack must be started"
|
|
7872
|
-
]
|
|
7873
|
-
});
|
|
7874
|
-
}
|
|
7875
|
-
} else {
|
|
7876
|
-
stories.push({
|
|
7877
|
-
key: "0.o2",
|
|
7878
|
-
title: "Start Docker observability stack",
|
|
7879
|
-
type: "observability",
|
|
7880
|
-
module: "docker-stack",
|
|
7881
|
-
acceptanceCriteria: [
|
|
7882
|
-
"**Given** observability is enabled but Docker compose file is not configured\n**When** onboard runs\n**Then** Docker observability stack must be configured and started"
|
|
7883
|
-
]
|
|
7884
|
-
});
|
|
7885
|
-
}
|
|
7886
|
-
return stories;
|
|
7887
|
-
}
|
|
7888
7758
|
var GAP_ID_PATTERN = /\[gap:[a-z-]+:[^\]]+\]/;
|
|
7889
7759
|
function getOnboardingProgress(beadsFns) {
|
|
7890
7760
|
let issues;
|
|
@@ -7905,42 +7775,6 @@ function getOnboardingProgress(beadsFns) {
|
|
|
7905
7775
|
).length;
|
|
7906
7776
|
return { total, resolved, remaining: total - resolved };
|
|
7907
7777
|
}
|
|
7908
|
-
function storyToGapId(story) {
|
|
7909
|
-
switch (story.type) {
|
|
7910
|
-
case "coverage":
|
|
7911
|
-
return buildGapId("coverage", story.module);
|
|
7912
|
-
case "agents-md":
|
|
7913
|
-
return buildGapId("docs", story.module + "/AGENTS.md");
|
|
7914
|
-
case "architecture":
|
|
7915
|
-
return buildGapId("docs", "ARCHITECTURE.md");
|
|
7916
|
-
case "doc-freshness":
|
|
7917
|
-
return buildGapId("docs", "stale-docs");
|
|
7918
|
-
case "verification":
|
|
7919
|
-
return buildGapId("verification", story.storyKey);
|
|
7920
|
-
case "observability":
|
|
7921
|
-
return buildGapId("observability", story.module);
|
|
7922
|
-
}
|
|
7923
|
-
}
|
|
7924
|
-
function filterTrackedGaps(stories, beadsFns) {
|
|
7925
|
-
let existingIssues;
|
|
7926
|
-
try {
|
|
7927
|
-
existingIssues = beadsFns.listIssues();
|
|
7928
|
-
} catch {
|
|
7929
|
-
return { untracked: [...stories], trackedCount: 0 };
|
|
7930
|
-
}
|
|
7931
|
-
const untracked = [];
|
|
7932
|
-
let trackedCount = 0;
|
|
7933
|
-
for (const story of stories) {
|
|
7934
|
-
const gapId = storyToGapId(story);
|
|
7935
|
-
const existing = findExistingByGapId(gapId, existingIssues);
|
|
7936
|
-
if (existing) {
|
|
7937
|
-
trackedCount++;
|
|
7938
|
-
} else {
|
|
7939
|
-
untracked.push(story);
|
|
7940
|
-
}
|
|
7941
|
-
}
|
|
7942
|
-
return { untracked, trackedCount };
|
|
7943
|
-
}
|
|
7944
7778
|
|
|
7945
7779
|
// src/commands/status.ts
|
|
7946
7780
|
function buildScopedEndpoints(endpoints, serviceName) {
|
|
@@ -8526,881 +8360,448 @@ function getBeadsData() {
|
|
|
8526
8360
|
}
|
|
8527
8361
|
}
|
|
8528
8362
|
|
|
8529
|
-
// src/
|
|
8530
|
-
import {
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
existsSync as existsSync28,
|
|
8535
|
-
readdirSync as readdirSync6,
|
|
8536
|
-
readFileSync as readFileSync25,
|
|
8537
|
-
statSync as statSync4
|
|
8538
|
-
} from "fs";
|
|
8539
|
-
import { join as join25, relative as relative2 } from "path";
|
|
8540
|
-
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
|
|
8541
|
-
var DEFAULT_MIN_MODULE_SIZE = 3;
|
|
8542
|
-
function getExtension2(filename) {
|
|
8543
|
-
const dot = filename.lastIndexOf(".");
|
|
8544
|
-
return dot >= 0 ? filename.slice(dot) : "";
|
|
8363
|
+
// src/modules/audit/dimensions.ts
|
|
8364
|
+
import { existsSync as existsSync28, readFileSync as readFileSync25, readdirSync as readdirSync6 } from "fs";
|
|
8365
|
+
import { join as join25 } from "path";
|
|
8366
|
+
function gap(dimension, description, suggestedFix) {
|
|
8367
|
+
return { dimension, description, suggestedFix };
|
|
8545
8368
|
}
|
|
8546
|
-
function
|
|
8547
|
-
return
|
|
8369
|
+
function dimOk(name, status, metric, gaps = []) {
|
|
8370
|
+
return ok2({ name, status, metric, gaps });
|
|
8548
8371
|
}
|
|
8549
|
-
function
|
|
8550
|
-
|
|
8372
|
+
function dimCatch(name, err) {
|
|
8373
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8374
|
+
return dimOk(name, "warn", "error", [gap(name, `${name} check failed: ${msg}`, `Check ${name} configuration`)]);
|
|
8551
8375
|
}
|
|
8552
|
-
function
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
}
|
|
8569
|
-
|
|
8570
|
-
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
walk(fullPath);
|
|
8575
|
-
} else if (stat.isFile()) {
|
|
8576
|
-
const ext = getExtension2(entry);
|
|
8577
|
-
if (SOURCE_EXTENSIONS2.has(ext) && !isTestFile2(entry)) {
|
|
8578
|
-
count++;
|
|
8376
|
+
function worstStatus(...statuses) {
|
|
8377
|
+
if (statuses.includes("fail")) return "fail";
|
|
8378
|
+
if (statuses.includes("warn")) return "warn";
|
|
8379
|
+
return "pass";
|
|
8380
|
+
}
|
|
8381
|
+
async function checkObservability(projectDir) {
|
|
8382
|
+
try {
|
|
8383
|
+
const gaps = [];
|
|
8384
|
+
let sStatus = "pass", sMetric = "";
|
|
8385
|
+
const sr = analyze(projectDir);
|
|
8386
|
+
if (isOk(sr)) {
|
|
8387
|
+
const d = sr.data;
|
|
8388
|
+
if (d.skipped) {
|
|
8389
|
+
sStatus = "warn";
|
|
8390
|
+
sMetric = `static: skipped (${d.skipReason ?? "unknown"})`;
|
|
8391
|
+
gaps.push(gap("observability", `Static analysis skipped: ${d.skipReason ?? "Semgrep not installed"}`, "Install Semgrep: pip install semgrep"));
|
|
8392
|
+
} else {
|
|
8393
|
+
const n = d.gaps.length;
|
|
8394
|
+
sMetric = `static: ${n} gap${n !== 1 ? "s" : ""}`;
|
|
8395
|
+
if (n > 0) {
|
|
8396
|
+
sStatus = "warn";
|
|
8397
|
+
for (const g of d.gaps) gaps.push(gap("observability", `${g.file}:${g.line} \u2014 ${g.message}`, g.fix ?? "Add observability instrumentation"));
|
|
8579
8398
|
}
|
|
8580
8399
|
}
|
|
8400
|
+
} else {
|
|
8401
|
+
sStatus = "warn";
|
|
8402
|
+
sMetric = "static: skipped (analysis failed)";
|
|
8403
|
+
gaps.push(gap("observability", `Static analysis failed: ${sr.error}`, "Check Semgrep installation and rules configuration"));
|
|
8581
8404
|
}
|
|
8582
|
-
|
|
8583
|
-
walk(dir);
|
|
8584
|
-
return count;
|
|
8585
|
-
}
|
|
8586
|
-
function countModuleFiles(modulePath, rootDir) {
|
|
8587
|
-
const fullModulePath = join25(rootDir, modulePath);
|
|
8588
|
-
let sourceFiles = 0;
|
|
8589
|
-
let testFiles = 0;
|
|
8590
|
-
function walk(current) {
|
|
8591
|
-
let entries;
|
|
8405
|
+
let rStatus = "pass", rMetric = "";
|
|
8592
8406
|
try {
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
}
|
|
8606
|
-
if (stat.isDirectory()) {
|
|
8607
|
-
walk(fullPath);
|
|
8608
|
-
} else if (stat.isFile()) {
|
|
8609
|
-
const ext = getExtension2(entry);
|
|
8610
|
-
if (SOURCE_EXTENSIONS2.has(ext)) {
|
|
8611
|
-
if (isTestFile2(entry) || current.includes("__tests__")) {
|
|
8612
|
-
testFiles++;
|
|
8613
|
-
} else {
|
|
8614
|
-
sourceFiles++;
|
|
8407
|
+
const rr = await validateRuntime(projectDir);
|
|
8408
|
+
if (isOk(rr)) {
|
|
8409
|
+
const d = rr.data;
|
|
8410
|
+
if (d.skipped) {
|
|
8411
|
+
rStatus = "warn";
|
|
8412
|
+
rMetric = `runtime: skipped (${d.skipReason ?? "unknown"})`;
|
|
8413
|
+
gaps.push(gap("observability", `Runtime validation skipped: ${d.skipReason ?? "backend unreachable"}`, "Start the observability stack: codeharness stack up"));
|
|
8414
|
+
} else {
|
|
8415
|
+
rMetric = `runtime: ${d.coveragePercent}%`;
|
|
8416
|
+
if (d.coveragePercent < 50) {
|
|
8417
|
+
rStatus = "warn";
|
|
8418
|
+
gaps.push(gap("observability", `Runtime coverage low: ${d.coveragePercent}%`, "Add telemetry instrumentation to more modules"));
|
|
8615
8419
|
}
|
|
8616
8420
|
}
|
|
8421
|
+
} else {
|
|
8422
|
+
rStatus = "warn";
|
|
8423
|
+
rMetric = "runtime: skipped (validation failed)";
|
|
8424
|
+
gaps.push(gap("observability", `Runtime validation failed: ${rr.error}`, "Ensure observability backend is running"));
|
|
8617
8425
|
}
|
|
8426
|
+
} catch {
|
|
8427
|
+
rStatus = "warn";
|
|
8428
|
+
rMetric = "runtime: skipped (error)";
|
|
8429
|
+
gaps.push(gap("observability", "Runtime validation threw an unexpected error", "Check observability stack health"));
|
|
8618
8430
|
}
|
|
8431
|
+
return dimOk("observability", worstStatus(sStatus, rStatus), `${sMetric}, ${rMetric}`, gaps);
|
|
8432
|
+
} catch (err) {
|
|
8433
|
+
return dimCatch("observability", err);
|
|
8619
8434
|
}
|
|
8620
|
-
walk(fullModulePath);
|
|
8621
|
-
return { sourceFiles, testFiles };
|
|
8622
|
-
}
|
|
8623
|
-
function detectArtifacts(dir) {
|
|
8624
|
-
const bmadPath = join25(dir, "_bmad");
|
|
8625
|
-
const hasBmad = existsSync28(bmadPath);
|
|
8626
|
-
return {
|
|
8627
|
-
hasBmad,
|
|
8628
|
-
bmadPath: hasBmad ? relative2(dir, bmadPath) || "_bmad" : null
|
|
8629
|
-
};
|
|
8630
|
-
}
|
|
8631
|
-
function scanCodebase(dir, options) {
|
|
8632
|
-
const threshold = options?.minModuleSize ?? DEFAULT_MIN_MODULE_SIZE;
|
|
8633
|
-
const modulePaths = findModules(dir, threshold);
|
|
8634
|
-
const modules = modulePaths.map((modPath) => {
|
|
8635
|
-
const { sourceFiles, testFiles } = countModuleFiles(modPath, dir);
|
|
8636
|
-
return { path: modPath, sourceFiles, testFiles };
|
|
8637
|
-
});
|
|
8638
|
-
const totalSourceFiles = countSourceFiles(dir);
|
|
8639
|
-
const artifacts = detectArtifacts(dir);
|
|
8640
|
-
return { modules, totalSourceFiles, artifacts };
|
|
8641
|
-
}
|
|
8642
|
-
function analyzeCoverageGaps(modules, dir) {
|
|
8643
|
-
const baseDir = dir ?? process.cwd();
|
|
8644
|
-
const toolInfo = detectCoverageTool(baseDir);
|
|
8645
|
-
if (toolInfo.tool === "unknown") {
|
|
8646
|
-
return {
|
|
8647
|
-
overall: 0,
|
|
8648
|
-
modules: modules.map((m) => ({
|
|
8649
|
-
path: m.path,
|
|
8650
|
-
coveragePercent: 0,
|
|
8651
|
-
uncoveredFileCount: m.sourceFiles
|
|
8652
|
-
})),
|
|
8653
|
-
uncoveredFiles: modules.reduce((sum, m) => sum + m.sourceFiles, 0)
|
|
8654
|
-
};
|
|
8655
|
-
}
|
|
8656
|
-
const overall = parseCoverageReport(baseDir, toolInfo.reportFormat);
|
|
8657
|
-
const perFileCoverage = readPerFileCoverage(baseDir, toolInfo.reportFormat);
|
|
8658
|
-
let totalUncovered = 0;
|
|
8659
|
-
const moduleCoverage = modules.map((mod) => {
|
|
8660
|
-
if (perFileCoverage === null) {
|
|
8661
|
-
return {
|
|
8662
|
-
path: mod.path,
|
|
8663
|
-
coveragePercent: overall,
|
|
8664
|
-
uncoveredFileCount: 0
|
|
8665
|
-
};
|
|
8666
|
-
}
|
|
8667
|
-
let coveredSum = 0;
|
|
8668
|
-
let fileCount = 0;
|
|
8669
|
-
let uncoveredCount = 0;
|
|
8670
|
-
for (const [filePath, pct] of perFileCoverage.entries()) {
|
|
8671
|
-
const relPath = filePath.startsWith("/") ? relative2(baseDir, filePath) : filePath;
|
|
8672
|
-
if (relPath.startsWith(mod.path + "/") || relPath === mod.path) {
|
|
8673
|
-
fileCount++;
|
|
8674
|
-
coveredSum += pct;
|
|
8675
|
-
if (pct === 0) {
|
|
8676
|
-
uncoveredCount++;
|
|
8677
|
-
}
|
|
8678
|
-
}
|
|
8679
|
-
}
|
|
8680
|
-
totalUncovered += uncoveredCount;
|
|
8681
|
-
const modulePercent = fileCount > 0 ? Math.round(coveredSum / fileCount * 100) / 100 : 0;
|
|
8682
|
-
return {
|
|
8683
|
-
path: mod.path,
|
|
8684
|
-
coveragePercent: modulePercent,
|
|
8685
|
-
uncoveredFileCount: uncoveredCount
|
|
8686
|
-
};
|
|
8687
|
-
});
|
|
8688
|
-
if (perFileCoverage === null) {
|
|
8689
|
-
totalUncovered = 0;
|
|
8690
|
-
}
|
|
8691
|
-
return {
|
|
8692
|
-
overall,
|
|
8693
|
-
modules: moduleCoverage,
|
|
8694
|
-
uncoveredFiles: totalUncovered
|
|
8695
|
-
};
|
|
8696
|
-
}
|
|
8697
|
-
function readPerFileCoverage(dir, format) {
|
|
8698
|
-
if (format === "vitest-json" || format === "jest-json") {
|
|
8699
|
-
return readVitestPerFileCoverage(dir);
|
|
8700
|
-
}
|
|
8701
|
-
if (format === "coverage-py-json") {
|
|
8702
|
-
return readPythonPerFileCoverage(dir);
|
|
8703
|
-
}
|
|
8704
|
-
return null;
|
|
8705
8435
|
}
|
|
8706
|
-
function
|
|
8707
|
-
const reportPath = join25(dir, "coverage", "coverage-summary.json");
|
|
8708
|
-
if (!existsSync28(reportPath)) return null;
|
|
8436
|
+
function checkTesting(projectDir) {
|
|
8709
8437
|
try {
|
|
8710
|
-
const
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8438
|
+
const r = checkOnlyCoverage(projectDir);
|
|
8439
|
+
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")]);
|
|
8440
|
+
const pct = r.coveragePercent;
|
|
8441
|
+
const gaps = [];
|
|
8442
|
+
let status = "pass";
|
|
8443
|
+
if (pct < 50) {
|
|
8444
|
+
status = "fail";
|
|
8445
|
+
gaps.push(gap("testing", `Test coverage critically low: ${pct}%`, "Add unit tests to increase coverage above 50%"));
|
|
8446
|
+
} else if (pct < 80) {
|
|
8447
|
+
status = "warn";
|
|
8448
|
+
gaps.push(gap("testing", `Test coverage below target: ${pct}%`, "Add tests to reach 80% coverage target"));
|
|
8715
8449
|
}
|
|
8716
|
-
return
|
|
8717
|
-
} catch {
|
|
8718
|
-
return
|
|
8450
|
+
return dimOk("testing", status, `${pct}%`, gaps);
|
|
8451
|
+
} catch (err) {
|
|
8452
|
+
return dimCatch("testing", err);
|
|
8719
8453
|
}
|
|
8720
8454
|
}
|
|
8721
|
-
function
|
|
8722
|
-
const reportPath = join25(dir, "coverage.json");
|
|
8723
|
-
if (!existsSync28(reportPath)) return null;
|
|
8455
|
+
function checkDocumentation(projectDir) {
|
|
8724
8456
|
try {
|
|
8725
|
-
const report =
|
|
8726
|
-
|
|
8727
|
-
const
|
|
8728
|
-
|
|
8729
|
-
|
|
8457
|
+
const report = scanDocHealth(projectDir);
|
|
8458
|
+
const gaps = [];
|
|
8459
|
+
const { fresh, stale, missing } = report.summary;
|
|
8460
|
+
let status = "pass";
|
|
8461
|
+
if (missing > 0) {
|
|
8462
|
+
status = "fail";
|
|
8463
|
+
for (const doc of report.documents) if (doc.grade === "missing") gaps.push(gap("documentation", `Missing: ${doc.path} \u2014 ${doc.reason}`, `Create ${doc.path}`));
|
|
8730
8464
|
}
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
}
|
|
8735
|
-
}
|
|
8736
|
-
var AUDIT_DOCUMENTS = ["README.md", "AGENTS.md", "ARCHITECTURE.md"];
|
|
8737
|
-
function auditDocumentation(dir) {
|
|
8738
|
-
const root = dir ?? process.cwd();
|
|
8739
|
-
const documents = [];
|
|
8740
|
-
for (const docName of AUDIT_DOCUMENTS) {
|
|
8741
|
-
const docPath = join25(root, docName);
|
|
8742
|
-
if (!existsSync28(docPath)) {
|
|
8743
|
-
documents.push({ name: docName, grade: "missing", path: null });
|
|
8744
|
-
continue;
|
|
8465
|
+
if (stale > 0) {
|
|
8466
|
+
if (status !== "fail") status = "warn";
|
|
8467
|
+
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`));
|
|
8745
8468
|
}
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
documents.push({
|
|
8750
|
-
name: docName,
|
|
8751
|
-
grade: stale ? "stale" : "present",
|
|
8752
|
-
path: docName
|
|
8753
|
-
});
|
|
8469
|
+
return dimOk("documentation", status, `${fresh} fresh, ${stale} stale, ${missing} missing`, gaps);
|
|
8470
|
+
} catch (err) {
|
|
8471
|
+
return dimCatch("documentation", err);
|
|
8754
8472
|
}
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8473
|
+
}
|
|
8474
|
+
function checkVerification(projectDir) {
|
|
8475
|
+
try {
|
|
8476
|
+
const gaps = [];
|
|
8477
|
+
const sprintPath = join25(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
|
|
8478
|
+
if (!existsSync28(sprintPath)) return dimOk("verification", "warn", "no sprint data", [gap("verification", "No sprint-status.yaml found", "Run sprint planning to create sprint status")]);
|
|
8479
|
+
const vDir = join25(projectDir, "verification");
|
|
8480
|
+
let proofCount = 0, totalChecked = 0;
|
|
8481
|
+
if (existsSync28(vDir)) {
|
|
8482
|
+
for (const file of readdirSafe(vDir)) {
|
|
8483
|
+
if (!file.endsWith("-proof.md")) continue;
|
|
8484
|
+
totalChecked++;
|
|
8485
|
+
const r = parseProof(join25(vDir, file));
|
|
8486
|
+
if (isOk(r) && r.data.passed) {
|
|
8487
|
+
proofCount++;
|
|
8488
|
+
} else {
|
|
8489
|
+
gaps.push(gap("verification", `Story ${file.replace("-proof.md", "")} proof incomplete or failing`, `Run codeharness verify ${file.replace("-proof.md", "")}`));
|
|
8490
|
+
}
|
|
8761
8491
|
}
|
|
8762
|
-
} catch {
|
|
8763
|
-
documents.push({ name: "docs/", grade: "missing", path: null });
|
|
8764
8492
|
}
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
const indexCodeDir = existsSync28(srcDir) ? srcDir : root;
|
|
8772
|
-
const indexStale = isDocStale(indexPath, indexCodeDir);
|
|
8773
|
-
documents.push({
|
|
8774
|
-
name: "docs/index.md",
|
|
8775
|
-
grade: indexStale ? "stale" : "present",
|
|
8776
|
-
path: "docs/index.md"
|
|
8777
|
-
});
|
|
8778
|
-
} else {
|
|
8779
|
-
documents.push({ name: "docs/index.md", grade: "missing", path: null });
|
|
8780
|
-
}
|
|
8781
|
-
const summaryParts = documents.filter((d) => !d.name.startsWith("docs/")).map((d) => `${d.name}(${d.grade})`);
|
|
8782
|
-
const summary = summaryParts.join(" ");
|
|
8783
|
-
return { documents, summary };
|
|
8784
|
-
}
|
|
8785
|
-
|
|
8786
|
-
// src/lib/epic-generator.ts
|
|
8787
|
-
import { createInterface } from "readline";
|
|
8788
|
-
import { existsSync as existsSync29, mkdirSync as mkdirSync9, writeFileSync as writeFileSync15 } from "fs";
|
|
8789
|
-
import { dirname as dirname7, join as join26 } from "path";
|
|
8790
|
-
var PRIORITY_BY_TYPE = {
|
|
8791
|
-
observability: 1,
|
|
8792
|
-
coverage: 2,
|
|
8793
|
-
verification: 2,
|
|
8794
|
-
"agents-md": 3,
|
|
8795
|
-
architecture: 3,
|
|
8796
|
-
"doc-freshness": 3
|
|
8797
|
-
};
|
|
8798
|
-
function generateOnboardingEpic(scan, coverage, audit, rootDir) {
|
|
8799
|
-
const root = rootDir ?? process.cwd();
|
|
8800
|
-
const stories = [];
|
|
8801
|
-
let storyNum = 1;
|
|
8802
|
-
const readmeDoc = audit.documents.find((d) => d.name === "README.md");
|
|
8803
|
-
if (readmeDoc && readmeDoc.grade === "missing") {
|
|
8804
|
-
stories.push({
|
|
8805
|
-
key: `0.${storyNum}`,
|
|
8806
|
-
title: "Create README.md",
|
|
8807
|
-
type: "doc-freshness",
|
|
8808
|
-
acceptanceCriteria: [
|
|
8809
|
-
"**Given** no README.md exists\n**When** the agent runs `codeharness init`\n**Then** README.md is created with project name, installation command, Quick Start section, and CLI reference"
|
|
8810
|
-
]
|
|
8811
|
-
});
|
|
8812
|
-
storyNum++;
|
|
8813
|
-
}
|
|
8814
|
-
const archDoc = audit.documents.find((d) => d.name === "ARCHITECTURE.md");
|
|
8815
|
-
if (archDoc && archDoc.grade === "missing") {
|
|
8816
|
-
stories.push({
|
|
8817
|
-
key: `0.${storyNum}`,
|
|
8818
|
-
title: "Create ARCHITECTURE.md",
|
|
8819
|
-
type: "architecture",
|
|
8820
|
-
acceptanceCriteria: [
|
|
8821
|
-
"**Given** no ARCHITECTURE.md exists\n**When** the agent analyzes the codebase\n**Then** ARCHITECTURE.md is created with module overview and dependencies"
|
|
8822
|
-
]
|
|
8823
|
-
});
|
|
8824
|
-
storyNum++;
|
|
8825
|
-
}
|
|
8826
|
-
for (const mod of scan.modules) {
|
|
8827
|
-
const agentsPath = join26(root, mod.path, "AGENTS.md");
|
|
8828
|
-
if (!existsSync29(agentsPath)) {
|
|
8829
|
-
stories.push({
|
|
8830
|
-
key: `0.${storyNum}`,
|
|
8831
|
-
title: `Create ${mod.path}/AGENTS.md`,
|
|
8832
|
-
type: "agents-md",
|
|
8833
|
-
module: mod.path,
|
|
8834
|
-
acceptanceCriteria: [
|
|
8835
|
-
`**Given** ${mod.path} has ${mod.sourceFiles} source files and no AGENTS.md
|
|
8836
|
-
**When** the agent reads the module
|
|
8837
|
-
**Then** ${mod.path}/AGENTS.md is created with module purpose and key files`
|
|
8838
|
-
]
|
|
8839
|
-
});
|
|
8840
|
-
storyNum++;
|
|
8841
|
-
}
|
|
8842
|
-
}
|
|
8843
|
-
for (const mod of coverage.modules) {
|
|
8844
|
-
if (mod.coveragePercent < 100) {
|
|
8845
|
-
stories.push({
|
|
8846
|
-
key: `0.${storyNum}`,
|
|
8847
|
-
title: `Add test coverage for ${mod.path}`,
|
|
8848
|
-
type: "coverage",
|
|
8849
|
-
module: mod.path,
|
|
8850
|
-
acceptanceCriteria: [
|
|
8851
|
-
`**Given** ${mod.path} has ${mod.uncoveredFileCount} uncovered files at ${mod.coveragePercent}% coverage
|
|
8852
|
-
**When** the agent writes tests
|
|
8853
|
-
**Then** ${mod.path} has 100% test coverage`
|
|
8854
|
-
]
|
|
8855
|
-
});
|
|
8856
|
-
storyNum++;
|
|
8857
|
-
}
|
|
8858
|
-
}
|
|
8859
|
-
const staleDocs = audit.documents.filter((d) => d.grade === "stale");
|
|
8860
|
-
if (staleDocs.length > 0) {
|
|
8861
|
-
const staleNames = staleDocs.map((d) => d.name).join(", ");
|
|
8862
|
-
stories.push({
|
|
8863
|
-
key: `0.${storyNum}`,
|
|
8864
|
-
title: "Update stale documentation",
|
|
8865
|
-
type: "doc-freshness",
|
|
8866
|
-
acceptanceCriteria: [
|
|
8867
|
-
`**Given** the following documents are stale: ${staleNames}
|
|
8868
|
-
**When** the agent reviews them against current source
|
|
8869
|
-
**Then** all stale documents are updated to reflect the current codebase`
|
|
8870
|
-
]
|
|
8871
|
-
});
|
|
8872
|
-
storyNum++;
|
|
8873
|
-
}
|
|
8874
|
-
const coverageStories = stories.filter((s) => s.type === "coverage").length;
|
|
8875
|
-
const docStories = stories.filter(
|
|
8876
|
-
(s) => s.type === "agents-md" || s.type === "architecture" || s.type === "doc-freshness"
|
|
8877
|
-
).length;
|
|
8878
|
-
const verificationStories = stories.filter((s) => s.type === "verification").length;
|
|
8879
|
-
const observabilityStories = stories.filter((s) => s.type === "observability").length;
|
|
8880
|
-
return {
|
|
8881
|
-
title: "Onboarding Epic: Bring Project to Harness Compliance",
|
|
8882
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
8883
|
-
stories,
|
|
8884
|
-
summary: {
|
|
8885
|
-
totalStories: stories.length,
|
|
8886
|
-
coverageStories,
|
|
8887
|
-
docStories,
|
|
8888
|
-
verificationStories,
|
|
8889
|
-
observabilityStories
|
|
8493
|
+
let status = "pass";
|
|
8494
|
+
if (totalChecked === 0) {
|
|
8495
|
+
status = "warn";
|
|
8496
|
+
gaps.push(gap("verification", "No verification proofs found", "Run codeharness verify for completed stories"));
|
|
8497
|
+
} else if (proofCount < totalChecked) {
|
|
8498
|
+
status = "warn";
|
|
8890
8499
|
}
|
|
8891
|
-
|
|
8500
|
+
return dimOk("verification", status, totalChecked > 0 ? `${proofCount}/${totalChecked} verified` : "no proofs", gaps);
|
|
8501
|
+
} catch (err) {
|
|
8502
|
+
return dimCatch("verification", err);
|
|
8503
|
+
}
|
|
8892
8504
|
}
|
|
8893
|
-
function
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
} else if (story.type === "observability") {
|
|
8916
|
-
lines.push("As a developer, I want observability infrastructure configured so the harness can monitor runtime behavior.");
|
|
8917
|
-
}
|
|
8918
|
-
lines.push("");
|
|
8919
|
-
for (const ac of story.acceptanceCriteria) {
|
|
8920
|
-
lines.push(ac);
|
|
8921
|
-
}
|
|
8922
|
-
lines.push("");
|
|
8923
|
-
}
|
|
8924
|
-
lines.push("---");
|
|
8925
|
-
lines.push("");
|
|
8926
|
-
lines.push(`**Total stories:** ${epic.stories.length}`);
|
|
8927
|
-
lines.push("");
|
|
8928
|
-
lines.push("Review and approve before execution.");
|
|
8929
|
-
lines.push("");
|
|
8930
|
-
writeFileSync15(outputPath, lines.join("\n"), "utf-8");
|
|
8931
|
-
}
|
|
8932
|
-
function formatEpicSummary(epic) {
|
|
8933
|
-
const { totalStories, coverageStories, docStories, verificationStories, observabilityStories } = epic.summary;
|
|
8934
|
-
return `Onboarding plan: ${totalStories} stories (${coverageStories} coverage, ${docStories} documentation, ${verificationStories} verification, ${observabilityStories} observability)`;
|
|
8935
|
-
}
|
|
8936
|
-
function promptApproval() {
|
|
8937
|
-
return new Promise((resolve3) => {
|
|
8938
|
-
let answered = false;
|
|
8939
|
-
const rl = createInterface({
|
|
8940
|
-
input: process.stdin,
|
|
8941
|
-
output: process.stdout
|
|
8942
|
-
});
|
|
8943
|
-
rl.on("close", () => {
|
|
8944
|
-
if (!answered) {
|
|
8945
|
-
answered = true;
|
|
8946
|
-
resolve3(false);
|
|
8505
|
+
function checkInfrastructure(projectDir) {
|
|
8506
|
+
try {
|
|
8507
|
+
const dfPath = join25(projectDir, "Dockerfile");
|
|
8508
|
+
if (!existsSync28(dfPath)) return dimOk("infrastructure", "fail", "no Dockerfile", [gap("infrastructure", "No Dockerfile found", "Create a Dockerfile for containerized deployment")]);
|
|
8509
|
+
let content;
|
|
8510
|
+
try {
|
|
8511
|
+
content = readFileSync25(dfPath, "utf-8");
|
|
8512
|
+
} catch {
|
|
8513
|
+
return dimOk("infrastructure", "warn", "Dockerfile unreadable", [gap("infrastructure", "Dockerfile exists but could not be read", "Check Dockerfile permissions")]);
|
|
8514
|
+
}
|
|
8515
|
+
const fromLines = content.split("\n").filter((l) => /^\s*FROM\s+/i.test(l));
|
|
8516
|
+
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")]);
|
|
8517
|
+
const gaps = [];
|
|
8518
|
+
let hasUnpinned = false;
|
|
8519
|
+
for (const line of fromLines) {
|
|
8520
|
+
const ref = line.replace(/^\s*FROM\s+/i, "").split(/\s+/)[0];
|
|
8521
|
+
if (ref.endsWith(":latest")) {
|
|
8522
|
+
hasUnpinned = true;
|
|
8523
|
+
gaps.push(gap("infrastructure", `Unpinned base image: ${ref}`, `Pin ${ref} to a specific version tag`));
|
|
8524
|
+
} else if (!ref.includes(":") && !ref.includes("@")) {
|
|
8525
|
+
hasUnpinned = true;
|
|
8526
|
+
gaps.push(gap("infrastructure", `Unpinned base image (no tag): ${ref}`, `Pin ${ref} to a specific version tag (e.g., ${ref}:22-slim)`));
|
|
8947
8527
|
}
|
|
8948
|
-
});
|
|
8949
|
-
rl.question("Review the onboarding plan. Approve? [Y/n] ", (answer) => {
|
|
8950
|
-
answered = true;
|
|
8951
|
-
rl.close();
|
|
8952
|
-
const trimmed = answer.trim().toLowerCase();
|
|
8953
|
-
resolve3(trimmed === "" || trimmed === "y");
|
|
8954
|
-
});
|
|
8955
|
-
});
|
|
8956
|
-
}
|
|
8957
|
-
function importOnboardingEpic(epicPath, beadsFns) {
|
|
8958
|
-
const epics = parseEpicsFile(epicPath);
|
|
8959
|
-
const allStories = [];
|
|
8960
|
-
for (const epic of epics) {
|
|
8961
|
-
for (const story of epic.stories) {
|
|
8962
|
-
allStories.push(story);
|
|
8963
8528
|
}
|
|
8529
|
+
const status = hasUnpinned ? "warn" : "pass";
|
|
8530
|
+
const metric = hasUnpinned ? `Dockerfile exists (${gaps.length} issue${gaps.length !== 1 ? "s" : ""})` : "Dockerfile valid";
|
|
8531
|
+
return dimOk("infrastructure", status, metric, gaps);
|
|
8532
|
+
} catch (err) {
|
|
8533
|
+
return dimCatch("infrastructure", err);
|
|
8964
8534
|
}
|
|
8965
|
-
|
|
8535
|
+
}
|
|
8536
|
+
function readdirSafe(dir) {
|
|
8537
|
+
try {
|
|
8538
|
+
return readdirSync6(dir);
|
|
8539
|
+
} catch {
|
|
8966
8540
|
return [];
|
|
8967
8541
|
}
|
|
8968
|
-
const wrappedBeadsFns = {
|
|
8969
|
-
listIssues: beadsFns.listIssues,
|
|
8970
|
-
createIssue: (title, opts) => {
|
|
8971
|
-
const priority = getPriorityFromTitle(title);
|
|
8972
|
-
const gapId = getGapIdFromTitle(title);
|
|
8973
|
-
const description = gapId ? appendGapId(opts?.description, gapId) : opts?.description;
|
|
8974
|
-
return beadsFns.createIssue(title, {
|
|
8975
|
-
...opts,
|
|
8976
|
-
type: "task",
|
|
8977
|
-
priority,
|
|
8978
|
-
description
|
|
8979
|
-
});
|
|
8980
|
-
}
|
|
8981
|
-
};
|
|
8982
|
-
return importStoriesToBeads(allStories, {}, wrappedBeadsFns);
|
|
8983
|
-
}
|
|
8984
|
-
function getPriorityFromTitle(title) {
|
|
8985
|
-
if (title.startsWith("Add test coverage for ")) return PRIORITY_BY_TYPE.coverage;
|
|
8986
|
-
if (title.startsWith("Create ") && title.endsWith("AGENTS.md")) return PRIORITY_BY_TYPE["agents-md"];
|
|
8987
|
-
if (title === "Create README.md") return PRIORITY_BY_TYPE["doc-freshness"];
|
|
8988
|
-
if (title === "Create ARCHITECTURE.md") return PRIORITY_BY_TYPE.architecture;
|
|
8989
|
-
if (title === "Update stale documentation") return PRIORITY_BY_TYPE["doc-freshness"];
|
|
8990
|
-
if (title.startsWith("Create verification proof for ")) return PRIORITY_BY_TYPE.verification;
|
|
8991
|
-
if (title === "Configure OTLP instrumentation" || title === "Start Docker observability stack") return PRIORITY_BY_TYPE.observability;
|
|
8992
|
-
return 3;
|
|
8993
8542
|
}
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
if (title === "Update stale documentation") {
|
|
9010
|
-
return "[gap:docs:stale-docs]";
|
|
9011
|
-
}
|
|
9012
|
-
if (title.startsWith("Create verification proof for ")) {
|
|
9013
|
-
const key = title.slice("Create verification proof for ".length);
|
|
9014
|
-
return `[gap:verification:${key}]`;
|
|
9015
|
-
}
|
|
9016
|
-
if (title === "Configure OTLP instrumentation") {
|
|
9017
|
-
return "[gap:observability:otlp-config]";
|
|
9018
|
-
}
|
|
9019
|
-
if (title === "Start Docker observability stack") {
|
|
9020
|
-
return "[gap:observability:docker-stack]";
|
|
8543
|
+
|
|
8544
|
+
// src/modules/audit/report.ts
|
|
8545
|
+
var STATUS_PREFIX = {
|
|
8546
|
+
pass: "[OK]",
|
|
8547
|
+
fail: "[FAIL]",
|
|
8548
|
+
warn: "[WARN]"
|
|
8549
|
+
};
|
|
8550
|
+
function formatAuditHuman(result) {
|
|
8551
|
+
const lines = [];
|
|
8552
|
+
for (const dimension of Object.values(result.dimensions)) {
|
|
8553
|
+
const prefix = STATUS_PREFIX[dimension.status] ?? "[WARN]";
|
|
8554
|
+
lines.push(`${prefix} ${dimension.name}: ${dimension.metric}`);
|
|
8555
|
+
for (const gap2 of dimension.gaps) {
|
|
8556
|
+
lines.push(` [WARN] ${gap2.description} -- fix: ${gap2.suggestedFix}`);
|
|
8557
|
+
}
|
|
9021
8558
|
}
|
|
9022
|
-
|
|
8559
|
+
const overallPrefix = STATUS_PREFIX[result.overallStatus] ?? "[WARN]";
|
|
8560
|
+
lines.push("");
|
|
8561
|
+
lines.push(
|
|
8562
|
+
`${overallPrefix} Audit complete: ${result.gapCount} gap${result.gapCount !== 1 ? "s" : ""} found (${result.durationMs}ms)`
|
|
8563
|
+
);
|
|
8564
|
+
return lines;
|
|
8565
|
+
}
|
|
8566
|
+
function formatAuditJson(result) {
|
|
8567
|
+
return result;
|
|
9023
8568
|
}
|
|
9024
8569
|
|
|
9025
|
-
// src/
|
|
9026
|
-
import { existsSync as
|
|
9027
|
-
import { join as
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
function saveScanCache(entry, dir) {
|
|
9032
|
-
try {
|
|
9033
|
-
const root = dir ?? process.cwd();
|
|
9034
|
-
const cacheDir = join27(root, CACHE_DIR);
|
|
9035
|
-
mkdirSync10(cacheDir, { recursive: true });
|
|
9036
|
-
const cachePath = join27(cacheDir, CACHE_FILE);
|
|
9037
|
-
writeFileSync16(cachePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
9038
|
-
} catch {
|
|
9039
|
-
}
|
|
8570
|
+
// src/modules/audit/fix-generator.ts
|
|
8571
|
+
import { existsSync as existsSync29, writeFileSync as writeFileSync15, mkdirSync as mkdirSync9 } from "fs";
|
|
8572
|
+
import { join as join26, dirname as dirname7 } from "path";
|
|
8573
|
+
function buildStoryKey(gap2, index) {
|
|
8574
|
+
const safeDimension = gap2.dimension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
8575
|
+
return `audit-fix-${safeDimension}-${index}`;
|
|
9040
8576
|
}
|
|
9041
|
-
function
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
8577
|
+
function buildStoryMarkdown(gap2, key) {
|
|
8578
|
+
return [
|
|
8579
|
+
`# Fix: ${gap2.dimension} \u2014 ${gap2.description}`,
|
|
8580
|
+
"",
|
|
8581
|
+
"Status: backlog",
|
|
8582
|
+
"",
|
|
8583
|
+
"## Story",
|
|
8584
|
+
"",
|
|
8585
|
+
`As an operator, I need ${gap2.description} fixed so that audit compliance improves.`,
|
|
8586
|
+
"",
|
|
8587
|
+
"## Acceptance Criteria",
|
|
8588
|
+
"",
|
|
8589
|
+
`1. **Given** ${gap2.description}, **When** the fix is applied, **Then** ${gap2.suggestedFix}.`,
|
|
8590
|
+
"",
|
|
8591
|
+
"## Dev Notes",
|
|
8592
|
+
"",
|
|
8593
|
+
"This is an auto-generated fix story created by `codeharness audit --fix`.",
|
|
8594
|
+
`**Audit Gap:** ${gap2.dimension}: ${gap2.description}`,
|
|
8595
|
+
`**Suggested Fix:** ${gap2.suggestedFix}`,
|
|
8596
|
+
""
|
|
8597
|
+
].join("\n");
|
|
9053
8598
|
}
|
|
9054
|
-
function
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
8599
|
+
function generateFixStories(auditResult) {
|
|
8600
|
+
try {
|
|
8601
|
+
const stories = [];
|
|
8602
|
+
let created = 0;
|
|
8603
|
+
let skipped = 0;
|
|
8604
|
+
const artifactsDir = join26(
|
|
8605
|
+
process.cwd(),
|
|
8606
|
+
"_bmad-output",
|
|
8607
|
+
"implementation-artifacts"
|
|
8608
|
+
);
|
|
8609
|
+
for (const dimension of Object.values(auditResult.dimensions)) {
|
|
8610
|
+
for (let i = 0; i < dimension.gaps.length; i++) {
|
|
8611
|
+
const gap2 = dimension.gaps[i];
|
|
8612
|
+
const key = buildStoryKey(gap2, i + 1);
|
|
8613
|
+
const filePath = join26(artifactsDir, `${key}.md`);
|
|
8614
|
+
if (existsSync29(filePath)) {
|
|
8615
|
+
stories.push({
|
|
8616
|
+
key,
|
|
8617
|
+
filePath,
|
|
8618
|
+
gap: gap2,
|
|
8619
|
+
skipped: true,
|
|
8620
|
+
skipReason: "Story file already exists"
|
|
8621
|
+
});
|
|
8622
|
+
skipped++;
|
|
8623
|
+
continue;
|
|
8624
|
+
}
|
|
8625
|
+
const markdown = buildStoryMarkdown(gap2, key);
|
|
8626
|
+
mkdirSync9(dirname7(filePath), { recursive: true });
|
|
8627
|
+
writeFileSync15(filePath, markdown, "utf-8");
|
|
8628
|
+
stories.push({ key, filePath, gap: gap2, skipped: false });
|
|
8629
|
+
created++;
|
|
8630
|
+
}
|
|
8631
|
+
}
|
|
8632
|
+
return ok2({ stories, created, skipped });
|
|
8633
|
+
} catch (err) {
|
|
8634
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8635
|
+
return fail2(`Failed to generate fix stories: ${msg}`);
|
|
9062
8636
|
}
|
|
9063
|
-
return Date.now() - ts < max;
|
|
9064
8637
|
}
|
|
9065
|
-
function
|
|
9066
|
-
|
|
9067
|
-
|
|
8638
|
+
function addFixStoriesToState(stories) {
|
|
8639
|
+
const newStories = stories.filter((s) => !s.skipped);
|
|
8640
|
+
if (newStories.length === 0) {
|
|
8641
|
+
return ok2(void 0);
|
|
9068
8642
|
}
|
|
9069
|
-
const
|
|
9070
|
-
if (!
|
|
9071
|
-
return
|
|
8643
|
+
const stateResult = getSprintState2();
|
|
8644
|
+
if (!stateResult.success) {
|
|
8645
|
+
return fail2(stateResult.error);
|
|
9072
8646
|
}
|
|
9073
|
-
|
|
9074
|
-
|
|
8647
|
+
const current = stateResult.data;
|
|
8648
|
+
const updatedStories = { ...current.stories };
|
|
8649
|
+
for (const story of newStories) {
|
|
8650
|
+
updatedStories[story.key] = {
|
|
8651
|
+
status: "backlog",
|
|
8652
|
+
attempts: 0,
|
|
8653
|
+
lastAttempt: null,
|
|
8654
|
+
lastError: null,
|
|
8655
|
+
proofPath: null,
|
|
8656
|
+
acResults: null
|
|
8657
|
+
};
|
|
9075
8658
|
}
|
|
9076
|
-
|
|
8659
|
+
const updatedSprint = computeSprintCounts2(updatedStories);
|
|
8660
|
+
return writeStateAtomic2({
|
|
8661
|
+
...current,
|
|
8662
|
+
sprint: updatedSprint,
|
|
8663
|
+
stories: updatedStories
|
|
8664
|
+
});
|
|
9077
8665
|
}
|
|
9078
8666
|
|
|
9079
|
-
// src/
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
for (const w of preconditions.warnings) {
|
|
9101
|
-
warn(w);
|
|
9102
|
-
}
|
|
9103
|
-
}
|
|
9104
|
-
const result = runScan(minModuleSize);
|
|
9105
|
-
saveScanCache({
|
|
9106
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9107
|
-
scan: result,
|
|
9108
|
-
coverage: null,
|
|
9109
|
-
audit: null
|
|
9110
|
-
});
|
|
9111
|
-
if (isJson) {
|
|
9112
|
-
jsonOutput({ preconditions: { initialized: preconditions.initialized, bmad: preconditions.bmad, hooks: preconditions.hooks }, scan: result });
|
|
9113
|
-
} else {
|
|
9114
|
-
printScanOutput(result);
|
|
8667
|
+
// src/modules/audit/index.ts
|
|
8668
|
+
async function runAudit(projectDir) {
|
|
8669
|
+
const start = performance.now();
|
|
8670
|
+
const [
|
|
8671
|
+
obsResult,
|
|
8672
|
+
testResult,
|
|
8673
|
+
docResult,
|
|
8674
|
+
verifyResult,
|
|
8675
|
+
infraResult
|
|
8676
|
+
] = await Promise.all([
|
|
8677
|
+
checkObservability(projectDir),
|
|
8678
|
+
Promise.resolve(checkTesting(projectDir)),
|
|
8679
|
+
Promise.resolve(checkDocumentation(projectDir)),
|
|
8680
|
+
Promise.resolve(checkVerification(projectDir)),
|
|
8681
|
+
Promise.resolve(checkInfrastructure(projectDir))
|
|
8682
|
+
]);
|
|
8683
|
+
const dimensions = {};
|
|
8684
|
+
const allResults = [obsResult, testResult, docResult, verifyResult, infraResult];
|
|
8685
|
+
for (const result of allResults) {
|
|
8686
|
+
if (result.success) {
|
|
8687
|
+
dimensions[result.data.name] = result.data;
|
|
9115
8688
|
}
|
|
9116
|
-
}
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
}
|
|
9135
|
-
let scan;
|
|
9136
|
-
if (lastScanResult) {
|
|
9137
|
-
scan = lastScanResult;
|
|
9138
|
-
} else {
|
|
9139
|
-
const cache = loadValidCache(process.cwd(), { forceScan });
|
|
9140
|
-
if (cache) {
|
|
9141
|
-
info(`Using cached scan from ${new Date(cache.timestamp).toLocaleString()}`);
|
|
9142
|
-
scan = cache.scan;
|
|
9143
|
-
lastScanResult = scan;
|
|
9144
|
-
} else {
|
|
9145
|
-
scan = runScan(minModuleSize);
|
|
9146
|
-
}
|
|
9147
|
-
}
|
|
9148
|
-
const result = runCoverageAnalysis(scan);
|
|
8689
|
+
}
|
|
8690
|
+
const statuses = Object.values(dimensions).map((d) => d.status);
|
|
8691
|
+
const overallStatus = computeOverallStatus(statuses);
|
|
8692
|
+
const gapCount = Object.values(dimensions).reduce((sum, d) => sum + d.gaps.length, 0);
|
|
8693
|
+
const durationMs = Math.round(performance.now() - start);
|
|
8694
|
+
return ok2({ dimensions, overallStatus, gapCount, durationMs });
|
|
8695
|
+
}
|
|
8696
|
+
function computeOverallStatus(statuses) {
|
|
8697
|
+
if (statuses.includes("fail")) return "fail";
|
|
8698
|
+
if (statuses.includes("warn")) return "warn";
|
|
8699
|
+
return "pass";
|
|
8700
|
+
}
|
|
8701
|
+
|
|
8702
|
+
// src/commands/audit-action.ts
|
|
8703
|
+
async function executeAudit(opts) {
|
|
8704
|
+
const { isJson, isFix } = opts;
|
|
8705
|
+
const preconditions = runPreconditions();
|
|
8706
|
+
if (!preconditions.canProceed) {
|
|
9149
8707
|
if (isJson) {
|
|
9150
|
-
jsonOutput({
|
|
8708
|
+
jsonOutput({
|
|
8709
|
+
status: "fail",
|
|
8710
|
+
message: "Harness not initialized -- run codeharness init first"
|
|
8711
|
+
});
|
|
9151
8712
|
} else {
|
|
9152
|
-
|
|
9153
|
-
}
|
|
9154
|
-
});
|
|
9155
|
-
onboard.command("audit").description("Audit project documentation").action((_, cmd) => {
|
|
9156
|
-
const opts = cmd.optsWithGlobals();
|
|
9157
|
-
const isJson = opts.json === true;
|
|
9158
|
-
const preconditions = runPreconditions();
|
|
9159
|
-
if (!preconditions.canProceed) {
|
|
9160
|
-
if (isJson) {
|
|
9161
|
-
jsonOutput({ status: "fail", message: "Harness not initialized \u2014 run codeharness init first" });
|
|
9162
|
-
} else {
|
|
9163
|
-
fail("Harness not initialized \u2014 run codeharness init first");
|
|
9164
|
-
}
|
|
9165
|
-
process.exitCode = 1;
|
|
9166
|
-
return;
|
|
9167
|
-
}
|
|
9168
|
-
for (const w of preconditions.warnings) {
|
|
9169
|
-
warn(w);
|
|
8713
|
+
fail("Harness not initialized -- run codeharness init first");
|
|
9170
8714
|
}
|
|
9171
|
-
|
|
8715
|
+
process.exitCode = 1;
|
|
8716
|
+
return;
|
|
8717
|
+
}
|
|
8718
|
+
const result = await runAudit(process.cwd());
|
|
8719
|
+
if (!result.success) {
|
|
9172
8720
|
if (isJson) {
|
|
9173
|
-
jsonOutput({
|
|
8721
|
+
jsonOutput({ status: "fail", message: result.error });
|
|
9174
8722
|
} else {
|
|
9175
|
-
|
|
8723
|
+
fail(result.error);
|
|
9176
8724
|
}
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
if (!preconditions.canProceed) {
|
|
9187
|
-
if (isJson) {
|
|
9188
|
-
jsonOutput({ status: "fail", message: "Harness not initialized \u2014 run codeharness init first" });
|
|
9189
|
-
} else {
|
|
9190
|
-
fail("Harness not initialized \u2014 run codeharness init first");
|
|
8725
|
+
process.exitCode = 1;
|
|
8726
|
+
return;
|
|
8727
|
+
}
|
|
8728
|
+
let fixStories;
|
|
8729
|
+
let fixStateError;
|
|
8730
|
+
if (isFix) {
|
|
8731
|
+
if (result.data.gapCount === 0) {
|
|
8732
|
+
if (!isJson) {
|
|
8733
|
+
ok("No gaps found -- nothing to fix");
|
|
9191
8734
|
}
|
|
9192
|
-
process.exitCode = 1;
|
|
9193
|
-
return;
|
|
9194
|
-
}
|
|
9195
|
-
for (const w of preconditions.warnings) {
|
|
9196
|
-
warn(w);
|
|
9197
|
-
}
|
|
9198
|
-
let scan;
|
|
9199
|
-
let coverage;
|
|
9200
|
-
let audit;
|
|
9201
|
-
if (lastScanResult) {
|
|
9202
|
-
scan = lastScanResult;
|
|
9203
8735
|
} else {
|
|
9204
|
-
const
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
|
|
8736
|
+
const fixResult = generateFixStories(result.data);
|
|
8737
|
+
fixStories = fixResult;
|
|
8738
|
+
if (fixResult.success) {
|
|
8739
|
+
const stateResult = addFixStoriesToState(fixResult.data.stories);
|
|
8740
|
+
if (!stateResult.success) {
|
|
8741
|
+
fixStateError = stateResult.error;
|
|
8742
|
+
if (!isJson) {
|
|
8743
|
+
fail(`Failed to update sprint state: ${stateResult.error}`);
|
|
8744
|
+
}
|
|
9211
8745
|
}
|
|
9212
|
-
if (
|
|
9213
|
-
|
|
8746
|
+
if (!isJson) {
|
|
8747
|
+
info(`Generated ${fixResult.data.created} fix stories (${fixResult.data.skipped} skipped)`);
|
|
9214
8748
|
}
|
|
9215
|
-
} else {
|
|
9216
|
-
|
|
9217
|
-
}
|
|
9218
|
-
}
|
|
9219
|
-
coverage = lastCoverageResult ?? runCoverageAnalysis(scan);
|
|
9220
|
-
audit = lastAuditResult ?? runAudit();
|
|
9221
|
-
const epicPath = join28(process.cwd(), "ralph", "onboarding-epic.md");
|
|
9222
|
-
const epic = generateOnboardingEpic(scan, coverage, audit);
|
|
9223
|
-
mergeExtendedGaps(epic);
|
|
9224
|
-
if (!isFull) {
|
|
9225
|
-
applyGapFiltering(epic);
|
|
9226
|
-
}
|
|
9227
|
-
writeOnboardingEpic(epic, epicPath);
|
|
9228
|
-
if (isJson) {
|
|
9229
|
-
jsonOutput({
|
|
9230
|
-
preconditions: { initialized: preconditions.initialized, bmad: preconditions.bmad, hooks: preconditions.hooks },
|
|
9231
|
-
epic,
|
|
9232
|
-
import_status: { stories_created: 0, stories_existing: 0 }
|
|
9233
|
-
});
|
|
9234
|
-
return;
|
|
9235
|
-
}
|
|
9236
|
-
printEpicOutput(epic);
|
|
9237
|
-
let approved;
|
|
9238
|
-
if (autoApprove) {
|
|
9239
|
-
approved = true;
|
|
9240
|
-
} else {
|
|
9241
|
-
approved = await promptApproval();
|
|
9242
|
-
}
|
|
9243
|
-
if (approved) {
|
|
9244
|
-
const results = importOnboardingEpic(epicPath, { listIssues, createIssue });
|
|
9245
|
-
const created = results.filter((r) => r.status === "created").length;
|
|
9246
|
-
ok(`Onboarding: ${created} stories imported into beads`);
|
|
9247
|
-
const sprintResult = appendOnboardingEpicToSprint(
|
|
9248
|
-
epic.stories.map((s) => ({ title: s.title }))
|
|
9249
|
-
);
|
|
9250
|
-
if (sprintResult.epicNumber >= 0) {
|
|
9251
|
-
ok(`Onboarding epic ${sprintResult.epicNumber} added to sprint-status.yaml (${sprintResult.storyKeys.length} stories)`);
|
|
9252
|
-
}
|
|
9253
|
-
info("Ready to run: codeharness run");
|
|
9254
|
-
} else {
|
|
9255
|
-
info("Plan saved to ralph/onboarding-epic.md \u2014 edit and re-run when ready");
|
|
9256
|
-
}
|
|
9257
|
-
});
|
|
9258
|
-
onboard.action(async (opts, cmd) => {
|
|
9259
|
-
const globalOpts = cmd.optsWithGlobals();
|
|
9260
|
-
const isJson = opts.json === true || globalOpts.json === true;
|
|
9261
|
-
const isFull = opts.full === true || globalOpts.full === true;
|
|
9262
|
-
const forceScan = opts.forceScan === true || globalOpts.forceScan === true;
|
|
9263
|
-
const minModuleSize = parseInt(opts.minModuleSize ?? "3", 10);
|
|
9264
|
-
const preconditions = runPreconditions();
|
|
9265
|
-
if (!preconditions.canProceed) {
|
|
9266
|
-
if (isJson) {
|
|
9267
|
-
jsonOutput({ status: "fail", message: "Harness not initialized \u2014 run codeharness init first" });
|
|
9268
|
-
} else {
|
|
9269
|
-
fail("Harness not initialized \u2014 run codeharness init first");
|
|
9270
|
-
}
|
|
9271
|
-
process.exitCode = 1;
|
|
9272
|
-
return;
|
|
9273
|
-
}
|
|
9274
|
-
for (const w of preconditions.warnings) {
|
|
9275
|
-
warn(w);
|
|
9276
|
-
}
|
|
9277
|
-
const progress = getOnboardingProgress({ listIssues });
|
|
9278
|
-
if (progress) {
|
|
9279
|
-
if (progress.remaining === 0 && !isFull && !forceScan) {
|
|
9280
|
-
ok("Onboarding complete \u2014 all gaps resolved");
|
|
9281
|
-
return;
|
|
8749
|
+
} else if (!isJson) {
|
|
8750
|
+
fail(fixResult.error);
|
|
9282
8751
|
}
|
|
9283
|
-
info(`Onboarding progress: ${progress.resolved}/${progress.total} gaps resolved (${progress.remaining} remaining)`);
|
|
9284
|
-
}
|
|
9285
|
-
const scan = runScan(minModuleSize);
|
|
9286
|
-
const coverage = runCoverageAnalysis(scan);
|
|
9287
|
-
const audit = runAudit();
|
|
9288
|
-
saveScanCache({
|
|
9289
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9290
|
-
scan,
|
|
9291
|
-
coverage,
|
|
9292
|
-
audit
|
|
9293
|
-
});
|
|
9294
|
-
const epicPath = join28(process.cwd(), "ralph", "onboarding-epic.md");
|
|
9295
|
-
const epic = generateOnboardingEpic(scan, coverage, audit);
|
|
9296
|
-
mergeExtendedGaps(epic);
|
|
9297
|
-
if (!isFull) {
|
|
9298
|
-
applyGapFiltering(epic);
|
|
9299
8752
|
}
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
if (approved) {
|
|
9316
|
-
const results = importOnboardingEpic(epicPath, { listIssues, createIssue });
|
|
9317
|
-
const created = results.filter((r) => r.status === "created").length;
|
|
9318
|
-
ok(`Onboarding: ${created} stories imported into beads`);
|
|
9319
|
-
const sprintResult = appendOnboardingEpicToSprint(
|
|
9320
|
-
epic.stories.map((s) => ({ title: s.title }))
|
|
9321
|
-
);
|
|
9322
|
-
if (sprintResult.epicNumber >= 0) {
|
|
9323
|
-
ok(`Onboarding epic ${sprintResult.epicNumber} added to sprint-status.yaml (${sprintResult.storyKeys.length} stories)`);
|
|
8753
|
+
}
|
|
8754
|
+
if (isJson) {
|
|
8755
|
+
const jsonData = { ...formatAuditJson(result.data) };
|
|
8756
|
+
if (isFix) {
|
|
8757
|
+
if (result.data.gapCount === 0) {
|
|
8758
|
+
jsonData.fixStories = [];
|
|
8759
|
+
} else if (fixStories.success) {
|
|
8760
|
+
jsonData.fixStories = fixStories.data.stories.map((s) => ({
|
|
8761
|
+
key: s.key,
|
|
8762
|
+
filePath: s.filePath,
|
|
8763
|
+
gap: s.gap,
|
|
8764
|
+
...s.skipped ? { skipped: true } : {}
|
|
8765
|
+
}));
|
|
8766
|
+
if (fixStateError) {
|
|
8767
|
+
jsonData.fixStateError = fixStateError;
|
|
9324
8768
|
}
|
|
9325
|
-
info("Ready to run: codeharness run");
|
|
9326
8769
|
} else {
|
|
9327
|
-
|
|
8770
|
+
jsonData.fixStories = [];
|
|
8771
|
+
jsonData.fixError = fixStories.error;
|
|
9328
8772
|
}
|
|
9329
8773
|
}
|
|
9330
|
-
|
|
9331
|
-
}
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
info(`${trackedCount} previously tracked gaps already in beads`);
|
|
9336
|
-
}
|
|
9337
|
-
epic.stories = untracked;
|
|
9338
|
-
rebuildEpicSummary(epic);
|
|
9339
|
-
}
|
|
9340
|
-
function mergeExtendedGaps(epic) {
|
|
9341
|
-
const verificationGaps = findVerificationGaps();
|
|
9342
|
-
const perFileCoverageGaps = findPerFileCoverageGaps(80);
|
|
9343
|
-
const observabilityGaps = findObservabilityGaps();
|
|
9344
|
-
epic.stories.push(...verificationGaps, ...perFileCoverageGaps, ...observabilityGaps);
|
|
9345
|
-
rebuildEpicSummary(epic);
|
|
9346
|
-
}
|
|
9347
|
-
function rebuildEpicSummary(epic) {
|
|
9348
|
-
const coverageStories = epic.stories.filter((s) => s.type === "coverage").length;
|
|
9349
|
-
const docStories = epic.stories.filter(
|
|
9350
|
-
(s) => s.type === "agents-md" || s.type === "architecture" || s.type === "doc-freshness"
|
|
9351
|
-
).length;
|
|
9352
|
-
const verificationStories = epic.stories.filter((s) => s.type === "verification").length;
|
|
9353
|
-
const observabilityStories = epic.stories.filter((s) => s.type === "observability").length;
|
|
9354
|
-
epic.summary = {
|
|
9355
|
-
totalStories: epic.stories.length,
|
|
9356
|
-
coverageStories,
|
|
9357
|
-
docStories,
|
|
9358
|
-
verificationStories,
|
|
9359
|
-
observabilityStories
|
|
9360
|
-
};
|
|
9361
|
-
}
|
|
9362
|
-
function runScan(minModuleSize) {
|
|
9363
|
-
const result = scanCodebase(process.cwd(), { minModuleSize });
|
|
9364
|
-
lastScanResult = result;
|
|
9365
|
-
return result;
|
|
9366
|
-
}
|
|
9367
|
-
function runCoverageAnalysis(scan) {
|
|
9368
|
-
const result = analyzeCoverageGaps(scan.modules);
|
|
9369
|
-
lastCoverageResult = result;
|
|
9370
|
-
return result;
|
|
9371
|
-
}
|
|
9372
|
-
function runAudit() {
|
|
9373
|
-
const result = auditDocumentation();
|
|
9374
|
-
lastAuditResult = result;
|
|
9375
|
-
return result;
|
|
9376
|
-
}
|
|
9377
|
-
function printScanOutput(result) {
|
|
9378
|
-
info(`Scan: ${result.totalSourceFiles} source files across ${result.modules.length} modules`);
|
|
9379
|
-
for (const mod of result.modules) {
|
|
9380
|
-
info(` ${mod.path}: ${mod.sourceFiles} source, ${mod.testFiles} test`);
|
|
9381
|
-
}
|
|
9382
|
-
}
|
|
9383
|
-
function printCoverageOutput2(result) {
|
|
9384
|
-
info(`Coverage: ${result.overall}% overall (${result.uncoveredFiles} files uncovered)`);
|
|
9385
|
-
for (const mod of result.modules) {
|
|
9386
|
-
if (mod.uncoveredFileCount > 0) {
|
|
9387
|
-
info(` ${mod.path}: ${mod.coveragePercent}% (${mod.uncoveredFileCount} uncovered)`);
|
|
8774
|
+
jsonOutput(jsonData);
|
|
8775
|
+
} else if (!isFix || result.data.gapCount > 0) {
|
|
8776
|
+
const lines = formatAuditHuman(result.data);
|
|
8777
|
+
for (const line of lines) {
|
|
8778
|
+
console.log(line);
|
|
9388
8779
|
}
|
|
9389
8780
|
}
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
info(`Docs: ${result.summary}`);
|
|
9393
|
-
}
|
|
9394
|
-
function printEpicOutput(epic) {
|
|
9395
|
-
info(formatEpicSummary(epic));
|
|
9396
|
-
for (const story of epic.stories) {
|
|
9397
|
-
info(` ${story.key}: ${story.title}`);
|
|
8781
|
+
if (result.data.overallStatus === "fail") {
|
|
8782
|
+
process.exitCode = 1;
|
|
9398
8783
|
}
|
|
9399
8784
|
}
|
|
9400
8785
|
|
|
8786
|
+
// src/commands/onboard.ts
|
|
8787
|
+
function registerOnboardCommand(program) {
|
|
8788
|
+
const onboard = program.command("onboard").description("Alias for audit \u2014 check all compliance dimensions").option("--json", "Output in machine-readable JSON format").option("--fix", "Generate fix stories for every gap found").action(async (opts, cmd) => {
|
|
8789
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
8790
|
+
const isJson = opts.json === true || globalOpts.json === true;
|
|
8791
|
+
const isFix = opts.fix === true;
|
|
8792
|
+
await executeAudit({ isJson, isFix });
|
|
8793
|
+
});
|
|
8794
|
+
onboard.command("scan").description('(deprecated) Use "codeharness audit" instead').action(async (_, cmd) => {
|
|
8795
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
8796
|
+
const isJson = globalOpts.json === true;
|
|
8797
|
+
warn("'onboard scan' is deprecated \u2014 use 'codeharness audit' instead");
|
|
8798
|
+
await executeAudit({ isJson, isFix: false });
|
|
8799
|
+
});
|
|
8800
|
+
}
|
|
8801
|
+
|
|
9401
8802
|
// src/commands/teardown.ts
|
|
9402
|
-
import { existsSync as
|
|
9403
|
-
import { join as
|
|
8803
|
+
import { existsSync as existsSync30, unlinkSync as unlinkSync2, readFileSync as readFileSync26, writeFileSync as writeFileSync16, rmSync as rmSync2 } from "fs";
|
|
8804
|
+
import { join as join27 } from "path";
|
|
9404
8805
|
function buildDefaultResult() {
|
|
9405
8806
|
return {
|
|
9406
8807
|
status: "ok",
|
|
@@ -9503,16 +8904,16 @@ function registerTeardownCommand(program) {
|
|
|
9503
8904
|
info("Docker stack: not running, skipping");
|
|
9504
8905
|
}
|
|
9505
8906
|
}
|
|
9506
|
-
const composeFilePath =
|
|
9507
|
-
if (
|
|
8907
|
+
const composeFilePath = join27(projectDir, composeFile);
|
|
8908
|
+
if (existsSync30(composeFilePath)) {
|
|
9508
8909
|
unlinkSync2(composeFilePath);
|
|
9509
8910
|
result.removed.push(composeFile);
|
|
9510
8911
|
if (!isJson) {
|
|
9511
8912
|
ok(`Removed: ${composeFile}`);
|
|
9512
8913
|
}
|
|
9513
8914
|
}
|
|
9514
|
-
const otelConfigPath =
|
|
9515
|
-
if (
|
|
8915
|
+
const otelConfigPath = join27(projectDir, "otel-collector-config.yaml");
|
|
8916
|
+
if (existsSync30(otelConfigPath)) {
|
|
9516
8917
|
unlinkSync2(otelConfigPath);
|
|
9517
8918
|
result.removed.push("otel-collector-config.yaml");
|
|
9518
8919
|
if (!isJson) {
|
|
@@ -9522,8 +8923,8 @@ function registerTeardownCommand(program) {
|
|
|
9522
8923
|
}
|
|
9523
8924
|
let patchesRemoved = 0;
|
|
9524
8925
|
for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
|
|
9525
|
-
const filePath =
|
|
9526
|
-
if (!
|
|
8926
|
+
const filePath = join27(projectDir, "_bmad", relativePath);
|
|
8927
|
+
if (!existsSync30(filePath)) {
|
|
9527
8928
|
continue;
|
|
9528
8929
|
}
|
|
9529
8930
|
try {
|
|
@@ -9543,10 +8944,10 @@ function registerTeardownCommand(program) {
|
|
|
9543
8944
|
}
|
|
9544
8945
|
}
|
|
9545
8946
|
if (state.otlp?.enabled && state.stack === "nodejs") {
|
|
9546
|
-
const pkgPath =
|
|
9547
|
-
if (
|
|
8947
|
+
const pkgPath = join27(projectDir, "package.json");
|
|
8948
|
+
if (existsSync30(pkgPath)) {
|
|
9548
8949
|
try {
|
|
9549
|
-
const raw =
|
|
8950
|
+
const raw = readFileSync26(pkgPath, "utf-8");
|
|
9550
8951
|
const pkg = JSON.parse(raw);
|
|
9551
8952
|
const scripts = pkg["scripts"];
|
|
9552
8953
|
if (scripts) {
|
|
@@ -9560,7 +8961,7 @@ function registerTeardownCommand(program) {
|
|
|
9560
8961
|
for (const key of keysToRemove) {
|
|
9561
8962
|
delete scripts[key];
|
|
9562
8963
|
}
|
|
9563
|
-
|
|
8964
|
+
writeFileSync16(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
9564
8965
|
result.otlp_cleaned = true;
|
|
9565
8966
|
if (!isJson) {
|
|
9566
8967
|
ok("OTLP: removed instrumented scripts from package.json");
|
|
@@ -9586,8 +8987,8 @@ function registerTeardownCommand(program) {
|
|
|
9586
8987
|
}
|
|
9587
8988
|
}
|
|
9588
8989
|
}
|
|
9589
|
-
const harnessDir =
|
|
9590
|
-
if (
|
|
8990
|
+
const harnessDir = join27(projectDir, ".harness");
|
|
8991
|
+
if (existsSync30(harnessDir)) {
|
|
9591
8992
|
rmSync2(harnessDir, { recursive: true, force: true });
|
|
9592
8993
|
result.removed.push(".harness/");
|
|
9593
8994
|
if (!isJson) {
|
|
@@ -9595,7 +8996,7 @@ function registerTeardownCommand(program) {
|
|
|
9595
8996
|
}
|
|
9596
8997
|
}
|
|
9597
8998
|
const statePath2 = getStatePath(projectDir);
|
|
9598
|
-
if (
|
|
8999
|
+
if (existsSync30(statePath2)) {
|
|
9599
9000
|
unlinkSync2(statePath2);
|
|
9600
9001
|
result.removed.push(".claude/codeharness.local.md");
|
|
9601
9002
|
if (!isJson) {
|
|
@@ -10339,8 +9740,8 @@ function registerQueryCommand(program) {
|
|
|
10339
9740
|
}
|
|
10340
9741
|
|
|
10341
9742
|
// src/commands/retro-import.ts
|
|
10342
|
-
import { existsSync as
|
|
10343
|
-
import { join as
|
|
9743
|
+
import { existsSync as existsSync31, readFileSync as readFileSync27 } from "fs";
|
|
9744
|
+
import { join as join28 } from "path";
|
|
10344
9745
|
|
|
10345
9746
|
// src/lib/retro-parser.ts
|
|
10346
9747
|
var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
|
|
@@ -10509,15 +9910,15 @@ function registerRetroImportCommand(program) {
|
|
|
10509
9910
|
return;
|
|
10510
9911
|
}
|
|
10511
9912
|
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
10512
|
-
const retroPath =
|
|
10513
|
-
if (!
|
|
9913
|
+
const retroPath = join28(root, STORY_DIR3, retroFile);
|
|
9914
|
+
if (!existsSync31(retroPath)) {
|
|
10514
9915
|
fail(`Retro file not found: ${retroFile}`, { json: isJson });
|
|
10515
9916
|
process.exitCode = 1;
|
|
10516
9917
|
return;
|
|
10517
9918
|
}
|
|
10518
9919
|
let content;
|
|
10519
9920
|
try {
|
|
10520
|
-
content =
|
|
9921
|
+
content = readFileSync27(retroPath, "utf-8");
|
|
10521
9922
|
} catch (err) {
|
|
10522
9923
|
const message = err instanceof Error ? err.message : String(err);
|
|
10523
9924
|
fail(`Failed to read retro file: ${message}`, { json: isJson });
|
|
@@ -10898,26 +10299,26 @@ function registerVerifyEnvCommand(program) {
|
|
|
10898
10299
|
}
|
|
10899
10300
|
|
|
10900
10301
|
// src/commands/retry.ts
|
|
10901
|
-
import { join as
|
|
10302
|
+
import { join as join30 } from "path";
|
|
10902
10303
|
|
|
10903
10304
|
// src/lib/retry-state.ts
|
|
10904
|
-
import { existsSync as
|
|
10905
|
-
import { join as
|
|
10305
|
+
import { existsSync as existsSync32, readFileSync as readFileSync28, writeFileSync as writeFileSync17 } from "fs";
|
|
10306
|
+
import { join as join29 } from "path";
|
|
10906
10307
|
var RETRIES_FILE = ".story_retries";
|
|
10907
10308
|
var FLAGGED_FILE = ".flagged_stories";
|
|
10908
10309
|
var LINE_PATTERN = /^([^=]+)=(\d+)$/;
|
|
10909
10310
|
function retriesPath(dir) {
|
|
10910
|
-
return
|
|
10311
|
+
return join29(dir, RETRIES_FILE);
|
|
10911
10312
|
}
|
|
10912
10313
|
function flaggedPath(dir) {
|
|
10913
|
-
return
|
|
10314
|
+
return join29(dir, FLAGGED_FILE);
|
|
10914
10315
|
}
|
|
10915
10316
|
function readRetries(dir) {
|
|
10916
10317
|
const filePath = retriesPath(dir);
|
|
10917
|
-
if (!
|
|
10318
|
+
if (!existsSync32(filePath)) {
|
|
10918
10319
|
return /* @__PURE__ */ new Map();
|
|
10919
10320
|
}
|
|
10920
|
-
const raw =
|
|
10321
|
+
const raw = readFileSync28(filePath, "utf-8");
|
|
10921
10322
|
const result = /* @__PURE__ */ new Map();
|
|
10922
10323
|
for (const line of raw.split("\n")) {
|
|
10923
10324
|
const trimmed = line.trim();
|
|
@@ -10939,7 +10340,7 @@ function writeRetries(dir, retries) {
|
|
|
10939
10340
|
for (const [key, count] of retries) {
|
|
10940
10341
|
lines.push(`${key}=${count}`);
|
|
10941
10342
|
}
|
|
10942
|
-
|
|
10343
|
+
writeFileSync17(filePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
|
|
10943
10344
|
}
|
|
10944
10345
|
function resetRetry(dir, storyKey) {
|
|
10945
10346
|
if (storyKey) {
|
|
@@ -10954,15 +10355,15 @@ function resetRetry(dir, storyKey) {
|
|
|
10954
10355
|
}
|
|
10955
10356
|
function readFlaggedStories(dir) {
|
|
10956
10357
|
const filePath = flaggedPath(dir);
|
|
10957
|
-
if (!
|
|
10358
|
+
if (!existsSync32(filePath)) {
|
|
10958
10359
|
return [];
|
|
10959
10360
|
}
|
|
10960
|
-
const raw =
|
|
10361
|
+
const raw = readFileSync28(filePath, "utf-8");
|
|
10961
10362
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l !== "");
|
|
10962
10363
|
}
|
|
10963
10364
|
function writeFlaggedStories(dir, stories) {
|
|
10964
10365
|
const filePath = flaggedPath(dir);
|
|
10965
|
-
|
|
10366
|
+
writeFileSync17(filePath, stories.length > 0 ? stories.join("\n") + "\n" : "", "utf-8");
|
|
10966
10367
|
}
|
|
10967
10368
|
function removeFlaggedStory(dir, key) {
|
|
10968
10369
|
const stories = readFlaggedStories(dir);
|
|
@@ -10982,7 +10383,7 @@ function registerRetryCommand(program) {
|
|
|
10982
10383
|
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) => {
|
|
10983
10384
|
const opts = cmd.optsWithGlobals();
|
|
10984
10385
|
const isJson = opts.json === true;
|
|
10985
|
-
const dir =
|
|
10386
|
+
const dir = join30(process.cwd(), RALPH_SUBDIR);
|
|
10986
10387
|
if (opts.story && !isValidStoryKey3(opts.story)) {
|
|
10987
10388
|
if (isJson) {
|
|
10988
10389
|
jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
|
|
@@ -11181,634 +10582,217 @@ function registerValidateCommand(program) {
|
|
|
11181
10582
|
process.exitCode = allPassed ? 0 : 1;
|
|
11182
10583
|
});
|
|
11183
10584
|
}
|
|
11184
|
-
function reportError(msg, isJson) {
|
|
11185
|
-
if (isJson) jsonOutput({ status: "fail", message: msg });
|
|
11186
|
-
else fail(msg);
|
|
11187
|
-
process.exitCode = 1;
|
|
11188
|
-
}
|
|
11189
|
-
function getFailures(p) {
|
|
11190
|
-
return p.perAC.filter((a) => a.status === "failed" || a.status === "blocked").map((a) => {
|
|
11191
|
-
const ac = getACById(a.acId);
|
|
11192
|
-
return {
|
|
11193
|
-
acId: a.acId,
|
|
11194
|
-
description: ac?.description ?? "unknown",
|
|
11195
|
-
command: ac?.command,
|
|
11196
|
-
output: a.lastError ?? "",
|
|
11197
|
-
attempts: a.attempts,
|
|
11198
|
-
blocker: a.status === "blocked" ? "blocked" : "failed"
|
|
11199
|
-
};
|
|
11200
|
-
});
|
|
11201
|
-
}
|
|
11202
|
-
function outputJson(p, cycles, allPassed) {
|
|
11203
|
-
jsonOutput({
|
|
11204
|
-
status: allPassed ? "pass" : "fail",
|
|
11205
|
-
total: p.total,
|
|
11206
|
-
passed: p.passed,
|
|
11207
|
-
failed: p.failed,
|
|
11208
|
-
blocked: p.blocked,
|
|
11209
|
-
remaining: p.remaining,
|
|
11210
|
-
cycles,
|
|
11211
|
-
gate: allPassed ? "RELEASE GATE: PASS -- v1.0 ready" : "RELEASE GATE: FAIL",
|
|
11212
|
-
failures: getFailures(p)
|
|
11213
|
-
});
|
|
11214
|
-
}
|
|
11215
|
-
function outputCi(p, allPassed) {
|
|
11216
|
-
if (allPassed) console.log("RELEASE GATE: PASS -- v1.0 ready");
|
|
11217
|
-
else console.log(`RELEASE GATE: FAIL (${p.passed}/${p.total} passed, ${p.failed} failed, ${p.blocked} blocked)`);
|
|
11218
|
-
}
|
|
11219
|
-
function outputHuman(p, cycles, allPassed) {
|
|
11220
|
-
console.log(`Total: ${p.total} | Passed: ${p.passed} | Failed: ${p.failed} | Blocked: ${p.blocked} | Cycles: ${cycles}`);
|
|
11221
|
-
if (allPassed) {
|
|
11222
|
-
ok("RELEASE GATE: PASS -- v1.0 ready");
|
|
11223
|
-
return;
|
|
11224
|
-
}
|
|
11225
|
-
for (const f of getFailures(p)) {
|
|
11226
|
-
console.log(` AC ${f.acId}: ${f.description}`);
|
|
11227
|
-
if (f.command) console.log(` Command: ${f.command}`);
|
|
11228
|
-
if (f.output) console.log(` Output: ${f.output}`);
|
|
11229
|
-
console.log(` Attempts: ${f.attempts}`);
|
|
11230
|
-
console.log(` Blocker: ${f.blocker}`);
|
|
11231
|
-
}
|
|
11232
|
-
fail("RELEASE GATE: FAIL");
|
|
11233
|
-
}
|
|
11234
|
-
|
|
11235
|
-
// src/commands/progress.ts
|
|
11236
|
-
function registerProgressCommand(program) {
|
|
11237
|
-
program.command("progress").description("Update live run progress in sprint-state.json").option("--story <key>", "Set run.currentStory").option("--phase <phase>", "Set run.currentPhase (create|dev|review|verify)").option("--action <text>", "Set run.lastAction").option("--ac-progress <progress>", 'Set run.acProgress (e.g., "4/12")').option("--clear", "Clear all run progress fields to null").action((opts, cmd) => {
|
|
11238
|
-
const globalOpts = cmd.optsWithGlobals();
|
|
11239
|
-
const isJson = globalOpts.json;
|
|
11240
|
-
const validPhases = ["create", "dev", "review", "verify"];
|
|
11241
|
-
if (opts.phase !== void 0 && !validPhases.includes(opts.phase)) {
|
|
11242
|
-
fail(`Invalid phase "${opts.phase}". Must be one of: ${validPhases.join(", ")}`, { json: isJson });
|
|
11243
|
-
process.exitCode = 1;
|
|
11244
|
-
return;
|
|
11245
|
-
}
|
|
11246
|
-
if (opts.clear) {
|
|
11247
|
-
const result2 = clearRunProgress2();
|
|
11248
|
-
if (result2.success) {
|
|
11249
|
-
if (isJson) {
|
|
11250
|
-
jsonOutput({ status: "ok", cleared: true });
|
|
11251
|
-
} else {
|
|
11252
|
-
ok("Run progress cleared");
|
|
11253
|
-
}
|
|
11254
|
-
} else {
|
|
11255
|
-
fail(result2.error, { json: isJson });
|
|
11256
|
-
process.exitCode = 1;
|
|
11257
|
-
}
|
|
11258
|
-
return;
|
|
11259
|
-
}
|
|
11260
|
-
const update = {
|
|
11261
|
-
...opts.story !== void 0 && { currentStory: opts.story },
|
|
11262
|
-
...opts.phase !== void 0 && { currentPhase: opts.phase },
|
|
11263
|
-
...opts.action !== void 0 && { lastAction: opts.action },
|
|
11264
|
-
...opts.acProgress !== void 0 && { acProgress: opts.acProgress }
|
|
11265
|
-
};
|
|
11266
|
-
if (Object.keys(update).length === 0) {
|
|
11267
|
-
fail("No progress fields specified. Use --story, --phase, --action, --ac-progress, or --clear.", { json: isJson });
|
|
11268
|
-
process.exitCode = 1;
|
|
11269
|
-
return;
|
|
11270
|
-
}
|
|
11271
|
-
const result = updateRunProgress2(update);
|
|
11272
|
-
if (result.success) {
|
|
11273
|
-
if (isJson) {
|
|
11274
|
-
jsonOutput({ status: "ok", updated: update });
|
|
11275
|
-
} else {
|
|
11276
|
-
ok("Run progress updated");
|
|
11277
|
-
}
|
|
11278
|
-
} else {
|
|
11279
|
-
fail(result.error, { json: isJson });
|
|
11280
|
-
process.exitCode = 1;
|
|
11281
|
-
}
|
|
11282
|
-
});
|
|
11283
|
-
}
|
|
11284
|
-
|
|
11285
|
-
// src/commands/observability-gate.ts
|
|
11286
|
-
function registerObservabilityGateCommand(program) {
|
|
11287
|
-
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) => {
|
|
11288
|
-
const globalOpts = cmd.optsWithGlobals();
|
|
11289
|
-
const isJson = opts.json === true || globalOpts.json === true;
|
|
11290
|
-
const root = process.cwd();
|
|
11291
|
-
const overrides = {};
|
|
11292
|
-
if (opts.minStatic !== void 0) {
|
|
11293
|
-
const parsed = parseInt(opts.minStatic, 10);
|
|
11294
|
-
if (isNaN(parsed) || parsed < 0 || parsed > 100) {
|
|
11295
|
-
if (isJson) {
|
|
11296
|
-
jsonOutput({ status: "error", message: "--min-static must be a number between 0 and 100" });
|
|
11297
|
-
} else {
|
|
11298
|
-
fail("--min-static must be a number between 0 and 100");
|
|
11299
|
-
}
|
|
11300
|
-
process.exitCode = 1;
|
|
11301
|
-
return;
|
|
11302
|
-
}
|
|
11303
|
-
overrides.staticTarget = parsed;
|
|
11304
|
-
}
|
|
11305
|
-
if (opts.minRuntime !== void 0) {
|
|
11306
|
-
const parsed = parseInt(opts.minRuntime, 10);
|
|
11307
|
-
if (isNaN(parsed) || parsed < 0 || parsed > 100) {
|
|
11308
|
-
if (isJson) {
|
|
11309
|
-
jsonOutput({ status: "error", message: "--min-runtime must be a number between 0 and 100" });
|
|
11310
|
-
} else {
|
|
11311
|
-
fail("--min-runtime must be a number between 0 and 100");
|
|
11312
|
-
}
|
|
11313
|
-
process.exitCode = 1;
|
|
11314
|
-
return;
|
|
11315
|
-
}
|
|
11316
|
-
overrides.runtimeTarget = parsed;
|
|
11317
|
-
}
|
|
11318
|
-
const result = checkObservabilityCoverageGate(root, overrides);
|
|
11319
|
-
if (!result.success) {
|
|
11320
|
-
if (isJson) {
|
|
11321
|
-
jsonOutput({ status: "error", message: result.error });
|
|
11322
|
-
} else {
|
|
11323
|
-
fail(`Observability gate error: ${result.error}`);
|
|
11324
|
-
}
|
|
11325
|
-
process.exitCode = 1;
|
|
11326
|
-
return;
|
|
11327
|
-
}
|
|
11328
|
-
const gate = result.data;
|
|
11329
|
-
if (isJson) {
|
|
11330
|
-
jsonOutput({
|
|
11331
|
-
status: gate.passed ? "pass" : "fail",
|
|
11332
|
-
passed: gate.passed,
|
|
11333
|
-
static: {
|
|
11334
|
-
current: gate.staticResult.current,
|
|
11335
|
-
target: gate.staticResult.target,
|
|
11336
|
-
met: gate.staticResult.met,
|
|
11337
|
-
gap: gate.staticResult.gap
|
|
11338
|
-
},
|
|
11339
|
-
runtime: gate.runtimeResult ? {
|
|
11340
|
-
current: gate.runtimeResult.current,
|
|
11341
|
-
target: gate.runtimeResult.target,
|
|
11342
|
-
met: gate.runtimeResult.met,
|
|
11343
|
-
gap: gate.runtimeResult.gap
|
|
11344
|
-
} : null,
|
|
11345
|
-
gaps: gate.gapSummary.map((g) => ({
|
|
11346
|
-
file: g.file,
|
|
11347
|
-
line: g.line,
|
|
11348
|
-
type: g.type,
|
|
11349
|
-
description: g.description
|
|
11350
|
-
}))
|
|
11351
|
-
});
|
|
11352
|
-
} else {
|
|
11353
|
-
const staticLine = `Static: ${gate.staticResult.current}% / ${gate.staticResult.target}% target`;
|
|
11354
|
-
if (gate.passed) {
|
|
11355
|
-
ok(`Observability gate passed. ${staticLine}`);
|
|
11356
|
-
if (gate.runtimeResult) {
|
|
11357
|
-
ok(`Runtime: ${gate.runtimeResult.current}% / ${gate.runtimeResult.target}% target`);
|
|
11358
|
-
}
|
|
11359
|
-
} else {
|
|
11360
|
-
fail(`Observability gate failed. ${staticLine}`);
|
|
11361
|
-
if (gate.runtimeResult && !gate.runtimeResult.met) {
|
|
11362
|
-
fail(`Runtime: ${gate.runtimeResult.current}% / ${gate.runtimeResult.target}% target`);
|
|
11363
|
-
}
|
|
11364
|
-
if (gate.gapSummary.length > 0) {
|
|
11365
|
-
fail("Gaps:");
|
|
11366
|
-
const shown = gate.gapSummary.slice(0, 5);
|
|
11367
|
-
for (const g of shown) {
|
|
11368
|
-
fail(` ${g.file}:${g.line} \u2014 ${g.description}`);
|
|
11369
|
-
}
|
|
11370
|
-
if (gate.gapSummary.length > 5) {
|
|
11371
|
-
fail(` ... and ${gate.gapSummary.length - 5} more.`);
|
|
11372
|
-
}
|
|
11373
|
-
}
|
|
11374
|
-
fail("Add logging to flagged functions. Run: codeharness observability-gate for details.");
|
|
11375
|
-
}
|
|
11376
|
-
}
|
|
11377
|
-
if (!gate.passed) {
|
|
11378
|
-
process.exitCode = 1;
|
|
11379
|
-
}
|
|
11380
|
-
});
|
|
11381
|
-
}
|
|
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
|
-
}
|
|
10585
|
+
function reportError(msg, isJson) {
|
|
10586
|
+
if (isJson) jsonOutput({ status: "fail", message: msg });
|
|
10587
|
+
else fail(msg);
|
|
10588
|
+
process.exitCode = 1;
|
|
11657
10589
|
}
|
|
11658
|
-
function
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
return
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
|
|
11666
|
-
|
|
11667
|
-
|
|
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
|
|
10590
|
+
function getFailures(p) {
|
|
10591
|
+
return p.perAC.filter((a) => a.status === "failed" || a.status === "blocked").map((a) => {
|
|
10592
|
+
const ac = getACById(a.acId);
|
|
10593
|
+
return {
|
|
10594
|
+
acId: a.acId,
|
|
10595
|
+
description: ac?.description ?? "unknown",
|
|
10596
|
+
command: ac?.command,
|
|
10597
|
+
output: a.lastError ?? "",
|
|
10598
|
+
attempts: a.attempts,
|
|
10599
|
+
blocker: a.status === "blocked" ? "blocked" : "failed"
|
|
11677
10600
|
};
|
|
11678
|
-
}
|
|
11679
|
-
const updatedSprint = computeSprintCounts2(updatedStories);
|
|
11680
|
-
return writeStateAtomic2({
|
|
11681
|
-
...current,
|
|
11682
|
-
sprint: updatedSprint,
|
|
11683
|
-
stories: updatedStories
|
|
11684
10601
|
});
|
|
11685
10602
|
}
|
|
11686
|
-
|
|
11687
|
-
|
|
11688
|
-
|
|
11689
|
-
|
|
11690
|
-
|
|
11691
|
-
|
|
11692
|
-
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
|
|
11697
|
-
|
|
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 });
|
|
10603
|
+
function outputJson(p, cycles, allPassed) {
|
|
10604
|
+
jsonOutput({
|
|
10605
|
+
status: allPassed ? "pass" : "fail",
|
|
10606
|
+
total: p.total,
|
|
10607
|
+
passed: p.passed,
|
|
10608
|
+
failed: p.failed,
|
|
10609
|
+
blocked: p.blocked,
|
|
10610
|
+
remaining: p.remaining,
|
|
10611
|
+
cycles,
|
|
10612
|
+
gate: allPassed ? "RELEASE GATE: PASS -- v1.0 ready" : "RELEASE GATE: FAIL",
|
|
10613
|
+
failures: getFailures(p)
|
|
10614
|
+
});
|
|
11715
10615
|
}
|
|
11716
|
-
function
|
|
11717
|
-
if (
|
|
11718
|
-
|
|
11719
|
-
|
|
10616
|
+
function outputCi(p, allPassed) {
|
|
10617
|
+
if (allPassed) console.log("RELEASE GATE: PASS -- v1.0 ready");
|
|
10618
|
+
else console.log(`RELEASE GATE: FAIL (${p.passed}/${p.total} passed, ${p.failed} failed, ${p.blocked} blocked)`);
|
|
10619
|
+
}
|
|
10620
|
+
function outputHuman(p, cycles, allPassed) {
|
|
10621
|
+
console.log(`Total: ${p.total} | Passed: ${p.passed} | Failed: ${p.failed} | Blocked: ${p.blocked} | Cycles: ${cycles}`);
|
|
10622
|
+
if (allPassed) {
|
|
10623
|
+
ok("RELEASE GATE: PASS -- v1.0 ready");
|
|
10624
|
+
return;
|
|
10625
|
+
}
|
|
10626
|
+
for (const f of getFailures(p)) {
|
|
10627
|
+
console.log(` AC ${f.acId}: ${f.description}`);
|
|
10628
|
+
if (f.command) console.log(` Command: ${f.command}`);
|
|
10629
|
+
if (f.output) console.log(` Output: ${f.output}`);
|
|
10630
|
+
console.log(` Attempts: ${f.attempts}`);
|
|
10631
|
+
console.log(` Blocker: ${f.blocker}`);
|
|
10632
|
+
}
|
|
10633
|
+
fail("RELEASE GATE: FAIL");
|
|
11720
10634
|
}
|
|
11721
10635
|
|
|
11722
|
-
// src/commands/
|
|
11723
|
-
function
|
|
11724
|
-
program.command("
|
|
10636
|
+
// src/commands/progress.ts
|
|
10637
|
+
function registerProgressCommand(program) {
|
|
10638
|
+
program.command("progress").description("Update live run progress in sprint-state.json").option("--story <key>", "Set run.currentStory").option("--phase <phase>", "Set run.currentPhase (create|dev|review|verify)").option("--action <text>", "Set run.lastAction").option("--ac-progress <progress>", 'Set run.acProgress (e.g., "4/12")').option("--clear", "Clear all run progress fields to null").action((opts, cmd) => {
|
|
11725
10639
|
const globalOpts = cmd.optsWithGlobals();
|
|
11726
|
-
const isJson =
|
|
11727
|
-
const
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
|
|
11732
|
-
|
|
11733
|
-
|
|
11734
|
-
|
|
10640
|
+
const isJson = globalOpts.json;
|
|
10641
|
+
const validPhases = ["create", "dev", "review", "verify"];
|
|
10642
|
+
if (opts.phase !== void 0 && !validPhases.includes(opts.phase)) {
|
|
10643
|
+
fail(`Invalid phase "${opts.phase}". Must be one of: ${validPhases.join(", ")}`, { json: isJson });
|
|
10644
|
+
process.exitCode = 1;
|
|
10645
|
+
return;
|
|
10646
|
+
}
|
|
10647
|
+
if (opts.clear) {
|
|
10648
|
+
const result2 = clearRunProgress2();
|
|
10649
|
+
if (result2.success) {
|
|
10650
|
+
if (isJson) {
|
|
10651
|
+
jsonOutput({ status: "ok", cleared: true });
|
|
10652
|
+
} else {
|
|
10653
|
+
ok("Run progress cleared");
|
|
10654
|
+
}
|
|
11735
10655
|
} else {
|
|
11736
|
-
fail(
|
|
10656
|
+
fail(result2.error, { json: isJson });
|
|
10657
|
+
process.exitCode = 1;
|
|
11737
10658
|
}
|
|
10659
|
+
return;
|
|
10660
|
+
}
|
|
10661
|
+
const update = {
|
|
10662
|
+
...opts.story !== void 0 && { currentStory: opts.story },
|
|
10663
|
+
...opts.phase !== void 0 && { currentPhase: opts.phase },
|
|
10664
|
+
...opts.action !== void 0 && { lastAction: opts.action },
|
|
10665
|
+
...opts.acProgress !== void 0 && { acProgress: opts.acProgress }
|
|
10666
|
+
};
|
|
10667
|
+
if (Object.keys(update).length === 0) {
|
|
10668
|
+
fail("No progress fields specified. Use --story, --phase, --action, --ac-progress, or --clear.", { json: isJson });
|
|
11738
10669
|
process.exitCode = 1;
|
|
11739
10670
|
return;
|
|
11740
10671
|
}
|
|
11741
|
-
const result =
|
|
11742
|
-
if (
|
|
10672
|
+
const result = updateRunProgress2(update);
|
|
10673
|
+
if (result.success) {
|
|
11743
10674
|
if (isJson) {
|
|
11744
|
-
jsonOutput({ status: "
|
|
10675
|
+
jsonOutput({ status: "ok", updated: update });
|
|
11745
10676
|
} else {
|
|
11746
|
-
|
|
10677
|
+
ok("Run progress updated");
|
|
11747
10678
|
}
|
|
10679
|
+
} else {
|
|
10680
|
+
fail(result.error, { json: isJson });
|
|
11748
10681
|
process.exitCode = 1;
|
|
11749
|
-
return;
|
|
11750
10682
|
}
|
|
11751
|
-
|
|
11752
|
-
|
|
11753
|
-
|
|
11754
|
-
|
|
11755
|
-
|
|
11756
|
-
|
|
10683
|
+
});
|
|
10684
|
+
}
|
|
10685
|
+
|
|
10686
|
+
// src/commands/observability-gate.ts
|
|
10687
|
+
function registerObservabilityGateCommand(program) {
|
|
10688
|
+
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) => {
|
|
10689
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
10690
|
+
const isJson = opts.json === true || globalOpts.json === true;
|
|
10691
|
+
const root = process.cwd();
|
|
10692
|
+
const overrides = {};
|
|
10693
|
+
if (opts.minStatic !== void 0) {
|
|
10694
|
+
const parsed = parseInt(opts.minStatic, 10);
|
|
10695
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 100) {
|
|
10696
|
+
if (isJson) {
|
|
10697
|
+
jsonOutput({ status: "error", message: "--min-static must be a number between 0 and 100" });
|
|
10698
|
+
} else {
|
|
10699
|
+
fail("--min-static must be a number between 0 and 100");
|
|
11757
10700
|
}
|
|
11758
|
-
|
|
11759
|
-
|
|
11760
|
-
|
|
11761
|
-
|
|
11762
|
-
|
|
11763
|
-
|
|
11764
|
-
|
|
11765
|
-
|
|
11766
|
-
|
|
11767
|
-
|
|
11768
|
-
|
|
11769
|
-
|
|
11770
|
-
info(`Generated ${fixResult.data.created} fix stories (${fixResult.data.skipped} skipped)`);
|
|
11771
|
-
}
|
|
11772
|
-
} else if (!isJson) {
|
|
11773
|
-
fail(fixResult.error);
|
|
10701
|
+
process.exitCode = 1;
|
|
10702
|
+
return;
|
|
10703
|
+
}
|
|
10704
|
+
overrides.staticTarget = parsed;
|
|
10705
|
+
}
|
|
10706
|
+
if (opts.minRuntime !== void 0) {
|
|
10707
|
+
const parsed = parseInt(opts.minRuntime, 10);
|
|
10708
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 100) {
|
|
10709
|
+
if (isJson) {
|
|
10710
|
+
jsonOutput({ status: "error", message: "--min-runtime must be a number between 0 and 100" });
|
|
10711
|
+
} else {
|
|
10712
|
+
fail("--min-runtime must be a number between 0 and 100");
|
|
11774
10713
|
}
|
|
10714
|
+
process.exitCode = 1;
|
|
10715
|
+
return;
|
|
10716
|
+
}
|
|
10717
|
+
overrides.runtimeTarget = parsed;
|
|
10718
|
+
}
|
|
10719
|
+
const result = checkObservabilityCoverageGate(root, overrides);
|
|
10720
|
+
if (!result.success) {
|
|
10721
|
+
if (isJson) {
|
|
10722
|
+
jsonOutput({ status: "error", message: result.error });
|
|
10723
|
+
} else {
|
|
10724
|
+
fail(`Observability gate error: ${result.error}`);
|
|
11775
10725
|
}
|
|
10726
|
+
process.exitCode = 1;
|
|
10727
|
+
return;
|
|
11776
10728
|
}
|
|
10729
|
+
const gate = result.data;
|
|
11777
10730
|
if (isJson) {
|
|
11778
|
-
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
11782
|
-
|
|
11783
|
-
|
|
11784
|
-
|
|
11785
|
-
|
|
11786
|
-
|
|
11787
|
-
|
|
11788
|
-
|
|
11789
|
-
|
|
11790
|
-
|
|
10731
|
+
jsonOutput({
|
|
10732
|
+
status: gate.passed ? "pass" : "fail",
|
|
10733
|
+
passed: gate.passed,
|
|
10734
|
+
static: {
|
|
10735
|
+
current: gate.staticResult.current,
|
|
10736
|
+
target: gate.staticResult.target,
|
|
10737
|
+
met: gate.staticResult.met,
|
|
10738
|
+
gap: gate.staticResult.gap
|
|
10739
|
+
},
|
|
10740
|
+
runtime: gate.runtimeResult ? {
|
|
10741
|
+
current: gate.runtimeResult.current,
|
|
10742
|
+
target: gate.runtimeResult.target,
|
|
10743
|
+
met: gate.runtimeResult.met,
|
|
10744
|
+
gap: gate.runtimeResult.gap
|
|
10745
|
+
} : null,
|
|
10746
|
+
gaps: gate.gapSummary.map((g) => ({
|
|
10747
|
+
file: g.file,
|
|
10748
|
+
line: g.line,
|
|
10749
|
+
type: g.type,
|
|
10750
|
+
description: g.description
|
|
10751
|
+
}))
|
|
10752
|
+
});
|
|
10753
|
+
} else {
|
|
10754
|
+
const staticLine = `Static: ${gate.staticResult.current}% / ${gate.staticResult.target}% target`;
|
|
10755
|
+
if (gate.passed) {
|
|
10756
|
+
ok(`Observability gate passed. ${staticLine}`);
|
|
10757
|
+
if (gate.runtimeResult) {
|
|
10758
|
+
ok(`Runtime: ${gate.runtimeResult.current}% / ${gate.runtimeResult.target}% target`);
|
|
10759
|
+
}
|
|
10760
|
+
} else {
|
|
10761
|
+
fail(`Observability gate failed. ${staticLine}`);
|
|
10762
|
+
if (gate.runtimeResult && !gate.runtimeResult.met) {
|
|
10763
|
+
fail(`Runtime: ${gate.runtimeResult.current}% / ${gate.runtimeResult.target}% target`);
|
|
10764
|
+
}
|
|
10765
|
+
if (gate.gapSummary.length > 0) {
|
|
10766
|
+
fail("Gaps:");
|
|
10767
|
+
const shown = gate.gapSummary.slice(0, 5);
|
|
10768
|
+
for (const g of shown) {
|
|
10769
|
+
fail(` ${g.file}:${g.line} \u2014 ${g.description}`);
|
|
10770
|
+
}
|
|
10771
|
+
if (gate.gapSummary.length > 5) {
|
|
10772
|
+
fail(` ... and ${gate.gapSummary.length - 5} more.`);
|
|
11791
10773
|
}
|
|
11792
|
-
} else if (fixStories && !fixStories.success) {
|
|
11793
|
-
jsonData.fixStories = [];
|
|
11794
|
-
jsonData.fixError = fixStories.error;
|
|
11795
10774
|
}
|
|
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);
|
|
10775
|
+
fail("Add logging to flagged functions. Run: codeharness observability-gate for details.");
|
|
11802
10776
|
}
|
|
11803
10777
|
}
|
|
11804
|
-
if (
|
|
10778
|
+
if (!gate.passed) {
|
|
11805
10779
|
process.exitCode = 1;
|
|
11806
10780
|
}
|
|
11807
10781
|
});
|
|
11808
10782
|
}
|
|
11809
10783
|
|
|
10784
|
+
// src/commands/audit.ts
|
|
10785
|
+
function registerAuditCommand(program) {
|
|
10786
|
+
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) => {
|
|
10787
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
10788
|
+
const isJson = opts.json === true || globalOpts.json === true;
|
|
10789
|
+
const isFix = opts.fix === true;
|
|
10790
|
+
await executeAudit({ isJson, isFix });
|
|
10791
|
+
});
|
|
10792
|
+
}
|
|
10793
|
+
|
|
11810
10794
|
// src/index.ts
|
|
11811
|
-
var VERSION = true ? "0.
|
|
10795
|
+
var VERSION = true ? "0.23.0" : "0.0.0-dev";
|
|
11812
10796
|
function createProgram() {
|
|
11813
10797
|
const program = new Command();
|
|
11814
10798
|
program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
|