codeharness 0.22.2 → 0.23.0

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