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.
Files changed (135) hide show
  1. package/.claude/commands/dev.md +2 -2
  2. package/.claude/commands/plan.md +2 -2
  3. package/.claude/commands/premerge.md +2 -2
  4. package/.claude/commands/review.md +5 -2
  5. package/.claude/commands/ship.md +6 -5
  6. package/.claude/commands/status.md +4 -4
  7. package/.claude/rules/greptile-review-process.md +4 -4
  8. package/.cline/workflows/dev.md +2 -2
  9. package/.cline/workflows/plan.md +2 -2
  10. package/.cline/workflows/premerge.md +2 -2
  11. package/.cline/workflows/review.md +5 -2
  12. package/.cline/workflows/ship.md +6 -5
  13. package/.cline/workflows/status.md +4 -4
  14. package/.codex/skills/dev/SKILL.md +2 -2
  15. package/.codex/skills/plan/SKILL.md +2 -2
  16. package/.codex/skills/premerge/SKILL.md +2 -2
  17. package/.codex/skills/review/SKILL.md +5 -2
  18. package/.codex/skills/ship/SKILL.md +6 -5
  19. package/.codex/skills/status/SKILL.md +4 -4
  20. package/.cursor/commands/dev.md +2 -2
  21. package/.cursor/commands/plan.md +2 -2
  22. package/.cursor/commands/premerge.md +2 -2
  23. package/.cursor/commands/review.md +5 -2
  24. package/.cursor/commands/ship.md +6 -5
  25. package/.cursor/commands/status.md +4 -4
  26. package/.github/prompts/dev.prompt.md +2 -2
  27. package/.github/prompts/plan.prompt.md +2 -2
  28. package/.github/prompts/premerge.prompt.md +2 -2
  29. package/.github/prompts/review.prompt.md +5 -2
  30. package/.github/prompts/ship.prompt.md +6 -5
  31. package/.github/prompts/status.prompt.md +4 -4
  32. package/.github/workflows/beads-to-github.yml +44 -11
  33. package/.github/workflows/github-to-beads.yml +11 -8
  34. package/.kilocode/workflows/dev.md +2 -2
  35. package/.kilocode/workflows/plan.md +2 -2
  36. package/.kilocode/workflows/premerge.md +2 -2
  37. package/.kilocode/workflows/review.md +5 -2
  38. package/.kilocode/workflows/ship.md +6 -5
  39. package/.kilocode/workflows/status.md +4 -4
  40. package/.opencode/commands/dev.md +2 -2
  41. package/.opencode/commands/plan.md +2 -2
  42. package/.opencode/commands/premerge.md +2 -2
  43. package/.opencode/commands/review.md +5 -2
  44. package/.opencode/commands/ship.md +6 -5
  45. package/.opencode/commands/status.md +4 -4
  46. package/.roo/commands/dev.md +2 -2
  47. package/.roo/commands/plan.md +2 -2
  48. package/.roo/commands/premerge.md +2 -2
  49. package/.roo/commands/review.md +5 -2
  50. package/.roo/commands/ship.md +6 -5
  51. package/.roo/commands/status.md +4 -4
  52. package/AGENTS.md +10 -9
  53. package/CLAUDE.md +12 -0
  54. package/README.md +12 -6
  55. package/bin/forge.js +24 -8
  56. package/docs/BEADS_GITHUB_SYNC.md +32 -2
  57. package/docs/EXAMPLES.md +22 -22
  58. package/docs/ROADMAP.md +3 -3
  59. package/docs/TOOLCHAIN.md +150 -160
  60. package/lib/agents/codex.plugin.json +3 -0
  61. package/lib/agents-config.js +18 -12
  62. package/lib/beads-bootstrap.js +225 -0
  63. package/lib/beads-health-check.js +55 -10
  64. package/lib/beads-setup.js +104 -28
  65. package/lib/beads-sync-scaffold.js +11 -6
  66. package/lib/codex-skills.js +54 -1
  67. package/lib/commands/_issue.js +11 -1
  68. package/lib/commands/issues.js +49 -0
  69. package/lib/commands/plan.js +5 -2
  70. package/lib/commands/recommend.js +22 -1
  71. package/lib/commands/setup.js +247 -27
  72. package/lib/commands/ship.js +188 -5
  73. package/lib/commands/status.js +187 -19
  74. package/lib/commands/team.js +11 -1
  75. package/lib/commands/test.js +125 -25
  76. package/lib/commands/validate.js +232 -9
  77. package/lib/commands/worktree.js +27 -54
  78. package/lib/dep-guard/keyword-ripple.js +184 -0
  79. package/lib/detect-worktree.js +9 -10
  80. package/lib/forge-issues.js +326 -0
  81. package/lib/issue-sync/authority.js +100 -0
  82. package/lib/issue-sync/github-pull.js +184 -0
  83. package/lib/issue-sync/import-primitives.js +98 -0
  84. package/lib/issue-sync/legacy-link-bridge.js +436 -0
  85. package/lib/issue-sync/link-store.js +292 -0
  86. package/lib/issue-sync/project-github.js +123 -0
  87. package/lib/issue-sync/reconcile.js +195 -0
  88. package/lib/issue-sync/schema.js +126 -0
  89. package/lib/lefthook-check.js +5 -2
  90. package/lib/project-memory.js +564 -0
  91. package/lib/runtime-health.js +100 -12
  92. package/lib/setup-action-log.js +2 -0
  93. package/lib/setup-summary-renderer.js +15 -11
  94. package/lib/smart-status/conflicts.js +205 -0
  95. package/lib/smart-status/scoring.js +177 -0
  96. package/lib/status/beads-snapshot.js +102 -0
  97. package/lib/status/presenter.js +65 -0
  98. package/lib/workflow/enforce-stage.js +14 -8
  99. package/lib/workflow/state-manager.js +349 -0
  100. package/package.json +12 -4
  101. package/scripts/beads-context.sh +124 -5
  102. package/scripts/beads-context.test.js +21 -4
  103. package/scripts/beads-migrate-to-dolt.sh +7 -0
  104. package/scripts/beads-upgrade-smoke.sh +263 -0
  105. package/scripts/behavioral-judge.sh +115 -11
  106. package/scripts/benchmark.js +349 -63
  107. package/scripts/dep-guard-analyze.js +52 -17
  108. package/scripts/dep-guard-keyword-ripple.js +29 -0
  109. package/scripts/dep-guard-render-review.js +86 -0
  110. package/scripts/dep-guard.sh +45 -222
  111. package/scripts/forge-team/lib/hooks.sh +1 -1
  112. package/scripts/forge-team/lib/sync-github.sh +160 -28
  113. package/scripts/forge-team/lib/verify.sh +1 -1
  114. package/scripts/forge-team/lib/workload.sh +56 -27
  115. package/scripts/forge-team/tests/sync-github.test.sh +195 -58
  116. package/scripts/forge-team/tests/workload.test.sh +35 -4
  117. package/scripts/github-beads-sync/index.mjs +122 -98
  118. package/scripts/github-beads-sync/mapping.mjs +54 -0
  119. package/scripts/github-beads-sync/reverse-sync-cli.mjs +2 -2
  120. package/scripts/github-beads-sync/reverse-sync.mjs +31 -7
  121. package/scripts/github-beads-sync/run-bd.mjs +4 -2
  122. package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
  123. package/scripts/preflight.sh +181 -0
  124. package/scripts/smart-status-score.js +31 -0
  125. package/scripts/smart-status-sessions.js +51 -0
  126. package/scripts/smart-status.sh +84 -330
  127. package/scripts/sync-agentic-workflow.js +48 -0
  128. package/scripts/sync-utils.sh +39 -0
  129. package/scripts/test-ci-shard.js +244 -0
  130. package/scripts/test-dashboard.js +188 -52
  131. package/scripts/test-full-suite.js +186 -0
  132. package/scripts/test-profile.js +278 -0
  133. package/scripts/test.js +335 -38
  134. package/scripts/validate.js +143 -0
  135. package/scripts/validate.sh +18 -1
