codeharness 0.6.1 → 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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +665 -109
  3. package/package.json +6 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ivan Vintik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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.6.1" : "0.0.0-dev";
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").requiredOption("--story <id>", "Story ID to verify").action((opts, cmd) => {
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 (!isValidStoryId(storyId)) {
3191
- fail(`Invalid story ID: ${storyId}. Story IDs must contain only alphanumeric characters, hyphens, and underscores.`, { json: isJson });
3192
- process.exitCode = 1;
3241
+ if (opts.retro) {
3242
+ verifyRetro(opts, isJson, root);
3193
3243
  return;
3194
3244
  }
3195
- const storyFilePath = join11(root, STORY_DIR, `${storyId}.md`);
3196
- if (!existsSync13(storyFilePath)) {
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
- let preconditions;
3202
- try {
3203
- preconditions = checkPreconditions(root, storyId);
3204
- } catch (err) {
3205
- const message = err instanceof Error ? err.message : String(err);
3206
- fail(`Precondition check failed: ${message}`, { json: isJson });
3207
- process.exitCode = 1;
3208
- return;
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
- if (!preconditions.passed) {
3211
- if (isJson) {
3212
- jsonOutput({
3213
- status: "fail",
3214
- message: "Preconditions not met",
3215
- failures: preconditions.failures
3216
- });
3217
- } else {
3218
- fail("Preconditions not met:");
3219
- for (const f of preconditions.failures) {
3220
- info(` - ${f}`);
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
- let acs;
3227
- try {
3228
- acs = parseStoryACs(storyFilePath);
3229
- } catch (err) {
3230
- const message = err instanceof Error ? err.message : String(err);
3231
- fail(`Failed to parse story file: ${message}`, { json: isJson });
3232
- process.exitCode = 1;
3233
- return;
3234
- }
3235
- const storyTitle = extractStoryTitle(storyFilePath);
3236
- const proofPath = createProofDocument(storyId, storyTitle, acs, root);
3237
- let showboatStatus = "skipped";
3238
- if (proofHasContent(proofPath)) {
3239
- const showboatResult = runShowboatVerify(proofPath);
3240
- if (showboatResult.output === "showboat not available") {
3241
- showboatStatus = "skipped";
3242
- warn("Showboat not installed \u2014 skipping re-verification");
3243
- } else {
3244
- showboatStatus = showboatResult.passed ? "pass" : "fail";
3245
- if (!showboatResult.passed) {
3246
- fail(`Showboat verify failed: ${showboatResult.output}`, { json: isJson });
3247
- process.exitCode = 1;
3248
- return;
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
- const acsVerified = showboatStatus === "pass";
3253
- const verifiedCount = acsVerified ? acs.length : 0;
3254
- const result = {
3255
- storyId,
3256
- success: true,
3257
- totalACs: acs.length,
3258
- verifiedCount,
3259
- failedCount: acs.length - verifiedCount,
3260
- proofPath: `verification/${storyId}-proof.md`,
3261
- showboatVerifyStatus: showboatStatus,
3262
- perAC: acs.map((ac) => ({
3263
- id: ac.id,
3264
- description: ac.description,
3265
- verified: acsVerified,
3266
- evidencePaths: []
3267
- }))
3268
- };
3269
- try {
3270
- updateVerificationState(storyId, result, root);
3271
- } catch (err) {
3272
- const message = err instanceof Error ? err.message : String(err);
3273
- warn(`Failed to update state: ${message}`);
3274
- }
3275
- try {
3276
- closeBeadsIssue(storyId, root);
3277
- } catch (err) {
3278
- const message = err instanceof Error ? err.message : String(err);
3279
- warn(`Failed to close beads issue: ${message}`);
3280
- }
3281
- try {
3282
- const completedPath = completeExecPlan(storyId, root);
3283
- if (completedPath) {
3284
- if (!isJson) {
3285
- ok(`Exec-plan moved to completed: ${completedPath}`);
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
- ok(`Story ${storyId}: verified \u2014 proof at verification/${storyId}-proof.md`);
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 {
@@ -4860,8 +4959,10 @@ function registerOnboardCommand(program) {
4860
4959
  process.exitCode = 1;
4861
4960
  return;
4862
4961
  }
4863
- for (const w of preconditions.warnings) {
4864
- warn(w);
4962
+ if (!isJson) {
4963
+ for (const w of preconditions.warnings) {
4964
+ warn(w);
4965
+ }
4865
4966
  }
4866
4967
  const result = runScan(minModuleSize);
4867
4968
  saveScanCache({
@@ -5122,9 +5223,17 @@ function runAudit() {
5122
5223
  }
5123
5224
  function printScanOutput(result) {
5124
5225
  info(`Scan: ${result.totalSourceFiles} source files across ${result.modules.length} modules`);
5226
+ for (const mod of result.modules) {
5227
+ info(` ${mod.path}: ${mod.sourceFiles} source, ${mod.testFiles} test`);
5228
+ }
5125
5229
  }
5126
5230
  function printCoverageOutput2(result) {
5127
5231
  info(`Coverage: ${result.overall}% overall (${result.uncoveredFiles} files uncovered)`);
5232
+ for (const mod of result.modules) {
5233
+ if (mod.uncoveredFileCount > 0) {
5234
+ info(` ${mod.path}: ${mod.coveragePercent}% (${mod.uncoveredFileCount} uncovered)`);
5235
+ }
5236
+ }
5128
5237
  }
5129
5238
  function printAuditOutput(result) {
5130
5239
  info(`Docs: ${result.summary}`);
@@ -6075,8 +6184,453 @@ function registerQueryCommand(program) {
6075
6184
  });
6076
6185
  }
6077
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
+
6078
6632
  // src/index.ts
6079
- var VERSION = true ? "0.6.1" : "0.0.0-dev";
6633
+ var VERSION = true ? "0.8.0" : "0.0.0-dev";
6080
6634
  function createProgram() {
6081
6635
  const program = new Command();
6082
6636
  program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
@@ -6093,6 +6647,8 @@ function createProgram() {
6093
6647
  registerDocHealthCommand(program);
6094
6648
  registerStackCommand(program);
6095
6649
  registerQueryCommand(program);
6650
+ registerRetroImportCommand(program);
6651
+ registerGithubImportCommand(program);
6096
6652
  return program;
6097
6653
  }
6098
6654
  if (!process.env["VITEST"]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeharness",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "CLI for codeharness — makes autonomous coding agents produce software that actually works",
6
6
  "bin": {
@@ -12,6 +12,11 @@
12
12
  "ralph/**/*.sh",
13
13
  "ralph/AGENTS.md"
14
14
  ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/iVintik/codeharness.git"
18
+ },
19
+ "license": "MIT",
15
20
  "engines": {
16
21
  "node": ">=18"
17
22
  },