codeharness 0.18.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 +926 -130
- package/package.json +1 -1
- package/ralph/ralph.sh +36 -0
package/dist/index.js
CHANGED
|
@@ -940,14 +940,9 @@ import { join as join5, dirname as dirname2 } from "path";
|
|
|
940
940
|
import { fileURLToPath } from "url";
|
|
941
941
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
942
942
|
function readPatchFile(name) {
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
return readFileSync5(projectPath, "utf-8").trim();
|
|
947
|
-
}
|
|
948
|
-
const pkgPath = join5(__dirname, "..", "..", "patches", `${name}.md`);
|
|
949
|
-
if (existsSync5(pkgPath)) {
|
|
950
|
-
return readFileSync5(pkgPath, "utf-8").trim();
|
|
943
|
+
const patchPath = join5(__dirname, "..", "..", "patches", `${name}.md`);
|
|
944
|
+
if (existsSync5(patchPath)) {
|
|
945
|
+
return readFileSync5(patchPath, "utf-8").trim();
|
|
951
946
|
}
|
|
952
947
|
return null;
|
|
953
948
|
}
|
|
@@ -1392,7 +1387,7 @@ function getInstallCommand(stack) {
|
|
|
1392
1387
|
}
|
|
1393
1388
|
|
|
1394
1389
|
// src/commands/init.ts
|
|
1395
|
-
var HARNESS_VERSION = true ? "0.
|
|
1390
|
+
var HARNESS_VERSION = true ? "0.19.2" : "0.0.0-dev";
|
|
1396
1391
|
function getProjectName(projectDir) {
|
|
1397
1392
|
try {
|
|
1398
1393
|
const pkgPath = join7(projectDir, "package.json");
|
|
@@ -1506,8 +1501,8 @@ function registerInitCommand(program) {
|
|
|
1506
1501
|
readme: "skipped"
|
|
1507
1502
|
}
|
|
1508
1503
|
};
|
|
1509
|
-
const
|
|
1510
|
-
if (existsSync7(
|
|
1504
|
+
const statePath2 = getStatePath(projectDir);
|
|
1505
|
+
if (existsSync7(statePath2)) {
|
|
1511
1506
|
try {
|
|
1512
1507
|
const existingState = readState(projectDir);
|
|
1513
1508
|
const legacyObsDisabled = existingState.enforcement.observability === false;
|
|
@@ -2591,7 +2586,7 @@ function buildSpawnArgs(opts) {
|
|
|
2591
2586
|
return args;
|
|
2592
2587
|
}
|
|
2593
2588
|
function registerRunCommand(program) {
|
|
2594
|
-
program.command("run").description("Execute the autonomous coding loop").option("--max-iterations <n>", "Maximum loop iterations", "50").option("--timeout <seconds>", "Total loop timeout in seconds", "14400").option("--iteration-timeout <minutes>", "Per-iteration timeout in minutes", "30").option("--live", "Show live output streaming", false).option("--calls <n>", "Max API calls per hour", "100").option("--max-story-retries <n>", "Max retries per story before flagging", "
|
|
2589
|
+
program.command("run").description("Execute the autonomous coding loop").option("--max-iterations <n>", "Maximum loop iterations", "50").option("--timeout <seconds>", "Total loop timeout in seconds", "14400").option("--iteration-timeout <minutes>", "Per-iteration timeout in minutes", "30").option("--live", "Show live output streaming", false).option("--calls <n>", "Max API calls per hour", "100").option("--max-story-retries <n>", "Max retries per story before flagging", "10").option("--reset", "Clear retry counters, flagged stories, and circuit breaker before starting", false).action(async (options, cmd) => {
|
|
2595
2590
|
const globalOpts = cmd.optsWithGlobals();
|
|
2596
2591
|
const isJson = !!globalOpts.json;
|
|
2597
2592
|
const outputOpts = { json: isJson };
|
|
@@ -2673,12 +2668,12 @@ function registerRunCommand(program) {
|
|
|
2673
2668
|
cwd: projectDir,
|
|
2674
2669
|
env
|
|
2675
2670
|
});
|
|
2676
|
-
const exitCode = await new Promise((
|
|
2671
|
+
const exitCode = await new Promise((resolve2, reject) => {
|
|
2677
2672
|
child.on("error", (err) => {
|
|
2678
2673
|
reject(err);
|
|
2679
2674
|
});
|
|
2680
2675
|
child.on("close", (code) => {
|
|
2681
|
-
|
|
2676
|
+
resolve2(code ?? 1);
|
|
2682
2677
|
});
|
|
2683
2678
|
});
|
|
2684
2679
|
if (isJson) {
|
|
@@ -4324,8 +4319,8 @@ function printCoverageOutput(result, evaluation) {
|
|
|
4324
4319
|
|
|
4325
4320
|
// src/lib/onboard-checks.ts
|
|
4326
4321
|
function checkHarnessInitialized(dir) {
|
|
4327
|
-
const
|
|
4328
|
-
return { ok: existsSync16(
|
|
4322
|
+
const statePath2 = getStatePath(dir ?? process.cwd());
|
|
4323
|
+
return { ok: existsSync16(statePath2) };
|
|
4329
4324
|
}
|
|
4330
4325
|
function checkBmadInstalled(dir) {
|
|
4331
4326
|
return { ok: isBmadInstalled(dir) };
|
|
@@ -4502,6 +4497,644 @@ function filterTrackedGaps(stories, beadsFns) {
|
|
|
4502
4497
|
return { untracked, trackedCount };
|
|
4503
4498
|
}
|
|
4504
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
|
+
|
|
4505
5138
|
// src/commands/status.ts
|
|
4506
5139
|
function buildScopedEndpoints(endpoints, serviceName) {
|
|
4507
5140
|
const encoded = encodeURIComponent(serviceName);
|
|
@@ -4518,9 +5151,13 @@ var DEFAULT_ENDPOINTS = {
|
|
|
4518
5151
|
otel_http: "http://localhost:4318"
|
|
4519
5152
|
};
|
|
4520
5153
|
function registerStatusCommand(program) {
|
|
4521
|
-
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) => {
|
|
4522
5155
|
const opts = cmd.optsWithGlobals();
|
|
4523
5156
|
const isJson = opts.json === true;
|
|
5157
|
+
if (options.story) {
|
|
5158
|
+
handleStoryDrillDown(options.story, isJson);
|
|
5159
|
+
return;
|
|
5160
|
+
}
|
|
4524
5161
|
if (options.checkDocker) {
|
|
4525
5162
|
await handleDockerCheck(isJson);
|
|
4526
5163
|
return;
|
|
@@ -4552,6 +5189,7 @@ function handleFullStatus(isJson) {
|
|
|
4552
5189
|
handleFullStatusJson(state);
|
|
4553
5190
|
return;
|
|
4554
5191
|
}
|
|
5192
|
+
printSprintState();
|
|
4555
5193
|
console.log(`Harness: codeharness v${state.harness_version}`);
|
|
4556
5194
|
console.log(`Stack: ${state.stack ?? "unknown"}`);
|
|
4557
5195
|
if (state.app_type) {
|
|
@@ -4688,10 +5326,12 @@ function handleFullStatusJson(state) {
|
|
|
4688
5326
|
const scoped_endpoints = serviceName ? buildScopedEndpoints(endpoints, serviceName) : void 0;
|
|
4689
5327
|
const beads = getBeadsData();
|
|
4690
5328
|
const onboarding = getOnboardingProgressData();
|
|
5329
|
+
const sprint = getSprintReportData();
|
|
4691
5330
|
jsonOutput({
|
|
4692
5331
|
version: state.harness_version,
|
|
4693
5332
|
stack: state.stack,
|
|
4694
5333
|
...state.app_type ? { app_type: state.app_type } : {},
|
|
5334
|
+
...sprint ? { sprint } : {},
|
|
4695
5335
|
enforcement: state.enforcement,
|
|
4696
5336
|
docker,
|
|
4697
5337
|
endpoints,
|
|
@@ -4883,6 +5523,116 @@ async function handleDockerCheck(isJson) {
|
|
|
4883
5523
|
}
|
|
4884
5524
|
}
|
|
4885
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
|
+
}
|
|
4886
5636
|
function printBeadsSummary() {
|
|
4887
5637
|
if (!isBeadsInitialized()) {
|
|
4888
5638
|
console.log("Beads: not initialized");
|
|
@@ -4949,16 +5699,16 @@ function getBeadsData() {
|
|
|
4949
5699
|
}
|
|
4950
5700
|
|
|
4951
5701
|
// src/commands/onboard.ts
|
|
4952
|
-
import { join as
|
|
5702
|
+
import { join as join22 } from "path";
|
|
4953
5703
|
|
|
4954
5704
|
// src/lib/scanner.ts
|
|
4955
5705
|
import {
|
|
4956
|
-
existsSync as
|
|
5706
|
+
existsSync as existsSync21,
|
|
4957
5707
|
readdirSync as readdirSync3,
|
|
4958
|
-
readFileSync as
|
|
5708
|
+
readFileSync as readFileSync19,
|
|
4959
5709
|
statSync as statSync2
|
|
4960
5710
|
} from "fs";
|
|
4961
|
-
import { join as
|
|
5711
|
+
import { join as join19, relative as relative2 } from "path";
|
|
4962
5712
|
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
|
|
4963
5713
|
var DEFAULT_MIN_MODULE_SIZE = 3;
|
|
4964
5714
|
function getExtension2(filename) {
|
|
@@ -4983,7 +5733,7 @@ function countSourceFiles(dir) {
|
|
|
4983
5733
|
for (const entry of entries) {
|
|
4984
5734
|
if (isSkippedDir(entry)) continue;
|
|
4985
5735
|
if (entry.startsWith(".") && current !== dir) continue;
|
|
4986
|
-
const fullPath =
|
|
5736
|
+
const fullPath = join19(current, entry);
|
|
4987
5737
|
let stat;
|
|
4988
5738
|
try {
|
|
4989
5739
|
stat = statSync2(fullPath);
|
|
@@ -5006,7 +5756,7 @@ function countSourceFiles(dir) {
|
|
|
5006
5756
|
return count;
|
|
5007
5757
|
}
|
|
5008
5758
|
function countModuleFiles(modulePath, rootDir) {
|
|
5009
|
-
const fullModulePath =
|
|
5759
|
+
const fullModulePath = join19(rootDir, modulePath);
|
|
5010
5760
|
let sourceFiles = 0;
|
|
5011
5761
|
let testFiles = 0;
|
|
5012
5762
|
function walk(current) {
|
|
@@ -5018,7 +5768,7 @@ function countModuleFiles(modulePath, rootDir) {
|
|
|
5018
5768
|
}
|
|
5019
5769
|
for (const entry of entries) {
|
|
5020
5770
|
if (isSkippedDir(entry)) continue;
|
|
5021
|
-
const fullPath =
|
|
5771
|
+
const fullPath = join19(current, entry);
|
|
5022
5772
|
let stat;
|
|
5023
5773
|
try {
|
|
5024
5774
|
stat = statSync2(fullPath);
|
|
@@ -5043,8 +5793,8 @@ function countModuleFiles(modulePath, rootDir) {
|
|
|
5043
5793
|
return { sourceFiles, testFiles };
|
|
5044
5794
|
}
|
|
5045
5795
|
function detectArtifacts(dir) {
|
|
5046
|
-
const bmadPath =
|
|
5047
|
-
const hasBmad =
|
|
5796
|
+
const bmadPath = join19(dir, "_bmad");
|
|
5797
|
+
const hasBmad = existsSync21(bmadPath);
|
|
5048
5798
|
return {
|
|
5049
5799
|
hasBmad,
|
|
5050
5800
|
bmadPath: hasBmad ? relative2(dir, bmadPath) || "_bmad" : null
|
|
@@ -5126,10 +5876,10 @@ function readPerFileCoverage(dir, format) {
|
|
|
5126
5876
|
return null;
|
|
5127
5877
|
}
|
|
5128
5878
|
function readVitestPerFileCoverage(dir) {
|
|
5129
|
-
const reportPath =
|
|
5130
|
-
if (!
|
|
5879
|
+
const reportPath = join19(dir, "coverage", "coverage-summary.json");
|
|
5880
|
+
if (!existsSync21(reportPath)) return null;
|
|
5131
5881
|
try {
|
|
5132
|
-
const report = JSON.parse(
|
|
5882
|
+
const report = JSON.parse(readFileSync19(reportPath, "utf-8"));
|
|
5133
5883
|
const result = /* @__PURE__ */ new Map();
|
|
5134
5884
|
for (const [key, value] of Object.entries(report)) {
|
|
5135
5885
|
if (key === "total") continue;
|
|
@@ -5141,10 +5891,10 @@ function readVitestPerFileCoverage(dir) {
|
|
|
5141
5891
|
}
|
|
5142
5892
|
}
|
|
5143
5893
|
function readPythonPerFileCoverage(dir) {
|
|
5144
|
-
const reportPath =
|
|
5145
|
-
if (!
|
|
5894
|
+
const reportPath = join19(dir, "coverage.json");
|
|
5895
|
+
if (!existsSync21(reportPath)) return null;
|
|
5146
5896
|
try {
|
|
5147
|
-
const report = JSON.parse(
|
|
5897
|
+
const report = JSON.parse(readFileSync19(reportPath, "utf-8"));
|
|
5148
5898
|
if (!report.files) return null;
|
|
5149
5899
|
const result = /* @__PURE__ */ new Map();
|
|
5150
5900
|
for (const [key, value] of Object.entries(report.files)) {
|
|
@@ -5160,13 +5910,13 @@ function auditDocumentation(dir) {
|
|
|
5160
5910
|
const root = dir ?? process.cwd();
|
|
5161
5911
|
const documents = [];
|
|
5162
5912
|
for (const docName of AUDIT_DOCUMENTS) {
|
|
5163
|
-
const docPath =
|
|
5164
|
-
if (!
|
|
5913
|
+
const docPath = join19(root, docName);
|
|
5914
|
+
if (!existsSync21(docPath)) {
|
|
5165
5915
|
documents.push({ name: docName, grade: "missing", path: null });
|
|
5166
5916
|
continue;
|
|
5167
5917
|
}
|
|
5168
|
-
const srcDir =
|
|
5169
|
-
const codeDir =
|
|
5918
|
+
const srcDir = join19(root, "src");
|
|
5919
|
+
const codeDir = existsSync21(srcDir) ? srcDir : root;
|
|
5170
5920
|
const stale = isDocStale(docPath, codeDir);
|
|
5171
5921
|
documents.push({
|
|
5172
5922
|
name: docName,
|
|
@@ -5174,8 +5924,8 @@ function auditDocumentation(dir) {
|
|
|
5174
5924
|
path: docName
|
|
5175
5925
|
});
|
|
5176
5926
|
}
|
|
5177
|
-
const docsDir =
|
|
5178
|
-
if (
|
|
5927
|
+
const docsDir = join19(root, "docs");
|
|
5928
|
+
if (existsSync21(docsDir)) {
|
|
5179
5929
|
try {
|
|
5180
5930
|
const stat = statSync2(docsDir);
|
|
5181
5931
|
if (stat.isDirectory()) {
|
|
@@ -5187,10 +5937,10 @@ function auditDocumentation(dir) {
|
|
|
5187
5937
|
} else {
|
|
5188
5938
|
documents.push({ name: "docs/", grade: "missing", path: null });
|
|
5189
5939
|
}
|
|
5190
|
-
const indexPath =
|
|
5191
|
-
if (
|
|
5192
|
-
const srcDir =
|
|
5193
|
-
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;
|
|
5194
5944
|
const indexStale = isDocStale(indexPath, indexCodeDir);
|
|
5195
5945
|
documents.push({
|
|
5196
5946
|
name: "docs/index.md",
|
|
@@ -5207,8 +5957,8 @@ function auditDocumentation(dir) {
|
|
|
5207
5957
|
|
|
5208
5958
|
// src/lib/epic-generator.ts
|
|
5209
5959
|
import { createInterface } from "readline";
|
|
5210
|
-
import { existsSync as
|
|
5211
|
-
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";
|
|
5212
5962
|
var PRIORITY_BY_TYPE = {
|
|
5213
5963
|
observability: 1,
|
|
5214
5964
|
coverage: 2,
|
|
@@ -5246,8 +5996,8 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
|
|
|
5246
5996
|
storyNum++;
|
|
5247
5997
|
}
|
|
5248
5998
|
for (const mod of scan.modules) {
|
|
5249
|
-
const agentsPath =
|
|
5250
|
-
if (!
|
|
5999
|
+
const agentsPath = join20(root, mod.path, "AGENTS.md");
|
|
6000
|
+
if (!existsSync22(agentsPath)) {
|
|
5251
6001
|
stories.push({
|
|
5252
6002
|
key: `0.${storyNum}`,
|
|
5253
6003
|
title: `Create ${mod.path}/AGENTS.md`,
|
|
@@ -5313,7 +6063,7 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
|
|
|
5313
6063
|
};
|
|
5314
6064
|
}
|
|
5315
6065
|
function writeOnboardingEpic(epic, outputPath) {
|
|
5316
|
-
|
|
6066
|
+
mkdirSync7(dirname6(outputPath), { recursive: true });
|
|
5317
6067
|
const lines = [];
|
|
5318
6068
|
lines.push(`# ${epic.title}`);
|
|
5319
6069
|
lines.push("");
|
|
@@ -5349,14 +6099,14 @@ function writeOnboardingEpic(epic, outputPath) {
|
|
|
5349
6099
|
lines.push("");
|
|
5350
6100
|
lines.push("Review and approve before execution.");
|
|
5351
6101
|
lines.push("");
|
|
5352
|
-
|
|
6102
|
+
writeFileSync12(outputPath, lines.join("\n"), "utf-8");
|
|
5353
6103
|
}
|
|
5354
6104
|
function formatEpicSummary(epic) {
|
|
5355
6105
|
const { totalStories, coverageStories, docStories, verificationStories, observabilityStories } = epic.summary;
|
|
5356
6106
|
return `Onboarding plan: ${totalStories} stories (${coverageStories} coverage, ${docStories} documentation, ${verificationStories} verification, ${observabilityStories} observability)`;
|
|
5357
6107
|
}
|
|
5358
6108
|
function promptApproval() {
|
|
5359
|
-
return new Promise((
|
|
6109
|
+
return new Promise((resolve2) => {
|
|
5360
6110
|
let answered = false;
|
|
5361
6111
|
const rl = createInterface({
|
|
5362
6112
|
input: process.stdin,
|
|
@@ -5365,14 +6115,14 @@ function promptApproval() {
|
|
|
5365
6115
|
rl.on("close", () => {
|
|
5366
6116
|
if (!answered) {
|
|
5367
6117
|
answered = true;
|
|
5368
|
-
|
|
6118
|
+
resolve2(false);
|
|
5369
6119
|
}
|
|
5370
6120
|
});
|
|
5371
6121
|
rl.question("Review the onboarding plan. Approve? [Y/n] ", (answer) => {
|
|
5372
6122
|
answered = true;
|
|
5373
6123
|
rl.close();
|
|
5374
6124
|
const trimmed = answer.trim().toLowerCase();
|
|
5375
|
-
|
|
6125
|
+
resolve2(trimmed === "" || trimmed === "y");
|
|
5376
6126
|
});
|
|
5377
6127
|
});
|
|
5378
6128
|
}
|
|
@@ -5445,29 +6195,29 @@ function getGapIdFromTitle(title) {
|
|
|
5445
6195
|
}
|
|
5446
6196
|
|
|
5447
6197
|
// src/lib/scan-cache.ts
|
|
5448
|
-
import { existsSync as
|
|
5449
|
-
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";
|
|
5450
6200
|
var CACHE_DIR = ".harness";
|
|
5451
6201
|
var CACHE_FILE = "last-onboard-scan.json";
|
|
5452
6202
|
var DEFAULT_MAX_AGE_MS = 864e5;
|
|
5453
6203
|
function saveScanCache(entry, dir) {
|
|
5454
6204
|
try {
|
|
5455
6205
|
const root = dir ?? process.cwd();
|
|
5456
|
-
const cacheDir =
|
|
5457
|
-
|
|
5458
|
-
const cachePath =
|
|
5459
|
-
|
|
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");
|
|
5460
6210
|
} catch {
|
|
5461
6211
|
}
|
|
5462
6212
|
}
|
|
5463
6213
|
function loadScanCache(dir) {
|
|
5464
6214
|
const root = dir ?? process.cwd();
|
|
5465
|
-
const cachePath =
|
|
5466
|
-
if (!
|
|
6215
|
+
const cachePath = join21(root, CACHE_DIR, CACHE_FILE);
|
|
6216
|
+
if (!existsSync23(cachePath)) {
|
|
5467
6217
|
return null;
|
|
5468
6218
|
}
|
|
5469
6219
|
try {
|
|
5470
|
-
const raw =
|
|
6220
|
+
const raw = readFileSync20(cachePath, "utf-8");
|
|
5471
6221
|
return JSON.parse(raw);
|
|
5472
6222
|
} catch {
|
|
5473
6223
|
return null;
|
|
@@ -5640,7 +6390,7 @@ function registerOnboardCommand(program) {
|
|
|
5640
6390
|
}
|
|
5641
6391
|
coverage = lastCoverageResult ?? runCoverageAnalysis(scan);
|
|
5642
6392
|
audit = lastAuditResult ?? runAudit();
|
|
5643
|
-
const epicPath =
|
|
6393
|
+
const epicPath = join22(process.cwd(), "ralph", "onboarding-epic.md");
|
|
5644
6394
|
const epic = generateOnboardingEpic(scan, coverage, audit);
|
|
5645
6395
|
mergeExtendedGaps(epic);
|
|
5646
6396
|
if (!isFull) {
|
|
@@ -5713,7 +6463,7 @@ function registerOnboardCommand(program) {
|
|
|
5713
6463
|
coverage,
|
|
5714
6464
|
audit
|
|
5715
6465
|
});
|
|
5716
|
-
const epicPath =
|
|
6466
|
+
const epicPath = join22(process.cwd(), "ralph", "onboarding-epic.md");
|
|
5717
6467
|
const epic = generateOnboardingEpic(scan, coverage, audit);
|
|
5718
6468
|
mergeExtendedGaps(epic);
|
|
5719
6469
|
if (!isFull) {
|
|
@@ -5821,8 +6571,8 @@ function printEpicOutput(epic) {
|
|
|
5821
6571
|
}
|
|
5822
6572
|
|
|
5823
6573
|
// src/commands/teardown.ts
|
|
5824
|
-
import { existsSync as
|
|
5825
|
-
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";
|
|
5826
6576
|
function buildDefaultResult() {
|
|
5827
6577
|
return {
|
|
5828
6578
|
status: "ok",
|
|
@@ -5925,16 +6675,16 @@ function registerTeardownCommand(program) {
|
|
|
5925
6675
|
info("Docker stack: not running, skipping");
|
|
5926
6676
|
}
|
|
5927
6677
|
}
|
|
5928
|
-
const composeFilePath =
|
|
5929
|
-
if (
|
|
6678
|
+
const composeFilePath = join23(projectDir, composeFile);
|
|
6679
|
+
if (existsSync24(composeFilePath)) {
|
|
5930
6680
|
unlinkSync2(composeFilePath);
|
|
5931
6681
|
result.removed.push(composeFile);
|
|
5932
6682
|
if (!isJson) {
|
|
5933
6683
|
ok(`Removed: ${composeFile}`);
|
|
5934
6684
|
}
|
|
5935
6685
|
}
|
|
5936
|
-
const otelConfigPath =
|
|
5937
|
-
if (
|
|
6686
|
+
const otelConfigPath = join23(projectDir, "otel-collector-config.yaml");
|
|
6687
|
+
if (existsSync24(otelConfigPath)) {
|
|
5938
6688
|
unlinkSync2(otelConfigPath);
|
|
5939
6689
|
result.removed.push("otel-collector-config.yaml");
|
|
5940
6690
|
if (!isJson) {
|
|
@@ -5944,8 +6694,8 @@ function registerTeardownCommand(program) {
|
|
|
5944
6694
|
}
|
|
5945
6695
|
let patchesRemoved = 0;
|
|
5946
6696
|
for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
|
|
5947
|
-
const filePath =
|
|
5948
|
-
if (!
|
|
6697
|
+
const filePath = join23(projectDir, "_bmad", relativePath);
|
|
6698
|
+
if (!existsSync24(filePath)) {
|
|
5949
6699
|
continue;
|
|
5950
6700
|
}
|
|
5951
6701
|
try {
|
|
@@ -5965,10 +6715,10 @@ function registerTeardownCommand(program) {
|
|
|
5965
6715
|
}
|
|
5966
6716
|
}
|
|
5967
6717
|
if (state.otlp?.enabled && state.stack === "nodejs") {
|
|
5968
|
-
const pkgPath =
|
|
5969
|
-
if (
|
|
6718
|
+
const pkgPath = join23(projectDir, "package.json");
|
|
6719
|
+
if (existsSync24(pkgPath)) {
|
|
5970
6720
|
try {
|
|
5971
|
-
const raw =
|
|
6721
|
+
const raw = readFileSync21(pkgPath, "utf-8");
|
|
5972
6722
|
const pkg = JSON.parse(raw);
|
|
5973
6723
|
const scripts = pkg["scripts"];
|
|
5974
6724
|
if (scripts) {
|
|
@@ -5982,7 +6732,7 @@ function registerTeardownCommand(program) {
|
|
|
5982
6732
|
for (const key of keysToRemove) {
|
|
5983
6733
|
delete scripts[key];
|
|
5984
6734
|
}
|
|
5985
|
-
|
|
6735
|
+
writeFileSync14(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
5986
6736
|
result.otlp_cleaned = true;
|
|
5987
6737
|
if (!isJson) {
|
|
5988
6738
|
ok("OTLP: removed instrumented scripts from package.json");
|
|
@@ -6008,17 +6758,17 @@ function registerTeardownCommand(program) {
|
|
|
6008
6758
|
}
|
|
6009
6759
|
}
|
|
6010
6760
|
}
|
|
6011
|
-
const harnessDir =
|
|
6012
|
-
if (
|
|
6761
|
+
const harnessDir = join23(projectDir, ".harness");
|
|
6762
|
+
if (existsSync24(harnessDir)) {
|
|
6013
6763
|
rmSync(harnessDir, { recursive: true, force: true });
|
|
6014
6764
|
result.removed.push(".harness/");
|
|
6015
6765
|
if (!isJson) {
|
|
6016
6766
|
ok("Removed: .harness/");
|
|
6017
6767
|
}
|
|
6018
6768
|
}
|
|
6019
|
-
const
|
|
6020
|
-
if (
|
|
6021
|
-
unlinkSync2(
|
|
6769
|
+
const statePath2 = getStatePath(projectDir);
|
|
6770
|
+
if (existsSync24(statePath2)) {
|
|
6771
|
+
unlinkSync2(statePath2);
|
|
6022
6772
|
result.removed.push(".claude/codeharness.local.md");
|
|
6023
6773
|
if (!isJson) {
|
|
6024
6774
|
ok("Removed: .claude/codeharness.local.md");
|
|
@@ -6761,8 +7511,8 @@ function registerQueryCommand(program) {
|
|
|
6761
7511
|
}
|
|
6762
7512
|
|
|
6763
7513
|
// src/commands/retro-import.ts
|
|
6764
|
-
import { existsSync as
|
|
6765
|
-
import { join as
|
|
7514
|
+
import { existsSync as existsSync25, readFileSync as readFileSync22 } from "fs";
|
|
7515
|
+
import { join as join24 } from "path";
|
|
6766
7516
|
|
|
6767
7517
|
// src/lib/retro-parser.ts
|
|
6768
7518
|
var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
|
|
@@ -6931,15 +7681,15 @@ function registerRetroImportCommand(program) {
|
|
|
6931
7681
|
return;
|
|
6932
7682
|
}
|
|
6933
7683
|
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
6934
|
-
const retroPath =
|
|
6935
|
-
if (!
|
|
7684
|
+
const retroPath = join24(root, STORY_DIR2, retroFile);
|
|
7685
|
+
if (!existsSync25(retroPath)) {
|
|
6936
7686
|
fail(`Retro file not found: ${retroFile}`, { json: isJson });
|
|
6937
7687
|
process.exitCode = 1;
|
|
6938
7688
|
return;
|
|
6939
7689
|
}
|
|
6940
7690
|
let content;
|
|
6941
7691
|
try {
|
|
6942
|
-
content =
|
|
7692
|
+
content = readFileSync22(retroPath, "utf-8");
|
|
6943
7693
|
} catch (err) {
|
|
6944
7694
|
const message = err instanceof Error ? err.message : String(err);
|
|
6945
7695
|
fail(`Failed to read retro file: ${message}`, { json: isJson });
|
|
@@ -7207,8 +7957,8 @@ function registerGithubImportCommand(program) {
|
|
|
7207
7957
|
|
|
7208
7958
|
// src/lib/verify-env.ts
|
|
7209
7959
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
7210
|
-
import { existsSync as
|
|
7211
|
-
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";
|
|
7212
7962
|
import { createHash } from "crypto";
|
|
7213
7963
|
var IMAGE_TAG = "codeharness-verify";
|
|
7214
7964
|
var STORY_DIR3 = "_bmad-output/implementation-artifacts";
|
|
@@ -7221,14 +7971,14 @@ function isValidStoryKey(storyKey) {
|
|
|
7221
7971
|
return /^[a-zA-Z0-9_-]+$/.test(storyKey);
|
|
7222
7972
|
}
|
|
7223
7973
|
function computeDistHash(projectDir) {
|
|
7224
|
-
const distDir =
|
|
7225
|
-
if (!
|
|
7974
|
+
const distDir = join25(projectDir, "dist");
|
|
7975
|
+
if (!existsSync26(distDir)) {
|
|
7226
7976
|
return null;
|
|
7227
7977
|
}
|
|
7228
7978
|
const hash = createHash("sha256");
|
|
7229
7979
|
const files = collectFiles(distDir).sort();
|
|
7230
7980
|
for (const file of files) {
|
|
7231
|
-
const content =
|
|
7981
|
+
const content = readFileSync23(file);
|
|
7232
7982
|
hash.update(file.slice(distDir.length));
|
|
7233
7983
|
hash.update(content);
|
|
7234
7984
|
}
|
|
@@ -7238,7 +7988,7 @@ function collectFiles(dir) {
|
|
|
7238
7988
|
const results = [];
|
|
7239
7989
|
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
7240
7990
|
for (const entry of entries) {
|
|
7241
|
-
const fullPath =
|
|
7991
|
+
const fullPath = join25(dir, entry.name);
|
|
7242
7992
|
if (entry.isDirectory()) {
|
|
7243
7993
|
results.push(...collectFiles(fullPath));
|
|
7244
7994
|
} else {
|
|
@@ -7310,13 +8060,13 @@ function buildNodeImage(projectDir) {
|
|
|
7310
8060
|
throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
|
|
7311
8061
|
}
|
|
7312
8062
|
const tarballName = basename3(lastLine);
|
|
7313
|
-
const tarballPath =
|
|
7314
|
-
const buildContext =
|
|
7315
|
-
|
|
8063
|
+
const tarballPath = join25("/tmp", tarballName);
|
|
8064
|
+
const buildContext = join25("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
8065
|
+
mkdirSync9(buildContext, { recursive: true });
|
|
7316
8066
|
try {
|
|
7317
|
-
cpSync(tarballPath,
|
|
8067
|
+
cpSync(tarballPath, join25(buildContext, tarballName));
|
|
7318
8068
|
const dockerfileSrc = resolveDockerfileTemplate(projectDir);
|
|
7319
|
-
cpSync(dockerfileSrc,
|
|
8069
|
+
cpSync(dockerfileSrc, join25(buildContext, "Dockerfile"));
|
|
7320
8070
|
execFileSync7("docker", [
|
|
7321
8071
|
"build",
|
|
7322
8072
|
"-t",
|
|
@@ -7335,7 +8085,7 @@ function buildNodeImage(projectDir) {
|
|
|
7335
8085
|
}
|
|
7336
8086
|
}
|
|
7337
8087
|
function buildPythonImage(projectDir) {
|
|
7338
|
-
const distDir =
|
|
8088
|
+
const distDir = join25(projectDir, "dist");
|
|
7339
8089
|
const distFiles = readdirSync4(distDir).filter(
|
|
7340
8090
|
(f) => f.endsWith(".tar.gz") || f.endsWith(".whl")
|
|
7341
8091
|
);
|
|
@@ -7343,12 +8093,12 @@ function buildPythonImage(projectDir) {
|
|
|
7343
8093
|
throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
|
|
7344
8094
|
}
|
|
7345
8095
|
const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
|
|
7346
|
-
const buildContext =
|
|
7347
|
-
|
|
8096
|
+
const buildContext = join25("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
8097
|
+
mkdirSync9(buildContext, { recursive: true });
|
|
7348
8098
|
try {
|
|
7349
|
-
cpSync(
|
|
8099
|
+
cpSync(join25(distDir, distFile), join25(buildContext, distFile));
|
|
7350
8100
|
const dockerfileSrc = resolveDockerfileTemplate(projectDir);
|
|
7351
|
-
cpSync(dockerfileSrc,
|
|
8101
|
+
cpSync(dockerfileSrc, join25(buildContext, "Dockerfile"));
|
|
7352
8102
|
execFileSync7("docker", [
|
|
7353
8103
|
"build",
|
|
7354
8104
|
"-t",
|
|
@@ -7370,25 +8120,25 @@ function prepareVerifyWorkspace(storyKey, projectDir) {
|
|
|
7370
8120
|
if (!isValidStoryKey(storyKey)) {
|
|
7371
8121
|
throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
|
|
7372
8122
|
}
|
|
7373
|
-
const storyFile =
|
|
7374
|
-
if (!
|
|
8123
|
+
const storyFile = join25(root, STORY_DIR3, `${storyKey}.md`);
|
|
8124
|
+
if (!existsSync26(storyFile)) {
|
|
7375
8125
|
throw new Error(`Story file not found: ${storyFile}`);
|
|
7376
8126
|
}
|
|
7377
8127
|
const workspace = `${TEMP_PREFIX}${storyKey}`;
|
|
7378
|
-
if (
|
|
8128
|
+
if (existsSync26(workspace)) {
|
|
7379
8129
|
rmSync2(workspace, { recursive: true, force: true });
|
|
7380
8130
|
}
|
|
7381
|
-
|
|
7382
|
-
cpSync(storyFile,
|
|
7383
|
-
const readmePath =
|
|
7384
|
-
if (
|
|
7385
|
-
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"));
|
|
7386
8136
|
}
|
|
7387
|
-
const docsDir =
|
|
7388
|
-
if (
|
|
7389
|
-
cpSync(docsDir,
|
|
8137
|
+
const docsDir = join25(root, "docs");
|
|
8138
|
+
if (existsSync26(docsDir) && statSync3(docsDir).isDirectory()) {
|
|
8139
|
+
cpSync(docsDir, join25(workspace, "docs"), { recursive: true });
|
|
7390
8140
|
}
|
|
7391
|
-
|
|
8141
|
+
mkdirSync9(join25(workspace, "verification"), { recursive: true });
|
|
7392
8142
|
return workspace;
|
|
7393
8143
|
}
|
|
7394
8144
|
function checkVerifyEnv() {
|
|
@@ -7437,7 +8187,7 @@ function cleanupVerifyEnv(storyKey) {
|
|
|
7437
8187
|
}
|
|
7438
8188
|
const workspace = `${TEMP_PREFIX}${storyKey}`;
|
|
7439
8189
|
const containerName = `codeharness-verify-${storyKey}`;
|
|
7440
|
-
if (
|
|
8190
|
+
if (existsSync26(workspace)) {
|
|
7441
8191
|
rmSync2(workspace, { recursive: true, force: true });
|
|
7442
8192
|
}
|
|
7443
8193
|
try {
|
|
@@ -7456,11 +8206,11 @@ function cleanupVerifyEnv(storyKey) {
|
|
|
7456
8206
|
}
|
|
7457
8207
|
}
|
|
7458
8208
|
function resolveDockerfileTemplate(projectDir) {
|
|
7459
|
-
const local =
|
|
7460
|
-
if (
|
|
8209
|
+
const local = join25(projectDir, "templates", "Dockerfile.verify");
|
|
8210
|
+
if (existsSync26(local)) return local;
|
|
7461
8211
|
const pkgDir = new URL("../../", import.meta.url).pathname;
|
|
7462
|
-
const pkg =
|
|
7463
|
-
if (
|
|
8212
|
+
const pkg = join25(pkgDir, "templates", "Dockerfile.verify");
|
|
8213
|
+
if (existsSync26(pkg)) return pkg;
|
|
7464
8214
|
throw new Error("Dockerfile.verify not found. Ensure templates/Dockerfile.verify exists in the project or installed package.");
|
|
7465
8215
|
}
|
|
7466
8216
|
function dockerImageExists(tag) {
|
|
@@ -7606,26 +8356,26 @@ function registerVerifyEnvCommand(program) {
|
|
|
7606
8356
|
}
|
|
7607
8357
|
|
|
7608
8358
|
// src/commands/retry.ts
|
|
7609
|
-
import { join as
|
|
8359
|
+
import { join as join27 } from "path";
|
|
7610
8360
|
|
|
7611
8361
|
// src/lib/retry-state.ts
|
|
7612
|
-
import { existsSync as
|
|
7613
|
-
import { join as
|
|
8362
|
+
import { existsSync as existsSync27, readFileSync as readFileSync24, writeFileSync as writeFileSync15 } from "fs";
|
|
8363
|
+
import { join as join26 } from "path";
|
|
7614
8364
|
var RETRIES_FILE = ".story_retries";
|
|
7615
8365
|
var FLAGGED_FILE = ".flagged_stories";
|
|
7616
8366
|
var LINE_PATTERN = /^([^=]+)=(\d+)$/;
|
|
7617
8367
|
function retriesPath(dir) {
|
|
7618
|
-
return
|
|
8368
|
+
return join26(dir, RETRIES_FILE);
|
|
7619
8369
|
}
|
|
7620
8370
|
function flaggedPath(dir) {
|
|
7621
|
-
return
|
|
8371
|
+
return join26(dir, FLAGGED_FILE);
|
|
7622
8372
|
}
|
|
7623
8373
|
function readRetries(dir) {
|
|
7624
8374
|
const filePath = retriesPath(dir);
|
|
7625
|
-
if (!
|
|
8375
|
+
if (!existsSync27(filePath)) {
|
|
7626
8376
|
return /* @__PURE__ */ new Map();
|
|
7627
8377
|
}
|
|
7628
|
-
const raw =
|
|
8378
|
+
const raw = readFileSync24(filePath, "utf-8");
|
|
7629
8379
|
const result = /* @__PURE__ */ new Map();
|
|
7630
8380
|
for (const line of raw.split("\n")) {
|
|
7631
8381
|
const trimmed = line.trim();
|
|
@@ -7647,7 +8397,7 @@ function writeRetries(dir, retries) {
|
|
|
7647
8397
|
for (const [key, count] of retries) {
|
|
7648
8398
|
lines.push(`${key}=${count}`);
|
|
7649
8399
|
}
|
|
7650
|
-
|
|
8400
|
+
writeFileSync15(filePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
|
|
7651
8401
|
}
|
|
7652
8402
|
function resetRetry(dir, storyKey) {
|
|
7653
8403
|
if (storyKey) {
|
|
@@ -7662,15 +8412,15 @@ function resetRetry(dir, storyKey) {
|
|
|
7662
8412
|
}
|
|
7663
8413
|
function readFlaggedStories(dir) {
|
|
7664
8414
|
const filePath = flaggedPath(dir);
|
|
7665
|
-
if (!
|
|
8415
|
+
if (!existsSync27(filePath)) {
|
|
7666
8416
|
return [];
|
|
7667
8417
|
}
|
|
7668
|
-
const raw =
|
|
8418
|
+
const raw = readFileSync24(filePath, "utf-8");
|
|
7669
8419
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l !== "");
|
|
7670
8420
|
}
|
|
7671
8421
|
function writeFlaggedStories(dir, stories) {
|
|
7672
8422
|
const filePath = flaggedPath(dir);
|
|
7673
|
-
|
|
8423
|
+
writeFileSync15(filePath, stories.length > 0 ? stories.join("\n") + "\n" : "", "utf-8");
|
|
7674
8424
|
}
|
|
7675
8425
|
function removeFlaggedStory(dir, key) {
|
|
7676
8426
|
const stories = readFlaggedStories(dir);
|
|
@@ -7690,7 +8440,7 @@ function registerRetryCommand(program) {
|
|
|
7690
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) => {
|
|
7691
8441
|
const opts = cmd.optsWithGlobals();
|
|
7692
8442
|
const isJson = opts.json === true;
|
|
7693
|
-
const dir =
|
|
8443
|
+
const dir = join27(process.cwd(), RALPH_SUBDIR);
|
|
7694
8444
|
if (opts.story && !isValidStoryKey3(opts.story)) {
|
|
7695
8445
|
if (isJson) {
|
|
7696
8446
|
jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
|
|
@@ -7763,8 +8513,53 @@ function handleStatus(dir, isJson, filterStory) {
|
|
|
7763
8513
|
}
|
|
7764
8514
|
}
|
|
7765
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
|
+
|
|
7766
8561
|
// src/index.ts
|
|
7767
|
-
var VERSION = true ? "0.
|
|
8562
|
+
var VERSION = true ? "0.19.2" : "0.0.0-dev";
|
|
7768
8563
|
function createProgram() {
|
|
7769
8564
|
const program = new Command();
|
|
7770
8565
|
program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
|
|
@@ -7785,6 +8580,7 @@ function createProgram() {
|
|
|
7785
8580
|
registerGithubImportCommand(program);
|
|
7786
8581
|
registerVerifyEnvCommand(program);
|
|
7787
8582
|
registerRetryCommand(program);
|
|
8583
|
+
registerTimeoutReportCommand(program);
|
|
7788
8584
|
return program;
|
|
7789
8585
|
}
|
|
7790
8586
|
if (!process.env["VITEST"]) {
|