codeharness 0.22.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1941,7 +1941,7 @@ async function scaffoldDocs(opts) {
1941
1941
  }
1942
1942
 
1943
1943
  // src/modules/infra/init-project.ts
1944
- var HARNESS_VERSION = true ? "0.22.0" : "0.0.0-dev";
1944
+ var HARNESS_VERSION = true ? "0.22.1" : "0.0.0-dev";
1945
1945
  function failResult(opts, error) {
1946
1946
  return {
1947
1947
  status: "fail",
@@ -2808,6 +2808,7 @@ function Header({ info: info2 }) {
2808
2808
  info2.storyKey,
2809
2809
  " \u2014 ",
2810
2810
  info2.phase,
2811
+ info2.elapsed ? ` | ${info2.elapsed}` : "",
2811
2812
  " | Sprint: ",
2812
2813
  info2.done,
2813
2814
  "/",
@@ -2858,6 +2859,51 @@ function truncateToWidth(text, maxWidth) {
2858
2859
  }
2859
2860
  return result;
2860
2861
  }
2862
+ var STATUS_SYMBOLS = {
2863
+ "done": "\u2713",
2864
+ "in-progress": "\u25C6",
2865
+ "pending": "\u25CB",
2866
+ "failed": "\u2717",
2867
+ "blocked": "\u2715"
2868
+ };
2869
+ function StoryBreakdown({ stories }) {
2870
+ if (stories.length === 0) return null;
2871
+ const groups = {};
2872
+ for (const s of stories) {
2873
+ if (!groups[s.status]) groups[s.status] = [];
2874
+ groups[s.status].push(s.key);
2875
+ }
2876
+ const fmt = (keys, status) => keys.map((k) => `${k} ${STATUS_SYMBOLS[status]}`).join(" ");
2877
+ const parts = [];
2878
+ if (groups["done"]?.length) {
2879
+ parts.push(`Done: ${fmt(groups["done"], "done")}`);
2880
+ }
2881
+ if (groups["in-progress"]?.length) {
2882
+ parts.push(`This: ${fmt(groups["in-progress"], "in-progress")}`);
2883
+ }
2884
+ if (groups["pending"]?.length) {
2885
+ parts.push(`Next: ${fmt(groups["pending"], "pending")}`);
2886
+ }
2887
+ if (groups["failed"]?.length) {
2888
+ parts.push(`Failed: ${fmt(groups["failed"], "failed")}`);
2889
+ }
2890
+ if (groups["blocked"]?.length) {
2891
+ parts.push(`Blocked: ${fmt(groups["blocked"], "blocked")}`);
2892
+ }
2893
+ return /* @__PURE__ */ jsx(Text, { children: parts.join(" | ") });
2894
+ }
2895
+ var MESSAGE_PREFIX = {
2896
+ ok: "[OK]",
2897
+ warn: "[WARN]",
2898
+ fail: "[FAIL]"
2899
+ };
2900
+ function StoryMessages({ messages }) {
2901
+ if (messages.length === 0) return null;
2902
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: messages.map((msg, i) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2903
+ /* @__PURE__ */ jsx(Text, { children: `${MESSAGE_PREFIX[msg.type]} Story ${msg.key}: ${msg.message}` }),
2904
+ msg.details?.map((d, j) => /* @__PURE__ */ jsx(Text, { children: ` \u2514 ${d}` }, j))
2905
+ ] }, i)) });
2906
+ }
2861
2907
  function RetryNotice({ info: info2 }) {
2862
2908
  return /* @__PURE__ */ jsxs(Text, { children: [
2863
2909
  "\u23F3 API retry ",
@@ -2872,6 +2918,8 @@ function App({
2872
2918
  }) {
2873
2919
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2874
2920
  /* @__PURE__ */ jsx(Header, { info: state.sprintInfo }),
2921
+ /* @__PURE__ */ jsx(StoryBreakdown, { stories: state.stories }),
2922
+ /* @__PURE__ */ jsx(StoryMessages, { messages: state.messages }),
2875
2923
  /* @__PURE__ */ jsx(CompletedTools, { tools: state.completedTools }),
2876
2924
  state.activeTool && /* @__PURE__ */ jsx(ActiveTool, { name: state.activeTool.name }),
2877
2925
  state.lastThought && /* @__PURE__ */ jsx(LastThought, { text: state.lastThought }),
@@ -2886,6 +2934,10 @@ var noopHandle = {
2886
2934
  },
2887
2935
  updateSprintState() {
2888
2936
  },
2937
+ updateStories() {
2938
+ },
2939
+ addMessage() {
2940
+ },
2889
2941
  cleanup() {
2890
2942
  }
2891
2943
  };
@@ -2896,6 +2948,8 @@ function startRenderer(options) {
2896
2948
  }
2897
2949
  let state = {
2898
2950
  sprintInfo: options?.sprintState ?? null,
2951
+ stories: [],
2952
+ messages: [],
2899
2953
  completedTools: [],
2900
2954
  activeTool: null,
2901
2955
  activeToolArgs: "",
@@ -2978,7 +3032,17 @@ function startRenderer(options) {
2978
3032
  state.sprintInfo = sprintState ?? null;
2979
3033
  rerender();
2980
3034
  }
2981
- return { update, updateSprintState, cleanup };
3035
+ function updateStories(stories) {
3036
+ if (cleaned) return;
3037
+ state.stories = [...stories];
3038
+ rerender();
3039
+ }
3040
+ function addMessage(msg) {
3041
+ if (cleaned) return;
3042
+ state.messages = [...state.messages, msg];
3043
+ rerender();
3044
+ }
3045
+ return { update, updateSprintState, updateStories, addMessage, cleanup };
2982
3046
  }
2983
3047
 
2984
3048
  // src/modules/sprint/state.ts
@@ -3864,21 +3928,8 @@ function clearRunProgress2() {
3864
3928
  return clearRunProgress();
3865
3929
  }
3866
3930
 
3867
- // src/commands/run.ts
3868
- var SPRINT_STATUS_REL = "_bmad-output/implementation-artifacts/sprint-status.yaml";
3931
+ // src/lib/run-helpers.ts
3869
3932
  var STORY_KEY_PATTERN = /^\d+-\d+-/;
3870
- function resolveRalphPath() {
3871
- const currentFile = fileURLToPath2(import.meta.url);
3872
- const currentDir = dirname4(currentFile);
3873
- let root = dirname4(currentDir);
3874
- if (root.endsWith("/src") || root.endsWith("\\src")) {
3875
- root = dirname4(root);
3876
- }
3877
- return join13(root, "ralph", "ralph.sh");
3878
- }
3879
- function resolvePluginDir() {
3880
- return join13(process.cwd(), ".claude");
3881
- }
3882
3933
  function countStories(statuses) {
3883
3934
  let total = 0;
3884
3935
  let ready = 0;
@@ -3919,6 +3970,108 @@ function buildSpawnArgs(opts) {
3919
3970
  }
3920
3971
  return args;
3921
3972
  }
3973
+ function formatElapsed(ms) {
3974
+ const totalMinutes = Math.max(0, Math.floor(ms / 6e4));
3975
+ const hours = Math.floor(totalMinutes / 60);
3976
+ const minutes = totalMinutes % 60;
3977
+ if (hours > 0) {
3978
+ return `${hours}h${minutes}m`;
3979
+ }
3980
+ return `${totalMinutes}m`;
3981
+ }
3982
+ function mapSprintStatus(status) {
3983
+ switch (status) {
3984
+ case "done":
3985
+ return "done";
3986
+ case "in-progress":
3987
+ case "review":
3988
+ case "verifying":
3989
+ return "in-progress";
3990
+ case "backlog":
3991
+ case "ready-for-dev":
3992
+ return "pending";
3993
+ case "failed":
3994
+ return "failed";
3995
+ case "blocked":
3996
+ case "exhausted":
3997
+ return "blocked";
3998
+ default:
3999
+ return "pending";
4000
+ }
4001
+ }
4002
+ function mapSprintStatuses(statuses) {
4003
+ const entries = [];
4004
+ for (const [key, status] of Object.entries(statuses)) {
4005
+ if (!STORY_KEY_PATTERN.test(key)) continue;
4006
+ if (status === "optional") continue;
4007
+ entries.push({ key, status: mapSprintStatus(status) });
4008
+ }
4009
+ return entries;
4010
+ }
4011
+ var ANSI_ESCAPE = /\x1b\[[0-9;]*m/g;
4012
+ var TIMESTAMP_PREFIX = /^\[[\d-]+\s[\d:]+\]\s*/;
4013
+ var SUCCESS_STORY = /\[SUCCESS\]\s+Story\s+([\w-]+):\s+DONE(.*)/;
4014
+ var WARN_STORY_RETRY = /\[WARN\]\s+Story\s+([\w-]+)\s+exceeded retry limit/;
4015
+ var WARN_STORY_RETRYING = /\[WARN\]\s+Story\s+([\w-]+)\s+.*retry\s+(\d+)\/(\d+)/;
4016
+ var ERROR_LINE = /\[ERROR\]\s+(.+)/;
4017
+ function parseRalphMessage(rawLine) {
4018
+ const clean = rawLine.replace(ANSI_ESCAPE, "").replace(TIMESTAMP_PREFIX, "").trim();
4019
+ if (clean.length === 0) return null;
4020
+ const success = SUCCESS_STORY.exec(clean);
4021
+ if (success) {
4022
+ const key = success[1];
4023
+ const rest = success[2].trim().replace(/^—\s*/, "");
4024
+ return {
4025
+ type: "ok",
4026
+ key,
4027
+ message: rest ? `DONE \u2014 ${rest}` : "DONE"
4028
+ };
4029
+ }
4030
+ const retryExceeded = WARN_STORY_RETRY.exec(clean);
4031
+ if (retryExceeded) {
4032
+ return {
4033
+ type: "fail",
4034
+ key: retryExceeded[1],
4035
+ message: "exceeded retry limit"
4036
+ };
4037
+ }
4038
+ const retrying = WARN_STORY_RETRYING.exec(clean);
4039
+ if (retrying) {
4040
+ return {
4041
+ type: "warn",
4042
+ key: retrying[1],
4043
+ message: `retry ${retrying[2]}/${retrying[3]}`
4044
+ };
4045
+ }
4046
+ const errorMatch = ERROR_LINE.exec(clean);
4047
+ if (errorMatch) {
4048
+ const keyMatch = errorMatch[1].match(/Story\s+([\w-]+)/);
4049
+ if (keyMatch) {
4050
+ return {
4051
+ type: "fail",
4052
+ key: keyMatch[1],
4053
+ message: errorMatch[1].trim()
4054
+ };
4055
+ }
4056
+ return null;
4057
+ }
4058
+ return null;
4059
+ }
4060
+
4061
+ // src/commands/run.ts
4062
+ var SPRINT_STATUS_REL = "_bmad-output/implementation-artifacts/sprint-status.yaml";
4063
+ function resolveRalphPath() {
4064
+ const currentFile = fileURLToPath2(import.meta.url);
4065
+ const currentDir = dirname4(currentFile);
4066
+ let root = dirname4(currentDir);
4067
+ if (root.endsWith("/src") || root.endsWith("\\src")) {
4068
+ root = dirname4(root);
4069
+ }
4070
+ return join13(root, "ralph", "ralph.sh");
4071
+ }
4072
+ function resolvePluginDir() {
4073
+ return join13(process.cwd(), ".claude");
4074
+ }
3922
4075
  function registerRunCommand(program) {
3923
4076
  program.command("run").description("Execute the autonomous coding loop").option("--max-iterations <n>", "Maximum loop iterations", "50").option("--timeout <seconds>", "Total loop timeout in seconds", "43200").option("--iteration-timeout <minutes>", "Per-iteration timeout in minutes", "30").option("--quiet", "Suppress terminal output (background mode)", false).option("--calls <n>", "Max API calls per hour", "100").option("--max-story-retries <n>", "Max retries per story before flagging", "10").option("--reset", "Clear retry counters, flagged stories, and circuit breaker before starting", false).action(async (options, cmd) => {
3924
4077
  const globalOpts = cmd.optsWithGlobals();
@@ -3999,6 +4152,7 @@ function registerRunCommand(program) {
3999
4152
  const quiet = options.quiet;
4000
4153
  const rendererHandle = startRenderer({ quiet });
4001
4154
  let sprintStateInterval = null;
4155
+ const sessionStartTime = Date.now();
4002
4156
  try {
4003
4157
  const initialState = getSprintState2();
4004
4158
  if (initialState.success) {
@@ -4007,17 +4161,23 @@ function registerRunCommand(program) {
4007
4161
  storyKey: s.run.currentStory ?? "",
4008
4162
  phase: s.run.currentPhase ?? "",
4009
4163
  done: s.sprint.done,
4010
- total: s.sprint.total
4164
+ total: s.sprint.total,
4165
+ elapsed: formatElapsed(Date.now() - sessionStartTime)
4011
4166
  };
4012
4167
  rendererHandle.updateSprintState(sprintInfo);
4013
4168
  }
4169
+ const initialStatuses = readSprintStatus(projectDir);
4170
+ const initialStories = mapSprintStatuses(initialStatuses);
4171
+ if (initialStories.length > 0) {
4172
+ rendererHandle.updateStories(initialStories);
4173
+ }
4014
4174
  const child = spawn("bash", args, {
4015
4175
  stdio: quiet ? "ignore" : ["inherit", "pipe", "pipe"],
4016
4176
  cwd: projectDir,
4017
4177
  env
4018
4178
  });
4019
4179
  if (!quiet && child.stdout && child.stderr) {
4020
- const makeLineHandler = () => {
4180
+ const makeLineHandler = (opts) => {
4021
4181
  let partial = "";
4022
4182
  const decoder = new StringDecoder("utf8");
4023
4183
  return (data) => {
@@ -4030,11 +4190,17 @@ function registerRunCommand(program) {
4030
4190
  if (event) {
4031
4191
  rendererHandle.update(event);
4032
4192
  }
4193
+ if (opts?.parseRalph) {
4194
+ const msg = parseRalphMessage(line);
4195
+ if (msg) {
4196
+ rendererHandle.addMessage(msg);
4197
+ }
4198
+ }
4033
4199
  }
4034
4200
  };
4035
4201
  };
4036
4202
  child.stdout.on("data", makeLineHandler());
4037
- child.stderr.on("data", makeLineHandler());
4203
+ child.stderr.on("data", makeLineHandler({ parseRalph: true }));
4038
4204
  sprintStateInterval = setInterval(() => {
4039
4205
  try {
4040
4206
  const stateResult = getSprintState2();
@@ -4044,10 +4210,14 @@ function registerRunCommand(program) {
4044
4210
  storyKey: s.run.currentStory ?? "",
4045
4211
  phase: s.run.currentPhase ?? "",
4046
4212
  done: s.sprint.done,
4047
- total: s.sprint.total
4213
+ total: s.sprint.total,
4214
+ elapsed: formatElapsed(Date.now() - sessionStartTime)
4048
4215
  };
4049
4216
  rendererHandle.updateSprintState(sprintInfo);
4050
4217
  }
4218
+ const currentStatuses = readSprintStatus(projectDir);
4219
+ const storyEntries = mapSprintStatuses(currentStatuses);
4220
+ rendererHandle.updateStories(storyEntries);
4051
4221
  } catch {
4052
4222
  }
4053
4223
  }, 5e3);
@@ -4121,7 +4291,7 @@ function registerRunCommand(program) {
4121
4291
 
4122
4292
  // src/commands/verify.ts
4123
4293
  import { existsSync as existsSync25, readFileSync as readFileSync23 } from "fs";
4124
- import { join as join21 } from "path";
4294
+ import { join as join22 } from "path";
4125
4295
 
4126
4296
  // src/modules/verify/index.ts
4127
4297
  import { readFileSync as readFileSync22 } from "fs";
@@ -5174,6 +5344,121 @@ function parseObservabilityGaps(proofContent) {
5174
5344
  // src/modules/observability/analyzer.ts
5175
5345
  import { execFileSync as execFileSync8 } from "child_process";
5176
5346
  import { join as join16 } from "path";
5347
+ var DEFAULT_RULES_DIR = "patches/observability/";
5348
+ var DEFAULT_TIMEOUT = 6e4;
5349
+ var FUNCTION_NO_LOG_RULE = "function-no-debug-log";
5350
+ var CATCH_WITHOUT_LOGGING_RULE = "catch-without-logging";
5351
+ var ERROR_PATH_NO_LOG_RULE = "error-path-no-log";
5352
+ function matchesRule(gapType, ruleName) {
5353
+ return gapType === ruleName || gapType.endsWith(`.${ruleName}`);
5354
+ }
5355
+ function analyze(projectDir, config) {
5356
+ if (!projectDir || typeof projectDir !== "string") {
5357
+ return fail2("projectDir is required and must be a non-empty string");
5358
+ }
5359
+ const tool = config?.tool ?? "semgrep";
5360
+ if (tool !== "semgrep") {
5361
+ return fail2(`Unsupported analyzer tool: ${tool}`);
5362
+ }
5363
+ if (!checkSemgrepInstalled()) {
5364
+ return ok2({
5365
+ tool: "semgrep",
5366
+ gaps: [],
5367
+ summary: {
5368
+ totalFunctions: 0,
5369
+ functionsWithLogs: 0,
5370
+ errorHandlersWithoutLogs: 0,
5371
+ coveragePercent: 0,
5372
+ levelDistribution: {}
5373
+ },
5374
+ skipped: true,
5375
+ skipReason: "static analysis skipped -- install semgrep"
5376
+ });
5377
+ }
5378
+ const rulesDir = config?.rulesDir ?? DEFAULT_RULES_DIR;
5379
+ const timeout = config?.timeout ?? DEFAULT_TIMEOUT;
5380
+ const fullRulesDir = join16(projectDir, rulesDir);
5381
+ const rawResult = runSemgrep(projectDir, fullRulesDir, timeout);
5382
+ if (!rawResult.success) {
5383
+ return fail2(rawResult.error);
5384
+ }
5385
+ const gaps = parseSemgrepOutput(rawResult.data);
5386
+ const summaryOpts = config?.totalFunctions != null ? { totalFunctions: config.totalFunctions } : void 0;
5387
+ const summary = computeSummary(gaps, summaryOpts);
5388
+ return ok2({
5389
+ tool: "semgrep",
5390
+ gaps,
5391
+ summary
5392
+ });
5393
+ }
5394
+ function checkSemgrepInstalled() {
5395
+ try {
5396
+ execFileSync8("semgrep", ["--version"], {
5397
+ encoding: "utf-8",
5398
+ timeout: 5e3,
5399
+ stdio: "pipe"
5400
+ });
5401
+ return true;
5402
+ } catch {
5403
+ return false;
5404
+ }
5405
+ }
5406
+ function runSemgrep(projectDir, rulesDir, timeout = DEFAULT_TIMEOUT) {
5407
+ try {
5408
+ const stdout = execFileSync8(
5409
+ "semgrep",
5410
+ ["scan", "--config", rulesDir, "--json", projectDir],
5411
+ { encoding: "utf-8", timeout, stdio: ["pipe", "pipe", "pipe"] }
5412
+ );
5413
+ const parsed = JSON.parse(stdout);
5414
+ if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.results)) {
5415
+ return fail2("Semgrep scan returned invalid JSON: missing results array");
5416
+ }
5417
+ return ok2(parsed);
5418
+ } catch (error) {
5419
+ return fail2(`Semgrep scan failed: ${String(error)}`);
5420
+ }
5421
+ }
5422
+ function parseSemgrepOutput(raw) {
5423
+ if (!raw.results || !Array.isArray(raw.results)) {
5424
+ return [];
5425
+ }
5426
+ return raw.results.map((r) => ({
5427
+ file: r.path,
5428
+ line: r.start.line,
5429
+ type: r.check_id,
5430
+ description: r.extra.message,
5431
+ severity: normalizeSeverity(r.extra.severity)
5432
+ }));
5433
+ }
5434
+ function computeSummary(gaps, opts) {
5435
+ const functionsWithoutLogs = gaps.filter(
5436
+ (g) => matchesRule(g.type, FUNCTION_NO_LOG_RULE)
5437
+ ).length;
5438
+ const errorHandlersWithoutLogs = gaps.filter(
5439
+ (g) => matchesRule(g.type, CATCH_WITHOUT_LOGGING_RULE) || matchesRule(g.type, ERROR_PATH_NO_LOG_RULE)
5440
+ ).length;
5441
+ const totalFunctions = opts?.totalFunctions ?? functionsWithoutLogs;
5442
+ const functionsWithLogs = totalFunctions - functionsWithoutLogs;
5443
+ const coveragePercent = totalFunctions === 0 ? 100 : Math.round(functionsWithLogs / totalFunctions * 100 * 100) / 100;
5444
+ const levelDistribution = {};
5445
+ for (const gap2 of gaps) {
5446
+ levelDistribution[gap2.severity] = (levelDistribution[gap2.severity] ?? 0) + 1;
5447
+ }
5448
+ return {
5449
+ totalFunctions,
5450
+ functionsWithLogs,
5451
+ errorHandlersWithoutLogs,
5452
+ coveragePercent,
5453
+ levelDistribution
5454
+ };
5455
+ }
5456
+ function normalizeSeverity(severity) {
5457
+ const lower = severity.toLowerCase();
5458
+ if (lower === "error") return "error";
5459
+ if (lower === "warning") return "warning";
5460
+ return "info";
5461
+ }
5177
5462
 
5178
5463
  // src/modules/observability/coverage.ts
5179
5464
  import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, renameSync as renameSync2, existsSync as existsSync21 } from "fs";
@@ -5306,6 +5591,167 @@ function checkObservabilityCoverageGate(projectDir, overrides) {
5306
5591
  return ok2({ passed, staticResult, runtimeResult, gapSummary });
5307
5592
  }
5308
5593
 
5594
+ // src/modules/observability/runtime-validator.ts
5595
+ import { execSync as execSync3 } from "child_process";
5596
+ import { readdirSync as readdirSync4, statSync as statSync2 } from "fs";
5597
+ import { join as join19 } from "path";
5598
+ var DEFAULT_CONFIG = {
5599
+ testCommand: "npm test",
5600
+ otlpEndpoint: "http://localhost:4318",
5601
+ queryEndpoint: "http://localhost:9428",
5602
+ timeoutMs: 12e4
5603
+ };
5604
+ var HEALTH_TIMEOUT_MS = 3e3;
5605
+ var QUERY_TIMEOUT_MS = 3e4;
5606
+ async function validateRuntime(projectDir, config) {
5607
+ if (!projectDir || typeof projectDir !== "string") {
5608
+ return fail2("projectDir is required and must be a non-empty string");
5609
+ }
5610
+ const cfg = { ...DEFAULT_CONFIG, ...config };
5611
+ if (/[;&|`$(){}!#]/.test(cfg.testCommand)) {
5612
+ return fail2(`testCommand contains disallowed shell metacharacters: ${cfg.testCommand}`);
5613
+ }
5614
+ const healthy = await checkBackendHealth(cfg.queryEndpoint);
5615
+ if (!healthy) {
5616
+ const modules2 = discoverModules(projectDir);
5617
+ const entries2 = modules2.map((m) => ({
5618
+ moduleName: m,
5619
+ telemetryDetected: false,
5620
+ eventCount: 0
5621
+ }));
5622
+ return ok2({
5623
+ entries: entries2,
5624
+ totalModules: modules2.length,
5625
+ modulesWithTelemetry: 0,
5626
+ coveragePercent: 0,
5627
+ skipped: true,
5628
+ skipReason: "runtime validation skipped -- observability stack not available"
5629
+ });
5630
+ }
5631
+ const startTime = (/* @__PURE__ */ new Date()).toISOString();
5632
+ try {
5633
+ execSync3(cfg.testCommand, {
5634
+ cwd: projectDir,
5635
+ timeout: cfg.timeoutMs,
5636
+ env: {
5637
+ ...process.env,
5638
+ OTEL_EXPORTER_OTLP_ENDPOINT: cfg.otlpEndpoint
5639
+ },
5640
+ stdio: "pipe"
5641
+ });
5642
+ } catch (err) {
5643
+ const msg = err instanceof Error ? err.message : String(err);
5644
+ return fail2(`Test command failed: ${msg}`);
5645
+ }
5646
+ const endTime = (/* @__PURE__ */ new Date()).toISOString();
5647
+ const eventsResult = await queryTelemetryEvents(cfg.queryEndpoint, startTime, endTime);
5648
+ if (!eventsResult.success) {
5649
+ return fail2(eventsResult.error);
5650
+ }
5651
+ const modules = discoverModules(projectDir);
5652
+ const entries = mapEventsToModules(eventsResult.data, projectDir, modules);
5653
+ const modulesWithTelemetry = entries.filter((e) => e.telemetryDetected).length;
5654
+ const totalModules = entries.length;
5655
+ const coveragePercent = totalModules === 0 ? 0 : modulesWithTelemetry / totalModules * 100;
5656
+ return ok2({
5657
+ entries,
5658
+ totalModules,
5659
+ modulesWithTelemetry,
5660
+ coveragePercent,
5661
+ skipped: false
5662
+ });
5663
+ }
5664
+ async function checkBackendHealth(queryEndpoint) {
5665
+ try {
5666
+ const response = await fetch(`${queryEndpoint}/health`, {
5667
+ signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
5668
+ });
5669
+ return response.ok;
5670
+ } catch {
5671
+ return false;
5672
+ }
5673
+ }
5674
+ async function queryTelemetryEvents(queryEndpoint, startTime, endTime) {
5675
+ let url;
5676
+ try {
5677
+ url = new URL("/select/logsql/query", queryEndpoint);
5678
+ } catch {
5679
+ return fail2(`Invalid queryEndpoint URL: ${queryEndpoint}`);
5680
+ }
5681
+ url.searchParams.set("query", "*");
5682
+ url.searchParams.set("start", startTime);
5683
+ url.searchParams.set("end", endTime);
5684
+ url.searchParams.set("limit", "1000");
5685
+ try {
5686
+ const response = await fetch(url.toString(), {
5687
+ signal: AbortSignal.timeout(QUERY_TIMEOUT_MS)
5688
+ });
5689
+ if (!response.ok) {
5690
+ return fail2(`VictoriaLogs returned ${response.status}`);
5691
+ }
5692
+ const text = await response.text();
5693
+ const events = parseLogEvents(text);
5694
+ return ok2(events);
5695
+ } catch (err) {
5696
+ const msg = err instanceof Error ? err.message : String(err);
5697
+ return fail2(`Failed to query telemetry events: ${msg}`);
5698
+ }
5699
+ }
5700
+ function mapEventsToModules(events, projectDir, modules) {
5701
+ void projectDir;
5702
+ const moduleList = modules ?? [];
5703
+ const moduleCounts = /* @__PURE__ */ new Map();
5704
+ for (const mod of moduleList) {
5705
+ moduleCounts.set(mod, 0);
5706
+ }
5707
+ for (const event of events) {
5708
+ for (const mod of moduleList) {
5709
+ if (event.source.includes(mod) || event.message.includes(mod)) {
5710
+ moduleCounts.set(mod, (moduleCounts.get(mod) ?? 0) + 1);
5711
+ }
5712
+ }
5713
+ }
5714
+ return moduleList.map((mod) => {
5715
+ const count = moduleCounts.get(mod) ?? 0;
5716
+ return {
5717
+ moduleName: mod,
5718
+ telemetryDetected: count > 0,
5719
+ eventCount: count
5720
+ };
5721
+ });
5722
+ }
5723
+ function discoverModules(projectDir) {
5724
+ const srcDir = join19(projectDir, "src");
5725
+ try {
5726
+ return readdirSync4(srcDir).filter((name) => {
5727
+ try {
5728
+ return statSync2(join19(srcDir, name)).isDirectory();
5729
+ } catch {
5730
+ return false;
5731
+ }
5732
+ });
5733
+ } catch {
5734
+ return [];
5735
+ }
5736
+ }
5737
+ function parseLogEvents(text) {
5738
+ if (!text.trim()) return [];
5739
+ const lines = text.trim().split("\n");
5740
+ const events = [];
5741
+ for (const line of lines) {
5742
+ try {
5743
+ const raw = JSON.parse(line);
5744
+ events.push({
5745
+ timestamp: String(raw._time ?? raw.timestamp ?? ""),
5746
+ message: String(raw._msg ?? raw.message ?? ""),
5747
+ source: String(raw.source ?? raw._source ?? raw.service ?? "")
5748
+ });
5749
+ } catch {
5750
+ }
5751
+ }
5752
+ return events;
5753
+ }
5754
+
5309
5755
  // src/modules/verify/browser.ts
5310
5756
  import { execFileSync as execFileSync9 } from "child_process";
5311
5757
  import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
@@ -5945,9 +6391,9 @@ function getACById(id) {
5945
6391
  }
5946
6392
 
5947
6393
  // src/modules/verify/validation-runner.ts
5948
- import { execSync as execSync3 } from "child_process";
6394
+ import { execSync as execSync4 } from "child_process";
5949
6395
  import { writeFileSync as writeFileSync14, mkdirSync as mkdirSync7 } from "fs";
5950
- import { join as join19, dirname as dirname5 } from "path";
6396
+ import { join as join20, dirname as dirname5 } from "path";
5951
6397
  var MAX_VALIDATION_ATTEMPTS = 10;
5952
6398
  var AC_COMMAND_TIMEOUT_MS = 3e4;
5953
6399
  var VAL_KEY_PREFIX = "val-";
@@ -6019,7 +6465,7 @@ function executeValidationAC(ac) {
6019
6465
  }
6020
6466
  const startTime = Date.now();
6021
6467
  try {
6022
- const output = execSync3(ac.command, {
6468
+ const output = execSync4(ac.command, {
6023
6469
  timeout: AC_COMMAND_TIMEOUT_MS,
6024
6470
  encoding: "utf-8",
6025
6471
  stdio: ["pipe", "pipe", "pipe"]
@@ -6056,7 +6502,7 @@ function executeValidationAC(ac) {
6056
6502
  function createFixStory(ac, error) {
6057
6503
  try {
6058
6504
  const storyKey = `val-fix-${ac.id}`;
6059
- const storyPath = join19(
6505
+ const storyPath = join20(
6060
6506
  process.cwd(),
6061
6507
  "_bmad-output",
6062
6508
  "implementation-artifacts",
@@ -6159,7 +6605,7 @@ function processValidationResult(acId, result) {
6159
6605
  }
6160
6606
 
6161
6607
  // src/modules/dev/orchestrator.ts
6162
- import { execFileSync as execFileSync10, execSync as execSync4 } from "child_process";
6608
+ import { execFileSync as execFileSync10, execSync as execSync5 } from "child_process";
6163
6609
  var DEFAULT_TIMEOUT_MS = 15e5;
6164
6610
  var MAX_OUTPUT_LINES = 200;
6165
6611
  var GIT_TIMEOUT_MS2 = 5e3;
@@ -6172,17 +6618,17 @@ function truncateOutput(output, maxLines) {
6172
6618
  }
6173
6619
  function captureFilesChanged() {
6174
6620
  try {
6175
- const unstaged = execSync4("git diff --name-only", {
6621
+ const unstaged = execSync5("git diff --name-only", {
6176
6622
  timeout: GIT_TIMEOUT_MS2,
6177
6623
  encoding: "utf-8",
6178
6624
  stdio: ["pipe", "pipe", "pipe"]
6179
6625
  }).trim();
6180
- const staged = execSync4("git diff --cached --name-only", {
6626
+ const staged = execSync5("git diff --cached --name-only", {
6181
6627
  timeout: GIT_TIMEOUT_MS2,
6182
6628
  encoding: "utf-8",
6183
6629
  stdio: ["pipe", "pipe", "pipe"]
6184
6630
  }).trim();
6185
- const untracked = execSync4("git ls-files --others --exclude-standard", {
6631
+ const untracked = execSync5("git ls-files --others --exclude-standard", {
6186
6632
  timeout: GIT_TIMEOUT_MS2,
6187
6633
  encoding: "utf-8",
6188
6634
  stdio: ["pipe", "pipe", "pipe"]
@@ -6426,8 +6872,8 @@ function runValidationCycle() {
6426
6872
 
6427
6873
  // src/modules/verify/env.ts
6428
6874
  import { execFileSync as execFileSync11 } from "child_process";
6429
- import { existsSync as existsSync24, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync21, cpSync, rmSync, statSync as statSync2 } from "fs";
6430
- import { join as join20, basename as basename4 } from "path";
6875
+ import { existsSync as existsSync24, mkdirSync as mkdirSync8, readdirSync as readdirSync5, readFileSync as readFileSync21, cpSync, rmSync, statSync as statSync3 } from "fs";
6876
+ import { join as join21, basename as basename4 } from "path";
6431
6877
  import { createHash } from "crypto";
6432
6878
  var IMAGE_TAG = "codeharness-verify";
6433
6879
  var STORY_DIR = "_bmad-output/implementation-artifacts";
@@ -6440,7 +6886,7 @@ function isValidStoryKey(storyKey) {
6440
6886
  return /^[a-zA-Z0-9_-]+$/.test(storyKey);
6441
6887
  }
6442
6888
  function computeDistHash(projectDir) {
6443
- const distDir = join20(projectDir, "dist");
6889
+ const distDir = join21(projectDir, "dist");
6444
6890
  if (!existsSync24(distDir)) return null;
6445
6891
  const hash = createHash("sha256");
6446
6892
  const files = collectFiles(distDir).sort();
@@ -6452,8 +6898,8 @@ function computeDistHash(projectDir) {
6452
6898
  }
6453
6899
  function collectFiles(dir) {
6454
6900
  const results = [];
6455
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
6456
- const fullPath = join20(dir, entry.name);
6901
+ for (const entry of readdirSync5(dir, { withFileTypes: true })) {
6902
+ const fullPath = join21(dir, entry.name);
6457
6903
  if (entry.isDirectory()) {
6458
6904
  results.push(...collectFiles(fullPath));
6459
6905
  } else {
@@ -6483,7 +6929,7 @@ function detectProjectType(projectDir) {
6483
6929
  const stack = detectStack(projectDir);
6484
6930
  if (stack === "nodejs") return "nodejs";
6485
6931
  if (stack === "python") return "python";
6486
- if (existsSync24(join20(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
6932
+ if (existsSync24(join21(projectDir, ".claude-plugin", "plugin.json"))) return "plugin";
6487
6933
  return "generic";
6488
6934
  }
6489
6935
  function buildVerifyImage(options = {}) {
@@ -6527,12 +6973,12 @@ function buildNodeImage(projectDir) {
6527
6973
  const lastLine = packOutput.split("\n").pop()?.trim();
6528
6974
  if (!lastLine) throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
6529
6975
  const tarballName = basename4(lastLine);
6530
- const tarballPath = join20("/tmp", tarballName);
6531
- const buildContext = join20("/tmp", `codeharness-verify-build-${Date.now()}`);
6976
+ const tarballPath = join21("/tmp", tarballName);
6977
+ const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
6532
6978
  mkdirSync8(buildContext, { recursive: true });
6533
6979
  try {
6534
- cpSync(tarballPath, join20(buildContext, tarballName));
6535
- cpSync(resolveDockerfileTemplate(projectDir), join20(buildContext, "Dockerfile"));
6980
+ cpSync(tarballPath, join21(buildContext, tarballName));
6981
+ cpSync(resolveDockerfileTemplate(projectDir), join21(buildContext, "Dockerfile"));
6536
6982
  execFileSync11("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${tarballName}`, "."], {
6537
6983
  cwd: buildContext,
6538
6984
  stdio: "pipe",
@@ -6544,17 +6990,17 @@ function buildNodeImage(projectDir) {
6544
6990
  }
6545
6991
  }
6546
6992
  function buildPythonImage(projectDir) {
6547
- const distDir = join20(projectDir, "dist");
6548
- const distFiles = readdirSync4(distDir).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".whl"));
6993
+ const distDir = join21(projectDir, "dist");
6994
+ const distFiles = readdirSync5(distDir).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".whl"));
6549
6995
  if (distFiles.length === 0) {
6550
6996
  throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
6551
6997
  }
6552
6998
  const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
6553
- const buildContext = join20("/tmp", `codeharness-verify-build-${Date.now()}`);
6999
+ const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
6554
7000
  mkdirSync8(buildContext, { recursive: true });
6555
7001
  try {
6556
- cpSync(join20(distDir, distFile), join20(buildContext, distFile));
6557
- cpSync(resolveDockerfileTemplate(projectDir), join20(buildContext, "Dockerfile"));
7002
+ cpSync(join21(distDir, distFile), join21(buildContext, distFile));
7003
+ cpSync(resolveDockerfileTemplate(projectDir), join21(buildContext, "Dockerfile"));
6558
7004
  execFileSync11("docker", ["build", "-t", IMAGE_TAG, "--build-arg", `TARBALL=${distFile}`, "."], {
6559
7005
  cwd: buildContext,
6560
7006
  stdio: "pipe",
@@ -6569,19 +7015,19 @@ function prepareVerifyWorkspace(storyKey, projectDir) {
6569
7015
  if (!isValidStoryKey(storyKey)) {
6570
7016
  throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
6571
7017
  }
6572
- const storyFile = join20(root, STORY_DIR, `${storyKey}.md`);
7018
+ const storyFile = join21(root, STORY_DIR, `${storyKey}.md`);
6573
7019
  if (!existsSync24(storyFile)) throw new Error(`Story file not found: ${storyFile}`);
6574
7020
  const workspace = `${TEMP_PREFIX}${storyKey}`;
6575
7021
  if (existsSync24(workspace)) rmSync(workspace, { recursive: true, force: true });
6576
7022
  mkdirSync8(workspace, { recursive: true });
6577
- cpSync(storyFile, join20(workspace, "story.md"));
6578
- const readmePath = join20(root, "README.md");
6579
- if (existsSync24(readmePath)) cpSync(readmePath, join20(workspace, "README.md"));
6580
- const docsDir = join20(root, "docs");
6581
- if (existsSync24(docsDir) && statSync2(docsDir).isDirectory()) {
6582
- cpSync(docsDir, join20(workspace, "docs"), { recursive: true });
6583
- }
6584
- mkdirSync8(join20(workspace, "verification"), { recursive: true });
7023
+ cpSync(storyFile, join21(workspace, "story.md"));
7024
+ const readmePath = join21(root, "README.md");
7025
+ if (existsSync24(readmePath)) cpSync(readmePath, join21(workspace, "README.md"));
7026
+ const docsDir = join21(root, "docs");
7027
+ if (existsSync24(docsDir) && statSync3(docsDir).isDirectory()) {
7028
+ cpSync(docsDir, join21(workspace, "docs"), { recursive: true });
7029
+ }
7030
+ mkdirSync8(join21(workspace, "verification"), { recursive: true });
6585
7031
  return workspace;
6586
7032
  }
6587
7033
  function checkVerifyEnv() {
@@ -6634,18 +7080,18 @@ function cleanupVerifyEnv(storyKey) {
6634
7080
  }
6635
7081
  }
6636
7082
  function buildPluginImage(projectDir) {
6637
- const buildContext = join20("/tmp", `codeharness-verify-build-${Date.now()}`);
7083
+ const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
6638
7084
  mkdirSync8(buildContext, { recursive: true });
6639
7085
  try {
6640
- const pluginDir = join20(projectDir, ".claude-plugin");
6641
- cpSync(pluginDir, join20(buildContext, ".claude-plugin"), { recursive: true });
7086
+ const pluginDir = join21(projectDir, ".claude-plugin");
7087
+ cpSync(pluginDir, join21(buildContext, ".claude-plugin"), { recursive: true });
6642
7088
  for (const dir of ["commands", "hooks", "knowledge", "skills"]) {
6643
- const src = join20(projectDir, dir);
6644
- if (existsSync24(src) && statSync2(src).isDirectory()) {
6645
- cpSync(src, join20(buildContext, dir), { recursive: true });
7089
+ const src = join21(projectDir, dir);
7090
+ if (existsSync24(src) && statSync3(src).isDirectory()) {
7091
+ cpSync(src, join21(buildContext, dir), { recursive: true });
6646
7092
  }
6647
7093
  }
6648
- cpSync(resolveDockerfileTemplate(projectDir, "generic"), join20(buildContext, "Dockerfile"));
7094
+ cpSync(resolveDockerfileTemplate(projectDir, "generic"), join21(buildContext, "Dockerfile"));
6649
7095
  execFileSync11("docker", ["build", "-t", IMAGE_TAG, "."], {
6650
7096
  cwd: buildContext,
6651
7097
  stdio: "pipe",
@@ -6656,10 +7102,10 @@ function buildPluginImage(projectDir) {
6656
7102
  }
6657
7103
  }
6658
7104
  function buildGenericImage(projectDir) {
6659
- const buildContext = join20("/tmp", `codeharness-verify-build-${Date.now()}`);
7105
+ const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
6660
7106
  mkdirSync8(buildContext, { recursive: true });
6661
7107
  try {
6662
- cpSync(resolveDockerfileTemplate(projectDir, "generic"), join20(buildContext, "Dockerfile"));
7108
+ cpSync(resolveDockerfileTemplate(projectDir, "generic"), join21(buildContext, "Dockerfile"));
6663
7109
  execFileSync11("docker", ["build", "-t", IMAGE_TAG, "."], {
6664
7110
  cwd: buildContext,
6665
7111
  stdio: "pipe",
@@ -6671,10 +7117,10 @@ function buildGenericImage(projectDir) {
6671
7117
  }
6672
7118
  function resolveDockerfileTemplate(projectDir, variant) {
6673
7119
  const filename = variant === "generic" ? "Dockerfile.verify.generic" : "Dockerfile.verify";
6674
- const local = join20(projectDir, "templates", filename);
7120
+ const local = join21(projectDir, "templates", filename);
6675
7121
  if (existsSync24(local)) return local;
6676
7122
  const pkgDir = new URL("../../", import.meta.url).pathname;
6677
- const pkg = join20(pkgDir, "templates", filename);
7123
+ const pkg = join21(pkgDir, "templates", filename);
6678
7124
  if (existsSync24(pkg)) return pkg;
6679
7125
  throw new Error(`${filename} not found. Ensure templates/${filename} exists.`);
6680
7126
  }
@@ -6703,6 +7149,17 @@ function getImageSize(tag) {
6703
7149
  }
6704
7150
  }
6705
7151
 
7152
+ // src/modules/verify/index.ts
7153
+ function parseProof(path) {
7154
+ try {
7155
+ const quality = validateProofQuality(path);
7156
+ return ok2(quality);
7157
+ } catch (err) {
7158
+ const message = err instanceof Error ? err.message : String(err);
7159
+ return fail2(message);
7160
+ }
7161
+ }
7162
+
6706
7163
  // src/commands/verify.ts
6707
7164
  var STORY_DIR2 = "_bmad-output/implementation-artifacts";
6708
7165
  function isValidStoryId(storyId) {
@@ -6739,7 +7196,7 @@ function verifyRetro(opts, isJson, root) {
6739
7196
  return;
6740
7197
  }
6741
7198
  const retroFile = `epic-${epicNum}-retrospective.md`;
6742
- const retroPath = join21(root, STORY_DIR2, retroFile);
7199
+ const retroPath = join22(root, STORY_DIR2, retroFile);
6743
7200
  if (!existsSync25(retroPath)) {
6744
7201
  if (isJson) {
6745
7202
  jsonOutput({ status: "fail", epic: epicNum, retroFile, message: `${retroFile} not found` });
@@ -6757,7 +7214,7 @@ function verifyRetro(opts, isJson, root) {
6757
7214
  warn(`Failed to update sprint status: ${message}`);
6758
7215
  }
6759
7216
  if (isJson) {
6760
- jsonOutput({ status: "ok", epic: epicNum, retroFile: join21(STORY_DIR2, retroFile) });
7217
+ jsonOutput({ status: "ok", epic: epicNum, retroFile: join22(STORY_DIR2, retroFile) });
6761
7218
  } else {
6762
7219
  ok(`Epic ${epicNum} retrospective: marked done`);
6763
7220
  }
@@ -6768,7 +7225,7 @@ function verifyStory(storyId, isJson, root) {
6768
7225
  process.exitCode = 1;
6769
7226
  return;
6770
7227
  }
6771
- const readmePath = join21(root, "README.md");
7228
+ const readmePath = join22(root, "README.md");
6772
7229
  if (!existsSync25(readmePath)) {
6773
7230
  if (isJson) {
6774
7231
  jsonOutput({ status: "fail", message: "No README.md found \u2014 verification requires user documentation" });
@@ -6778,7 +7235,7 @@ function verifyStory(storyId, isJson, root) {
6778
7235
  process.exitCode = 1;
6779
7236
  return;
6780
7237
  }
6781
- const storyFilePath = join21(root, STORY_DIR2, `${storyId}.md`);
7238
+ const storyFilePath = join22(root, STORY_DIR2, `${storyId}.md`);
6782
7239
  if (!existsSync25(storyFilePath)) {
6783
7240
  fail(`Story file not found: ${storyFilePath}`, { json: isJson });
6784
7241
  process.exitCode = 1;
@@ -6819,7 +7276,7 @@ function verifyStory(storyId, isJson, root) {
6819
7276
  return;
6820
7277
  }
6821
7278
  const storyTitle = extractStoryTitle(storyFilePath);
6822
- const expectedProofPath = join21(root, "verification", `${storyId}-proof.md`);
7279
+ const expectedProofPath = join22(root, "verification", `${storyId}-proof.md`);
6823
7280
  const proofPath = existsSync25(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
6824
7281
  const proofQuality = validateProofQuality(proofPath);
6825
7282
  if (!proofQuality.passed) {
@@ -6935,13 +7392,13 @@ function extractStoryTitle(filePath) {
6935
7392
 
6936
7393
  // src/lib/onboard-checks.ts
6937
7394
  import { existsSync as existsSync27 } from "fs";
6938
- import { join as join23, dirname as dirname6 } from "path";
7395
+ import { join as join24, dirname as dirname6 } from "path";
6939
7396
  import { fileURLToPath as fileURLToPath3 } from "url";
6940
7397
 
6941
7398
  // src/lib/coverage.ts
6942
- import { execSync as execSync5 } from "child_process";
7399
+ import { execSync as execSync6 } from "child_process";
6943
7400
  import { existsSync as existsSync26, readFileSync as readFileSync24 } from "fs";
6944
- import { join as join22 } from "path";
7401
+ import { join as join23 } from "path";
6945
7402
  function detectCoverageTool(dir) {
6946
7403
  const baseDir = dir ?? process.cwd();
6947
7404
  const stateHint = getStateToolHint(baseDir);
@@ -6964,8 +7421,8 @@ function getStateToolHint(dir) {
6964
7421
  }
6965
7422
  }
6966
7423
  function detectNodeCoverageTool(dir, stateHint) {
6967
- const hasVitestConfig = existsSync26(join22(dir, "vitest.config.ts")) || existsSync26(join22(dir, "vitest.config.js"));
6968
- const pkgPath = join22(dir, "package.json");
7424
+ const hasVitestConfig = existsSync26(join23(dir, "vitest.config.ts")) || existsSync26(join23(dir, "vitest.config.js"));
7425
+ const pkgPath = join23(dir, "package.json");
6969
7426
  let hasVitestCoverageV8 = false;
6970
7427
  let hasVitestCoverageIstanbul = false;
6971
7428
  let hasC8 = false;
@@ -7026,7 +7483,7 @@ function getNodeTestCommand(scripts, runner) {
7026
7483
  return "npm test";
7027
7484
  }
7028
7485
  function detectPythonCoverageTool(dir) {
7029
- const reqPath = join22(dir, "requirements.txt");
7486
+ const reqPath = join23(dir, "requirements.txt");
7030
7487
  if (existsSync26(reqPath)) {
7031
7488
  try {
7032
7489
  const content = readFileSync24(reqPath, "utf-8");
@@ -7040,7 +7497,7 @@ function detectPythonCoverageTool(dir) {
7040
7497
  } catch {
7041
7498
  }
7042
7499
  }
7043
- const pyprojectPath = join22(dir, "pyproject.toml");
7500
+ const pyprojectPath = join23(dir, "pyproject.toml");
7044
7501
  if (existsSync26(pyprojectPath)) {
7045
7502
  try {
7046
7503
  const content = readFileSync24(pyprojectPath, "utf-8");
@@ -7073,7 +7530,7 @@ function runCoverage(dir) {
7073
7530
  let rawOutput = "";
7074
7531
  let testsPassed = true;
7075
7532
  try {
7076
- rawOutput = execSync5(toolInfo.runCommand, {
7533
+ rawOutput = execSync6(toolInfo.runCommand, {
7077
7534
  cwd: baseDir,
7078
7535
  encoding: "utf-8",
7079
7536
  stdio: ["pipe", "pipe", "pipe"],
@@ -7133,7 +7590,7 @@ function parseVitestCoverage(dir) {
7133
7590
  }
7134
7591
  }
7135
7592
  function parsePythonCoverage(dir) {
7136
- const reportPath = join22(dir, "coverage.json");
7593
+ const reportPath = join23(dir, "coverage.json");
7137
7594
  if (!existsSync26(reportPath)) {
7138
7595
  warn("Coverage report not found at coverage.json");
7139
7596
  return 0;
@@ -7270,8 +7727,8 @@ function checkPerFileCoverage(floor, dir) {
7270
7727
  }
7271
7728
  function findCoverageSummary(dir) {
7272
7729
  const candidates = [
7273
- join22(dir, "coverage", "coverage-summary.json"),
7274
- join22(dir, "src", "coverage", "coverage-summary.json")
7730
+ join23(dir, "coverage", "coverage-summary.json"),
7731
+ join23(dir, "src", "coverage", "coverage-summary.json")
7275
7732
  ];
7276
7733
  for (const p of candidates) {
7277
7734
  if (existsSync26(p)) return p;
@@ -7307,7 +7764,7 @@ function checkBmadInstalled(dir) {
7307
7764
  function checkHooksRegistered(dir) {
7308
7765
  const __filename = fileURLToPath3(import.meta.url);
7309
7766
  const __dirname2 = dirname6(__filename);
7310
- const hooksPath = join23(__dirname2, "..", "..", "hooks", "hooks.json");
7767
+ const hooksPath = join24(__dirname2, "..", "..", "hooks", "hooks.json");
7311
7768
  return { ok: existsSync27(hooksPath) };
7312
7769
  }
7313
7770
  function runPreconditions(dir) {
@@ -7348,7 +7805,7 @@ function findVerificationGaps(dir) {
7348
7805
  for (const [key, status] of Object.entries(statuses)) {
7349
7806
  if (status !== "done") continue;
7350
7807
  if (!STORY_KEY_PATTERN2.test(key)) continue;
7351
- const proofPath = join23(root, "verification", `${key}-proof.md`);
7808
+ const proofPath = join24(root, "verification", `${key}-proof.md`);
7352
7809
  if (!existsSync27(proofPath)) {
7353
7810
  unverified.push(key);
7354
7811
  }
@@ -8061,16 +8518,16 @@ function getBeadsData() {
8061
8518
  }
8062
8519
 
8063
8520
  // src/commands/onboard.ts
8064
- import { join as join27 } from "path";
8521
+ import { join as join28 } from "path";
8065
8522
 
8066
8523
  // src/lib/scanner.ts
8067
8524
  import {
8068
8525
  existsSync as existsSync28,
8069
- readdirSync as readdirSync5,
8526
+ readdirSync as readdirSync6,
8070
8527
  readFileSync as readFileSync25,
8071
- statSync as statSync3
8528
+ statSync as statSync4
8072
8529
  } from "fs";
8073
- import { join as join24, relative as relative2 } from "path";
8530
+ import { join as join25, relative as relative2 } from "path";
8074
8531
  var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
8075
8532
  var DEFAULT_MIN_MODULE_SIZE = 3;
8076
8533
  function getExtension2(filename) {
@@ -8088,17 +8545,17 @@ function countSourceFiles(dir) {
8088
8545
  function walk(current) {
8089
8546
  let entries;
8090
8547
  try {
8091
- entries = readdirSync5(current);
8548
+ entries = readdirSync6(current);
8092
8549
  } catch {
8093
8550
  return;
8094
8551
  }
8095
8552
  for (const entry of entries) {
8096
8553
  if (isSkippedDir(entry)) continue;
8097
8554
  if (entry.startsWith(".") && current !== dir) continue;
8098
- const fullPath = join24(current, entry);
8555
+ const fullPath = join25(current, entry);
8099
8556
  let stat;
8100
8557
  try {
8101
- stat = statSync3(fullPath);
8558
+ stat = statSync4(fullPath);
8102
8559
  } catch {
8103
8560
  continue;
8104
8561
  }
@@ -8118,22 +8575,22 @@ function countSourceFiles(dir) {
8118
8575
  return count;
8119
8576
  }
8120
8577
  function countModuleFiles(modulePath, rootDir) {
8121
- const fullModulePath = join24(rootDir, modulePath);
8578
+ const fullModulePath = join25(rootDir, modulePath);
8122
8579
  let sourceFiles = 0;
8123
8580
  let testFiles = 0;
8124
8581
  function walk(current) {
8125
8582
  let entries;
8126
8583
  try {
8127
- entries = readdirSync5(current);
8584
+ entries = readdirSync6(current);
8128
8585
  } catch {
8129
8586
  return;
8130
8587
  }
8131
8588
  for (const entry of entries) {
8132
8589
  if (isSkippedDir(entry)) continue;
8133
- const fullPath = join24(current, entry);
8590
+ const fullPath = join25(current, entry);
8134
8591
  let stat;
8135
8592
  try {
8136
- stat = statSync3(fullPath);
8593
+ stat = statSync4(fullPath);
8137
8594
  } catch {
8138
8595
  continue;
8139
8596
  }
@@ -8155,7 +8612,7 @@ function countModuleFiles(modulePath, rootDir) {
8155
8612
  return { sourceFiles, testFiles };
8156
8613
  }
8157
8614
  function detectArtifacts(dir) {
8158
- const bmadPath = join24(dir, "_bmad");
8615
+ const bmadPath = join25(dir, "_bmad");
8159
8616
  const hasBmad = existsSync28(bmadPath);
8160
8617
  return {
8161
8618
  hasBmad,
@@ -8238,7 +8695,7 @@ function readPerFileCoverage(dir, format) {
8238
8695
  return null;
8239
8696
  }
8240
8697
  function readVitestPerFileCoverage(dir) {
8241
- const reportPath = join24(dir, "coverage", "coverage-summary.json");
8698
+ const reportPath = join25(dir, "coverage", "coverage-summary.json");
8242
8699
  if (!existsSync28(reportPath)) return null;
8243
8700
  try {
8244
8701
  const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
@@ -8253,7 +8710,7 @@ function readVitestPerFileCoverage(dir) {
8253
8710
  }
8254
8711
  }
8255
8712
  function readPythonPerFileCoverage(dir) {
8256
- const reportPath = join24(dir, "coverage.json");
8713
+ const reportPath = join25(dir, "coverage.json");
8257
8714
  if (!existsSync28(reportPath)) return null;
8258
8715
  try {
8259
8716
  const report = JSON.parse(readFileSync25(reportPath, "utf-8"));
@@ -8272,12 +8729,12 @@ function auditDocumentation(dir) {
8272
8729
  const root = dir ?? process.cwd();
8273
8730
  const documents = [];
8274
8731
  for (const docName of AUDIT_DOCUMENTS) {
8275
- const docPath = join24(root, docName);
8732
+ const docPath = join25(root, docName);
8276
8733
  if (!existsSync28(docPath)) {
8277
8734
  documents.push({ name: docName, grade: "missing", path: null });
8278
8735
  continue;
8279
8736
  }
8280
- const srcDir = join24(root, "src");
8737
+ const srcDir = join25(root, "src");
8281
8738
  const codeDir = existsSync28(srcDir) ? srcDir : root;
8282
8739
  const stale = isDocStale(docPath, codeDir);
8283
8740
  documents.push({
@@ -8286,10 +8743,10 @@ function auditDocumentation(dir) {
8286
8743
  path: docName
8287
8744
  });
8288
8745
  }
8289
- const docsDir = join24(root, "docs");
8746
+ const docsDir = join25(root, "docs");
8290
8747
  if (existsSync28(docsDir)) {
8291
8748
  try {
8292
- const stat = statSync3(docsDir);
8749
+ const stat = statSync4(docsDir);
8293
8750
  if (stat.isDirectory()) {
8294
8751
  documents.push({ name: "docs/", grade: "present", path: "docs/" });
8295
8752
  }
@@ -8299,9 +8756,9 @@ function auditDocumentation(dir) {
8299
8756
  } else {
8300
8757
  documents.push({ name: "docs/", grade: "missing", path: null });
8301
8758
  }
8302
- const indexPath = join24(root, "docs", "index.md");
8759
+ const indexPath = join25(root, "docs", "index.md");
8303
8760
  if (existsSync28(indexPath)) {
8304
- const srcDir = join24(root, "src");
8761
+ const srcDir = join25(root, "src");
8305
8762
  const indexCodeDir = existsSync28(srcDir) ? srcDir : root;
8306
8763
  const indexStale = isDocStale(indexPath, indexCodeDir);
8307
8764
  documents.push({
@@ -8320,7 +8777,7 @@ function auditDocumentation(dir) {
8320
8777
  // src/lib/epic-generator.ts
8321
8778
  import { createInterface } from "readline";
8322
8779
  import { existsSync as existsSync29, mkdirSync as mkdirSync9, writeFileSync as writeFileSync15 } from "fs";
8323
- import { dirname as dirname7, join as join25 } from "path";
8780
+ import { dirname as dirname7, join as join26 } from "path";
8324
8781
  var PRIORITY_BY_TYPE = {
8325
8782
  observability: 1,
8326
8783
  coverage: 2,
@@ -8358,7 +8815,7 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
8358
8815
  storyNum++;
8359
8816
  }
8360
8817
  for (const mod of scan.modules) {
8361
- const agentsPath = join25(root, mod.path, "AGENTS.md");
8818
+ const agentsPath = join26(root, mod.path, "AGENTS.md");
8362
8819
  if (!existsSync29(agentsPath)) {
8363
8820
  stories.push({
8364
8821
  key: `0.${storyNum}`,
@@ -8558,23 +9015,23 @@ function getGapIdFromTitle(title) {
8558
9015
 
8559
9016
  // src/lib/scan-cache.ts
8560
9017
  import { existsSync as existsSync30, mkdirSync as mkdirSync10, readFileSync as readFileSync26, writeFileSync as writeFileSync16 } from "fs";
8561
- import { join as join26 } from "path";
9018
+ import { join as join27 } from "path";
8562
9019
  var CACHE_DIR = ".harness";
8563
9020
  var CACHE_FILE = "last-onboard-scan.json";
8564
9021
  var DEFAULT_MAX_AGE_MS = 864e5;
8565
9022
  function saveScanCache(entry, dir) {
8566
9023
  try {
8567
9024
  const root = dir ?? process.cwd();
8568
- const cacheDir = join26(root, CACHE_DIR);
9025
+ const cacheDir = join27(root, CACHE_DIR);
8569
9026
  mkdirSync10(cacheDir, { recursive: true });
8570
- const cachePath = join26(cacheDir, CACHE_FILE);
9027
+ const cachePath = join27(cacheDir, CACHE_FILE);
8571
9028
  writeFileSync16(cachePath, JSON.stringify(entry, null, 2), "utf-8");
8572
9029
  } catch {
8573
9030
  }
8574
9031
  }
8575
9032
  function loadScanCache(dir) {
8576
9033
  const root = dir ?? process.cwd();
8577
- const cachePath = join26(root, CACHE_DIR, CACHE_FILE);
9034
+ const cachePath = join27(root, CACHE_DIR, CACHE_FILE);
8578
9035
  if (!existsSync30(cachePath)) {
8579
9036
  return null;
8580
9037
  }
@@ -8752,7 +9209,7 @@ function registerOnboardCommand(program) {
8752
9209
  }
8753
9210
  coverage = lastCoverageResult ?? runCoverageAnalysis(scan);
8754
9211
  audit = lastAuditResult ?? runAudit();
8755
- const epicPath = join27(process.cwd(), "ralph", "onboarding-epic.md");
9212
+ const epicPath = join28(process.cwd(), "ralph", "onboarding-epic.md");
8756
9213
  const epic = generateOnboardingEpic(scan, coverage, audit);
8757
9214
  mergeExtendedGaps(epic);
8758
9215
  if (!isFull) {
@@ -8825,7 +9282,7 @@ function registerOnboardCommand(program) {
8825
9282
  coverage,
8826
9283
  audit
8827
9284
  });
8828
- const epicPath = join27(process.cwd(), "ralph", "onboarding-epic.md");
9285
+ const epicPath = join28(process.cwd(), "ralph", "onboarding-epic.md");
8829
9286
  const epic = generateOnboardingEpic(scan, coverage, audit);
8830
9287
  mergeExtendedGaps(epic);
8831
9288
  if (!isFull) {
@@ -8934,7 +9391,7 @@ function printEpicOutput(epic) {
8934
9391
 
8935
9392
  // src/commands/teardown.ts
8936
9393
  import { existsSync as existsSync31, unlinkSync as unlinkSync2, readFileSync as readFileSync27, writeFileSync as writeFileSync17, rmSync as rmSync2 } from "fs";
8937
- import { join as join28 } from "path";
9394
+ import { join as join29 } from "path";
8938
9395
  function buildDefaultResult() {
8939
9396
  return {
8940
9397
  status: "ok",
@@ -9037,7 +9494,7 @@ function registerTeardownCommand(program) {
9037
9494
  info("Docker stack: not running, skipping");
9038
9495
  }
9039
9496
  }
9040
- const composeFilePath = join28(projectDir, composeFile);
9497
+ const composeFilePath = join29(projectDir, composeFile);
9041
9498
  if (existsSync31(composeFilePath)) {
9042
9499
  unlinkSync2(composeFilePath);
9043
9500
  result.removed.push(composeFile);
@@ -9045,7 +9502,7 @@ function registerTeardownCommand(program) {
9045
9502
  ok(`Removed: ${composeFile}`);
9046
9503
  }
9047
9504
  }
9048
- const otelConfigPath = join28(projectDir, "otel-collector-config.yaml");
9505
+ const otelConfigPath = join29(projectDir, "otel-collector-config.yaml");
9049
9506
  if (existsSync31(otelConfigPath)) {
9050
9507
  unlinkSync2(otelConfigPath);
9051
9508
  result.removed.push("otel-collector-config.yaml");
@@ -9056,7 +9513,7 @@ function registerTeardownCommand(program) {
9056
9513
  }
9057
9514
  let patchesRemoved = 0;
9058
9515
  for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
9059
- const filePath = join28(projectDir, "_bmad", relativePath);
9516
+ const filePath = join29(projectDir, "_bmad", relativePath);
9060
9517
  if (!existsSync31(filePath)) {
9061
9518
  continue;
9062
9519
  }
@@ -9077,7 +9534,7 @@ function registerTeardownCommand(program) {
9077
9534
  }
9078
9535
  }
9079
9536
  if (state.otlp?.enabled && state.stack === "nodejs") {
9080
- const pkgPath = join28(projectDir, "package.json");
9537
+ const pkgPath = join29(projectDir, "package.json");
9081
9538
  if (existsSync31(pkgPath)) {
9082
9539
  try {
9083
9540
  const raw = readFileSync27(pkgPath, "utf-8");
@@ -9120,7 +9577,7 @@ function registerTeardownCommand(program) {
9120
9577
  }
9121
9578
  }
9122
9579
  }
9123
- const harnessDir = join28(projectDir, ".harness");
9580
+ const harnessDir = join29(projectDir, ".harness");
9124
9581
  if (existsSync31(harnessDir)) {
9125
9582
  rmSync2(harnessDir, { recursive: true, force: true });
9126
9583
  result.removed.push(".harness/");
@@ -9874,7 +10331,7 @@ function registerQueryCommand(program) {
9874
10331
 
9875
10332
  // src/commands/retro-import.ts
9876
10333
  import { existsSync as existsSync32, readFileSync as readFileSync28 } from "fs";
9877
- import { join as join29 } from "path";
10334
+ import { join as join30 } from "path";
9878
10335
 
9879
10336
  // src/lib/retro-parser.ts
9880
10337
  var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
@@ -10043,7 +10500,7 @@ function registerRetroImportCommand(program) {
10043
10500
  return;
10044
10501
  }
10045
10502
  const retroFile = `epic-${epicNum}-retrospective.md`;
10046
- const retroPath = join29(root, STORY_DIR3, retroFile);
10503
+ const retroPath = join30(root, STORY_DIR3, retroFile);
10047
10504
  if (!existsSync32(retroPath)) {
10048
10505
  fail(`Retro file not found: ${retroFile}`, { json: isJson });
10049
10506
  process.exitCode = 1;
@@ -10432,19 +10889,19 @@ function registerVerifyEnvCommand(program) {
10432
10889
  }
10433
10890
 
10434
10891
  // src/commands/retry.ts
10435
- import { join as join31 } from "path";
10892
+ import { join as join32 } from "path";
10436
10893
 
10437
10894
  // src/lib/retry-state.ts
10438
10895
  import { existsSync as existsSync33, readFileSync as readFileSync29, writeFileSync as writeFileSync18 } from "fs";
10439
- import { join as join30 } from "path";
10896
+ import { join as join31 } from "path";
10440
10897
  var RETRIES_FILE = ".story_retries";
10441
10898
  var FLAGGED_FILE = ".flagged_stories";
10442
10899
  var LINE_PATTERN = /^([^=]+)=(\d+)$/;
10443
10900
  function retriesPath(dir) {
10444
- return join30(dir, RETRIES_FILE);
10901
+ return join31(dir, RETRIES_FILE);
10445
10902
  }
10446
10903
  function flaggedPath(dir) {
10447
- return join30(dir, FLAGGED_FILE);
10904
+ return join31(dir, FLAGGED_FILE);
10448
10905
  }
10449
10906
  function readRetries(dir) {
10450
10907
  const filePath = retriesPath(dir);
@@ -10516,7 +10973,7 @@ function registerRetryCommand(program) {
10516
10973
  program.command("retry").description("Manage retry state for stories").option("--reset", "Clear retry counters and flagged stories").option("--story <key>", "Target a specific story key (used with --reset or --status)").option("--status", "Show retry status for all stories").action((_options, cmd) => {
10517
10974
  const opts = cmd.optsWithGlobals();
10518
10975
  const isJson = opts.json === true;
10519
- const dir = join31(process.cwd(), RALPH_SUBDIR);
10976
+ const dir = join32(process.cwd(), RALPH_SUBDIR);
10520
10977
  if (opts.story && !isValidStoryKey3(opts.story)) {
10521
10978
  if (isJson) {
10522
10979
  jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
@@ -10914,8 +11371,435 @@ function registerObservabilityGateCommand(program) {
10914
11371
  });
10915
11372
  }
10916
11373
 
11374
+ // src/modules/audit/dimensions.ts
11375
+ import { existsSync as existsSync34, readFileSync as readFileSync30, readdirSync as readdirSync7 } from "fs";
11376
+ import { join as join33 } from "path";
11377
+ function gap(dimension, description, suggestedFix) {
11378
+ return { dimension, description, suggestedFix };
11379
+ }
11380
+ function dimOk(name, status, metric, gaps = []) {
11381
+ return ok2({ name, status, metric, gaps });
11382
+ }
11383
+ function dimCatch(name, err) {
11384
+ const msg = err instanceof Error ? err.message : String(err);
11385
+ return dimOk(name, "warn", "error", [gap(name, `${name} check failed: ${msg}`, `Check ${name} configuration`)]);
11386
+ }
11387
+ function worstStatus(...statuses) {
11388
+ if (statuses.includes("fail")) return "fail";
11389
+ if (statuses.includes("warn")) return "warn";
11390
+ return "pass";
11391
+ }
11392
+ async function checkObservability(projectDir) {
11393
+ try {
11394
+ const gaps = [];
11395
+ let sStatus = "pass", sMetric = "";
11396
+ const sr = analyze(projectDir);
11397
+ if (isOk(sr)) {
11398
+ const d = sr.data;
11399
+ if (d.skipped) {
11400
+ sStatus = "warn";
11401
+ sMetric = `static: skipped (${d.skipReason ?? "unknown"})`;
11402
+ gaps.push(gap("observability", `Static analysis skipped: ${d.skipReason ?? "Semgrep not installed"}`, "Install Semgrep: pip install semgrep"));
11403
+ } else {
11404
+ const n = d.gaps.length;
11405
+ sMetric = `static: ${n} gap${n !== 1 ? "s" : ""}`;
11406
+ if (n > 0) {
11407
+ sStatus = "warn";
11408
+ for (const g of d.gaps) gaps.push(gap("observability", `${g.file}:${g.line} \u2014 ${g.message}`, g.fix ?? "Add observability instrumentation"));
11409
+ }
11410
+ }
11411
+ } else {
11412
+ sStatus = "warn";
11413
+ sMetric = "static: skipped (analysis failed)";
11414
+ gaps.push(gap("observability", `Static analysis failed: ${sr.error}`, "Check Semgrep installation and rules configuration"));
11415
+ }
11416
+ let rStatus = "pass", rMetric = "";
11417
+ try {
11418
+ const rr = await validateRuntime(projectDir);
11419
+ if (isOk(rr)) {
11420
+ const d = rr.data;
11421
+ if (d.skipped) {
11422
+ rStatus = "warn";
11423
+ rMetric = `runtime: skipped (${d.skipReason ?? "unknown"})`;
11424
+ gaps.push(gap("observability", `Runtime validation skipped: ${d.skipReason ?? "backend unreachable"}`, "Start the observability stack: codeharness stack up"));
11425
+ } else {
11426
+ rMetric = `runtime: ${d.coveragePercent}%`;
11427
+ if (d.coveragePercent < 50) {
11428
+ rStatus = "warn";
11429
+ gaps.push(gap("observability", `Runtime coverage low: ${d.coveragePercent}%`, "Add telemetry instrumentation to more modules"));
11430
+ }
11431
+ }
11432
+ } else {
11433
+ rStatus = "warn";
11434
+ rMetric = "runtime: skipped (validation failed)";
11435
+ gaps.push(gap("observability", `Runtime validation failed: ${rr.error}`, "Ensure observability backend is running"));
11436
+ }
11437
+ } catch {
11438
+ rStatus = "warn";
11439
+ rMetric = "runtime: skipped (error)";
11440
+ gaps.push(gap("observability", "Runtime validation threw an unexpected error", "Check observability stack health"));
11441
+ }
11442
+ return dimOk("observability", worstStatus(sStatus, rStatus), `${sMetric}, ${rMetric}`, gaps);
11443
+ } catch (err) {
11444
+ return dimCatch("observability", err);
11445
+ }
11446
+ }
11447
+ function checkTesting(projectDir) {
11448
+ try {
11449
+ const r = checkOnlyCoverage(projectDir);
11450
+ if (!r.success) return dimOk("testing", "warn", "no coverage data", [gap("testing", "No coverage tool detected or coverage data unavailable", "Run tests with coverage: npm run test:coverage")]);
11451
+ const pct = r.coveragePercent;
11452
+ const gaps = [];
11453
+ let status = "pass";
11454
+ if (pct < 50) {
11455
+ status = "fail";
11456
+ gaps.push(gap("testing", `Test coverage critically low: ${pct}%`, "Add unit tests to increase coverage above 50%"));
11457
+ } else if (pct < 80) {
11458
+ status = "warn";
11459
+ gaps.push(gap("testing", `Test coverage below target: ${pct}%`, "Add tests to reach 80% coverage target"));
11460
+ }
11461
+ return dimOk("testing", status, `${pct}%`, gaps);
11462
+ } catch (err) {
11463
+ return dimCatch("testing", err);
11464
+ }
11465
+ }
11466
+ function checkDocumentation(projectDir) {
11467
+ try {
11468
+ const report = scanDocHealth(projectDir);
11469
+ const gaps = [];
11470
+ const { fresh, stale, missing } = report.summary;
11471
+ let status = "pass";
11472
+ if (missing > 0) {
11473
+ status = "fail";
11474
+ for (const doc of report.documents) if (doc.grade === "missing") gaps.push(gap("documentation", `Missing: ${doc.path} \u2014 ${doc.reason}`, `Create ${doc.path}`));
11475
+ }
11476
+ if (stale > 0) {
11477
+ if (status !== "fail") status = "warn";
11478
+ for (const doc of report.documents) if (doc.grade === "stale") gaps.push(gap("documentation", `Stale: ${doc.path} \u2014 ${doc.reason}`, `Update ${doc.path} to reflect current code`));
11479
+ }
11480
+ return dimOk("documentation", status, `${fresh} fresh, ${stale} stale, ${missing} missing`, gaps);
11481
+ } catch (err) {
11482
+ return dimCatch("documentation", err);
11483
+ }
11484
+ }
11485
+ function checkVerification(projectDir) {
11486
+ try {
11487
+ const gaps = [];
11488
+ const sprintPath = join33(projectDir, "_bmad-output", "implementation-artifacts", "sprint-status.yaml");
11489
+ if (!existsSync34(sprintPath)) return dimOk("verification", "warn", "no sprint data", [gap("verification", "No sprint-status.yaml found", "Run sprint planning to create sprint status")]);
11490
+ const vDir = join33(projectDir, "verification");
11491
+ let proofCount = 0, totalChecked = 0;
11492
+ if (existsSync34(vDir)) {
11493
+ for (const file of readdirSafe(vDir)) {
11494
+ if (!file.endsWith("-proof.md")) continue;
11495
+ totalChecked++;
11496
+ const r = parseProof(join33(vDir, file));
11497
+ if (isOk(r) && r.data.passed) {
11498
+ proofCount++;
11499
+ } else {
11500
+ gaps.push(gap("verification", `Story ${file.replace("-proof.md", "")} proof incomplete or failing`, `Run codeharness verify ${file.replace("-proof.md", "")}`));
11501
+ }
11502
+ }
11503
+ }
11504
+ let status = "pass";
11505
+ if (totalChecked === 0) {
11506
+ status = "warn";
11507
+ gaps.push(gap("verification", "No verification proofs found", "Run codeharness verify for completed stories"));
11508
+ } else if (proofCount < totalChecked) {
11509
+ status = "warn";
11510
+ }
11511
+ return dimOk("verification", status, totalChecked > 0 ? `${proofCount}/${totalChecked} verified` : "no proofs", gaps);
11512
+ } catch (err) {
11513
+ return dimCatch("verification", err);
11514
+ }
11515
+ }
11516
+ function checkInfrastructure(projectDir) {
11517
+ try {
11518
+ const dfPath = join33(projectDir, "Dockerfile");
11519
+ if (!existsSync34(dfPath)) return dimOk("infrastructure", "fail", "no Dockerfile", [gap("infrastructure", "No Dockerfile found", "Create a Dockerfile for containerized deployment")]);
11520
+ let content;
11521
+ try {
11522
+ content = readFileSync30(dfPath, "utf-8");
11523
+ } catch {
11524
+ return dimOk("infrastructure", "warn", "Dockerfile unreadable", [gap("infrastructure", "Dockerfile exists but could not be read", "Check Dockerfile permissions")]);
11525
+ }
11526
+ const fromLines = content.split("\n").filter((l) => /^\s*FROM\s+/i.test(l));
11527
+ if (fromLines.length === 0) return dimOk("infrastructure", "fail", "invalid Dockerfile", [gap("infrastructure", "Dockerfile has no FROM instruction", "Add a FROM instruction with a pinned base image")]);
11528
+ const gaps = [];
11529
+ let hasUnpinned = false;
11530
+ for (const line of fromLines) {
11531
+ const ref = line.replace(/^\s*FROM\s+/i, "").split(/\s+/)[0];
11532
+ if (ref.endsWith(":latest")) {
11533
+ hasUnpinned = true;
11534
+ gaps.push(gap("infrastructure", `Unpinned base image: ${ref}`, `Pin ${ref} to a specific version tag`));
11535
+ } else if (!ref.includes(":") && !ref.includes("@")) {
11536
+ hasUnpinned = true;
11537
+ gaps.push(gap("infrastructure", `Unpinned base image (no tag): ${ref}`, `Pin ${ref} to a specific version tag (e.g., ${ref}:22-slim)`));
11538
+ }
11539
+ }
11540
+ const status = hasUnpinned ? "warn" : "pass";
11541
+ const metric = hasUnpinned ? `Dockerfile exists (${gaps.length} issue${gaps.length !== 1 ? "s" : ""})` : "Dockerfile valid";
11542
+ return dimOk("infrastructure", status, metric, gaps);
11543
+ } catch (err) {
11544
+ return dimCatch("infrastructure", err);
11545
+ }
11546
+ }
11547
+ function readdirSafe(dir) {
11548
+ try {
11549
+ return readdirSync7(dir);
11550
+ } catch {
11551
+ return [];
11552
+ }
11553
+ }
11554
+
11555
+ // src/modules/audit/report.ts
11556
+ var STATUS_PREFIX = {
11557
+ pass: "[OK]",
11558
+ fail: "[FAIL]",
11559
+ warn: "[WARN]"
11560
+ };
11561
+ function formatAuditHuman(result) {
11562
+ const lines = [];
11563
+ for (const dimension of Object.values(result.dimensions)) {
11564
+ const prefix = STATUS_PREFIX[dimension.status] ?? "[WARN]";
11565
+ lines.push(`${prefix} ${dimension.name}: ${dimension.metric}`);
11566
+ for (const gap2 of dimension.gaps) {
11567
+ lines.push(` [WARN] ${gap2.description} -- fix: ${gap2.suggestedFix}`);
11568
+ }
11569
+ }
11570
+ const overallPrefix = STATUS_PREFIX[result.overallStatus] ?? "[WARN]";
11571
+ lines.push("");
11572
+ lines.push(
11573
+ `${overallPrefix} Audit complete: ${result.gapCount} gap${result.gapCount !== 1 ? "s" : ""} found (${result.durationMs}ms)`
11574
+ );
11575
+ return lines;
11576
+ }
11577
+ function formatAuditJson(result) {
11578
+ return result;
11579
+ }
11580
+
11581
+ // src/modules/audit/fix-generator.ts
11582
+ import { existsSync as existsSync35, writeFileSync as writeFileSync19, mkdirSync as mkdirSync11 } from "fs";
11583
+ import { join as join34, dirname as dirname8 } from "path";
11584
+ function buildStoryKey(gap2, index) {
11585
+ const safeDimension = gap2.dimension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
11586
+ return `audit-fix-${safeDimension}-${index}`;
11587
+ }
11588
+ function buildStoryMarkdown(gap2, key) {
11589
+ return [
11590
+ `# Fix: ${gap2.dimension} \u2014 ${gap2.description}`,
11591
+ "",
11592
+ "Status: backlog",
11593
+ "",
11594
+ "## Story",
11595
+ "",
11596
+ `As an operator, I need ${gap2.description} fixed so that audit compliance improves.`,
11597
+ "",
11598
+ "## Acceptance Criteria",
11599
+ "",
11600
+ `1. **Given** ${gap2.description}, **When** the fix is applied, **Then** ${gap2.suggestedFix}.`,
11601
+ "",
11602
+ "## Dev Notes",
11603
+ "",
11604
+ "This is an auto-generated fix story created by `codeharness audit --fix`.",
11605
+ `**Audit Gap:** ${gap2.dimension}: ${gap2.description}`,
11606
+ `**Suggested Fix:** ${gap2.suggestedFix}`,
11607
+ ""
11608
+ ].join("\n");
11609
+ }
11610
+ function generateFixStories(auditResult) {
11611
+ try {
11612
+ const stories = [];
11613
+ let created = 0;
11614
+ let skipped = 0;
11615
+ const artifactsDir = join34(
11616
+ process.cwd(),
11617
+ "_bmad-output",
11618
+ "implementation-artifacts"
11619
+ );
11620
+ for (const dimension of Object.values(auditResult.dimensions)) {
11621
+ for (let i = 0; i < dimension.gaps.length; i++) {
11622
+ const gap2 = dimension.gaps[i];
11623
+ const key = buildStoryKey(gap2, i + 1);
11624
+ const filePath = join34(artifactsDir, `${key}.md`);
11625
+ if (existsSync35(filePath)) {
11626
+ stories.push({
11627
+ key,
11628
+ filePath,
11629
+ gap: gap2,
11630
+ skipped: true,
11631
+ skipReason: "Story file already exists"
11632
+ });
11633
+ skipped++;
11634
+ continue;
11635
+ }
11636
+ const markdown = buildStoryMarkdown(gap2, key);
11637
+ mkdirSync11(dirname8(filePath), { recursive: true });
11638
+ writeFileSync19(filePath, markdown, "utf-8");
11639
+ stories.push({ key, filePath, gap: gap2, skipped: false });
11640
+ created++;
11641
+ }
11642
+ }
11643
+ return ok2({ stories, created, skipped });
11644
+ } catch (err) {
11645
+ const msg = err instanceof Error ? err.message : String(err);
11646
+ return fail2(`Failed to generate fix stories: ${msg}`);
11647
+ }
11648
+ }
11649
+ function addFixStoriesToState(stories) {
11650
+ const newStories = stories.filter((s) => !s.skipped);
11651
+ if (newStories.length === 0) {
11652
+ return ok2(void 0);
11653
+ }
11654
+ const stateResult = getSprintState2();
11655
+ if (!stateResult.success) {
11656
+ return fail2(stateResult.error);
11657
+ }
11658
+ const current = stateResult.data;
11659
+ const updatedStories = { ...current.stories };
11660
+ for (const story of newStories) {
11661
+ updatedStories[story.key] = {
11662
+ status: "backlog",
11663
+ attempts: 0,
11664
+ lastAttempt: null,
11665
+ lastError: null,
11666
+ proofPath: null,
11667
+ acResults: null
11668
+ };
11669
+ }
11670
+ const updatedSprint = computeSprintCounts2(updatedStories);
11671
+ return writeStateAtomic2({
11672
+ ...current,
11673
+ sprint: updatedSprint,
11674
+ stories: updatedStories
11675
+ });
11676
+ }
11677
+
11678
+ // src/modules/audit/index.ts
11679
+ async function runAudit2(projectDir) {
11680
+ const start = performance.now();
11681
+ const [
11682
+ obsResult,
11683
+ testResult,
11684
+ docResult,
11685
+ verifyResult,
11686
+ infraResult
11687
+ ] = await Promise.all([
11688
+ checkObservability(projectDir),
11689
+ Promise.resolve(checkTesting(projectDir)),
11690
+ Promise.resolve(checkDocumentation(projectDir)),
11691
+ Promise.resolve(checkVerification(projectDir)),
11692
+ Promise.resolve(checkInfrastructure(projectDir))
11693
+ ]);
11694
+ const dimensions = {};
11695
+ const allResults = [obsResult, testResult, docResult, verifyResult, infraResult];
11696
+ for (const result of allResults) {
11697
+ if (result.success) {
11698
+ dimensions[result.data.name] = result.data;
11699
+ }
11700
+ }
11701
+ const statuses = Object.values(dimensions).map((d) => d.status);
11702
+ const overallStatus = computeOverallStatus(statuses);
11703
+ const gapCount = Object.values(dimensions).reduce((sum, d) => sum + d.gaps.length, 0);
11704
+ const durationMs = Math.round(performance.now() - start);
11705
+ return ok2({ dimensions, overallStatus, gapCount, durationMs });
11706
+ }
11707
+ function computeOverallStatus(statuses) {
11708
+ if (statuses.includes("fail")) return "fail";
11709
+ if (statuses.includes("warn")) return "warn";
11710
+ return "pass";
11711
+ }
11712
+
11713
+ // src/commands/audit.ts
11714
+ function registerAuditCommand(program) {
11715
+ program.command("audit").description("Check all compliance dimensions and report project health").option("--json", "Output in machine-readable JSON format").option("--fix", "Generate fix stories for every gap found").action(async (opts, cmd) => {
11716
+ const globalOpts = cmd.optsWithGlobals();
11717
+ const isJson = opts.json === true || globalOpts.json === true;
11718
+ const isFix = opts.fix === true;
11719
+ const preconditions = runPreconditions();
11720
+ if (!preconditions.canProceed) {
11721
+ if (isJson) {
11722
+ jsonOutput({
11723
+ status: "fail",
11724
+ message: "Harness not initialized -- run codeharness init first"
11725
+ });
11726
+ } else {
11727
+ fail("Harness not initialized -- run codeharness init first");
11728
+ }
11729
+ process.exitCode = 1;
11730
+ return;
11731
+ }
11732
+ const result = await runAudit2(process.cwd());
11733
+ if (!result.success) {
11734
+ if (isJson) {
11735
+ jsonOutput({ status: "fail", message: result.error });
11736
+ } else {
11737
+ fail(result.error);
11738
+ }
11739
+ process.exitCode = 1;
11740
+ return;
11741
+ }
11742
+ let fixStories;
11743
+ let fixStateError;
11744
+ if (isFix) {
11745
+ if (result.data.gapCount === 0) {
11746
+ if (!isJson) {
11747
+ ok("No gaps found -- nothing to fix");
11748
+ }
11749
+ } else {
11750
+ const fixResult = generateFixStories(result.data);
11751
+ fixStories = fixResult;
11752
+ if (fixResult.success) {
11753
+ const stateResult = addFixStoriesToState(fixResult.data.stories);
11754
+ if (!stateResult.success) {
11755
+ fixStateError = stateResult.error;
11756
+ if (!isJson) {
11757
+ fail(`Failed to update sprint state: ${stateResult.error}`);
11758
+ }
11759
+ }
11760
+ if (!isJson) {
11761
+ info(`Generated ${fixResult.data.created} fix stories (${fixResult.data.skipped} skipped)`);
11762
+ }
11763
+ } else if (!isJson) {
11764
+ fail(fixResult.error);
11765
+ }
11766
+ }
11767
+ }
11768
+ if (isJson) {
11769
+ const jsonData = formatAuditJson(result.data);
11770
+ if (isFix) {
11771
+ if (result.data.gapCount === 0) {
11772
+ jsonData.fixStories = [];
11773
+ } else if (fixStories && fixStories.success) {
11774
+ jsonData.fixStories = fixStories.data.stories.map((s) => ({
11775
+ key: s.key,
11776
+ filePath: s.filePath,
11777
+ gap: s.gap,
11778
+ ...s.skipped ? { skipped: true } : {}
11779
+ }));
11780
+ if (fixStateError) {
11781
+ jsonData.fixStateError = fixStateError;
11782
+ }
11783
+ } else if (fixStories && !fixStories.success) {
11784
+ jsonData.fixStories = [];
11785
+ jsonData.fixError = fixStories.error;
11786
+ }
11787
+ }
11788
+ jsonOutput(jsonData);
11789
+ } else if (!isFix || result.data.gapCount > 0) {
11790
+ const lines = formatAuditHuman(result.data);
11791
+ for (const line of lines) {
11792
+ console.log(line);
11793
+ }
11794
+ }
11795
+ if (result.data.overallStatus === "fail") {
11796
+ process.exitCode = 1;
11797
+ }
11798
+ });
11799
+ }
11800
+
10917
11801
  // src/index.ts
10918
- var VERSION = true ? "0.22.0" : "0.0.0-dev";
11802
+ var VERSION = true ? "0.22.1" : "0.0.0-dev";
10919
11803
  function createProgram() {
10920
11804
  const program = new Command();
10921
11805
  program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
@@ -10941,6 +11825,7 @@ function createProgram() {
10941
11825
  registerValidateCommand(program);
10942
11826
  registerProgressCommand(program);
10943
11827
  registerObservabilityGateCommand(program);
11828
+ registerAuditCommand(program);
10944
11829
  return program;
10945
11830
  }
10946
11831
  if (!process.env["VITEST"]) {