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
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const BACKUP_FILES = [
|
|
10
|
+
'issues.jsonl',
|
|
11
|
+
'labels.jsonl',
|
|
12
|
+
'dependencies.jsonl',
|
|
13
|
+
'comments.jsonl',
|
|
14
|
+
'events.jsonl',
|
|
15
|
+
'config.jsonl',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function sanitizePrefix(value) {
|
|
19
|
+
return value
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replaceAll(/[^a-z0-9-]/g, '-')
|
|
23
|
+
.replaceAll(/-{2,}/g, '-')
|
|
24
|
+
.replace(/^-/, '')
|
|
25
|
+
.replace(/-$/, '');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readJsonl(filePath) {
|
|
29
|
+
return fs
|
|
30
|
+
.readFileSync(filePath, 'utf8')
|
|
31
|
+
.split(/\r?\n/)
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.map((line) => JSON.parse(line));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureBackupShape(legacyBackupDir) {
|
|
37
|
+
if (!fs.existsSync(legacyBackupDir) || !fs.statSync(legacyBackupDir).isDirectory()) {
|
|
38
|
+
throw new Error(`Legacy backup directory not found: ${legacyBackupDir}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const file of BACKUP_FILES) {
|
|
42
|
+
const filePath = path.join(legacyBackupDir, file);
|
|
43
|
+
if (!fs.existsSync(filePath)) {
|
|
44
|
+
throw new Error(`Missing backup file: ${filePath}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadBackupData(backupDir) {
|
|
50
|
+
ensureBackupShape(backupDir);
|
|
51
|
+
return {
|
|
52
|
+
issues: readJsonl(path.join(backupDir, 'issues.jsonl')),
|
|
53
|
+
labels: readJsonl(path.join(backupDir, 'labels.jsonl')),
|
|
54
|
+
dependencies: readJsonl(path.join(backupDir, 'dependencies.jsonl')),
|
|
55
|
+
comments: readJsonl(path.join(backupDir, 'comments.jsonl')),
|
|
56
|
+
events: readJsonl(path.join(backupDir, 'events.jsonl')),
|
|
57
|
+
config: readJsonl(path.join(backupDir, 'config.jsonl')),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function setDifference(left, right) {
|
|
62
|
+
const rightSet = new Set(right);
|
|
63
|
+
return left.filter((item) => !rightSet.has(item));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeRecord(value) {
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
return value.map((entry) => normalizeRecord(entry));
|
|
69
|
+
}
|
|
70
|
+
if (value && typeof value === 'object') {
|
|
71
|
+
return Object.fromEntries(
|
|
72
|
+
Object.keys(value)
|
|
73
|
+
.sort()
|
|
74
|
+
.map((key) => [key, normalizeRecord(value[key])]),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function stableSerializeRecord(record) {
|
|
81
|
+
return JSON.stringify(normalizeRecord(record));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function arraysEqual(left, right) {
|
|
85
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function collectParity(data) {
|
|
89
|
+
return {
|
|
90
|
+
counts: {
|
|
91
|
+
issues: data.issues.length,
|
|
92
|
+
labels: data.labels.length,
|
|
93
|
+
dependencies: data.dependencies.length,
|
|
94
|
+
comments: data.comments.length,
|
|
95
|
+
events: data.events.length,
|
|
96
|
+
config: data.config.length,
|
|
97
|
+
},
|
|
98
|
+
issueIds: data.issues.map((issue) => issue.id).sort(),
|
|
99
|
+
dependencyEdges: data.dependencies
|
|
100
|
+
.map((dep) => `${dep.issue_id} ${dep.type} ${dep.depends_on_id}`)
|
|
101
|
+
.sort(),
|
|
102
|
+
commentIds: data.comments.map((comment) => comment.id).sort(),
|
|
103
|
+
configKeys: data.config.map((entry) => entry.key).sort(),
|
|
104
|
+
issueRecords: data.issues.map((issue) => stableSerializeRecord(issue)).sort(),
|
|
105
|
+
labelRecords: data.labels.map((label) => stableSerializeRecord(label)).sort(),
|
|
106
|
+
dependencyRecords: data.dependencies
|
|
107
|
+
.map((dependency) => stableSerializeRecord(dependency))
|
|
108
|
+
.sort(),
|
|
109
|
+
commentRecords: data.comments.map((comment) => stableSerializeRecord(comment)).sort(),
|
|
110
|
+
eventRecords: data.events.map((event) => stableSerializeRecord(event)).sort(),
|
|
111
|
+
configRecords: data.config.map((entry) => stableSerializeRecord(entry)).sort(),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function summarizeParityMismatch(parity) {
|
|
116
|
+
const parts = [];
|
|
117
|
+
if (parity.missingIssueIds.length > 0 || parity.extraIssueIds.length > 0) {
|
|
118
|
+
parts.push('issue ids differ');
|
|
119
|
+
}
|
|
120
|
+
if (
|
|
121
|
+
parity.missingDependencyEdges.length > 0 ||
|
|
122
|
+
parity.extraDependencyEdges.length > 0
|
|
123
|
+
) {
|
|
124
|
+
parts.push('dependency edges differ');
|
|
125
|
+
}
|
|
126
|
+
if (parity.missingCommentIds.length > 0 || parity.extraCommentIds.length > 0) {
|
|
127
|
+
parts.push('comment ids differ');
|
|
128
|
+
}
|
|
129
|
+
if (parity.missingConfigKeys.length > 0 || parity.extraConfigKeys.length > 0) {
|
|
130
|
+
parts.push('config keys differ');
|
|
131
|
+
}
|
|
132
|
+
const recordMismatchMessages = {
|
|
133
|
+
issues: 'issue records differ',
|
|
134
|
+
labels: 'label records differ',
|
|
135
|
+
dependencies: 'dependency records differ',
|
|
136
|
+
comments: 'comment records differ',
|
|
137
|
+
events: 'event records differ',
|
|
138
|
+
config: 'config records differ',
|
|
139
|
+
};
|
|
140
|
+
for (const kind of parity.recordMismatches || []) {
|
|
141
|
+
const message = recordMismatchMessages[kind];
|
|
142
|
+
if (message && !parts.includes(message)) {
|
|
143
|
+
parts.push(message);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (parts.length === 0) {
|
|
147
|
+
parts.push('record counts differ');
|
|
148
|
+
}
|
|
149
|
+
return parts.join('; ');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function verifyMigrationParity({ legacyBackupDir, exportDir }) {
|
|
153
|
+
const source = collectParity(loadBackupData(legacyBackupDir));
|
|
154
|
+
const exported = collectParity(loadBackupData(exportDir));
|
|
155
|
+
const recordMismatches = Object.entries({
|
|
156
|
+
issues: arraysEqual(source.issueRecords, exported.issueRecords),
|
|
157
|
+
labels: arraysEqual(source.labelRecords, exported.labelRecords),
|
|
158
|
+
dependencies: arraysEqual(source.dependencyRecords, exported.dependencyRecords),
|
|
159
|
+
comments: arraysEqual(source.commentRecords, exported.commentRecords),
|
|
160
|
+
events: arraysEqual(source.eventRecords, exported.eventRecords),
|
|
161
|
+
config: arraysEqual(source.configRecords, exported.configRecords),
|
|
162
|
+
})
|
|
163
|
+
.filter(([, match]) => !match)
|
|
164
|
+
.map(([kind]) => kind);
|
|
165
|
+
|
|
166
|
+
const parity = {
|
|
167
|
+
ok:
|
|
168
|
+
arraysEqual(source.counts, exported.counts) &&
|
|
169
|
+
arraysEqual(source.issueIds, exported.issueIds) &&
|
|
170
|
+
arraysEqual(source.dependencyEdges, exported.dependencyEdges) &&
|
|
171
|
+
arraysEqual(source.commentIds, exported.commentIds) &&
|
|
172
|
+
arraysEqual(source.configKeys, exported.configKeys) &&
|
|
173
|
+
recordMismatches.length === 0,
|
|
174
|
+
counts: source.counts,
|
|
175
|
+
issueIds: source.issueIds,
|
|
176
|
+
dependencyEdges: source.dependencyEdges,
|
|
177
|
+
commentIds: source.commentIds,
|
|
178
|
+
configKeys: source.configKeys,
|
|
179
|
+
recordMismatches,
|
|
180
|
+
missingIssueIds: setDifference(source.issueIds, exported.issueIds),
|
|
181
|
+
extraIssueIds: setDifference(exported.issueIds, source.issueIds),
|
|
182
|
+
missingDependencyEdges: setDifference(
|
|
183
|
+
source.dependencyEdges,
|
|
184
|
+
exported.dependencyEdges,
|
|
185
|
+
),
|
|
186
|
+
extraDependencyEdges: setDifference(
|
|
187
|
+
exported.dependencyEdges,
|
|
188
|
+
source.dependencyEdges,
|
|
189
|
+
),
|
|
190
|
+
missingCommentIds: setDifference(source.commentIds, exported.commentIds),
|
|
191
|
+
extraCommentIds: setDifference(exported.commentIds, source.commentIds),
|
|
192
|
+
missingConfigKeys: setDifference(source.configKeys, exported.configKeys),
|
|
193
|
+
extraConfigKeys: setDifference(exported.configKeys, source.configKeys),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return parity;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function snapshotCurrentBeads(projectRoot, snapshotRoot, now = new Date()) {
|
|
200
|
+
const liveBeadsDir = path.join(projectRoot, '.beads');
|
|
201
|
+
if (!fs.existsSync(liveBeadsDir)) {
|
|
202
|
+
throw new Error(`Beads directory not found: ${liveBeadsDir}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const timestamp = now.toISOString().replaceAll(':', '-');
|
|
206
|
+
const snapshotDir = path.join(snapshotRoot, timestamp);
|
|
207
|
+
const snapshotBeadsDir = path.join(snapshotDir, 'current-beads');
|
|
208
|
+
fs.mkdirSync(snapshotDir, { recursive: true });
|
|
209
|
+
fs.cpSync(liveBeadsDir, snapshotBeadsDir, { recursive: true });
|
|
210
|
+
return snapshotDir;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function materializeLegacyBackupDir({ legacyBackupDir, snapshotDir }) {
|
|
214
|
+
const snapshotBeadsDir = path.join(snapshotDir, 'current-beads');
|
|
215
|
+
const synthesizedBackupDir = path.join(snapshotDir, 'legacy-backup');
|
|
216
|
+
fs.mkdirSync(synthesizedBackupDir, { recursive: true });
|
|
217
|
+
|
|
218
|
+
for (const file of BACKUP_FILES) {
|
|
219
|
+
const backupCandidate = path.join(legacyBackupDir, file);
|
|
220
|
+
const liveCandidate = path.join(snapshotBeadsDir, file);
|
|
221
|
+
const sourcePath = fs.existsSync(liveCandidate) ? liveCandidate : backupCandidate;
|
|
222
|
+
if (!fs.existsSync(sourcePath)) {
|
|
223
|
+
throw new Error(`Missing legacy backup file: ${file}`);
|
|
224
|
+
}
|
|
225
|
+
fs.copyFileSync(sourcePath, path.join(synthesizedBackupDir, file));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return synthesizedBackupDir;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function applyMigratedBeadsState({ projectRoot, migratedDir }) {
|
|
232
|
+
const liveBeadsDir = path.join(projectRoot, '.beads');
|
|
233
|
+
const migratedBeadsDir = path.join(migratedDir, '.beads');
|
|
234
|
+
const stagedBeadsDir = path.join(projectRoot, `.beads.next-${Date.now()}`);
|
|
235
|
+
|
|
236
|
+
if (!fs.existsSync(migratedBeadsDir) || !fs.statSync(migratedBeadsDir).isDirectory()) {
|
|
237
|
+
throw new Error(`Migrated Beads directory not found: ${migratedBeadsDir}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fs.rmSync(stagedBeadsDir, { recursive: true, force: true });
|
|
241
|
+
fs.cpSync(migratedBeadsDir, stagedBeadsDir, { recursive: true });
|
|
242
|
+
fs.rmSync(liveBeadsDir, { recursive: true, force: true });
|
|
243
|
+
fs.renameSync(stagedBeadsDir, liveBeadsDir);
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
liveBeadsDir,
|
|
247
|
+
migratedBeadsDir,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function defaultCliPaths(projectRoot) {
|
|
252
|
+
return {
|
|
253
|
+
legacyBackupDir: path.join(projectRoot, '.beads', 'backup'),
|
|
254
|
+
snapshotRoot: path.join(projectRoot, '.beads-migration-snapshots'),
|
|
255
|
+
migratedDir: path.join(projectRoot, '.beads-migrated'),
|
|
256
|
+
exportDir: path.join(projectRoot, '.beads-migrated-export'),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function rollbackLegacyBeadsMigration({ projectRoot, snapshotDir }) {
|
|
261
|
+
const liveBeadsDir = path.join(projectRoot, '.beads');
|
|
262
|
+
const snapshotBeadsDir = path.join(snapshotDir, 'current-beads');
|
|
263
|
+
|
|
264
|
+
if (!fs.existsSync(snapshotBeadsDir)) {
|
|
265
|
+
throw new Error(`Snapshot payload not found: ${snapshotBeadsDir}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fs.rmSync(liveBeadsDir, { recursive: true, force: true });
|
|
269
|
+
fs.cpSync(snapshotBeadsDir, liveBeadsDir, { recursive: true });
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
restored: true,
|
|
273
|
+
snapshotDir,
|
|
274
|
+
liveBeadsDir,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function resolveBdCommand() {
|
|
279
|
+
return process.env.BD_CMD || 'bd';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function inferPrefix(projectRoot) {
|
|
283
|
+
const configPath = path.join(projectRoot, '.beads', 'config.yaml');
|
|
284
|
+
if (fs.existsSync(configPath)) {
|
|
285
|
+
const match = /^issue-prefix:\s*(.+)$/m.exec(fs.readFileSync(configPath, 'utf8'));
|
|
286
|
+
if (match?.[1]) {
|
|
287
|
+
return sanitizePrefix(match[1]);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return sanitizePrefix(path.basename(projectRoot) || 'beads');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function defaultImportBackup({
|
|
294
|
+
projectRoot,
|
|
295
|
+
sourceDir,
|
|
296
|
+
migratedDir,
|
|
297
|
+
exportDir,
|
|
298
|
+
}) {
|
|
299
|
+
const bd = resolveBdCommand();
|
|
300
|
+
fs.mkdirSync(migratedDir, { recursive: true });
|
|
301
|
+
fs.rmSync(exportDir, { recursive: true, force: true });
|
|
302
|
+
|
|
303
|
+
const prefix = inferPrefix(projectRoot);
|
|
304
|
+
const execOpts = {
|
|
305
|
+
cwd: migratedDir,
|
|
306
|
+
encoding: 'utf8',
|
|
307
|
+
env: {
|
|
308
|
+
...process.env,
|
|
309
|
+
BEADS_DIR: path.join(migratedDir, '.beads'),
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
execFileSync(
|
|
314
|
+
bd,
|
|
315
|
+
['init', '--force', '--prefix', prefix, '--skip-hooks', '--skip-agents'],
|
|
316
|
+
execOpts,
|
|
317
|
+
);
|
|
318
|
+
execFileSync(bd, ['backup', 'restore', sourceDir], execOpts);
|
|
319
|
+
execFileSync(bd, ['backup', '--force'], execOpts);
|
|
320
|
+
|
|
321
|
+
const exportedBackupDir = path.join(migratedDir, '.beads', 'backup');
|
|
322
|
+
ensureBackupShape(exportedBackupDir);
|
|
323
|
+
fs.cpSync(exportedBackupDir, exportDir, { recursive: true });
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
strategy: 'bd-backup-restore',
|
|
327
|
+
prefix,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function writeManifest(manifestPath, payload) {
|
|
332
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
333
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export async function runLegacyBeadsMigration(options) {
|
|
337
|
+
const {
|
|
338
|
+
projectRoot,
|
|
339
|
+
legacyBackupDir,
|
|
340
|
+
snapshotRoot,
|
|
341
|
+
migratedDir,
|
|
342
|
+
exportDir,
|
|
343
|
+
importBackup = defaultImportBackup,
|
|
344
|
+
now = new Date(),
|
|
345
|
+
} = options;
|
|
346
|
+
|
|
347
|
+
if (!projectRoot || !legacyBackupDir || !snapshotRoot || !migratedDir || !exportDir) {
|
|
348
|
+
throw new Error('projectRoot, legacyBackupDir, snapshotRoot, migratedDir, and exportDir are required');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const manifestPath = path.join(migratedDir, 'migration-manifest.json');
|
|
352
|
+
if (fs.existsSync(manifestPath)) {
|
|
353
|
+
return {
|
|
354
|
+
status: 'skipped',
|
|
355
|
+
reason: 'already migrated',
|
|
356
|
+
migratedDir,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const snapshotDir = snapshotCurrentBeads(projectRoot, snapshotRoot, now);
|
|
361
|
+
const resolvedLegacyBackupDir = materializeLegacyBackupDir({
|
|
362
|
+
legacyBackupDir,
|
|
363
|
+
snapshotDir,
|
|
364
|
+
});
|
|
365
|
+
let rolledBack = false;
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const importResult = await importBackup({
|
|
369
|
+
projectRoot,
|
|
370
|
+
sourceDir: resolvedLegacyBackupDir,
|
|
371
|
+
migratedDir,
|
|
372
|
+
exportDir,
|
|
373
|
+
snapshotDir,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const parity = await verifyMigrationParity({
|
|
377
|
+
legacyBackupDir: resolvedLegacyBackupDir,
|
|
378
|
+
exportDir,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
if (!parity.ok) {
|
|
382
|
+
await rollbackLegacyBeadsMigration({ projectRoot, snapshotDir });
|
|
383
|
+
rolledBack = true;
|
|
384
|
+
|
|
385
|
+
const error = new Error(
|
|
386
|
+
`Parity verification failed: ${summarizeParityMismatch(parity)}`,
|
|
387
|
+
);
|
|
388
|
+
error.parity = parity;
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const appliedState = applyMigratedBeadsState({
|
|
393
|
+
projectRoot,
|
|
394
|
+
migratedDir,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
writeManifest(manifestPath, {
|
|
398
|
+
status: 'migrated',
|
|
399
|
+
createdAt: now.toISOString(),
|
|
400
|
+
snapshotDir,
|
|
401
|
+
legacyBackupDir: resolvedLegacyBackupDir,
|
|
402
|
+
parity,
|
|
403
|
+
appliedState,
|
|
404
|
+
importResult: importResult || null,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
status: 'migrated',
|
|
409
|
+
snapshotDir,
|
|
410
|
+
migratedDir,
|
|
411
|
+
exportDir,
|
|
412
|
+
legacyBackupDir: resolvedLegacyBackupDir,
|
|
413
|
+
appliedState,
|
|
414
|
+
parity,
|
|
415
|
+
importResult: importResult || null,
|
|
416
|
+
};
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (!rolledBack) {
|
|
419
|
+
await rollbackLegacyBeadsMigration({ projectRoot, snapshotDir });
|
|
420
|
+
}
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function parseCliArgs(argv, cwd = process.cwd()) {
|
|
426
|
+
const options = {
|
|
427
|
+
projectRoot: cwd,
|
|
428
|
+
...defaultCliPaths(cwd),
|
|
429
|
+
};
|
|
430
|
+
const explicitPathFlags = new Set();
|
|
431
|
+
|
|
432
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
433
|
+
const arg = argv[i];
|
|
434
|
+
switch (arg) {
|
|
435
|
+
case '--project-root':
|
|
436
|
+
options.projectRoot = path.resolve(argv[++i]);
|
|
437
|
+
for (const [key, value] of Object.entries(defaultCliPaths(options.projectRoot))) {
|
|
438
|
+
if (!explicitPathFlags.has(key)) {
|
|
439
|
+
options[key] = value;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
case '--legacy-backup-dir':
|
|
444
|
+
options.legacyBackupDir = path.resolve(argv[++i]);
|
|
445
|
+
explicitPathFlags.add('legacyBackupDir');
|
|
446
|
+
break;
|
|
447
|
+
case '--snapshot-root':
|
|
448
|
+
options.snapshotRoot = path.resolve(argv[++i]);
|
|
449
|
+
explicitPathFlags.add('snapshotRoot');
|
|
450
|
+
break;
|
|
451
|
+
case '--migrated-dir':
|
|
452
|
+
options.migratedDir = path.resolve(argv[++i]);
|
|
453
|
+
explicitPathFlags.add('migratedDir');
|
|
454
|
+
break;
|
|
455
|
+
case '--export-dir':
|
|
456
|
+
options.exportDir = path.resolve(argv[++i]);
|
|
457
|
+
explicitPathFlags.add('exportDir');
|
|
458
|
+
break;
|
|
459
|
+
case '-h':
|
|
460
|
+
case '--help':
|
|
461
|
+
options.help = true;
|
|
462
|
+
break;
|
|
463
|
+
default:
|
|
464
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return options;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function printUsage() {
|
|
472
|
+
console.log(`Usage: beads-migrate-to-dolt [options]
|
|
473
|
+
|
|
474
|
+
Options:
|
|
475
|
+
--project-root <path> Project root containing .beads/
|
|
476
|
+
--legacy-backup-dir <path> Legacy JSONL backup directory
|
|
477
|
+
--snapshot-root <path> Where rollback snapshots are stored
|
|
478
|
+
--migrated-dir <path> Working directory for migrated Beads state
|
|
479
|
+
--export-dir <path> Export directory used for parity verification
|
|
480
|
+
-h, --help Show this help message
|
|
481
|
+
`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function main() {
|
|
485
|
+
const options = parseCliArgs(process.argv.slice(2));
|
|
486
|
+
if (options.help) {
|
|
487
|
+
printUsage();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const result = await runLegacyBeadsMigration(options);
|
|
492
|
+
console.log(JSON.stringify(result, null, 2));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
496
|
+
main().catch((error) => {
|
|
497
|
+
console.error(error.message);
|
|
498
|
+
if (error.parity) {
|
|
499
|
+
console.error(JSON.stringify(error.parity, null, 2));
|
|
500
|
+
}
|
|
501
|
+
process.exitCode = 1;
|
|
502
|
+
});
|
|
503
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -u
|
|
3
|
+
|
|
4
|
+
exit_code=0
|
|
5
|
+
have_bd=0
|
|
6
|
+
have_gh=0
|
|
7
|
+
|
|
8
|
+
ok() {
|
|
9
|
+
printf 'OK %s - %s\n' "$1" "$2"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
fixed() {
|
|
13
|
+
printf 'FIXED %s - %s\n' "$1" "$2"
|
|
14
|
+
if [ "$exit_code" -lt 1 ]; then
|
|
15
|
+
exit_code=1
|
|
16
|
+
fi
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
action() {
|
|
20
|
+
printf 'ACTION %s - %s\n' "$1" "$2"
|
|
21
|
+
exit_code=2
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
snapshot_hooks_dir() {
|
|
25
|
+
hooks_dir="$1"
|
|
26
|
+
hooks_snapshot_dir="$(mktemp -d 2>/dev/null || mktemp -d -t forge-preflight-hooks)"
|
|
27
|
+
|
|
28
|
+
if [ -d "$hooks_dir" ]; then
|
|
29
|
+
cp -pR "$hooks_dir"/. "$hooks_snapshot_dir"/
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
restore_hooks_dir() {
|
|
34
|
+
hooks_dir="$1"
|
|
35
|
+
mkdir -p "$hooks_dir"
|
|
36
|
+
|
|
37
|
+
find "$hooks_dir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
|
38
|
+
|
|
39
|
+
if [ -d "$hooks_snapshot_dir" ]; then
|
|
40
|
+
cp -pR "$hooks_snapshot_dir"/. "$hooks_dir"/
|
|
41
|
+
rm -rf "$hooks_snapshot_dir"
|
|
42
|
+
fi
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
write_beads_config() {
|
|
46
|
+
mkdir -p .beads
|
|
47
|
+
{
|
|
48
|
+
printf '%s\n' 'issue-prefix: forge'
|
|
49
|
+
printf '\n'
|
|
50
|
+
printf '%s\n' 'database:'
|
|
51
|
+
printf '%s\n' ' backend: dolt'
|
|
52
|
+
printf '\n'
|
|
53
|
+
} > .beads/config.yaml
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
write_beads_gitignore() {
|
|
57
|
+
mkdir -p .beads
|
|
58
|
+
{
|
|
59
|
+
printf '%s\n' '# Dolt database files (binary, not suitable for git)'
|
|
60
|
+
printf '%s\n' 'dolt/'
|
|
61
|
+
printf '%s\n' '*.db'
|
|
62
|
+
printf '%s\n' '*.lock'
|
|
63
|
+
printf '\n'
|
|
64
|
+
} > .beads/.gitignore
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
safe_bd_init() {
|
|
68
|
+
hooks_dir="$(git rev-parse --git-path hooks 2>/dev/null || printf '.git/hooks')"
|
|
69
|
+
write_beads_config
|
|
70
|
+
write_beads_gitignore
|
|
71
|
+
snapshot_hooks_dir "$hooks_dir"
|
|
72
|
+
|
|
73
|
+
bd init --database forge --prefix forge >/dev/null 2>&1
|
|
74
|
+
init_status=$?
|
|
75
|
+
|
|
76
|
+
restore_hooks_dir "$hooks_dir"
|
|
77
|
+
return "$init_status"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
is_windows_shell() {
|
|
81
|
+
case "$(uname -s 2>/dev/null || printf unknown)" in
|
|
82
|
+
MINGW*|MSYS*|CYGWIN*)
|
|
83
|
+
return 0
|
|
84
|
+
;;
|
|
85
|
+
*)
|
|
86
|
+
return 1
|
|
87
|
+
;;
|
|
88
|
+
esac
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
check_tool() {
|
|
92
|
+
tool="$1"
|
|
93
|
+
hint="$2"
|
|
94
|
+
|
|
95
|
+
if command -v "$tool" >/dev/null 2>&1; then
|
|
96
|
+
ok "tool $tool" "found $(command -v "$tool")"
|
|
97
|
+
return 0
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
action "tool $tool" "missing"
|
|
101
|
+
if is_windows_shell; then
|
|
102
|
+
printf ' Windows hint: %s\n' "$hint"
|
|
103
|
+
else
|
|
104
|
+
printf ' Install hint: %s\n' "$hint"
|
|
105
|
+
fi
|
|
106
|
+
return 1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
check_tools() {
|
|
110
|
+
if check_tool "bd" "install Forge/Beads tooling, then rerun: bunx forge setup --quick"; then
|
|
111
|
+
have_bd=1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
check_tool "jq" "winget install jqlang.jq" || true
|
|
115
|
+
|
|
116
|
+
if check_tool "gh" "winget install GitHub.cli"; then
|
|
117
|
+
have_gh=1
|
|
118
|
+
fi
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
check_github_auth() {
|
|
122
|
+
if [ "$have_gh" -ne 1 ]; then
|
|
123
|
+
return 0
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
if gh auth status >/dev/null 2>&1; then
|
|
127
|
+
ok "github-auth" "gh auth status succeeded"
|
|
128
|
+
return 0
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
action "github-auth" "not logged in; run: gh auth login"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
check_beads() {
|
|
135
|
+
if [ "$have_bd" -ne 1 ]; then
|
|
136
|
+
return 0
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
init_ok=0
|
|
140
|
+
if bd list --json --limit 1 >/dev/null 2>&1; then
|
|
141
|
+
ok "beads-init" "Beads database is readable"
|
|
142
|
+
init_ok=1
|
|
143
|
+
else
|
|
144
|
+
if safe_bd_init; then
|
|
145
|
+
fixed "beads-init" "ran hook-preserving bd init --database forge --prefix forge"
|
|
146
|
+
init_ok=1
|
|
147
|
+
else
|
|
148
|
+
action "beads-init" "bd init failed; inspect Beads setup manually"
|
|
149
|
+
fi
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
if [ "$init_ok" -ne 1 ]; then
|
|
153
|
+
return 0
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
doctor_output="$(bd doctor --fix --yes 2>&1)"
|
|
157
|
+
doctor_status=$?
|
|
158
|
+
if [ "$doctor_status" -eq 0 ]; then
|
|
159
|
+
doctor_output_lower="$(printf '%s' "$doctor_output" | tr '[:upper:]' '[:lower:]')"
|
|
160
|
+
if [ "$exit_code" -eq 1 ] ||
|
|
161
|
+
[[ "$doctor_output_lower" == *fix* ]] ||
|
|
162
|
+
[[ "$doctor_output_lower" == *repair* ]] ||
|
|
163
|
+
[[ "$doctor_output_lower" == *migrat* ]] ||
|
|
164
|
+
[[ "$doctor_output_lower" == *updat* ]]; then
|
|
165
|
+
fixed "beads-doctor" "ran bd doctor --fix --yes after Beads repair"
|
|
166
|
+
else
|
|
167
|
+
ok "beads-doctor" "bd doctor --fix --yes succeeded"
|
|
168
|
+
fi
|
|
169
|
+
else
|
|
170
|
+
action "beads-doctor" "bd doctor --fix --yes failed; inspect Beads manually"
|
|
171
|
+
fi
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
main() {
|
|
175
|
+
check_tools
|
|
176
|
+
check_github_auth
|
|
177
|
+
check_beads
|
|
178
|
+
exit "$exit_code"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
main "$@"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
|
|
6
|
+
const { scoreIssues } = require('../lib/smart-status/scoring.js');
|
|
7
|
+
|
|
8
|
+
function readPayload() {
|
|
9
|
+
const raw = fs.readFileSync(0, 'utf8');
|
|
10
|
+
if (!raw.trim()) {
|
|
11
|
+
return { issues: [], epicStats: {} };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const payload = JSON.parse(raw);
|
|
15
|
+
return {
|
|
16
|
+
issues: Array.isArray(payload.issues) ? payload.issues : [],
|
|
17
|
+
epicStats: payload.epicStats && typeof payload.epicStats === 'object' ? payload.epicStats : {},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function main() {
|
|
22
|
+
try {
|
|
23
|
+
const payload = readPayload();
|
|
24
|
+
process.stdout.write(JSON.stringify(scoreIssues(payload.issues, { epicStats: payload.epicStats })));
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(`smart-status-score: ${error.message}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
main();
|