forge-workflow 0.0.4 → 0.0.6

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 (248) hide show
  1. package/.claude/commands/dev.md +345 -340
  2. package/.claude/commands/plan.md +566 -521
  3. package/.claude/commands/premerge.md +186 -176
  4. package/.claude/commands/research.md +42 -42
  5. package/.claude/commands/review.md +448 -442
  6. package/.claude/commands/rollback.md +721 -721
  7. package/.claude/commands/ship.md +212 -164
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +90 -48
  10. package/.claude/commands/validate.md +288 -282
  11. package/.claude/commands/verify.md +269 -221
  12. package/.claude/rules/greptile-review-process.md +285 -285
  13. package/.claude/rules/workflow.md +121 -105
  14. package/.claude/scripts/greptile-resolve.sh +558 -526
  15. package/.claude/scripts/load-env.sh +32 -32
  16. package/.cline/workflows/dev.md +342 -337
  17. package/.cline/workflows/plan.md +563 -518
  18. package/.cline/workflows/premerge.md +183 -173
  19. package/.cline/workflows/research.md +39 -39
  20. package/.cline/workflows/review.md +445 -439
  21. package/.cline/workflows/rollback.md +718 -718
  22. package/.cline/workflows/ship.md +209 -161
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +87 -45
  25. package/.cline/workflows/validate.md +285 -279
  26. package/.cline/workflows/verify.md +266 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +345 -340
  29. package/.codex/skills/plan/SKILL.md +566 -521
  30. package/.codex/skills/premerge/SKILL.md +186 -176
  31. package/.codex/skills/research/SKILL.md +42 -42
  32. package/.codex/skills/review/SKILL.md +448 -442
  33. package/.codex/skills/rollback/SKILL.md +721 -721
  34. package/.codex/skills/ship/SKILL.md +212 -164
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +90 -48
  37. package/.codex/skills/validate/SKILL.md +288 -282
  38. package/.codex/skills/verify/SKILL.md +269 -221
  39. package/.cursor/commands/dev.md +342 -337
  40. package/.cursor/commands/plan.md +563 -518
  41. package/.cursor/commands/premerge.md +183 -173
  42. package/.cursor/commands/research.md +39 -39
  43. package/.cursor/commands/review.md +445 -439
  44. package/.cursor/commands/rollback.md +718 -718
  45. package/.cursor/commands/ship.md +209 -161
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +87 -45
  48. package/.cursor/commands/validate.md +285 -279
  49. package/.cursor/commands/verify.md +266 -218
  50. package/.cursor/rules/permissions-guidance.mdc +37 -37
  51. package/.forge/hooks/check-tdd.js +240 -240
  52. package/.github/PLUGIN_TEMPLATE.json +32 -32
  53. package/.github/prompts/dev.prompt.md +347 -342
  54. package/.github/prompts/plan.prompt.md +568 -523
  55. package/.github/prompts/premerge.prompt.md +188 -178
  56. package/.github/prompts/research.prompt.md +44 -44
  57. package/.github/prompts/review.prompt.md +450 -444
  58. package/.github/prompts/rollback.prompt.md +723 -723
  59. package/.github/prompts/ship.prompt.md +214 -166
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +92 -50
  62. package/.github/prompts/validate.prompt.md +290 -284
  63. package/.github/prompts/verify.prompt.md +271 -223
  64. package/.github/workflows/beads-to-github.yml +56 -0
  65. package/.github/workflows/github-to-beads.yml +97 -0
  66. package/.kilocode/workflows/dev.md +346 -341
  67. package/.kilocode/workflows/plan.md +567 -522
  68. package/.kilocode/workflows/premerge.md +187 -177
  69. package/.kilocode/workflows/research.md +43 -43
  70. package/.kilocode/workflows/review.md +449 -443
  71. package/.kilocode/workflows/rollback.md +722 -722
  72. package/.kilocode/workflows/ship.md +213 -165
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +91 -49
  75. package/.kilocode/workflows/validate.md +289 -283
  76. package/.kilocode/workflows/verify.md +270 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +345 -340
  79. package/.opencode/commands/plan.md +566 -521
  80. package/.opencode/commands/premerge.md +186 -176
  81. package/.opencode/commands/research.md +42 -42
  82. package/.opencode/commands/review.md +448 -442
  83. package/.opencode/commands/rollback.md +721 -721
  84. package/.opencode/commands/ship.md +212 -164
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +90 -48
  87. package/.opencode/commands/validate.md +288 -282
  88. package/.opencode/commands/verify.md +269 -221
  89. package/.roo/commands/dev.md +346 -341
  90. package/.roo/commands/plan.md +567 -522
  91. package/.roo/commands/premerge.md +187 -177
  92. package/.roo/commands/research.md +43 -43
  93. package/.roo/commands/review.md +449 -443
  94. package/.roo/commands/rollback.md +722 -722
  95. package/.roo/commands/ship.md +213 -165
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +91 -49
  98. package/.roo/commands/validate.md +289 -283
  99. package/.roo/commands/verify.md +270 -222
  100. package/AGENTS.md +272 -175
  101. package/CLAUDE.md +110 -100
  102. package/README.md +429 -416
  103. package/bin/forge-cmd.js +317 -313
  104. package/bin/forge-preflight.js +322 -309
  105. package/bin/forge.js +4765 -4303
  106. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  107. package/docs/BEADS_GITHUB_SYNC.md +251 -251
  108. package/docs/ENHANCED_ONBOARDING.md +612 -602
  109. package/docs/EXAMPLES.md +482 -482
  110. package/docs/GREPTILE_SETUP.md +400 -400
  111. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  112. package/docs/ROADMAP.md +359 -359
  113. package/docs/SETUP.md +663 -631
  114. package/docs/TOOLCHAIN.md +653 -630
  115. package/docs/VALIDATION.md +363 -363
  116. package/install.sh +40 -1056
  117. package/lefthook.yml +50 -39
  118. package/lib/agents/README.md +198 -198
  119. package/lib/agents/claude.plugin.json +28 -28
  120. package/lib/agents/cline.plugin.json +22 -22
  121. package/lib/agents/codex.plugin.json +19 -19
  122. package/lib/agents/copilot.plugin.json +24 -24
  123. package/lib/agents/cursor.plugin.json +25 -25
  124. package/lib/agents/kilocode.plugin.json +22 -22
  125. package/lib/agents/opencode.plugin.json +20 -20
  126. package/lib/agents/roo.plugin.json +23 -23
  127. package/lib/agents-config.js +2112 -2112
  128. package/lib/beads-health-check.js +143 -0
  129. package/lib/beads-setup.js +341 -0
  130. package/lib/beads-sync-scaffold.js +260 -0
  131. package/lib/commands/_registry.js +134 -0
  132. package/lib/commands/clean.js +181 -0
  133. package/lib/commands/dev.js +571 -513
  134. package/lib/commands/plan.js +692 -692
  135. package/lib/commands/push.js +196 -0
  136. package/lib/commands/recommend.js +119 -119
  137. package/lib/commands/ship.js +377 -377
  138. package/lib/commands/status.js +378 -378
  139. package/lib/commands/sync.js +55 -0
  140. package/lib/commands/team.js +37 -0
  141. package/lib/commands/test.js +207 -0
  142. package/lib/commands/validate.js +602 -602
  143. package/lib/commands/worktree.js +310 -0
  144. package/lib/context-merge.js +359 -359
  145. package/lib/dep-guard/analyzer.js +294 -294
  146. package/lib/dep-guard/behavior-detector.js +98 -98
  147. package/lib/dep-guard/contract-detector.js +162 -162
  148. package/lib/dep-guard/import-detector.js +498 -498
  149. package/lib/dep-guard/path-utils.js +13 -13
  150. package/lib/dep-guard/rubric.js +120 -120
  151. package/lib/dep-guard/task-parser.js +318 -318
  152. package/lib/detect-agent.js +191 -191
  153. package/lib/detect-worktree.js +47 -47
  154. package/lib/docs-command.js +51 -0
  155. package/lib/docs-copy.js +50 -0
  156. package/lib/file-hash.js +26 -26
  157. package/lib/freshness-token.js +148 -0
  158. package/lib/greptile-match.js +80 -0
  159. package/lib/husky-migration.js +450 -0
  160. package/lib/lefthook-check.js +65 -0
  161. package/lib/pat-setup.js +207 -0
  162. package/lib/plugin-catalog.js +350 -350
  163. package/lib/plugin-manager.js +166 -166
  164. package/lib/plugin-recommender.js +141 -141
  165. package/lib/project-discovery.js +491 -491
  166. package/lib/reset.js +309 -0
  167. package/lib/setup-action-log.js +139 -139
  168. package/lib/setup-summary-renderer.js +106 -106
  169. package/lib/setup-utils.js +96 -0
  170. package/lib/setup.js +192 -192
  171. package/lib/smart-merge.js +64 -0
  172. package/lib/symlink-utils.js +81 -0
  173. package/lib/task-ownership.js +117 -0
  174. package/lib/workflow-profiles.js +197 -197
  175. package/package.json +131 -128
  176. package/scripts/beads-context.sh +426 -0
  177. package/scripts/beads-context.test.js +567 -0
  178. package/scripts/behavioral-judge.sh +378 -0
  179. package/scripts/benchmark.js +85 -0
  180. package/scripts/branch-protection.js +183 -0
  181. package/scripts/check-agents.js +172 -0
  182. package/scripts/check-forge-token.js +98 -0
  183. package/scripts/commitlint.js +42 -0
  184. package/scripts/conflict-detect.sh +323 -0
  185. package/scripts/dep-guard-analyze.js +71 -0
  186. package/scripts/dep-guard.sh +789 -0
  187. package/scripts/eval_win.py +249 -0
  188. package/scripts/file-index.sh +493 -0
  189. package/scripts/forge-team/index.sh +86 -0
  190. package/scripts/forge-team/lib/agent-prompt.sh +52 -0
  191. package/scripts/forge-team/lib/claim.sh +256 -0
  192. package/scripts/forge-team/lib/dashboard.sh +341 -0
  193. package/scripts/forge-team/lib/epic.sh +332 -0
  194. package/scripts/forge-team/lib/hooks.sh +253 -0
  195. package/scripts/forge-team/lib/identity.sh +235 -0
  196. package/scripts/forge-team/lib/sync-github.sh +317 -0
  197. package/scripts/forge-team/lib/verify.sh +284 -0
  198. package/scripts/forge-team/lib/workload.sh +296 -0
  199. package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
  200. package/scripts/forge-team/tests/claim.test.sh +179 -0
  201. package/scripts/forge-team/tests/dashboard.test.sh +170 -0
  202. package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
  203. package/scripts/forge-team/tests/epic.test.sh +176 -0
  204. package/scripts/forge-team/tests/hooks.test.sh +239 -0
  205. package/scripts/forge-team/tests/identity.test.sh +176 -0
  206. package/scripts/forge-team/tests/integration.test.sh +371 -0
  207. package/scripts/forge-team/tests/sync-github.test.sh +209 -0
  208. package/scripts/forge-team/tests/verify.test.sh +314 -0
  209. package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
  210. package/scripts/forge-team/tests/workload.test.sh +209 -0
  211. package/scripts/github-beads-sync/comment.mjs +64 -0
  212. package/scripts/github-beads-sync/config.mjs +148 -0
  213. package/scripts/github-beads-sync/github-api.mjs +131 -0
  214. package/scripts/github-beads-sync/index.mjs +332 -0
  215. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  216. package/scripts/github-beads-sync/mapping.mjs +78 -0
  217. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  218. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  219. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  220. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  221. package/scripts/github-beads-sync.config.json +26 -0
  222. package/scripts/improve-command.js +375 -0
  223. package/scripts/lib/eval-runner.js +268 -0
  224. package/scripts/lib/eval-schema.js +135 -0
  225. package/scripts/lib/eval-storage.js +78 -0
  226. package/scripts/lib/grading.js +203 -0
  227. package/scripts/lib/jsonl-lock.sh +48 -0
  228. package/scripts/lib/sanitize.sh +116 -0
  229. package/scripts/lib/transcript-parser.js +63 -0
  230. package/scripts/lint.js +47 -0
  231. package/scripts/migrate-to-bun-test.js +412 -0
  232. package/scripts/pr-coordinator.sh +706 -0
  233. package/scripts/run-command-eval.js +236 -0
  234. package/scripts/smart-status.sh +809 -0
  235. package/scripts/sync-commands.js +571 -0
  236. package/scripts/sync-utils.sh +455 -0
  237. package/scripts/test-dashboard.js +123 -0
  238. package/scripts/test.js +46 -0
  239. package/scripts/validate.sh +94 -0
  240. package/skills/parallel-deep-research/SKILL.md +108 -108
  241. package/skills/parallel-deep-research/evals/README.md +27 -27
  242. package/skills/parallel-deep-research/evals/evals.json +62 -62
  243. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  244. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  245. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  246. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  247. package/.cursor/hooks/state/continual-learning-index.json +0 -19
  248. package/.cursor/hooks/state/continual-learning.json +0 -8
