forge-workflow 0.0.3 → 0.0.5

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 (209) hide show
  1. package/.claude/commands/dev.md +340 -314
  2. package/.claude/commands/plan.md +521 -478
  3. package/.claude/commands/premerge.md +176 -179
  4. package/.claude/commands/research.md +42 -42
  5. package/.claude/commands/review.md +442 -442
  6. package/.claude/commands/rollback.md +721 -721
  7. package/.claude/commands/ship.md +164 -134
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +48 -77
  10. package/.claude/commands/validate.md +282 -237
  11. package/.claude/commands/verify.md +221 -221
  12. package/.claude/rules/greptile-review-process.md +285 -285
  13. package/.claude/rules/workflow.md +105 -105
  14. package/.claude/scripts/greptile-resolve.sh +526 -526
  15. package/.claude/scripts/load-env.sh +32 -32
  16. package/.cline/workflows/dev.md +337 -311
  17. package/.cline/workflows/plan.md +518 -475
  18. package/.cline/workflows/premerge.md +173 -176
  19. package/.cline/workflows/research.md +39 -39
  20. package/.cline/workflows/review.md +439 -439
  21. package/.cline/workflows/rollback.md +718 -718
  22. package/.cline/workflows/ship.md +161 -131
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +45 -74
  25. package/.cline/workflows/validate.md +279 -234
  26. package/.cline/workflows/verify.md +218 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +340 -314
  29. package/.codex/skills/plan/SKILL.md +521 -478
  30. package/.codex/skills/premerge/SKILL.md +176 -179
  31. package/.codex/skills/research/SKILL.md +42 -42
  32. package/.codex/skills/review/SKILL.md +442 -442
  33. package/.codex/skills/rollback/SKILL.md +721 -721
  34. package/.codex/skills/ship/SKILL.md +164 -134
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +48 -77
  37. package/.codex/skills/validate/SKILL.md +282 -237
  38. package/.codex/skills/verify/SKILL.md +221 -221
  39. package/.cursor/commands/dev.md +337 -311
  40. package/.cursor/commands/plan.md +518 -475
  41. package/.cursor/commands/premerge.md +173 -176
  42. package/.cursor/commands/research.md +39 -39
  43. package/.cursor/commands/review.md +439 -439
  44. package/.cursor/commands/rollback.md +718 -718
  45. package/.cursor/commands/ship.md +161 -131
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +45 -74
  48. package/.cursor/commands/validate.md +279 -234
  49. package/.cursor/commands/verify.md +218 -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 +342 -316
  54. package/.github/prompts/plan.prompt.md +523 -480
  55. package/.github/prompts/premerge.prompt.md +178 -181
  56. package/.github/prompts/research.prompt.md +44 -44
  57. package/.github/prompts/review.prompt.md +444 -444
  58. package/.github/prompts/rollback.prompt.md +723 -723
  59. package/.github/prompts/ship.prompt.md +166 -136
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +50 -79
  62. package/.github/prompts/validate.prompt.md +284 -239
  63. package/.github/prompts/verify.prompt.md +223 -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 +341 -315
  67. package/.kilocode/workflows/plan.md +522 -479
  68. package/.kilocode/workflows/premerge.md +177 -180
  69. package/.kilocode/workflows/research.md +43 -43
  70. package/.kilocode/workflows/review.md +443 -443
  71. package/.kilocode/workflows/rollback.md +722 -722
  72. package/.kilocode/workflows/ship.md +165 -135
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +49 -78
  75. package/.kilocode/workflows/validate.md +283 -238
  76. package/.kilocode/workflows/verify.md +222 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +340 -314
  79. package/.opencode/commands/plan.md +521 -478
  80. package/.opencode/commands/premerge.md +176 -179
  81. package/.opencode/commands/research.md +42 -42
  82. package/.opencode/commands/review.md +442 -442
  83. package/.opencode/commands/rollback.md +721 -721
  84. package/.opencode/commands/ship.md +164 -134
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +48 -77
  87. package/.opencode/commands/validate.md +282 -237
  88. package/.opencode/commands/verify.md +221 -221
  89. package/.roo/commands/dev.md +341 -315
  90. package/.roo/commands/plan.md +522 -479
  91. package/.roo/commands/premerge.md +177 -180
  92. package/.roo/commands/research.md +43 -43
  93. package/.roo/commands/review.md +443 -443
  94. package/.roo/commands/rollback.md +722 -722
  95. package/.roo/commands/ship.md +165 -135
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +49 -78
  98. package/.roo/commands/validate.md +283 -238
  99. package/.roo/commands/verify.md +222 -222
  100. package/AGENTS.md +175 -169
  101. package/CLAUDE.md +100 -99
  102. package/LICENSE +21 -21
  103. package/README.md +429 -414
  104. package/bin/forge-cmd.js +313 -313
  105. package/bin/{forge-validate.js → forge-preflight.js} +309 -303
  106. package/bin/forge.js +4596 -4232
  107. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  108. package/docs/BEADS_GITHUB_SYNC.md +251 -0
  109. package/docs/ENHANCED_ONBOARDING.md +602 -602
  110. package/docs/EXAMPLES.md +482 -482
  111. package/docs/GREPTILE_SETUP.md +400 -400
  112. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  113. package/docs/ROADMAP.md +359 -359
  114. package/docs/SETUP.md +663 -632
  115. package/docs/TOOLCHAIN.md +630 -630
  116. package/docs/VALIDATION.md +363 -363
  117. package/install.sh +40 -1058
  118. package/lefthook.yml +39 -39
  119. package/lib/agents/README.md +198 -198
  120. package/lib/agents/claude.plugin.json +28 -28
  121. package/lib/agents/cline.plugin.json +22 -22
  122. package/lib/agents/codex.plugin.json +19 -19
  123. package/lib/agents/copilot.plugin.json +24 -24
  124. package/lib/agents/cursor.plugin.json +25 -25
  125. package/lib/agents/kilocode.plugin.json +22 -22
  126. package/lib/agents/opencode.plugin.json +20 -20
  127. package/lib/agents/roo.plugin.json +23 -23
  128. package/lib/agents-config.js +2112 -2112
  129. package/lib/beads-health-check.js +143 -0
  130. package/lib/beads-setup.js +341 -0
  131. package/lib/beads-sync-scaffold.js +260 -0
  132. package/lib/commands/dev.js +513 -513
  133. package/lib/commands/plan.js +692 -692
  134. package/lib/commands/recommend.js +119 -119
  135. package/lib/commands/ship.js +377 -377
  136. package/lib/commands/status.js +378 -378
  137. package/lib/commands/validate.js +602 -602
  138. package/lib/context-merge.js +359 -359
  139. package/lib/dep-guard/analyzer.js +294 -294
  140. package/lib/dep-guard/behavior-detector.js +98 -98
  141. package/lib/dep-guard/contract-detector.js +162 -162
  142. package/lib/dep-guard/import-detector.js +498 -498
  143. package/lib/dep-guard/path-utils.js +13 -13
  144. package/lib/dep-guard/rubric.js +120 -120
  145. package/lib/dep-guard/task-parser.js +318 -318
  146. package/lib/detect-agent.js +191 -0
  147. package/lib/detect-worktree.js +47 -0
  148. package/lib/file-hash.js +26 -0
  149. package/lib/husky-migration.js +450 -0
  150. package/lib/lefthook-check.js +65 -0
  151. package/lib/pat-setup.js +207 -0
  152. package/lib/plugin-catalog.js +350 -350
  153. package/lib/plugin-manager.js +166 -166
  154. package/lib/plugin-recommender.js +141 -141
  155. package/lib/project-discovery.js +491 -491
  156. package/lib/setup-action-log.js +139 -0
  157. package/lib/setup-summary-renderer.js +106 -0
  158. package/lib/setup-utils.js +96 -0
  159. package/lib/setup.js +192 -118
  160. package/lib/smart-merge.js +64 -0
  161. package/lib/symlink-utils.js +81 -0
  162. package/lib/workflow-profiles.js +197 -197
  163. package/package.json +131 -129
  164. package/scripts/beads-context.sh +291 -0
  165. package/scripts/beads-context.test.js +563 -0
  166. package/scripts/behavioral-judge.sh +378 -0
  167. package/scripts/benchmark.js +85 -0
  168. package/scripts/branch-protection.js +183 -0
  169. package/scripts/check-agents.js +172 -0
  170. package/scripts/commitlint.js +42 -0
  171. package/scripts/conflict-detect.sh +323 -0
  172. package/scripts/dep-guard-analyze.js +71 -0
  173. package/scripts/dep-guard.sh +811 -0
  174. package/scripts/eval_win.py +249 -0
  175. package/scripts/file-index.sh +399 -0
  176. package/scripts/github-beads-sync/comment.mjs +64 -0
  177. package/scripts/github-beads-sync/config.mjs +148 -0
  178. package/scripts/github-beads-sync/github-api.mjs +131 -0
  179. package/scripts/github-beads-sync/index.mjs +332 -0
  180. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  181. package/scripts/github-beads-sync/mapping.mjs +78 -0
  182. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  183. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  184. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  185. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  186. package/scripts/github-beads-sync.config.json +26 -0
  187. package/scripts/improve-command.js +375 -0
  188. package/scripts/lib/eval-runner.js +229 -0
  189. package/scripts/lib/eval-schema.js +135 -0
  190. package/scripts/lib/eval-storage.js +78 -0
  191. package/scripts/lib/grading.js +203 -0
  192. package/scripts/lib/transcript-parser.js +63 -0
  193. package/scripts/lint.js +47 -0
  194. package/scripts/migrate-to-bun-test.js +412 -0
  195. package/scripts/run-command-eval.js +236 -0
  196. package/scripts/smart-status.sh +782 -0
  197. package/scripts/sync-commands.js +571 -0
  198. package/scripts/sync-utils.sh +460 -0
  199. package/scripts/test-dashboard.js +123 -0
  200. package/scripts/test.js +44 -0
  201. package/scripts/validate.sh +94 -0
  202. package/skills/parallel-deep-research/SKILL.md +108 -108
  203. package/skills/parallel-deep-research/evals/README.md +27 -27
  204. package/skills/parallel-deep-research/evals/evals.json +62 -62
  205. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  206. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  207. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  208. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  209. package/docs/WORKFLOW.md +0 -400
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Maps GitHub issue labels to Beads type and priority.
3
+ *
4
+ * @param {Array<string|{name: string}>} labels - GitHub labels (strings or objects)
5
+ * @param {object} config - Mapping configuration
6
+ * @param {Record<string, string>} config.labelToType - Label name to Beads type
7
+ * @param {Record<string, number>} config.labelToPriority - Label name to priority number
8
+ * @param {string} [config.defaultType="task"] - Fallback type
9
+ * @param {number} [config.defaultPriority=2] - Fallback priority
10
+ * @returns {{ type: string, priority: number }}
11
+ */
12
+ export function mapLabels(labels, config) {
13
+ const {
14
+ defaultType = 'task',
15
+ defaultPriority = 2,
16
+ } = config;
17
+ const labelToType = (config.labelToType && typeof config.labelToType === 'object' && !Array.isArray(config.labelToType)) ? config.labelToType : {};
18
+ const labelToPriority = (config.labelToPriority && typeof config.labelToPriority === 'object' && !Array.isArray(config.labelToPriority)) ? config.labelToPriority : {};
19
+
20
+ // Normalize labels to lowercase strings
21
+ const names = labels.map((l) =>
22
+ (typeof l === 'string' ? l : l.name).toLowerCase()
23
+ );
24
+
25
+ // Build lowercase lookup maps
26
+ const typeLookup = Object.create(null);
27
+ for (const [key, value] of Object.entries(labelToType)) {
28
+ typeLookup[key.toLowerCase()] = value;
29
+ }
30
+
31
+ const priorityLookup = Object.create(null);
32
+ for (const [key, value] of Object.entries(labelToPriority)) {
33
+ priorityLookup[key.toLowerCase()] = value;
34
+ }
35
+
36
+ // First match wins
37
+ let type = defaultType;
38
+ for (const name of names) {
39
+ if (name in typeLookup) {
40
+ type = typeLookup[name];
41
+ break;
42
+ }
43
+ }
44
+
45
+ let priority = defaultPriority;
46
+ for (const name of names) {
47
+ if (name in priorityLookup) {
48
+ priority = priorityLookup[name];
49
+ break;
50
+ }
51
+ }
52
+
53
+ return { type, priority };
54
+ }
@@ -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
+ }