codeharness 0.7.3 → 0.8.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 +653 -107
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -822,6 +822,18 @@ function appendGapId(existingDescription, gapId) {
|
|
|
822
822
|
return `${existingDescription}
|
|
823
823
|
${gapId}`;
|
|
824
824
|
}
|
|
825
|
+
function createOrFindIssue(title, gapId, opts) {
|
|
826
|
+
const issues = listIssues();
|
|
827
|
+
const existing = findExistingByGapId(gapId, issues);
|
|
828
|
+
if (existing) {
|
|
829
|
+
return { issue: existing, created: false };
|
|
830
|
+
}
|
|
831
|
+
const issue = createIssue(title, {
|
|
832
|
+
...opts,
|
|
833
|
+
description: appendGapId(opts?.description, gapId)
|
|
834
|
+
});
|
|
835
|
+
return { issue, created: true };
|
|
836
|
+
}
|
|
825
837
|
function configureHookCoexistence(dir) {
|
|
826
838
|
const detection = detectBeadsHooks(dir);
|
|
827
839
|
if (!detection.hasHooks) {
|
|
@@ -971,9 +983,15 @@ function retroEnforcementPatch() {
|
|
|
971
983
|
function sprintBeadsPatch() {
|
|
972
984
|
return `## Codeharness Backlog Integration
|
|
973
985
|
|
|
986
|
+
### Pre-Triage Import Verification
|
|
987
|
+
- [ ] Confirm \`codeharness retro-import\` was run for all completed retrospectives
|
|
988
|
+
- [ ] Confirm \`codeharness github-import\` was run to pull labeled GitHub issues
|
|
989
|
+
- [ ] Verify all sources are reflected in beads before starting triage
|
|
990
|
+
|
|
974
991
|
### Beads Issue Status
|
|
975
992
|
- [ ] Run \`bd ready\` to display issues ready for development
|
|
976
993
|
- [ ] Review beads issue counts by status (open, in-progress, done)
|
|
994
|
+
- [ ] Verify issues from all sources are visible: retro (\`[gap:retro:...]\`), GitHub (\`[source:github:...]\`), and manual
|
|
977
995
|
- [ ] Verify no blocked issues without documented reason
|
|
978
996
|
|
|
979
997
|
### Sprint Readiness
|
|
@@ -981,12 +999,46 @@ function sprintBeadsPatch() {
|
|
|
981
999
|
- [ ] Dependencies between stories are reflected in beads deps
|
|
982
1000
|
- [ ] Capacity aligns with estimated story complexity`;
|
|
983
1001
|
}
|
|
1002
|
+
function sprintPlanningRetroPatch() {
|
|
1003
|
+
return `## Retrospective Action Items Review
|
|
1004
|
+
|
|
1005
|
+
### Unresolved Action Items from Previous Retrospectives
|
|
1006
|
+
|
|
1007
|
+
Before starting sprint planning, review all completed retrospectives for unresolved action items:
|
|
1008
|
+
|
|
1009
|
+
1. **Scan for retrospective files:** Look for all \`epic-N-retrospective.md\` files in \`_bmad-output/implementation-artifacts/\`
|
|
1010
|
+
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
|
|
1011
|
+
3. **Import GitHub issues to beads:** Run \`codeharness github-import\` to pull labeled issues into beads with \`[source:github:owner/repo#N]\` gap-ids
|
|
1012
|
+
4. **Display combined backlog:** Run \`bd ready\` to present the unified backlog containing retro findings, GitHub issues, and manually created issues
|
|
1013
|
+
5. **Identify unresolved items:** Filter for action items that are NOT marked as completed/done
|
|
1014
|
+
6. **Surface during planning:** Present unresolved items to the team before selecting stories for the sprint
|
|
1015
|
+
|
|
1016
|
+
### Source-Aware Backlog Presentation
|
|
1017
|
+
|
|
1018
|
+
When presenting the backlog during triage, issues should be identifiable by source:
|
|
1019
|
+
|
|
1020
|
+
- **Retro findings** have gap-ids matching \`[gap:retro:...]\` \u2014 originated from retrospective action items
|
|
1021
|
+
- **GitHub issues** have gap-ids matching \`[source:github:...]\` \u2014 imported from GitHub via label query
|
|
1022
|
+
- **Manual issues** have no gap-id prefix \u2014 created directly in beads
|
|
1023
|
+
|
|
1024
|
+
### Integration with Sprint Planning
|
|
1025
|
+
|
|
1026
|
+
- [ ] All \`epic-N-retrospective.md\` files scanned for action items
|
|
1027
|
+
- [ ] \`codeharness retro-import --epic N\` run for each unimported retrospective
|
|
1028
|
+
- [ ] \`codeharness github-import\` run to pull labeled GitHub issues
|
|
1029
|
+
- [ ] \`bd ready\` run to display combined backlog from all sources
|
|
1030
|
+
- [ ] Unresolved action items listed and reviewed
|
|
1031
|
+
- [ ] Relevant action items incorporated into sprint goals or new stories
|
|
1032
|
+
- [ ] Recurring issues from multiple retros flagged for systemic fixes
|
|
1033
|
+
- [ ] All sources (retro, GitHub, manual) triaged uniformly \u2014 no source left unreviewed`;
|
|
1034
|
+
}
|
|
984
1035
|
var PATCH_TEMPLATES = {
|
|
985
1036
|
"story-verification": storyVerificationPatch,
|
|
986
1037
|
"dev-enforcement": devEnforcementPatch,
|
|
987
1038
|
"review-enforcement": reviewEnforcementPatch,
|
|
988
1039
|
"retro-enforcement": retroEnforcementPatch,
|
|
989
|
-
"sprint-beads": sprintBeadsPatch
|
|
1040
|
+
"sprint-beads": sprintBeadsPatch,
|
|
1041
|
+
"sprint-retro": sprintPlanningRetroPatch
|
|
990
1042
|
};
|
|
991
1043
|
|
|
992
1044
|
// src/lib/bmad.ts
|
|
@@ -1003,7 +1055,8 @@ var PATCH_TARGETS = {
|
|
|
1003
1055
|
"dev-enforcement": "bmm/workflows/4-implementation/dev-story/checklist.md",
|
|
1004
1056
|
"review-enforcement": "bmm/workflows/4-implementation/code-review/checklist.md",
|
|
1005
1057
|
"retro-enforcement": "bmm/workflows/4-implementation/retrospective/instructions.md",
|
|
1006
|
-
"sprint-beads": "bmm/workflows/4-implementation/sprint-planning/checklist.md"
|
|
1058
|
+
"sprint-beads": "bmm/workflows/4-implementation/sprint-planning/checklist.md",
|
|
1059
|
+
"sprint-retro": "bmm/workflows/4-implementation/sprint-planning/instructions.md"
|
|
1007
1060
|
};
|
|
1008
1061
|
function isBmadInstalled(dir) {
|
|
1009
1062
|
const bmadDir = join5(dir ?? process.cwd(), "_bmad");
|
|
@@ -1064,8 +1117,7 @@ function installBmad(dir) {
|
|
|
1064
1117
|
return {
|
|
1065
1118
|
status: "installed",
|
|
1066
1119
|
version,
|
|
1067
|
-
patches_applied: []
|
|
1068
|
-
bmalph_detected: false
|
|
1120
|
+
patches_applied: []
|
|
1069
1121
|
};
|
|
1070
1122
|
}
|
|
1071
1123
|
function applyAllPatches(dir) {
|
|
@@ -1288,7 +1340,7 @@ function importStoriesToBeads(stories, opts, beadsFns) {
|
|
|
1288
1340
|
}
|
|
1289
1341
|
|
|
1290
1342
|
// src/commands/init.ts
|
|
1291
|
-
var HARNESS_VERSION = true ? "0.
|
|
1343
|
+
var HARNESS_VERSION = true ? "0.8.0" : "0.0.0-dev";
|
|
1292
1344
|
function getStackLabel(stack) {
|
|
1293
1345
|
if (stack === "nodejs") return "Node.js (package.json)";
|
|
1294
1346
|
if (stack === "python") return "Python";
|
|
@@ -3182,123 +3234,170 @@ function isValidStoryId(storyId) {
|
|
|
3182
3234
|
return /^[a-zA-Z0-9_-]+$/.test(storyId);
|
|
3183
3235
|
}
|
|
3184
3236
|
function registerVerifyCommand(program) {
|
|
3185
|
-
program.command("verify").description("Run verification pipeline on completed work").
|
|
3237
|
+
program.command("verify").description("Run verification pipeline on completed work").option("--story <id>", "Story ID to verify").option("--retro", "Verify retrospective completion for an epic").option("--epic <n>", "Epic number (required with --retro)").action((opts, cmd) => {
|
|
3186
3238
|
const globalOpts = cmd.optsWithGlobals();
|
|
3187
3239
|
const isJson = globalOpts.json === true;
|
|
3188
|
-
const storyId = opts.story;
|
|
3189
3240
|
const root = process.cwd();
|
|
3190
|
-
if (
|
|
3191
|
-
|
|
3192
|
-
process.exitCode = 1;
|
|
3241
|
+
if (opts.retro) {
|
|
3242
|
+
verifyRetro(opts, isJson, root);
|
|
3193
3243
|
return;
|
|
3194
3244
|
}
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
fail(`Story file not found: ${storyFilePath}`, { json: isJson });
|
|
3245
|
+
if (!opts.story) {
|
|
3246
|
+
fail("--story is required when --retro is not set", { json: isJson });
|
|
3198
3247
|
process.exitCode = 1;
|
|
3199
3248
|
return;
|
|
3200
3249
|
}
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3250
|
+
verifyStory(opts.story, isJson, root);
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
function verifyRetro(opts, isJson, root) {
|
|
3254
|
+
if (!opts.epic) {
|
|
3255
|
+
fail("--epic is required with --retro", { json: isJson });
|
|
3256
|
+
process.exitCode = 1;
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
const epicNum = parseInt(opts.epic, 10);
|
|
3260
|
+
if (isNaN(epicNum) || epicNum < 1) {
|
|
3261
|
+
fail(`Invalid epic number: ${opts.epic}`, { json: isJson });
|
|
3262
|
+
process.exitCode = 1;
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
3266
|
+
const retroPath = join11(root, STORY_DIR, retroFile);
|
|
3267
|
+
if (!existsSync13(retroPath)) {
|
|
3268
|
+
if (isJson) {
|
|
3269
|
+
jsonOutput({ status: "fail", epic: epicNum, retroFile, message: `${retroFile} not found` });
|
|
3270
|
+
} else {
|
|
3271
|
+
fail(`${retroFile} not found`);
|
|
3209
3272
|
}
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3273
|
+
process.exitCode = 1;
|
|
3274
|
+
return;
|
|
3275
|
+
}
|
|
3276
|
+
const retroKey = `epic-${epicNum}-retrospective`;
|
|
3277
|
+
try {
|
|
3278
|
+
updateSprintStatus(retroKey, "done", root);
|
|
3279
|
+
} catch (err) {
|
|
3280
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3281
|
+
warn(`Failed to update sprint status: ${message}`);
|
|
3282
|
+
}
|
|
3283
|
+
if (isJson) {
|
|
3284
|
+
jsonOutput({ status: "ok", epic: epicNum, retroFile: join11(STORY_DIR, retroFile) });
|
|
3285
|
+
} else {
|
|
3286
|
+
ok(`Epic ${epicNum} retrospective: marked done`);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
function verifyStory(storyId, isJson, root) {
|
|
3290
|
+
if (!isValidStoryId(storyId)) {
|
|
3291
|
+
fail(`Invalid story ID: ${storyId}. Story IDs must contain only alphanumeric characters, hyphens, and underscores.`, { json: isJson });
|
|
3292
|
+
process.exitCode = 1;
|
|
3293
|
+
return;
|
|
3294
|
+
}
|
|
3295
|
+
const storyFilePath = join11(root, STORY_DIR, `${storyId}.md`);
|
|
3296
|
+
if (!existsSync13(storyFilePath)) {
|
|
3297
|
+
fail(`Story file not found: ${storyFilePath}`, { json: isJson });
|
|
3298
|
+
process.exitCode = 1;
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
let preconditions;
|
|
3302
|
+
try {
|
|
3303
|
+
preconditions = checkPreconditions(root, storyId);
|
|
3304
|
+
} catch (err) {
|
|
3305
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3306
|
+
fail(`Precondition check failed: ${message}`, { json: isJson });
|
|
3307
|
+
process.exitCode = 1;
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
if (!preconditions.passed) {
|
|
3311
|
+
if (isJson) {
|
|
3312
|
+
jsonOutput({
|
|
3313
|
+
status: "fail",
|
|
3314
|
+
message: "Preconditions not met",
|
|
3315
|
+
failures: preconditions.failures
|
|
3316
|
+
});
|
|
3317
|
+
} else {
|
|
3318
|
+
fail("Preconditions not met:");
|
|
3319
|
+
for (const f of preconditions.failures) {
|
|
3320
|
+
info(` - ${f}`);
|
|
3222
3321
|
}
|
|
3223
|
-
process.exitCode = 1;
|
|
3224
|
-
return;
|
|
3225
3322
|
}
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
}
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
}
|
|
3323
|
+
process.exitCode = 1;
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
let acs;
|
|
3327
|
+
try {
|
|
3328
|
+
acs = parseStoryACs(storyFilePath);
|
|
3329
|
+
} catch (err) {
|
|
3330
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3331
|
+
fail(`Failed to parse story file: ${message}`, { json: isJson });
|
|
3332
|
+
process.exitCode = 1;
|
|
3333
|
+
return;
|
|
3334
|
+
}
|
|
3335
|
+
const storyTitle = extractStoryTitle(storyFilePath);
|
|
3336
|
+
const proofPath = createProofDocument(storyId, storyTitle, acs, root);
|
|
3337
|
+
let showboatStatus = "skipped";
|
|
3338
|
+
if (proofHasContent(proofPath)) {
|
|
3339
|
+
const showboatResult = runShowboatVerify(proofPath);
|
|
3340
|
+
if (showboatResult.output === "showboat not available") {
|
|
3341
|
+
showboatStatus = "skipped";
|
|
3342
|
+
warn("Showboat not installed \u2014 skipping re-verification");
|
|
3343
|
+
} else {
|
|
3344
|
+
showboatStatus = showboatResult.passed ? "pass" : "fail";
|
|
3345
|
+
if (!showboatResult.passed) {
|
|
3346
|
+
fail(`Showboat verify failed: ${showboatResult.output}`, { json: isJson });
|
|
3347
|
+
process.exitCode = 1;
|
|
3348
|
+
return;
|
|
3250
3349
|
}
|
|
3251
3350
|
}
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
}
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
}
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
}
|
|
3287
|
-
} else {
|
|
3288
|
-
if (!isJson) {
|
|
3289
|
-
warn(`No exec-plan found for story: ${storyId}`);
|
|
3290
|
-
}
|
|
3351
|
+
}
|
|
3352
|
+
const acsVerified = showboatStatus === "pass";
|
|
3353
|
+
const verifiedCount = acsVerified ? acs.length : 0;
|
|
3354
|
+
const result = {
|
|
3355
|
+
storyId,
|
|
3356
|
+
success: true,
|
|
3357
|
+
totalACs: acs.length,
|
|
3358
|
+
verifiedCount,
|
|
3359
|
+
failedCount: acs.length - verifiedCount,
|
|
3360
|
+
proofPath: `verification/${storyId}-proof.md`,
|
|
3361
|
+
showboatVerifyStatus: showboatStatus,
|
|
3362
|
+
perAC: acs.map((ac) => ({
|
|
3363
|
+
id: ac.id,
|
|
3364
|
+
description: ac.description,
|
|
3365
|
+
verified: acsVerified,
|
|
3366
|
+
evidencePaths: []
|
|
3367
|
+
}))
|
|
3368
|
+
};
|
|
3369
|
+
try {
|
|
3370
|
+
updateVerificationState(storyId, result, root);
|
|
3371
|
+
} catch (err) {
|
|
3372
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3373
|
+
warn(`Failed to update state: ${message}`);
|
|
3374
|
+
}
|
|
3375
|
+
try {
|
|
3376
|
+
closeBeadsIssue(storyId, root);
|
|
3377
|
+
} catch (err) {
|
|
3378
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3379
|
+
warn(`Failed to close beads issue: ${message}`);
|
|
3380
|
+
}
|
|
3381
|
+
try {
|
|
3382
|
+
const completedPath = completeExecPlan(storyId, root);
|
|
3383
|
+
if (completedPath) {
|
|
3384
|
+
if (!isJson) {
|
|
3385
|
+
ok(`Exec-plan moved to completed: ${completedPath}`);
|
|
3291
3386
|
}
|
|
3292
|
-
} catch (err) {
|
|
3293
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
3294
|
-
warn(`Failed to complete exec-plan: ${message}`);
|
|
3295
|
-
}
|
|
3296
|
-
if (isJson) {
|
|
3297
|
-
jsonOutput(result);
|
|
3298
3387
|
} else {
|
|
3299
|
-
|
|
3388
|
+
if (!isJson) {
|
|
3389
|
+
warn(`No exec-plan found for story: ${storyId}`);
|
|
3390
|
+
}
|
|
3300
3391
|
}
|
|
3301
|
-
})
|
|
3392
|
+
} catch (err) {
|
|
3393
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3394
|
+
warn(`Failed to complete exec-plan: ${message}`);
|
|
3395
|
+
}
|
|
3396
|
+
if (isJson) {
|
|
3397
|
+
jsonOutput(result);
|
|
3398
|
+
} else {
|
|
3399
|
+
ok(`Story ${storyId}: verified \u2014 proof at verification/${storyId}-proof.md`);
|
|
3400
|
+
}
|
|
3302
3401
|
}
|
|
3303
3402
|
function extractStoryTitle(filePath) {
|
|
3304
3403
|
try {
|
|
@@ -6085,8 +6184,453 @@ function registerQueryCommand(program) {
|
|
|
6085
6184
|
});
|
|
6086
6185
|
}
|
|
6087
6186
|
|
|
6187
|
+
// src/commands/retro-import.ts
|
|
6188
|
+
import { existsSync as existsSync20, readFileSync as readFileSync16 } from "fs";
|
|
6189
|
+
import { join as join19 } from "path";
|
|
6190
|
+
|
|
6191
|
+
// src/lib/retro-parser.ts
|
|
6192
|
+
var KNOWN_TOOLS = ["showboat", "ralph", "beads", "bmad"];
|
|
6193
|
+
function parseRetroActionItems(content) {
|
|
6194
|
+
const lines = content.split("\n");
|
|
6195
|
+
const items = [];
|
|
6196
|
+
let inTable = false;
|
|
6197
|
+
for (const line of lines) {
|
|
6198
|
+
const trimmed = line.trim();
|
|
6199
|
+
if (!inTable && /^\|\s*#\s*\|\s*Action\s*\|\s*Status\s*\|\s*Notes\s*\|/i.test(trimmed)) {
|
|
6200
|
+
inTable = true;
|
|
6201
|
+
continue;
|
|
6202
|
+
}
|
|
6203
|
+
if (inTable && /^\|[\s\-|]+\|$/.test(trimmed)) {
|
|
6204
|
+
continue;
|
|
6205
|
+
}
|
|
6206
|
+
if (inTable && trimmed.startsWith("|")) {
|
|
6207
|
+
const cells = trimmed.split("|").slice(1, -1).map((c) => c.trim());
|
|
6208
|
+
if (cells.length >= 4) {
|
|
6209
|
+
const number = cells[0];
|
|
6210
|
+
const description = cells[1];
|
|
6211
|
+
const status = cells[2];
|
|
6212
|
+
const notes = cells[3];
|
|
6213
|
+
if (/^[A-Za-z]\d+$/.test(number)) {
|
|
6214
|
+
items.push({ number, description, status, notes });
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
6217
|
+
}
|
|
6218
|
+
if (inTable && !trimmed.startsWith("|") && trimmed !== "") {
|
|
6219
|
+
inTable = false;
|
|
6220
|
+
}
|
|
6221
|
+
}
|
|
6222
|
+
return items;
|
|
6223
|
+
}
|
|
6224
|
+
function classifyFinding(item) {
|
|
6225
|
+
const text = item.description.toLowerCase();
|
|
6226
|
+
if (text.includes("harness") || text.includes("codeharness")) {
|
|
6227
|
+
return { type: "harness" };
|
|
6228
|
+
}
|
|
6229
|
+
for (const tool of KNOWN_TOOLS) {
|
|
6230
|
+
if (text.includes(tool)) {
|
|
6231
|
+
return { type: "tool", name: tool };
|
|
6232
|
+
}
|
|
6233
|
+
}
|
|
6234
|
+
return { type: "project" };
|
|
6235
|
+
}
|
|
6236
|
+
function derivePriority(item) {
|
|
6237
|
+
const statusLower = item.status.toLowerCase();
|
|
6238
|
+
const notesLower = item.notes.toLowerCase();
|
|
6239
|
+
if (statusLower.includes("regressed") || notesLower.includes("urgent") || notesLower.includes("critical")) {
|
|
6240
|
+
return 1;
|
|
6241
|
+
}
|
|
6242
|
+
return 2;
|
|
6243
|
+
}
|
|
6244
|
+
|
|
6245
|
+
// src/lib/github.ts
|
|
6246
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
6247
|
+
var GitHubError = class extends Error {
|
|
6248
|
+
constructor(command, originalMessage) {
|
|
6249
|
+
super(`GitHub CLI failed: ${originalMessage}. Command: ${command}`);
|
|
6250
|
+
this.command = command;
|
|
6251
|
+
this.originalMessage = originalMessage;
|
|
6252
|
+
this.name = "GitHubError";
|
|
6253
|
+
}
|
|
6254
|
+
};
|
|
6255
|
+
function isGhAvailable() {
|
|
6256
|
+
try {
|
|
6257
|
+
execFileSync6("which", ["gh"], { stdio: "pipe", timeout: 5e3 });
|
|
6258
|
+
return true;
|
|
6259
|
+
} catch {
|
|
6260
|
+
return false;
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
function ghIssueCreate(repo, title, body, labels) {
|
|
6264
|
+
const args = ["issue", "create", "--repo", repo, "--title", title, "--body", body];
|
|
6265
|
+
for (const label of labels) {
|
|
6266
|
+
args.push("--label", label);
|
|
6267
|
+
}
|
|
6268
|
+
args.push("--json", "number,url");
|
|
6269
|
+
const cmdStr = `gh ${args.join(" ")}`;
|
|
6270
|
+
try {
|
|
6271
|
+
const output = execFileSync6("gh", args, {
|
|
6272
|
+
stdio: "pipe",
|
|
6273
|
+
timeout: 3e4
|
|
6274
|
+
});
|
|
6275
|
+
const result = JSON.parse(output.toString().trim());
|
|
6276
|
+
return result;
|
|
6277
|
+
} catch (err) {
|
|
6278
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6279
|
+
throw new GitHubError(cmdStr, message);
|
|
6280
|
+
}
|
|
6281
|
+
}
|
|
6282
|
+
function ghIssueSearch(repo, query) {
|
|
6283
|
+
const args = ["issue", "list", "--repo", repo, "--search", query, "--state", "all", "--json", "number,title,body,url,labels"];
|
|
6284
|
+
const cmdStr = `gh ${args.join(" ")}`;
|
|
6285
|
+
try {
|
|
6286
|
+
const output = execFileSync6("gh", args, {
|
|
6287
|
+
stdio: "pipe",
|
|
6288
|
+
timeout: 3e4
|
|
6289
|
+
});
|
|
6290
|
+
const text = output.toString().trim();
|
|
6291
|
+
if (!text) return [];
|
|
6292
|
+
return JSON.parse(text);
|
|
6293
|
+
} catch (err) {
|
|
6294
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6295
|
+
throw new GitHubError(cmdStr, message);
|
|
6296
|
+
}
|
|
6297
|
+
}
|
|
6298
|
+
function findExistingGhIssue(repo, gapId) {
|
|
6299
|
+
try {
|
|
6300
|
+
const issues = ghIssueSearch(repo, gapId);
|
|
6301
|
+
return issues.find((issue) => issue.body?.includes(gapId));
|
|
6302
|
+
} catch {
|
|
6303
|
+
return void 0;
|
|
6304
|
+
}
|
|
6305
|
+
}
|
|
6306
|
+
function getRepoFromRemote() {
|
|
6307
|
+
try {
|
|
6308
|
+
const output = execFileSync6("git", ["remote", "get-url", "origin"], {
|
|
6309
|
+
stdio: "pipe",
|
|
6310
|
+
timeout: 5e3
|
|
6311
|
+
});
|
|
6312
|
+
const url = output.toString().trim();
|
|
6313
|
+
return parseRepoFromUrl(url);
|
|
6314
|
+
} catch {
|
|
6315
|
+
return void 0;
|
|
6316
|
+
}
|
|
6317
|
+
}
|
|
6318
|
+
function parseRepoFromUrl(url) {
|
|
6319
|
+
const sshMatch = url.match(/git@[^:]+:([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
6320
|
+
if (sshMatch) return sshMatch[1];
|
|
6321
|
+
const httpsMatch = url.match(/https?:\/\/[^/]+\/([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
6322
|
+
if (httpsMatch) return httpsMatch[1];
|
|
6323
|
+
return void 0;
|
|
6324
|
+
}
|
|
6325
|
+
function ensureLabels(repo, labels) {
|
|
6326
|
+
for (const label of labels) {
|
|
6327
|
+
try {
|
|
6328
|
+
execFileSync6("gh", ["label", "create", label, "--repo", repo], {
|
|
6329
|
+
stdio: "pipe",
|
|
6330
|
+
timeout: 1e4
|
|
6331
|
+
});
|
|
6332
|
+
} catch {
|
|
6333
|
+
}
|
|
6334
|
+
}
|
|
6335
|
+
}
|
|
6336
|
+
|
|
6337
|
+
// src/commands/retro-import.ts
|
|
6338
|
+
var STORY_DIR2 = "_bmad-output/implementation-artifacts";
|
|
6339
|
+
var MAX_TITLE_LENGTH = 120;
|
|
6340
|
+
function classificationToString(c) {
|
|
6341
|
+
if (c.type === "tool") {
|
|
6342
|
+
return `tool:${c.name}`;
|
|
6343
|
+
}
|
|
6344
|
+
return c.type;
|
|
6345
|
+
}
|
|
6346
|
+
function registerRetroImportCommand(program) {
|
|
6347
|
+
program.command("retro-import").description("Import retrospective action items as beads issues").requiredOption("--epic <n>", "Epic number to import action items from").action((opts, cmd) => {
|
|
6348
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
6349
|
+
const isJson = globalOpts.json === true;
|
|
6350
|
+
const root = process.cwd();
|
|
6351
|
+
const epicNum = parseInt(opts.epic, 10);
|
|
6352
|
+
if (isNaN(epicNum) || epicNum < 1) {
|
|
6353
|
+
fail(`Invalid epic number: ${opts.epic}`, { json: isJson });
|
|
6354
|
+
process.exitCode = 1;
|
|
6355
|
+
return;
|
|
6356
|
+
}
|
|
6357
|
+
const retroFile = `epic-${epicNum}-retrospective.md`;
|
|
6358
|
+
const retroPath = join19(root, STORY_DIR2, retroFile);
|
|
6359
|
+
if (!existsSync20(retroPath)) {
|
|
6360
|
+
fail(`Retro file not found: ${retroFile}`, { json: isJson });
|
|
6361
|
+
process.exitCode = 1;
|
|
6362
|
+
return;
|
|
6363
|
+
}
|
|
6364
|
+
let content;
|
|
6365
|
+
try {
|
|
6366
|
+
content = readFileSync16(retroPath, "utf-8");
|
|
6367
|
+
} catch (err) {
|
|
6368
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6369
|
+
fail(`Failed to read retro file: ${message}`, { json: isJson });
|
|
6370
|
+
process.exitCode = 1;
|
|
6371
|
+
return;
|
|
6372
|
+
}
|
|
6373
|
+
const items = parseRetroActionItems(content);
|
|
6374
|
+
if (items.length === 0) {
|
|
6375
|
+
if (isJson) {
|
|
6376
|
+
jsonOutput({ imported: 0, skipped: 0, issues: [] });
|
|
6377
|
+
} else {
|
|
6378
|
+
info("No action items found in retro file");
|
|
6379
|
+
}
|
|
6380
|
+
return;
|
|
6381
|
+
}
|
|
6382
|
+
let imported = 0;
|
|
6383
|
+
let skipped = 0;
|
|
6384
|
+
const issues = [];
|
|
6385
|
+
for (const item of items) {
|
|
6386
|
+
const classification = classifyFinding(item);
|
|
6387
|
+
const priority = derivePriority(item);
|
|
6388
|
+
const gapId = buildGapId("retro", `epic-${epicNum}-item-${item.number}`);
|
|
6389
|
+
const title = item.description.length > MAX_TITLE_LENGTH ? item.description.slice(0, MAX_TITLE_LENGTH - 3) + "..." : item.description;
|
|
6390
|
+
const retroContext = `Retro action item ${item.number} from Epic ${epicNum}.
|
|
6391
|
+
Status: ${item.status}
|
|
6392
|
+
Notes: ${item.notes}
|
|
6393
|
+
Classification: ${classificationToString(classification)}`;
|
|
6394
|
+
try {
|
|
6395
|
+
const result = createOrFindIssue(title, gapId, {
|
|
6396
|
+
type: "task",
|
|
6397
|
+
priority,
|
|
6398
|
+
description: retroContext
|
|
6399
|
+
});
|
|
6400
|
+
const issueRecord = {
|
|
6401
|
+
number: item.number,
|
|
6402
|
+
title,
|
|
6403
|
+
gapId,
|
|
6404
|
+
classification: classificationToString(classification),
|
|
6405
|
+
created: result.created,
|
|
6406
|
+
status: item.status,
|
|
6407
|
+
notes: item.notes
|
|
6408
|
+
};
|
|
6409
|
+
issues.push(issueRecord);
|
|
6410
|
+
if (result.created) {
|
|
6411
|
+
imported++;
|
|
6412
|
+
if (!isJson) {
|
|
6413
|
+
ok(`Imported: ${title}`);
|
|
6414
|
+
}
|
|
6415
|
+
} else {
|
|
6416
|
+
skipped++;
|
|
6417
|
+
if (!isJson) {
|
|
6418
|
+
info(`Skipping existing: ${title}`);
|
|
6419
|
+
}
|
|
6420
|
+
}
|
|
6421
|
+
} catch (err) {
|
|
6422
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6423
|
+
fail(`Failed to import ${item.number}: ${message}`, { json: isJson });
|
|
6424
|
+
}
|
|
6425
|
+
}
|
|
6426
|
+
const githubResult = createGitHubIssues(issues, epicNum, isJson);
|
|
6427
|
+
if (isJson) {
|
|
6428
|
+
jsonOutput({
|
|
6429
|
+
imported,
|
|
6430
|
+
skipped,
|
|
6431
|
+
issues,
|
|
6432
|
+
github: githubResult
|
|
6433
|
+
});
|
|
6434
|
+
}
|
|
6435
|
+
});
|
|
6436
|
+
}
|
|
6437
|
+
function resolveTargetRepo(classification, targets) {
|
|
6438
|
+
if (targets.length === 0) return void 0;
|
|
6439
|
+
if (classification === "harness") {
|
|
6440
|
+
const explicit = targets.find((t) => t.repo === "iVintik/codeharness");
|
|
6441
|
+
if (explicit) return explicit;
|
|
6442
|
+
const nonAuto = targets.find((t) => t.repo !== "auto");
|
|
6443
|
+
if (nonAuto) return nonAuto;
|
|
6444
|
+
return targets[0];
|
|
6445
|
+
}
|
|
6446
|
+
const auto = targets.find((t) => t.repo === "auto");
|
|
6447
|
+
if (auto) return auto;
|
|
6448
|
+
return targets[0];
|
|
6449
|
+
}
|
|
6450
|
+
function buildGitHubIssueBody(item, epicNum, projectName) {
|
|
6451
|
+
return `## Retro Action Item ${item.number} \u2014 Epic ${epicNum}
|
|
6452
|
+
|
|
6453
|
+
**Source project:** ${projectName}
|
|
6454
|
+
**Classification:** ${item.classification}
|
|
6455
|
+
**Original status:** ${item.status}
|
|
6456
|
+
**Notes:** ${item.notes}
|
|
6457
|
+
|
|
6458
|
+
${item.title}
|
|
6459
|
+
|
|
6460
|
+
<!-- gap-id: ${item.gapId} -->`;
|
|
6461
|
+
}
|
|
6462
|
+
function createGitHubIssues(issues, epicNum, isJson) {
|
|
6463
|
+
let targets;
|
|
6464
|
+
try {
|
|
6465
|
+
const state = readState();
|
|
6466
|
+
targets = state.retro_issue_targets;
|
|
6467
|
+
} catch (err) {
|
|
6468
|
+
if (err instanceof StateFileNotFoundError) {
|
|
6469
|
+
if (!isJson) {
|
|
6470
|
+
info("No state file found \u2014 skipping GitHub issues");
|
|
6471
|
+
}
|
|
6472
|
+
return void 0;
|
|
6473
|
+
}
|
|
6474
|
+
if (!isJson) {
|
|
6475
|
+
info("Could not read state file \u2014 skipping GitHub issues");
|
|
6476
|
+
}
|
|
6477
|
+
return void 0;
|
|
6478
|
+
}
|
|
6479
|
+
if (!targets || targets.length === 0) {
|
|
6480
|
+
if (!isJson) {
|
|
6481
|
+
info("No retro_issue_targets configured \u2014 skipping GitHub issues");
|
|
6482
|
+
}
|
|
6483
|
+
return void 0;
|
|
6484
|
+
}
|
|
6485
|
+
if (!isGhAvailable()) {
|
|
6486
|
+
if (!isJson) {
|
|
6487
|
+
warn("gh CLI not available \u2014 skipping GitHub issue creation");
|
|
6488
|
+
}
|
|
6489
|
+
return void 0;
|
|
6490
|
+
}
|
|
6491
|
+
const resolvedAutoRepo = getRepoFromRemote();
|
|
6492
|
+
const result = { created: 0, skipped: 0, errors: 0 };
|
|
6493
|
+
const projectName = resolvedAutoRepo ?? "unknown";
|
|
6494
|
+
for (const item of issues) {
|
|
6495
|
+
const target = resolveTargetRepo(item.classification, targets);
|
|
6496
|
+
if (!target) continue;
|
|
6497
|
+
const repo = target.repo === "auto" ? resolvedAutoRepo : target.repo;
|
|
6498
|
+
if (!repo) {
|
|
6499
|
+
if (!isJson) {
|
|
6500
|
+
warn(`Cannot resolve repo for ${item.number} \u2014 git remote not detected`);
|
|
6501
|
+
}
|
|
6502
|
+
result.errors++;
|
|
6503
|
+
continue;
|
|
6504
|
+
}
|
|
6505
|
+
try {
|
|
6506
|
+
const existing = findExistingGhIssue(repo, item.gapId);
|
|
6507
|
+
if (existing) {
|
|
6508
|
+
if (!isJson) {
|
|
6509
|
+
info(`GitHub issue exists: ${repo}#${existing.number}`);
|
|
6510
|
+
}
|
|
6511
|
+
result.skipped++;
|
|
6512
|
+
continue;
|
|
6513
|
+
}
|
|
6514
|
+
ensureLabels(repo, target.labels);
|
|
6515
|
+
const body = buildGitHubIssueBody(item, epicNum, projectName);
|
|
6516
|
+
const created = ghIssueCreate(repo, item.title, body, target.labels);
|
|
6517
|
+
if (!isJson) {
|
|
6518
|
+
ok(`GitHub issue created: ${repo}#${created.number}`);
|
|
6519
|
+
}
|
|
6520
|
+
result.created++;
|
|
6521
|
+
} catch (err) {
|
|
6522
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6523
|
+
if (!isJson) {
|
|
6524
|
+
fail(`GitHub issue failed for ${item.number}: ${message}`);
|
|
6525
|
+
}
|
|
6526
|
+
result.errors++;
|
|
6527
|
+
}
|
|
6528
|
+
}
|
|
6529
|
+
return result;
|
|
6530
|
+
}
|
|
6531
|
+
|
|
6532
|
+
// src/commands/github-import.ts
|
|
6533
|
+
var MAX_TITLE_LENGTH2 = 120;
|
|
6534
|
+
function mapLabelsToType(labels) {
|
|
6535
|
+
if (!labels) return "task";
|
|
6536
|
+
const names = labels.map((l) => l.name);
|
|
6537
|
+
if (names.includes("bug")) return "bug";
|
|
6538
|
+
if (names.includes("enhancement")) return "story";
|
|
6539
|
+
return "task";
|
|
6540
|
+
}
|
|
6541
|
+
function mapLabelsToPriority(labels) {
|
|
6542
|
+
if (!labels) return 2;
|
|
6543
|
+
const names = labels.map((l) => l.name);
|
|
6544
|
+
if (names.includes("priority:high")) return 1;
|
|
6545
|
+
if (names.includes("priority:low")) return 3;
|
|
6546
|
+
return 2;
|
|
6547
|
+
}
|
|
6548
|
+
function registerGithubImportCommand(program) {
|
|
6549
|
+
program.command("github-import").description("Import GitHub issues labeled for sprint planning into beads").option("--repo <owner/repo>", "GitHub repository (auto-detected from git remote if omitted)").option("--label <label>", "GitHub label to filter issues by", "sprint-candidate").action((opts, cmd) => {
|
|
6550
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
6551
|
+
const isJson = globalOpts.json === true;
|
|
6552
|
+
if (!isGhAvailable()) {
|
|
6553
|
+
fail("gh CLI not found. Install: https://cli.github.com/", { json: isJson });
|
|
6554
|
+
process.exitCode = 1;
|
|
6555
|
+
return;
|
|
6556
|
+
}
|
|
6557
|
+
let repo = opts.repo;
|
|
6558
|
+
if (!repo) {
|
|
6559
|
+
repo = getRepoFromRemote();
|
|
6560
|
+
}
|
|
6561
|
+
if (!repo) {
|
|
6562
|
+
fail("Cannot detect repo. Use --repo owner/repo", { json: isJson });
|
|
6563
|
+
process.exitCode = 1;
|
|
6564
|
+
return;
|
|
6565
|
+
}
|
|
6566
|
+
const label = opts.label;
|
|
6567
|
+
let ghIssues;
|
|
6568
|
+
try {
|
|
6569
|
+
ghIssues = ghIssueSearch(repo, `label:${label}`);
|
|
6570
|
+
} catch (err) {
|
|
6571
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6572
|
+
fail(`Failed to search GitHub issues: ${message}`, { json: isJson });
|
|
6573
|
+
process.exitCode = 1;
|
|
6574
|
+
return;
|
|
6575
|
+
}
|
|
6576
|
+
let imported = 0;
|
|
6577
|
+
let skipped = 0;
|
|
6578
|
+
let errors = 0;
|
|
6579
|
+
const issues = [];
|
|
6580
|
+
for (const ghIssue of ghIssues) {
|
|
6581
|
+
const gapId = buildGapId("source", `github:${repo}#${ghIssue.number}`);
|
|
6582
|
+
const type = mapLabelsToType(ghIssue.labels);
|
|
6583
|
+
const priority = mapLabelsToPriority(ghIssue.labels);
|
|
6584
|
+
const title = ghIssue.title.length > MAX_TITLE_LENGTH2 ? ghIssue.title.slice(0, MAX_TITLE_LENGTH2 - 3) + "..." : ghIssue.title;
|
|
6585
|
+
try {
|
|
6586
|
+
const result = createOrFindIssue(title, gapId, {
|
|
6587
|
+
type,
|
|
6588
|
+
priority,
|
|
6589
|
+
description: ghIssue.body ?? ""
|
|
6590
|
+
});
|
|
6591
|
+
const issueRecord = {
|
|
6592
|
+
number: ghIssue.number,
|
|
6593
|
+
title,
|
|
6594
|
+
gapId,
|
|
6595
|
+
type,
|
|
6596
|
+
created: result.created
|
|
6597
|
+
};
|
|
6598
|
+
issues.push(issueRecord);
|
|
6599
|
+
if (result.created) {
|
|
6600
|
+
imported++;
|
|
6601
|
+
if (!isJson) {
|
|
6602
|
+
ok(`Imported: ${repo}#${ghIssue.number} \u2014 ${title}`);
|
|
6603
|
+
}
|
|
6604
|
+
} else {
|
|
6605
|
+
skipped++;
|
|
6606
|
+
if (!isJson) {
|
|
6607
|
+
info(`Skipping existing: ${repo}#${ghIssue.number} \u2014 ${title}`);
|
|
6608
|
+
}
|
|
6609
|
+
}
|
|
6610
|
+
} catch (err) {
|
|
6611
|
+
errors++;
|
|
6612
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6613
|
+
fail(`Failed to import ${repo}#${ghIssue.number}: ${message}`, { json: isJson });
|
|
6614
|
+
}
|
|
6615
|
+
}
|
|
6616
|
+
if (errors > 0) {
|
|
6617
|
+
process.exitCode = 1;
|
|
6618
|
+
}
|
|
6619
|
+
if (isJson) {
|
|
6620
|
+
jsonOutput({
|
|
6621
|
+
imported,
|
|
6622
|
+
skipped,
|
|
6623
|
+
errors,
|
|
6624
|
+
issues
|
|
6625
|
+
});
|
|
6626
|
+
} else if (ghIssues.length > 0) {
|
|
6627
|
+
info(`Summary: ${imported} imported, ${skipped} skipped, ${errors} errors`);
|
|
6628
|
+
}
|
|
6629
|
+
});
|
|
6630
|
+
}
|
|
6631
|
+
|
|
6088
6632
|
// src/index.ts
|
|
6089
|
-
var VERSION = true ? "0.
|
|
6633
|
+
var VERSION = true ? "0.8.0" : "0.0.0-dev";
|
|
6090
6634
|
function createProgram() {
|
|
6091
6635
|
const program = new Command();
|
|
6092
6636
|
program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
|
|
@@ -6103,6 +6647,8 @@ function createProgram() {
|
|
|
6103
6647
|
registerDocHealthCommand(program);
|
|
6104
6648
|
registerStackCommand(program);
|
|
6105
6649
|
registerQueryCommand(program);
|
|
6650
|
+
registerRetroImportCommand(program);
|
|
6651
|
+
registerGithubImportCommand(program);
|
|
6106
6652
|
return program;
|
|
6107
6653
|
}
|
|
6108
6654
|
if (!process.env["VITEST"]) {
|