@@ -0,0 +1,78 @@
1
+ /**
2
+ * CRUD module for .github/beads-mapping.json
3
+ *
4
+ * Maps GitHub issue numbers (string keys) to Beads issue IDs.
5
+ * Format: { "42": "forge-abc", "7": "forge-xyz" }
6
+ *
7
+ * @module mapping
8
+ */
9
+
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+
13
+ /**
14
+ * Reads the mapping file and returns parsed JSON.
15
+ * Returns {} if the file does not exist.
16
+ * Throws with a helpful message if JSON is invalid.
17
+ *
18
+ * @param {string} mappingPath - Absolute path to the mapping JSON file
19
+ * @returns {Record<string, string>} The mapping object
20
+ */
21
+ export function readMapping(mappingPath) {
22
+ if (!fs.existsSync(mappingPath)) {
23
+ return {};
24
+ }
25
+ const raw = fs.readFileSync(mappingPath, 'utf8');
26
+ try {
27
+ return JSON.parse(raw);
28
+ } catch (err) {
29
+ throw new Error(`Failed to parse mapping file at ${mappingPath}: ${err.message}`, { cause: err });
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Writes the mapping object to disk as JSON with 2-space indent.
35
+ * Atomic: writes to a temp file then renames.
36
+ * Creates parent directories if they do not exist.
37
+ *
38
+ * @param {string} mappingPath - Absolute path to the mapping JSON file
39
+ * @param {Record<string, string>} data - The mapping object to write
40
+ */
41
+ export function writeMapping(mappingPath, data) {
42
+ const dir = path.dirname(mappingPath);
43
+ fs.mkdirSync(dir, { recursive: true });
44
+
45
+ const tmpPath = mappingPath + '.tmp';
46
+ const content = JSON.stringify(data, null, 2) + '\n';
47
+ fs.writeFileSync(tmpPath, content, 'utf8');
48
+ fs.renameSync(tmpPath, mappingPath);
49
+ }
50
+
51
+ /**
52
+ * Returns the Beads ID for a given GitHub issue number, or null if not found.
53
+ * Coerces numeric issueNumber to string for lookup.
54
+ *
55
+ * @param {string} mappingPath - Absolute path to the mapping JSON file
56
+ * @param {string|number} issueNumber - The GitHub issue number
57
+ * @returns {string|null} The Beads ID or null
58
+ */
59
+ export function getBeadsId(mappingPath, issueNumber) {
60
+ const data = readMapping(mappingPath);
61
+ const key = String(issueNumber);
62
+ return data[key] ?? null;
63
+ }
64
+
65
+ /**
66
+ * Adds or updates a mapping entry. Reads existing data, merges, and writes back.
67
+ * Preserves all existing entries.
68
+ *
69
+ * @param {string} mappingPath - Absolute path to the mapping JSON file
70
+ * @param {string|number} issueNumber - The GitHub issue number
71
+ * @param {string} beadsId - The Beads issue ID
72
+ */
73
+ export function setBeadsId(mappingPath, issueNumber, beadsId) {
74
+ const data = readMapping(mappingPath);
75
+ const key = String(issueNumber);
76
+ data[key] = beadsId;
77
+ writeMapping(mappingPath, data);
78
+ }
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for Beads → GitHub reverse sync.
4
+ * Usage: node reverse-sync-cli.mjs <old-jsonl-path> <new-jsonl-path>
5
+ *
6
+ * @module scripts/github-beads-sync/reverse-sync-cli
7
+ */
8
+
9
+ import { readFileSync } from 'node:fs';
10
+ import { handleBeadsClosed } from './reverse-sync.mjs';
11
+
12
+ const oldPath = process.argv[2];
13
+ const newPath = process.argv[3];
14
+
15
+ if (!oldPath || !newPath) {
16
+ console.error('Usage: node reverse-sync-cli.mjs <old-jsonl-path> <new-jsonl-path>');
17
+ process.exit(1);
18
+ }
19
+
20
+ const oldContent = readFileSync(oldPath, 'utf-8');
21
+ const newContent = readFileSync(newPath, 'utf-8');
22
+
23
+ const result = handleBeadsClosed(oldContent, newContent);
24
+ console.log(JSON.stringify(result, null, 2));
25
+
26
+ if (result.errors.length > 0) {
27
+ console.error(`${result.errors.length} issue(s) failed to close on GitHub`);
28
+ process.exit(1);
29
+ }
30
+
31
+ console.log(`Closed ${result.closed.length} GitHub issue(s), skipped ${result.skipped.length}`);
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Reverse sync: Beads → GitHub.
3
+ * When a Beads issue is closed (via git push updating .beads/),
4
+ * close the linked GitHub issue.
5
+ *
6
+ * @module scripts/github-beads-sync/reverse-sync
7
+ */
8
+
9
+ import { execFileSync } from 'node:child_process';
10
+
11
+ /**
12
+ * Parse JSONL lines into an array of objects, skipping empty/malformed lines.
13
+ * @param {string[]} lines - Array of JSONL strings
14
+ * @returns {Array<{id: string, status: string, description?: string}>}
15
+ */
16
+ function parseJsonlLines(lines) {
17
+ const results = [];
18
+ for (const line of lines) {
19
+ const trimmed = line.trim();
20
+ if (!trimmed) continue;
21
+ try {
22
+ const obj = JSON.parse(trimmed);
23
+ if (obj && obj.id) results.push(obj);
24
+ } catch (_err) {
25
+ // Skip malformed lines
26
+ }
27
+ }
28
+ return results;
29
+ }
30
+
31
+ /**
32
+ * Detect Beads issues that transitioned to "closed" status.
33
+ * Only detects transitions: issue must exist in oldLines as non-closed,
34
+ * and appear in newLines as "closed".
35
+ *
36
+ * @param {string[]} oldLines - JSONL lines from previous .beads/issues.jsonl
37
+ * @param {string[]} newLines - JSONL lines from current .beads/issues.jsonl
38
+ * @returns {Array<{id: string, description: string}>} Issues that transitioned to closed
39
+ */
40
+ export function detectClosedIssues(oldLines, newLines) {
41
+ const oldMap = new Map();
42
+ for (const issue of parseJsonlLines(oldLines)) {
43
+ oldMap.set(issue.id, issue.status);
44
+ }
45
+
46
+ const closed = [];
47
+ for (const issue of parseJsonlLines(newLines)) {
48
+ if (issue.status !== 'closed') continue;
49
+ const oldStatus = oldMap.get(issue.id);
50
+ // Must have existed before and not been closed already
51
+ if (oldStatus == null || oldStatus === 'closed') continue;
52
+ closed.push({ id: issue.id, description: issue.description || '' });
53
+ }
54
+ return closed;
55
+ }
56
+
57
+ /**
58
+ * Extract GitHub owner, repo, and issue number from a description string
59
+ * containing a GitHub issue URL.
60
+ *
61
+ * @param {string|null|undefined} description - Text that may contain a GitHub issue URL
62
+ * @returns {{owner: string, repo: string, issueNumber: number}|null}
63
+ */
64
+ export function extractGitHubUrl(description) {
65
+ if (!description) return null;
66
+ const match = description.match(
67
+ /https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/,
68
+ );
69
+ if (!match) return null;
70
+ return {
71
+ owner: match[1],
72
+ repo: match[2],
73
+ issueNumber: parseInt(match[3], 10),
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Close a GitHub issue using the `gh api` CLI.
79
+ * Uses execFileSync with array args (no shell).
80
+ *
81
+ * @param {string} owner - Repository owner
82
+ * @param {string} repo - Repository name
83
+ * @param {number|string} issueNumber - Issue number
84
+ */
85
+ export function closeGitHubIssue(owner, repo, issueNumber) {
86
+ execFileSync('gh', [
87
+ 'api',
88
+ `repos/${owner}/${repo}/issues/${issueNumber}`,
89
+ '-X', 'PATCH',
90
+ '-f', 'state=closed',
91
+ ], { encoding: 'utf-8' });
92
+ }
93
+
94
+ /**
95
+ * Handle Beads-to-GitHub reverse sync.
96
+ * Detects issues that transitioned to closed in .beads/issues.jsonl
97
+ * and closes the linked GitHub issues.
98
+ *
99
+ * @param {string} oldContent - Previous .beads/issues.jsonl content
100
+ * @param {string} newContent - Current .beads/issues.jsonl content
101
+ * @param {object} [deps] - Dependency injection
102
+ * @param {Function} [deps.closeGitHubIssue] - Override for testing
103
+ * @returns {{closed: Array, skipped: Array, errors: Array}}
104
+ */
105
+ export function handleBeadsClosed(oldContent, newContent, deps = {}) {
106
+ const closeFn = deps.closeGitHubIssue ?? closeGitHubIssue;
107
+
108
+ const oldLines = oldContent.split('\n');
109
+ const newLines = newContent.split('\n');
110
+
111
+ const transitions = detectClosedIssues(oldLines, newLines);
112
+
113
+ const closed = [];
114
+ const skipped = [];
115
+ const errors = [];
116
+
117
+ for (const issue of transitions) {
118
+ const parsed = extractGitHubUrl(issue.description);
119
+ if (!parsed) {
120
+ skipped.push({ beadsId: issue.id, reason: 'no GitHub URL' });
121
+ continue;
122
+ }
123
+
124
+ try {
125
+ closeFn(parsed.owner, parsed.repo, parsed.issueNumber);
126
+ closed.push({
127
+ beadsId: issue.id,
128
+ owner: parsed.owner,
129
+ repo: parsed.repo,
130
+ issueNumber: parsed.issueNumber,
131
+ });
132
+ } catch (err) {
133
+ errors.push({ beadsId: issue.id, error: err.message });
134
+ }
135
+ }
136
+
137
+ return { closed, skipped, errors };
138
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * @module run-bd
3
+ * @description Wrapper around the `bd` (Beads) CLI for GitHub-Beads sync.
4
+ *
5
+ * Exports pure arg-building and output-parsing functions (unit-testable),
6
+ * plus thin exec wrappers that shell out to the real `bd` binary.
7
+ */
8
+
9
+ import { execFileSync } from 'node:child_process';
10
+
11
+ const EXEC_OPTS = { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Arg builders (pure)
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /**
18
+ * Build args array for `bd create`.
19
+ * Omits flags whose values are null or undefined.
20
+ * @param {object} opts
21
+ * @param {string} opts.title
22
+ * @param {string} [opts.type]
23
+ * @param {number|string} [opts.priority]
24
+ * @param {string} [opts.assignee]
25
+ * @param {string} [opts.description]
26
+ * @param {string} [opts.externalRef]
27
+ * @returns {string[]}
28
+ */
29
+ export function buildCreateArgs({ title, type, priority, assignee, description, externalRef } = {}) {
30
+ const args = ['create', '--title', title];
31
+
32
+ if (type != null) args.push('--type', type);
33
+ if (priority != null) args.push('--priority', String(priority));
34
+ if (assignee != null) args.push('--assignee', assignee);
35
+ if (description != null) args.push('--description', description);
36
+ if (externalRef != null) args.push('--external-ref', externalRef);
37
+
38
+ return args;
39
+ }
40
+
41
+ /**
42
+ * Build args array for `bd close`.
43
+ * @param {string} beadsId
44
+ * @param {string} [reason]
45
+ * @returns {string[]}
46
+ */
47
+ export function buildCloseArgs(beadsId, reason) {
48
+ const args = ['close', beadsId];
49
+ if (reason != null) args.push('--reason', reason);
50
+ return args;
51
+ }
52
+
53
+ /**
54
+ * Build args array for `bd show`.
55
+ * @param {string} beadsId
56
+ * @returns {string[]}
57
+ */
58
+ export function buildShowArgs(beadsId) {
59
+ return ['show', beadsId, '--json'];
60
+ }
61
+
62
+ /**
63
+ * Build args array for `bd search`.
64
+ * @param {string} query
65
+ * @returns {string[]}
66
+ */
67
+ export function buildSearchArgs(query) {
68
+ return ['search', query];
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Output parsers (pure)
73
+ // ---------------------------------------------------------------------------
74
+
75
+ /**
76
+ * Extract beads ID from `bd create` stdout.
77
+ * Expects pattern: "Created issue: forge-xxxx"
78
+ * @param {string} stdout
79
+ * @returns {string|null}
80
+ */
81
+ export function parseCreateOutput(stdout) {
82
+ const match = stdout.match(/Created issue:\s+(forge-[\w-]+)/);
83
+ if (match) return match[1];
84
+ // Fallback: loose match for any forge-prefixed ID anywhere in output
85
+ const fallback = stdout.match(/(forge-[a-z0-9]+)/i);
86
+ return fallback ? fallback[1] : null;
87
+ }
88
+
89
+ /**
90
+ * Extract status from `bd show --json` stdout.
91
+ * Parses JSON output for reliable status extraction.
92
+ * @param {string} stdout - JSON output from `bd show --json`
93
+ * @returns {string|null} Lowercase status or null
94
+ */
95
+ export function parseShowOutput(stdout) {
96
+ try {
97
+ const data = JSON.parse(stdout);
98
+ const issue = Array.isArray(data) ? data[0] : data;
99
+ return issue?.status ? issue.status.toLowerCase() : null;
100
+ } catch (_err) {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Exec wrappers (side-effectful — not unit-tested)
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /**
110
+ * Run `bd create` and return the parsed beads ID.
111
+ * @param {object} opts - Same as buildCreateArgs
112
+ * @returns {string|null} The created beads ID, or null if parsing failed
113
+ */
114
+ export function bdCreate(opts) {
115
+ try {
116
+ const stdout = execFileSync('bd', buildCreateArgs(opts), EXEC_OPTS);
117
+ const id = parseCreateOutput(stdout);
118
+ if (!id) {
119
+ console.error('bd create succeeded but ID not parsed. stdout:', stdout);
120
+ }
121
+ return id;
122
+ } catch (err) {
123
+ console.error('bd create failed:', err.stderr?.toString() || err.message);
124
+ return null;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Run `bd close`.
130
+ * @param {string} beadsId
131
+ * @param {string} [reason]
132
+ */
133
+ export function bdClose(beadsId, reason) {
134
+ execFileSync('bd', buildCloseArgs(beadsId, reason), EXEC_OPTS);
135
+ }
136
+
137
+ /**
138
+ * Run `bd show` and return the parsed status.
139
+ * Returns null if bd exits non-zero (e.g., deleted issue, corrupted .beads/).
140
+ * @param {string} beadsId
141
+ * @returns {string|null} Lowercase status or null
142
+ */
143
+ export function bdShow(beadsId) {
144
+ try {
145
+ const stdout = execFileSync('bd', buildShowArgs(beadsId), EXEC_OPTS);
146
+ return parseShowOutput(stdout);
147
+ } catch (_err) {
148
+ return null;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Run `bd search` and return raw stdout.
154
+ * @param {string} query
155
+ * @returns {string}
156
+ */
157
+ export function bdSearch(query) {
158
+ return execFileSync('bd', buildSearchArgs(query), EXEC_OPTS);
159
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Input sanitizer for GitHub issue data before passing to bd CLI.
3
+ * Strips shell metacharacters, GitHub Actions interpolation, and control chars.
4
+ * @module sanitize
5
+ */
6
+
7
+ /** Shell metacharacters to strip from titles and bodies */
8
+ const SHELL_META_RE = /[;|&$`()<>\r\n]/g;
9
+
10
+ /** GitHub Actions interpolation pattern: ${{ ... }} */
11
+ const INTERPOLATION_RE = /\$\{\{[^}]*\}\}/g;
12
+
13
+ /** Control characters (C0 range, excluding normal whitespace) */
14
+ // eslint-disable-next-line no-control-regex
15
+ const CONTROL_CHARS_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
16
+
17
+ /** Label: only allow alphanumeric, dot, underscore, hyphen */
18
+ const LABEL_INVALID_RE = /[^a-zA-Z0-9._-]/g;
19
+
20
+ /**
21
+ * Sanitize a general text field (shared logic for title/body).
22
+ * @param {string} input - Raw input string
23
+ * @param {number} maxLen - Maximum allowed length
24
+ * @param {string} fieldName - Name for warning messages
25
+ * @returns {{ sanitized: string, warnings: string[] }}
26
+ */
27
+ function sanitizeText(input, maxLen, fieldName) {
28
+ const warnings = [];
29
+ let text = String(input ?? '');
30
+
31
+ // Strip GitHub Actions interpolation patterns
32
+ // Use local regex to avoid stateful /g lastIndex bug with .test()
33
+ const interpolationReLocal = /\$\{\{[^}]*\}\}/g;
34
+ if (interpolationReLocal.test(text)) {
35
+ warnings.push(`${fieldName}: stripped GitHub Actions interpolation pattern(s)`);
36
+ }
37
+ text = text.replace(INTERPOLATION_RE, '');
38
+
39
+ // Strip shell metacharacters
40
+ const metaMatches = text.match(SHELL_META_RE);
41
+ if (metaMatches) {
42
+ const unique = [...new Set(metaMatches)];
43
+ warnings.push(`${fieldName}: stripped shell metacharacters: ${unique.join(' ')}`);
44
+ }
45
+ text = text.replace(SHELL_META_RE, '');
46
+
47
+ // Strip control characters
48
+ // eslint-disable-next-line no-control-regex
49
+ const controlCharsReLocal = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
50
+ if (controlCharsReLocal.test(text)) {
51
+ warnings.push(`${fieldName}: stripped control characters`);
52
+ }
53
+ text = text.replace(CONTROL_CHARS_RE, '');
54
+
55
+ // Trim whitespace
56
+ text = text.trim();
57
+
58
+ // Truncate
59
+ if (text.length > maxLen) {
60
+ warnings.push(`${fieldName}: truncated from ${text.length} to ${maxLen} chars`);
61
+ text = text.slice(0, maxLen);
62
+ }
63
+
64
+ // Empty check
65
+ if (text.length === 0) {
66
+ warnings.push(`${fieldName}: empty after sanitization`);
67
+ text = '(empty)';
68
+ }
69
+
70
+ return { sanitized: text, warnings };
71
+ }
72
+
73
+ /**
74
+ * Sanitize an issue title for bd CLI args.
75
+ * @param {string} title - Raw GitHub issue title
76
+ * @returns {{ sanitized: string, warnings: string[] }}
77
+ */
78
+ export function sanitizeTitle(title) {
79
+ return sanitizeText(title, 256, 'title');
80
+ }
81
+
82
+ /**
83
+ * Sanitize an issue body for the description field.
84
+ * @param {string} body - Raw GitHub issue body
85
+ * @returns {{ sanitized: string, warnings: string[] }}
86
+ */
87
+ export function sanitizeBody(body) {
88
+ return sanitizeText(body, 1024, 'body');
89
+ }
90
+
91
+ /**
92
+ * Sanitize a single label string.
93
+ * Only allows [a-zA-Z0-9._-], max 64 chars.
94
+ * @param {string} label - Raw GitHub label name
95
+ * @returns {{ sanitized: string, warnings: string[] }}
96
+ */
97
+ export function sanitizeLabel(label) {
98
+ const warnings = [];
99
+ let text = String(label ?? '').trim();
100
+
101
+ // Strip invalid characters
102
+ const invalidMatches = text.match(LABEL_INVALID_RE);
103
+ if (invalidMatches) {
104
+ warnings.push(`label: stripped invalid characters: ${[...new Set(invalidMatches)].join(' ')}`);
105
+ text = text.replace(LABEL_INVALID_RE, '');
106
+ }
107
+
108
+ // Truncate
109
+ if (text.length > 64) {
110
+ warnings.push(`label: truncated from ${text.length} to 64 chars`);
111
+ text = text.slice(0, 64);
112
+ }
113
+
114
+ // Empty check
115
+ if (text.length === 0) {
116
+ warnings.push('label: empty after sanitization');
117
+ text = '(empty)';
118
+ }
119
+
120
+ return { sanitized: text, warnings };
121
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "labelToType": {
3
+ "bug": "bug",
4
+ "enhancement": "feature",
5
+ "documentation": "task",
6
+ "question": "task"
7
+ },
8
+ "labelToPriority": {
9
+ "P0": 0,
10
+ "critical": 0,
11
+ "P1": 1,
12
+ "high": 1,
13
+ "P2": 2,
14
+ "medium": 2,
15
+ "P3": 3,
16
+ "low": 3,
17
+ "P4": 4,
18
+ "backlog": 4
19
+ },
20
+ "defaultType": "task",
21
+ "defaultPriority": 2,
22
+ "mapAssignee": true,
23
+ "publicRepoGate": "none",
24
+ "gateLabelName": "beads-track",
25
+ "gateAssociations": ["MEMBER", "COLLABORATOR", "OWNER"]
26
+ }