@@ -0,0 +1,225 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('node:child_process');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { safeBeadsInit } = require('./beads-setup');
7
+
8
+ function isIgnorableFsError(error) {
9
+ return ['EACCES', 'ENOENT', 'ENOTDIR', 'EPERM'].includes(error?.code);
10
+ }
11
+
12
+ function isRecoverableCommandError(error) {
13
+ return error?.code === 'ENOENT' || typeof error?.status === 'number';
14
+ }
15
+
16
+ function hasBackupJsonl(backupDir, fsApi) {
17
+ try {
18
+ return fsApi.readdirSync(backupDir).some((entry) => entry.endsWith('.jsonl'));
19
+ } catch (err) {
20
+ if (isIgnorableFsError(err)) {
21
+ return false;
22
+ }
23
+ throw err;
24
+ }
25
+ }
26
+
27
+ function resolveMainWorktree(projectRoot, exec) {
28
+ try {
29
+ const commonDir = exec('git', ['rev-parse', '--git-common-dir'], {
30
+ cwd: projectRoot,
31
+ encoding: 'utf8',
32
+ stdio: 'pipe'
33
+ }).trim();
34
+ return path.resolve(projectRoot, commonDir, '..');
35
+ } catch (err) {
36
+ if (isRecoverableCommandError(err)) {
37
+ return projectRoot;
38
+ }
39
+ throw err;
40
+ }
41
+ }
42
+
43
+ function isRecoverableBeadsError(error) {
44
+ const message = error?.message ?? String(error ?? '');
45
+ return (
46
+ /database(?:\s+[^\s]+)?\s+not found/i.test(message) ||
47
+ message.includes('failed to open database') ||
48
+ message.includes('no beads configuration found')
49
+ );
50
+ }
51
+
52
+ function readMetadataDatabaseName(projectRoot, fsApi) {
53
+ const metadataPath = path.resolve(projectRoot, '.beads', 'metadata.json');
54
+ if (!fsApi.existsSync(metadataPath)) {
55
+ return null;
56
+ }
57
+
58
+ try {
59
+ const metadata = JSON.parse(fsApi.readFileSync(metadataPath, 'utf8'));
60
+ if (typeof metadata.dolt_database === 'string' && metadata.dolt_database.trim()) {
61
+ return metadata.dolt_database.trim();
62
+ }
63
+ } catch (err) {
64
+ if (err instanceof SyntaxError || isIgnorableFsError(err)) {
65
+ return null;
66
+ }
67
+ throw err;
68
+ }
69
+
70
+ return null;
71
+ }
72
+
73
+ function stageExistingBeadsDest(beadsDest, fsApi) {
74
+ if (!fsApi.existsSync(beadsDest)) {
75
+ return null;
76
+ }
77
+
78
+ const stagedDest = `${beadsDest}.bootstrap-backup`;
79
+ if (fsApi.existsSync(stagedDest) && typeof fsApi.rmSync === 'function') {
80
+ fsApi.rmSync(stagedDest, { recursive: true, force: true });
81
+ }
82
+
83
+ if (typeof fsApi.renameSync === 'function') {
84
+ fsApi.renameSync(beadsDest, stagedDest);
85
+ return stagedDest;
86
+ }
87
+
88
+ if (typeof fsApi.rmSync === 'function') {
89
+ fsApi.rmSync(beadsDest, { recursive: true, force: true });
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ function restoreStagedBeadsDest(beadsDest, stagedDest, fsApi) {
96
+ if (!stagedDest || fsApi.existsSync(beadsDest) || typeof fsApi.renameSync !== 'function') {
97
+ return;
98
+ }
99
+
100
+ fsApi.renameSync(stagedDest, beadsDest);
101
+ }
102
+
103
+ function cleanupStagedBeadsDest(stagedDest, fsApi) {
104
+ if (!stagedDest || !fsApi.existsSync(stagedDest) || typeof fsApi.rmSync !== 'function') {
105
+ return;
106
+ }
107
+
108
+ fsApi.rmSync(stagedDest, { recursive: true, force: true });
109
+ }
110
+
111
+ function tryLinkBeadsSource(beadsSource, beadsDest, platform, fsApi) {
112
+ if (!fsApi.existsSync(beadsSource)) {
113
+ return false;
114
+ }
115
+
116
+ const stagedDest = stageExistingBeadsDest(beadsDest, fsApi);
117
+
118
+ try {
119
+ if (platform === 'win32') {
120
+ fsApi.symlinkSync(beadsSource, beadsDest, 'junction');
121
+ } else {
122
+ fsApi.symlinkSync(beadsSource, beadsDest);
123
+ }
124
+
125
+ cleanupStagedBeadsDest(stagedDest, fsApi);
126
+ return true;
127
+ } catch (symlinkErr) {
128
+ restoreStagedBeadsDest(beadsDest, stagedDest, fsApi);
129
+ if (symlinkErr.code !== 'EPERM') {
130
+ throw symlinkErr;
131
+ }
132
+ return false;
133
+ }
134
+ }
135
+
136
+ function buildInitArgs(mainProjectRoot, projectRoot, fsApi) {
137
+ const metadataDatabaseName = readMetadataDatabaseName(mainProjectRoot, fsApi)
138
+ || readMetadataDatabaseName(projectRoot, fsApi);
139
+ const initArgs = ['init', '--force'];
140
+ if (metadataDatabaseName) {
141
+ initArgs.push('--database', metadataDatabaseName);
142
+ }
143
+
144
+ return initArgs;
145
+ }
146
+
147
+ function runFreshInit(projectRoot, mainProjectRoot, beadsSource, initArgs, exec, runSafeBeadsInit, fsApi) {
148
+ const safeInitResult = runSafeBeadsInit(projectRoot, {
149
+ prefix: path.basename(mainProjectRoot),
150
+ execBdInit: (root) => {
151
+ exec('bd', initArgs, { cwd: root, stdio: 'pipe' });
152
+ }
153
+ });
154
+
155
+ if (!safeInitResult.success) {
156
+ const initMessage = safeInitResult.errors?.[0]
157
+ || safeInitResult.warnings?.[0]
158
+ || 'unknown safeBeadsInit failure';
159
+ return {
160
+ success: false,
161
+ strategy: 'fresh-init-failed',
162
+ warning: `Beads bootstrap fresh init failed: ${initMessage}`
163
+ };
164
+ }
165
+
166
+ if (safeInitResult.skipped) {
167
+ return {
168
+ success: true,
169
+ strategy: 'existing-state',
170
+ warning: 'Beads bootstrap reused existing initialized state'
171
+ };
172
+ }
173
+
174
+ const backupDir = path.resolve(beadsSource, 'backup');
175
+ if (hasBackupJsonl(backupDir, fsApi)) {
176
+ try {
177
+ exec('bd', ['backup', 'restore', backupDir], { cwd: projectRoot, stdio: 'pipe' });
178
+ return {
179
+ success: true,
180
+ strategy: 'backup-restore',
181
+ warning: 'Beads bootstrap restored from backup'
182
+ };
183
+ } catch (restoreErr) {
184
+ return {
185
+ success: true,
186
+ strategy: 'fresh-init',
187
+ warning: `Beads bootstrap initialized fresh after backup restore failed: ${restoreErr.message}`
188
+ };
189
+ }
190
+ }
191
+
192
+ return {
193
+ success: true,
194
+ strategy: 'fresh-init',
195
+ warning: 'Beads bootstrap initialized fresh (no backup found)'
196
+ };
197
+ }
198
+
199
+ function bootstrapBeads(projectRoot, options = {}) {
200
+ const exec = options._exec || execFileSync;
201
+ const fsApi = options._fs || fs;
202
+ const platform = options._platform || process.platform;
203
+ const runSafeBeadsInit = options._safeBeadsInit || safeBeadsInit;
204
+ const mainProjectRoot = options.mainProjectRoot || resolveMainWorktree(projectRoot, exec);
205
+ const beadsSource = path.resolve(mainProjectRoot, '.beads');
206
+ const beadsDest = path.resolve(projectRoot, '.beads');
207
+
208
+ if (beadsSource === beadsDest) {
209
+ return { success: true, strategy: 'in-place', warning: null };
210
+ }
211
+
212
+ if (tryLinkBeadsSource(beadsSource, beadsDest, platform, fsApi)) {
213
+ return { success: true, strategy: 'linked', warning: null };
214
+ }
215
+
216
+ const initArgs = buildInitArgs(mainProjectRoot, projectRoot, fsApi);
217
+ return runFreshInit(projectRoot, mainProjectRoot, beadsSource, initArgs, exec, runSafeBeadsInit, fsApi);
218
+ }
219
+
220
+ module.exports = {
221
+ bootstrapBeads,
222
+ hasBackupJsonl,
223
+ isRecoverableBeadsError,
224
+ resolveMainWorktree,
225
+ };
@@ -10,6 +10,7 @@
10
10
  const { execFileSync } = require('node:child_process');
11
11
  const fs = require('node:fs');
12
12
  const path = require('node:path');
13
+ const { bootstrapBeads, isRecoverableBeadsError } = require('./beads-bootstrap');
13
14
 
14
15
  /**
15
16
  * @typedef {Object} HealthCheckResult
@@ -49,6 +50,46 @@ function cleanupTestIssue(jsonlPath, issueId) {
49
50
  fs.writeFileSync(jsonlPath, filtered.length > 0 ? filtered.join('\n') + '\n' : '');
50
51
  }
51
52
 
53
+ function appendWarning(existingWarning, nextWarning) {
54
+ return existingWarning ? `${existingWarning}; ${nextWarning}` : nextWarning;
55
+ }
56
+
57
+ function createHealthCheckIssue(exec, execOpts) {
58
+ return exec('bd', [
59
+ 'create',
60
+ '--title=Setup verification',
61
+ '--type=task',
62
+ '--priority=4'
63
+ ], execOpts);
64
+ }
65
+
66
+ function createIssueWithRecovery(projectRoot, exec, execOpts, bootstrap) {
67
+ try {
68
+ return { output: createHealthCheckIssue(exec, execOpts), warning: null };
69
+ } catch (createErr) {
70
+ if (!isRecoverableBeadsError(createErr)) {
71
+ throw createErr;
72
+ }
73
+
74
+ const bootstrapResult = bootstrap(projectRoot);
75
+ if (!bootstrapResult?.success) {
76
+ const extra = bootstrapResult?.warning ? `; ${bootstrapResult.warning}` : '';
77
+ return {
78
+ error: `${createErr.message}${extra}`
79
+ };
80
+ }
81
+
82
+ const bootstrapWarning = bootstrapResult.warning
83
+ ? `bootstrap warning: ${bootstrapResult.warning}`
84
+ : `bootstrap warning: recovered via ${bootstrapResult.strategy}`;
85
+
86
+ return {
87
+ output: createHealthCheckIssue(exec, execOpts),
88
+ warning: bootstrapWarning
89
+ };
90
+ }
91
+ }
92
+
52
93
  /**
53
94
  * Run a Beads health check smoke test.
54
95
  *
@@ -62,6 +103,7 @@ function cleanupTestIssue(jsonlPath, issueId) {
62
103
  */
63
104
  function beadsHealthCheck(projectRoot, options = {}) {
64
105
  const exec = options._exec || execFileSync;
106
+ const bootstrap = options._bootstrap || ((root) => bootstrapBeads(root, { _exec: exec }));
65
107
  const execOpts = { cwd: projectRoot, encoding: 'utf8' };
66
108
 
67
109
  /** @type {string|undefined} */
@@ -71,14 +113,18 @@ function beadsHealthCheck(projectRoot, options = {}) {
71
113
  /** @type {string|null} */
72
114
  let issueId;
73
115
  try {
74
- const createOutput = exec('bd', [
75
- 'create',
76
- '--title=Setup verification',
77
- '--type=task',
78
- '--priority=4'
79
- ], execOpts);
116
+ const createResult = createIssueWithRecovery(projectRoot, exec, execOpts, bootstrap);
117
+ if (createResult.error) {
118
+ return {
119
+ healthy: false,
120
+ failedStep: 'create',
121
+ error: createResult.error
122
+ };
123
+ }
124
+
125
+ warning = createResult.warning || warning;
80
126
 
81
- issueId = parseIssueId(createOutput);
127
+ issueId = parseIssueId(createResult.output);
82
128
  if (!issueId) {
83
129
  return {
84
130
  healthy: false,
@@ -120,7 +166,7 @@ function beadsHealthCheck(projectRoot, options = {}) {
120
166
  exec('bd', ['sync'], execOpts);
121
167
  } catch (syncErr) {
122
168
  // Sync failure is non-fatal (no remote configured is common)
123
- warning = `bd sync warning: ${syncErr.message}`;
169
+ warning = appendWarning(warning, `bd sync warning: ${syncErr.message}`);
124
170
  }
125
171
 
126
172
  // Step d: Remove the test issue from JSONL
@@ -128,8 +174,7 @@ function beadsHealthCheck(projectRoot, options = {}) {
128
174
  cleanupTestIssue(path.join(projectRoot, '.beads', 'issues.jsonl'), issueId);
129
175
  } catch (cleanupErr) {
130
176
  // Cleanup failure is non-fatal
131
- const cleanupWarning = `cleanup warning: ${cleanupErr.message}`;
132
- warning = warning ? `${warning}; ${cleanupWarning}` : cleanupWarning;
177
+ warning = appendWarning(warning, `cleanup warning: ${cleanupErr.message}`);
133
178
  }
134
179
 
135
180
  return {
@@ -2,7 +2,7 @@
2
2
  * Utilities for configuring Beads during `forge setup`.
3
3
  *
4
4
  * Handles prefix sanitization, config/gitignore generation,
5
- * initialization detection, and issues.jsonl pre-seeding.
5
+ * initialization detection, and legacy JSONL compatibility shims.
6
6
  *
7
7
  * @module beads-setup
8
8
  */
@@ -10,6 +10,15 @@
10
10
  const fs = require('node:fs');
11
11
  const path = require('node:path');
12
12
  const { spawnSync } = require('node:child_process');
13
+ const { secureExecFileSync } = require('./shell-utils');
14
+
15
+ function isIgnorableFsError(error) {
16
+ return ['EACCES', 'ENOENT', 'ENOTDIR', 'EPERM'].includes(error?.code);
17
+ }
18
+
19
+ function isRecoverableCommandError(error) {
20
+ return error?.code === 'ENOENT' || typeof error?.status === 'number';
21
+ }
13
22
 
14
23
  /**
15
24
  * Resolve a command to its full path to avoid relying on inherited PATH.
@@ -24,7 +33,10 @@ function resolveCommand(command) {
24
33
  if (result.status === 0 && result.stdout) {
25
34
  return result.stdout.trim().split(/\r?\n/)[0].trim();
26
35
  }
27
- } catch (_e) {
36
+ } catch (error) {
37
+ if (!isRecoverableCommandError(error)) {
38
+ throw error;
39
+ }
28
40
  // Expected: command resolution may fail if 'which'/'where.exe' is unavailable — fall back to bare command name
29
41
  }
30
42
  return command;
@@ -108,10 +120,15 @@ function writeBeadsGitignore(projectRoot) {
108
120
  /**
109
121
  * Check whether Beads is properly initialized in a project.
110
122
  *
111
- * Returns `true` only when all of the following are present:
123
+ * Returns `true` when all of the following are present:
112
124
  * - `.beads/` directory exists
113
125
  * - `.beads/config.yaml` exists and contains an `issue-prefix` key
114
- * - `.beads/issues.jsonl` exists
126
+ * - the config points to either:
127
+ * - a legacy SQLite tracker with legacy data markers present, or
128
+ * - a Dolt-backed tracker with at least one post-`bd init` marker present
129
+ *
130
+ * Legacy SQLite-backed Beads installs also count as initialized so `forge setup`
131
+ * does not run `bd init` over an existing tracker before migration.
115
132
  *
116
133
  * @param {string} projectRoot - Absolute path to the project root
117
134
  * @returns {boolean} `true` if Beads is fully initialized
@@ -119,34 +136,74 @@ function writeBeadsGitignore(projectRoot) {
119
136
  function isBeadsInitialized(projectRoot) {
120
137
  const beadsDir = path.join(projectRoot, '.beads');
121
138
  const configPath = path.join(beadsDir, 'config.yaml');
122
- const jsonlPath = path.join(beadsDir, 'issues.jsonl');
123
139
 
124
140
  if (!fs.existsSync(beadsDir)) return false;
125
141
  if (!fs.existsSync(configPath)) return false;
126
- if (!fs.existsSync(jsonlPath)) return false;
127
142
 
128
143
  const configContent = fs.readFileSync(configPath, 'utf8');
129
144
  if (!configContent.includes('issue-prefix:')) return false;
130
145
 
131
- return true;
146
+ const hasLegacyState = [
147
+ 'issues.jsonl',
148
+ 'backup',
149
+ 'bd.db',
150
+ 'beads.db',
151
+ 'db.sqlite'
152
+ ].some((entry) => fs.existsSync(path.join(beadsDir, entry)));
153
+ const hasDoltMetadata = fs.existsSync(path.join(beadsDir, 'metadata.json'));
154
+ const hasDoltCompanionState = [
155
+ 'README.md',
156
+ 'hooks',
157
+ 'redirect'
158
+ ].some((entry) => fs.existsSync(path.join(beadsDir, entry)));
159
+ const hasDoltState = hasDoltMetadata && hasDoltCompanionState;
160
+
161
+ if (configContent.includes('backend: sqlite')) {
162
+ return hasLegacyState;
163
+ }
164
+
165
+ if (configContent.includes('backend: dolt')) {
166
+ return hasDoltState;
167
+ }
168
+
169
+ return hasLegacyState || hasDoltState;
170
+ }
171
+
172
+ function readBeadsDatabaseName(projectRoot) {
173
+ const metadataPath = path.join(projectRoot, '.beads', 'metadata.json');
174
+ if (!fs.existsSync(metadataPath)) {
175
+ return null;
176
+ }
177
+
178
+ try {
179
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
180
+ if (typeof metadata.dolt_database === 'string' && metadata.dolt_database.trim()) {
181
+ return metadata.dolt_database.trim();
182
+ }
183
+ if (typeof metadata.database === 'string' && metadata.database.trim()) {
184
+ return metadata.database.trim();
185
+ }
186
+ } catch (error) {
187
+ if (error instanceof SyntaxError || isIgnorableFsError(error)) {
188
+ return null;
189
+ }
190
+ throw error;
191
+ }
192
+
193
+ return null;
132
194
  }
133
195
 
134
196
  /**
135
- * Ensure `issues.jsonl` exists inside `.beads/`.
197
+ * Legacy compatibility shim for the removed JSONL pre-seed workaround.
136
198
  *
137
- * Creates the `.beads/` directory and an empty `issues.jsonl` if either
138
- * is missing. Leaves an existing `issues.jsonl` untouched.
199
+ * Beads latest no longer requires `.beads/issues.jsonl` during init, so
200
+ * this helper intentionally does nothing. It remains exported because some
201
+ * older tests and setup paths still import it.
139
202
  *
140
- * @param {string} projectRoot - Absolute path to the project root
203
+ * @param {string} _projectRoot - Absolute path to the project root
141
204
  */
142
- function preSeedJsonl(projectRoot) {
143
- const beadsDir = path.join(projectRoot, '.beads');
144
- fs.mkdirSync(beadsDir, { recursive: true });
145
-
146
- const jsonlPath = path.join(beadsDir, 'issues.jsonl');
147
- if (!fs.existsSync(jsonlPath)) {
148
- fs.writeFileSync(jsonlPath, '', 'utf8');
149
- }
205
+ function preSeedJsonl(_projectRoot) {
206
+ // Legacy no-op: Dolt-backed Beads no longer requires JSONL pre-seeding.
150
207
  }
151
208
 
152
209
  // ---------------------------------------------------------------------------
@@ -212,6 +269,31 @@ function restoreDirectory(dirPath, snapshot) {
212
269
  }
213
270
  }
214
271
 
272
+ function resolveHooksDir(projectRoot) {
273
+ const fallbackHooksDir = path.join(projectRoot, '.git', 'hooks');
274
+
275
+ try {
276
+ const gitHooksPath = secureExecFileSync(resolveCommand('git'), ['rev-parse', '--git-path', 'hooks'], {
277
+ cwd: projectRoot,
278
+ encoding: 'utf8',
279
+ stdio: 'pipe'
280
+ }).trim();
281
+
282
+ if (!gitHooksPath) {
283
+ return fallbackHooksDir;
284
+ }
285
+
286
+ return path.isAbsolute(gitHooksPath)
287
+ ? gitHooksPath
288
+ : path.resolve(projectRoot, gitHooksPath);
289
+ } catch (error) {
290
+ if (isRecoverableCommandError(error)) {
291
+ return fallbackHooksDir;
292
+ }
293
+ throw error;
294
+ }
295
+ }
296
+
215
297
  /**
216
298
  * Default bd init executor — calls `execFileSync('bd', ['init'])`.
217
299
  *
@@ -237,7 +319,7 @@ function defaultExecBdInit(projectRoot) {
237
319
  * e. Run bd init (via execBdInit callback or default execFileSync)
238
320
  * f. Restore .git/hooks/ from snapshot
239
321
  * g. Restore lefthook hooks if callback provided
240
- * h. Pre-seed issues.jsonl
322
+ * h. Skip the removed issues.jsonl pre-seed workaround
241
323
  *
242
324
  * @param {string} projectRoot - Absolute path to the project root
243
325
  * @param {Object} [options={}] - Configuration options
@@ -279,7 +361,7 @@ function safeBeadsInit(projectRoot, options = {}) {
279
361
  }
280
362
 
281
363
  // (d) Snapshot current .git/hooks/ directory
282
- const hooksDir = path.join(projectRoot, '.git', 'hooks');
364
+ const hooksDir = resolveHooksDir(projectRoot);
283
365
  const hooksSnapshot = snapshotDirectory(hooksDir);
284
366
 
285
367
  // (e) Run bd init — wrapped in try/catch
@@ -316,13 +398,6 @@ function safeBeadsInit(projectRoot, options = {}) {
316
398
  }
317
399
  }
318
400
 
319
- // (h) Pre-seed JSONL if empty
320
- try {
321
- preSeedJsonl(projectRoot);
322
- } catch (err) {
323
- warnings.push(`Failed to pre-seed issues.jsonl: ${err.message}`);
324
- }
325
-
326
401
  return {
327
402
  success: true,
328
403
  skipped: false,
@@ -336,6 +411,7 @@ module.exports = {
336
411
  writeBeadsConfig,
337
412
  writeBeadsGitignore,
338
413
  isBeadsInitialized,
414
+ readBeadsDatabaseName,
339
415
  preSeedJsonl,
340
416
  safeBeadsInit
341
417
  };
@@ -4,6 +4,8 @@ const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const { execFileSync } = require('node:child_process');
6
6
 
7
+ const DEFAULT_BEADS_VERSION = '1.0.0';
8
+
7
9
  /**
8
10
  * Detect the default branch of the repository.
9
11
  *
@@ -60,7 +62,7 @@ function detectDefaultBranch(projectRoot, options = {}) {
60
62
  *
61
63
  * Strategy (in order):
62
64
  * 1. `bd --version` -> parse version string (e.g. "beads version 0.52.0" -> "0.52.0")
63
- * 2. Fall back to known-good default `'0.49.1'`
65
+ * 2. Fall back to the current repo baseline release
64
66
  *
65
67
  * @param {object} [options] - Options object.
66
68
  * @param {Function} [options._exec] - Injected execFileSync for testing.
@@ -83,7 +85,7 @@ function detectBeadsVersion(options = {}) {
83
85
  // Expected: 'bd --version' fails when bd CLI is not installed — fall through to default version
84
86
  }
85
87
 
86
- return '0.49.1';
88
+ return DEFAULT_BEADS_VERSION;
87
89
  }
88
90
 
89
91
  /**
@@ -126,10 +128,12 @@ function templateWorkflows(workflowDir, branch, beadsVersion, createdFiles = [])
126
128
  /branches:\s*\[master\]/g,
127
129
  `branches: [${branch}]`
128
130
  );
129
- content = content.replaceAll(
130
- /BD_VERSION="0\.49\.1"/g,
131
- `BD_VERSION="${beadsVersion}"`
132
- );
131
+ for (const version of ['0.49.1', DEFAULT_BEADS_VERSION, '__FORGE_BEADS_VERSION__']) {
132
+ content = content.replaceAll(
133
+ `BD_VERSION="${version}"`,
134
+ `BD_VERSION="${beadsVersion}"`
135
+ );
136
+ }
133
137
  // Only write if content actually changed
134
138
  if (content !== original) {
135
139
  fs.writeFileSync(filePath, content);
@@ -253,6 +257,7 @@ function scaffoldBeadsSync(projectRoot, packageDir, _options = {}) {
253
257
  }
254
258
 
255
259
  module.exports = {
260
+ DEFAULT_BEADS_VERSION,
256
261
  detectDefaultBranch,
257
262
  detectBeadsVersion,
258
263
  templateWorkflows,
@@ -1,4 +1,5 @@
1
1
  const fs = require('node:fs');
2
+ const os = require('node:os');
2
3
  const path = require('node:path');
3
4
 
4
5
  function injectForgeAdapter(content, commandName) {
@@ -37,7 +38,6 @@ function listCodexSkillEntries(sourceRoot) {
37
38
 
38
39
  return {
39
40
  commandName,
40
- dir: `.codex/skills/${commandName}/`,
41
41
  filename: 'SKILL.md',
42
42
  content: injectForgeAdapter(fs.readFileSync(sourceFile, 'utf8'), commandName),
43
43
  };
@@ -45,6 +45,59 @@ function listCodexSkillEntries(sourceRoot) {
45
45
  .filter(Boolean);
46
46
  }
47
47
 
48
+ function resolveCodexHome(options = {}) {
49
+ const env = options.env || process.env;
50
+ const homeDir = options.homeDir || os.homedir();
51
+ const explicitHome = String(env.CODEX_HOME || '').trim();
52
+
53
+ if (explicitHome) {
54
+ return path.resolve(explicitHome);
55
+ }
56
+
57
+ return path.join(homeDir, '.codex');
58
+ }
59
+
60
+ function resolveCodexSkillsInstallDir(options = {}) {
61
+ return path.join(resolveCodexHome(options), 'skills');
62
+ }
63
+
64
+ function formatCodexSkillsInstallDir(options = {}) {
65
+ const installDir = resolveCodexSkillsInstallDir(options);
66
+ const homeDir = path.resolve(options.homeDir || os.homedir());
67
+ const explicitHome = String((options.env || process.env).CODEX_HOME || '').trim();
68
+ const normalizedInstallDir = installDir.replace(/\\/g, '/');
69
+
70
+ if (explicitHome) {
71
+ const explicitInstallDir = path.join(path.resolve(explicitHome), 'skills').replace(/\\/g, '/');
72
+ if (normalizedInstallDir === explicitInstallDir) {
73
+ return '$CODEX_HOME/skills';
74
+ }
75
+ }
76
+
77
+ const defaultInstallDir = path.join(homeDir, '.codex', 'skills').replace(/\\/g, '/');
78
+ if (normalizedInstallDir === defaultInstallDir) {
79
+ return '~/.codex/skills';
80
+ }
81
+
82
+ return normalizedInstallDir;
83
+ }
84
+
85
+ function buildCodexSkillInstallPlan(sourceRoot, options = {}) {
86
+ const installDir = resolveCodexSkillsInstallDir(options);
87
+ const displayRoot = formatCodexSkillsInstallDir(options);
88
+
89
+ return listCodexSkillEntries(sourceRoot).map((entry) => ({
90
+ ...entry,
91
+ absolutePath: path.join(installDir, entry.commandName, entry.filename),
92
+ displayPath: `${displayRoot}/${entry.commandName}/${entry.filename}`,
93
+ displayRoot,
94
+ }));
95
+ }
96
+
48
97
  module.exports = {
98
+ buildCodexSkillInstallPlan,
99
+ formatCodexSkillsInstallDir,
49
100
  listCodexSkillEntries,
101
+ resolveCodexHome,
102
+ resolveCodexSkillsInstallDir,
50
103
  };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { execFileSync } = require('node:child_process');
4
+ const { runIssueOperation } = require('../forge-issues');
4
5
 
5
6
  const SUBCOMMANDS = {
6
7
  create: {
@@ -110,14 +111,23 @@ function buildBdArgs(subcommand, rawArgs) {
110
111
  return spec.buildBdArgs(args);
111
112
  }
112
113
 
114
+ const WRITE_SUBCOMMANDS = new Set(['create', 'update', 'claim', 'close']);
115
+
113
116
  async function runIssueSubcommand(subcommand, args, projectRoot, opts = {}) {
114
- const exec = opts._exec || execFileSync;
115
117
  const bdArgs = buildBdArgs(subcommand, args);
116
118
 
117
119
  if (!Array.isArray(bdArgs)) {
118
120
  return { success: false, error: bdArgs.error };
119
121
  }
120
122
 
123
+ if (WRITE_SUBCOMMANDS.has(subcommand)) {
124
+ const runner = opts.runIssueOperation || runIssueOperation;
125
+ const operation = subcommand === 'claim' ? 'update' : subcommand;
126
+ return runner(operation, bdArgs.slice(1), projectRoot, opts);
127
+ }
128
+
129
+ const exec = opts._exec || execFileSync;
130
+
121
131
  try {
122
132
  exec('bd', bdArgs, getExecOptions(projectRoot));
123
133
  return { success: true, subcommand };