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.
- package/.claude/commands/dev.md +345 -340
- package/.claude/commands/plan.md +566 -521
- package/.claude/commands/premerge.md +186 -176
- package/.claude/commands/research.md +42 -42
- package/.claude/commands/review.md +448 -442
- package/.claude/commands/rollback.md +721 -721
- package/.claude/commands/ship.md +212 -164
- package/.claude/commands/sonarcloud.md +152 -152
- package/.claude/commands/status.md +90 -48
- package/.claude/commands/validate.md +288 -282
- package/.claude/commands/verify.md +269 -221
- package/.claude/rules/greptile-review-process.md +285 -285
- package/.claude/rules/workflow.md +121 -105
- package/.claude/scripts/greptile-resolve.sh +558 -526
- package/.claude/scripts/load-env.sh +32 -32
- package/.cline/workflows/dev.md +342 -337
- package/.cline/workflows/plan.md +563 -518
- package/.cline/workflows/premerge.md +183 -173
- package/.cline/workflows/research.md +39 -39
- package/.cline/workflows/review.md +445 -439
- package/.cline/workflows/rollback.md +718 -718
- package/.cline/workflows/ship.md +209 -161
- package/.cline/workflows/sonarcloud.md +146 -146
- package/.cline/workflows/status.md +87 -45
- package/.cline/workflows/validate.md +285 -279
- package/.cline/workflows/verify.md +266 -218
- package/.codex/config.toml +11 -11
- package/.codex/skills/dev/SKILL.md +345 -340
- package/.codex/skills/plan/SKILL.md +566 -521
- package/.codex/skills/premerge/SKILL.md +186 -176
- package/.codex/skills/research/SKILL.md +42 -42
- package/.codex/skills/review/SKILL.md +448 -442
- package/.codex/skills/rollback/SKILL.md +721 -721
- package/.codex/skills/ship/SKILL.md +212 -164
- package/.codex/skills/sonarcloud/SKILL.md +149 -149
- package/.codex/skills/status/SKILL.md +90 -48
- package/.codex/skills/validate/SKILL.md +288 -282
- package/.codex/skills/verify/SKILL.md +269 -221
- package/.cursor/commands/dev.md +342 -337
- package/.cursor/commands/plan.md +563 -518
- package/.cursor/commands/premerge.md +183 -173
- package/.cursor/commands/research.md +39 -39
- package/.cursor/commands/review.md +445 -439
- package/.cursor/commands/rollback.md +718 -718
- package/.cursor/commands/ship.md +209 -161
- package/.cursor/commands/sonarcloud.md +146 -146
- package/.cursor/commands/status.md +87 -45
- package/.cursor/commands/validate.md +285 -279
- package/.cursor/commands/verify.md +266 -218
- package/.cursor/rules/permissions-guidance.mdc +37 -37
- package/.forge/hooks/check-tdd.js +240 -240
- package/.github/PLUGIN_TEMPLATE.json +32 -32
- package/.github/prompts/dev.prompt.md +347 -342
- package/.github/prompts/plan.prompt.md +568 -523
- package/.github/prompts/premerge.prompt.md +188 -178
- package/.github/prompts/research.prompt.md +44 -44
- package/.github/prompts/review.prompt.md +450 -444
- package/.github/prompts/rollback.prompt.md +723 -723
- package/.github/prompts/ship.prompt.md +214 -166
- package/.github/prompts/sonarcloud.prompt.md +151 -151
- package/.github/prompts/status.prompt.md +92 -50
- package/.github/prompts/validate.prompt.md +290 -284
- package/.github/prompts/verify.prompt.md +271 -223
- package/.github/workflows/beads-to-github.yml +56 -0
- package/.github/workflows/github-to-beads.yml +97 -0
- package/.kilocode/workflows/dev.md +346 -341
- package/.kilocode/workflows/plan.md +567 -522
- package/.kilocode/workflows/premerge.md +187 -177
- package/.kilocode/workflows/research.md +43 -43
- package/.kilocode/workflows/review.md +449 -443
- package/.kilocode/workflows/rollback.md +722 -722
- package/.kilocode/workflows/ship.md +213 -165
- package/.kilocode/workflows/sonarcloud.md +150 -150
- package/.kilocode/workflows/status.md +91 -49
- package/.kilocode/workflows/validate.md +289 -283
- package/.kilocode/workflows/verify.md +270 -222
- package/.mcp.json.example +12 -12
- package/.opencode/commands/dev.md +345 -340
- package/.opencode/commands/plan.md +566 -521
- package/.opencode/commands/premerge.md +186 -176
- package/.opencode/commands/research.md +42 -42
- package/.opencode/commands/review.md +448 -442
- package/.opencode/commands/rollback.md +721 -721
- package/.opencode/commands/ship.md +212 -164
- package/.opencode/commands/sonarcloud.md +149 -149
- package/.opencode/commands/status.md +90 -48
- package/.opencode/commands/validate.md +288 -282
- package/.opencode/commands/verify.md +269 -221
- package/.roo/commands/dev.md +346 -341
- package/.roo/commands/plan.md +567 -522
- package/.roo/commands/premerge.md +187 -177
- package/.roo/commands/research.md +43 -43
- package/.roo/commands/review.md +449 -443
- package/.roo/commands/rollback.md +722 -722
- package/.roo/commands/ship.md +213 -165
- package/.roo/commands/sonarcloud.md +150 -150
- package/.roo/commands/status.md +91 -49
- package/.roo/commands/validate.md +289 -283
- package/.roo/commands/verify.md +270 -222
- package/AGENTS.md +272 -175
- package/CLAUDE.md +110 -100
- package/README.md +429 -416
- package/bin/forge-cmd.js +317 -313
- package/bin/forge-preflight.js +322 -309
- package/bin/forge.js +4765 -4303
- package/docs/AGENT_INSTALL_PROMPT.md +342 -342
- package/docs/BEADS_GITHUB_SYNC.md +251 -251
- package/docs/ENHANCED_ONBOARDING.md +612 -602
- package/docs/EXAMPLES.md +482 -482
- package/docs/GREPTILE_SETUP.md +400 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
- package/docs/ROADMAP.md +359 -359
- package/docs/SETUP.md +663 -631
- package/docs/TOOLCHAIN.md +653 -630
- package/docs/VALIDATION.md +363 -363
- package/install.sh +40 -1056
- package/lefthook.yml +50 -39
- package/lib/agents/README.md +198 -198
- package/lib/agents/claude.plugin.json +28 -28
- package/lib/agents/cline.plugin.json +22 -22
- package/lib/agents/codex.plugin.json +19 -19
- package/lib/agents/copilot.plugin.json +24 -24
- package/lib/agents/cursor.plugin.json +25 -25
- package/lib/agents/kilocode.plugin.json +22 -22
- package/lib/agents/opencode.plugin.json +20 -20
- package/lib/agents/roo.plugin.json +23 -23
- package/lib/agents-config.js +2112 -2112
- package/lib/beads-health-check.js +143 -0
- package/lib/beads-setup.js +341 -0
- package/lib/beads-sync-scaffold.js +260 -0
- package/lib/commands/_registry.js +134 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/dev.js +571 -513
- package/lib/commands/plan.js +692 -692
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +119 -119
- package/lib/commands/ship.js +377 -377
- package/lib/commands/status.js +378 -378
- package/lib/commands/sync.js +55 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +602 -602
- package/lib/commands/worktree.js +310 -0
- package/lib/context-merge.js +359 -359
- package/lib/dep-guard/analyzer.js +294 -294
- package/lib/dep-guard/behavior-detector.js +98 -98
- package/lib/dep-guard/contract-detector.js +162 -162
- package/lib/dep-guard/import-detector.js +498 -498
- package/lib/dep-guard/path-utils.js +13 -13
- package/lib/dep-guard/rubric.js +120 -120
- package/lib/dep-guard/task-parser.js +318 -318
- package/lib/detect-agent.js +191 -191
- package/lib/detect-worktree.js +47 -47
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-hash.js +26 -26
- package/lib/freshness-token.js +148 -0
- package/lib/greptile-match.js +80 -0
- package/lib/husky-migration.js +450 -0
- package/lib/lefthook-check.js +65 -0
- package/lib/pat-setup.js +207 -0
- package/lib/plugin-catalog.js +350 -350
- package/lib/plugin-manager.js +166 -166
- package/lib/plugin-recommender.js +141 -141
- package/lib/project-discovery.js +491 -491
- package/lib/reset.js +309 -0
- package/lib/setup-action-log.js +139 -139
- package/lib/setup-summary-renderer.js +106 -106
- package/lib/setup-utils.js +96 -0
- package/lib/setup.js +192 -192
- package/lib/smart-merge.js +64 -0
- package/lib/symlink-utils.js +81 -0
- package/lib/task-ownership.js +117 -0
- package/lib/workflow-profiles.js +197 -197
- package/package.json +131 -128
- package/scripts/beads-context.sh +426 -0
- package/scripts/beads-context.test.js +567 -0
- package/scripts/behavioral-judge.sh +378 -0
- package/scripts/benchmark.js +85 -0
- package/scripts/branch-protection.js +183 -0
- package/scripts/check-agents.js +172 -0
- package/scripts/check-forge-token.js +98 -0
- package/scripts/commitlint.js +42 -0
- package/scripts/conflict-detect.sh +323 -0
- package/scripts/dep-guard-analyze.js +71 -0
- package/scripts/dep-guard.sh +789 -0
- package/scripts/eval_win.py +249 -0
- package/scripts/file-index.sh +493 -0
- package/scripts/forge-team/index.sh +86 -0
- package/scripts/forge-team/lib/agent-prompt.sh +52 -0
- package/scripts/forge-team/lib/claim.sh +256 -0
- package/scripts/forge-team/lib/dashboard.sh +341 -0
- package/scripts/forge-team/lib/epic.sh +332 -0
- package/scripts/forge-team/lib/hooks.sh +253 -0
- package/scripts/forge-team/lib/identity.sh +235 -0
- package/scripts/forge-team/lib/sync-github.sh +317 -0
- package/scripts/forge-team/lib/verify.sh +284 -0
- package/scripts/forge-team/lib/workload.sh +296 -0
- package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
- package/scripts/forge-team/tests/claim.test.sh +179 -0
- package/scripts/forge-team/tests/dashboard.test.sh +170 -0
- package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
- package/scripts/forge-team/tests/epic.test.sh +176 -0
- package/scripts/forge-team/tests/hooks.test.sh +239 -0
- package/scripts/forge-team/tests/identity.test.sh +176 -0
- package/scripts/forge-team/tests/integration.test.sh +371 -0
- package/scripts/forge-team/tests/sync-github.test.sh +209 -0
- package/scripts/forge-team/tests/verify.test.sh +314 -0
- package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
- package/scripts/forge-team/tests/workload.test.sh +209 -0
- package/scripts/github-beads-sync/comment.mjs +64 -0
- package/scripts/github-beads-sync/config.mjs +148 -0
- package/scripts/github-beads-sync/github-api.mjs +131 -0
- package/scripts/github-beads-sync/index.mjs +332 -0
- package/scripts/github-beads-sync/label-mapper.mjs +54 -0
- package/scripts/github-beads-sync/mapping.mjs +78 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
- package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
- package/scripts/github-beads-sync/run-bd.mjs +159 -0
- package/scripts/github-beads-sync/sanitize.mjs +121 -0
- package/scripts/github-beads-sync.config.json +26 -0
- package/scripts/improve-command.js +375 -0
- package/scripts/lib/eval-runner.js +268 -0
- package/scripts/lib/eval-schema.js +135 -0
- package/scripts/lib/eval-storage.js +78 -0
- package/scripts/lib/grading.js +203 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/lib/transcript-parser.js +63 -0
- package/scripts/lint.js +47 -0
- package/scripts/migrate-to-bun-test.js +412 -0
- package/scripts/pr-coordinator.sh +706 -0
- package/scripts/run-command-eval.js +236 -0
- package/scripts/smart-status.sh +809 -0
- package/scripts/sync-commands.js +571 -0
- package/scripts/sync-utils.sh +455 -0
- package/scripts/test-dashboard.js +123 -0
- package/scripts/test.js +46 -0
- package/scripts/validate.sh +94 -0
- package/skills/parallel-deep-research/SKILL.md +108 -108
- package/skills/parallel-deep-research/evals/README.md +27 -27
- package/skills/parallel-deep-research/evals/evals.json +62 -62
- package/skills/sonarcloud-analysis/SKILL.md +171 -171
- package/skills/sonarcloud-analysis/evals/README.md +27 -27
- package/skills/sonarcloud-analysis/evals/evals.json +50 -50
- package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
- package/.cursor/hooks/state/continual-learning-index.json +0 -19
- 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
|
+
}
|