forge-workflow 0.0.8 → 0.0.10
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 +2 -2
- package/.claude/commands/plan.md +2 -2
- package/.claude/commands/premerge.md +2 -2
- package/.claude/commands/review.md +5 -2
- package/.claude/commands/ship.md +6 -5
- package/.claude/commands/status.md +4 -4
- package/.claude/rules/greptile-review-process.md +4 -4
- package/.cline/workflows/dev.md +2 -2
- package/.cline/workflows/plan.md +2 -2
- package/.cline/workflows/premerge.md +2 -2
- package/.cline/workflows/review.md +5 -2
- package/.cline/workflows/ship.md +6 -5
- package/.cline/workflows/status.md +4 -4
- package/.codex/skills/dev/SKILL.md +2 -2
- package/.codex/skills/plan/SKILL.md +2 -2
- package/.codex/skills/premerge/SKILL.md +2 -2
- package/.codex/skills/review/SKILL.md +5 -2
- package/.codex/skills/ship/SKILL.md +6 -5
- package/.codex/skills/status/SKILL.md +4 -4
- package/.cursor/commands/dev.md +2 -2
- package/.cursor/commands/plan.md +2 -2
- package/.cursor/commands/premerge.md +2 -2
- package/.cursor/commands/review.md +5 -2
- package/.cursor/commands/ship.md +6 -5
- package/.cursor/commands/status.md +4 -4
- package/.github/prompts/dev.prompt.md +2 -2
- package/.github/prompts/plan.prompt.md +2 -2
- package/.github/prompts/premerge.prompt.md +2 -2
- package/.github/prompts/review.prompt.md +5 -2
- package/.github/prompts/ship.prompt.md +6 -5
- package/.github/prompts/status.prompt.md +4 -4
- package/.github/workflows/beads-to-github.yml +44 -11
- package/.github/workflows/github-to-beads.yml +11 -8
- package/.kilocode/workflows/dev.md +2 -2
- package/.kilocode/workflows/plan.md +2 -2
- package/.kilocode/workflows/premerge.md +2 -2
- package/.kilocode/workflows/review.md +5 -2
- package/.kilocode/workflows/ship.md +6 -5
- package/.kilocode/workflows/status.md +4 -4
- package/.opencode/commands/dev.md +2 -2
- package/.opencode/commands/plan.md +2 -2
- package/.opencode/commands/premerge.md +2 -2
- package/.opencode/commands/review.md +5 -2
- package/.opencode/commands/ship.md +6 -5
- package/.opencode/commands/status.md +4 -4
- package/.roo/commands/dev.md +2 -2
- package/.roo/commands/plan.md +2 -2
- package/.roo/commands/premerge.md +2 -2
- package/.roo/commands/review.md +5 -2
- package/.roo/commands/ship.md +6 -5
- package/.roo/commands/status.md +4 -4
- package/AGENTS.md +10 -9
- package/CLAUDE.md +12 -0
- package/README.md +12 -6
- package/bin/forge.js +24 -8
- package/docs/BEADS_GITHUB_SYNC.md +32 -2
- package/docs/EXAMPLES.md +22 -22
- package/docs/ROADMAP.md +3 -3
- package/docs/TOOLCHAIN.md +150 -160
- package/lib/agents/codex.plugin.json +3 -0
- package/lib/agents-config.js +18 -12
- package/lib/beads-bootstrap.js +225 -0
- package/lib/beads-health-check.js +55 -10
- package/lib/beads-setup.js +104 -28
- package/lib/beads-sync-scaffold.js +11 -6
- package/lib/codex-skills.js +54 -1
- package/lib/commands/_issue.js +11 -1
- package/lib/commands/issues.js +49 -0
- package/lib/commands/plan.js +5 -2
- package/lib/commands/recommend.js +22 -1
- package/lib/commands/setup.js +247 -27
- package/lib/commands/ship.js +188 -5
- package/lib/commands/status.js +187 -19
- package/lib/commands/team.js +11 -1
- package/lib/commands/test.js +125 -25
- package/lib/commands/validate.js +232 -9
- package/lib/commands/worktree.js +27 -54
- package/lib/dep-guard/keyword-ripple.js +184 -0
- package/lib/detect-worktree.js +9 -10
- package/lib/forge-issues.js +326 -0
- package/lib/issue-sync/authority.js +100 -0
- package/lib/issue-sync/github-pull.js +184 -0
- package/lib/issue-sync/import-primitives.js +98 -0
- package/lib/issue-sync/legacy-link-bridge.js +436 -0
- package/lib/issue-sync/link-store.js +292 -0
- package/lib/issue-sync/project-github.js +123 -0
- package/lib/issue-sync/reconcile.js +195 -0
- package/lib/issue-sync/schema.js +126 -0
- package/lib/lefthook-check.js +5 -2
- package/lib/project-memory.js +564 -0
- package/lib/runtime-health.js +100 -12
- package/lib/setup-action-log.js +2 -0
- package/lib/setup-summary-renderer.js +15 -11
- package/lib/smart-status/conflicts.js +205 -0
- package/lib/smart-status/scoring.js +177 -0
- package/lib/status/beads-snapshot.js +102 -0
- package/lib/status/presenter.js +65 -0
- package/lib/workflow/enforce-stage.js +14 -8
- package/lib/workflow/state-manager.js +349 -0
- package/package.json +12 -4
- package/scripts/beads-context.sh +124 -5
- package/scripts/beads-context.test.js +21 -4
- package/scripts/beads-migrate-to-dolt.sh +7 -0
- package/scripts/beads-upgrade-smoke.sh +263 -0
- package/scripts/behavioral-judge.sh +115 -11
- package/scripts/benchmark.js +349 -63
- package/scripts/dep-guard-analyze.js +52 -17
- package/scripts/dep-guard-keyword-ripple.js +29 -0
- package/scripts/dep-guard-render-review.js +86 -0
- package/scripts/dep-guard.sh +45 -222
- package/scripts/forge-team/lib/hooks.sh +1 -1
- package/scripts/forge-team/lib/sync-github.sh +160 -28
- package/scripts/forge-team/lib/verify.sh +1 -1
- package/scripts/forge-team/lib/workload.sh +56 -27
- package/scripts/forge-team/tests/sync-github.test.sh +195 -58
- package/scripts/forge-team/tests/workload.test.sh +35 -4
- package/scripts/github-beads-sync/index.mjs +122 -98
- package/scripts/github-beads-sync/mapping.mjs +54 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +2 -2
- package/scripts/github-beads-sync/reverse-sync.mjs +31 -7
- package/scripts/github-beads-sync/run-bd.mjs +4 -2
- package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
- package/scripts/preflight.sh +181 -0
- package/scripts/smart-status-score.js +31 -0
- package/scripts/smart-status-sessions.js +51 -0
- package/scripts/smart-status.sh +84 -330
- package/scripts/sync-agentic-workflow.js +48 -0
- package/scripts/sync-utils.sh +39 -0
- package/scripts/test-ci-shard.js +244 -0
- package/scripts/test-dashboard.js +188 -52
- package/scripts/test-full-suite.js +186 -0
- package/scripts/test-profile.js +278 -0
- package/scripts/test.js +335 -38
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
|
@@ -10,29 +10,18 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
10
|
import { loadConfig } from './config.mjs';
|
|
11
11
|
import { sanitizeTitle } from './sanitize.mjs';
|
|
12
12
|
import { mapLabels } from './label-mapper.mjs';
|
|
13
|
+
import { resolveCanonicalBeadsLink, upsertCanonicalBeadsLink } from './mapping.mjs';
|
|
13
14
|
import { bdCreate as realBdCreate, bdClose as realBdClose, bdShow as realBdShow } from './run-bd.mjs';
|
|
14
|
-
import { getBeadsId as realGetBeadsId, setBeadsId as realSetBeadsId } from './mapping.mjs';
|
|
15
15
|
import { buildComment, parseComment } from './comment.mjs';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from './github-api.mjs';
|
|
16
|
+
import { createOrEditComment as realCreateOrEditComment, findSyncComment as realFindSyncComment } from './github-api.mjs';
|
|
17
|
+
|
|
18
|
+
const DEFAULT_MAPPING_PATH = '.github/beads-mapping.json';
|
|
20
19
|
|
|
21
|
-
/**
|
|
22
|
-
* Check if a sender login matches known bot patterns.
|
|
23
|
-
* @param {string} login - GitHub sender login
|
|
24
|
-
* @returns {boolean}
|
|
25
|
-
*/
|
|
26
20
|
function isBot(login) {
|
|
27
21
|
if (!login) return false;
|
|
28
22
|
return login.includes('[bot]') || login === 'github-actions';
|
|
29
23
|
}
|
|
30
24
|
|
|
31
|
-
/**
|
|
32
|
-
* Check if an issue has the skip-beads-sync label.
|
|
33
|
-
* @param {Array<{name: string}|string>} labels
|
|
34
|
-
* @returns {boolean}
|
|
35
|
-
*/
|
|
36
25
|
function hasSkipLabel(labels) {
|
|
37
26
|
if (!labels) return false;
|
|
38
27
|
return labels.some((l) => {
|
|
@@ -41,6 +30,39 @@ function hasSkipLabel(labels) {
|
|
|
41
30
|
});
|
|
42
31
|
}
|
|
43
32
|
|
|
33
|
+
function getGitHubLink(issue = {}) {
|
|
34
|
+
return {
|
|
35
|
+
nodeId: issue.node_id ?? issue.nodeId ?? null,
|
|
36
|
+
number: issue.number ?? null,
|
|
37
|
+
url: issue.html_url ?? null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createCanonicalLinkStore(mappingPath) {
|
|
42
|
+
return {
|
|
43
|
+
resolveCanonicalLink(lookup) {
|
|
44
|
+
return resolveCanonicalBeadsLink(mappingPath, lookup);
|
|
45
|
+
},
|
|
46
|
+
upsertCanonicalLink(link) {
|
|
47
|
+
return upsertCanonicalBeadsLink(mappingPath, link);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getCanonicalLinkStore(options = {}) {
|
|
53
|
+
const defaultStore = createCanonicalLinkStore(options.mappingPath ?? DEFAULT_MAPPING_PATH);
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
options.linkStore &&
|
|
57
|
+
typeof options.linkStore.resolveCanonicalLink === 'function' &&
|
|
58
|
+
typeof options.linkStore.upsertCanonicalLink === 'function'
|
|
59
|
+
) {
|
|
60
|
+
return options.linkStore;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return defaultStore;
|
|
64
|
+
}
|
|
65
|
+
|
|
44
66
|
/**
|
|
45
67
|
* Handle a GitHub issue "opened" event.
|
|
46
68
|
*
|
|
@@ -52,35 +74,30 @@ function hasSkipLabel(labels) {
|
|
|
52
74
|
* @param {string} options.repo - Repository name
|
|
53
75
|
* @param {object} [options.bd] - Dependency injection for bd functions
|
|
54
76
|
* @param {object} [options.github] - Dependency injection for github-api functions
|
|
55
|
-
* @param {object} [options.
|
|
77
|
+
* @param {object} [options.linkStore] - Dependency injection for canonical link store
|
|
56
78
|
* @param {object} [options.configOverride] - Merge into loaded config (for testing)
|
|
57
79
|
* @returns {object} Result object
|
|
58
80
|
*/
|
|
59
81
|
export function handleOpened(event, options = {}) {
|
|
60
82
|
const {
|
|
61
83
|
configPath,
|
|
62
|
-
mappingPath,
|
|
63
84
|
owner,
|
|
64
85
|
repo,
|
|
65
86
|
bd = {},
|
|
66
87
|
github = {},
|
|
67
|
-
mapping = {},
|
|
68
88
|
configOverride,
|
|
69
89
|
} = options;
|
|
70
90
|
|
|
71
91
|
const bdCreate = bd.bdCreate ?? realBdCreate;
|
|
72
|
-
const findSyncComment = github.findSyncComment ?? realFindSyncComment;
|
|
73
92
|
const createOrEditComment = github.createOrEditComment ?? realCreateOrEditComment;
|
|
74
|
-
const
|
|
75
|
-
const
|
|
93
|
+
const findSyncComment = github.findSyncComment ?? realFindSyncComment;
|
|
94
|
+
const canonicalLinkStore = getCanonicalLinkStore(options);
|
|
76
95
|
|
|
77
|
-
// 1. Load config
|
|
78
96
|
let config = loadConfig(configPath);
|
|
79
97
|
if (configOverride) {
|
|
80
98
|
config = { ...config, ...configOverride };
|
|
81
99
|
}
|
|
82
100
|
|
|
83
|
-
// 2. Extract event data
|
|
84
101
|
const issue = event.issue;
|
|
85
102
|
const issueNumber = issue.number;
|
|
86
103
|
const rawTitle = issue.title;
|
|
@@ -89,23 +106,22 @@ export function handleOpened(event, options = {}) {
|
|
|
89
106
|
const htmlUrl = issue.html_url;
|
|
90
107
|
const body = issue.body ?? '';
|
|
91
108
|
const authorAssociation = issue.author_association;
|
|
109
|
+
const rawLabelNames = labels.map((l) => (typeof l === 'string' ? l : l.name));
|
|
110
|
+
const { type, priority } = mapLabels(rawLabelNames, config);
|
|
111
|
+
const externalRef = `gh-${issueNumber}`;
|
|
92
112
|
|
|
93
|
-
// 3. Guard: bot actor
|
|
94
113
|
if (isBot(event.sender?.login)) {
|
|
95
114
|
return { skipped: true, reason: 'bot actor' };
|
|
96
115
|
}
|
|
97
116
|
|
|
98
|
-
// 4. Guard: skip label
|
|
99
117
|
if (hasSkipLabel(labels)) {
|
|
100
118
|
return { skipped: true, reason: 'skip label' };
|
|
101
119
|
}
|
|
102
120
|
|
|
103
|
-
// 5. Guard: no-beads body
|
|
104
121
|
if (body.includes('no-beads')) {
|
|
105
122
|
return { skipped: true, reason: 'no-beads in body' };
|
|
106
123
|
}
|
|
107
124
|
|
|
108
|
-
// 6. Guard: public repo gate
|
|
109
125
|
if (config.publicRepoGate === 'author_association') {
|
|
110
126
|
const allowed = config.gateAssociations || [];
|
|
111
127
|
if (!allowed.includes(authorAssociation)) {
|
|
@@ -122,48 +138,54 @@ export function handleOpened(event, options = {}) {
|
|
|
122
138
|
}
|
|
123
139
|
}
|
|
124
140
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
141
|
+
const canonicalLink = canonicalLinkStore.resolveCanonicalLink({
|
|
142
|
+
githubNodeId: issue.node_id ?? issue.nodeId ?? null,
|
|
143
|
+
githubNumber: issue.number,
|
|
144
|
+
});
|
|
145
|
+
if (canonicalLink) {
|
|
146
|
+
const commentBody = buildComment(canonicalLink.forgeIssueId, issueNumber, {
|
|
147
|
+
type,
|
|
148
|
+
priority,
|
|
149
|
+
externalRef,
|
|
150
|
+
});
|
|
151
|
+
createOrEditComment(owner, repo, issueNumber, commentBody);
|
|
135
152
|
return {
|
|
136
153
|
skipped: true,
|
|
137
|
-
reason: 'already synced (
|
|
138
|
-
beadsId:
|
|
154
|
+
reason: 'already synced (canonical link)',
|
|
155
|
+
beadsId: canonicalLink.forgeIssueId,
|
|
139
156
|
};
|
|
140
157
|
}
|
|
141
158
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
159
|
+
const existingSyncComment = parseComment(findSyncComment(owner, repo, issueNumber)?.body);
|
|
160
|
+
if (existingSyncComment?.beadsId) {
|
|
161
|
+
canonicalLinkStore.upsertCanonicalLink({
|
|
162
|
+
forgeIssueId: existingSyncComment.beadsId,
|
|
163
|
+
github: getGitHubLink(issue),
|
|
164
|
+
sources: [
|
|
165
|
+
{
|
|
166
|
+
source: 'syncComment',
|
|
167
|
+
forgeIssueId: existingSyncComment.beadsId,
|
|
168
|
+
githubNumber: issueNumber,
|
|
169
|
+
url: htmlUrl,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
diagnostics: [],
|
|
173
|
+
});
|
|
174
|
+
const commentBody = buildComment(existingSyncComment.beadsId, issueNumber, {
|
|
175
|
+
type,
|
|
176
|
+
priority,
|
|
177
|
+
externalRef,
|
|
178
|
+
});
|
|
179
|
+
createOrEditComment(owner, repo, issueNumber, commentBody);
|
|
150
180
|
return {
|
|
151
181
|
skipped: true,
|
|
152
|
-
reason: 'already synced (comment)',
|
|
153
|
-
beadsId:
|
|
182
|
+
reason: 'already synced (sync comment)',
|
|
183
|
+
beadsId: existingSyncComment.beadsId,
|
|
154
184
|
};
|
|
155
185
|
}
|
|
156
186
|
|
|
157
|
-
// 8. Sanitize title (labels mapped from raw names — sanitization only needed for CLI args)
|
|
158
187
|
const { sanitized: sanitizedTitle, warnings: titleWarnings } = sanitizeTitle(rawTitle);
|
|
159
188
|
if (titleWarnings.length) console.warn('sanitize:', titleWarnings);
|
|
160
|
-
|
|
161
|
-
// 9. Map labels using raw names (case-insensitive matching in mapLabels handles normalization)
|
|
162
|
-
const rawLabelNames = labels.map((l) => (typeof l === 'string' ? l : l.name));
|
|
163
|
-
const { type, priority } = mapLabels(rawLabelNames, config);
|
|
164
|
-
|
|
165
|
-
// 10. Create beads issue
|
|
166
|
-
const externalRef = `gh-${issueNumber}`;
|
|
167
189
|
const beadsId = bdCreate({
|
|
168
190
|
title: sanitizedTitle,
|
|
169
191
|
type,
|
|
@@ -174,17 +196,26 @@ export function handleOpened(event, options = {}) {
|
|
|
174
196
|
});
|
|
175
197
|
|
|
176
198
|
if (!beadsId) {
|
|
177
|
-
return { success: false, reason: 'bd create failed
|
|
199
|
+
return { success: false, reason: 'bd create failed - no beads ID returned', issueNumber };
|
|
178
200
|
}
|
|
179
201
|
|
|
180
|
-
|
|
181
|
-
|
|
202
|
+
canonicalLinkStore.upsertCanonicalLink({
|
|
203
|
+
forgeIssueId: beadsId,
|
|
204
|
+
github: getGitHubLink(issue),
|
|
205
|
+
sources: [
|
|
206
|
+
{
|
|
207
|
+
source: 'externalRef',
|
|
208
|
+
forgeIssueId: beadsId,
|
|
209
|
+
githubNumber: issueNumber,
|
|
210
|
+
url: htmlUrl,
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
diagnostics: [],
|
|
214
|
+
});
|
|
182
215
|
|
|
183
|
-
// 12. Build and post comment
|
|
184
216
|
const commentBody = buildComment(beadsId, issueNumber, { type, priority, externalRef });
|
|
185
217
|
createOrEditComment(owner, repo, issueNumber, commentBody);
|
|
186
218
|
|
|
187
|
-
// 13. Return success
|
|
188
219
|
return { success: true, beadsId, issueNumber };
|
|
189
220
|
}
|
|
190
221
|
|
|
@@ -198,65 +229,66 @@ export function handleOpened(event, options = {}) {
|
|
|
198
229
|
* @param {string} options.repo - Repository name
|
|
199
230
|
* @param {object} [options.bd] - Dependency injection for bd functions
|
|
200
231
|
* @param {object} [options.github] - Dependency injection for github-api functions
|
|
201
|
-
* @param {object} [options.
|
|
232
|
+
* @param {object} [options.linkStore] - Dependency injection for canonical link store
|
|
202
233
|
* @returns {object} Result object
|
|
203
234
|
*/
|
|
204
235
|
export function handleClosed(event, options = {}) {
|
|
205
236
|
const {
|
|
206
|
-
mappingPath,
|
|
207
|
-
owner,
|
|
208
|
-
repo,
|
|
209
237
|
bd = {},
|
|
210
|
-
github = {},
|
|
211
|
-
mapping = {},
|
|
212
238
|
} = options;
|
|
213
239
|
|
|
214
240
|
const bdClose = bd.bdClose ?? realBdClose;
|
|
215
241
|
const bdShow = bd.bdShow ?? realBdShow;
|
|
216
|
-
const
|
|
217
|
-
const
|
|
242
|
+
const findSyncComment = options.github?.findSyncComment ?? realFindSyncComment;
|
|
243
|
+
const canonicalLinkStore = getCanonicalLinkStore(options);
|
|
218
244
|
|
|
219
|
-
|
|
220
|
-
const issueNumber =
|
|
221
|
-
const labels =
|
|
222
|
-
const stateReason =
|
|
245
|
+
const issue = event.issue;
|
|
246
|
+
const issueNumber = issue.number;
|
|
247
|
+
const labels = issue.labels || [];
|
|
248
|
+
const stateReason = issue.state_reason;
|
|
223
249
|
|
|
224
|
-
// 2. Guard: bot actor
|
|
225
250
|
if (isBot(event.sender?.login)) {
|
|
226
251
|
return { skipped: true, reason: 'bot actor' };
|
|
227
252
|
}
|
|
228
253
|
|
|
229
|
-
// 3. Guard: skip label
|
|
230
254
|
if (hasSkipLabel(labels)) {
|
|
231
255
|
return { skipped: true, reason: 'skip label' };
|
|
232
256
|
}
|
|
233
257
|
|
|
234
|
-
// 4. Guard: skip "not planned" closures (only close on "completed")
|
|
235
258
|
if (stateReason && stateReason !== 'completed') {
|
|
236
259
|
return { skipped: true, reason: `closed as ${stateReason}` };
|
|
237
260
|
}
|
|
238
261
|
|
|
239
|
-
|
|
240
|
-
|
|
262
|
+
const canonicalLink = canonicalLinkStore.resolveCanonicalLink({
|
|
263
|
+
githubNodeId: issue.node_id ?? issue.nodeId ?? null,
|
|
264
|
+
githubNumber: issue.number,
|
|
265
|
+
});
|
|
266
|
+
let beadsId = canonicalLink?.forgeIssueId ?? null;
|
|
241
267
|
|
|
242
|
-
// 6. Fallback: find via sync comment
|
|
243
268
|
if (!beadsId) {
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
269
|
+
const syncCommentLink = parseComment(findSyncComment(options.owner, options.repo, issueNumber)?.body);
|
|
270
|
+
if (syncCommentLink?.beadsId && syncCommentLink.issueNumber === issueNumber) {
|
|
271
|
+
canonicalLinkStore.upsertCanonicalLink({
|
|
272
|
+
forgeIssueId: syncCommentLink.beadsId,
|
|
273
|
+
github: getGitHubLink(issue),
|
|
274
|
+
sources: [
|
|
275
|
+
{
|
|
276
|
+
source: 'syncComment',
|
|
277
|
+
forgeIssueId: syncCommentLink.beadsId,
|
|
278
|
+
githubNumber: issueNumber,
|
|
279
|
+
url: issue.html_url ?? null,
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
diagnostics: [],
|
|
283
|
+
});
|
|
284
|
+
beadsId = syncCommentLink.beadsId;
|
|
251
285
|
}
|
|
252
286
|
}
|
|
253
287
|
|
|
254
|
-
// 7. No beads link found
|
|
255
288
|
if (!beadsId) {
|
|
256
289
|
return { skipped: true, reason: 'no beads link found' };
|
|
257
290
|
}
|
|
258
291
|
|
|
259
|
-
// 8. Check if already closed
|
|
260
292
|
const status = bdShow(beadsId);
|
|
261
293
|
if (status === null) {
|
|
262
294
|
return { skipped: true, reason: 'could not determine beads status' };
|
|
@@ -265,16 +297,11 @@ export function handleClosed(event, options = {}) {
|
|
|
265
297
|
return { skipped: true, reason: 'already closed' };
|
|
266
298
|
}
|
|
267
299
|
|
|
268
|
-
// 9. Close beads issue
|
|
269
300
|
bdClose(beadsId, `Closed via GitHub issue #${issueNumber}`);
|
|
270
301
|
|
|
271
|
-
// 10. Return success
|
|
272
302
|
return { success: true, beadsId, issueNumber };
|
|
273
303
|
}
|
|
274
304
|
|
|
275
|
-
// ---------------------------------------------------------------------------
|
|
276
|
-
// CLI entry point
|
|
277
|
-
// ---------------------------------------------------------------------------
|
|
278
305
|
const __filename = fileURLToPath(import.meta.url);
|
|
279
306
|
if (process.argv[1] === __filename) {
|
|
280
307
|
const action = process.argv[2];
|
|
@@ -291,12 +318,11 @@ if (process.argv[1] === __filename) {
|
|
|
291
318
|
|
|
292
319
|
const options = {
|
|
293
320
|
configPath: process.env.BEADS_SYNC_CONFIG || undefined,
|
|
294
|
-
mappingPath: process.env.BEADS_SYNC_MAPPING ||
|
|
321
|
+
mappingPath: process.env.BEADS_SYNC_MAPPING || DEFAULT_MAPPING_PATH,
|
|
295
322
|
owner,
|
|
296
323
|
repo,
|
|
297
324
|
};
|
|
298
325
|
|
|
299
|
-
// Validate action explicitly — only allow known actions
|
|
300
326
|
const VALID_ACTIONS = ['opened', 'closed'];
|
|
301
327
|
if (!VALID_ACTIONS.includes(action)) {
|
|
302
328
|
console.error(`Unknown action: "${action}". Expected one of: ${VALID_ACTIONS.join(', ')}`);
|
|
@@ -309,13 +335,11 @@ if (process.argv[1] === __filename) {
|
|
|
309
335
|
.then((result) => {
|
|
310
336
|
console.log(JSON.stringify(result, null, 2));
|
|
311
337
|
|
|
312
|
-
// Exit non-zero on logical failure (e.g., bdCreate returned null)
|
|
313
338
|
if (result.success === false) {
|
|
314
339
|
console.error(`Sync failed: ${result.reason || 'unknown'}`);
|
|
315
340
|
process.exit(1);
|
|
316
341
|
}
|
|
317
342
|
|
|
318
|
-
// Write to GITHUB_OUTPUT if available
|
|
319
343
|
const outputPath = process.env.GITHUB_OUTPUT;
|
|
320
344
|
if (outputPath) {
|
|
321
345
|
for (const [key, value] of Object.entries(result)) {
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import fs from 'node:fs';
|
|
11
11
|
import path from 'node:path';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const { createLinkStore, resolveCanonicalLink, upsertCanonicalLink } = require('../../lib/issue-sync/link-store.js');
|
|
16
|
+
const { bridgeLegacyLinkHints } = require('../../lib/issue-sync/legacy-link-bridge.js');
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* Reads the mapping file and returns parsed JSON.
|
|
@@ -76,3 +81,52 @@ export function setBeadsId(mappingPath, issueNumber, beadsId) {
|
|
|
76
81
|
data[key] = beadsId;
|
|
77
82
|
writeMapping(mappingPath, data);
|
|
78
83
|
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Loads a canonical link store seeded from the legacy mapping file plus any
|
|
87
|
+
* optional migration hints.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} mappingPath - Path to the legacy mapping file
|
|
90
|
+
* @param {object} [legacyHints] - Additional migration hints to bridge
|
|
91
|
+
* @returns {ReturnType<typeof createLinkStore>}
|
|
92
|
+
*/
|
|
93
|
+
export function loadCanonicalLinkStore(mappingPath, legacyHints = {}) {
|
|
94
|
+
const store = createLinkStore();
|
|
95
|
+
const mapping = readMapping(mappingPath);
|
|
96
|
+
bridgeLegacyLinkHints({ ...legacyHints, mapping }, { store });
|
|
97
|
+
return store;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolves the canonical link record for a GitHub lookup using the legacy
|
|
102
|
+
* mapping file only as a bridge input.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} mappingPath - Path to the legacy mapping file
|
|
105
|
+
* @param {object} lookup - Canonical lookup values
|
|
106
|
+
* @param {object} [legacyHints] - Additional migration hints to bridge
|
|
107
|
+
* @returns {object|null}
|
|
108
|
+
*/
|
|
109
|
+
export function resolveCanonicalBeadsLink(mappingPath, lookup = {}, legacyHints = {}) {
|
|
110
|
+
const store = loadCanonicalLinkStore(mappingPath, legacyHints);
|
|
111
|
+
return resolveCanonicalLink(store, lookup);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Upserts a canonical link and mirrors it into the legacy mapping file for
|
|
116
|
+
* compatibility during migration.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} mappingPath - Path to the legacy mapping file
|
|
119
|
+
* @param {object} link - Canonical link record
|
|
120
|
+
* @param {object} [legacyHints] - Additional migration hints to bridge
|
|
121
|
+
* @returns {object}
|
|
122
|
+
*/
|
|
123
|
+
export function upsertCanonicalBeadsLink(mappingPath, link, legacyHints = {}) {
|
|
124
|
+
const store = loadCanonicalLinkStore(mappingPath, legacyHints);
|
|
125
|
+
const canonical = upsertCanonicalLink(store, link);
|
|
126
|
+
|
|
127
|
+
if (canonical?.github?.number != null && canonical.forgeIssueId) {
|
|
128
|
+
setBeadsId(mappingPath, canonical.github.number, canonical.forgeIssueId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return canonical;
|
|
132
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* CLI entry point for Beads → GitHub reverse sync.
|
|
4
|
-
* Usage: node reverse-sync-cli.mjs <old-jsonl-path> <new-jsonl-path>
|
|
4
|
+
* Usage: node reverse-sync-cli.mjs <old-snapshot-jsonl-path> <new-snapshot-jsonl-path>
|
|
5
5
|
*
|
|
6
6
|
* @module scripts/github-beads-sync/reverse-sync-cli
|
|
7
7
|
*/
|
|
@@ -13,7 +13,7 @@ const oldPath = process.argv[2];
|
|
|
13
13
|
const newPath = process.argv[3];
|
|
14
14
|
|
|
15
15
|
if (!oldPath || !newPath) {
|
|
16
|
-
console.error('Usage: node reverse-sync-cli.mjs <old-jsonl-path> <new-jsonl-path>');
|
|
16
|
+
console.error('Usage: node reverse-sync-cli.mjs <old-snapshot-jsonl-path> <new-snapshot-jsonl-path>');
|
|
17
17
|
process.exit(1);
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Reverse sync: Beads → GitHub.
|
|
3
|
-
* When a Beads issue is closed (via git push updating
|
|
3
|
+
* When a Beads issue is closed (via git push updating exported issue snapshots),
|
|
4
4
|
* close the linked GitHub issue.
|
|
5
5
|
*
|
|
6
6
|
* @module scripts/github-beads-sync/reverse-sync
|
|
@@ -28,13 +28,34 @@ function parseJsonlLines(lines) {
|
|
|
28
28
|
return results;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function extractCanonicalGitHubLink(issue = {}) {
|
|
32
|
+
const github = issue.github ?? {};
|
|
33
|
+
if (typeof github.url === 'string' && github.url.length > 0) {
|
|
34
|
+
const match = github.url.match(
|
|
35
|
+
/https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/,
|
|
36
|
+
);
|
|
37
|
+
if (match) {
|
|
38
|
+
return {
|
|
39
|
+
owner: match[1],
|
|
40
|
+
repo: match[2],
|
|
41
|
+
issueNumber: parseInt(match[3], 10),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function extractPreferredGitHubLink(issue = {}) {
|
|
49
|
+
return extractCanonicalGitHubLink(issue) ?? extractGitHubUrl(issue.description);
|
|
50
|
+
}
|
|
51
|
+
|
|
31
52
|
/**
|
|
32
53
|
* Detect Beads issues that transitioned to "closed" status.
|
|
33
54
|
* Only detects transitions: issue must exist in oldLines as non-closed,
|
|
34
55
|
* and appear in newLines as "closed".
|
|
35
56
|
*
|
|
36
|
-
* @param {string[]} oldLines - JSONL lines from previous
|
|
37
|
-
* @param {string[]} newLines - JSONL lines from current
|
|
57
|
+
* @param {string[]} oldLines - JSONL lines from a previous exported issue snapshot
|
|
58
|
+
* @param {string[]} newLines - JSONL lines from a current exported issue snapshot
|
|
38
59
|
* @returns {Array<{id: string, description: string}>} Issues that transitioned to closed
|
|
39
60
|
*/
|
|
40
61
|
export function detectClosedIssues(oldLines, newLines) {
|
|
@@ -93,11 +114,11 @@ export function closeGitHubIssue(owner, repo, issueNumber) {
|
|
|
93
114
|
|
|
94
115
|
/**
|
|
95
116
|
* Handle Beads-to-GitHub reverse sync.
|
|
96
|
-
* Detects issues that transitioned to closed in
|
|
117
|
+
* Detects issues that transitioned to closed in exported issue snapshot JSONL
|
|
97
118
|
* and closes the linked GitHub issues.
|
|
98
119
|
*
|
|
99
|
-
* @param {string} oldContent - Previous
|
|
100
|
-
* @param {string} newContent - Current
|
|
120
|
+
* @param {string} oldContent - Previous exported issue snapshot content
|
|
121
|
+
* @param {string} newContent - Current exported issue snapshot content
|
|
101
122
|
* @param {object} [deps] - Dependency injection
|
|
102
123
|
* @param {Function} [deps.closeGitHubIssue] - Override for testing
|
|
103
124
|
* @returns {{closed: Array, skipped: Array, errors: Array}}
|
|
@@ -109,13 +130,16 @@ export function handleBeadsClosed(oldContent, newContent, deps = {}) {
|
|
|
109
130
|
const newLines = newContent.split('\n');
|
|
110
131
|
|
|
111
132
|
const transitions = detectClosedIssues(oldLines, newLines);
|
|
133
|
+
const latestIssues = new Map(
|
|
134
|
+
parseJsonlLines(newLines).map((issue) => [issue.id, issue]),
|
|
135
|
+
);
|
|
112
136
|
|
|
113
137
|
const closed = [];
|
|
114
138
|
const skipped = [];
|
|
115
139
|
const errors = [];
|
|
116
140
|
|
|
117
141
|
for (const issue of transitions) {
|
|
118
|
-
const parsed =
|
|
142
|
+
const parsed = extractPreferredGitHubLink(latestIssues.get(issue.id) ?? issue);
|
|
119
143
|
if (!parsed) {
|
|
120
144
|
skipped.push({ beadsId: issue.id, reason: 'no GitHub URL' });
|
|
121
145
|
continue;
|
|
@@ -79,10 +79,12 @@ export function buildSearchArgs(query) {
|
|
|
79
79
|
* @returns {string|null}
|
|
80
80
|
*/
|
|
81
81
|
export function parseCreateOutput(stdout) {
|
|
82
|
-
|
|
82
|
+
// Match forge-xxx with optional dotted sub-IDs (e.g. forge-m1n8.6, forge-dq8j.1).
|
|
83
|
+
// \w in JS doesn't include '.', so the dotted sub-ID group is explicit.
|
|
84
|
+
const match = stdout.match(/Created issue:\s+(forge-[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*)/);
|
|
83
85
|
if (match) return match[1];
|
|
84
86
|
// Fallback: loose match for any forge-prefixed ID anywhere in output
|
|
85
|
-
const fallback = stdout.match(/(forge-[a-
|
|
87
|
+
const fallback = stdout.match(/(forge-[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*)/);
|
|
86
88
|
return fallback ? fallback[1] : null;
|
|
87
89
|
}
|
|
88
90
|
|