codeharness 0.17.6 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +342 -393
- package/package.json +2 -1
- package/patches/dev-enforcement.md +34 -0
- package/patches/retro-enforcement.md +34 -0
- package/patches/review-enforcement.md +23 -0
- package/patches/sprint-planning.md +21 -0
- package/patches/story-verification.md +25 -0
package/dist/index.js
CHANGED
|
@@ -19,8 +19,8 @@ import {
|
|
|
19
19
|
import { Command } from "commander";
|
|
20
20
|
|
|
21
21
|
// src/commands/init.ts
|
|
22
|
-
import { existsSync as
|
|
23
|
-
import { join as
|
|
22
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
23
|
+
import { join as join7, basename as basename2 } from "path";
|
|
24
24
|
|
|
25
25
|
// src/lib/output.ts
|
|
26
26
|
function ok(message, options) {
|
|
@@ -865,8 +865,8 @@ function configureHookCoexistence(dir) {
|
|
|
865
865
|
|
|
866
866
|
// src/lib/bmad.ts
|
|
867
867
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
868
|
-
import { existsSync as
|
|
869
|
-
import { join as
|
|
868
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
869
|
+
import { join as join6 } from "path";
|
|
870
870
|
|
|
871
871
|
// src/lib/patch-engine.ts
|
|
872
872
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
@@ -935,133 +935,81 @@ function removePatch(filePath, patchName) {
|
|
|
935
935
|
}
|
|
936
936
|
|
|
937
937
|
// src/templates/bmad-patches.ts
|
|
938
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
939
|
+
import { join as join5, dirname as dirname2 } from "path";
|
|
940
|
+
import { fileURLToPath } from "url";
|
|
941
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
942
|
+
function readPatchFile(name) {
|
|
943
|
+
const projectRoot = join5(__dirname, "..", "..");
|
|
944
|
+
const projectPath = join5(projectRoot, "patches", `${name}.md`);
|
|
945
|
+
if (existsSync5(projectPath)) {
|
|
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();
|
|
951
|
+
}
|
|
952
|
+
return null;
|
|
953
|
+
}
|
|
938
954
|
function storyVerificationPatch() {
|
|
939
|
-
return `## Verification Requirements
|
|
955
|
+
return readPatchFile("story-verification") ?? `## Verification Requirements
|
|
940
956
|
|
|
941
|
-
- [ ] Showboat proof document created (
|
|
942
|
-
- [ ] All acceptance criteria verified with real-world evidence
|
|
957
|
+
- [ ] Showboat proof document created (verification/<story-key>-proof.md)
|
|
958
|
+
- [ ] All acceptance criteria verified with real-world evidence via docker exec
|
|
943
959
|
- [ ] Test coverage meets target (100%)
|
|
944
960
|
|
|
945
|
-
### Verification Tags
|
|
946
|
-
|
|
947
|
-
For each AC, append a verification tag to indicate how it can be verified:
|
|
948
|
-
- \`<!-- verification: cli-verifiable -->\` \u2014 AC can be verified by running CLI commands in a subprocess
|
|
949
|
-
- \`<!-- verification: integration-required -->\` \u2014 AC requires integration testing, multi-system interaction, or manual verification
|
|
950
|
-
|
|
951
|
-
ACs referencing workflows, sprint planning, user sessions, or external system interactions should be tagged as \`integration-required\`. If no tag is present, a heuristic classifier will attempt to determine verifiability at runtime.
|
|
952
|
-
|
|
953
961
|
## Documentation Requirements
|
|
954
962
|
|
|
955
|
-
- [ ] Relevant AGENTS.md files updated
|
|
956
|
-
- [ ] Exec-plan created in
|
|
963
|
+
- [ ] Relevant AGENTS.md files updated
|
|
964
|
+
- [ ] Exec-plan created in docs/exec-plans/active/<story-key>.md
|
|
957
965
|
|
|
958
966
|
## Testing Requirements
|
|
959
967
|
|
|
960
968
|
- [ ] Unit tests written for all new/changed code
|
|
961
|
-
- [ ] Integration tests for cross-module interactions
|
|
962
969
|
- [ ] Coverage target: 100%`;
|
|
963
970
|
}
|
|
964
971
|
function devEnforcementPatch() {
|
|
965
|
-
return `## Codeharness Enforcement
|
|
972
|
+
return readPatchFile("dev-enforcement") ?? `## Codeharness Enforcement
|
|
966
973
|
|
|
967
974
|
### Observability Check
|
|
968
975
|
- [ ] Query VictoriaLogs after test runs to verify telemetry flows
|
|
969
|
-
- [ ] Confirm logs, metrics, and traces are being collected
|
|
970
976
|
|
|
971
977
|
### Documentation Update
|
|
972
978
|
- [ ] AGENTS.md updated for all changed modules
|
|
973
|
-
- [ ] Exec-plan reflects current implementation state
|
|
974
979
|
|
|
975
980
|
### Test Enforcement
|
|
976
|
-
- [ ] All tests pass
|
|
977
|
-
- [ ] Coverage gate: 100% of new/changed code
|
|
978
|
-
- [ ] No skipped or pending tests without justification`;
|
|
981
|
+
- [ ] All tests pass
|
|
982
|
+
- [ ] Coverage gate: 100% of new/changed code`;
|
|
979
983
|
}
|
|
980
984
|
function reviewEnforcementPatch() {
|
|
981
|
-
return `## Codeharness Review Gates
|
|
985
|
+
return readPatchFile("review-enforcement") ?? `## Codeharness Review Gates
|
|
982
986
|
|
|
983
987
|
### Verification
|
|
984
|
-
- [ ]
|
|
988
|
+
- [ ] Proof document exists and passes codeharness verify
|
|
985
989
|
- [ ] All acceptance criteria have evidence in proof document
|
|
986
990
|
|
|
987
|
-
### Documentation Freshness
|
|
988
|
-
- [ ] AGENTS.md is current for all changed modules
|
|
989
|
-
- [ ] No stale references to removed or renamed modules
|
|
990
|
-
|
|
991
991
|
### Coverage
|
|
992
|
-
- [ ]
|
|
993
|
-
- [ ] No coverage regression in changed files
|
|
994
|
-
- [ ] Overall coverage meets project target`;
|
|
992
|
+
- [ ] No coverage regression in changed files`;
|
|
995
993
|
}
|
|
996
994
|
function retroEnforcementPatch() {
|
|
997
|
-
return `## Codeharness Quality Metrics
|
|
995
|
+
return readPatchFile("retro-enforcement") ?? `## Codeharness Quality Metrics
|
|
998
996
|
|
|
999
997
|
### Verification Effectiveness
|
|
1000
998
|
- [ ] How many ACs were caught by verification vs manual review?
|
|
1001
|
-
- [ ] Were there any false positives in
|
|
1002
|
-
- [ ] Time spent on verification vs value delivered
|
|
1003
|
-
|
|
1004
|
-
### Documentation Health
|
|
1005
|
-
- [ ] AGENTS.md accuracy grade (A/B/C/D/F)
|
|
1006
|
-
- [ ] Exec-plans completeness \u2014 are all active stories documented?
|
|
1007
|
-
- [ ] Stale documentation identified and cleaned up
|
|
999
|
+
- [ ] Were there any false positives in proofs?
|
|
1008
1000
|
|
|
1009
1001
|
### Test Quality
|
|
1010
|
-
- [ ] Coverage trend (improving, stable, declining)
|
|
1011
|
-
- [ ] Test reliability \u2014 any flaky tests introduced?
|
|
1012
|
-
- [ ] Integration test coverage for cross-module interactions`;
|
|
1002
|
+
- [ ] Coverage trend (improving, stable, declining)`;
|
|
1013
1003
|
}
|
|
1014
1004
|
function sprintBeadsPatch() {
|
|
1015
|
-
return `## Codeharness
|
|
1016
|
-
|
|
1017
|
-
### Pre-Triage Import Verification
|
|
1018
|
-
- [ ] Confirm \`codeharness retro-import\` was run for all completed retrospectives
|
|
1019
|
-
- [ ] Confirm \`codeharness github-import\` was run to pull labeled GitHub issues
|
|
1020
|
-
- [ ] Verify all sources are reflected in beads before starting triage
|
|
1021
|
-
|
|
1022
|
-
### Beads Issue Status
|
|
1023
|
-
- [ ] Run \`bd ready\` to display issues ready for development
|
|
1024
|
-
- [ ] Review beads issue counts by status (open, in-progress, done)
|
|
1025
|
-
- [ ] Verify issues from all sources are visible: retro (\`[gap:retro:...]\`), GitHub (\`[source:github:...]\`), and manual
|
|
1026
|
-
- [ ] Verify no blocked issues without documented reason
|
|
1005
|
+
return readPatchFile("sprint-planning") ?? `## Codeharness Sprint Planning
|
|
1027
1006
|
|
|
1028
|
-
|
|
1029
|
-
- [ ]
|
|
1030
|
-
- [ ]
|
|
1031
|
-
- [ ] Capacity aligns with estimated story complexity`;
|
|
1007
|
+
- [ ] Review unresolved retrospective action items
|
|
1008
|
+
- [ ] Import from all backlog sources before triage
|
|
1009
|
+
- [ ] Verify story ACs are testable via CLI + Docker`;
|
|
1032
1010
|
}
|
|
1033
1011
|
function sprintPlanningRetroPatch() {
|
|
1034
|
-
return
|
|
1035
|
-
|
|
1036
|
-
### Unresolved Action Items from Previous Retrospectives
|
|
1037
|
-
|
|
1038
|
-
Before starting sprint planning, review all completed retrospectives for unresolved action items:
|
|
1039
|
-
|
|
1040
|
-
1. **Scan for retrospective files:** Look for all \`epic-N-retrospective.md\` files in \`_bmad-output/implementation-artifacts/\`
|
|
1041
|
-
2. **Import retro findings to beads:** For each retrospective not yet imported, run \`codeharness retro-import --epic N\` to classify findings and create beads issues with \`[gap:retro:epic-N-item-M]\` gap-ids
|
|
1042
|
-
3. **Import GitHub issues to beads:** Run \`codeharness github-import\` to pull labeled issues into beads with \`[source:github:owner/repo#N]\` gap-ids
|
|
1043
|
-
4. **Display combined backlog:** Run \`bd ready\` to present the unified backlog containing retro findings, GitHub issues, and manually created issues
|
|
1044
|
-
5. **Identify unresolved items:** Filter for action items that are NOT marked as completed/done
|
|
1045
|
-
6. **Surface during planning:** Present unresolved items to the team before selecting stories for the sprint
|
|
1046
|
-
|
|
1047
|
-
### Source-Aware Backlog Presentation
|
|
1048
|
-
|
|
1049
|
-
When presenting the backlog during triage, issues should be identifiable by source:
|
|
1050
|
-
|
|
1051
|
-
- **Retro findings** have gap-ids matching \`[gap:retro:...]\` \u2014 originated from retrospective action items
|
|
1052
|
-
- **GitHub issues** have gap-ids matching \`[source:github:...]\` \u2014 imported from GitHub via label query
|
|
1053
|
-
- **Manual issues** have no gap-id prefix \u2014 created directly in beads
|
|
1054
|
-
|
|
1055
|
-
### Integration with Sprint Planning
|
|
1056
|
-
|
|
1057
|
-
- [ ] All \`epic-N-retrospective.md\` files scanned for action items
|
|
1058
|
-
- [ ] \`codeharness retro-import --epic N\` run for each unimported retrospective
|
|
1059
|
-
- [ ] \`codeharness github-import\` run to pull labeled GitHub issues
|
|
1060
|
-
- [ ] \`bd ready\` run to display combined backlog from all sources
|
|
1061
|
-
- [ ] Unresolved action items listed and reviewed
|
|
1062
|
-
- [ ] Relevant action items incorporated into sprint goals or new stories
|
|
1063
|
-
- [ ] Recurring issues from multiple retros flagged for systemic fixes
|
|
1064
|
-
- [ ] All sources (retro, GitHub, manual) triaged uniformly \u2014 no source left unreviewed`;
|
|
1012
|
+
return sprintBeadsPatch();
|
|
1065
1013
|
}
|
|
1066
1014
|
var PATCH_TEMPLATES = {
|
|
1067
1015
|
"story-verification": storyVerificationPatch,
|
|
@@ -1090,15 +1038,15 @@ var PATCH_TARGETS = {
|
|
|
1090
1038
|
"sprint-retro": "bmm/workflows/4-implementation/sprint-planning/instructions.md"
|
|
1091
1039
|
};
|
|
1092
1040
|
function isBmadInstalled(dir) {
|
|
1093
|
-
const bmadDir =
|
|
1094
|
-
return
|
|
1041
|
+
const bmadDir = join6(dir ?? process.cwd(), "_bmad");
|
|
1042
|
+
return existsSync6(bmadDir);
|
|
1095
1043
|
}
|
|
1096
1044
|
function detectBmadVersion(dir) {
|
|
1097
1045
|
const root = dir ?? process.cwd();
|
|
1098
|
-
const moduleYamlPath =
|
|
1099
|
-
if (
|
|
1046
|
+
const moduleYamlPath = join6(root, "_bmad", "core", "module.yaml");
|
|
1047
|
+
if (existsSync6(moduleYamlPath)) {
|
|
1100
1048
|
try {
|
|
1101
|
-
const content =
|
|
1049
|
+
const content = readFileSync6(moduleYamlPath, "utf-8");
|
|
1102
1050
|
const versionMatch = content.match(/version:\s*["']?([^\s"']+)["']?/);
|
|
1103
1051
|
if (versionMatch) {
|
|
1104
1052
|
return versionMatch[1];
|
|
@@ -1106,17 +1054,17 @@ function detectBmadVersion(dir) {
|
|
|
1106
1054
|
} catch {
|
|
1107
1055
|
}
|
|
1108
1056
|
}
|
|
1109
|
-
const versionFilePath =
|
|
1110
|
-
if (
|
|
1057
|
+
const versionFilePath = join6(root, "_bmad", "VERSION");
|
|
1058
|
+
if (existsSync6(versionFilePath)) {
|
|
1111
1059
|
try {
|
|
1112
|
-
return
|
|
1060
|
+
return readFileSync6(versionFilePath, "utf-8").trim() || null;
|
|
1113
1061
|
} catch {
|
|
1114
1062
|
}
|
|
1115
1063
|
}
|
|
1116
|
-
const packageJsonPath =
|
|
1117
|
-
if (
|
|
1064
|
+
const packageJsonPath = join6(root, "_bmad", "package.json");
|
|
1065
|
+
if (existsSync6(packageJsonPath)) {
|
|
1118
1066
|
try {
|
|
1119
|
-
const pkg = JSON.parse(
|
|
1067
|
+
const pkg = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
|
|
1120
1068
|
return pkg.version ?? null;
|
|
1121
1069
|
} catch {
|
|
1122
1070
|
}
|
|
@@ -1134,15 +1082,27 @@ function installBmad(dir) {
|
|
|
1134
1082
|
};
|
|
1135
1083
|
}
|
|
1136
1084
|
const cmdStr = "npx bmad-method install";
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1085
|
+
const maxAttempts = 3;
|
|
1086
|
+
let lastError = "";
|
|
1087
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1088
|
+
try {
|
|
1089
|
+
execFileSync4("npx", ["bmad-method", "install"], {
|
|
1090
|
+
stdio: "pipe",
|
|
1091
|
+
timeout: 12e4,
|
|
1092
|
+
// 2 min — npx may download the package
|
|
1093
|
+
cwd: root
|
|
1094
|
+
});
|
|
1095
|
+
lastError = "";
|
|
1096
|
+
break;
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
1099
|
+
if (attempt < maxAttempts) {
|
|
1100
|
+
execFileSync4("sleep", ["2"]);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (lastError) {
|
|
1105
|
+
throw new BmadError(cmdStr, lastError);
|
|
1146
1106
|
}
|
|
1147
1107
|
if (!isBmadInstalled(root)) {
|
|
1148
1108
|
throw new BmadError(cmdStr, "_bmad/ directory was not created after successful npx bmad-method install");
|
|
@@ -1158,8 +1118,8 @@ function applyAllPatches(dir) {
|
|
|
1158
1118
|
const root = dir ?? process.cwd();
|
|
1159
1119
|
const results = [];
|
|
1160
1120
|
for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
|
|
1161
|
-
const targetFile =
|
|
1162
|
-
if (!
|
|
1121
|
+
const targetFile = join6(root, "_bmad", relativePath);
|
|
1122
|
+
if (!existsSync6(targetFile)) {
|
|
1163
1123
|
warn(`Patch target not found: ${relativePath}`);
|
|
1164
1124
|
results.push({
|
|
1165
1125
|
patchName,
|
|
@@ -1206,12 +1166,12 @@ function applyAllPatches(dir) {
|
|
|
1206
1166
|
function detectBmalph(dir) {
|
|
1207
1167
|
const root = dir ?? process.cwd();
|
|
1208
1168
|
const files = [];
|
|
1209
|
-
const ralphRcPath =
|
|
1210
|
-
if (
|
|
1169
|
+
const ralphRcPath = join6(root, ".ralph", ".ralphrc");
|
|
1170
|
+
if (existsSync6(ralphRcPath)) {
|
|
1211
1171
|
files.push(".ralph/.ralphrc");
|
|
1212
1172
|
}
|
|
1213
|
-
const dotRalphDir =
|
|
1214
|
-
if (
|
|
1173
|
+
const dotRalphDir = join6(root, ".ralph");
|
|
1174
|
+
if (existsSync6(dotRalphDir)) {
|
|
1215
1175
|
if (files.length === 0) {
|
|
1216
1176
|
files.push(".ralph/");
|
|
1217
1177
|
}
|
|
@@ -1226,10 +1186,10 @@ function getStoryFilePath(storyKey) {
|
|
|
1226
1186
|
return `_bmad-output/implementation-artifacts/${storyKey}.md`;
|
|
1227
1187
|
}
|
|
1228
1188
|
function parseEpicsFile(filePath) {
|
|
1229
|
-
if (!
|
|
1189
|
+
if (!existsSync6(filePath)) {
|
|
1230
1190
|
return [];
|
|
1231
1191
|
}
|
|
1232
|
-
const content =
|
|
1192
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
1233
1193
|
if (!content.trim()) {
|
|
1234
1194
|
return [];
|
|
1235
1195
|
}
|
|
@@ -1443,12 +1403,12 @@ function getInstallCommand(stack) {
|
|
|
1443
1403
|
}
|
|
1444
1404
|
|
|
1445
1405
|
// src/commands/init.ts
|
|
1446
|
-
var HARNESS_VERSION = true ? "0.
|
|
1406
|
+
var HARNESS_VERSION = true ? "0.18.0" : "0.0.0-dev";
|
|
1447
1407
|
function getProjectName(projectDir) {
|
|
1448
1408
|
try {
|
|
1449
|
-
const pkgPath =
|
|
1450
|
-
if (
|
|
1451
|
-
const pkg = JSON.parse(
|
|
1409
|
+
const pkgPath = join7(projectDir, "package.json");
|
|
1410
|
+
if (existsSync7(pkgPath)) {
|
|
1411
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
1452
1412
|
if (pkg.name && typeof pkg.name === "string") {
|
|
1453
1413
|
return pkg.name;
|
|
1454
1414
|
}
|
|
@@ -1558,7 +1518,7 @@ function registerInitCommand(program) {
|
|
|
1558
1518
|
}
|
|
1559
1519
|
};
|
|
1560
1520
|
const statePath = getStatePath(projectDir);
|
|
1561
|
-
if (
|
|
1521
|
+
if (existsSync7(statePath)) {
|
|
1562
1522
|
try {
|
|
1563
1523
|
const existingState = readState(projectDir);
|
|
1564
1524
|
const legacyObsDisabled = existingState.enforcement.observability === false;
|
|
@@ -1820,27 +1780,27 @@ function registerInitCommand(program) {
|
|
|
1820
1780
|
if (!isJson) {
|
|
1821
1781
|
ok("State file: .claude/codeharness.local.md created");
|
|
1822
1782
|
}
|
|
1823
|
-
const agentsMdPath =
|
|
1824
|
-
if (!
|
|
1783
|
+
const agentsMdPath = join7(projectDir, "AGENTS.md");
|
|
1784
|
+
if (!existsSync7(agentsMdPath)) {
|
|
1825
1785
|
const agentsMdContent = generateAgentsMdContent(projectDir, stack);
|
|
1826
1786
|
generateFile(agentsMdPath, agentsMdContent);
|
|
1827
1787
|
result.documentation.agents_md = "created";
|
|
1828
1788
|
} else {
|
|
1829
1789
|
result.documentation.agents_md = "exists";
|
|
1830
1790
|
}
|
|
1831
|
-
const docsDir =
|
|
1832
|
-
if (!
|
|
1833
|
-
generateFile(
|
|
1834
|
-
generateFile(
|
|
1835
|
-
generateFile(
|
|
1836
|
-
generateFile(
|
|
1837
|
-
generateFile(
|
|
1791
|
+
const docsDir = join7(projectDir, "docs");
|
|
1792
|
+
if (!existsSync7(docsDir)) {
|
|
1793
|
+
generateFile(join7(docsDir, "index.md"), generateDocsIndexContent());
|
|
1794
|
+
generateFile(join7(docsDir, "exec-plans", "active", ".gitkeep"), "");
|
|
1795
|
+
generateFile(join7(docsDir, "exec-plans", "completed", ".gitkeep"), "");
|
|
1796
|
+
generateFile(join7(docsDir, "quality", ".gitkeep"), DO_NOT_EDIT_HEADER);
|
|
1797
|
+
generateFile(join7(docsDir, "generated", ".gitkeep"), DO_NOT_EDIT_HEADER);
|
|
1838
1798
|
result.documentation.docs_scaffold = "created";
|
|
1839
1799
|
} else {
|
|
1840
1800
|
result.documentation.docs_scaffold = "exists";
|
|
1841
1801
|
}
|
|
1842
|
-
const readmePath =
|
|
1843
|
-
if (!
|
|
1802
|
+
const readmePath = join7(projectDir, "README.md");
|
|
1803
|
+
if (!existsSync7(readmePath)) {
|
|
1844
1804
|
let cliHelpOutput = "";
|
|
1845
1805
|
try {
|
|
1846
1806
|
const { execFileSync: execFileSync8 } = await import("child_process");
|
|
@@ -2047,7 +2007,7 @@ function registerInitCommand(program) {
|
|
|
2047
2007
|
}
|
|
2048
2008
|
|
|
2049
2009
|
// src/commands/bridge.ts
|
|
2050
|
-
import { existsSync as
|
|
2010
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2051
2011
|
function registerBridgeCommand(program) {
|
|
2052
2012
|
program.command("bridge").description("Bridge BMAD epics/stories into beads task store").option("--epics <path>", "Path to BMAD epics markdown file").option("--dry-run", "Parse and display without creating beads issues").action((opts, cmd) => {
|
|
2053
2013
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -2059,7 +2019,7 @@ function registerBridgeCommand(program) {
|
|
|
2059
2019
|
process.exitCode = 2;
|
|
2060
2020
|
return;
|
|
2061
2021
|
}
|
|
2062
|
-
if (!
|
|
2022
|
+
if (!existsSync8(epicsPath)) {
|
|
2063
2023
|
fail(`Epics file not found: ${epicsPath}`, { json: isJson });
|
|
2064
2024
|
process.exitCode = 1;
|
|
2065
2025
|
return;
|
|
@@ -2132,13 +2092,13 @@ function registerBridgeCommand(program) {
|
|
|
2132
2092
|
|
|
2133
2093
|
// src/commands/run.ts
|
|
2134
2094
|
import { spawn } from "child_process";
|
|
2135
|
-
import { existsSync as
|
|
2136
|
-
import { join as
|
|
2137
|
-
import { fileURLToPath } from "url";
|
|
2095
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
|
|
2096
|
+
import { join as join9, dirname as dirname3 } from "path";
|
|
2097
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2138
2098
|
|
|
2139
2099
|
// src/lib/beads-sync.ts
|
|
2140
|
-
import { existsSync as
|
|
2141
|
-
import { join as
|
|
2100
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
2101
|
+
import { join as join8 } from "path";
|
|
2142
2102
|
import { parse as parse2 } from "yaml";
|
|
2143
2103
|
var BEADS_TO_STORY_STATUS = {
|
|
2144
2104
|
open: "in-progress",
|
|
@@ -2169,10 +2129,10 @@ function resolveStoryFilePath(beadsIssue) {
|
|
|
2169
2129
|
return trimmed;
|
|
2170
2130
|
}
|
|
2171
2131
|
function readStoryFileStatus(filePath) {
|
|
2172
|
-
if (!
|
|
2132
|
+
if (!existsSync9(filePath)) {
|
|
2173
2133
|
return null;
|
|
2174
2134
|
}
|
|
2175
|
-
const content =
|
|
2135
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
2176
2136
|
const match = content.match(/^Status:\s*(.+)$/m);
|
|
2177
2137
|
if (!match) {
|
|
2178
2138
|
return null;
|
|
@@ -2180,7 +2140,7 @@ function readStoryFileStatus(filePath) {
|
|
|
2180
2140
|
return match[1].trim();
|
|
2181
2141
|
}
|
|
2182
2142
|
function updateStoryFileStatus(filePath, newStatus) {
|
|
2183
|
-
const content =
|
|
2143
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
2184
2144
|
const statusRegex = /^Status:\s*.+$/m;
|
|
2185
2145
|
if (statusRegex.test(content)) {
|
|
2186
2146
|
const updated = content.replace(statusRegex, `Status: ${newStatus}`);
|
|
@@ -2199,12 +2159,12 @@ function updateStoryFileStatus(filePath, newStatus) {
|
|
|
2199
2159
|
var SPRINT_STATUS_PATH = "_bmad-output/implementation-artifacts/sprint-status.yaml";
|
|
2200
2160
|
function readSprintStatus(dir) {
|
|
2201
2161
|
const root = dir ?? process.cwd();
|
|
2202
|
-
const filePath =
|
|
2203
|
-
if (!
|
|
2162
|
+
const filePath = join8(root, SPRINT_STATUS_PATH);
|
|
2163
|
+
if (!existsSync9(filePath)) {
|
|
2204
2164
|
return {};
|
|
2205
2165
|
}
|
|
2206
2166
|
try {
|
|
2207
|
-
const content =
|
|
2167
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
2208
2168
|
const parsed = parse2(content);
|
|
2209
2169
|
if (!parsed || typeof parsed !== "object") {
|
|
2210
2170
|
return {};
|
|
@@ -2220,12 +2180,12 @@ function readSprintStatus(dir) {
|
|
|
2220
2180
|
}
|
|
2221
2181
|
function updateSprintStatus(storyKey, newStatus, dir) {
|
|
2222
2182
|
const root = dir ?? process.cwd();
|
|
2223
|
-
const filePath =
|
|
2224
|
-
if (!
|
|
2183
|
+
const filePath = join8(root, SPRINT_STATUS_PATH);
|
|
2184
|
+
if (!existsSync9(filePath)) {
|
|
2225
2185
|
warn(`sprint-status.yaml not found at ${filePath}, skipping update`);
|
|
2226
2186
|
return;
|
|
2227
2187
|
}
|
|
2228
|
-
const content =
|
|
2188
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
2229
2189
|
const keyPattern = new RegExp(`^(\\s*${escapeRegExp(storyKey)}:\\s*)\\S+(.*)$`, "m");
|
|
2230
2190
|
if (!keyPattern.test(content)) {
|
|
2231
2191
|
return;
|
|
@@ -2252,8 +2212,8 @@ function slugify(title) {
|
|
|
2252
2212
|
}
|
|
2253
2213
|
function appendOnboardingEpicToSprint(stories, dir) {
|
|
2254
2214
|
const root = dir ?? process.cwd();
|
|
2255
|
-
const filePath =
|
|
2256
|
-
if (!
|
|
2215
|
+
const filePath = join8(root, SPRINT_STATUS_PATH);
|
|
2216
|
+
if (!existsSync9(filePath)) {
|
|
2257
2217
|
warn(`sprint-status.yaml not found at ${filePath}, cannot append onboarding epic`);
|
|
2258
2218
|
return { epicNumber: -1, storyKeys: [] };
|
|
2259
2219
|
}
|
|
@@ -2270,7 +2230,7 @@ function appendOnboardingEpicToSprint(stories, dir) {
|
|
|
2270
2230
|
}
|
|
2271
2231
|
lines.push(` epic-${epicNum}-retrospective: optional`);
|
|
2272
2232
|
lines.push("");
|
|
2273
|
-
const content =
|
|
2233
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
2274
2234
|
const updated = content.trimEnd() + "\n" + lines.join("\n");
|
|
2275
2235
|
writeFileSync5(filePath, updated, "utf-8");
|
|
2276
2236
|
return { epicNumber: epicNum, storyKeys };
|
|
@@ -2305,7 +2265,7 @@ function syncBeadsToStoryFile(beadsId, beadsFns, dir) {
|
|
|
2305
2265
|
};
|
|
2306
2266
|
}
|
|
2307
2267
|
const storyKey = storyKeyFromPath(storyFilePath);
|
|
2308
|
-
const fullPath =
|
|
2268
|
+
const fullPath = join8(root, storyFilePath);
|
|
2309
2269
|
const currentStoryStatus = readStoryFileStatus(fullPath);
|
|
2310
2270
|
if (currentStoryStatus === null) {
|
|
2311
2271
|
return {
|
|
@@ -2350,7 +2310,7 @@ function syncBeadsToStoryFile(beadsId, beadsFns, dir) {
|
|
|
2350
2310
|
function syncStoryFileToBeads(storyKey, beadsFns, dir) {
|
|
2351
2311
|
const root = dir ?? process.cwd();
|
|
2352
2312
|
const storyFilePath = `_bmad-output/implementation-artifacts/${storyKey}.md`;
|
|
2353
|
-
const fullPath =
|
|
2313
|
+
const fullPath = join8(root, storyFilePath);
|
|
2354
2314
|
const currentStoryStatus = readStoryFileStatus(fullPath);
|
|
2355
2315
|
if (currentStoryStatus === null) {
|
|
2356
2316
|
return {
|
|
@@ -2439,10 +2399,10 @@ function syncClose(beadsId, beadsFns, dir) {
|
|
|
2439
2399
|
};
|
|
2440
2400
|
}
|
|
2441
2401
|
const storyKey = storyKeyFromPath(storyFilePath);
|
|
2442
|
-
const fullPath =
|
|
2402
|
+
const fullPath = join8(root, storyFilePath);
|
|
2443
2403
|
const previousStatus = readStoryFileStatus(fullPath);
|
|
2444
2404
|
if (previousStatus === null) {
|
|
2445
|
-
if (!
|
|
2405
|
+
if (!existsSync9(fullPath)) {
|
|
2446
2406
|
return {
|
|
2447
2407
|
storyKey,
|
|
2448
2408
|
beadsId,
|
|
@@ -2587,16 +2547,16 @@ function generateRalphPrompt(config) {
|
|
|
2587
2547
|
var SPRINT_STATUS_REL = "_bmad-output/implementation-artifacts/sprint-status.yaml";
|
|
2588
2548
|
var STORY_KEY_PATTERN = /^\d+-\d+-/;
|
|
2589
2549
|
function resolveRalphPath() {
|
|
2590
|
-
const currentFile =
|
|
2591
|
-
const currentDir =
|
|
2592
|
-
let root =
|
|
2550
|
+
const currentFile = fileURLToPath2(import.meta.url);
|
|
2551
|
+
const currentDir = dirname3(currentFile);
|
|
2552
|
+
let root = dirname3(currentDir);
|
|
2593
2553
|
if (root.endsWith("/src") || root.endsWith("\\src")) {
|
|
2594
|
-
root =
|
|
2554
|
+
root = dirname3(root);
|
|
2595
2555
|
}
|
|
2596
|
-
return
|
|
2556
|
+
return join9(root, "ralph", "ralph.sh");
|
|
2597
2557
|
}
|
|
2598
2558
|
function resolvePluginDir() {
|
|
2599
|
-
return
|
|
2559
|
+
return join9(process.cwd(), ".claude");
|
|
2600
2560
|
}
|
|
2601
2561
|
function countStories(statuses) {
|
|
2602
2562
|
let total = 0;
|
|
@@ -2647,19 +2607,19 @@ function registerRunCommand(program) {
|
|
|
2647
2607
|
const isJson = !!globalOpts.json;
|
|
2648
2608
|
const outputOpts = { json: isJson };
|
|
2649
2609
|
const ralphPath = resolveRalphPath();
|
|
2650
|
-
if (!
|
|
2610
|
+
if (!existsSync10(ralphPath)) {
|
|
2651
2611
|
fail("Ralph loop not found \u2014 reinstall codeharness", outputOpts);
|
|
2652
2612
|
process.exitCode = 1;
|
|
2653
2613
|
return;
|
|
2654
2614
|
}
|
|
2655
2615
|
const pluginDir = resolvePluginDir();
|
|
2656
|
-
if (!
|
|
2616
|
+
if (!existsSync10(pluginDir)) {
|
|
2657
2617
|
fail("Plugin directory not found \u2014 run codeharness init first", outputOpts);
|
|
2658
2618
|
process.exitCode = 1;
|
|
2659
2619
|
return;
|
|
2660
2620
|
}
|
|
2661
2621
|
const projectDir = process.cwd();
|
|
2662
|
-
const sprintStatusPath =
|
|
2622
|
+
const sprintStatusPath = join9(projectDir, SPRINT_STATUS_REL);
|
|
2663
2623
|
const statuses = readSprintStatus(projectDir);
|
|
2664
2624
|
const counts = countStories(statuses);
|
|
2665
2625
|
if (counts.total === 0) {
|
|
@@ -2678,12 +2638,12 @@ function registerRunCommand(program) {
|
|
|
2678
2638
|
process.exitCode = 1;
|
|
2679
2639
|
return;
|
|
2680
2640
|
}
|
|
2681
|
-
const promptFile =
|
|
2682
|
-
const flaggedFilePath =
|
|
2641
|
+
const promptFile = join9(projectDir, "ralph", ".harness-prompt.md");
|
|
2642
|
+
const flaggedFilePath = join9(projectDir, "ralph", ".flagged_stories");
|
|
2683
2643
|
let flaggedStories;
|
|
2684
|
-
if (
|
|
2644
|
+
if (existsSync10(flaggedFilePath)) {
|
|
2685
2645
|
try {
|
|
2686
|
-
const flaggedContent =
|
|
2646
|
+
const flaggedContent = readFileSync9(flaggedFilePath, "utf-8");
|
|
2687
2647
|
flaggedStories = flaggedContent.split("\n").filter((s) => s.trim().length > 0);
|
|
2688
2648
|
} catch {
|
|
2689
2649
|
}
|
|
@@ -2694,7 +2654,7 @@ function registerRunCommand(program) {
|
|
|
2694
2654
|
flaggedStories
|
|
2695
2655
|
});
|
|
2696
2656
|
try {
|
|
2697
|
-
mkdirSync3(
|
|
2657
|
+
mkdirSync3(dirname3(promptFile), { recursive: true });
|
|
2698
2658
|
writeFileSync6(promptFile, promptContent, "utf-8");
|
|
2699
2659
|
} catch (err) {
|
|
2700
2660
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2733,10 +2693,10 @@ function registerRunCommand(program) {
|
|
|
2733
2693
|
});
|
|
2734
2694
|
});
|
|
2735
2695
|
if (isJson) {
|
|
2736
|
-
const statusFile =
|
|
2737
|
-
if (
|
|
2696
|
+
const statusFile = join9(projectDir, "ralph", "status.json");
|
|
2697
|
+
if (existsSync10(statusFile)) {
|
|
2738
2698
|
try {
|
|
2739
|
-
const statusData = JSON.parse(
|
|
2699
|
+
const statusData = JSON.parse(readFileSync9(statusFile, "utf-8"));
|
|
2740
2700
|
const finalStatuses = readSprintStatus(projectDir);
|
|
2741
2701
|
const finalCounts = countStories(finalStatuses);
|
|
2742
2702
|
jsonOutput({
|
|
@@ -2786,11 +2746,11 @@ function registerRunCommand(program) {
|
|
|
2786
2746
|
}
|
|
2787
2747
|
|
|
2788
2748
|
// src/commands/verify.ts
|
|
2789
|
-
import { existsSync as
|
|
2790
|
-
import { join as
|
|
2749
|
+
import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
|
|
2750
|
+
import { join as join12 } from "path";
|
|
2791
2751
|
|
|
2792
2752
|
// src/lib/verify-parser.ts
|
|
2793
|
-
import { existsSync as
|
|
2753
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
|
|
2794
2754
|
var UI_KEYWORDS = [
|
|
2795
2755
|
"agent-browser",
|
|
2796
2756
|
"screenshot",
|
|
@@ -2860,12 +2820,12 @@ function classifyAC(description) {
|
|
|
2860
2820
|
return "general";
|
|
2861
2821
|
}
|
|
2862
2822
|
function parseStoryACs(storyFilePath) {
|
|
2863
|
-
if (!
|
|
2823
|
+
if (!existsSync11(storyFilePath)) {
|
|
2864
2824
|
throw new Error(
|
|
2865
2825
|
`Story file not found: ${storyFilePath}. Ensure the story file exists at the expected path.`
|
|
2866
2826
|
);
|
|
2867
2827
|
}
|
|
2868
|
-
const content =
|
|
2828
|
+
const content = readFileSync10(storyFilePath, "utf-8");
|
|
2869
2829
|
const lines = content.split("\n");
|
|
2870
2830
|
let acSectionStart = -1;
|
|
2871
2831
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -2926,21 +2886,21 @@ function parseStoryACs(storyFilePath) {
|
|
|
2926
2886
|
|
|
2927
2887
|
// src/lib/verify.ts
|
|
2928
2888
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
2929
|
-
import { existsSync as
|
|
2930
|
-
import { join as
|
|
2889
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
|
|
2890
|
+
import { join as join11 } from "path";
|
|
2931
2891
|
|
|
2932
2892
|
// src/lib/doc-health.ts
|
|
2933
2893
|
import { execSync } from "child_process";
|
|
2934
2894
|
import {
|
|
2935
|
-
existsSync as
|
|
2895
|
+
existsSync as existsSync12,
|
|
2936
2896
|
mkdirSync as mkdirSync4,
|
|
2937
|
-
readFileSync as
|
|
2897
|
+
readFileSync as readFileSync11,
|
|
2938
2898
|
readdirSync as readdirSync2,
|
|
2939
2899
|
statSync,
|
|
2940
2900
|
unlinkSync,
|
|
2941
2901
|
writeFileSync as writeFileSync7
|
|
2942
2902
|
} from "fs";
|
|
2943
|
-
import { join as
|
|
2903
|
+
import { join as join10, relative } from "path";
|
|
2944
2904
|
var DO_NOT_EDIT_HEADER2 = "<!-- DO NOT EDIT MANUALLY";
|
|
2945
2905
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
|
|
2946
2906
|
var DEFAULT_MODULE_THRESHOLD = 3;
|
|
@@ -2962,7 +2922,7 @@ function findModules(dir, threshold) {
|
|
|
2962
2922
|
let sourceCount = 0;
|
|
2963
2923
|
const subdirs = [];
|
|
2964
2924
|
for (const entry of entries) {
|
|
2965
|
-
const fullPath =
|
|
2925
|
+
const fullPath = join10(current, entry);
|
|
2966
2926
|
let stat;
|
|
2967
2927
|
try {
|
|
2968
2928
|
stat = statSync(fullPath);
|
|
@@ -2999,8 +2959,8 @@ function isTestFile(filename) {
|
|
|
2999
2959
|
return filename.includes(".test.") || filename.includes(".spec.") || filename.includes("__tests__") || filename.startsWith("test_");
|
|
3000
2960
|
}
|
|
3001
2961
|
function isDocStale(docPath, codeDir) {
|
|
3002
|
-
if (!
|
|
3003
|
-
if (!
|
|
2962
|
+
if (!existsSync12(docPath)) return true;
|
|
2963
|
+
if (!existsSync12(codeDir)) return false;
|
|
3004
2964
|
const docMtime = statSync(docPath).mtime;
|
|
3005
2965
|
const newestCode = getNewestSourceMtime(codeDir);
|
|
3006
2966
|
if (newestCode === null) return false;
|
|
@@ -3018,7 +2978,7 @@ function getNewestSourceMtime(dir) {
|
|
|
3018
2978
|
const dirName = current.split("/").pop() ?? "";
|
|
3019
2979
|
if (dirName === "node_modules" || dirName === ".git") return;
|
|
3020
2980
|
for (const entry of entries) {
|
|
3021
|
-
const fullPath =
|
|
2981
|
+
const fullPath = join10(current, entry);
|
|
3022
2982
|
let stat;
|
|
3023
2983
|
try {
|
|
3024
2984
|
stat = statSync(fullPath);
|
|
@@ -3054,7 +3014,7 @@ function getSourceFilesInModule(modulePath) {
|
|
|
3054
3014
|
const dirName = current.split("/").pop() ?? "";
|
|
3055
3015
|
if (dirName === "node_modules" || dirName === ".git" || dirName === "__tests__" || dirName === "dist" || dirName === "coverage" || dirName.startsWith(".") && current !== modulePath) return;
|
|
3056
3016
|
for (const entry of entries) {
|
|
3057
|
-
const fullPath =
|
|
3017
|
+
const fullPath = join10(current, entry);
|
|
3058
3018
|
let stat;
|
|
3059
3019
|
try {
|
|
3060
3020
|
stat = statSync(fullPath);
|
|
@@ -3075,8 +3035,8 @@ function getSourceFilesInModule(modulePath) {
|
|
|
3075
3035
|
return files;
|
|
3076
3036
|
}
|
|
3077
3037
|
function getMentionedFilesInAgentsMd(agentsPath) {
|
|
3078
|
-
if (!
|
|
3079
|
-
const content =
|
|
3038
|
+
if (!existsSync12(agentsPath)) return [];
|
|
3039
|
+
const content = readFileSync11(agentsPath, "utf-8");
|
|
3080
3040
|
const mentioned = /* @__PURE__ */ new Set();
|
|
3081
3041
|
const filenamePattern = /[\w./-]*[\w-]+\.(?:ts|js|py)\b/g;
|
|
3082
3042
|
let match;
|
|
@@ -3100,12 +3060,12 @@ function checkAgentsMdCompleteness(agentsPath, modulePath) {
|
|
|
3100
3060
|
}
|
|
3101
3061
|
function checkAgentsMdForModule(modulePath, dir) {
|
|
3102
3062
|
const root = dir ?? process.cwd();
|
|
3103
|
-
const fullModulePath =
|
|
3104
|
-
let agentsPath =
|
|
3105
|
-
if (!
|
|
3106
|
-
agentsPath =
|
|
3063
|
+
const fullModulePath = join10(root, modulePath);
|
|
3064
|
+
let agentsPath = join10(fullModulePath, "AGENTS.md");
|
|
3065
|
+
if (!existsSync12(agentsPath)) {
|
|
3066
|
+
agentsPath = join10(root, "AGENTS.md");
|
|
3107
3067
|
}
|
|
3108
|
-
if (!
|
|
3068
|
+
if (!existsSync12(agentsPath)) {
|
|
3109
3069
|
return {
|
|
3110
3070
|
path: relative(root, agentsPath),
|
|
3111
3071
|
grade: "missing",
|
|
@@ -3136,9 +3096,9 @@ function checkAgentsMdForModule(modulePath, dir) {
|
|
|
3136
3096
|
};
|
|
3137
3097
|
}
|
|
3138
3098
|
function checkDoNotEditHeaders(docPath) {
|
|
3139
|
-
if (!
|
|
3099
|
+
if (!existsSync12(docPath)) return false;
|
|
3140
3100
|
try {
|
|
3141
|
-
const content =
|
|
3101
|
+
const content = readFileSync11(docPath, "utf-8");
|
|
3142
3102
|
if (content.length === 0) return false;
|
|
3143
3103
|
return content.trimStart().startsWith(DO_NOT_EDIT_HEADER2);
|
|
3144
3104
|
} catch {
|
|
@@ -3150,17 +3110,17 @@ function scanDocHealth(dir) {
|
|
|
3150
3110
|
const root = dir ?? process.cwd();
|
|
3151
3111
|
const documents = [];
|
|
3152
3112
|
const modules = findModules(root);
|
|
3153
|
-
const rootAgentsPath =
|
|
3154
|
-
if (
|
|
3113
|
+
const rootAgentsPath = join10(root, "AGENTS.md");
|
|
3114
|
+
if (existsSync12(rootAgentsPath)) {
|
|
3155
3115
|
if (modules.length > 0) {
|
|
3156
3116
|
const docMtime = statSync(rootAgentsPath).mtime;
|
|
3157
3117
|
let allMissing = [];
|
|
3158
3118
|
let staleModule = "";
|
|
3159
3119
|
let newestCode = null;
|
|
3160
3120
|
for (const mod of modules) {
|
|
3161
|
-
const fullModPath =
|
|
3162
|
-
const modAgentsPath =
|
|
3163
|
-
if (
|
|
3121
|
+
const fullModPath = join10(root, mod);
|
|
3122
|
+
const modAgentsPath = join10(fullModPath, "AGENTS.md");
|
|
3123
|
+
if (existsSync12(modAgentsPath)) continue;
|
|
3164
3124
|
const { missing } = checkAgentsMdCompleteness(rootAgentsPath, fullModPath);
|
|
3165
3125
|
if (missing.length > 0 && staleModule === "") {
|
|
3166
3126
|
staleModule = mod;
|
|
@@ -3208,8 +3168,8 @@ function scanDocHealth(dir) {
|
|
|
3208
3168
|
});
|
|
3209
3169
|
}
|
|
3210
3170
|
for (const mod of modules) {
|
|
3211
|
-
const modAgentsPath =
|
|
3212
|
-
if (
|
|
3171
|
+
const modAgentsPath = join10(root, mod, "AGENTS.md");
|
|
3172
|
+
if (existsSync12(modAgentsPath)) {
|
|
3213
3173
|
const result = checkAgentsMdForModule(mod, root);
|
|
3214
3174
|
if (result.path !== "AGENTS.md") {
|
|
3215
3175
|
documents.push(result);
|
|
@@ -3217,9 +3177,9 @@ function scanDocHealth(dir) {
|
|
|
3217
3177
|
}
|
|
3218
3178
|
}
|
|
3219
3179
|
}
|
|
3220
|
-
const indexPath =
|
|
3221
|
-
if (
|
|
3222
|
-
const content =
|
|
3180
|
+
const indexPath = join10(root, "docs", "index.md");
|
|
3181
|
+
if (existsSync12(indexPath)) {
|
|
3182
|
+
const content = readFileSync11(indexPath, "utf-8");
|
|
3223
3183
|
const hasAbsolutePaths = /https?:\/\/|file:\/\//i.test(content);
|
|
3224
3184
|
documents.push({
|
|
3225
3185
|
path: "docs/index.md",
|
|
@@ -3229,11 +3189,11 @@ function scanDocHealth(dir) {
|
|
|
3229
3189
|
reason: hasAbsolutePaths ? "Contains absolute URLs (may violate NFR25)" : "Uses relative paths"
|
|
3230
3190
|
});
|
|
3231
3191
|
}
|
|
3232
|
-
const activeDir =
|
|
3233
|
-
if (
|
|
3192
|
+
const activeDir = join10(root, "docs", "exec-plans", "active");
|
|
3193
|
+
if (existsSync12(activeDir)) {
|
|
3234
3194
|
const files = readdirSync2(activeDir).filter((f) => f.endsWith(".md"));
|
|
3235
3195
|
for (const file of files) {
|
|
3236
|
-
const filePath =
|
|
3196
|
+
const filePath = join10(activeDir, file);
|
|
3237
3197
|
documents.push({
|
|
3238
3198
|
path: `docs/exec-plans/active/${file}`,
|
|
3239
3199
|
grade: "fresh",
|
|
@@ -3244,11 +3204,11 @@ function scanDocHealth(dir) {
|
|
|
3244
3204
|
}
|
|
3245
3205
|
}
|
|
3246
3206
|
for (const subdir of ["quality", "generated"]) {
|
|
3247
|
-
const dirPath =
|
|
3248
|
-
if (!
|
|
3207
|
+
const dirPath = join10(root, "docs", subdir);
|
|
3208
|
+
if (!existsSync12(dirPath)) continue;
|
|
3249
3209
|
const files = readdirSync2(dirPath).filter((f) => !f.startsWith("."));
|
|
3250
3210
|
for (const file of files) {
|
|
3251
|
-
const filePath =
|
|
3211
|
+
const filePath = join10(dirPath, file);
|
|
3252
3212
|
let stat;
|
|
3253
3213
|
try {
|
|
3254
3214
|
stat = statSync(filePath);
|
|
@@ -3281,7 +3241,7 @@ function scanDocHealth(dir) {
|
|
|
3281
3241
|
}
|
|
3282
3242
|
function checkAgentsMdLineCount(filePath, docPath, documents) {
|
|
3283
3243
|
try {
|
|
3284
|
-
const content =
|
|
3244
|
+
const content = readFileSync11(filePath, "utf-8");
|
|
3285
3245
|
const lineCount = content.split("\n").length;
|
|
3286
3246
|
if (lineCount > 100) {
|
|
3287
3247
|
documents.push({
|
|
@@ -3319,15 +3279,15 @@ function checkStoryDocFreshness(storyId, dir) {
|
|
|
3319
3279
|
for (const mod of modulesToCheck) {
|
|
3320
3280
|
const result = checkAgentsMdForModule(mod, root);
|
|
3321
3281
|
documents.push(result);
|
|
3322
|
-
const moduleAgentsPath =
|
|
3323
|
-
const actualAgentsPath =
|
|
3324
|
-
if (
|
|
3282
|
+
const moduleAgentsPath = join10(root, mod, "AGENTS.md");
|
|
3283
|
+
const actualAgentsPath = existsSync12(moduleAgentsPath) ? moduleAgentsPath : join10(root, "AGENTS.md");
|
|
3284
|
+
if (existsSync12(actualAgentsPath)) {
|
|
3325
3285
|
checkAgentsMdLineCount(actualAgentsPath, result.path, documents);
|
|
3326
3286
|
}
|
|
3327
3287
|
}
|
|
3328
3288
|
if (modulesToCheck.length === 0) {
|
|
3329
|
-
const rootAgentsPath =
|
|
3330
|
-
if (
|
|
3289
|
+
const rootAgentsPath = join10(root, "AGENTS.md");
|
|
3290
|
+
if (existsSync12(rootAgentsPath)) {
|
|
3331
3291
|
documents.push({
|
|
3332
3292
|
path: "AGENTS.md",
|
|
3333
3293
|
grade: "fresh",
|
|
@@ -3365,11 +3325,11 @@ function getRecentlyChangedFiles(dir) {
|
|
|
3365
3325
|
}
|
|
3366
3326
|
function completeExecPlan(storyId, dir) {
|
|
3367
3327
|
const root = dir ?? process.cwd();
|
|
3368
|
-
const activePath =
|
|
3369
|
-
if (!
|
|
3328
|
+
const activePath = join10(root, "docs", "exec-plans", "active", `${storyId}.md`);
|
|
3329
|
+
if (!existsSync12(activePath)) {
|
|
3370
3330
|
return null;
|
|
3371
3331
|
}
|
|
3372
|
-
let content =
|
|
3332
|
+
let content = readFileSync11(activePath, "utf-8");
|
|
3373
3333
|
content = content.replace(/^Status:\s*active$/m, "Status: completed");
|
|
3374
3334
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3375
3335
|
content = content.replace(
|
|
@@ -3377,9 +3337,9 @@ function completeExecPlan(storyId, dir) {
|
|
|
3377
3337
|
`$1
|
|
3378
3338
|
Completed: ${timestamp}`
|
|
3379
3339
|
);
|
|
3380
|
-
const completedDir =
|
|
3340
|
+
const completedDir = join10(root, "docs", "exec-plans", "completed");
|
|
3381
3341
|
mkdirSync4(completedDir, { recursive: true });
|
|
3382
|
-
const completedPath =
|
|
3342
|
+
const completedPath = join10(completedDir, `${storyId}.md`);
|
|
3383
3343
|
writeFileSync7(completedPath, content, "utf-8");
|
|
3384
3344
|
try {
|
|
3385
3345
|
unlinkSync(activePath);
|
|
@@ -3571,8 +3531,8 @@ function checkPreconditions(dir, storyId) {
|
|
|
3571
3531
|
}
|
|
3572
3532
|
function createProofDocument(storyId, storyTitle, acs, dir) {
|
|
3573
3533
|
const root = dir ?? process.cwd();
|
|
3574
|
-
const verificationDir =
|
|
3575
|
-
const screenshotsDir =
|
|
3534
|
+
const verificationDir = join11(root, "verification");
|
|
3535
|
+
const screenshotsDir = join11(verificationDir, "screenshots");
|
|
3576
3536
|
mkdirSync5(verificationDir, { recursive: true });
|
|
3577
3537
|
mkdirSync5(screenshotsDir, { recursive: true });
|
|
3578
3538
|
const criteria = acs.map((ac) => ({
|
|
@@ -3586,7 +3546,7 @@ function createProofDocument(storyId, storyTitle, acs, dir) {
|
|
|
3586
3546
|
storyTitle,
|
|
3587
3547
|
acceptanceCriteria: criteria
|
|
3588
3548
|
});
|
|
3589
|
-
const proofPath =
|
|
3549
|
+
const proofPath = join11(verificationDir, `${storyId}-proof.md`);
|
|
3590
3550
|
writeFileSync8(proofPath, content, "utf-8");
|
|
3591
3551
|
return proofPath;
|
|
3592
3552
|
}
|
|
@@ -3620,10 +3580,10 @@ function validateProofQuality(proofPath) {
|
|
|
3620
3580
|
otherCount: 0,
|
|
3621
3581
|
blackBoxPass: false
|
|
3622
3582
|
};
|
|
3623
|
-
if (!
|
|
3583
|
+
if (!existsSync13(proofPath)) {
|
|
3624
3584
|
return emptyResult;
|
|
3625
3585
|
}
|
|
3626
|
-
const content =
|
|
3586
|
+
const content = readFileSync12(proofPath, "utf-8");
|
|
3627
3587
|
const bbEnforcement = checkBlackBoxEnforcement(content);
|
|
3628
3588
|
function buildResult(base) {
|
|
3629
3589
|
const basePassed = base.pending === 0 && base.verified > 0;
|
|
@@ -3827,8 +3787,8 @@ function verifyRetro(opts, isJson, root) {
|
|
|
3827
3787
|
return;
|
|
3828
3788
|
}
|
|
3829
3789
|
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
3830
|
-
const retroPath =
|
|
3831
|
-
if (!
|
|
3790
|
+
const retroPath = join12(root, STORY_DIR, retroFile);
|
|
3791
|
+
if (!existsSync14(retroPath)) {
|
|
3832
3792
|
if (isJson) {
|
|
3833
3793
|
jsonOutput({ status: "fail", epic: epicNum, retroFile, message: `${retroFile} not found` });
|
|
3834
3794
|
} else {
|
|
@@ -3845,7 +3805,7 @@ function verifyRetro(opts, isJson, root) {
|
|
|
3845
3805
|
warn(`Failed to update sprint status: ${message}`);
|
|
3846
3806
|
}
|
|
3847
3807
|
if (isJson) {
|
|
3848
|
-
jsonOutput({ status: "ok", epic: epicNum, retroFile:
|
|
3808
|
+
jsonOutput({ status: "ok", epic: epicNum, retroFile: join12(STORY_DIR, retroFile) });
|
|
3849
3809
|
} else {
|
|
3850
3810
|
ok(`Epic ${epicNum} retrospective: marked done`);
|
|
3851
3811
|
}
|
|
@@ -3856,8 +3816,8 @@ function verifyStory(storyId, isJson, root) {
|
|
|
3856
3816
|
process.exitCode = 1;
|
|
3857
3817
|
return;
|
|
3858
3818
|
}
|
|
3859
|
-
const readmePath =
|
|
3860
|
-
if (!
|
|
3819
|
+
const readmePath = join12(root, "README.md");
|
|
3820
|
+
if (!existsSync14(readmePath)) {
|
|
3861
3821
|
if (isJson) {
|
|
3862
3822
|
jsonOutput({ status: "fail", message: "No README.md found \u2014 verification requires user documentation" });
|
|
3863
3823
|
} else {
|
|
@@ -3866,8 +3826,8 @@ function verifyStory(storyId, isJson, root) {
|
|
|
3866
3826
|
process.exitCode = 1;
|
|
3867
3827
|
return;
|
|
3868
3828
|
}
|
|
3869
|
-
const storyFilePath =
|
|
3870
|
-
if (!
|
|
3829
|
+
const storyFilePath = join12(root, STORY_DIR, `${storyId}.md`);
|
|
3830
|
+
if (!existsSync14(storyFilePath)) {
|
|
3871
3831
|
fail(`Story file not found: ${storyFilePath}`, { json: isJson });
|
|
3872
3832
|
process.exitCode = 1;
|
|
3873
3833
|
return;
|
|
@@ -3907,8 +3867,8 @@ function verifyStory(storyId, isJson, root) {
|
|
|
3907
3867
|
return;
|
|
3908
3868
|
}
|
|
3909
3869
|
const storyTitle = extractStoryTitle(storyFilePath);
|
|
3910
|
-
const expectedProofPath =
|
|
3911
|
-
const proofPath =
|
|
3870
|
+
const expectedProofPath = join12(root, "verification", `${storyId}-proof.md`);
|
|
3871
|
+
const proofPath = existsSync14(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
|
|
3912
3872
|
const proofQuality = validateProofQuality(proofPath);
|
|
3913
3873
|
if (!proofQuality.passed) {
|
|
3914
3874
|
if (isJson) {
|
|
@@ -4002,7 +3962,7 @@ function verifyStory(storyId, isJson, root) {
|
|
|
4002
3962
|
}
|
|
4003
3963
|
function extractStoryTitle(filePath) {
|
|
4004
3964
|
try {
|
|
4005
|
-
const content =
|
|
3965
|
+
const content = readFileSync13(filePath, "utf-8");
|
|
4006
3966
|
const match = /^#\s+(.+)$/m.exec(content);
|
|
4007
3967
|
return match ? match[1] : "Unknown Story";
|
|
4008
3968
|
} catch {
|
|
@@ -4011,14 +3971,14 @@ function extractStoryTitle(filePath) {
|
|
|
4011
3971
|
}
|
|
4012
3972
|
|
|
4013
3973
|
// src/lib/onboard-checks.ts
|
|
4014
|
-
import { existsSync as
|
|
4015
|
-
import { join as
|
|
4016
|
-
import { fileURLToPath as
|
|
3974
|
+
import { existsSync as existsSync16 } from "fs";
|
|
3975
|
+
import { join as join14, dirname as dirname4 } from "path";
|
|
3976
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4017
3977
|
|
|
4018
3978
|
// src/lib/coverage.ts
|
|
4019
3979
|
import { execSync as execSync2 } from "child_process";
|
|
4020
|
-
import { existsSync as
|
|
4021
|
-
import { join as
|
|
3980
|
+
import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
|
|
3981
|
+
import { join as join13 } from "path";
|
|
4022
3982
|
function detectCoverageTool(dir) {
|
|
4023
3983
|
const baseDir = dir ?? process.cwd();
|
|
4024
3984
|
const stateHint = getStateToolHint(baseDir);
|
|
@@ -4041,16 +4001,16 @@ function getStateToolHint(dir) {
|
|
|
4041
4001
|
}
|
|
4042
4002
|
}
|
|
4043
4003
|
function detectNodeCoverageTool(dir, stateHint) {
|
|
4044
|
-
const hasVitestConfig =
|
|
4045
|
-
const pkgPath =
|
|
4004
|
+
const hasVitestConfig = existsSync15(join13(dir, "vitest.config.ts")) || existsSync15(join13(dir, "vitest.config.js"));
|
|
4005
|
+
const pkgPath = join13(dir, "package.json");
|
|
4046
4006
|
let hasVitestCoverageV8 = false;
|
|
4047
4007
|
let hasVitestCoverageIstanbul = false;
|
|
4048
4008
|
let hasC8 = false;
|
|
4049
4009
|
let hasJest = false;
|
|
4050
4010
|
let pkgScripts = {};
|
|
4051
|
-
if (
|
|
4011
|
+
if (existsSync15(pkgPath)) {
|
|
4052
4012
|
try {
|
|
4053
|
-
const pkg = JSON.parse(
|
|
4013
|
+
const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
|
|
4054
4014
|
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
4055
4015
|
hasVitestCoverageV8 = "@vitest/coverage-v8" in allDeps;
|
|
4056
4016
|
hasVitestCoverageIstanbul = "@vitest/coverage-istanbul" in allDeps;
|
|
@@ -4103,10 +4063,10 @@ function getNodeTestCommand(scripts, runner) {
|
|
|
4103
4063
|
return "npm test";
|
|
4104
4064
|
}
|
|
4105
4065
|
function detectPythonCoverageTool(dir) {
|
|
4106
|
-
const reqPath =
|
|
4107
|
-
if (
|
|
4066
|
+
const reqPath = join13(dir, "requirements.txt");
|
|
4067
|
+
if (existsSync15(reqPath)) {
|
|
4108
4068
|
try {
|
|
4109
|
-
const content =
|
|
4069
|
+
const content = readFileSync14(reqPath, "utf-8");
|
|
4110
4070
|
if (content.includes("pytest-cov") || content.includes("coverage")) {
|
|
4111
4071
|
return {
|
|
4112
4072
|
tool: "coverage.py",
|
|
@@ -4117,10 +4077,10 @@ function detectPythonCoverageTool(dir) {
|
|
|
4117
4077
|
} catch {
|
|
4118
4078
|
}
|
|
4119
4079
|
}
|
|
4120
|
-
const pyprojectPath =
|
|
4121
|
-
if (
|
|
4080
|
+
const pyprojectPath = join13(dir, "pyproject.toml");
|
|
4081
|
+
if (existsSync15(pyprojectPath)) {
|
|
4122
4082
|
try {
|
|
4123
|
-
const content =
|
|
4083
|
+
const content = readFileSync14(pyprojectPath, "utf-8");
|
|
4124
4084
|
if (content.includes("pytest-cov") || content.includes("coverage")) {
|
|
4125
4085
|
return {
|
|
4126
4086
|
tool: "coverage.py",
|
|
@@ -4202,7 +4162,7 @@ function parseVitestCoverage(dir) {
|
|
|
4202
4162
|
return 0;
|
|
4203
4163
|
}
|
|
4204
4164
|
try {
|
|
4205
|
-
const report = JSON.parse(
|
|
4165
|
+
const report = JSON.parse(readFileSync14(reportPath, "utf-8"));
|
|
4206
4166
|
return report.total?.statements?.pct ?? 0;
|
|
4207
4167
|
} catch {
|
|
4208
4168
|
warn("Failed to parse coverage report");
|
|
@@ -4210,13 +4170,13 @@ function parseVitestCoverage(dir) {
|
|
|
4210
4170
|
}
|
|
4211
4171
|
}
|
|
4212
4172
|
function parsePythonCoverage(dir) {
|
|
4213
|
-
const reportPath =
|
|
4214
|
-
if (!
|
|
4173
|
+
const reportPath = join13(dir, "coverage.json");
|
|
4174
|
+
if (!existsSync15(reportPath)) {
|
|
4215
4175
|
warn("Coverage report not found at coverage.json");
|
|
4216
4176
|
return 0;
|
|
4217
4177
|
}
|
|
4218
4178
|
try {
|
|
4219
|
-
const report = JSON.parse(
|
|
4179
|
+
const report = JSON.parse(readFileSync14(reportPath, "utf-8"));
|
|
4220
4180
|
return report.totals?.percent_covered ?? 0;
|
|
4221
4181
|
} catch {
|
|
4222
4182
|
warn("Failed to parse coverage report");
|
|
@@ -4317,7 +4277,7 @@ function checkPerFileCoverage(floor, dir) {
|
|
|
4317
4277
|
}
|
|
4318
4278
|
let report;
|
|
4319
4279
|
try {
|
|
4320
|
-
report = JSON.parse(
|
|
4280
|
+
report = JSON.parse(readFileSync14(reportPath, "utf-8"));
|
|
4321
4281
|
} catch {
|
|
4322
4282
|
warn("Failed to parse coverage-summary.json");
|
|
4323
4283
|
return { floor, violations: [], totalFiles: 0 };
|
|
@@ -4347,11 +4307,11 @@ function checkPerFileCoverage(floor, dir) {
|
|
|
4347
4307
|
}
|
|
4348
4308
|
function findCoverageSummary(dir) {
|
|
4349
4309
|
const candidates = [
|
|
4350
|
-
|
|
4351
|
-
|
|
4310
|
+
join13(dir, "coverage", "coverage-summary.json"),
|
|
4311
|
+
join13(dir, "src", "coverage", "coverage-summary.json")
|
|
4352
4312
|
];
|
|
4353
4313
|
for (const p of candidates) {
|
|
4354
|
-
if (
|
|
4314
|
+
if (existsSync15(p)) return p;
|
|
4355
4315
|
}
|
|
4356
4316
|
return null;
|
|
4357
4317
|
}
|
|
@@ -4376,16 +4336,16 @@ function printCoverageOutput(result, evaluation) {
|
|
|
4376
4336
|
// src/lib/onboard-checks.ts
|
|
4377
4337
|
function checkHarnessInitialized(dir) {
|
|
4378
4338
|
const statePath = getStatePath(dir ?? process.cwd());
|
|
4379
|
-
return { ok:
|
|
4339
|
+
return { ok: existsSync16(statePath) };
|
|
4380
4340
|
}
|
|
4381
4341
|
function checkBmadInstalled(dir) {
|
|
4382
4342
|
return { ok: isBmadInstalled(dir) };
|
|
4383
4343
|
}
|
|
4384
4344
|
function checkHooksRegistered(dir) {
|
|
4385
|
-
const __filename =
|
|
4386
|
-
const
|
|
4387
|
-
const hooksPath =
|
|
4388
|
-
return { ok:
|
|
4345
|
+
const __filename = fileURLToPath3(import.meta.url);
|
|
4346
|
+
const __dirname2 = dirname4(__filename);
|
|
4347
|
+
const hooksPath = join14(__dirname2, "..", "..", "hooks", "hooks.json");
|
|
4348
|
+
return { ok: existsSync16(hooksPath) };
|
|
4389
4349
|
}
|
|
4390
4350
|
function runPreconditions(dir) {
|
|
4391
4351
|
const harnessCheck = checkHarnessInitialized(dir);
|
|
@@ -4425,8 +4385,8 @@ function findVerificationGaps(dir) {
|
|
|
4425
4385
|
for (const [key, status] of Object.entries(statuses)) {
|
|
4426
4386
|
if (status !== "done") continue;
|
|
4427
4387
|
if (!STORY_KEY_PATTERN2.test(key)) continue;
|
|
4428
|
-
const proofPath =
|
|
4429
|
-
if (!
|
|
4388
|
+
const proofPath = join14(root, "verification", `${key}-proof.md`);
|
|
4389
|
+
if (!existsSync16(proofPath)) {
|
|
4430
4390
|
unverified.push(key);
|
|
4431
4391
|
}
|
|
4432
4392
|
}
|
|
@@ -4632,29 +4592,18 @@ function handleFullStatus(isJson) {
|
|
|
4632
4592
|
const composeFile = state.docker?.compose_file ?? "docker-compose.harness.yml";
|
|
4633
4593
|
const stackDir = getStackDir();
|
|
4634
4594
|
const isShared = composeFile.startsWith(stackDir);
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
}
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
} else {
|
|
4648
|
-
const health = getStackHealth(composeFile);
|
|
4649
|
-
console.log("Docker:");
|
|
4650
|
-
for (const svc of health.services) {
|
|
4651
|
-
console.log(` ${svc.name}: ${svc.running ? "running" : "stopped"}`);
|
|
4652
|
-
}
|
|
4653
|
-
if (health.healthy) {
|
|
4654
|
-
console.log(
|
|
4655
|
-
` Endpoints: logs=${DEFAULT_ENDPOINTS.logs} metrics=${DEFAULT_ENDPOINTS.metrics} traces=${DEFAULT_ENDPOINTS.traces}`
|
|
4656
|
-
);
|
|
4657
|
-
}
|
|
4595
|
+
const resolvedComposeFile = isShared ? getComposeFilePath() : composeFile;
|
|
4596
|
+
const projectName = isShared ? "codeharness-shared" : void 0;
|
|
4597
|
+
const header = isShared ? "Docker: shared stack at ~/.codeharness/stack/" : "Docker:";
|
|
4598
|
+
console.log(header);
|
|
4599
|
+
const health = getStackHealth(resolvedComposeFile, projectName);
|
|
4600
|
+
for (const svc of health.services) {
|
|
4601
|
+
console.log(` ${svc.name}: ${svc.running ? "running" : "stopped"}`);
|
|
4602
|
+
}
|
|
4603
|
+
if (health.healthy) {
|
|
4604
|
+
console.log(
|
|
4605
|
+
` Endpoints: logs=${DEFAULT_ENDPOINTS.logs} metrics=${DEFAULT_ENDPOINTS.metrics} traces=${DEFAULT_ENDPOINTS.traces}`
|
|
4606
|
+
);
|
|
4658
4607
|
}
|
|
4659
4608
|
}
|
|
4660
4609
|
}
|
|
@@ -5011,16 +4960,16 @@ function getBeadsData() {
|
|
|
5011
4960
|
}
|
|
5012
4961
|
|
|
5013
4962
|
// src/commands/onboard.ts
|
|
5014
|
-
import { join as
|
|
4963
|
+
import { join as join18 } from "path";
|
|
5015
4964
|
|
|
5016
4965
|
// src/lib/scanner.ts
|
|
5017
4966
|
import {
|
|
5018
|
-
existsSync as
|
|
4967
|
+
existsSync as existsSync17,
|
|
5019
4968
|
readdirSync as readdirSync3,
|
|
5020
|
-
readFileSync as
|
|
4969
|
+
readFileSync as readFileSync15,
|
|
5021
4970
|
statSync as statSync2
|
|
5022
4971
|
} from "fs";
|
|
5023
|
-
import { join as
|
|
4972
|
+
import { join as join15, relative as relative2 } from "path";
|
|
5024
4973
|
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".js", ".py"]);
|
|
5025
4974
|
var DEFAULT_MIN_MODULE_SIZE = 3;
|
|
5026
4975
|
function getExtension2(filename) {
|
|
@@ -5045,7 +4994,7 @@ function countSourceFiles(dir) {
|
|
|
5045
4994
|
for (const entry of entries) {
|
|
5046
4995
|
if (isSkippedDir(entry)) continue;
|
|
5047
4996
|
if (entry.startsWith(".") && current !== dir) continue;
|
|
5048
|
-
const fullPath =
|
|
4997
|
+
const fullPath = join15(current, entry);
|
|
5049
4998
|
let stat;
|
|
5050
4999
|
try {
|
|
5051
5000
|
stat = statSync2(fullPath);
|
|
@@ -5068,7 +5017,7 @@ function countSourceFiles(dir) {
|
|
|
5068
5017
|
return count;
|
|
5069
5018
|
}
|
|
5070
5019
|
function countModuleFiles(modulePath, rootDir) {
|
|
5071
|
-
const fullModulePath =
|
|
5020
|
+
const fullModulePath = join15(rootDir, modulePath);
|
|
5072
5021
|
let sourceFiles = 0;
|
|
5073
5022
|
let testFiles = 0;
|
|
5074
5023
|
function walk(current) {
|
|
@@ -5080,7 +5029,7 @@ function countModuleFiles(modulePath, rootDir) {
|
|
|
5080
5029
|
}
|
|
5081
5030
|
for (const entry of entries) {
|
|
5082
5031
|
if (isSkippedDir(entry)) continue;
|
|
5083
|
-
const fullPath =
|
|
5032
|
+
const fullPath = join15(current, entry);
|
|
5084
5033
|
let stat;
|
|
5085
5034
|
try {
|
|
5086
5035
|
stat = statSync2(fullPath);
|
|
@@ -5105,8 +5054,8 @@ function countModuleFiles(modulePath, rootDir) {
|
|
|
5105
5054
|
return { sourceFiles, testFiles };
|
|
5106
5055
|
}
|
|
5107
5056
|
function detectArtifacts(dir) {
|
|
5108
|
-
const bmadPath =
|
|
5109
|
-
const hasBmad =
|
|
5057
|
+
const bmadPath = join15(dir, "_bmad");
|
|
5058
|
+
const hasBmad = existsSync17(bmadPath);
|
|
5110
5059
|
return {
|
|
5111
5060
|
hasBmad,
|
|
5112
5061
|
bmadPath: hasBmad ? relative2(dir, bmadPath) || "_bmad" : null
|
|
@@ -5188,10 +5137,10 @@ function readPerFileCoverage(dir, format) {
|
|
|
5188
5137
|
return null;
|
|
5189
5138
|
}
|
|
5190
5139
|
function readVitestPerFileCoverage(dir) {
|
|
5191
|
-
const reportPath =
|
|
5192
|
-
if (!
|
|
5140
|
+
const reportPath = join15(dir, "coverage", "coverage-summary.json");
|
|
5141
|
+
if (!existsSync17(reportPath)) return null;
|
|
5193
5142
|
try {
|
|
5194
|
-
const report = JSON.parse(
|
|
5143
|
+
const report = JSON.parse(readFileSync15(reportPath, "utf-8"));
|
|
5195
5144
|
const result = /* @__PURE__ */ new Map();
|
|
5196
5145
|
for (const [key, value] of Object.entries(report)) {
|
|
5197
5146
|
if (key === "total") continue;
|
|
@@ -5203,10 +5152,10 @@ function readVitestPerFileCoverage(dir) {
|
|
|
5203
5152
|
}
|
|
5204
5153
|
}
|
|
5205
5154
|
function readPythonPerFileCoverage(dir) {
|
|
5206
|
-
const reportPath =
|
|
5207
|
-
if (!
|
|
5155
|
+
const reportPath = join15(dir, "coverage.json");
|
|
5156
|
+
if (!existsSync17(reportPath)) return null;
|
|
5208
5157
|
try {
|
|
5209
|
-
const report = JSON.parse(
|
|
5158
|
+
const report = JSON.parse(readFileSync15(reportPath, "utf-8"));
|
|
5210
5159
|
if (!report.files) return null;
|
|
5211
5160
|
const result = /* @__PURE__ */ new Map();
|
|
5212
5161
|
for (const [key, value] of Object.entries(report.files)) {
|
|
@@ -5222,13 +5171,13 @@ function auditDocumentation(dir) {
|
|
|
5222
5171
|
const root = dir ?? process.cwd();
|
|
5223
5172
|
const documents = [];
|
|
5224
5173
|
for (const docName of AUDIT_DOCUMENTS) {
|
|
5225
|
-
const docPath =
|
|
5226
|
-
if (!
|
|
5174
|
+
const docPath = join15(root, docName);
|
|
5175
|
+
if (!existsSync17(docPath)) {
|
|
5227
5176
|
documents.push({ name: docName, grade: "missing", path: null });
|
|
5228
5177
|
continue;
|
|
5229
5178
|
}
|
|
5230
|
-
const srcDir =
|
|
5231
|
-
const codeDir =
|
|
5179
|
+
const srcDir = join15(root, "src");
|
|
5180
|
+
const codeDir = existsSync17(srcDir) ? srcDir : root;
|
|
5232
5181
|
const stale = isDocStale(docPath, codeDir);
|
|
5233
5182
|
documents.push({
|
|
5234
5183
|
name: docName,
|
|
@@ -5236,8 +5185,8 @@ function auditDocumentation(dir) {
|
|
|
5236
5185
|
path: docName
|
|
5237
5186
|
});
|
|
5238
5187
|
}
|
|
5239
|
-
const docsDir =
|
|
5240
|
-
if (
|
|
5188
|
+
const docsDir = join15(root, "docs");
|
|
5189
|
+
if (existsSync17(docsDir)) {
|
|
5241
5190
|
try {
|
|
5242
5191
|
const stat = statSync2(docsDir);
|
|
5243
5192
|
if (stat.isDirectory()) {
|
|
@@ -5249,10 +5198,10 @@ function auditDocumentation(dir) {
|
|
|
5249
5198
|
} else {
|
|
5250
5199
|
documents.push({ name: "docs/", grade: "missing", path: null });
|
|
5251
5200
|
}
|
|
5252
|
-
const indexPath =
|
|
5253
|
-
if (
|
|
5254
|
-
const srcDir =
|
|
5255
|
-
const indexCodeDir =
|
|
5201
|
+
const indexPath = join15(root, "docs", "index.md");
|
|
5202
|
+
if (existsSync17(indexPath)) {
|
|
5203
|
+
const srcDir = join15(root, "src");
|
|
5204
|
+
const indexCodeDir = existsSync17(srcDir) ? srcDir : root;
|
|
5256
5205
|
const indexStale = isDocStale(indexPath, indexCodeDir);
|
|
5257
5206
|
documents.push({
|
|
5258
5207
|
name: "docs/index.md",
|
|
@@ -5269,8 +5218,8 @@ function auditDocumentation(dir) {
|
|
|
5269
5218
|
|
|
5270
5219
|
// src/lib/epic-generator.ts
|
|
5271
5220
|
import { createInterface } from "readline";
|
|
5272
|
-
import { existsSync as
|
|
5273
|
-
import { dirname as
|
|
5221
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync6, writeFileSync as writeFileSync9 } from "fs";
|
|
5222
|
+
import { dirname as dirname5, join as join16 } from "path";
|
|
5274
5223
|
var PRIORITY_BY_TYPE = {
|
|
5275
5224
|
observability: 1,
|
|
5276
5225
|
coverage: 2,
|
|
@@ -5308,8 +5257,8 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
|
|
|
5308
5257
|
storyNum++;
|
|
5309
5258
|
}
|
|
5310
5259
|
for (const mod of scan.modules) {
|
|
5311
|
-
const agentsPath =
|
|
5312
|
-
if (!
|
|
5260
|
+
const agentsPath = join16(root, mod.path, "AGENTS.md");
|
|
5261
|
+
if (!existsSync18(agentsPath)) {
|
|
5313
5262
|
stories.push({
|
|
5314
5263
|
key: `0.${storyNum}`,
|
|
5315
5264
|
title: `Create ${mod.path}/AGENTS.md`,
|
|
@@ -5375,7 +5324,7 @@ function generateOnboardingEpic(scan, coverage, audit, rootDir) {
|
|
|
5375
5324
|
};
|
|
5376
5325
|
}
|
|
5377
5326
|
function writeOnboardingEpic(epic, outputPath) {
|
|
5378
|
-
mkdirSync6(
|
|
5327
|
+
mkdirSync6(dirname5(outputPath), { recursive: true });
|
|
5379
5328
|
const lines = [];
|
|
5380
5329
|
lines.push(`# ${epic.title}`);
|
|
5381
5330
|
lines.push("");
|
|
@@ -5507,29 +5456,29 @@ function getGapIdFromTitle(title) {
|
|
|
5507
5456
|
}
|
|
5508
5457
|
|
|
5509
5458
|
// src/lib/scan-cache.ts
|
|
5510
|
-
import { existsSync as
|
|
5511
|
-
import { join as
|
|
5459
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync7, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
|
|
5460
|
+
import { join as join17 } from "path";
|
|
5512
5461
|
var CACHE_DIR = ".harness";
|
|
5513
5462
|
var CACHE_FILE = "last-onboard-scan.json";
|
|
5514
5463
|
var DEFAULT_MAX_AGE_MS = 864e5;
|
|
5515
5464
|
function saveScanCache(entry, dir) {
|
|
5516
5465
|
try {
|
|
5517
5466
|
const root = dir ?? process.cwd();
|
|
5518
|
-
const cacheDir =
|
|
5467
|
+
const cacheDir = join17(root, CACHE_DIR);
|
|
5519
5468
|
mkdirSync7(cacheDir, { recursive: true });
|
|
5520
|
-
const cachePath =
|
|
5469
|
+
const cachePath = join17(cacheDir, CACHE_FILE);
|
|
5521
5470
|
writeFileSync10(cachePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
5522
5471
|
} catch {
|
|
5523
5472
|
}
|
|
5524
5473
|
}
|
|
5525
5474
|
function loadScanCache(dir) {
|
|
5526
5475
|
const root = dir ?? process.cwd();
|
|
5527
|
-
const cachePath =
|
|
5528
|
-
if (!
|
|
5476
|
+
const cachePath = join17(root, CACHE_DIR, CACHE_FILE);
|
|
5477
|
+
if (!existsSync19(cachePath)) {
|
|
5529
5478
|
return null;
|
|
5530
5479
|
}
|
|
5531
5480
|
try {
|
|
5532
|
-
const raw =
|
|
5481
|
+
const raw = readFileSync16(cachePath, "utf-8");
|
|
5533
5482
|
return JSON.parse(raw);
|
|
5534
5483
|
} catch {
|
|
5535
5484
|
return null;
|
|
@@ -5702,7 +5651,7 @@ function registerOnboardCommand(program) {
|
|
|
5702
5651
|
}
|
|
5703
5652
|
coverage = lastCoverageResult ?? runCoverageAnalysis(scan);
|
|
5704
5653
|
audit = lastAuditResult ?? runAudit();
|
|
5705
|
-
const epicPath =
|
|
5654
|
+
const epicPath = join18(process.cwd(), "ralph", "onboarding-epic.md");
|
|
5706
5655
|
const epic = generateOnboardingEpic(scan, coverage, audit);
|
|
5707
5656
|
mergeExtendedGaps(epic);
|
|
5708
5657
|
if (!isFull) {
|
|
@@ -5775,7 +5724,7 @@ function registerOnboardCommand(program) {
|
|
|
5775
5724
|
coverage,
|
|
5776
5725
|
audit
|
|
5777
5726
|
});
|
|
5778
|
-
const epicPath =
|
|
5727
|
+
const epicPath = join18(process.cwd(), "ralph", "onboarding-epic.md");
|
|
5779
5728
|
const epic = generateOnboardingEpic(scan, coverage, audit);
|
|
5780
5729
|
mergeExtendedGaps(epic);
|
|
5781
5730
|
if (!isFull) {
|
|
@@ -5883,8 +5832,8 @@ function printEpicOutput(epic) {
|
|
|
5883
5832
|
}
|
|
5884
5833
|
|
|
5885
5834
|
// src/commands/teardown.ts
|
|
5886
|
-
import { existsSync as
|
|
5887
|
-
import { join as
|
|
5835
|
+
import { existsSync as existsSync20, unlinkSync as unlinkSync2, readFileSync as readFileSync17, writeFileSync as writeFileSync11, rmSync } from "fs";
|
|
5836
|
+
import { join as join19 } from "path";
|
|
5888
5837
|
function buildDefaultResult() {
|
|
5889
5838
|
return {
|
|
5890
5839
|
status: "ok",
|
|
@@ -5987,16 +5936,16 @@ function registerTeardownCommand(program) {
|
|
|
5987
5936
|
info("Docker stack: not running, skipping");
|
|
5988
5937
|
}
|
|
5989
5938
|
}
|
|
5990
|
-
const composeFilePath =
|
|
5991
|
-
if (
|
|
5939
|
+
const composeFilePath = join19(projectDir, composeFile);
|
|
5940
|
+
if (existsSync20(composeFilePath)) {
|
|
5992
5941
|
unlinkSync2(composeFilePath);
|
|
5993
5942
|
result.removed.push(composeFile);
|
|
5994
5943
|
if (!isJson) {
|
|
5995
5944
|
ok(`Removed: ${composeFile}`);
|
|
5996
5945
|
}
|
|
5997
5946
|
}
|
|
5998
|
-
const otelConfigPath =
|
|
5999
|
-
if (
|
|
5947
|
+
const otelConfigPath = join19(projectDir, "otel-collector-config.yaml");
|
|
5948
|
+
if (existsSync20(otelConfigPath)) {
|
|
6000
5949
|
unlinkSync2(otelConfigPath);
|
|
6001
5950
|
result.removed.push("otel-collector-config.yaml");
|
|
6002
5951
|
if (!isJson) {
|
|
@@ -6006,8 +5955,8 @@ function registerTeardownCommand(program) {
|
|
|
6006
5955
|
}
|
|
6007
5956
|
let patchesRemoved = 0;
|
|
6008
5957
|
for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
|
|
6009
|
-
const filePath =
|
|
6010
|
-
if (!
|
|
5958
|
+
const filePath = join19(projectDir, "_bmad", relativePath);
|
|
5959
|
+
if (!existsSync20(filePath)) {
|
|
6011
5960
|
continue;
|
|
6012
5961
|
}
|
|
6013
5962
|
try {
|
|
@@ -6027,10 +5976,10 @@ function registerTeardownCommand(program) {
|
|
|
6027
5976
|
}
|
|
6028
5977
|
}
|
|
6029
5978
|
if (state.otlp?.enabled && state.stack === "nodejs") {
|
|
6030
|
-
const pkgPath =
|
|
6031
|
-
if (
|
|
5979
|
+
const pkgPath = join19(projectDir, "package.json");
|
|
5980
|
+
if (existsSync20(pkgPath)) {
|
|
6032
5981
|
try {
|
|
6033
|
-
const raw =
|
|
5982
|
+
const raw = readFileSync17(pkgPath, "utf-8");
|
|
6034
5983
|
const pkg = JSON.parse(raw);
|
|
6035
5984
|
const scripts = pkg["scripts"];
|
|
6036
5985
|
if (scripts) {
|
|
@@ -6070,8 +6019,8 @@ function registerTeardownCommand(program) {
|
|
|
6070
6019
|
}
|
|
6071
6020
|
}
|
|
6072
6021
|
}
|
|
6073
|
-
const harnessDir =
|
|
6074
|
-
if (
|
|
6022
|
+
const harnessDir = join19(projectDir, ".harness");
|
|
6023
|
+
if (existsSync20(harnessDir)) {
|
|
6075
6024
|
rmSync(harnessDir, { recursive: true, force: true });
|
|
6076
6025
|
result.removed.push(".harness/");
|
|
6077
6026
|
if (!isJson) {
|
|
@@ -6079,7 +6028,7 @@ function registerTeardownCommand(program) {
|
|
|
6079
6028
|
}
|
|
6080
6029
|
}
|
|
6081
6030
|
const statePath = getStatePath(projectDir);
|
|
6082
|
-
if (
|
|
6031
|
+
if (existsSync20(statePath)) {
|
|
6083
6032
|
unlinkSync2(statePath);
|
|
6084
6033
|
result.removed.push(".claude/codeharness.local.md");
|
|
6085
6034
|
if (!isJson) {
|
|
@@ -6823,8 +6772,8 @@ function registerQueryCommand(program) {
|
|
|
6823
6772
|
}
|
|
6824
6773
|
|
|
6825
6774
|
// src/commands/retro-import.ts
|
|
6826
|
-
import { existsSync as
|
|
6827
|
-
import { join as
|
|
6775
|
+
import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
|
|
6776
|
+
import { join as join20 } from "path";
|
|
6828
6777
|
|
|
6829
6778
|
// src/lib/retro-parser.ts
|
|
6830
6779
|
var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
|
|
@@ -6993,15 +6942,15 @@ function registerRetroImportCommand(program) {
|
|
|
6993
6942
|
return;
|
|
6994
6943
|
}
|
|
6995
6944
|
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
6996
|
-
const retroPath =
|
|
6997
|
-
if (!
|
|
6945
|
+
const retroPath = join20(root, STORY_DIR2, retroFile);
|
|
6946
|
+
if (!existsSync21(retroPath)) {
|
|
6998
6947
|
fail(`Retro file not found: ${retroFile}`, { json: isJson });
|
|
6999
6948
|
process.exitCode = 1;
|
|
7000
6949
|
return;
|
|
7001
6950
|
}
|
|
7002
6951
|
let content;
|
|
7003
6952
|
try {
|
|
7004
|
-
content =
|
|
6953
|
+
content = readFileSync18(retroPath, "utf-8");
|
|
7005
6954
|
} catch (err) {
|
|
7006
6955
|
const message = err instanceof Error ? err.message : String(err);
|
|
7007
6956
|
fail(`Failed to read retro file: ${message}`, { json: isJson });
|
|
@@ -7269,8 +7218,8 @@ function registerGithubImportCommand(program) {
|
|
|
7269
7218
|
|
|
7270
7219
|
// src/lib/verify-env.ts
|
|
7271
7220
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
7272
|
-
import { existsSync as
|
|
7273
|
-
import { join as
|
|
7221
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync19, cpSync, rmSync as rmSync2, statSync as statSync3 } from "fs";
|
|
7222
|
+
import { join as join21, basename as basename3 } from "path";
|
|
7274
7223
|
import { createHash } from "crypto";
|
|
7275
7224
|
var IMAGE_TAG = "codeharness-verify";
|
|
7276
7225
|
var STORY_DIR3 = "_bmad-output/implementation-artifacts";
|
|
@@ -7283,14 +7232,14 @@ function isValidStoryKey(storyKey) {
|
|
|
7283
7232
|
return /^[a-zA-Z0-9_-]+$/.test(storyKey);
|
|
7284
7233
|
}
|
|
7285
7234
|
function computeDistHash(projectDir) {
|
|
7286
|
-
const distDir =
|
|
7287
|
-
if (!
|
|
7235
|
+
const distDir = join21(projectDir, "dist");
|
|
7236
|
+
if (!existsSync22(distDir)) {
|
|
7288
7237
|
return null;
|
|
7289
7238
|
}
|
|
7290
7239
|
const hash = createHash("sha256");
|
|
7291
7240
|
const files = collectFiles(distDir).sort();
|
|
7292
7241
|
for (const file of files) {
|
|
7293
|
-
const content =
|
|
7242
|
+
const content = readFileSync19(file);
|
|
7294
7243
|
hash.update(file.slice(distDir.length));
|
|
7295
7244
|
hash.update(content);
|
|
7296
7245
|
}
|
|
@@ -7300,7 +7249,7 @@ function collectFiles(dir) {
|
|
|
7300
7249
|
const results = [];
|
|
7301
7250
|
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
7302
7251
|
for (const entry of entries) {
|
|
7303
|
-
const fullPath =
|
|
7252
|
+
const fullPath = join21(dir, entry.name);
|
|
7304
7253
|
if (entry.isDirectory()) {
|
|
7305
7254
|
results.push(...collectFiles(fullPath));
|
|
7306
7255
|
} else {
|
|
@@ -7372,13 +7321,13 @@ function buildNodeImage(projectDir) {
|
|
|
7372
7321
|
throw new Error("npm pack produced no output \u2014 cannot determine tarball filename.");
|
|
7373
7322
|
}
|
|
7374
7323
|
const tarballName = basename3(lastLine);
|
|
7375
|
-
const tarballPath =
|
|
7376
|
-
const buildContext =
|
|
7324
|
+
const tarballPath = join21("/tmp", tarballName);
|
|
7325
|
+
const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
7377
7326
|
mkdirSync8(buildContext, { recursive: true });
|
|
7378
7327
|
try {
|
|
7379
|
-
cpSync(tarballPath,
|
|
7328
|
+
cpSync(tarballPath, join21(buildContext, tarballName));
|
|
7380
7329
|
const dockerfileSrc = resolveDockerfileTemplate(projectDir);
|
|
7381
|
-
cpSync(dockerfileSrc,
|
|
7330
|
+
cpSync(dockerfileSrc, join21(buildContext, "Dockerfile"));
|
|
7382
7331
|
execFileSync7("docker", [
|
|
7383
7332
|
"build",
|
|
7384
7333
|
"-t",
|
|
@@ -7397,7 +7346,7 @@ function buildNodeImage(projectDir) {
|
|
|
7397
7346
|
}
|
|
7398
7347
|
}
|
|
7399
7348
|
function buildPythonImage(projectDir) {
|
|
7400
|
-
const distDir =
|
|
7349
|
+
const distDir = join21(projectDir, "dist");
|
|
7401
7350
|
const distFiles = readdirSync4(distDir).filter(
|
|
7402
7351
|
(f) => f.endsWith(".tar.gz") || f.endsWith(".whl")
|
|
7403
7352
|
);
|
|
@@ -7405,12 +7354,12 @@ function buildPythonImage(projectDir) {
|
|
|
7405
7354
|
throw new Error("No distribution files found in dist/. Run your build command first (e.g., python -m build).");
|
|
7406
7355
|
}
|
|
7407
7356
|
const distFile = distFiles.filter((f) => f.endsWith(".tar.gz"))[0] ?? distFiles[0];
|
|
7408
|
-
const buildContext =
|
|
7357
|
+
const buildContext = join21("/tmp", `codeharness-verify-build-${Date.now()}`);
|
|
7409
7358
|
mkdirSync8(buildContext, { recursive: true });
|
|
7410
7359
|
try {
|
|
7411
|
-
cpSync(
|
|
7360
|
+
cpSync(join21(distDir, distFile), join21(buildContext, distFile));
|
|
7412
7361
|
const dockerfileSrc = resolveDockerfileTemplate(projectDir);
|
|
7413
|
-
cpSync(dockerfileSrc,
|
|
7362
|
+
cpSync(dockerfileSrc, join21(buildContext, "Dockerfile"));
|
|
7414
7363
|
execFileSync7("docker", [
|
|
7415
7364
|
"build",
|
|
7416
7365
|
"-t",
|
|
@@ -7432,25 +7381,25 @@ function prepareVerifyWorkspace(storyKey, projectDir) {
|
|
|
7432
7381
|
if (!isValidStoryKey(storyKey)) {
|
|
7433
7382
|
throw new Error(`Invalid story key: ${storyKey}. Keys must contain only alphanumeric characters, hyphens, and underscores.`);
|
|
7434
7383
|
}
|
|
7435
|
-
const storyFile =
|
|
7436
|
-
if (!
|
|
7384
|
+
const storyFile = join21(root, STORY_DIR3, `${storyKey}.md`);
|
|
7385
|
+
if (!existsSync22(storyFile)) {
|
|
7437
7386
|
throw new Error(`Story file not found: ${storyFile}`);
|
|
7438
7387
|
}
|
|
7439
7388
|
const workspace = `${TEMP_PREFIX}${storyKey}`;
|
|
7440
|
-
if (
|
|
7389
|
+
if (existsSync22(workspace)) {
|
|
7441
7390
|
rmSync2(workspace, { recursive: true, force: true });
|
|
7442
7391
|
}
|
|
7443
7392
|
mkdirSync8(workspace, { recursive: true });
|
|
7444
|
-
cpSync(storyFile,
|
|
7445
|
-
const readmePath =
|
|
7446
|
-
if (
|
|
7447
|
-
cpSync(readmePath,
|
|
7393
|
+
cpSync(storyFile, join21(workspace, "story.md"));
|
|
7394
|
+
const readmePath = join21(root, "README.md");
|
|
7395
|
+
if (existsSync22(readmePath)) {
|
|
7396
|
+
cpSync(readmePath, join21(workspace, "README.md"));
|
|
7448
7397
|
}
|
|
7449
|
-
const docsDir =
|
|
7450
|
-
if (
|
|
7451
|
-
cpSync(docsDir,
|
|
7398
|
+
const docsDir = join21(root, "docs");
|
|
7399
|
+
if (existsSync22(docsDir) && statSync3(docsDir).isDirectory()) {
|
|
7400
|
+
cpSync(docsDir, join21(workspace, "docs"), { recursive: true });
|
|
7452
7401
|
}
|
|
7453
|
-
mkdirSync8(
|
|
7402
|
+
mkdirSync8(join21(workspace, "verification"), { recursive: true });
|
|
7454
7403
|
return workspace;
|
|
7455
7404
|
}
|
|
7456
7405
|
function checkVerifyEnv() {
|
|
@@ -7499,7 +7448,7 @@ function cleanupVerifyEnv(storyKey) {
|
|
|
7499
7448
|
}
|
|
7500
7449
|
const workspace = `${TEMP_PREFIX}${storyKey}`;
|
|
7501
7450
|
const containerName = `codeharness-verify-${storyKey}`;
|
|
7502
|
-
if (
|
|
7451
|
+
if (existsSync22(workspace)) {
|
|
7503
7452
|
rmSync2(workspace, { recursive: true, force: true });
|
|
7504
7453
|
}
|
|
7505
7454
|
try {
|
|
@@ -7518,11 +7467,11 @@ function cleanupVerifyEnv(storyKey) {
|
|
|
7518
7467
|
}
|
|
7519
7468
|
}
|
|
7520
7469
|
function resolveDockerfileTemplate(projectDir) {
|
|
7521
|
-
const local =
|
|
7522
|
-
if (
|
|
7470
|
+
const local = join21(projectDir, "templates", "Dockerfile.verify");
|
|
7471
|
+
if (existsSync22(local)) return local;
|
|
7523
7472
|
const pkgDir = new URL("../../", import.meta.url).pathname;
|
|
7524
|
-
const pkg =
|
|
7525
|
-
if (
|
|
7473
|
+
const pkg = join21(pkgDir, "templates", "Dockerfile.verify");
|
|
7474
|
+
if (existsSync22(pkg)) return pkg;
|
|
7526
7475
|
throw new Error("Dockerfile.verify not found. Ensure templates/Dockerfile.verify exists in the project or installed package.");
|
|
7527
7476
|
}
|
|
7528
7477
|
function dockerImageExists(tag) {
|
|
@@ -7668,26 +7617,26 @@ function registerVerifyEnvCommand(program) {
|
|
|
7668
7617
|
}
|
|
7669
7618
|
|
|
7670
7619
|
// src/commands/retry.ts
|
|
7671
|
-
import { join as
|
|
7620
|
+
import { join as join23 } from "path";
|
|
7672
7621
|
|
|
7673
7622
|
// src/lib/retry-state.ts
|
|
7674
|
-
import { existsSync as
|
|
7675
|
-
import { join as
|
|
7623
|
+
import { existsSync as existsSync23, readFileSync as readFileSync20, writeFileSync as writeFileSync12 } from "fs";
|
|
7624
|
+
import { join as join22 } from "path";
|
|
7676
7625
|
var RETRIES_FILE = ".story_retries";
|
|
7677
7626
|
var FLAGGED_FILE = ".flagged_stories";
|
|
7678
7627
|
var LINE_PATTERN = /^([^=]+)=(\d+)$/;
|
|
7679
7628
|
function retriesPath(dir) {
|
|
7680
|
-
return
|
|
7629
|
+
return join22(dir, RETRIES_FILE);
|
|
7681
7630
|
}
|
|
7682
7631
|
function flaggedPath(dir) {
|
|
7683
|
-
return
|
|
7632
|
+
return join22(dir, FLAGGED_FILE);
|
|
7684
7633
|
}
|
|
7685
7634
|
function readRetries(dir) {
|
|
7686
7635
|
const filePath = retriesPath(dir);
|
|
7687
|
-
if (!
|
|
7636
|
+
if (!existsSync23(filePath)) {
|
|
7688
7637
|
return /* @__PURE__ */ new Map();
|
|
7689
7638
|
}
|
|
7690
|
-
const raw =
|
|
7639
|
+
const raw = readFileSync20(filePath, "utf-8");
|
|
7691
7640
|
const result = /* @__PURE__ */ new Map();
|
|
7692
7641
|
for (const line of raw.split("\n")) {
|
|
7693
7642
|
const trimmed = line.trim();
|
|
@@ -7724,10 +7673,10 @@ function resetRetry(dir, storyKey) {
|
|
|
7724
7673
|
}
|
|
7725
7674
|
function readFlaggedStories(dir) {
|
|
7726
7675
|
const filePath = flaggedPath(dir);
|
|
7727
|
-
if (!
|
|
7676
|
+
if (!existsSync23(filePath)) {
|
|
7728
7677
|
return [];
|
|
7729
7678
|
}
|
|
7730
|
-
const raw =
|
|
7679
|
+
const raw = readFileSync20(filePath, "utf-8");
|
|
7731
7680
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l !== "");
|
|
7732
7681
|
}
|
|
7733
7682
|
function writeFlaggedStories(dir, stories) {
|
|
@@ -7752,7 +7701,7 @@ function registerRetryCommand(program) {
|
|
|
7752
7701
|
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) => {
|
|
7753
7702
|
const opts = cmd.optsWithGlobals();
|
|
7754
7703
|
const isJson = opts.json === true;
|
|
7755
|
-
const dir =
|
|
7704
|
+
const dir = join23(process.cwd(), RALPH_SUBDIR);
|
|
7756
7705
|
if (opts.story && !isValidStoryKey3(opts.story)) {
|
|
7757
7706
|
if (isJson) {
|
|
7758
7707
|
jsonOutput({ status: "fail", message: `Invalid story key: ${opts.story}` });
|
|
@@ -7826,7 +7775,7 @@ function handleStatus(dir, isJson, filterStory) {
|
|
|
7826
7775
|
}
|
|
7827
7776
|
|
|
7828
7777
|
// src/index.ts
|
|
7829
|
-
var VERSION = true ? "0.
|
|
7778
|
+
var VERSION = true ? "0.18.0" : "0.0.0-dev";
|
|
7830
7779
|
function createProgram() {
|
|
7831
7780
|
const program = new Command();
|
|
7832
7781
|
program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
|