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 +922 -121
- package/package.json +1 -1
- package/ralph/ralph.sh +26 -0
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.
|
|
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
|
|
1505
|
-
if (existsSync7(
|
|
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((
|
|
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
|
-
|
|
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
|
|
4323
|
-
return { ok: existsSync16(
|
|
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
|
|
5702
|
+
import { join as join22 } from "path";
|
|
4948
5703
|
|
|
4949
5704
|
// src/lib/scanner.ts
|
|
4950
5705
|
import {
|
|
4951
|
-
existsSync as
|
|
5706
|
+
existsSync as existsSync21,
|
|
4952
5707
|
readdirSync as readdirSync3,
|
|
4953
|
-
readFileSync as
|
|
5708
|
+
readFileSync as readFileSync19,
|
|
4954
5709
|
statSync as statSync2
|
|
4955
5710
|
} from "fs";
|
|
4956
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
5042
|
-
const hasBmad =
|
|
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 =
|
|
5125
|
-
if (!
|
|
5879
|
+
const reportPath = join19(dir, "coverage", "coverage-summary.json");
|
|
5880
|
+
if (!existsSync21(reportPath)) return null;
|
|
5126
5881
|
try {
|
|
5127
|
-
const report = JSON.parse(
|
|
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 =
|
|
5140
|
-
if (!
|
|
5894
|
+
const reportPath = join19(dir, "coverage.json");
|
|
5895
|
+
if (!existsSync21(reportPath)) return null;
|
|
5141
5896
|
try {
|
|
5142
|
-
const report = JSON.parse(
|
|
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 =
|
|
5159
|
-
if (!
|
|
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 =
|
|
5164
|
-
const codeDir =
|
|
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 =
|
|
5173
|
-
if (
|
|
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 =
|
|
5186
|
-
if (
|
|
5187
|
-
const srcDir =
|
|
5188
|
-
const indexCodeDir =
|
|
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
|
|
5206
|
-
import { dirname as
|
|
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 =
|
|
5245
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5444
|
-
import { join as
|
|
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 =
|
|
5452
|
-
|
|
5453
|
-
const cachePath =
|
|
5454
|
-
|
|
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 =
|
|
5461
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
5820
|
-
import { join as
|
|
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 =
|
|
5924
|
-
if (
|
|
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 =
|
|
5932
|
-
if (
|
|
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 =
|
|
5943
|
-
if (!
|
|
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 =
|
|
5964
|
-
if (
|
|
6718
|
+
const pkgPath = join23(projectDir, "package.json");
|
|
6719
|
+
if (existsSync24(pkgPath)) {
|
|
5965
6720
|
try {
|
|
5966
|
-
const raw =
|
|
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
|
-
|
|
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 =
|
|
6007
|
-
if (
|
|
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
|
|
6015
|
-
if (
|
|
6016
|
-
unlinkSync2(
|
|
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
|
|
6760
|
-
import { join as
|
|
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 =
|
|
6930
|
-
if (!
|
|
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 =
|
|
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
|
|
7206
|
-
import { join as
|
|
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 =
|
|
7220
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
7309
|
-
const buildContext =
|
|
7310
|
-
|
|
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,
|
|
8067
|
+
cpSync(tarballPath, join25(buildContext, tarballName));
|
|
7313
8068
|
const dockerfileSrc = resolveDockerfileTemplate(projectDir);
|
|
7314
|
-
cpSync(dockerfileSrc,
|
|
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 =
|
|
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 =
|
|
7342
|
-
|
|
8096
|
+
const buildContext = join25("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
8097
|
+
mkdirSync9(buildContext, { recursive: true });
|
|
7343
8098
|
try {
|
|
7344
|
-
cpSync(
|
|
8099
|
+
cpSync(join25(distDir, distFile), join25(buildContext, distFile));
|
|
7345
8100
|
const dockerfileSrc = resolveDockerfileTemplate(projectDir);
|
|
7346
|
-
cpSync(dockerfileSrc,
|
|
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 =
|
|
7369
|
-
if (!
|
|
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 (
|
|
8128
|
+
if (existsSync26(workspace)) {
|
|
7374
8129
|
rmSync2(workspace, { recursive: true, force: true });
|
|
7375
8130
|
}
|
|
7376
|
-
|
|
7377
|
-
cpSync(storyFile,
|
|
7378
|
-
const readmePath =
|
|
7379
|
-
if (
|
|
7380
|
-
cpSync(readmePath,
|
|
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 =
|
|
7383
|
-
if (
|
|
7384
|
-
cpSync(docsDir,
|
|
8137
|
+
const docsDir = join25(root, "docs");
|
|
8138
|
+
if (existsSync26(docsDir) && statSync3(docsDir).isDirectory()) {
|
|
8139
|
+
cpSync(docsDir, join25(workspace, "docs"), { recursive: true });
|
|
7385
8140
|
}
|
|
7386
|
-
|
|
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 (
|
|
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 =
|
|
7455
|
-
if (
|
|
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 =
|
|
7458
|
-
if (
|
|
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
|
|
8359
|
+
import { join as join27 } from "path";
|
|
7605
8360
|
|
|
7606
8361
|
// src/lib/retry-state.ts
|
|
7607
|
-
import { existsSync as
|
|
7608
|
-
import { join as
|
|
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
|
|
8368
|
+
return join26(dir, RETRIES_FILE);
|
|
7614
8369
|
}
|
|
7615
8370
|
function flaggedPath(dir) {
|
|
7616
|
-
return
|
|
8371
|
+
return join26(dir, FLAGGED_FILE);
|
|
7617
8372
|
}
|
|
7618
8373
|
function readRetries(dir) {
|
|
7619
8374
|
const filePath = retriesPath(dir);
|
|
7620
|
-
if (!
|
|
8375
|
+
if (!existsSync27(filePath)) {
|
|
7621
8376
|
return /* @__PURE__ */ new Map();
|
|
7622
8377
|
}
|
|
7623
|
-
const raw =
|
|
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
|
-
|
|
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 (!
|
|
8415
|
+
if (!existsSync27(filePath)) {
|
|
7661
8416
|
return [];
|
|
7662
8417
|
}
|
|
7663
|
-
const raw =
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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"]) {
|