codeharness 0.22.1 → 0.23.0

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