codeharness 0.19.1 → 0.19.2

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
@@ -1387,7 +1387,7 @@ function getInstallCommand(stack) {
1387
1387
  }
1388
1388
 
1389
1389
  // src/commands/init.ts
1390
- var HARNESS_VERSION = true ? "0.19.1" : "0.0.0-dev";
1390
+ var HARNESS_VERSION = true ? "0.19.2" : "0.0.0-dev";
1391
1391
  function getProjectName(projectDir) {
1392
1392
  try {
1393
1393
  const pkgPath = join7(projectDir, "package.json");
@@ -1501,8 +1501,8 @@ function registerInitCommand(program) {
1501
1501
  readme: "skipped"
1502
1502
  }
1503
1503
  };
1504
- const statePath = getStatePath(projectDir);
1505
- if (existsSync7(statePath)) {
1504
+ const statePath2 = getStatePath(projectDir);
1505
+ if (existsSync7(statePath2)) {
1506
1506
  try {
1507
1507
  const existingState = readState(projectDir);
1508
1508
  const legacyObsDisabled = existingState.enforcement.observability === false;
@@ -2668,12 +2668,12 @@ function registerRunCommand(program) {
2668
2668
  cwd: projectDir,
2669
2669
  env
2670
2670
  });
2671
- const exitCode = await new Promise((resolve, reject) => {
2671
+ const exitCode = await new Promise((resolve2, reject) => {
2672
2672
  child.on("error", (err) => {
2673
2673
  reject(err);
2674
2674
  });
2675
2675
  child.on("close", (code) => {
2676
- resolve(code ?? 1);
2676
+ resolve2(code ?? 1);
2677
2677
  });
2678
2678
  });
2679
2679
  if (isJson) {
@@ -4319,8 +4319,8 @@ function printCoverageOutput(result, evaluation) {
4319
4319
 
4320
4320
  // src/lib/onboard-checks.ts
4321
4321
  function checkHarnessInitialized(dir) {
4322
- const statePath = getStatePath(dir ?? process.cwd());
4323
- return { ok: existsSync16(statePath) };
4322
+ const statePath2 = getStatePath(dir ?? process.cwd());
4323
+ return { ok: existsSync16(statePath2) };
4324
4324
  }
4325
4325
  function checkBmadInstalled(dir) {
4326
4326
  return { ok: isBmadInstalled(dir) };
@@ -4497,6 +4497,644 @@ function filterTrackedGaps(stories, beadsFns) {
4497
4497
  return { untracked, trackedCount };
4498
4498
  }
4499
4499
 
4500
+ // src/types/result.ts
4501
+ function ok2(data) {
4502
+ return { success: true, data };
4503
+ }
4504
+ function fail2(error, context) {
4505
+ if (context !== void 0) {
4506
+ return { success: false, error, context };
4507
+ }
4508
+ return { success: false, error };
4509
+ }
4510
+
4511
+ // src/modules/sprint/state.ts
4512
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync9, renameSync, existsSync as existsSync18 } from "fs";
4513
+ import { join as join16 } from "path";
4514
+
4515
+ // src/modules/sprint/migration.ts
4516
+ import { readFileSync as readFileSync15, existsSync as existsSync17 } from "fs";
4517
+ import { join as join15 } from "path";
4518
+ var OLD_FILES = {
4519
+ storyRetries: "ralph/.story_retries",
4520
+ flaggedStories: "ralph/.flagged_stories",
4521
+ ralphStatus: "ralph/status.json",
4522
+ sprintStatusYaml: "_bmad-output/implementation-artifacts/sprint-status.yaml",
4523
+ sessionIssues: "_bmad-output/implementation-artifacts/.session-issues.md"
4524
+ };
4525
+ function resolve(relative3) {
4526
+ return join15(process.cwd(), relative3);
4527
+ }
4528
+ function readIfExists(relative3) {
4529
+ const p = resolve(relative3);
4530
+ if (!existsSync17(p)) return null;
4531
+ try {
4532
+ return readFileSync15(p, "utf-8");
4533
+ } catch {
4534
+ return null;
4535
+ }
4536
+ }
4537
+ function emptyStory() {
4538
+ return {
4539
+ status: "backlog",
4540
+ attempts: 0,
4541
+ lastAttempt: null,
4542
+ lastError: null,
4543
+ proofPath: null,
4544
+ acResults: null
4545
+ };
4546
+ }
4547
+ function upsertStory(stories, key, patch) {
4548
+ stories[key] = { ...stories[key] ?? emptyStory(), ...patch };
4549
+ }
4550
+ function parseStoryRetries(content, stories) {
4551
+ for (const line of content.split("\n")) {
4552
+ const trimmed = line.trim();
4553
+ if (!trimmed) continue;
4554
+ const parts = trimmed.split(/\s+/);
4555
+ if (parts.length < 2) continue;
4556
+ const count = parseInt(parts[1], 10);
4557
+ if (!isNaN(count)) upsertStory(stories, parts[0], { attempts: count });
4558
+ }
4559
+ }
4560
+ function parseFlaggedStories(content, stories) {
4561
+ for (const line of content.split("\n")) {
4562
+ const key = line.trim();
4563
+ if (key) upsertStory(stories, key, { status: "blocked" });
4564
+ }
4565
+ }
4566
+ function mapYamlStatus(value) {
4567
+ const mapping = {
4568
+ done: "done",
4569
+ backlog: "backlog",
4570
+ verifying: "verifying",
4571
+ "in-progress": "in-progress",
4572
+ "ready-for-dev": "ready",
4573
+ blocked: "blocked",
4574
+ failed: "failed",
4575
+ review: "review",
4576
+ ready: "ready"
4577
+ };
4578
+ return mapping[value.trim().toLowerCase()] ?? null;
4579
+ }
4580
+ function parseSprintStatusYaml(content, stories) {
4581
+ for (const line of content.split("\n")) {
4582
+ const trimmed = line.trim();
4583
+ if (!trimmed || trimmed.startsWith("#")) continue;
4584
+ const match = trimmed.match(/^([a-zA-Z0-9_-]+):\s*(.+)$/);
4585
+ if (!match) continue;
4586
+ const key = match[1];
4587
+ if (key === "development_status" || key.startsWith("epic-")) continue;
4588
+ const status = mapYamlStatus(match[2]);
4589
+ if (status) upsertStory(stories, key, { status });
4590
+ }
4591
+ }
4592
+ function parseRalphStatus(content) {
4593
+ try {
4594
+ const data = JSON.parse(content);
4595
+ return {
4596
+ active: data.status === "running",
4597
+ startedAt: null,
4598
+ iteration: data.loop_count ?? 0,
4599
+ cost: 0,
4600
+ completed: [],
4601
+ failed: []
4602
+ };
4603
+ } catch {
4604
+ return null;
4605
+ }
4606
+ }
4607
+ function parseSessionIssues(content) {
4608
+ const items = [];
4609
+ let currentStory = "";
4610
+ let itemId = 0;
4611
+ for (const line of content.split("\n")) {
4612
+ const headerMatch = line.match(/^###\s+([a-zA-Z0-9_-]+)\s*[—-]/);
4613
+ if (headerMatch) {
4614
+ currentStory = headerMatch[1];
4615
+ continue;
4616
+ }
4617
+ const bulletMatch = line.match(/^-\s+(.+)/);
4618
+ if (bulletMatch && currentStory) {
4619
+ itemId++;
4620
+ items.push({
4621
+ id: `migrated-${itemId}`,
4622
+ story: currentStory,
4623
+ description: bulletMatch[1],
4624
+ source: "retro",
4625
+ resolved: false
4626
+ });
4627
+ }
4628
+ }
4629
+ return items;
4630
+ }
4631
+ function migrateFromOldFormat() {
4632
+ const hasAnyOldFile = Object.values(OLD_FILES).some((rel) => existsSync17(resolve(rel)));
4633
+ if (!hasAnyOldFile) return fail2("No old format files found for migration");
4634
+ try {
4635
+ const stories = {};
4636
+ let run = defaultState().run;
4637
+ let actionItems = [];
4638
+ const yamlContent = readIfExists(OLD_FILES.sprintStatusYaml);
4639
+ if (yamlContent) parseSprintStatusYaml(yamlContent, stories);
4640
+ const retriesContent = readIfExists(OLD_FILES.storyRetries);
4641
+ if (retriesContent) parseStoryRetries(retriesContent, stories);
4642
+ const flaggedContent = readIfExists(OLD_FILES.flaggedStories);
4643
+ if (flaggedContent) parseFlaggedStories(flaggedContent, stories);
4644
+ const statusContent = readIfExists(OLD_FILES.ralphStatus);
4645
+ if (statusContent) {
4646
+ const parsed = parseRalphStatus(statusContent);
4647
+ if (parsed) run = parsed;
4648
+ }
4649
+ const issuesContent = readIfExists(OLD_FILES.sessionIssues);
4650
+ if (issuesContent) actionItems = parseSessionIssues(issuesContent);
4651
+ const sprint = computeSprintCounts(stories);
4652
+ const migrated = {
4653
+ version: 1,
4654
+ sprint,
4655
+ stories,
4656
+ run,
4657
+ actionItems
4658
+ };
4659
+ const writeResult = writeStateAtomic(migrated);
4660
+ if (!writeResult.success) return fail2(writeResult.error);
4661
+ return ok2(migrated);
4662
+ } catch (err) {
4663
+ const msg = err instanceof Error ? err.message : String(err);
4664
+ return fail2(`Migration failed: ${msg}`);
4665
+ }
4666
+ }
4667
+
4668
+ // src/modules/sprint/state.ts
4669
+ function projectRoot() {
4670
+ return process.cwd();
4671
+ }
4672
+ function statePath() {
4673
+ return join16(projectRoot(), "sprint-state.json");
4674
+ }
4675
+ function tmpPath() {
4676
+ return join16(projectRoot(), ".sprint-state.json.tmp");
4677
+ }
4678
+ function defaultState() {
4679
+ return {
4680
+ version: 1,
4681
+ sprint: {
4682
+ total: 0,
4683
+ done: 0,
4684
+ failed: 0,
4685
+ blocked: 0,
4686
+ inProgress: null
4687
+ },
4688
+ stories: {},
4689
+ run: {
4690
+ active: false,
4691
+ startedAt: null,
4692
+ iteration: 0,
4693
+ cost: 0,
4694
+ completed: [],
4695
+ failed: []
4696
+ },
4697
+ actionItems: []
4698
+ };
4699
+ }
4700
+ function writeStateAtomic(state) {
4701
+ try {
4702
+ const data = JSON.stringify(state, null, 2) + "\n";
4703
+ const tmp = tmpPath();
4704
+ const final = statePath();
4705
+ writeFileSync9(tmp, data, "utf-8");
4706
+ renameSync(tmp, final);
4707
+ return ok2(void 0);
4708
+ } catch (err) {
4709
+ const msg = err instanceof Error ? err.message : String(err);
4710
+ return fail2(`Failed to write sprint state: ${msg}`);
4711
+ }
4712
+ }
4713
+ function getSprintState() {
4714
+ const fp = statePath();
4715
+ if (existsSync18(fp)) {
4716
+ try {
4717
+ const raw = readFileSync16(fp, "utf-8");
4718
+ const parsed = JSON.parse(raw);
4719
+ return ok2(parsed);
4720
+ } catch (err) {
4721
+ const msg = err instanceof Error ? err.message : String(err);
4722
+ return fail2(`Failed to read sprint state: ${msg}`);
4723
+ }
4724
+ }
4725
+ const migrationResult = migrateFromOldFormat();
4726
+ if (migrationResult.success) {
4727
+ return migrationResult;
4728
+ }
4729
+ return ok2(defaultState());
4730
+ }
4731
+ function computeSprintCounts(stories) {
4732
+ let total = 0;
4733
+ let done = 0;
4734
+ let failed = 0;
4735
+ let blocked = 0;
4736
+ let inProgress = null;
4737
+ for (const [key, story] of Object.entries(stories)) {
4738
+ total++;
4739
+ if (story.status === "done") done++;
4740
+ else if (story.status === "failed") failed++;
4741
+ else if (story.status === "blocked") blocked++;
4742
+ else if (story.status === "in-progress") inProgress = key;
4743
+ }
4744
+ return { total, done, failed, blocked, inProgress };
4745
+ }
4746
+
4747
+ // src/modules/sprint/selector.ts
4748
+ var MAX_STORY_ATTEMPTS = 10;
4749
+
4750
+ // src/modules/sprint/drill-down.ts
4751
+ var MAX_ATTEMPTS = MAX_STORY_ATTEMPTS;
4752
+ function epicPrefix(key) {
4753
+ const dashIdx = key.indexOf("-");
4754
+ if (dashIdx === -1) return key;
4755
+ return key.slice(0, dashIdx);
4756
+ }
4757
+ function buildAcDetails(story) {
4758
+ if (!story.acResults) return [];
4759
+ return story.acResults.map((ac) => {
4760
+ const detail = { id: ac.id, verdict: ac.verdict };
4761
+ if (ac.verdict === "fail" && story.lastError) {
4762
+ return { ...detail, reason: story.lastError };
4763
+ }
4764
+ return detail;
4765
+ });
4766
+ }
4767
+ function buildAttemptHistory(story) {
4768
+ const records = [];
4769
+ if (story.attempts === 0) return records;
4770
+ for (let i = 1; i < story.attempts; i++) {
4771
+ records.push({
4772
+ number: i,
4773
+ outcome: "details unavailable"
4774
+ });
4775
+ }
4776
+ const lastOutcome = story.status === "done" ? "passed" : story.status === "failed" ? "verify failed" : story.status === "blocked" ? "blocked" : story.status;
4777
+ const lastRecord = {
4778
+ number: story.attempts,
4779
+ outcome: lastOutcome,
4780
+ ...story.lastAttempt ? { timestamp: story.lastAttempt } : {}
4781
+ };
4782
+ if (story.acResults) {
4783
+ const failingAc = story.acResults.find((ac) => ac.verdict === "fail");
4784
+ if (failingAc) {
4785
+ records.push({ ...lastRecord, failingAc: failingAc.id });
4786
+ return records;
4787
+ }
4788
+ }
4789
+ records.push(lastRecord);
4790
+ return records;
4791
+ }
4792
+ function buildProofSummary(story) {
4793
+ if (!story.proofPath) return null;
4794
+ let passCount = 0;
4795
+ let failCount = 0;
4796
+ let escalateCount = 0;
4797
+ let pendingCount = 0;
4798
+ if (story.acResults) {
4799
+ for (const ac of story.acResults) {
4800
+ if (ac.verdict === "pass") passCount++;
4801
+ else if (ac.verdict === "fail") failCount++;
4802
+ else if (ac.verdict === "escalate") escalateCount++;
4803
+ else if (ac.verdict === "pending") pendingCount++;
4804
+ }
4805
+ }
4806
+ return { path: story.proofPath, passCount, failCount, escalateCount, pendingCount };
4807
+ }
4808
+ function getStoryDrillDown(state, key) {
4809
+ try {
4810
+ const story = state.stories[key];
4811
+ if (!story) {
4812
+ return fail2(`Story '${key}' not found in sprint state`);
4813
+ }
4814
+ const epic = epicPrefix(key);
4815
+ const acDetails = buildAcDetails(story);
4816
+ const attemptHistory = buildAttemptHistory(story);
4817
+ const proofSummary = buildProofSummary(story);
4818
+ return ok2({
4819
+ key,
4820
+ status: story.status,
4821
+ epic,
4822
+ attempts: story.attempts,
4823
+ maxAttempts: MAX_ATTEMPTS,
4824
+ lastAttempt: story.lastAttempt,
4825
+ acDetails,
4826
+ attemptHistory,
4827
+ proofSummary
4828
+ });
4829
+ } catch (err) {
4830
+ const msg = err instanceof Error ? err.message : String(err);
4831
+ return fail2(`Failed to get story drill-down: ${msg}`);
4832
+ }
4833
+ }
4834
+
4835
+ // src/modules/sprint/reporter.ts
4836
+ var MAX_ATTEMPTS2 = MAX_STORY_ATTEMPTS;
4837
+ function formatDuration(ms) {
4838
+ const clamped = Math.max(0, ms);
4839
+ const totalMinutes = Math.floor(clamped / 6e4);
4840
+ const hours = Math.floor(totalMinutes / 60);
4841
+ const minutes = totalMinutes % 60;
4842
+ if (hours > 0) return `${hours}h${minutes}m`;
4843
+ return `${minutes}m`;
4844
+ }
4845
+ function computeEpicProgress(stories) {
4846
+ const epicGroups = /* @__PURE__ */ new Map();
4847
+ for (const [key, story] of Object.entries(stories)) {
4848
+ const prefix = epicPrefix(key);
4849
+ const group = epicGroups.get(prefix) ?? { total: 0, done: 0 };
4850
+ group.total++;
4851
+ if (story.status === "done") group.done++;
4852
+ epicGroups.set(prefix, group);
4853
+ }
4854
+ let epicsTotal = 0;
4855
+ let epicsDone = 0;
4856
+ for (const group of epicGroups.values()) {
4857
+ epicsTotal++;
4858
+ if (group.total > 0 && group.done === group.total) epicsDone++;
4859
+ }
4860
+ return { epicsTotal, epicsDone };
4861
+ }
4862
+ function buildFailedDetails(stories) {
4863
+ const details = [];
4864
+ for (const [key, story] of Object.entries(stories)) {
4865
+ if (story.status !== "failed") continue;
4866
+ let acNumber = null;
4867
+ if (story.acResults) {
4868
+ for (const ac of story.acResults) {
4869
+ if (ac.verdict === "fail") {
4870
+ const num = parseInt(ac.id.replace(/\D/g, ""), 10);
4871
+ if (!isNaN(num)) {
4872
+ acNumber = num;
4873
+ break;
4874
+ }
4875
+ }
4876
+ }
4877
+ }
4878
+ details.push({
4879
+ key,
4880
+ acNumber,
4881
+ errorLine: story.lastError ?? "unknown error",
4882
+ attempts: story.attempts,
4883
+ maxAttempts: MAX_ATTEMPTS2
4884
+ });
4885
+ }
4886
+ return details;
4887
+ }
4888
+ function buildActionItemsLabeled(state) {
4889
+ const runStories = /* @__PURE__ */ new Set([
4890
+ ...state.run.completed,
4891
+ ...state.run.failed
4892
+ ]);
4893
+ return state.actionItems.map((item) => {
4894
+ const isNew = item.source === "verification" && runStories.has(item.story);
4895
+ return { item, label: isNew ? "NEW" : "CARRIED" };
4896
+ });
4897
+ }
4898
+ function buildRunSummary(state, now) {
4899
+ if (!state.run.startedAt) return null;
4900
+ const startedAt = new Date(state.run.startedAt);
4901
+ const elapsed = now.getTime() - startedAt.getTime();
4902
+ const blocked = [];
4903
+ const skipped = [];
4904
+ for (const [key, story] of Object.entries(state.stories)) {
4905
+ if (story.status === "blocked") {
4906
+ blocked.push(key);
4907
+ if (story.attempts >= MAX_ATTEMPTS2) {
4908
+ skipped.push(key);
4909
+ }
4910
+ }
4911
+ }
4912
+ return {
4913
+ duration: formatDuration(elapsed),
4914
+ cost: state.run.cost,
4915
+ iterations: state.run.iteration,
4916
+ completed: [...state.run.completed],
4917
+ failed: [...state.run.failed],
4918
+ blocked,
4919
+ skipped
4920
+ };
4921
+ }
4922
+ function generateReport(state, now) {
4923
+ try {
4924
+ const effectiveNow = now ?? /* @__PURE__ */ new Date();
4925
+ const stories = state.stories;
4926
+ let total = 0;
4927
+ let done = 0;
4928
+ let failed = 0;
4929
+ let blocked = 0;
4930
+ const storyStatuses = [];
4931
+ for (const [key, story] of Object.entries(stories)) {
4932
+ total++;
4933
+ if (story.status === "done") done++;
4934
+ else if (story.status === "failed") failed++;
4935
+ else if (story.status === "blocked") blocked++;
4936
+ storyStatuses.push({ key, status: story.status });
4937
+ }
4938
+ const sprintPercent = total > 0 ? Math.round(done / total * 100) : 0;
4939
+ const { epicsTotal, epicsDone } = computeEpicProgress(stories);
4940
+ const runSummary = buildRunSummary(state, effectiveNow);
4941
+ const activeRun = state.run.active ? runSummary : null;
4942
+ const lastRun = !state.run.active ? runSummary : null;
4943
+ const failedDetails = buildFailedDetails(stories);
4944
+ const actionItemsLabeled = buildActionItemsLabeled(state);
4945
+ return ok2({
4946
+ total,
4947
+ done,
4948
+ failed,
4949
+ blocked,
4950
+ inProgress: state.sprint.inProgress,
4951
+ storyStatuses,
4952
+ epicsTotal,
4953
+ epicsDone,
4954
+ sprintPercent,
4955
+ activeRun,
4956
+ lastRun,
4957
+ failedDetails,
4958
+ actionItemsLabeled
4959
+ });
4960
+ } catch (err) {
4961
+ const msg = err instanceof Error ? err.message : String(err);
4962
+ return fail2(`Failed to generate report: ${msg}`);
4963
+ }
4964
+ }
4965
+
4966
+ // src/modules/sprint/timeout.ts
4967
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync10, existsSync as existsSync19, mkdirSync as mkdirSync6 } from "fs";
4968
+ import { execSync as execSync3 } from "child_process";
4969
+ import { join as join17 } from "path";
4970
+ var GIT_TIMEOUT_MS = 5e3;
4971
+ var DEFAULT_MAX_LINES = 100;
4972
+ function captureGitDiff() {
4973
+ try {
4974
+ const unstaged = execSync3("git diff --stat", {
4975
+ timeout: GIT_TIMEOUT_MS,
4976
+ encoding: "utf-8",
4977
+ stdio: ["pipe", "pipe", "pipe"]
4978
+ }).trim();
4979
+ const staged = execSync3("git diff --cached --stat", {
4980
+ timeout: GIT_TIMEOUT_MS,
4981
+ encoding: "utf-8",
4982
+ stdio: ["pipe", "pipe", "pipe"]
4983
+ }).trim();
4984
+ const parts = [];
4985
+ if (unstaged) parts.push("Unstaged:\n" + unstaged);
4986
+ if (staged) parts.push("Staged:\n" + staged);
4987
+ if (parts.length === 0) {
4988
+ return ok2("No changes detected");
4989
+ }
4990
+ return ok2(parts.join("\n\n"));
4991
+ } catch (err) {
4992
+ const msg = err instanceof Error ? err.message : String(err);
4993
+ return fail2(`Failed to capture git diff: ${msg}`);
4994
+ }
4995
+ }
4996
+ function captureStateDelta(beforePath, afterPath) {
4997
+ try {
4998
+ if (!existsSync19(beforePath)) {
4999
+ return fail2(`State snapshot not found: ${beforePath}`);
5000
+ }
5001
+ if (!existsSync19(afterPath)) {
5002
+ return fail2(`Current state file not found: ${afterPath}`);
5003
+ }
5004
+ const beforeRaw = readFileSync17(beforePath, "utf-8");
5005
+ const afterRaw = readFileSync17(afterPath, "utf-8");
5006
+ const before = JSON.parse(beforeRaw);
5007
+ const after = JSON.parse(afterRaw);
5008
+ const beforeStories = before.stories ?? {};
5009
+ const afterStories = after.stories ?? {};
5010
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(beforeStories), ...Object.keys(afterStories)]);
5011
+ const changes = [];
5012
+ for (const key of allKeys) {
5013
+ const beforeStatus = beforeStories[key]?.status ?? "(absent)";
5014
+ const afterStatus = afterStories[key]?.status ?? "(absent)";
5015
+ if (beforeStatus !== afterStatus) {
5016
+ changes.push(`${key}: ${beforeStatus} \u2192 ${afterStatus}`);
5017
+ }
5018
+ }
5019
+ if (changes.length === 0) {
5020
+ return ok2("No state changes");
5021
+ }
5022
+ return ok2(changes.join("\n"));
5023
+ } catch (err) {
5024
+ const msg = err instanceof Error ? err.message : String(err);
5025
+ return fail2(`Failed to capture state delta: ${msg}`);
5026
+ }
5027
+ }
5028
+ function capturePartialStderr(outputFile, maxLines = DEFAULT_MAX_LINES) {
5029
+ try {
5030
+ if (!existsSync19(outputFile)) {
5031
+ return fail2(`Output file not found: ${outputFile}`);
5032
+ }
5033
+ const content = readFileSync17(outputFile, "utf-8");
5034
+ const lines = content.split("\n");
5035
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
5036
+ lines.pop();
5037
+ }
5038
+ const lastLines = lines.slice(-maxLines).join("\n");
5039
+ return ok2(lastLines);
5040
+ } catch (err) {
5041
+ const msg = err instanceof Error ? err.message : String(err);
5042
+ return fail2(`Failed to capture partial stderr: ${msg}`);
5043
+ }
5044
+ }
5045
+ function formatReport(capture) {
5046
+ const lines = [
5047
+ `# Timeout Report: Iteration ${capture.iteration}`,
5048
+ "",
5049
+ `- **Story:** ${capture.storyKey}`,
5050
+ `- **Duration:** ${capture.durationMinutes} minutes (timeout)`,
5051
+ `- **Timestamp:** ${capture.timestamp}`,
5052
+ "",
5053
+ "## Git Changes",
5054
+ "",
5055
+ capture.gitDiff,
5056
+ "",
5057
+ "## State Delta",
5058
+ "",
5059
+ capture.stateDelta,
5060
+ "",
5061
+ "## Partial Output (last 100 lines)",
5062
+ "",
5063
+ "```",
5064
+ capture.partialStderr,
5065
+ "```",
5066
+ ""
5067
+ ];
5068
+ return lines.join("\n");
5069
+ }
5070
+ function captureTimeoutReport(opts) {
5071
+ try {
5072
+ if (opts.iteration < 1 || !Number.isInteger(opts.iteration)) {
5073
+ return fail2(`Invalid iteration number: ${opts.iteration}`);
5074
+ }
5075
+ if (opts.durationMinutes < 0) {
5076
+ return fail2(`Invalid duration: ${opts.durationMinutes}`);
5077
+ }
5078
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
5079
+ const gitResult = captureGitDiff();
5080
+ const gitDiff = gitResult.success ? gitResult.data : `(unavailable: ${gitResult.error})`;
5081
+ const statePath2 = join17(process.cwd(), "sprint-state.json");
5082
+ const deltaResult = captureStateDelta(opts.stateSnapshotPath, statePath2);
5083
+ const stateDelta = deltaResult.success ? deltaResult.data : `(unavailable: ${deltaResult.error})`;
5084
+ const stderrResult = capturePartialStderr(opts.outputFile);
5085
+ const partialStderr = stderrResult.success ? stderrResult.data : `(unavailable: ${stderrResult.error})`;
5086
+ const capture = {
5087
+ storyKey: opts.storyKey,
5088
+ iteration: opts.iteration,
5089
+ durationMinutes: opts.durationMinutes,
5090
+ gitDiff,
5091
+ stateDelta,
5092
+ partialStderr,
5093
+ timestamp
5094
+ };
5095
+ const reportDir = join17(process.cwd(), "ralph", "logs");
5096
+ const safeStoryKey = opts.storyKey.replace(/[^a-zA-Z0-9._-]/g, "_");
5097
+ const reportFileName = `timeout-report-${opts.iteration}-${safeStoryKey}.md`;
5098
+ const reportPath = join17(reportDir, reportFileName);
5099
+ if (!existsSync19(reportDir)) {
5100
+ mkdirSync6(reportDir, { recursive: true });
5101
+ }
5102
+ const reportContent = formatReport(capture);
5103
+ writeFileSync10(reportPath, reportContent, "utf-8");
5104
+ return ok2({
5105
+ filePath: reportPath,
5106
+ capture
5107
+ });
5108
+ } catch (err) {
5109
+ const msg = err instanceof Error ? err.message : String(err);
5110
+ return fail2(`Failed to capture timeout report: ${msg}`);
5111
+ }
5112
+ }
5113
+
5114
+ // src/modules/sprint/feedback.ts
5115
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
5116
+ import { existsSync as existsSync20 } from "fs";
5117
+ import { join as join18 } from "path";
5118
+
5119
+ // src/modules/sprint/index.ts
5120
+ function generateReport2() {
5121
+ const stateResult = getSprintState();
5122
+ if (!stateResult.success) {
5123
+ return fail2(stateResult.error);
5124
+ }
5125
+ return generateReport(stateResult.data);
5126
+ }
5127
+ function getStoryDrillDown2(key) {
5128
+ const stateResult = getSprintState();
5129
+ if (!stateResult.success) {
5130
+ return fail2(stateResult.error);
5131
+ }
5132
+ return getStoryDrillDown(stateResult.data, key);
5133
+ }
5134
+ function captureTimeoutReport2(opts) {
5135
+ return captureTimeoutReport(opts);
5136
+ }
5137
+
4500
5138
  // src/commands/status.ts
4501
5139
  function buildScopedEndpoints(endpoints, serviceName) {
4502
5140
  const encoded = encodeURIComponent(serviceName);
@@ -4513,9 +5151,13 @@ var DEFAULT_ENDPOINTS = {
4513
5151
  otel_http: "http://localhost:4318"
4514
5152
  };
4515
5153
  function registerStatusCommand(program) {
4516
- program.command("status").description("Show current harness status and health").option("--check-docker", "Check Docker stack health").option("--check", "Run health checks with pass/fail exit code").action(async (options, cmd) => {
5154
+ program.command("status").description("Show current harness status and health").option("--check-docker", "Check Docker stack health").option("--check", "Run health checks with pass/fail exit code").option("--story <id>", "Show detailed status for a specific story").action(async (options, cmd) => {
4517
5155
  const opts = cmd.optsWithGlobals();
4518
5156
  const isJson = opts.json === true;
5157
+ if (options.story) {
5158
+ handleStoryDrillDown(options.story, isJson);
5159
+ return;
5160
+ }
4519
5161
  if (options.checkDocker) {
4520
5162
  await handleDockerCheck(isJson);
4521
5163
  return;
@@ -4547,6 +5189,7 @@ function handleFullStatus(isJson) {
4547
5189
  handleFullStatusJson(state);
4548
5190
  return;
4549
5191
  }
5192
+ printSprintState();
4550
5193
  console.log(`Harness: codeharness v${state.harness_version}`);
4551
5194
  console.log(`Stack: ${state.stack ?? "unknown"}`);
4552
5195
  if (state.app_type) {
@@ -4683,10 +5326,12 @@ function handleFullStatusJson(state) {
4683
5326
  const scoped_endpoints = serviceName ? buildScopedEndpoints(endpoints, serviceName) : void 0;
4684
5327
  const beads = getBeadsData();
4685
5328
  const onboarding = getOnboardingProgressData();
5329
+ const sprint = getSprintReportData();
4686
5330
  jsonOutput({
4687
5331
  version: state.harness_version,
4688
5332
  stack: state.stack,
4689
5333
  ...state.app_type ? { app_type: state.app_type } : {},
5334
+ ...sprint ? { sprint } : {},
4690
5335
  enforcement: state.enforcement,
4691
5336
  docker,
4692
5337
  endpoints,
@@ -4878,6 +5523,116 @@ async function handleDockerCheck(isJson) {
4878
5523
  }
4879
5524
  }
4880
5525
  }
5526
+ function handleStoryDrillDown(storyId, isJson) {
5527
+ const result = getStoryDrillDown2(storyId);
5528
+ if (!result.success) {
5529
+ if (isJson) {
5530
+ jsonOutput({ status: "fail", message: result.error });
5531
+ } else {
5532
+ fail(result.error);
5533
+ }
5534
+ process.exitCode = 1;
5535
+ return;
5536
+ }
5537
+ const d = result.data;
5538
+ if (isJson) {
5539
+ jsonOutput({
5540
+ key: d.key,
5541
+ status: d.status,
5542
+ epic: d.epic,
5543
+ attempts: d.attempts,
5544
+ maxAttempts: d.maxAttempts,
5545
+ lastAttempt: d.lastAttempt,
5546
+ acResults: d.acDetails,
5547
+ attemptHistory: d.attemptHistory,
5548
+ proof: d.proofSummary
5549
+ });
5550
+ return;
5551
+ }
5552
+ console.log(`Story: ${d.key}`);
5553
+ console.log(`Status: ${d.status} (attempt ${d.attempts}/${d.maxAttempts})`);
5554
+ console.log(`Epic: ${d.epic}`);
5555
+ console.log(`Last attempt: ${d.lastAttempt ?? "none"}`);
5556
+ console.log("");
5557
+ console.log("-- AC Results -------------------------------------------------------");
5558
+ if (d.acDetails.length === 0) {
5559
+ console.log("No AC results recorded");
5560
+ } else {
5561
+ for (const ac of d.acDetails) {
5562
+ const tag = ac.verdict.toUpperCase();
5563
+ console.log(`${ac.id}: [${tag}]`);
5564
+ if (ac.verdict === "fail") {
5565
+ if (ac.command) console.log(` Command: ${ac.command}`);
5566
+ if (ac.expected) console.log(` Expected: ${ac.expected}`);
5567
+ if (ac.actual) console.log(` Actual: ${ac.actual}`);
5568
+ if (ac.reason) console.log(` Reason: ${ac.reason}`);
5569
+ if (ac.suggestedFix) console.log(` Suggest: ${ac.suggestedFix}`);
5570
+ }
5571
+ }
5572
+ }
5573
+ if (d.attemptHistory.length > 0) {
5574
+ console.log("");
5575
+ console.log("-- History ----------------------------------------------------------");
5576
+ for (const attempt of d.attemptHistory) {
5577
+ const acPart = attempt.failingAc ? ` (${attempt.failingAc})` : "";
5578
+ console.log(`Attempt ${attempt.number}: ${attempt.outcome}${acPart}`);
5579
+ }
5580
+ }
5581
+ if (d.proofSummary) {
5582
+ console.log("");
5583
+ const p = d.proofSummary;
5584
+ const total = p.passCount + p.failCount + p.escalateCount + p.pendingCount;
5585
+ console.log(
5586
+ `Proof: ${p.path} (${p.passCount}/${total} pass, ${p.failCount} fail, ${p.escalateCount} escalate)`
5587
+ );
5588
+ }
5589
+ }
5590
+ function printSprintState() {
5591
+ const reportResult = generateReport2();
5592
+ if (!reportResult.success) {
5593
+ console.log("Sprint state: unavailable");
5594
+ return;
5595
+ }
5596
+ const r = reportResult.data;
5597
+ console.log(`\u2500\u2500 Project State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
5598
+ console.log(`Sprint: ${r.done}/${r.total} done (${r.sprintPercent}%) | ${r.epicsDone}/${r.epicsTotal} epics complete`);
5599
+ if (r.activeRun) {
5600
+ console.log("");
5601
+ console.log(`\u2500\u2500 Active Run \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
5602
+ const currentStory = r.inProgress ?? "none";
5603
+ console.log(`Status: running (iteration ${r.activeRun.iterations}, ${r.activeRun.duration} elapsed)`);
5604
+ console.log(`Current: ${currentStory}`);
5605
+ console.log(`Budget: $${r.activeRun.cost.toFixed(2)} spent`);
5606
+ } else if (r.lastRun) {
5607
+ console.log("");
5608
+ console.log(`\u2500\u2500 Last Run Summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
5609
+ console.log(`Duration: ${r.lastRun.duration} | Cost: $${r.lastRun.cost.toFixed(2)} | Iterations: ${r.lastRun.iterations}`);
5610
+ console.log(`Completed: ${r.lastRun.completed.length} stories${r.lastRun.completed.length > 0 ? ` (${r.lastRun.completed.join(", ")})` : ""}`);
5611
+ if (r.failedDetails.length > 0) {
5612
+ console.log(`Failed: ${r.failedDetails.length} stor${r.failedDetails.length === 1 ? "y" : "ies"}`);
5613
+ for (const fd of r.failedDetails) {
5614
+ const acPart = fd.acNumber !== null ? `AC ${fd.acNumber}` : "unknown AC";
5615
+ console.log(` \u2514 ${fd.key}: ${acPart} \u2014 ${fd.errorLine} (attempt ${fd.attempts}/${fd.maxAttempts})`);
5616
+ }
5617
+ }
5618
+ if (r.lastRun.blocked.length > 0) {
5619
+ console.log(`Blocked: ${r.lastRun.blocked.length} stories (retry-exhausted)`);
5620
+ }
5621
+ }
5622
+ if (r.actionItemsLabeled.length > 0) {
5623
+ console.log("");
5624
+ console.log(`\u2500\u2500 Action Items \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
5625
+ for (const la of r.actionItemsLabeled) {
5626
+ console.log(` [${la.label}] ${la.item.story}: ${la.item.description}`);
5627
+ }
5628
+ }
5629
+ console.log("");
5630
+ }
5631
+ function getSprintReportData() {
5632
+ const reportResult = generateReport2();
5633
+ if (!reportResult.success) return null;
5634
+ return reportResult.data;
5635
+ }
4881
5636
  function printBeadsSummary() {
4882
5637
  if (!isBeadsInitialized()) {
4883
5638
  console.log("Beads: not initialized");
@@ -4944,16 +5699,16 @@ function getBeadsData() {
4944
5699
  }
4945
5700
 
4946
5701
  // src/commands/onboard.ts
4947
- import { join as join18 } from "path";
5702
+ import { join as join22 } from "path";
4948
5703
 
4949
5704
  // src/lib/scanner.ts
4950
5705
  import {
4951
- existsSync as existsSync17,
5706
+ existsSync as existsSync21,
4952
5707
  readdirSync as readdirSync3,
4953
- readFileSync as readFileSync15,
5708
+ readFileSync as readFileSync19,
4954
5709
  statSync as statSync2
4955
5710
  } from "fs";
4956
- import { join as join15, relative as relative2 } from "path";
5711
+ import { join as join19, relative as relative2 } from "path";
4957
5712
  var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
4958
5713
  var DEFAULT_MIN_MODULE_SIZE = 3;
4959
5714
  function getExtension2(filename) {
@@ -4978,7 +5733,7 @@ function countSourceFiles(dir) {
4978
5733
  for (const entry of entries) {
4979
5734
  if (isSkippedDir(entry)) continue;
4980
5735
  if (entry.startsWith(".") && current !== dir) continue;
4981
- const fullPath = join15(current, entry);
5736
+ const fullPath = join19(current, entry);
4982
5737
  let stat;
4983
5738
  try {
4984
5739
  stat = statSync2(fullPath);
@@ -5001,7 +5756,7 @@ function countSourceFiles(dir) {
5001
5756
  return count;
5002
5757
  }
5003
5758
  function countModuleFiles(modulePath, rootDir) {
5004
- const fullModulePath = join15(rootDir, modulePath);
5759
+ const fullModulePath = join19(rootDir, modulePath);
5005
5760
  let sourceFiles = 0;
5006
5761
  let testFiles = 0;
5007
5762
  function walk(current) {
@@ -5013,7 +5768,7 @@ function countModuleFiles(modulePath, rootDir) {
5013
5768
  }
5014
5769
  for (const entry of entries) {
5015
5770
  if (isSkippedDir(entry)) continue;
5016
- const fullPath = join15(current, entry);
5771
+ const fullPath = join19(current, entry);
5017
5772
  let stat;
5018
5773
  try {
5019
5774
  stat = statSync2(fullPath);
@@ -5038,8 +5793,8 @@ function countModuleFiles(modulePath, rootDir) {
5038
5793
  return { sourceFiles, testFiles };
5039
5794
  }
5040
5795
  function detectArtifacts(dir) {
5041
- const bmadPath = join15(dir, "_bmad");
5042
- const hasBmad = existsSync17(bmadPath);
5796
+ const bmadPath = join19(dir, "_bmad");
5797
+ const hasBmad = existsSync21(bmadPath);
5043
5798
  return {
5044
5799
  hasBmad,
5045
5800
  bmadPath: hasBmad ? relative2(dir, bmadPath) || "_bmad" : null
@@ -5121,10 +5876,10 @@ function readPerFileCoverage(dir, format) {
5121
5876
  return null;
5122
5877
  }
5123
5878
  function readVitestPerFileCoverage(dir) {
5124
- const reportPath = join15(dir, "coverage", "coverage-summary.json");
5125
- if (!existsSync17(reportPath)) return null;
5879
+ const reportPath = join19(dir, "coverage", "coverage-summary.json");
5880
+ if (!existsSync21(reportPath)) return null;
5126
5881
  try {
5127
- const report = JSON.parse(readFileSync15(reportPath, "utf-8"));
5882
+ const report = JSON.parse(readFileSync19(reportPath, "utf-8"));
5128
5883
  const result = /* @__PURE__ */ new Map();
5129
5884
  for (const [key, value] of Object.entries(report)) {
5130
5885
  if (key === "total") continue;
@@ -5136,10 +5891,10 @@ function readVitestPerFileCoverage(dir) {
5136
5891
  }
5137
5892
  }
5138
5893
  function readPythonPerFileCoverage(dir) {
5139
- const reportPath = join15(dir, "coverage.json");
5140
- if (!existsSync17(reportPath)) return null;
5894
+ const reportPath = join19(dir, "coverage.json");
5895
+ if (!existsSync21(reportPath)) return null;
5141
5896
  try {
5142
- const report = JSON.parse(readFileSync15(reportPath, "utf-8"));
5897
+ const report = JSON.parse(readFileSync19(reportPath, "utf-8"));
5143
5898
  if (!report.files) return null;
5144
5899
  const result = /* @__PURE__ */ new Map();
5145
5900
  for (const [key, value] of Object.entries(report.files)) {
@@ -5155,13 +5910,13 @@ function auditDocumentation(dir) {
5155
5910
  const root = dir ?? process.cwd();
5156
5911
  const documents = [];
5157
5912
  for (const docName of AUDIT_DOCUMENTS) {
5158
- const docPath = join15(root, docName);
5159
- if (!existsSync17(docPath)) {
5913
+ const docPath = join19(root, docName);
5914
+ if (!existsSync21(docPath)) {
5160
5915
  documents.push({ name: docName, grade: "missing", path: null });
5161
5916
  continue;
5162
5917
  }
5163
- const srcDir = join15(root, "src");
5164
- const codeDir = existsSync17(srcDir) ? srcDir : root;
5918
+ const srcDir = join19(root, "src");
5919
+ const codeDir = existsSync21(srcDir) ? srcDir : root;
5165
5920
  const stale = isDocStale(docPath, codeDir);
5166
5921
  documents.push({
5167
5922
  name: docName,
@@ -5169,8 +5924,8 @@ function auditDocumentation(dir) {
5169
5924
  path: docName
5170
5925
  });
5171
5926
  }
5172
- const docsDir = join15(root, "docs");
5173
- if (existsSync17(docsDir)) {
5927
+ const docsDir = join19(root, "docs");
5928
+ if (existsSync21(docsDir)) {
5174
5929
  try {
5175
5930
  const stat = statSync2(docsDir);
5176
5931
  if (stat.isDirectory()) {
@@ -5182,10 +5937,10 @@ function auditDocumentation(dir) {
5182
5937
  } else {
5183
5938
  documents.push({ name: "docs/", grade: "missing", path: null });
5184
5939
  }
5185
- const indexPath = join15(root, "docs", "index.md");
5186
- if (existsSync17(indexPath)) {
5187
- const srcDir = join15(root, "src");
5188
- const indexCodeDir = existsSync17(srcDir) ? srcDir : root;
5940
+ const indexPath = join19(root, "docs", "index.md");
5941
+ if (existsSync21(indexPath)) {
5942
+ const srcDir = join19(root, "src");
5943
+ const indexCodeDir = existsSync21(srcDir) ? srcDir : root;
5189
5944
  const indexStale = isDocStale(indexPath, indexCodeDir);
5190
5945
  documents.push({
5191
5946
  name: "docs/index.md",
@@ -5202,8 +5957,8 @@ function auditDocumentation(dir) {
5202
5957
 
5203
5958
  // src/lib/epic-generator.ts
5204
5959
  import { createInterface } from "readline";
5205
- import { existsSync as existsSync18, mkdirSync as mkdirSync6, writeFileSync as writeFileSync9 } from "fs";
5206
- import { dirname as dirname5, join as join16 } from "path";
5960
+ import { existsSync as existsSync22, mkdirSync as mkdirSync7, writeFileSync as writeFileSync12 } from "fs";
5961
+ import { dirname as dirname6, join as join20 } from "path";
5207
5962
  var PRIORITY_BY_TYPE = {
5208
5963
  observability: 1,
5209
5964
  coverage: 2,
@@ -5241,8 +5996,8 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
5241
5996
  storyNum++;
5242
5997
  }
5243
5998
  for (const mod of scan.modules) {
5244
- const agentsPath = join16(root, mod.path, "AGENTS.md");
5245
- if (!existsSync18(agentsPath)) {
5999
+ const agentsPath = join20(root, mod.path, "AGENTS.md");
6000
+ if (!existsSync22(agentsPath)) {
5246
6001
  stories.push({
5247
6002
  key: `0.${storyNum}`,
5248
6003
  title: `Create ${mod.path}/AGENTS.md`,
@@ -5308,7 +6063,7 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
5308
6063
  };
5309
6064
  }
5310
6065
  function writeOnboardingEpic(epic, outputPath) {
5311
- mkdirSync6(dirname5(outputPath), { recursive: true });
6066
+ mkdirSync7(dirname6(outputPath), { recursive: true });
5312
6067
  const lines = [];
5313
6068
  lines.push(`# ${epic.title}`);
5314
6069
  lines.push("");
@@ -5344,14 +6099,14 @@ function writeOnboardingEpic(epic, outputPath) {
5344
6099
  lines.push("");
5345
6100
  lines.push("Review and approve before execution.");
5346
6101
  lines.push("");
5347
- writeFileSync9(outputPath, lines.join("\n"), "utf-8");
6102
+ writeFileSync12(outputPath, lines.join("\n"), "utf-8");
5348
6103
  }
5349
6104
  function formatEpicSummary(epic) {
5350
6105
  const { totalStories, coverageStories, docStories, verificationStories, observabilityStories } = epic.summary;
5351
6106
  return `Onboarding plan: ${totalStories} stories (${coverageStories} coverage, ${docStories} documentation, ${verificationStories} verification, ${observabilityStories} observability)`;
5352
6107
  }
5353
6108
  function promptApproval() {
5354
- return new Promise((resolve) => {
6109
+ return new Promise((resolve2) => {
5355
6110
  let answered = false;
5356
6111
  const rl = createInterface({
5357
6112
  input: process.stdin,
@@ -5360,14 +6115,14 @@ function promptApproval() {
5360
6115
  rl.on("close", () => {
5361
6116
  if (!answered) {
5362
6117
  answered = true;
5363
- resolve(false);
6118
+ resolve2(false);
5364
6119
  }
5365
6120
  });
5366
6121
  rl.question("Review the onboarding plan. Approve? [Y/n] ", (answer) => {
5367
6122
  answered = true;
5368
6123
  rl.close();
5369
6124
  const trimmed = answer.trim().toLowerCase();
5370
- resolve(trimmed === "" || trimmed === "y");
6125
+ resolve2(trimmed === "" || trimmed === "y");
5371
6126
  });
5372
6127
  });
5373
6128
  }
@@ -5440,29 +6195,29 @@ function getGapIdFromTitle(title) {
5440
6195
  }
5441
6196
 
5442
6197
  // src/lib/scan-cache.ts
5443
- import { existsSync as existsSync19, mkdirSync as mkdirSync7, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
5444
- import { join as join17 } from "path";
6198
+ import { existsSync as existsSync23, mkdirSync as mkdirSync8, readFileSync as readFileSync20, writeFileSync as writeFileSync13 } from "fs";
6199
+ import { join as join21 } from "path";
5445
6200
  var CACHE_DIR = ".harness";
5446
6201
  var CACHE_FILE = "last-onboard-scan.json";
5447
6202
  var DEFAULT_MAX_AGE_MS = 864e5;
5448
6203
  function saveScanCache(entry, dir) {
5449
6204
  try {
5450
6205
  const root = dir ?? process.cwd();
5451
- const cacheDir = join17(root, CACHE_DIR);
5452
- mkdirSync7(cacheDir, { recursive: true });
5453
- const cachePath = join17(cacheDir, CACHE_FILE);
5454
- writeFileSync10(cachePath, JSON.stringify(entry, null, 2), "utf-8");
6206
+ const cacheDir = join21(root, CACHE_DIR);
6207
+ mkdirSync8(cacheDir, { recursive: true });
6208
+ const cachePath = join21(cacheDir, CACHE_FILE);
6209
+ writeFileSync13(cachePath, JSON.stringify(entry, null, 2), "utf-8");
5455
6210
  } catch {
5456
6211
  }
5457
6212
  }
5458
6213
  function loadScanCache(dir) {
5459
6214
  const root = dir ?? process.cwd();
5460
- const cachePath = join17(root, CACHE_DIR, CACHE_FILE);
5461
- if (!existsSync19(cachePath)) {
6215
+ const cachePath = join21(root, CACHE_DIR, CACHE_FILE);
6216
+ if (!existsSync23(cachePath)) {
5462
6217
  return null;
5463
6218
  }
5464
6219
  try {
5465
- const raw = readFileSync16(cachePath, "utf-8");
6220
+ const raw = readFileSync20(cachePath, "utf-8");
5466
6221
  return JSON.parse(raw);
5467
6222
  } catch {
5468
6223
  return null;
@@ -5635,7 +6390,7 @@ function registerOnboardCommand(program) {
5635
6390
  }
5636
6391
  coverage = lastCoverageResult ?? runCoverageAnalysis(scan);
5637
6392
  audit = lastAuditResult ?? runAudit();
5638
- const epicPath = join18(process.cwd(), "ralph", "onboarding-epic.md");
6393
+ const epicPath = join22(process.cwd(), "ralph", "onboarding-epic.md");
5639
6394
  const epic = generateOnboardingEpic(scan, coverage, audit);
5640
6395
  mergeExtendedGaps(epic);
5641
6396
  if (!isFull) {
@@ -5708,7 +6463,7 @@ function registerOnboardCommand(program) {
5708
6463
  coverage,
5709
6464
  audit
5710
6465
  });
5711
- const epicPath = join18(process.cwd(), "ralph", "onboarding-epic.md");
6466
+ const epicPath = join22(process.cwd(), "ralph", "onboarding-epic.md");
5712
6467
  const epic = generateOnboardingEpic(scan, coverage, audit);
5713
6468
  mergeExtendedGaps(epic);
5714
6469
  if (!isFull) {
@@ -5816,8 +6571,8 @@ function printEpicOutput(epic) {
5816
6571
  }
5817
6572
 
5818
6573
  // src/commands/teardown.ts
5819
- import { existsSync as existsSync20, unlinkSync as unlinkSync2, readFileSync as readFileSync17, writeFileSync as writeFileSync11, rmSync } from "fs";
5820
- import { join as join19 } from "path";
6574
+ import { existsSync as existsSync24, unlinkSync as unlinkSync2, readFileSync as readFileSync21, writeFileSync as writeFileSync14, rmSync } from "fs";
6575
+ import { join as join23 } from "path";
5821
6576
  function buildDefaultResult() {
5822
6577
  return {
5823
6578
  status: "ok",
@@ -5920,16 +6675,16 @@ function registerTeardownCommand(program) {
5920
6675
  info("Docker stack: not running, skipping");
5921
6676
  }
5922
6677
  }
5923
- const composeFilePath = join19(projectDir, composeFile);
5924
- if (existsSync20(composeFilePath)) {
6678
+ const composeFilePath = join23(projectDir, composeFile);
6679
+ if (existsSync24(composeFilePath)) {
5925
6680
  unlinkSync2(composeFilePath);
5926
6681
  result.removed.push(composeFile);
5927
6682
  if (!isJson) {
5928
6683
  ok(`Removed: ${composeFile}`);
5929
6684
  }
5930
6685
  }
5931
- const otelConfigPath = join19(projectDir, "otel-collector-config.yaml");
5932
- if (existsSync20(otelConfigPath)) {
6686
+ const otelConfigPath = join23(projectDir, "otel-collector-config.yaml");
6687
+ if (existsSync24(otelConfigPath)) {
5933
6688
  unlinkSync2(otelConfigPath);
5934
6689
  result.removed.push("otel-collector-config.yaml");
5935
6690
  if (!isJson) {
@@ -5939,8 +6694,8 @@ function registerTeardownCommand(program) {
5939
6694
  }
5940
6695
  let patchesRemoved = 0;
5941
6696
  for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
5942
- const filePath = join19(projectDir, "_bmad", relativePath);
5943
- if (!existsSync20(filePath)) {
6697
+ const filePath = join23(projectDir, "_bmad", relativePath);
6698
+ if (!existsSync24(filePath)) {
5944
6699
  continue;
5945
6700
  }
5946
6701
  try {
@@ -5960,10 +6715,10 @@ function registerTeardownCommand(program) {
5960
6715
  }
5961
6716
  }
5962
6717
  if (state.otlp?.enabled && state.stack === "nodejs") {
5963
- const pkgPath = join19(projectDir, "package.json");
5964
- if (existsSync20(pkgPath)) {
6718
+ const pkgPath = join23(projectDir, "package.json");
6719
+ if (existsSync24(pkgPath)) {
5965
6720
  try {
5966
- const raw = readFileSync17(pkgPath, "utf-8");
6721
+ const raw = readFileSync21(pkgPath, "utf-8");
5967
6722
  const pkg = JSON.parse(raw);
5968
6723
  const scripts = pkg["scripts"];
5969
6724
  if (scripts) {
@@ -5977,7 +6732,7 @@ function registerTeardownCommand(program) {
5977
6732
  for (const key of keysToRemove) {
5978
6733
  delete scripts[key];
5979
6734
  }
5980
- writeFileSync11(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
6735
+ writeFileSync14(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
5981
6736
  result.otlp_cleaned = true;
5982
6737
  if (!isJson) {
5983
6738
  ok("OTLP: removed instrumented scripts from package.json");
@@ -6003,17 +6758,17 @@ function registerTeardownCommand(program) {
6003
6758
  }
6004
6759
  }
6005
6760
  }
6006
- const harnessDir = join19(projectDir, ".harness");
6007
- if (existsSync20(harnessDir)) {
6761
+ const harnessDir = join23(projectDir, ".harness");
6762
+ if (existsSync24(harnessDir)) {
6008
6763
  rmSync(harnessDir, { recursive: true, force: true });
6009
6764
  result.removed.push(".harness/");
6010
6765
  if (!isJson) {
6011
6766
  ok("Removed: .harness/");
6012
6767
  }
6013
6768
  }
6014
- const statePath = getStatePath(projectDir);
6015
- if (existsSync20(statePath)) {
6016
- unlinkSync2(statePath);
6769
+ const statePath2 = getStatePath(projectDir);
6770
+ if (existsSync24(statePath2)) {
6771
+ unlinkSync2(statePath2);
6017
6772
  result.removed.push(".claude/codeharness.local.md");
6018
6773
  if (!isJson) {
6019
6774
  ok("Removed: .claude/codeharness.local.md");
@@ -6756,8 +7511,8 @@ function registerQueryCommand(program) {
6756
7511
  }
6757
7512
 
6758
7513
  // src/commands/retro-import.ts
6759
- import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
6760
- import { join as join20 } from "path";
7514
+ import { existsSync as existsSync25, readFileSync as readFileSync22 } from "fs";
7515
+ import { join as join24 } from "path";
6761
7516
 
6762
7517
  // src/lib/retro-parser.ts
6763
7518
  var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
@@ -6926,15 +7681,15 @@ function registerRetroImportCommand(program) {
6926
7681
  return;
6927
7682
  }
6928
7683
  const retroFile = `epic-${epicNum}-retrospective.md`;
6929
- const retroPath = join20(root, STORY_DIR2, retroFile);
6930
- if (!existsSync21(retroPath)) {
7684
+ const retroPath = join24(root, STORY_DIR2, retroFile);
7685
+ if (!existsSync25(retroPath)) {
6931
7686
  fail(`Retro file not found: ${retroFile}`, { json: isJson });
6932
7687
  process.exitCode = 1;
6933
7688
  return;
6934
7689
  }
6935
7690
  let content;
6936
7691
  try {
6937
- content = readFileSync18(retroPath, "utf-8");
7692
+ content = readFileSync22(retroPath, "utf-8");
6938
7693
  } catch (err) {
6939
7694
  const message = err instanceof Error ? err.message : String(err);
6940
7695
  fail(`Failed to read retro file: ${message}`, { json: isJson });
@@ -7202,8 +7957,8 @@ function registerGithubImportCommand(program) {
7202
7957
 
7203
7958
  // src/lib/verify-env.ts
7204
7959
  import { execFileSync as execFileSync7 } from "child_process";
7205
- import { existsSync as existsSync22, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync19, cpSync, rmSync as rmSync2, statSync as statSync3 } from "fs";
7206
- import { join as join21, basename as basename3 } from "path";
7960
+ import { existsSync as existsSync26, mkdirSync as mkdirSync9, readdirSync as readdirSync4, readFileSync as readFileSync23, cpSync, rmSync as rmSync2, statSync as statSync3 } from "fs";
7961
+ import { join as join25, basename as basename3 } from "path";
7207
7962
  import { createHash } from "crypto";
7208
7963
  var IMAGE_TAG = "codeharness-verify";
7209
7964
  var STORY_DIR3 = "_bmad-output/implementation-artifacts";
@@ -7216,14 +7971,14 @@ function isValidStoryKey(storyKey) {
7216
7971
  return /^[a-zA-Z0-9_-]+$/.test(storyKey);
7217
7972
  }
7218
7973
  function computeDistHash(projectDir) {
7219
- const distDir = join21(projectDir, "dist");
7220
- if (!existsSync22(distDir)) {
7974
+ const distDir = join25(projectDir, "dist");
7975
+ if (!existsSync26(distDir)) {
7221
7976
  return null;
7222
7977
  }
7223
7978
  const hash = createHash("sha256");
7224
7979
  const files = collectFiles(distDir).sort();
7225
7980
  for (const file of files) {
7226
- const content = readFileSync19(file);
7981
+ const content = readFileSync23(file);
7227
7982
  hash.update(file.slice(distDir.length));
7228
7983
  hash.update(content);
7229
7984
  }
@@ -7233,7 +7988,7 @@ function collectFiles(dir) {
7233
7988
  const results = [];
7234
7989
  const entries = readdirSync4(dir, { withFileTypes: true });
7235
7990
  for (const entry of entries) {
7236
- const fullPath = join21(dir, entry.name);
7991
+ const fullPath = join25(dir, entry.name);
7237
7992
  if (entry.isDirectory()) {
7238
7993
  results.push(...collectFiles(fullPath));
7239
7994
  } else {
@@ -7305,13 +8060,13 @@ function buildNodeImage(projectDir) {
7305
8060
  throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
7306
8061
  }
7307
8062
  const tarballName = basename3(lastLine);
7308
- const tarballPath = join21("/tmp", tarballName);
7309
- const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
7310
- mkdirSync8(buildContext, { recursive: true });
8063
+ const tarballPath = join25("/tmp", tarballName);
8064
+ const buildContext = join25("/tmp", `codeharness-verify-build-${Date.now()}`);
8065
+ mkdirSync9(buildContext, { recursive: true });
7311
8066
  try {
7312
- cpSync(tarballPath, join21(buildContext, tarballName));
8067
+ cpSync(tarballPath, join25(buildContext, tarballName));
7313
8068
  const dockerfileSrc = resolveDockerfileTemplate(projectDir);
7314
- cpSync(dockerfileSrc, join21(buildContext, "Dockerfile"));
8069
+ cpSync(dockerfileSrc, join25(buildContext, "Dockerfile"));
7315
8070
  execFileSync7("docker", [
7316
8071
  "build",
7317
8072
  "-t",
@@ -7330,7 +8085,7 @@ function buildNodeImage(projectDir) {
7330
8085
  }
7331
8086
  }
7332
8087
  function buildPythonImage(projectDir) {
7333
- const distDir = join21(projectDir, "dist");
8088
+ const distDir = join25(projectDir, "dist");
7334
8089
  const distFiles = readdirSync4(distDir).filter(
7335
8090
  (f) => f.endsWith(".tar.gz") || f.endsWith(".whl")
7336
8091
  );
@@ -7338,12 +8093,12 @@ function buildPythonImage(projectDir) {
7338
8093
  throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
7339
8094
  }
7340
8095
  const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
7341
- const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
7342
- mkdirSync8(buildContext, { recursive: true });
8096
+ const buildContext = join25("/tmp", `codeharness-verify-build-${Date.now()}`);
8097
+ mkdirSync9(buildContext, { recursive: true });
7343
8098
  try {
7344
- cpSync(join21(distDir, distFile), join21(buildContext, distFile));
8099
+ cpSync(join25(distDir, distFile), join25(buildContext, distFile));
7345
8100
  const dockerfileSrc = resolveDockerfileTemplate(projectDir);
7346
- cpSync(dockerfileSrc, join21(buildContext, "Dockerfile"));
8101
+ cpSync(dockerfileSrc, join25(buildContext, "Dockerfile"));
7347
8102
  execFileSync7("docker", [
7348
8103
  "build",
7349
8104
  "-t",
@@ -7365,25 +8120,25 @@ function prepareVerifyWorkspace(storyKey, projectDir) {
7365
8120
  if (!isValidStoryKey(storyKey)) {
7366
8121
  throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
7367
8122
  }
7368
- const storyFile = join21(root, STORY_DIR3, `${storyKey}.md`);
7369
- if (!existsSync22(storyFile)) {
8123
+ const storyFile = join25(root, STORY_DIR3, `${storyKey}.md`);
8124
+ if (!existsSync26(storyFile)) {
7370
8125
  throw new Error(`Story file not found: ${storyFile}`);
7371
8126
  }
7372
8127
  const workspace = `${TEMP_PREFIX}${storyKey}`;
7373
- if (existsSync22(workspace)) {
8128
+ if (existsSync26(workspace)) {
7374
8129
  rmSync2(workspace, { recursive: true, force: true });
7375
8130
  }
7376
- mkdirSync8(workspace, { recursive: true });
7377
- cpSync(storyFile, join21(workspace, "story.md"));
7378
- const readmePath = join21(root, "README.md");
7379
- if (existsSync22(readmePath)) {
7380
- cpSync(readmePath, join21(workspace, "README.md"));
8131
+ mkdirSync9(workspace, { recursive: true });
8132
+ cpSync(storyFile, join25(workspace, "story.md"));
8133
+ const readmePath = join25(root, "README.md");
8134
+ if (existsSync26(readmePath)) {
8135
+ cpSync(readmePath, join25(workspace, "README.md"));
7381
8136
  }
7382
- const docsDir = join21(root, "docs");
7383
- if (existsSync22(docsDir) && statSync3(docsDir).isDirectory()) {
7384
- cpSync(docsDir, join21(workspace, "docs"), { recursive: true });
8137
+ const docsDir = join25(root, "docs");
8138
+ if (existsSync26(docsDir) && statSync3(docsDir).isDirectory()) {
8139
+ cpSync(docsDir, join25(workspace, "docs"), { recursive: true });
7385
8140
  }
7386
- mkdirSync8(join21(workspace, "verification"), { recursive: true });
8141
+ mkdirSync9(join25(workspace, "verification"), { recursive: true });
7387
8142
  return workspace;
7388
8143
  }
7389
8144
  function checkVerifyEnv() {
@@ -7432,7 +8187,7 @@ function cleanupVerifyEnv(storyKey) {
7432
8187
  }
7433
8188
  const workspace = `${TEMP_PREFIX}${storyKey}`;
7434
8189
  const containerName = `codeharness-verify-${storyKey}`;
7435
- if (existsSync22(workspace)) {
8190
+ if (existsSync26(workspace)) {
7436
8191
  rmSync2(workspace, { recursive: true, force: true });
7437
8192
  }
7438
8193
  try {
@@ -7451,11 +8206,11 @@ function cleanupVerifyEnv(storyKey) {
7451
8206
  }
7452
8207
  }
7453
8208
  function resolveDockerfileTemplate(projectDir) {
7454
- const local = join21(projectDir, "templates", "Dockerfile.verify");
7455
- if (existsSync22(local)) return local;
8209
+ const local = join25(projectDir, "templates", "Dockerfile.verify");
8210
+ if (existsSync26(local)) return local;
7456
8211
  const pkgDir = new URL("../../", import.meta.url).pathname;
7457
- const pkg = join21(pkgDir, "templates", "Dockerfile.verify");
7458
- if (existsSync22(pkg)) return pkg;
8212
+ const pkg = join25(pkgDir, "templates", "Dockerfile.verify");
8213
+ if (existsSync26(pkg)) return pkg;
7459
8214
  throw new Error("Dockerfile.verify not found. Ensure templates/Dockerfile.verify exists in the project or installed package.");
7460
8215
  }
7461
8216
  function dockerImageExists(tag) {
@@ -7601,26 +8356,26 @@ function registerVerifyEnvCommand(program) {
7601
8356
  }
7602
8357
 
7603
8358
  // src/commands/retry.ts
7604
- import { join as join23 } from "path";
8359
+ import { join as join27 } from "path";
7605
8360
 
7606
8361
  // src/lib/retry-state.ts
7607
- import { existsSync as existsSync23, readFileSync as readFileSync20, writeFileSync as writeFileSync12 } from "fs";
7608
- import { join as join22 } from "path";
8362
+ import { existsSync as existsSync27, readFileSync as readFileSync24, writeFileSync as writeFileSync15 } from "fs";
8363
+ import { join as join26 } from "path";
7609
8364
  var RETRIES_FILE = ".story_retries";
7610
8365
  var FLAGGED_FILE = ".flagged_stories";
7611
8366
  var LINE_PATTERN = /^([^=]+)=(\d+)$/;
7612
8367
  function retriesPath(dir) {
7613
- return join22(dir, RETRIES_FILE);
8368
+ return join26(dir, RETRIES_FILE);
7614
8369
  }
7615
8370
  function flaggedPath(dir) {
7616
- return join22(dir, FLAGGED_FILE);
8371
+ return join26(dir, FLAGGED_FILE);
7617
8372
  }
7618
8373
  function readRetries(dir) {
7619
8374
  const filePath = retriesPath(dir);
7620
- if (!existsSync23(filePath)) {
8375
+ if (!existsSync27(filePath)) {
7621
8376
  return /* @__PURE__ */ new Map();
7622
8377
  }
7623
- const raw = readFileSync20(filePath, "utf-8");
8378
+ const raw = readFileSync24(filePath, "utf-8");
7624
8379
  const result = /* @__PURE__ */ new Map();
7625
8380
  for (const line of raw.split("\n")) {
7626
8381
  const trimmed = line.trim();
@@ -7642,7 +8397,7 @@ function writeRetries(dir, retries) {
7642
8397
  for (const [key, count] of retries) {
7643
8398
  lines.push(`${key}=${count}`);
7644
8399
  }
7645
- writeFileSync12(filePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
8400
+ writeFileSync15(filePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
7646
8401
  }
7647
8402
  function resetRetry(dir, storyKey) {
7648
8403
  if (storyKey) {
@@ -7657,15 +8412,15 @@ function resetRetry(dir, storyKey) {
7657
8412
  }
7658
8413
  function readFlaggedStories(dir) {
7659
8414
  const filePath = flaggedPath(dir);
7660
- if (!existsSync23(filePath)) {
8415
+ if (!existsSync27(filePath)) {
7661
8416
  return [];
7662
8417
  }
7663
- const raw = readFileSync20(filePath, "utf-8");
8418
+ const raw = readFileSync24(filePath, "utf-8");
7664
8419
  return raw.split("\n").map((l) => l.trim()).filter((l) => l !== "");
7665
8420
  }
7666
8421
  function writeFlaggedStories(dir, stories) {
7667
8422
  const filePath = flaggedPath(dir);
7668
- writeFileSync12(filePath, stories.length > 0 ? stories.join("\n") + "\n" : "", "utf-8");
8423
+ writeFileSync15(filePath, stories.length > 0 ? stories.join("\n") + "\n" : "", "utf-8");
7669
8424
  }
7670
8425
  function removeFlaggedStory(dir, key) {
7671
8426
  const stories = readFlaggedStories(dir);
@@ -7685,7 +8440,7 @@ function registerRetryCommand(program) {
7685
8440
  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) => {
7686
8441
  const opts = cmd.optsWithGlobals();
7687
8442
  const isJson = opts.json === true;
7688
- const dir = join23(process.cwd(), RALPH_SUBDIR);
8443
+ const dir = join27(process.cwd(), RALPH_SUBDIR);
7689
8444
  if (opts.story && !isValidStoryKey3(opts.story)) {
7690
8445
  if (isJson) {
7691
8446
  jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
@@ -7758,8 +8513,53 @@ function handleStatus(dir, isJson, filterStory) {
7758
8513
  }
7759
8514
  }
7760
8515
 
8516
+ // src/commands/timeout-report.ts
8517
+ function registerTimeoutReportCommand(program) {
8518
+ program.command("timeout-report").description("Capture diagnostic data from a timed-out iteration").requiredOption("--story <key>", "Story key").requiredOption("--iteration <n>", "Iteration number").requiredOption("--duration <minutes>", "Timeout duration in minutes").requiredOption("--output-file <path>", "Path to iteration output log").requiredOption("--state-snapshot <path>", "Path to pre-iteration state snapshot").action((options, cmd) => {
8519
+ const opts = cmd.optsWithGlobals();
8520
+ const isJson = opts.json === true;
8521
+ const iteration = parseInt(options.iteration, 10);
8522
+ const duration = parseInt(options.duration, 10);
8523
+ if (isNaN(iteration) || isNaN(duration)) {
8524
+ if (isJson) {
8525
+ jsonOutput({ status: "fail", message: "iteration and duration must be numbers" });
8526
+ } else {
8527
+ fail("iteration and duration must be numbers");
8528
+ }
8529
+ process.exitCode = 1;
8530
+ return;
8531
+ }
8532
+ const result = captureTimeoutReport2({
8533
+ storyKey: options.story,
8534
+ iteration,
8535
+ durationMinutes: duration,
8536
+ outputFile: options.outputFile,
8537
+ stateSnapshotPath: options.stateSnapshot
8538
+ });
8539
+ if (!result.success) {
8540
+ if (isJson) {
8541
+ jsonOutput({ status: "fail", message: result.error });
8542
+ } else {
8543
+ fail(result.error);
8544
+ }
8545
+ process.exitCode = 1;
8546
+ return;
8547
+ }
8548
+ if (isJson) {
8549
+ jsonOutput({
8550
+ status: "ok",
8551
+ reportPath: result.data.filePath,
8552
+ storyKey: result.data.capture.storyKey,
8553
+ iteration: result.data.capture.iteration
8554
+ });
8555
+ } else {
8556
+ ok(`Timeout report written: ${result.data.filePath}`);
8557
+ }
8558
+ });
8559
+ }
8560
+
7761
8561
  // src/index.ts
7762
- var VERSION = true ? "0.19.1" : "0.0.0-dev";
8562
+ var VERSION = true ? "0.19.2" : "0.0.0-dev";
7763
8563
  function createProgram() {
7764
8564
  const program = new Command();
7765
8565
  program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
@@ -7780,6 +8580,7 @@ function createProgram() {
7780
8580
  registerGithubImportCommand(program);
7781
8581
  registerVerifyEnvCommand(program);
7782
8582
  registerRetryCommand(program);
8583
+ registerTimeoutReportCommand(program);
7783
8584
  return program;
7784
8585
  }
7785
8586
  if (!process.env["VITEST"]) {