codeharness 0.22.1 → 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 +689 -1696
- 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,114 +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 parts = [];
|
|
2878
|
-
if (groups["done"]?.length) {
|
|
2879
|
-
parts.push(`Done: ${fmt(groups["done"], "done")}`);
|
|
2880
|
-
}
|
|
2881
|
-
if (groups["in-progress"]?.length) {
|
|
2882
|
-
parts.push(`This: ${fmt(groups["in-progress"], "in-progress")}`);
|
|
2883
|
-
}
|
|
2884
|
-
if (groups["pending"]?.length) {
|
|
2885
|
-
parts.push(`Next: ${fmt(groups["pending"], "pending")}`);
|
|
2886
|
-
}
|
|
2887
|
-
if (groups["failed"]?.length) {
|
|
2888
|
-
parts.push(`Failed: ${fmt(groups["failed"], "failed")}`);
|
|
2889
|
-
}
|
|
2890
|
-
if (groups["blocked"]?.length) {
|
|
2891
|
-
parts.push(`Blocked: ${fmt(groups["blocked"], "blocked")}`);
|
|
2892
|
-
}
|
|
2893
|
-
return /* @__PURE__ */ jsx(Text, { children: parts.join(" | ") });
|
|
2894
|
-
}
|
|
2895
|
-
var MESSAGE_PREFIX = {
|
|
2896
|
-
ok: "[OK]",
|
|
2897
|
-
warn: "[WARN]",
|
|
2898
|
-
fail: "[FAIL]"
|
|
2899
|
-
};
|
|
2900
|
-
function StoryMessages({ messages }) {
|
|
2901
|
-
if (messages.length === 0) return null;
|
|
2902
|
-
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: messages.map((msg, i) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2903
|
-
/* @__PURE__ */ jsx(Text, { children: `${MESSAGE_PREFIX[msg.type]} Story ${msg.key}: ${msg.message}` }),
|
|
2904
|
-
msg.details?.map((d, j) => /* @__PURE__ */ jsx(Text, { children: ` \u2514 ${d}` }, j))
|
|
2905
|
-
] }, i)) });
|
|
2906
|
-
}
|
|
2907
2867
|
function RetryNotice({ info: info2 }) {
|
|
2908
|
-
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
2868
|
+
return /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
2909
2869
|
"\u23F3 API retry ",
|
|
2910
2870
|
info2.attempt,
|
|
2911
2871
|
" (waiting ",
|
|
@@ -2917,13 +2877,15 @@ function App({
|
|
|
2917
2877
|
state
|
|
2918
2878
|
}) {
|
|
2919
2879
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2880
|
+
/* @__PURE__ */ jsx(Static, { items: state.messages, children: (msg, i) => /* @__PURE__ */ jsx(StoryMessageLine, { msg }, i) }),
|
|
2920
2881
|
/* @__PURE__ */ jsx(Header, { info: state.sprintInfo }),
|
|
2921
2882
|
/* @__PURE__ */ jsx(StoryBreakdown, { stories: state.stories }),
|
|
2922
|
-
/* @__PURE__ */
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
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
|
+
] })
|
|
2927
2889
|
] });
|
|
2928
2890
|
}
|
|
2929
2891
|
|
|
@@ -2959,7 +2921,11 @@ function startRenderer(options) {
|
|
|
2959
2921
|
let cleaned = false;
|
|
2960
2922
|
const inkInstance = inkRender(/* @__PURE__ */ jsx2(App, { state }), {
|
|
2961
2923
|
exitOnCtrlC: false,
|
|
2962
|
-
patchConsole: false
|
|
2924
|
+
patchConsole: false,
|
|
2925
|
+
incrementalRendering: true,
|
|
2926
|
+
// Only redraw changed lines (v6.5+)
|
|
2927
|
+
maxFps: 15
|
|
2928
|
+
// Dashboard doesn't need 30fps
|
|
2963
2929
|
});
|
|
2964
2930
|
function rerender() {
|
|
2965
2931
|
if (!cleaned) {
|
|
@@ -3059,11 +3025,11 @@ var OLD_FILES = {
|
|
|
3059
3025
|
sprintStatusYaml: "_bmad-output/implementation-artifacts/sprint-status.yaml",
|
|
3060
3026
|
sessionIssues: "_bmad-output/implementation-artifacts/.session-issues.md"
|
|
3061
3027
|
};
|
|
3062
|
-
function resolve(
|
|
3063
|
-
return join9(process.cwd(),
|
|
3028
|
+
function resolve(relative2) {
|
|
3029
|
+
return join9(process.cwd(), relative2);
|
|
3064
3030
|
}
|
|
3065
|
-
function readIfExists(
|
|
3066
|
-
const p = resolve(
|
|
3031
|
+
function readIfExists(relative2) {
|
|
3032
|
+
const p = resolve(relative2);
|
|
3067
3033
|
if (!existsSync11(p)) return null;
|
|
3068
3034
|
try {
|
|
3069
3035
|
return readFileSync9(p, "utf-8");
|
|
@@ -4601,14 +4567,6 @@ function getExtension(filename) {
|
|
|
4601
4567
|
function isTestFile(filename) {
|
|
4602
4568
|
return filename.includes(".test.") || filename.includes(".spec.") || filename.includes("__tests__") || filename.startsWith("test_");
|
|
4603
4569
|
}
|
|
4604
|
-
function isDocStale(docPath, codeDir) {
|
|
4605
|
-
if (!existsSync18(docPath)) return true;
|
|
4606
|
-
if (!existsSync18(codeDir)) return false;
|
|
4607
|
-
const docMtime = statSync(docPath).mtime;
|
|
4608
|
-
const newestCode = getNewestSourceMtime(codeDir);
|
|
4609
|
-
if (newestCode === null) return false;
|
|
4610
|
-
return newestCode.getTime() > docMtime.getTime();
|
|
4611
|
-
}
|
|
4612
4570
|
function getNewestSourceMtime(dir) {
|
|
4613
4571
|
let newest = null;
|
|
4614
4572
|
function walk(current) {
|
|
@@ -7712,9 +7670,9 @@ function checkPerFileCoverage(floor, dir) {
|
|
|
7712
7670
|
const funcs = data.functions?.pct ?? 0;
|
|
7713
7671
|
const lines = data.lines?.pct ?? 0;
|
|
7714
7672
|
if (stmts < floor) {
|
|
7715
|
-
const
|
|
7673
|
+
const relative2 = key.startsWith(baseDir) ? key.slice(baseDir.length + 1) : key;
|
|
7716
7674
|
violations.push({
|
|
7717
|
-
file:
|
|
7675
|
+
file: relative2,
|
|
7718
7676
|
statements: stmts,
|
|
7719
7677
|
branches,
|
|
7720
7678
|
functions: funcs,
|
|
@@ -7797,85 +7755,6 @@ function runPreconditions(dir) {
|
|
|
7797
7755
|
hooks: hooksCheck.ok
|
|
7798
7756
|
};
|
|
7799
7757
|
}
|
|
7800
|
-
var STORY_KEY_PATTERN2 = /^\d+-\d+-/;
|
|
7801
|
-
function findVerificationGaps(dir) {
|
|
7802
|
-
const statuses = readSprintStatus(dir);
|
|
7803
|
-
const root = dir ?? process.cwd();
|
|
7804
|
-
const unverified = [];
|
|
7805
|
-
for (const [key, status] of Object.entries(statuses)) {
|
|
7806
|
-
if (status !== "done") continue;
|
|
7807
|
-
if (!STORY_KEY_PATTERN2.test(key)) continue;
|
|
7808
|
-
const proofPath = join24(root, "verification", `${key}-proof.md`);
|
|
7809
|
-
if (!existsSync27(proofPath)) {
|
|
7810
|
-
unverified.push(key);
|
|
7811
|
-
}
|
|
7812
|
-
}
|
|
7813
|
-
return [];
|
|
7814
|
-
}
|
|
7815
|
-
function findPerFileCoverageGaps(floor, dir) {
|
|
7816
|
-
const result = checkPerFileCoverage(floor, dir);
|
|
7817
|
-
const stories = [];
|
|
7818
|
-
let counter = 1;
|
|
7819
|
-
for (const violation of result.violations) {
|
|
7820
|
-
stories.push({
|
|
7821
|
-
key: `0.fc${counter}`,
|
|
7822
|
-
title: `Add test coverage for ${violation.file}`,
|
|
7823
|
-
type: "coverage",
|
|
7824
|
-
module: violation.file,
|
|
7825
|
-
acceptanceCriteria: [
|
|
7826
|
-
`**Given** ${violation.file} has ${violation.statements}% statement coverage (below ${floor}% floor)
|
|
7827
|
-
**When** the agent writes tests
|
|
7828
|
-
**Then** ${violation.file} reaches at least ${floor}% statement coverage`
|
|
7829
|
-
]
|
|
7830
|
-
});
|
|
7831
|
-
counter++;
|
|
7832
|
-
}
|
|
7833
|
-
return stories;
|
|
7834
|
-
}
|
|
7835
|
-
function findObservabilityGaps(dir) {
|
|
7836
|
-
let state;
|
|
7837
|
-
try {
|
|
7838
|
-
state = readState(dir);
|
|
7839
|
-
} catch {
|
|
7840
|
-
return [];
|
|
7841
|
-
}
|
|
7842
|
-
const stories = [];
|
|
7843
|
-
if (!state.otlp?.enabled) {
|
|
7844
|
-
stories.push({
|
|
7845
|
-
key: "0.o1",
|
|
7846
|
-
title: "Configure OTLP instrumentation",
|
|
7847
|
-
type: "observability",
|
|
7848
|
-
module: "otlp-config",
|
|
7849
|
-
acceptanceCriteria: [
|
|
7850
|
-
"**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"
|
|
7851
|
-
]
|
|
7852
|
-
});
|
|
7853
|
-
}
|
|
7854
|
-
if (state.docker?.compose_file) {
|
|
7855
|
-
if (!isStackRunning(state.docker.compose_file)) {
|
|
7856
|
-
stories.push({
|
|
7857
|
-
key: "0.o2",
|
|
7858
|
-
title: "Start Docker observability stack",
|
|
7859
|
-
type: "observability",
|
|
7860
|
-
module: "docker-stack",
|
|
7861
|
-
acceptanceCriteria: [
|
|
7862
|
-
"**Given** observability is enabled but Docker stack is not running\n**When** onboard runs\n**Then** Docker observability stack must be started"
|
|
7863
|
-
]
|
|
7864
|
-
});
|
|
7865
|
-
}
|
|
7866
|
-
} else {
|
|
7867
|
-
stories.push({
|
|
7868
|
-
key: "0.o2",
|
|
7869
|
-
title: "Start Docker observability stack",
|
|
7870
|
-
type: "observability",
|
|
7871
|
-
module: "docker-stack",
|
|
7872
|
-
acceptanceCriteria: [
|
|
7873
|
-
"**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"
|
|
7874
|
-
]
|
|
7875
|
-
});
|
|
7876
|
-
}
|
|
7877
|
-
return stories;
|
|
7878
|
-
}
|
|
7879
7758
|
var GAP_ID_PATTERN = /\[gap:[a-z-]+:[^\]]+\]/;
|
|
7880
7759
|
function getOnboardingProgress(beadsFns) {
|
|
7881
7760
|
let issues;
|
|
@@ -7896,42 +7775,6 @@ function getOnboardingProgress(beadsFns) {
|
|
|
7896
7775
|
).length;
|
|
7897
7776
|
return { total, resolved, remaining: total - resolved };
|
|
7898
7777
|
}
|
|
7899
|
-
function storyToGapId(story) {
|
|
7900
|
-
switch (story.type) {
|
|
7901
|
-
case "coverage":
|
|
7902
|
-
return buildGapId("coverage", story.module);
|
|
7903
|
-
case "agents-md":
|
|
7904
|
-
return buildGapId("docs", story.module + "/AGENTS.md");
|
|
7905
|
-
case "architecture":
|
|
7906
|
-
return buildGapId("docs", "ARCHITECTURE.md");
|
|
7907
|
-
case "doc-freshness":
|
|
7908
|
-
return buildGapId("docs", "stale-docs");
|
|
7909
|
-
case "verification":
|
|
7910
|
-
return buildGapId("verification", story.storyKey);
|
|
7911
|
-
case "observability":
|
|
7912
|
-
return buildGapId("observability", story.module);
|
|
7913
|
-
}
|
|
7914
|
-
}
|
|
7915
|
-
function filterTrackedGaps(stories, beadsFns) {
|
|
7916
|
-
let existingIssues;
|
|
7917
|
-
try {
|
|
7918
|
-
existingIssues = beadsFns.listIssues();
|
|
7919
|
-
} catch {
|
|
7920
|
-
return { untracked: [...stories], trackedCount: 0 };
|
|
7921
|
-
}
|
|
7922
|
-
const untracked = [];
|
|
7923
|
-
let trackedCount = 0;
|
|
7924
|
-
for (const story of stories) {
|
|
7925
|
-
const gapId = storyToGapId(story);
|
|
7926
|
-
const existing = findExistingByGapId(gapId, existingIssues);
|
|
7927
|
-
if (existing) {
|
|
7928
|
-
trackedCount++;
|
|
7929
|
-
} else {
|
|
7930
|
-
untracked.push(story);
|
|
7931
|
-
}
|
|
7932
|
-
}
|
|
7933
|
-
return { untracked, trackedCount };
|
|
7934
|
-
}
|
|
7935
7778
|
|
|
7936
7779
|
// src/commands/status.ts
|
|
7937
7780
|
function buildScopedEndpoints(endpoints, serviceName) {
|
|
@@ -8517,881 +8360,448 @@ function getBeadsData() {
|
|
|
8517
8360
|
}
|
|
8518
8361
|
}
|
|
8519
8362
|
|
|
8520
|
-
// src/
|
|
8521
|
-
import {
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
existsSync as existsSync28,
|
|
8526
|
-
readdirSync as readdirSync6,
|
|
8527
|
-
readFileSync as readFileSync25,
|
|
8528
|
-
statSync as statSync4
|
|
8529
|
-
} from "fs";
|
|
8530
|
-
import { join as join25, relative as relative2 } from "path";
|
|
8531
|
-
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
|
|
8532
|
-
var DEFAULT_MIN_MODULE_SIZE = 3;
|
|
8533
|
-
function getExtension2(filename) {
|
|
8534
|
-
const dot = filename.lastIndexOf(".");
|
|
8535
|
-
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 };
|
|
8536
8368
|
}
|
|
8537
|
-
function
|
|
8538
|
-
return
|
|
8369
|
+
function dimOk(name, status, metric, gaps = []) {
|
|
8370
|
+
return ok2({ name, status, metric, gaps });
|
|
8539
8371
|
}
|
|
8540
|
-
function
|
|
8541
|
-
|
|
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`)]);
|
|
8542
8375
|
}
|
|
8543
|
-
function
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
}
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
walk(fullPath);
|
|
8566
|
-
} else if (stat.isFile()) {
|
|
8567
|
-
const ext = getExtension2(entry);
|
|
8568
|
-
if (SOURCE_EXTENSIONS2.has(ext) && !isTestFile2(entry)) {
|
|
8569
|
-
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"));
|
|
8570
8398
|
}
|
|
8571
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"));
|
|
8572
8404
|
}
|
|
8573
|
-
|
|
8574
|
-
walk(dir);
|
|
8575
|
-
return count;
|
|
8576
|
-
}
|
|
8577
|
-
function countModuleFiles(modulePath, rootDir) {
|
|
8578
|
-
const fullModulePath = join25(rootDir, modulePath);
|
|
8579
|
-
let sourceFiles = 0;
|
|
8580
|
-
let testFiles = 0;
|
|
8581
|
-
function walk(current) {
|
|
8582
|
-
let entries;
|
|
8405
|
+
let rStatus = "pass", rMetric = "";
|
|
8583
8406
|
try {
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
}
|
|
8597
|
-
if (stat.isDirectory()) {
|
|
8598
|
-
walk(fullPath);
|
|
8599
|
-
} else if (stat.isFile()) {
|
|
8600
|
-
const ext = getExtension2(entry);
|
|
8601
|
-
if (SOURCE_EXTENSIONS2.has(ext)) {
|
|
8602
|
-
if (isTestFile2(entry) || current.includes("__tests__")) {
|
|
8603
|
-
testFiles++;
|
|
8604
|
-
} else {
|
|
8605
|
-
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"));
|
|
8606
8419
|
}
|
|
8607
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"));
|
|
8608
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"));
|
|
8609
8430
|
}
|
|
8431
|
+
return dimOk("observability", worstStatus(sStatus, rStatus), `${sMetric}, ${rMetric}`, gaps);
|
|
8432
|
+
} catch (err) {
|
|
8433
|
+
return dimCatch("observability", err);
|
|
8610
8434
|
}
|
|
8611
|
-
walk(fullModulePath);
|
|
8612
|
-
return { sourceFiles, testFiles };
|
|
8613
|
-
}
|
|
8614
|
-
function detectArtifacts(dir) {
|
|
8615
|
-
const bmadPath = join25(dir, "_bmad");
|
|
8616
|
-
const hasBmad = existsSync28(bmadPath);
|
|
8617
|
-
return {
|
|
8618
|
-
hasBmad,
|
|
8619
|
-
bmadPath: hasBmad ? relative2(dir, bmadPath) || "_bmad" : null
|
|
8620
|
-
};
|
|
8621
|
-
}
|
|
8622
|
-
function scanCodebase(dir, options) {
|
|
8623
|
-
const threshold = options?.minModuleSize ?? DEFAULT_MIN_MODULE_SIZE;
|
|
8624
|
-
const modulePaths = findModules(dir, threshold);
|
|
8625
|
-
const modules = modulePaths.map((modPath) => {
|
|
8626
|
-
const { sourceFiles, testFiles } = countModuleFiles(modPath, dir);
|
|
8627
|
-
return { path: modPath, sourceFiles, testFiles };
|
|
8628
|
-
});
|
|
8629
|
-
const totalSourceFiles = countSourceFiles(dir);
|
|
8630
|
-
const artifacts = detectArtifacts(dir);
|
|
8631
|
-
return { modules, totalSourceFiles, artifacts };
|
|
8632
8435
|
}
|
|
8633
|
-
function
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
}
|
|
8647
|
-
const overall = parseCoverageReport(baseDir, toolInfo.reportFormat);
|
|
8648
|
-
const perFileCoverage = readPerFileCoverage(baseDir, toolInfo.reportFormat);
|
|
8649
|
-
let totalUncovered = 0;
|
|
8650
|
-
const moduleCoverage = modules.map((mod) => {
|
|
8651
|
-
if (perFileCoverage === null) {
|
|
8652
|
-
return {
|
|
8653
|
-
path: mod.path,
|
|
8654
|
-
coveragePercent: overall,
|
|
8655
|
-
uncoveredFileCount: 0
|
|
8656
|
-
};
|
|
8657
|
-
}
|
|
8658
|
-
let coveredSum = 0;
|
|
8659
|
-
let fileCount = 0;
|
|
8660
|
-
let uncoveredCount = 0;
|
|
8661
|
-
for (const [filePath, pct] of perFileCoverage.entries()) {
|
|
8662
|
-
const relPath = filePath.startsWith("/") ? relative2(baseDir, filePath) : filePath;
|
|
8663
|
-
if (relPath.startsWith(mod.path + "/") || relPath === mod.path) {
|
|
8664
|
-
fileCount++;
|
|
8665
|
-
coveredSum += pct;
|
|
8666
|
-
if (pct === 0) {
|
|
8667
|
-
uncoveredCount++;
|
|
8668
|
-
}
|
|
8669
|
-
}
|
|
8436
|
+
function checkTesting(projectDir) {
|
|
8437
|
+
try {
|
|
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"));
|
|
8670
8449
|
}
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
return
|
|
8674
|
-
path: mod.path,
|
|
8675
|
-
coveragePercent: modulePercent,
|
|
8676
|
-
uncoveredFileCount: uncoveredCount
|
|
8677
|
-
};
|
|
8678
|
-
});
|
|
8679
|
-
if (perFileCoverage === null) {
|
|
8680
|
-
totalUncovered = 0;
|
|
8681
|
-
}
|
|
8682
|
-
return {
|
|
8683
|
-
overall,
|
|
8684
|
-
modules: moduleCoverage,
|
|
8685
|
-
uncoveredFiles: totalUncovered
|
|
8686
|
-
};
|
|
8687
|
-
}
|
|
8688
|
-
function readPerFileCoverage(dir, format) {
|
|
8689
|
-
if (format === "vitest-json" || format === "jest-json") {
|
|
8690
|
-
return readVitestPerFileCoverage(dir);
|
|
8691
|
-
}
|
|
8692
|
-
if (format === "coverage-py-json") {
|
|
8693
|
-
return readPythonPerFileCoverage(dir);
|
|
8450
|
+
return dimOk("testing", status, `${pct}%`, gaps);
|
|
8451
|
+
} catch (err) {
|
|
8452
|
+
return dimCatch("testing", err);
|
|
8694
8453
|
}
|
|
8695
|
-
return null;
|
|
8696
8454
|
}
|
|
8697
|
-
function
|
|
8698
|
-
const reportPath = join25(dir, "coverage", "coverage-summary.json");
|
|
8699
|
-
if (!existsSync28(reportPath)) return null;
|
|
8455
|
+
function checkDocumentation(projectDir) {
|
|
8700
8456
|
try {
|
|
8701
|
-
const report =
|
|
8702
|
-
const
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
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}`));
|
|
8706
8464
|
}
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
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`));
|
|
8468
|
+
}
|
|
8469
|
+
return dimOk("documentation", status, `${fresh} fresh, ${stale} stale, ${missing} missing`, gaps);
|
|
8470
|
+
} catch (err) {
|
|
8471
|
+
return dimCatch("documentation", err);
|
|
8710
8472
|
}
|
|
8711
8473
|
}
|
|
8712
|
-
function
|
|
8713
|
-
const reportPath = join25(dir, "coverage.json");
|
|
8714
|
-
if (!existsSync28(reportPath)) return null;
|
|
8474
|
+
function checkVerification(projectDir) {
|
|
8715
8475
|
try {
|
|
8716
|
-
const
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
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
|
+
}
|
|
8491
|
+
}
|
|
8721
8492
|
}
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
}
|
|
8727
|
-
|
|
8728
|
-
function auditDocumentation(dir) {
|
|
8729
|
-
const root = dir ?? process.cwd();
|
|
8730
|
-
const documents = [];
|
|
8731
|
-
for (const docName of AUDIT_DOCUMENTS) {
|
|
8732
|
-
const docPath = join25(root, docName);
|
|
8733
|
-
if (!existsSync28(docPath)) {
|
|
8734
|
-
documents.push({ name: docName, grade: "missing", path: null });
|
|
8735
|
-
continue;
|
|
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";
|
|
8736
8499
|
}
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
documents.push({
|
|
8741
|
-
name: docName,
|
|
8742
|
-
grade: stale ? "stale" : "present",
|
|
8743
|
-
path: docName
|
|
8744
|
-
});
|
|
8500
|
+
return dimOk("verification", status, totalChecked > 0 ? `${proofCount}/${totalChecked} verified` : "no proofs", gaps);
|
|
8501
|
+
} catch (err) {
|
|
8502
|
+
return dimCatch("verification", err);
|
|
8745
8503
|
}
|
|
8746
|
-
|
|
8747
|
-
|
|
8504
|
+
}
|
|
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;
|
|
8748
8510
|
try {
|
|
8749
|
-
|
|
8750
|
-
if (stat.isDirectory()) {
|
|
8751
|
-
documents.push({ name: "docs/", grade: "present", path: "docs/" });
|
|
8752
|
-
}
|
|
8511
|
+
content = readFileSync25(dfPath, "utf-8");
|
|
8753
8512
|
} catch {
|
|
8754
|
-
|
|
8755
|
-
}
|
|
8756
|
-
} else {
|
|
8757
|
-
documents.push({ name: "docs/", grade: "missing", path: null });
|
|
8758
|
-
}
|
|
8759
|
-
const indexPath = join25(root, "docs", "index.md");
|
|
8760
|
-
if (existsSync28(indexPath)) {
|
|
8761
|
-
const srcDir = join25(root, "src");
|
|
8762
|
-
const indexCodeDir = existsSync28(srcDir) ? srcDir : root;
|
|
8763
|
-
const indexStale = isDocStale(indexPath, indexCodeDir);
|
|
8764
|
-
documents.push({
|
|
8765
|
-
name: "docs/index.md",
|
|
8766
|
-
grade: indexStale ? "stale" : "present",
|
|
8767
|
-
path: "docs/index.md"
|
|
8768
|
-
});
|
|
8769
|
-
} else {
|
|
8770
|
-
documents.push({ name: "docs/index.md", grade: "missing", path: null });
|
|
8771
|
-
}
|
|
8772
|
-
const summaryParts = documents.filter((d) => !d.name.startsWith("docs/")).map((d) => `${d.name}(${d.grade})`);
|
|
8773
|
-
const summary = summaryParts.join(" ");
|
|
8774
|
-
return { documents, summary };
|
|
8775
|
-
}
|
|
8776
|
-
|
|
8777
|
-
// src/lib/epic-generator.ts
|
|
8778
|
-
import { createInterface } from "readline";
|
|
8779
|
-
import { existsSync as existsSync29, mkdirSync as mkdirSync9, writeFileSync as writeFileSync15 } from "fs";
|
|
8780
|
-
import { dirname as dirname7, join as join26 } from "path";
|
|
8781
|
-
var PRIORITY_BY_TYPE = {
|
|
8782
|
-
observability: 1,
|
|
8783
|
-
coverage: 2,
|
|
8784
|
-
verification: 2,
|
|
8785
|
-
"agents-md": 3,
|
|
8786
|
-
architecture: 3,
|
|
8787
|
-
"doc-freshness": 3
|
|
8788
|
-
};
|
|
8789
|
-
function generateOnboardingEpic(scan, coverage, audit, rootDir) {
|
|
8790
|
-
const root = rootDir ?? process.cwd();
|
|
8791
|
-
const stories = [];
|
|
8792
|
-
let storyNum = 1;
|
|
8793
|
-
const readmeDoc = audit.documents.find((d) => d.name === "README.md");
|
|
8794
|
-
if (readmeDoc && readmeDoc.grade === "missing") {
|
|
8795
|
-
stories.push({
|
|
8796
|
-
key: `0.${storyNum}`,
|
|
8797
|
-
title: "Create README.md",
|
|
8798
|
-
type: "doc-freshness",
|
|
8799
|
-
acceptanceCriteria: [
|
|
8800
|
-
"**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"
|
|
8801
|
-
]
|
|
8802
|
-
});
|
|
8803
|
-
storyNum++;
|
|
8804
|
-
}
|
|
8805
|
-
const archDoc = audit.documents.find((d) => d.name === "ARCHITECTURE.md");
|
|
8806
|
-
if (archDoc && archDoc.grade === "missing") {
|
|
8807
|
-
stories.push({
|
|
8808
|
-
key: `0.${storyNum}`,
|
|
8809
|
-
title: "Create ARCHITECTURE.md",
|
|
8810
|
-
type: "architecture",
|
|
8811
|
-
acceptanceCriteria: [
|
|
8812
|
-
"**Given** no ARCHITECTURE.md exists\n**When** the agent analyzes the codebase\n**Then** ARCHITECTURE.md is created with module overview and dependencies"
|
|
8813
|
-
]
|
|
8814
|
-
});
|
|
8815
|
-
storyNum++;
|
|
8816
|
-
}
|
|
8817
|
-
for (const mod of scan.modules) {
|
|
8818
|
-
const agentsPath = join26(root, mod.path, "AGENTS.md");
|
|
8819
|
-
if (!existsSync29(agentsPath)) {
|
|
8820
|
-
stories.push({
|
|
8821
|
-
key: `0.${storyNum}`,
|
|
8822
|
-
title: `Create ${mod.path}/AGENTS.md`,
|
|
8823
|
-
type: "agents-md",
|
|
8824
|
-
module: mod.path,
|
|
8825
|
-
acceptanceCriteria: [
|
|
8826
|
-
`**Given** ${mod.path} has ${mod.sourceFiles} source files and no AGENTS.md
|
|
8827
|
-
**When** the agent reads the module
|
|
8828
|
-
**Then** ${mod.path}/AGENTS.md is created with module purpose and key files`
|
|
8829
|
-
]
|
|
8830
|
-
});
|
|
8831
|
-
storyNum++;
|
|
8832
|
-
}
|
|
8833
|
-
}
|
|
8834
|
-
for (const mod of coverage.modules) {
|
|
8835
|
-
if (mod.coveragePercent < 100) {
|
|
8836
|
-
stories.push({
|
|
8837
|
-
key: `0.${storyNum}`,
|
|
8838
|
-
title: `Add test coverage for ${mod.path}`,
|
|
8839
|
-
type: "coverage",
|
|
8840
|
-
module: mod.path,
|
|
8841
|
-
acceptanceCriteria: [
|
|
8842
|
-
`**Given** ${mod.path} has ${mod.uncoveredFileCount} uncovered files at ${mod.coveragePercent}% coverage
|
|
8843
|
-
**When** the agent writes tests
|
|
8844
|
-
**Then** ${mod.path} has 100% test coverage`
|
|
8845
|
-
]
|
|
8846
|
-
});
|
|
8847
|
-
storyNum++;
|
|
8848
|
-
}
|
|
8849
|
-
}
|
|
8850
|
-
const staleDocs = audit.documents.filter((d) => d.grade === "stale");
|
|
8851
|
-
if (staleDocs.length > 0) {
|
|
8852
|
-
const staleNames = staleDocs.map((d) => d.name).join(", ");
|
|
8853
|
-
stories.push({
|
|
8854
|
-
key: `0.${storyNum}`,
|
|
8855
|
-
title: "Update stale documentation",
|
|
8856
|
-
type: "doc-freshness",
|
|
8857
|
-
acceptanceCriteria: [
|
|
8858
|
-
`**Given** the following documents are stale: ${staleNames}
|
|
8859
|
-
**When** the agent reviews them against current source
|
|
8860
|
-
**Then** all stale documents are updated to reflect the current codebase`
|
|
8861
|
-
]
|
|
8862
|
-
});
|
|
8863
|
-
storyNum++;
|
|
8864
|
-
}
|
|
8865
|
-
const coverageStories = stories.filter((s) => s.type === "coverage").length;
|
|
8866
|
-
const docStories = stories.filter(
|
|
8867
|
-
(s) => s.type === "agents-md" || s.type === "architecture" || s.type === "doc-freshness"
|
|
8868
|
-
).length;
|
|
8869
|
-
const verificationStories = stories.filter((s) => s.type === "verification").length;
|
|
8870
|
-
const observabilityStories = stories.filter((s) => s.type === "observability").length;
|
|
8871
|
-
return {
|
|
8872
|
-
title: "Onboarding Epic: Bring Project to Harness Compliance",
|
|
8873
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
8874
|
-
stories,
|
|
8875
|
-
summary: {
|
|
8876
|
-
totalStories: stories.length,
|
|
8877
|
-
coverageStories,
|
|
8878
|
-
docStories,
|
|
8879
|
-
verificationStories,
|
|
8880
|
-
observabilityStories
|
|
8513
|
+
return dimOk("infrastructure", "warn", "Dockerfile unreadable", [gap("infrastructure", "Dockerfile exists but could not be read", "Check Dockerfile permissions")]);
|
|
8881
8514
|
}
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
lines.push(`### Story ${story.key}: ${story.title}`);
|
|
8895
|
-
lines.push("");
|
|
8896
|
-
if (story.type === "coverage") {
|
|
8897
|
-
lines.push(`As a developer, I want tests for ${story.module} to ensure correctness.`);
|
|
8898
|
-
} else if (story.type === "agents-md") {
|
|
8899
|
-
lines.push(`As an agent, I want AGENTS.md for ${story.module} so I have local context.`);
|
|
8900
|
-
} else if (story.type === "architecture") {
|
|
8901
|
-
lines.push("As a developer, I want an ARCHITECTURE.md documenting the project's architecture.");
|
|
8902
|
-
} else if (story.type === "doc-freshness") {
|
|
8903
|
-
lines.push("As a developer, I want up-to-date documentation reflecting the current codebase.");
|
|
8904
|
-
} else if (story.type === "verification") {
|
|
8905
|
-
lines.push(`As a developer, I want verification proof for ${story.storyKey} to ensure it's properly documented.`);
|
|
8906
|
-
} else if (story.type === "observability") {
|
|
8907
|
-
lines.push("As a developer, I want observability infrastructure configured so the harness can monitor runtime behavior.");
|
|
8908
|
-
}
|
|
8909
|
-
lines.push("");
|
|
8910
|
-
for (const ac of story.acceptanceCriteria) {
|
|
8911
|
-
lines.push(ac);
|
|
8912
|
-
}
|
|
8913
|
-
lines.push("");
|
|
8914
|
-
}
|
|
8915
|
-
lines.push("---");
|
|
8916
|
-
lines.push("");
|
|
8917
|
-
lines.push(`**Total stories:** ${epic.stories.length}`);
|
|
8918
|
-
lines.push("");
|
|
8919
|
-
lines.push("Review and approve before execution.");
|
|
8920
|
-
lines.push("");
|
|
8921
|
-
writeFileSync15(outputPath, lines.join("\n"), "utf-8");
|
|
8922
|
-
}
|
|
8923
|
-
function formatEpicSummary(epic) {
|
|
8924
|
-
const { totalStories, coverageStories, docStories, verificationStories, observabilityStories } = epic.summary;
|
|
8925
|
-
return `Onboarding plan: ${totalStories} stories (${coverageStories} coverage, ${docStories} documentation, ${verificationStories} verification, ${observabilityStories} observability)`;
|
|
8926
|
-
}
|
|
8927
|
-
function promptApproval() {
|
|
8928
|
-
return new Promise((resolve3) => {
|
|
8929
|
-
let answered = false;
|
|
8930
|
-
const rl = createInterface({
|
|
8931
|
-
input: process.stdin,
|
|
8932
|
-
output: process.stdout
|
|
8933
|
-
});
|
|
8934
|
-
rl.on("close", () => {
|
|
8935
|
-
if (!answered) {
|
|
8936
|
-
answered = true;
|
|
8937
|
-
resolve3(false);
|
|
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)`));
|
|
8938
8527
|
}
|
|
8939
|
-
});
|
|
8940
|
-
rl.question("Review the onboarding plan. Approve? [Y/n] ", (answer) => {
|
|
8941
|
-
answered = true;
|
|
8942
|
-
rl.close();
|
|
8943
|
-
const trimmed = answer.trim().toLowerCase();
|
|
8944
|
-
resolve3(trimmed === "" || trimmed === "y");
|
|
8945
|
-
});
|
|
8946
|
-
});
|
|
8947
|
-
}
|
|
8948
|
-
function importOnboardingEpic(epicPath, beadsFns) {
|
|
8949
|
-
const epics = parseEpicsFile(epicPath);
|
|
8950
|
-
const allStories = [];
|
|
8951
|
-
for (const epic of epics) {
|
|
8952
|
-
for (const story of epic.stories) {
|
|
8953
|
-
allStories.push(story);
|
|
8954
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);
|
|
8955
8534
|
}
|
|
8956
|
-
|
|
8535
|
+
}
|
|
8536
|
+
function readdirSafe(dir) {
|
|
8537
|
+
try {
|
|
8538
|
+
return readdirSync6(dir);
|
|
8539
|
+
} catch {
|
|
8957
8540
|
return [];
|
|
8958
8541
|
}
|
|
8959
|
-
const wrappedBeadsFns = {
|
|
8960
|
-
listIssues: beadsFns.listIssues,
|
|
8961
|
-
createIssue: (title, opts) => {
|
|
8962
|
-
const priority = getPriorityFromTitle(title);
|
|
8963
|
-
const gapId = getGapIdFromTitle(title);
|
|
8964
|
-
const description = gapId ? appendGapId(opts?.description, gapId) : opts?.description;
|
|
8965
|
-
return beadsFns.createIssue(title, {
|
|
8966
|
-
...opts,
|
|
8967
|
-
type: "task",
|
|
8968
|
-
priority,
|
|
8969
|
-
description
|
|
8970
|
-
});
|
|
8971
|
-
}
|
|
8972
|
-
};
|
|
8973
|
-
return importStoriesToBeads(allStories, {}, wrappedBeadsFns);
|
|
8974
8542
|
}
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
if (title.startsWith("Create ") && title.endsWith("/AGENTS.md")) {
|
|
8991
|
-
const mod = title.slice("Create ".length);
|
|
8992
|
-
return `[gap:docs:${mod}]`;
|
|
8993
|
-
}
|
|
8994
|
-
if (title === "Create README.md") {
|
|
8995
|
-
return "[gap:docs:README.md]";
|
|
8996
|
-
}
|
|
8997
|
-
if (title === "Create ARCHITECTURE.md") {
|
|
8998
|
-
return "[gap:docs:ARCHITECTURE.md]";
|
|
8999
|
-
}
|
|
9000
|
-
if (title === "Update stale documentation") {
|
|
9001
|
-
return "[gap:docs:stale-docs]";
|
|
9002
|
-
}
|
|
9003
|
-
if (title.startsWith("Create verification proof for ")) {
|
|
9004
|
-
const key = title.slice("Create verification proof for ".length);
|
|
9005
|
-
return `[gap:verification:${key}]`;
|
|
9006
|
-
}
|
|
9007
|
-
if (title === "Configure OTLP instrumentation") {
|
|
9008
|
-
return "[gap:observability:otlp-config]";
|
|
9009
|
-
}
|
|
9010
|
-
if (title === "Start Docker observability stack") {
|
|
9011
|
-
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
|
+
}
|
|
9012
8558
|
}
|
|
9013
|
-
|
|
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;
|
|
9014
8568
|
}
|
|
9015
8569
|
|
|
9016
|
-
// src/
|
|
9017
|
-
import { existsSync as
|
|
9018
|
-
import { join as
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
function saveScanCache(entry, dir) {
|
|
9023
|
-
try {
|
|
9024
|
-
const root = dir ?? process.cwd();
|
|
9025
|
-
const cacheDir = join27(root, CACHE_DIR);
|
|
9026
|
-
mkdirSync10(cacheDir, { recursive: true });
|
|
9027
|
-
const cachePath = join27(cacheDir, CACHE_FILE);
|
|
9028
|
-
writeFileSync16(cachePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
9029
|
-
} catch {
|
|
9030
|
-
}
|
|
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}`;
|
|
9031
8576
|
}
|
|
9032
|
-
function
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
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");
|
|
9044
8598
|
}
|
|
9045
|
-
function
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
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}`);
|
|
9053
8636
|
}
|
|
9054
|
-
return Date.now() - ts < max;
|
|
9055
8637
|
}
|
|
9056
|
-
function
|
|
9057
|
-
|
|
9058
|
-
|
|
8638
|
+
function addFixStoriesToState(stories) {
|
|
8639
|
+
const newStories = stories.filter((s) => !s.skipped);
|
|
8640
|
+
if (newStories.length === 0) {
|
|
8641
|
+
return ok2(void 0);
|
|
9059
8642
|
}
|
|
9060
|
-
const
|
|
9061
|
-
if (!
|
|
9062
|
-
return
|
|
8643
|
+
const stateResult = getSprintState2();
|
|
8644
|
+
if (!stateResult.success) {
|
|
8645
|
+
return fail2(stateResult.error);
|
|
9063
8646
|
}
|
|
9064
|
-
|
|
9065
|
-
|
|
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
|
+
};
|
|
9066
8658
|
}
|
|
9067
|
-
|
|
8659
|
+
const updatedSprint = computeSprintCounts2(updatedStories);
|
|
8660
|
+
return writeStateAtomic2({
|
|
8661
|
+
...current,
|
|
8662
|
+
sprint: updatedSprint,
|
|
8663
|
+
stories: updatedStories
|
|
8664
|
+
});
|
|
9068
8665
|
}
|
|
9069
8666
|
|
|
9070
|
-
// src/
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
for (const w of preconditions.warnings) {
|
|
9092
|
-
warn(w);
|
|
9093
|
-
}
|
|
9094
|
-
}
|
|
9095
|
-
const result = runScan(minModuleSize);
|
|
9096
|
-
saveScanCache({
|
|
9097
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9098
|
-
scan: result,
|
|
9099
|
-
coverage: null,
|
|
9100
|
-
audit: null
|
|
9101
|
-
});
|
|
9102
|
-
if (isJson) {
|
|
9103
|
-
jsonOutput({ preconditions: { initialized: preconditions.initialized, bmad: preconditions.bmad, hooks: preconditions.hooks }, scan: result });
|
|
9104
|
-
} else {
|
|
9105
|
-
printScanOutput(result);
|
|
9106
|
-
}
|
|
9107
|
-
});
|
|
9108
|
-
onboard.command("coverage").description("Analyze per-module coverage gaps").action((_, cmd) => {
|
|
9109
|
-
const opts = cmd.optsWithGlobals();
|
|
9110
|
-
const isJson = opts.json === true;
|
|
9111
|
-
const forceScan = opts.forceScan === true;
|
|
9112
|
-
const minModuleSize = parseInt(opts.minModuleSize ?? "3", 10);
|
|
9113
|
-
const preconditions = runPreconditions();
|
|
9114
|
-
if (!preconditions.canProceed) {
|
|
9115
|
-
if (isJson) {
|
|
9116
|
-
jsonOutput({ status: "fail", message: "Harness not initialized \u2014 run codeharness init first" });
|
|
9117
|
-
} else {
|
|
9118
|
-
fail("Harness not initialized \u2014 run codeharness init first");
|
|
9119
|
-
}
|
|
9120
|
-
process.exitCode = 1;
|
|
9121
|
-
return;
|
|
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;
|
|
9122
8688
|
}
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
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) {
|
|
9140
8707
|
if (isJson) {
|
|
9141
|
-
jsonOutput({
|
|
8708
|
+
jsonOutput({
|
|
8709
|
+
status: "fail",
|
|
8710
|
+
message: "Harness not initialized -- run codeharness init first"
|
|
8711
|
+
});
|
|
9142
8712
|
} else {
|
|
9143
|
-
|
|
9144
|
-
}
|
|
9145
|
-
});
|
|
9146
|
-
onboard.command("audit").description("Audit project documentation").action((_, cmd) => {
|
|
9147
|
-
const opts = cmd.optsWithGlobals();
|
|
9148
|
-
const isJson = opts.json === true;
|
|
9149
|
-
const preconditions = runPreconditions();
|
|
9150
|
-
if (!preconditions.canProceed) {
|
|
9151
|
-
if (isJson) {
|
|
9152
|
-
jsonOutput({ status: "fail", message: "Harness not initialized \u2014 run codeharness init first" });
|
|
9153
|
-
} else {
|
|
9154
|
-
fail("Harness not initialized \u2014 run codeharness init first");
|
|
9155
|
-
}
|
|
9156
|
-
process.exitCode = 1;
|
|
9157
|
-
return;
|
|
8713
|
+
fail("Harness not initialized -- run codeharness init first");
|
|
9158
8714
|
}
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
8715
|
+
process.exitCode = 1;
|
|
8716
|
+
return;
|
|
8717
|
+
}
|
|
8718
|
+
const result = await runAudit(process.cwd());
|
|
8719
|
+
if (!result.success) {
|
|
9163
8720
|
if (isJson) {
|
|
9164
|
-
jsonOutput({
|
|
8721
|
+
jsonOutput({ status: "fail", message: result.error });
|
|
9165
8722
|
} else {
|
|
9166
|
-
|
|
8723
|
+
fail(result.error);
|
|
9167
8724
|
}
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
if (!preconditions.canProceed) {
|
|
9178
|
-
if (isJson) {
|
|
9179
|
-
jsonOutput({ status: "fail", message: "Harness not initialized \u2014 run codeharness init first" });
|
|
9180
|
-
} else {
|
|
9181
|
-
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");
|
|
9182
8734
|
}
|
|
9183
|
-
process.exitCode = 1;
|
|
9184
|
-
return;
|
|
9185
|
-
}
|
|
9186
|
-
for (const w of preconditions.warnings) {
|
|
9187
|
-
warn(w);
|
|
9188
|
-
}
|
|
9189
|
-
let scan;
|
|
9190
|
-
let coverage;
|
|
9191
|
-
let audit;
|
|
9192
|
-
if (lastScanResult) {
|
|
9193
|
-
scan = lastScanResult;
|
|
9194
8735
|
} else {
|
|
9195
|
-
const
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
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
|
+
}
|
|
9202
8745
|
}
|
|
9203
|
-
if (
|
|
9204
|
-
|
|
8746
|
+
if (!isJson) {
|
|
8747
|
+
info(`Generated ${fixResult.data.created} fix stories (${fixResult.data.skipped} skipped)`);
|
|
9205
8748
|
}
|
|
9206
|
-
} else {
|
|
9207
|
-
|
|
9208
|
-
}
|
|
9209
|
-
}
|
|
9210
|
-
coverage = lastCoverageResult ?? runCoverageAnalysis(scan);
|
|
9211
|
-
audit = lastAuditResult ?? runAudit();
|
|
9212
|
-
const epicPath = join28(process.cwd(), "ralph", "onboarding-epic.md");
|
|
9213
|
-
const epic = generateOnboardingEpic(scan, coverage, audit);
|
|
9214
|
-
mergeExtendedGaps(epic);
|
|
9215
|
-
if (!isFull) {
|
|
9216
|
-
applyGapFiltering(epic);
|
|
9217
|
-
}
|
|
9218
|
-
writeOnboardingEpic(epic, epicPath);
|
|
9219
|
-
if (isJson) {
|
|
9220
|
-
jsonOutput({
|
|
9221
|
-
preconditions: { initialized: preconditions.initialized, bmad: preconditions.bmad, hooks: preconditions.hooks },
|
|
9222
|
-
epic,
|
|
9223
|
-
import_status: { stories_created: 0, stories_existing: 0 }
|
|
9224
|
-
});
|
|
9225
|
-
return;
|
|
9226
|
-
}
|
|
9227
|
-
printEpicOutput(epic);
|
|
9228
|
-
let approved;
|
|
9229
|
-
if (autoApprove) {
|
|
9230
|
-
approved = true;
|
|
9231
|
-
} else {
|
|
9232
|
-
approved = await promptApproval();
|
|
9233
|
-
}
|
|
9234
|
-
if (approved) {
|
|
9235
|
-
const results = importOnboardingEpic(epicPath, { listIssues, createIssue });
|
|
9236
|
-
const created = results.filter((r) => r.status === "created").length;
|
|
9237
|
-
ok(`Onboarding: ${created} stories imported into beads`);
|
|
9238
|
-
const sprintResult = appendOnboardingEpicToSprint(
|
|
9239
|
-
epic.stories.map((s) => ({ title: s.title }))
|
|
9240
|
-
);
|
|
9241
|
-
if (sprintResult.epicNumber >= 0) {
|
|
9242
|
-
ok(`Onboarding epic ${sprintResult.epicNumber} added to sprint-status.yaml (${sprintResult.storyKeys.length} stories)`);
|
|
8749
|
+
} else if (!isJson) {
|
|
8750
|
+
fail(fixResult.error);
|
|
9243
8751
|
}
|
|
9244
|
-
info("Ready to run: codeharness run");
|
|
9245
|
-
} else {
|
|
9246
|
-
info("Plan saved to ralph/onboarding-epic.md \u2014 edit and re-run when ready");
|
|
9247
|
-
}
|
|
9248
|
-
});
|
|
9249
|
-
onboard.action(async (opts, cmd) => {
|
|
9250
|
-
const globalOpts = cmd.optsWithGlobals();
|
|
9251
|
-
const isJson = opts.json === true || globalOpts.json === true;
|
|
9252
|
-
const isFull = opts.full === true || globalOpts.full === true;
|
|
9253
|
-
const forceScan = opts.forceScan === true || globalOpts.forceScan === true;
|
|
9254
|
-
const minModuleSize = parseInt(opts.minModuleSize ?? "3", 10);
|
|
9255
|
-
const preconditions = runPreconditions();
|
|
9256
|
-
if (!preconditions.canProceed) {
|
|
9257
|
-
if (isJson) {
|
|
9258
|
-
jsonOutput({ status: "fail", message: "Harness not initialized \u2014 run codeharness init first" });
|
|
9259
|
-
} else {
|
|
9260
|
-
fail("Harness not initialized \u2014 run codeharness init first");
|
|
9261
|
-
}
|
|
9262
|
-
process.exitCode = 1;
|
|
9263
|
-
return;
|
|
9264
|
-
}
|
|
9265
|
-
for (const w of preconditions.warnings) {
|
|
9266
|
-
warn(w);
|
|
9267
|
-
}
|
|
9268
|
-
const progress = getOnboardingProgress({ listIssues });
|
|
9269
|
-
if (progress) {
|
|
9270
|
-
if (progress.remaining === 0 && !isFull && !forceScan) {
|
|
9271
|
-
ok("Onboarding complete \u2014 all gaps resolved");
|
|
9272
|
-
return;
|
|
9273
|
-
}
|
|
9274
|
-
info(`Onboarding progress: ${progress.resolved}/${progress.total} gaps resolved (${progress.remaining} remaining)`);
|
|
9275
|
-
}
|
|
9276
|
-
const scan = runScan(minModuleSize);
|
|
9277
|
-
const coverage = runCoverageAnalysis(scan);
|
|
9278
|
-
const audit = runAudit();
|
|
9279
|
-
saveScanCache({
|
|
9280
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9281
|
-
scan,
|
|
9282
|
-
coverage,
|
|
9283
|
-
audit
|
|
9284
|
-
});
|
|
9285
|
-
const epicPath = join28(process.cwd(), "ralph", "onboarding-epic.md");
|
|
9286
|
-
const epic = generateOnboardingEpic(scan, coverage, audit);
|
|
9287
|
-
mergeExtendedGaps(epic);
|
|
9288
|
-
if (!isFull) {
|
|
9289
|
-
applyGapFiltering(epic);
|
|
9290
8752
|
}
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
if (approved) {
|
|
9307
|
-
const results = importOnboardingEpic(epicPath, { listIssues, createIssue });
|
|
9308
|
-
const created = results.filter((r) => r.status === "created").length;
|
|
9309
|
-
ok(`Onboarding: ${created} stories imported into beads`);
|
|
9310
|
-
const sprintResult = appendOnboardingEpicToSprint(
|
|
9311
|
-
epic.stories.map((s) => ({ title: s.title }))
|
|
9312
|
-
);
|
|
9313
|
-
if (sprintResult.epicNumber >= 0) {
|
|
9314
|
-
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;
|
|
9315
8768
|
}
|
|
9316
|
-
info("Ready to run: codeharness run");
|
|
9317
8769
|
} else {
|
|
9318
|
-
|
|
8770
|
+
jsonData.fixStories = [];
|
|
8771
|
+
jsonData.fixError = fixStories.error;
|
|
9319
8772
|
}
|
|
9320
8773
|
}
|
|
9321
|
-
|
|
9322
|
-
}
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
info(`${trackedCount} previously tracked gaps already in beads`);
|
|
9327
|
-
}
|
|
9328
|
-
epic.stories = untracked;
|
|
9329
|
-
rebuildEpicSummary(epic);
|
|
9330
|
-
}
|
|
9331
|
-
function mergeExtendedGaps(epic) {
|
|
9332
|
-
const verificationGaps = findVerificationGaps();
|
|
9333
|
-
const perFileCoverageGaps = findPerFileCoverageGaps(80);
|
|
9334
|
-
const observabilityGaps = findObservabilityGaps();
|
|
9335
|
-
epic.stories.push(...verificationGaps, ...perFileCoverageGaps, ...observabilityGaps);
|
|
9336
|
-
rebuildEpicSummary(epic);
|
|
9337
|
-
}
|
|
9338
|
-
function rebuildEpicSummary(epic) {
|
|
9339
|
-
const coverageStories = epic.stories.filter((s) => s.type === "coverage").length;
|
|
9340
|
-
const docStories = epic.stories.filter(
|
|
9341
|
-
(s) => s.type === "agents-md" || s.type === "architecture" || s.type === "doc-freshness"
|
|
9342
|
-
).length;
|
|
9343
|
-
const verificationStories = epic.stories.filter((s) => s.type === "verification").length;
|
|
9344
|
-
const observabilityStories = epic.stories.filter((s) => s.type === "observability").length;
|
|
9345
|
-
epic.summary = {
|
|
9346
|
-
totalStories: epic.stories.length,
|
|
9347
|
-
coverageStories,
|
|
9348
|
-
docStories,
|
|
9349
|
-
verificationStories,
|
|
9350
|
-
observabilityStories
|
|
9351
|
-
};
|
|
9352
|
-
}
|
|
9353
|
-
function runScan(minModuleSize) {
|
|
9354
|
-
const result = scanCodebase(process.cwd(), { minModuleSize });
|
|
9355
|
-
lastScanResult = result;
|
|
9356
|
-
return result;
|
|
9357
|
-
}
|
|
9358
|
-
function runCoverageAnalysis(scan) {
|
|
9359
|
-
const result = analyzeCoverageGaps(scan.modules);
|
|
9360
|
-
lastCoverageResult = result;
|
|
9361
|
-
return result;
|
|
9362
|
-
}
|
|
9363
|
-
function runAudit() {
|
|
9364
|
-
const result = auditDocumentation();
|
|
9365
|
-
lastAuditResult = result;
|
|
9366
|
-
return result;
|
|
9367
|
-
}
|
|
9368
|
-
function printScanOutput(result) {
|
|
9369
|
-
info(`Scan: ${result.totalSourceFiles} source files across ${result.modules.length} modules`);
|
|
9370
|
-
for (const mod of result.modules) {
|
|
9371
|
-
info(` ${mod.path}: ${mod.sourceFiles} source, ${mod.testFiles} test`);
|
|
9372
|
-
}
|
|
9373
|
-
}
|
|
9374
|
-
function printCoverageOutput2(result) {
|
|
9375
|
-
info(`Coverage: ${result.overall}% overall (${result.uncoveredFiles} files uncovered)`);
|
|
9376
|
-
for (const mod of result.modules) {
|
|
9377
|
-
if (mod.uncoveredFileCount > 0) {
|
|
9378
|
-
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);
|
|
9379
8779
|
}
|
|
9380
8780
|
}
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
info(`Docs: ${result.summary}`);
|
|
9384
|
-
}
|
|
9385
|
-
function printEpicOutput(epic) {
|
|
9386
|
-
info(formatEpicSummary(epic));
|
|
9387
|
-
for (const story of epic.stories) {
|
|
9388
|
-
info(` ${story.key}: ${story.title}`);
|
|
8781
|
+
if (result.data.overallStatus === "fail") {
|
|
8782
|
+
process.exitCode = 1;
|
|
9389
8783
|
}
|
|
9390
8784
|
}
|
|
9391
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
|
+
|
|
9392
8802
|
// src/commands/teardown.ts
|
|
9393
|
-
import { existsSync as
|
|
9394
|
-
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";
|
|
9395
8805
|
function buildDefaultResult() {
|
|
9396
8806
|
return {
|
|
9397
8807
|
status: "ok",
|
|
@@ -9494,16 +8904,16 @@ function registerTeardownCommand(program) {
|
|
|
9494
8904
|
info("Docker stack: not running, skipping");
|
|
9495
8905
|
}
|
|
9496
8906
|
}
|
|
9497
|
-
const composeFilePath =
|
|
9498
|
-
if (
|
|
8907
|
+
const composeFilePath = join27(projectDir, composeFile);
|
|
8908
|
+
if (existsSync30(composeFilePath)) {
|
|
9499
8909
|
unlinkSync2(composeFilePath);
|
|
9500
8910
|
result.removed.push(composeFile);
|
|
9501
8911
|
if (!isJson) {
|
|
9502
8912
|
ok(`Removed: ${composeFile}`);
|
|
9503
8913
|
}
|
|
9504
8914
|
}
|
|
9505
|
-
const otelConfigPath =
|
|
9506
|
-
if (
|
|
8915
|
+
const otelConfigPath = join27(projectDir, "otel-collector-config.yaml");
|
|
8916
|
+
if (existsSync30(otelConfigPath)) {
|
|
9507
8917
|
unlinkSync2(otelConfigPath);
|
|
9508
8918
|
result.removed.push("otel-collector-config.yaml");
|
|
9509
8919
|
if (!isJson) {
|
|
@@ -9513,8 +8923,8 @@ function registerTeardownCommand(program) {
|
|
|
9513
8923
|
}
|
|
9514
8924
|
let patchesRemoved = 0;
|
|
9515
8925
|
for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
|
|
9516
|
-
const filePath =
|
|
9517
|
-
if (!
|
|
8926
|
+
const filePath = join27(projectDir, "_bmad", relativePath);
|
|
8927
|
+
if (!existsSync30(filePath)) {
|
|
9518
8928
|
continue;
|
|
9519
8929
|
}
|
|
9520
8930
|
try {
|
|
@@ -9534,10 +8944,10 @@ function registerTeardownCommand(program) {
|
|
|
9534
8944
|
}
|
|
9535
8945
|
}
|
|
9536
8946
|
if (state.otlp?.enabled && state.stack === "nodejs") {
|
|
9537
|
-
const pkgPath =
|
|
9538
|
-
if (
|
|
8947
|
+
const pkgPath = join27(projectDir, "package.json");
|
|
8948
|
+
if (existsSync30(pkgPath)) {
|
|
9539
8949
|
try {
|
|
9540
|
-
const raw =
|
|
8950
|
+
const raw = readFileSync26(pkgPath, "utf-8");
|
|
9541
8951
|
const pkg = JSON.parse(raw);
|
|
9542
8952
|
const scripts = pkg["scripts"];
|
|
9543
8953
|
if (scripts) {
|
|
@@ -9551,7 +8961,7 @@ function registerTeardownCommand(program) {
|
|
|
9551
8961
|
for (const key of keysToRemove) {
|
|
9552
8962
|
delete scripts[key];
|
|
9553
8963
|
}
|
|
9554
|
-
|
|
8964
|
+
writeFileSync16(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
9555
8965
|
result.otlp_cleaned = true;
|
|
9556
8966
|
if (!isJson) {
|
|
9557
8967
|
ok("OTLP: removed instrumented scripts from package.json");
|
|
@@ -9577,8 +8987,8 @@ function registerTeardownCommand(program) {
|
|
|
9577
8987
|
}
|
|
9578
8988
|
}
|
|
9579
8989
|
}
|
|
9580
|
-
const harnessDir =
|
|
9581
|
-
if (
|
|
8990
|
+
const harnessDir = join27(projectDir, ".harness");
|
|
8991
|
+
if (existsSync30(harnessDir)) {
|
|
9582
8992
|
rmSync2(harnessDir, { recursive: true, force: true });
|
|
9583
8993
|
result.removed.push(".harness/");
|
|
9584
8994
|
if (!isJson) {
|
|
@@ -9586,7 +8996,7 @@ function registerTeardownCommand(program) {
|
|
|
9586
8996
|
}
|
|
9587
8997
|
}
|
|
9588
8998
|
const statePath2 = getStatePath(projectDir);
|
|
9589
|
-
if (
|
|
8999
|
+
if (existsSync30(statePath2)) {
|
|
9590
9000
|
unlinkSync2(statePath2);
|
|
9591
9001
|
result.removed.push(".claude/codeharness.local.md");
|
|
9592
9002
|
if (!isJson) {
|
|
@@ -10330,8 +9740,8 @@ function registerQueryCommand(program) {
|
|
|
10330
9740
|
}
|
|
10331
9741
|
|
|
10332
9742
|
// src/commands/retro-import.ts
|
|
10333
|
-
import { existsSync as
|
|
10334
|
-
import { join as
|
|
9743
|
+
import { existsSync as existsSync31, readFileSync as readFileSync27 } from "fs";
|
|
9744
|
+
import { join as join28 } from "path";
|
|
10335
9745
|
|
|
10336
9746
|
// src/lib/retro-parser.ts
|
|
10337
9747
|
var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
|
|
@@ -10500,15 +9910,15 @@ function registerRetroImportCommand(program) {
|
|
|
10500
9910
|
return;
|
|
10501
9911
|
}
|
|
10502
9912
|
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
10503
|
-
const retroPath =
|
|
10504
|
-
if (!
|
|
9913
|
+
const retroPath = join28(root, STORY_DIR3, retroFile);
|
|
9914
|
+
if (!existsSync31(retroPath)) {
|
|
10505
9915
|
fail(`Retro file not found: ${retroFile}`, { json: isJson });
|
|
10506
9916
|
process.exitCode = 1;
|
|
10507
9917
|
return;
|
|
10508
9918
|
}
|
|
10509
9919
|
let content;
|
|
10510
9920
|
try {
|
|
10511
|
-
content =
|
|
9921
|
+
content = readFileSync27(retroPath, "utf-8");
|
|
10512
9922
|
} catch (err) {
|
|
10513
9923
|
const message = err instanceof Error ? err.message : String(err);
|
|
10514
9924
|
fail(`Failed to read retro file: ${message}`, { json: isJson });
|
|
@@ -10889,26 +10299,26 @@ function registerVerifyEnvCommand(program) {
|
|
|
10889
10299
|
}
|
|
10890
10300
|
|
|
10891
10301
|
// src/commands/retry.ts
|
|
10892
|
-
import { join as
|
|
10302
|
+
import { join as join30 } from "path";
|
|
10893
10303
|
|
|
10894
10304
|
// src/lib/retry-state.ts
|
|
10895
|
-
import { existsSync as
|
|
10896
|
-
import { join as
|
|
10305
|
+
import { existsSync as existsSync32, readFileSync as readFileSync28, writeFileSync as writeFileSync17 } from "fs";
|
|
10306
|
+
import { join as join29 } from "path";
|
|
10897
10307
|
var RETRIES_FILE = ".story_retries";
|
|
10898
10308
|
var FLAGGED_FILE = ".flagged_stories";
|
|
10899
10309
|
var LINE_PATTERN = /^([^=]+)=(\d+)$/;
|
|
10900
10310
|
function retriesPath(dir) {
|
|
10901
|
-
return
|
|
10311
|
+
return join29(dir, RETRIES_FILE);
|
|
10902
10312
|
}
|
|
10903
10313
|
function flaggedPath(dir) {
|
|
10904
|
-
return
|
|
10314
|
+
return join29(dir, FLAGGED_FILE);
|
|
10905
10315
|
}
|
|
10906
10316
|
function readRetries(dir) {
|
|
10907
10317
|
const filePath = retriesPath(dir);
|
|
10908
|
-
if (!
|
|
10318
|
+
if (!existsSync32(filePath)) {
|
|
10909
10319
|
return /* @__PURE__ */ new Map();
|
|
10910
10320
|
}
|
|
10911
|
-
const raw =
|
|
10321
|
+
const raw = readFileSync28(filePath, "utf-8");
|
|
10912
10322
|
const result = /* @__PURE__ */ new Map();
|
|
10913
10323
|
for (const line of raw.split("\n")) {
|
|
10914
10324
|
const trimmed = line.trim();
|
|
@@ -10930,7 +10340,7 @@ function writeRetries(dir, retries) {
|
|
|
10930
10340
|
for (const [key, count] of retries) {
|
|
10931
10341
|
lines.push(`${key}=${count}`);
|
|
10932
10342
|
}
|
|
10933
|
-
|
|
10343
|
+
writeFileSync17(filePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
|
|
10934
10344
|
}
|
|
10935
10345
|
function resetRetry(dir, storyKey) {
|
|
10936
10346
|
if (storyKey) {
|
|
@@ -10945,15 +10355,15 @@ function resetRetry(dir, storyKey) {
|
|
|
10945
10355
|
}
|
|
10946
10356
|
function readFlaggedStories(dir) {
|
|
10947
10357
|
const filePath = flaggedPath(dir);
|
|
10948
|
-
if (!
|
|
10358
|
+
if (!existsSync32(filePath)) {
|
|
10949
10359
|
return [];
|
|
10950
10360
|
}
|
|
10951
|
-
const raw =
|
|
10361
|
+
const raw = readFileSync28(filePath, "utf-8");
|
|
10952
10362
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l !== "");
|
|
10953
10363
|
}
|
|
10954
10364
|
function writeFlaggedStories(dir, stories) {
|
|
10955
10365
|
const filePath = flaggedPath(dir);
|
|
10956
|
-
|
|
10366
|
+
writeFileSync17(filePath, stories.length > 0 ? stories.join("\n") + "\n" : "", "utf-8");
|
|
10957
10367
|
}
|
|
10958
10368
|
function removeFlaggedStory(dir, key) {
|
|
10959
10369
|
const stories = readFlaggedStories(dir);
|
|
@@ -10973,7 +10383,7 @@ function registerRetryCommand(program) {
|
|
|
10973
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) => {
|
|
10974
10384
|
const opts = cmd.optsWithGlobals();
|
|
10975
10385
|
const isJson = opts.json === true;
|
|
10976
|
-
const dir =
|
|
10386
|
+
const dir = join30(process.cwd(), RALPH_SUBDIR);
|
|
10977
10387
|
if (opts.story && !isValidStoryKey3(opts.story)) {
|
|
10978
10388
|
if (isJson) {
|
|
10979
10389
|
jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
|
|
@@ -11172,634 +10582,217 @@ function registerValidateCommand(program) {
|
|
|
11172
10582
|
process.exitCode = allPassed ? 0 : 1;
|
|
11173
10583
|
});
|
|
11174
10584
|
}
|
|
11175
|
-
function reportError(msg, isJson) {
|
|
11176
|
-
if (isJson) jsonOutput({ status: "fail", message: msg });
|
|
11177
|
-
else fail(msg);
|
|
11178
|
-
process.exitCode = 1;
|
|
11179
|
-
}
|
|
11180
|
-
function getFailures(p) {
|
|
11181
|
-
return p.perAC.filter((a) => a.status === "failed" || a.status === "blocked").map((a) => {
|
|
11182
|
-
const ac = getACById(a.acId);
|
|
11183
|
-
return {
|
|
11184
|
-
acId: a.acId,
|
|
11185
|
-
description: ac?.description ?? "unknown",
|
|
11186
|
-
command: ac?.command,
|
|
11187
|
-
output: a.lastError ?? "",
|
|
11188
|
-
attempts: a.attempts,
|
|
11189
|
-
blocker: a.status === "blocked" ? "blocked" : "failed"
|
|
11190
|
-
};
|
|
11191
|
-
});
|
|
11192
|
-
}
|
|
11193
|
-
function outputJson(p, cycles, allPassed) {
|
|
11194
|
-
jsonOutput({
|
|
11195
|
-
status: allPassed ? "pass" : "fail",
|
|
11196
|
-
total: p.total,
|
|
11197
|
-
passed: p.passed,
|
|
11198
|
-
failed: p.failed,
|
|
11199
|
-
blocked: p.blocked,
|
|
11200
|
-
remaining: p.remaining,
|
|
11201
|
-
cycles,
|
|
11202
|
-
gate: allPassed ? "RELEASE GATE: PASS -- v1.0 ready" : "RELEASE GATE: FAIL",
|
|
11203
|
-
failures: getFailures(p)
|
|
11204
|
-
});
|
|
11205
|
-
}
|
|
11206
|
-
function outputCi(p, allPassed) {
|
|
11207
|
-
if (allPassed) console.log("RELEASE GATE: PASS -- v1.0 ready");
|
|
11208
|
-
else console.log(`RELEASE GATE: FAIL (${p.passed}/${p.total} passed, ${p.failed} failed, ${p.blocked} blocked)`);
|
|
11209
|
-
}
|
|
11210
|
-
function outputHuman(p, cycles, allPassed) {
|
|
11211
|
-
console.log(`Total: ${p.total} | Passed: ${p.passed} | Failed: ${p.failed} | Blocked: ${p.blocked} | Cycles: ${cycles}`);
|
|
11212
|
-
if (allPassed) {
|
|
11213
|
-
ok("RELEASE GATE: PASS -- v1.0 ready");
|
|
11214
|
-
return;
|
|
11215
|
-
}
|
|
11216
|
-
for (const f of getFailures(p)) {
|
|
11217
|
-
console.log(` AC ${f.acId}: ${f.description}`);
|
|
11218
|
-
if (f.command) console.log(` Command: ${f.command}`);
|
|
11219
|
-
if (f.output) console.log(` Output: ${f.output}`);
|
|
11220
|
-
console.log(` Attempts: ${f.attempts}`);
|
|
11221
|
-
console.log(` Blocker: ${f.blocker}`);
|
|
11222
|
-
}
|
|
11223
|
-
fail("RELEASE GATE: FAIL");
|
|
11224
|
-
}
|
|
11225
|
-
|
|
11226
|
-
// src/commands/progress.ts
|
|
11227
|
-
function registerProgressCommand(program) {
|
|
11228
|
-
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) => {
|
|
11229
|
-
const globalOpts = cmd.optsWithGlobals();
|
|
11230
|
-
const isJson = globalOpts.json;
|
|
11231
|
-
const validPhases = ["create", "dev", "review", "verify"];
|
|
11232
|
-
if (opts.phase !== void 0 && !validPhases.includes(opts.phase)) {
|
|
11233
|
-
fail(`Invalid phase "${opts.phase}". Must be one of: ${validPhases.join(", ")}`, { json: isJson });
|
|
11234
|
-
process.exitCode = 1;
|
|
11235
|
-
return;
|
|
11236
|
-
}
|
|
11237
|
-
if (opts.clear) {
|
|
11238
|
-
const result2 = clearRunProgress2();
|
|
11239
|
-
if (result2.success) {
|
|
11240
|
-
if (isJson) {
|
|
11241
|
-
jsonOutput({ status: "ok", cleared: true });
|
|
11242
|
-
} else {
|
|
11243
|
-
ok("Run progress cleared");
|
|
11244
|
-
}
|
|
11245
|
-
} else {
|
|
11246
|
-
fail(result2.error, { json: isJson });
|
|
11247
|
-
process.exitCode = 1;
|
|
11248
|
-
}
|
|
11249
|
-
return;
|
|
11250
|
-
}
|
|
11251
|
-
const update = {
|
|
11252
|
-
...opts.story !== void 0 && { currentStory: opts.story },
|
|
11253
|
-
...opts.phase !== void 0 && { currentPhase: opts.phase },
|
|
11254
|
-
...opts.action !== void 0 && { lastAction: opts.action },
|
|
11255
|
-
...opts.acProgress !== void 0 && { acProgress: opts.acProgress }
|
|
11256
|
-
};
|
|
11257
|
-
if (Object.keys(update).length === 0) {
|
|
11258
|
-
fail("No progress fields specified. Use --story, --phase, --action, --ac-progress, or --clear.", { json: isJson });
|
|
11259
|
-
process.exitCode = 1;
|
|
11260
|
-
return;
|
|
11261
|
-
}
|
|
11262
|
-
const result = updateRunProgress2(update);
|
|
11263
|
-
if (result.success) {
|
|
11264
|
-
if (isJson) {
|
|
11265
|
-
jsonOutput({ status: "ok", updated: update });
|
|
11266
|
-
} else {
|
|
11267
|
-
ok("Run progress updated");
|
|
11268
|
-
}
|
|
11269
|
-
} else {
|
|
11270
|
-
fail(result.error, { json: isJson });
|
|
11271
|
-
process.exitCode = 1;
|
|
11272
|
-
}
|
|
11273
|
-
});
|
|
11274
|
-
}
|
|
11275
|
-
|
|
11276
|
-
// src/commands/observability-gate.ts
|
|
11277
|
-
function registerObservabilityGateCommand(program) {
|
|
11278
|
-
program.command("observability-gate").description("Check observability coverage against targets (commit gate)").option("--json", "Machine-readable JSON output").option("--min-static <percent>", "Override static coverage target").option("--min-runtime <percent>", "Override runtime coverage target").action((opts, cmd) => {
|
|
11279
|
-
const globalOpts = cmd.optsWithGlobals();
|
|
11280
|
-
const isJson = opts.json === true || globalOpts.json === true;
|
|
11281
|
-
const root = process.cwd();
|
|
11282
|
-
const overrides = {};
|
|
11283
|
-
if (opts.minStatic !== void 0) {
|
|
11284
|
-
const parsed = parseInt(opts.minStatic, 10);
|
|
11285
|
-
if (isNaN(parsed) || parsed < 0 || parsed > 100) {
|
|
11286
|
-
if (isJson) {
|
|
11287
|
-
jsonOutput({ status: "error", message: "--min-static must be a number between 0 and 100" });
|
|
11288
|
-
} else {
|
|
11289
|
-
fail("--min-static must be a number between 0 and 100");
|
|
11290
|
-
}
|
|
11291
|
-
process.exitCode = 1;
|
|
11292
|
-
return;
|
|
11293
|
-
}
|
|
11294
|
-
overrides.staticTarget = parsed;
|
|
11295
|
-
}
|
|
11296
|
-
if (opts.minRuntime !== void 0) {
|
|
11297
|
-
const parsed = parseInt(opts.minRuntime, 10);
|
|
11298
|
-
if (isNaN(parsed) || parsed < 0 || parsed > 100) {
|
|
11299
|
-
if (isJson) {
|
|
11300
|
-
jsonOutput({ status: "error", message: "--min-runtime must be a number between 0 and 100" });
|
|
11301
|
-
} else {
|
|
11302
|
-
fail("--min-runtime must be a number between 0 and 100");
|
|
11303
|
-
}
|
|
11304
|
-
process.exitCode = 1;
|
|
11305
|
-
return;
|
|
11306
|
-
}
|
|
11307
|
-
overrides.runtimeTarget = parsed;
|
|
11308
|
-
}
|
|
11309
|
-
const result = checkObservabilityCoverageGate(root, overrides);
|
|
11310
|
-
if (!result.success) {
|
|
11311
|
-
if (isJson) {
|
|
11312
|
-
jsonOutput({ status: "error", message: result.error });
|
|
11313
|
-
} else {
|
|
11314
|
-
fail(`Observability gate error: ${result.error}`);
|
|
11315
|
-
}
|
|
11316
|
-
process.exitCode = 1;
|
|
11317
|
-
return;
|
|
11318
|
-
}
|
|
11319
|
-
const gate = result.data;
|
|
11320
|
-
if (isJson) {
|
|
11321
|
-
jsonOutput({
|
|
11322
|
-
status: gate.passed ? "pass" : "fail",
|
|
11323
|
-
passed: gate.passed,
|
|
11324
|
-
static: {
|
|
11325
|
-
current: gate.staticResult.current,
|
|
11326
|
-
target: gate.staticResult.target,
|
|
11327
|
-
met: gate.staticResult.met,
|
|
11328
|
-
gap: gate.staticResult.gap
|
|
11329
|
-
},
|
|
11330
|
-
runtime: gate.runtimeResult ? {
|
|
11331
|
-
current: gate.runtimeResult.current,
|
|
11332
|
-
target: gate.runtimeResult.target,
|
|
11333
|
-
met: gate.runtimeResult.met,
|
|
11334
|
-
gap: gate.runtimeResult.gap
|
|
11335
|
-
} : null,
|
|
11336
|
-
gaps: gate.gapSummary.map((g) => ({
|
|
11337
|
-
file: g.file,
|
|
11338
|
-
line: g.line,
|
|
11339
|
-
type: g.type,
|
|
11340
|
-
description: g.description
|
|
11341
|
-
}))
|
|
11342
|
-
});
|
|
11343
|
-
} else {
|
|
11344
|
-
const staticLine = `Static: ${gate.staticResult.current}% / ${gate.staticResult.target}% target`;
|
|
11345
|
-
if (gate.passed) {
|
|
11346
|
-
ok(`Observability gate passed. ${staticLine}`);
|
|
11347
|
-
if (gate.runtimeResult) {
|
|
11348
|
-
ok(`Runtime: ${gate.runtimeResult.current}% / ${gate.runtimeResult.target}% target`);
|
|
11349
|
-
}
|
|
11350
|
-
} else {
|
|
11351
|
-
fail(`Observability gate failed. ${staticLine}`);
|
|
11352
|
-
if (gate.runtimeResult && !gate.runtimeResult.met) {
|
|
11353
|
-
fail(`Runtime: ${gate.runtimeResult.current}% / ${gate.runtimeResult.target}% target`);
|
|
11354
|
-
}
|
|
11355
|
-
if (gate.gapSummary.length > 0) {
|
|
11356
|
-
fail("Gaps:");
|
|
11357
|
-
const shown = gate.gapSummary.slice(0, 5);
|
|
11358
|
-
for (const g of shown) {
|
|
11359
|
-
fail(` ${g.file}:${g.line} \u2014 ${g.description}`);
|
|
11360
|
-
}
|
|
11361
|
-
if (gate.gapSummary.length > 5) {
|
|
11362
|
-
fail(` ... and ${gate.gapSummary.length - 5} more.`);
|
|
11363
|
-
}
|
|
11364
|
-
}
|
|
11365
|
-
fail("Add logging to flagged functions. Run: codeharness observability-gate for details.");
|
|
11366
|
-
}
|
|
11367
|
-
}
|
|
11368
|
-
if (!gate.passed) {
|
|
11369
|
-
process.exitCode = 1;
|
|
11370
|
-
}
|
|
11371
|
-
});
|
|
11372
|
-
}
|
|
11373
|
-
|
|
11374
|
-
// src/modules/audit/dimensions.ts
|
|
11375
|
-
import { existsSync as existsSync34, readFileSync as readFileSync30, readdirSync as readdirSync7 } from "fs";
|
|
11376
|
-
import { join as join33 } from "path";
|
|
11377
|
-
function gap(dimension, description, suggestedFix) {
|
|
11378
|
-
return { dimension, description, suggestedFix };
|
|
11379
|
-
}
|
|
11380
|
-
function dimOk(name, status, metric, gaps = []) {
|
|
11381
|
-
return ok2({ name, status, metric, gaps });
|
|
11382
|
-
}
|
|
11383
|
-
function dimCatch(name, err) {
|
|
11384
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
11385
|
-
return dimOk(name, "warn", "error", [gap(name, `${name} check failed: ${msg}`, `Check ${name} configuration`)]);
|
|
11386
|
-
}
|
|
11387
|
-
function worstStatus(...statuses) {
|
|
11388
|
-
if (statuses.includes("fail")) return "fail";
|
|
11389
|
-
if (statuses.includes("warn")) return "warn";
|
|
11390
|
-
return "pass";
|
|
11391
|
-
}
|
|
11392
|
-
async function checkObservability(projectDir) {
|
|
11393
|
-
try {
|
|
11394
|
-
const gaps = [];
|
|
11395
|
-
let sStatus = "pass", sMetric = "";
|
|
11396
|
-
const sr = analyze(projectDir);
|
|
11397
|
-
if (isOk(sr)) {
|
|
11398
|
-
const d = sr.data;
|
|
11399
|
-
if (d.skipped) {
|
|
11400
|
-
sStatus = "warn";
|
|
11401
|
-
sMetric = `static: skipped (${d.skipReason ?? "unknown"})`;
|
|
11402
|
-
gaps.push(gap("observability", `Static analysis skipped: ${d.skipReason ?? "Semgrep not installed"}`, "Install Semgrep: pip install semgrep"));
|
|
11403
|
-
} else {
|
|
11404
|
-
const n = d.gaps.length;
|
|
11405
|
-
sMetric = `static: ${n} gap${n !== 1 ? "s" : ""}`;
|
|
11406
|
-
if (n > 0) {
|
|
11407
|
-
sStatus = "warn";
|
|
11408
|
-
for (const g of d.gaps) gaps.push(gap("observability", `${g.file}:${g.line} \u2014 ${g.message}`, g.fix ?? "Add observability instrumentation"));
|
|
11409
|
-
}
|
|
11410
|
-
}
|
|
11411
|
-
} else {
|
|
11412
|
-
sStatus = "warn";
|
|
11413
|
-
sMetric = "static: skipped (analysis failed)";
|
|
11414
|
-
gaps.push(gap("observability", `Static analysis failed: ${sr.error}`, "Check Semgrep installation and rules configuration"));
|
|
11415
|
-
}
|
|
11416
|
-
let rStatus = "pass", rMetric = "";
|
|
11417
|
-
try {
|
|
11418
|
-
const rr = await validateRuntime(projectDir);
|
|
11419
|
-
if (isOk(rr)) {
|
|
11420
|
-
const d = rr.data;
|
|
11421
|
-
if (d.skipped) {
|
|
11422
|
-
rStatus = "warn";
|
|
11423
|
-
rMetric = `runtime: skipped (${d.skipReason ?? "unknown"})`;
|
|
11424
|
-
gaps.push(gap("observability", `Runtime validation skipped: ${d.skipReason ?? "backend unreachable"}`, "Start the observability stack: codeharness stack up"));
|
|
11425
|
-
} else {
|
|
11426
|
-
rMetric = `runtime: ${d.coveragePercent}%`;
|
|
11427
|
-
if (d.coveragePercent < 50) {
|
|
11428
|
-
rStatus = "warn";
|
|
11429
|
-
gaps.push(gap("observability", `Runtime coverage low: ${d.coveragePercent}%`, "Add telemetry instrumentation to more modules"));
|
|
11430
|
-
}
|
|
11431
|
-
}
|
|
11432
|
-
} else {
|
|
11433
|
-
rStatus = "warn";
|
|
11434
|
-
rMetric = "runtime: skipped (validation failed)";
|
|
11435
|
-
gaps.push(gap("observability", `Runtime validation failed: ${rr.error}`, "Ensure observability backend is running"));
|
|
11436
|
-
}
|
|
11437
|
-
} catch {
|
|
11438
|
-
rStatus = "warn";
|
|
11439
|
-
rMetric = "runtime: skipped (error)";
|
|
11440
|
-
gaps.push(gap("observability", "Runtime validation threw an unexpected error", "Check observability stack health"));
|
|
11441
|
-
}
|
|
11442
|
-
return dimOk("observability", worstStatus(sStatus, rStatus), `${sMetric}, ${rMetric}`, gaps);
|
|
11443
|
-
} catch (err) {
|
|
11444
|
-
return dimCatch("observability", err);
|
|
11445
|
-
}
|
|
11446
|
-
}
|
|
11447
|
-
function checkTesting(projectDir) {
|
|
11448
|
-
try {
|
|
11449
|
-
const r = checkOnlyCoverage(projectDir);
|
|
11450
|
-
if (!r.success) return dimOk("testing", "warn", "no coverage data", [gap("testing", "No coverage tool detected or coverage data unavailable", "Run tests with coverage: npm run test:coverage")]);
|
|
11451
|
-
const pct = r.coveragePercent;
|
|
11452
|
-
const gaps = [];
|
|
11453
|
-
let status = "pass";
|
|
11454
|
-
if (pct < 50) {
|
|
11455
|
-
status = "fail";
|
|
11456
|
-
gaps.push(gap("testing", `Test coverage critically low: ${pct}%`, "Add unit tests to increase coverage above 50%"));
|
|
11457
|
-
} else if (pct < 80) {
|
|
11458
|
-
status = "warn";
|
|
11459
|
-
gaps.push(gap("testing", `Test coverage below target: ${pct}%`, "Add tests to reach 80% coverage target"));
|
|
11460
|
-
}
|
|
11461
|
-
return dimOk("testing", status, `${pct}%`, gaps);
|
|
11462
|
-
} catch (err) {
|
|
11463
|
-
return dimCatch("testing", err);
|
|
11464
|
-
}
|
|
11465
|
-
}
|
|
11466
|
-
function checkDocumentation(projectDir) {
|
|
11467
|
-
try {
|
|
11468
|
-
const report = scanDocHealth(projectDir);
|
|
11469
|
-
const gaps = [];
|
|
11470
|
-
const { fresh, stale, missing } = report.summary;
|
|
11471
|
-
let status = "pass";
|
|
11472
|
-
if (missing > 0) {
|
|
11473
|
-
status = "fail";
|
|
11474
|
-
for (const doc of report.documents) if (doc.grade === "missing") gaps.push(gap("documentation", `Missing: ${doc.path} \u2014 ${doc.reason}`, `Create ${doc.path}`));
|
|
11475
|
-
}
|
|
11476
|
-
if (stale > 0) {
|
|
11477
|
-
if (status !== "fail") status = "warn";
|
|
11478
|
-
for (const doc of report.documents) if (doc.grade === "stale") gaps.push(gap("documentation", `Stale: ${doc.path} \u2014 ${doc.reason}`, `Update ${doc.path} to reflect current code`));
|
|
11479
|
-
}
|
|
11480
|
-
return dimOk("documentation", status, `${fresh} fresh, ${stale} stale, ${missing} missing`, gaps);
|
|
11481
|
-
} catch (err) {
|
|
11482
|
-
return dimCatch("documentation", err);
|
|
11483
|
-
}
|
|
11484
|
-
}
|
|
11485
|
-
function checkVerification(projectDir) {
|
|
11486
|
-
try {
|
|
11487
|
-
const gaps = [];
|
|
11488
|
-
const sprintPath = join33(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
|
|
11489
|
-
if (!existsSync34(sprintPath)) return dimOk("verification", "warn", "no sprint data", [gap("verification", "No sprint-status.yaml found", "Run sprint planning to create sprint status")]);
|
|
11490
|
-
const vDir = join33(projectDir, "verification");
|
|
11491
|
-
let proofCount = 0, totalChecked = 0;
|
|
11492
|
-
if (existsSync34(vDir)) {
|
|
11493
|
-
for (const file of readdirSafe(vDir)) {
|
|
11494
|
-
if (!file.endsWith("-proof.md")) continue;
|
|
11495
|
-
totalChecked++;
|
|
11496
|
-
const r = parseProof(join33(vDir, file));
|
|
11497
|
-
if (isOk(r) && r.data.passed) {
|
|
11498
|
-
proofCount++;
|
|
11499
|
-
} else {
|
|
11500
|
-
gaps.push(gap("verification", `Story ${file.replace("-proof.md", "")} proof incomplete or failing`, `Run codeharness verify ${file.replace("-proof.md", "")}`));
|
|
11501
|
-
}
|
|
11502
|
-
}
|
|
11503
|
-
}
|
|
11504
|
-
let status = "pass";
|
|
11505
|
-
if (totalChecked === 0) {
|
|
11506
|
-
status = "warn";
|
|
11507
|
-
gaps.push(gap("verification", "No verification proofs found", "Run codeharness verify for completed stories"));
|
|
11508
|
-
} else if (proofCount < totalChecked) {
|
|
11509
|
-
status = "warn";
|
|
11510
|
-
}
|
|
11511
|
-
return dimOk("verification", status, totalChecked > 0 ? `${proofCount}/${totalChecked} verified` : "no proofs", gaps);
|
|
11512
|
-
} catch (err) {
|
|
11513
|
-
return dimCatch("verification", err);
|
|
11514
|
-
}
|
|
11515
|
-
}
|
|
11516
|
-
function checkInfrastructure(projectDir) {
|
|
11517
|
-
try {
|
|
11518
|
-
const dfPath = join33(projectDir, "Dockerfile");
|
|
11519
|
-
if (!existsSync34(dfPath)) return dimOk("infrastructure", "fail", "no Dockerfile", [gap("infrastructure", "No Dockerfile found", "Create a Dockerfile for containerized deployment")]);
|
|
11520
|
-
let content;
|
|
11521
|
-
try {
|
|
11522
|
-
content = readFileSync30(dfPath, "utf-8");
|
|
11523
|
-
} catch {
|
|
11524
|
-
return dimOk("infrastructure", "warn", "Dockerfile unreadable", [gap("infrastructure", "Dockerfile exists but could not be read", "Check Dockerfile permissions")]);
|
|
11525
|
-
}
|
|
11526
|
-
const fromLines = content.split("\n").filter((l) => /^\s*FROM\s+/i.test(l));
|
|
11527
|
-
if (fromLines.length === 0) return dimOk("infrastructure", "fail", "invalid Dockerfile", [gap("infrastructure", "Dockerfile has no FROM instruction", "Add a FROM instruction with a pinned base image")]);
|
|
11528
|
-
const gaps = [];
|
|
11529
|
-
let hasUnpinned = false;
|
|
11530
|
-
for (const line of fromLines) {
|
|
11531
|
-
const ref = line.replace(/^\s*FROM\s+/i, "").split(/\s+/)[0];
|
|
11532
|
-
if (ref.endsWith(":latest")) {
|
|
11533
|
-
hasUnpinned = true;
|
|
11534
|
-
gaps.push(gap("infrastructure", `Unpinned base image: ${ref}`, `Pin ${ref} to a specific version tag`));
|
|
11535
|
-
} else if (!ref.includes(":") && !ref.includes("@")) {
|
|
11536
|
-
hasUnpinned = true;
|
|
11537
|
-
gaps.push(gap("infrastructure", `Unpinned base image (no tag): ${ref}`, `Pin ${ref} to a specific version tag (e.g., ${ref}:22-slim)`));
|
|
11538
|
-
}
|
|
11539
|
-
}
|
|
11540
|
-
const status = hasUnpinned ? "warn" : "pass";
|
|
11541
|
-
const metric = hasUnpinned ? `Dockerfile exists (${gaps.length} issue${gaps.length !== 1 ? "s" : ""})` : "Dockerfile valid";
|
|
11542
|
-
return dimOk("infrastructure", status, metric, gaps);
|
|
11543
|
-
} catch (err) {
|
|
11544
|
-
return dimCatch("infrastructure", err);
|
|
11545
|
-
}
|
|
11546
|
-
}
|
|
11547
|
-
function readdirSafe(dir) {
|
|
11548
|
-
try {
|
|
11549
|
-
return readdirSync7(dir);
|
|
11550
|
-
} catch {
|
|
11551
|
-
return [];
|
|
11552
|
-
}
|
|
11553
|
-
}
|
|
11554
|
-
|
|
11555
|
-
// src/modules/audit/report.ts
|
|
11556
|
-
var STATUS_PREFIX = {
|
|
11557
|
-
pass: "[OK]",
|
|
11558
|
-
fail: "[FAIL]",
|
|
11559
|
-
warn: "[WARN]"
|
|
11560
|
-
};
|
|
11561
|
-
function formatAuditHuman(result) {
|
|
11562
|
-
const lines = [];
|
|
11563
|
-
for (const dimension of Object.values(result.dimensions)) {
|
|
11564
|
-
const prefix = STATUS_PREFIX[dimension.status] ?? "[WARN]";
|
|
11565
|
-
lines.push(`${prefix} ${dimension.name}: ${dimension.metric}`);
|
|
11566
|
-
for (const gap2 of dimension.gaps) {
|
|
11567
|
-
lines.push(` [WARN] ${gap2.description} -- fix: ${gap2.suggestedFix}`);
|
|
11568
|
-
}
|
|
11569
|
-
}
|
|
11570
|
-
const overallPrefix = STATUS_PREFIX[result.overallStatus] ?? "[WARN]";
|
|
11571
|
-
lines.push("");
|
|
11572
|
-
lines.push(
|
|
11573
|
-
`${overallPrefix} Audit complete: ${result.gapCount} gap${result.gapCount !== 1 ? "s" : ""} found (${result.durationMs}ms)`
|
|
11574
|
-
);
|
|
11575
|
-
return lines;
|
|
11576
|
-
}
|
|
11577
|
-
function formatAuditJson(result) {
|
|
11578
|
-
return result;
|
|
11579
|
-
}
|
|
11580
|
-
|
|
11581
|
-
// src/modules/audit/fix-generator.ts
|
|
11582
|
-
import { existsSync as existsSync35, writeFileSync as writeFileSync19, mkdirSync as mkdirSync11 } from "fs";
|
|
11583
|
-
import { join as join34, dirname as dirname8 } from "path";
|
|
11584
|
-
function buildStoryKey(gap2, index) {
|
|
11585
|
-
const safeDimension = gap2.dimension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
11586
|
-
return `audit-fix-${safeDimension}-${index}`;
|
|
11587
|
-
}
|
|
11588
|
-
function buildStoryMarkdown(gap2, key) {
|
|
11589
|
-
return [
|
|
11590
|
-
`# Fix: ${gap2.dimension} \u2014 ${gap2.description}`,
|
|
11591
|
-
"",
|
|
11592
|
-
"Status: backlog",
|
|
11593
|
-
"",
|
|
11594
|
-
"## Story",
|
|
11595
|
-
"",
|
|
11596
|
-
`As an operator, I need ${gap2.description} fixed so that audit compliance improves.`,
|
|
11597
|
-
"",
|
|
11598
|
-
"## Acceptance Criteria",
|
|
11599
|
-
"",
|
|
11600
|
-
`1. **Given** ${gap2.description}, **When** the fix is applied, **Then** ${gap2.suggestedFix}.`,
|
|
11601
|
-
"",
|
|
11602
|
-
"## Dev Notes",
|
|
11603
|
-
"",
|
|
11604
|
-
"This is an auto-generated fix story created by `codeharness audit --fix`.",
|
|
11605
|
-
`**Audit Gap:** ${gap2.dimension}: ${gap2.description}`,
|
|
11606
|
-
`**Suggested Fix:** ${gap2.suggestedFix}`,
|
|
11607
|
-
""
|
|
11608
|
-
].join("\n");
|
|
11609
|
-
}
|
|
11610
|
-
function generateFixStories(auditResult) {
|
|
11611
|
-
try {
|
|
11612
|
-
const stories = [];
|
|
11613
|
-
let created = 0;
|
|
11614
|
-
let skipped = 0;
|
|
11615
|
-
const artifactsDir = join34(
|
|
11616
|
-
process.cwd(),
|
|
11617
|
-
"_bmad-output",
|
|
11618
|
-
"implementation-artifacts"
|
|
11619
|
-
);
|
|
11620
|
-
for (const dimension of Object.values(auditResult.dimensions)) {
|
|
11621
|
-
for (let i = 0; i < dimension.gaps.length; i++) {
|
|
11622
|
-
const gap2 = dimension.gaps[i];
|
|
11623
|
-
const key = buildStoryKey(gap2, i + 1);
|
|
11624
|
-
const filePath = join34(artifactsDir, `${key}.md`);
|
|
11625
|
-
if (existsSync35(filePath)) {
|
|
11626
|
-
stories.push({
|
|
11627
|
-
key,
|
|
11628
|
-
filePath,
|
|
11629
|
-
gap: gap2,
|
|
11630
|
-
skipped: true,
|
|
11631
|
-
skipReason: "Story file already exists"
|
|
11632
|
-
});
|
|
11633
|
-
skipped++;
|
|
11634
|
-
continue;
|
|
11635
|
-
}
|
|
11636
|
-
const markdown = buildStoryMarkdown(gap2, key);
|
|
11637
|
-
mkdirSync11(dirname8(filePath), { recursive: true });
|
|
11638
|
-
writeFileSync19(filePath, markdown, "utf-8");
|
|
11639
|
-
stories.push({ key, filePath, gap: gap2, skipped: false });
|
|
11640
|
-
created++;
|
|
11641
|
-
}
|
|
11642
|
-
}
|
|
11643
|
-
return ok2({ stories, created, skipped });
|
|
11644
|
-
} catch (err) {
|
|
11645
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
11646
|
-
return fail2(`Failed to generate fix stories: ${msg}`);
|
|
11647
|
-
}
|
|
10585
|
+
function reportError(msg, isJson) {
|
|
10586
|
+
if (isJson) jsonOutput({ status: "fail", message: msg });
|
|
10587
|
+
else fail(msg);
|
|
10588
|
+
process.exitCode = 1;
|
|
11648
10589
|
}
|
|
11649
|
-
function
|
|
11650
|
-
|
|
11651
|
-
|
|
11652
|
-
return
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
const updatedStories = { ...current.stories };
|
|
11660
|
-
for (const story of newStories) {
|
|
11661
|
-
updatedStories[story.key] = {
|
|
11662
|
-
status: "backlog",
|
|
11663
|
-
attempts: 0,
|
|
11664
|
-
lastAttempt: null,
|
|
11665
|
-
lastError: null,
|
|
11666
|
-
proofPath: null,
|
|
11667
|
-
acResults: null
|
|
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"
|
|
11668
10600
|
};
|
|
11669
|
-
}
|
|
11670
|
-
const updatedSprint = computeSprintCounts2(updatedStories);
|
|
11671
|
-
return writeStateAtomic2({
|
|
11672
|
-
...current,
|
|
11673
|
-
sprint: updatedSprint,
|
|
11674
|
-
stories: updatedStories
|
|
11675
10601
|
});
|
|
11676
10602
|
}
|
|
11677
|
-
|
|
11678
|
-
|
|
11679
|
-
|
|
11680
|
-
|
|
11681
|
-
|
|
11682
|
-
|
|
11683
|
-
|
|
11684
|
-
|
|
11685
|
-
|
|
11686
|
-
|
|
11687
|
-
|
|
11688
|
-
|
|
11689
|
-
Promise.resolve(checkTesting(projectDir)),
|
|
11690
|
-
Promise.resolve(checkDocumentation(projectDir)),
|
|
11691
|
-
Promise.resolve(checkVerification(projectDir)),
|
|
11692
|
-
Promise.resolve(checkInfrastructure(projectDir))
|
|
11693
|
-
]);
|
|
11694
|
-
const dimensions = {};
|
|
11695
|
-
const allResults = [obsResult, testResult, docResult, verifyResult, infraResult];
|
|
11696
|
-
for (const result of allResults) {
|
|
11697
|
-
if (result.success) {
|
|
11698
|
-
dimensions[result.data.name] = result.data;
|
|
11699
|
-
}
|
|
11700
|
-
}
|
|
11701
|
-
const statuses = Object.values(dimensions).map((d) => d.status);
|
|
11702
|
-
const overallStatus = computeOverallStatus(statuses);
|
|
11703
|
-
const gapCount = Object.values(dimensions).reduce((sum, d) => sum + d.gaps.length, 0);
|
|
11704
|
-
const durationMs = Math.round(performance.now() - start);
|
|
11705
|
-
return ok2({ dimensions, overallStatus, gapCount, durationMs });
|
|
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
|
+
});
|
|
11706
10615
|
}
|
|
11707
|
-
function
|
|
11708
|
-
if (
|
|
11709
|
-
|
|
11710
|
-
|
|
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");
|
|
11711
10634
|
}
|
|
11712
10635
|
|
|
11713
|
-
// src/commands/
|
|
11714
|
-
function
|
|
11715
|
-
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) => {
|
|
11716
10639
|
const globalOpts = cmd.optsWithGlobals();
|
|
11717
|
-
const isJson =
|
|
11718
|
-
const
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
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
|
+
}
|
|
11726
10655
|
} else {
|
|
11727
|
-
fail(
|
|
10656
|
+
fail(result2.error, { json: isJson });
|
|
10657
|
+
process.exitCode = 1;
|
|
11728
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 });
|
|
11729
10669
|
process.exitCode = 1;
|
|
11730
10670
|
return;
|
|
11731
10671
|
}
|
|
11732
|
-
const result =
|
|
11733
|
-
if (
|
|
10672
|
+
const result = updateRunProgress2(update);
|
|
10673
|
+
if (result.success) {
|
|
11734
10674
|
if (isJson) {
|
|
11735
|
-
jsonOutput({ status: "
|
|
10675
|
+
jsonOutput({ status: "ok", updated: update });
|
|
11736
10676
|
} else {
|
|
11737
|
-
|
|
10677
|
+
ok("Run progress updated");
|
|
11738
10678
|
}
|
|
10679
|
+
} else {
|
|
10680
|
+
fail(result.error, { json: isJson });
|
|
11739
10681
|
process.exitCode = 1;
|
|
11740
|
-
return;
|
|
11741
10682
|
}
|
|
11742
|
-
|
|
11743
|
-
|
|
11744
|
-
|
|
11745
|
-
|
|
11746
|
-
|
|
11747
|
-
|
|
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");
|
|
11748
10700
|
}
|
|
11749
|
-
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11753
|
-
|
|
11754
|
-
|
|
11755
|
-
|
|
11756
|
-
|
|
11757
|
-
|
|
11758
|
-
|
|
11759
|
-
|
|
11760
|
-
|
|
11761
|
-
info(`Generated ${fixResult.data.created} fix stories (${fixResult.data.skipped} skipped)`);
|
|
11762
|
-
}
|
|
11763
|
-
} else if (!isJson) {
|
|
11764
|
-
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");
|
|
11765
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}`);
|
|
11766
10725
|
}
|
|
10726
|
+
process.exitCode = 1;
|
|
10727
|
+
return;
|
|
11767
10728
|
}
|
|
10729
|
+
const gate = result.data;
|
|
11768
10730
|
if (isJson) {
|
|
11769
|
-
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
11773
|
-
|
|
11774
|
-
|
|
11775
|
-
|
|
11776
|
-
|
|
11777
|
-
|
|
11778
|
-
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
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.`);
|
|
11782
10773
|
}
|
|
11783
|
-
} else if (fixStories && !fixStories.success) {
|
|
11784
|
-
jsonData.fixStories = [];
|
|
11785
|
-
jsonData.fixError = fixStories.error;
|
|
11786
10774
|
}
|
|
11787
|
-
|
|
11788
|
-
jsonOutput(jsonData);
|
|
11789
|
-
} else if (!isFix || result.data.gapCount > 0) {
|
|
11790
|
-
const lines = formatAuditHuman(result.data);
|
|
11791
|
-
for (const line of lines) {
|
|
11792
|
-
console.log(line);
|
|
10775
|
+
fail("Add logging to flagged functions. Run: codeharness observability-gate for details.");
|
|
11793
10776
|
}
|
|
11794
10777
|
}
|
|
11795
|
-
if (
|
|
10778
|
+
if (!gate.passed) {
|
|
11796
10779
|
process.exitCode = 1;
|
|
11797
10780
|
}
|
|
11798
10781
|
});
|
|
11799
10782
|
}
|
|
11800
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
|
+
|
|
11801
10794
|
// src/index.ts
|
|
11802
|
-
var VERSION = true ? "0.
|
|
10795
|
+
var VERSION = true ? "0.23.0" : "0.0.0-dev";
|
|
11803
10796
|
function createProgram() {
|
|
11804
10797
|
const program = new Command();
|
|
11805
10798
|
program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
|