forge-workflow 0.1.0-beta.3 → 0.1.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/AGENTS.md +14 -7
  2. package/CHANGELOG.md +43 -1
  3. package/README.md +6 -2
  4. package/bin/forge-cmd.js +21 -1
  5. package/bin/forge.js +16 -369
  6. package/docs/INDEX.md +1 -1
  7. package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
  8. package/docs/guides/MIGRATION.md +4 -4
  9. package/docs/guides/SETUP.md +16 -16
  10. package/docs/reference/COMMANDS.md +9 -4
  11. package/docs/reference/INSIGHTS_RECAP.md +9 -20
  12. package/docs/reference/RELEASE.md +5 -3
  13. package/docs/reference/TOOLCHAIN.md +8 -0
  14. package/docs/reference/protected-state-surfaces.md +4 -4
  15. package/docs/reference/shepherd.md +117 -17
  16. package/lefthook.yml +12 -0
  17. package/lib/activation/ensure-forge-home.js +33 -15
  18. package/lib/adapters/greptile-review-adapter.js +1 -1
  19. package/lib/adapters/pr-state-adapter.js +397 -100
  20. package/lib/agents-config.js +5 -0
  21. package/lib/audit-evidence.js +71 -110
  22. package/lib/capped-jsonl-log.js +236 -0
  23. package/lib/commands/_issue.js +31 -46
  24. package/lib/commands/_manifest.js +1 -1
  25. package/lib/commands/_registry.js +2 -2
  26. package/lib/commands/_resolve-command-opts.js +36 -29
  27. package/lib/commands/claim.js +2 -4
  28. package/lib/commands/clean.js +196 -32
  29. package/lib/commands/dev.js +4 -33
  30. package/lib/commands/hooks.js +358 -13
  31. package/lib/commands/insights.js +8 -3
  32. package/lib/commands/merge.js +600 -40
  33. package/lib/commands/plan.js +23 -115
  34. package/lib/commands/pr.js +1 -1
  35. package/lib/commands/preflight.js +11 -2
  36. package/lib/commands/prime.js +23 -3
  37. package/lib/commands/push.js +41 -51
  38. package/lib/commands/recall.js +60 -16
  39. package/lib/commands/recap.js +6 -1
  40. package/lib/commands/release.js +18 -4
  41. package/lib/commands/serve.js +5 -2
  42. package/lib/commands/setup.js +191 -95
  43. package/lib/commands/shepherd.js +49 -4
  44. package/lib/commands/ship.js +22 -23
  45. package/lib/commands/skill.js +383 -0
  46. package/lib/commands/status.js +54 -33
  47. package/lib/commands/test.js +56 -34
  48. package/lib/commands/worktree.js +247 -43
  49. package/lib/core/runtime-graph.js +89 -15
  50. package/lib/doc-assertions.js +297 -0
  51. package/lib/existing-tdd-gate.js +253 -0
  52. package/lib/forge-context.js +1 -4
  53. package/lib/forge-issues.js +64 -491
  54. package/lib/git-defaults.js +56 -0
  55. package/lib/harness-capability-matrix.js +5 -5
  56. package/lib/hook-renderer.js +147 -16
  57. package/lib/insights.js +96 -80
  58. package/lib/issue-backend.js +42 -3
  59. package/lib/kernel/backing-issue.js +14 -2
  60. package/lib/kernel/broker.js +44 -0
  61. package/lib/kernel/cli-broker-factory.js +12 -1
  62. package/lib/kernel/close-on-merge.js +154 -0
  63. package/lib/kernel/fs-class.js +42 -25
  64. package/lib/kernel/migrations.js +30 -2
  65. package/lib/kernel/schema.js +35 -0
  66. package/lib/kernel/sqlite-driver.js +292 -18
  67. package/lib/lefthook-wiring.js +21 -1
  68. package/lib/memory/router.js +16 -1
  69. package/lib/memory-digest.js +47 -15
  70. package/lib/memory-recall-events.js +145 -0
  71. package/lib/memory-recall.js +212 -0
  72. package/lib/merge-rules.js +8 -4
  73. package/lib/npm-publish-workflow.js +272 -0
  74. package/lib/orientation.js +371 -49
  75. package/lib/plugin-catalog.js +14 -4
  76. package/lib/pr-bundle.js +9 -6
  77. package/lib/pr-monitor/journal.js +18 -2
  78. package/lib/pr-monitor/reconcile-executor.js +842 -0
  79. package/lib/pr-monitor/reconcile-tick.js +138 -0
  80. package/lib/pr-monitor/reconcile.js +0 -0
  81. package/lib/pr-monitor/render-summary.js +196 -0
  82. package/lib/pr-monitor/shepherd-lease.js +252 -0
  83. package/lib/pr-monitor/watch-lifecycle.js +14 -2
  84. package/lib/pr-pull.js +98 -24
  85. package/lib/pr-shepherd.js +34 -8
  86. package/lib/preflight/gates.js +65 -18
  87. package/lib/preflight/runner.js +5 -0
  88. package/lib/project-memory.js +40 -0
  89. package/lib/protected-state-authority.js +305 -0
  90. package/lib/protected-state-surfaces.js +64 -44
  91. package/lib/release-readiness.js +51 -4
  92. package/lib/rules-sync.js +4 -0
  93. package/lib/runtime-health.js +15 -46
  94. package/lib/shell-utils.js +1 -1
  95. package/lib/skill-eval.js +750 -0
  96. package/lib/skills-sync.js +6 -3
  97. package/lib/smart-merge.js +28 -4
  98. package/lib/status/identity.js +46 -0
  99. package/lib/status/presenter.js +0 -35
  100. package/lib/status/snapshot.js +11 -16
  101. package/lib/symlink-utils.js +74 -26
  102. package/lib/upgrade-safety.js +47 -9
  103. package/lib/using-forge.js +328 -0
  104. package/lib/workflow/enforce-stage.js +5 -5
  105. package/lib/workflow/state-manager.js +23 -23
  106. package/package.json +6 -7
  107. package/rules/using-forge.md +24 -0
  108. package/scripts/doc-asserting-tests.js +158 -0
  109. package/scripts/forge-team/index.sh +0 -5
  110. package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
  111. package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
  112. package/scripts/lib/behavioral-eval-runner.js +310 -0
  113. package/scripts/lib/behavioral-eval-runtime.js +456 -0
  114. package/scripts/lib/eval-evidence.js +328 -0
  115. package/scripts/lib/eval-runner.js +81 -41
  116. package/scripts/lib/immutable-eval-corpus.js +309 -0
  117. package/scripts/lib/promotion-evidence-loader.js +94 -0
  118. package/scripts/lib/promotion-scorecard.js +314 -0
  119. package/scripts/npm-release-receipt.js +134 -0
  120. package/scripts/process-tree.js +761 -0
  121. package/scripts/protected-state-check.js +47 -22
  122. package/scripts/run-command-eval.js +29 -1
  123. package/scripts/sync-d20-audit.js +172 -0
  124. package/scripts/test-full-suite.js +249 -37
  125. package/scripts/test.js +184 -44
  126. package/skills/claim-safety/SKILL.md +4 -0
  127. package/skills/claim-safety/evals/scorecard.json +41 -0
  128. package/skills/coverage.json +83 -0
  129. package/skills/dev/SKILL.md +4 -0
  130. package/skills/dev/evals/scorecard.json +41 -0
  131. package/skills/gates/SKILL.md +80 -0
  132. package/skills/gates/evals/evals.json +38 -0
  133. package/skills/gates/evals/scorecard.json +41 -0
  134. package/skills/hermes-forge/SKILL.md +1 -0
  135. package/skills/hermes-forge/evals/scorecard.json +41 -0
  136. package/skills/issue-basics/SKILL.md +1 -0
  137. package/skills/issue-basics/evals/scorecard.json +41 -0
  138. package/skills/kernel/SKILL.md +38 -0
  139. package/skills/kernel/evals/scorecard.json +41 -0
  140. package/skills/memory/SKILL.md +16 -1
  141. package/skills/memory/evals/scorecard.json +41 -0
  142. package/skills/parallel-deep-research/SKILL.md +1 -0
  143. package/skills/parallel-deep-research/evals/scorecard.json +41 -0
  144. package/skills/plan/SKILL.md +6 -0
  145. package/skills/plan/evals/scorecard.json +41 -0
  146. package/skills/portability/SKILL.md +47 -0
  147. package/skills/portability/evals/evals.json +34 -0
  148. package/skills/portability/evals/scorecard.json +41 -0
  149. package/skills/research/SKILL.md +1 -0
  150. package/skills/research/evals/scorecard.json +41 -0
  151. package/skills/review/SKILL.md +10 -11
  152. package/skills/review/evals/scorecard.json +41 -0
  153. package/skills/rollback/SKILL.md +5 -11
  154. package/skills/rollback/evals/scorecard.json +41 -0
  155. package/skills/setup/SKILL.md +91 -0
  156. package/skills/setup/evals/evals.json +42 -0
  157. package/skills/setup/evals/scorecard.json +41 -0
  158. package/skills/shepherd/SKILL.md +84 -38
  159. package/skills/shepherd/evals/evals.json +21 -9
  160. package/skills/shepherd/evals/scorecard.json +41 -0
  161. package/skills/ship/SKILL.md +10 -12
  162. package/skills/ship/evals/scorecard.json +41 -0
  163. package/skills/smith/SKILL.md +8 -0
  164. package/skills/smith/evals/scorecard.json +41 -0
  165. package/skills/sonarcloud/SKILL.md +1 -0
  166. package/skills/sonarcloud/evals/scorecard.json +41 -0
  167. package/skills/sonarcloud-analysis/SKILL.md +1 -0
  168. package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
  169. package/skills/status/SKILL.md +3 -0
  170. package/skills/status/evals/scorecard.json +41 -0
  171. package/skills/triage-ready/SKILL.md +2 -0
  172. package/skills/triage-ready/evals/scorecard.json +41 -0
  173. package/skills/using-forge/SKILL.md +104 -0
  174. package/skills/using-forge/evals/scorecard.json +41 -0
  175. package/skills/validate/SKILL.md +4 -0
  176. package/skills/validate/evals/scorecard.json +41 -0
  177. package/skills/verify/SKILL.md +4 -0
  178. package/skills/verify/evals/scorecard.json +41 -0
  179. package/skills/worktree/SKILL.md +92 -0
  180. package/skills/worktree/evals/evals.json +38 -0
  181. package/skills/worktree/evals/scorecard.json +41 -0
  182. package/lib/adapters/beads-issue-adapter.js +0 -127
  183. package/lib/beads-nudge.js +0 -91
  184. package/lib/beads-setup.js +0 -538
  185. package/lib/beads-sync-scaffold.js +0 -189
  186. package/lib/commands/board.js +0 -64
  187. package/lib/pat-setup.js +0 -207
  188. package/lib/pr-monitor/render-sticky.js +0 -192
  189. package/lib/pr-monitor/upsert-sticky.js +0 -169
  190. package/lib/status/beads-snapshot.js +0 -145
  191. package/scripts/beads-context.sh +0 -577
  192. package/scripts/beads-migrate-to-dolt.sh +0 -7
  193. package/scripts/beads-upgrade-smoke.sh +0 -284
  194. package/scripts/forge-team/lib/dashboard.sh +0 -316
  195. package/scripts/forge-team/tests/dashboard.test.sh +0 -155
  196. package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
@@ -1,371 +1,23 @@
1
1
  'use strict';
2
2
 
3
- const { spawn } = require('node:child_process');
4
- const { existsSync } = require('node:fs');
5
3
  const path = require('node:path');
6
- const { isBeadsInitialized } = require('./beads-setup');
7
- const { BeadsIssueAdapter } = require('./adapters/beads-issue-adapter');
8
4
  const { KernelIssueAdapter } = require('./adapters/kernel-issue-adapter');
9
5
  const { createGitHubProjectionPlan } = require('./issue-sync/project-github');
10
6
  const { createLocalBroker, buildLocalBrokerConfig } = require('./kernel/broker');
11
7
  const { createBuiltinSQLiteDriver } = require('./kernel/sqlite-driver');
12
- const { shouldUseKernelBroker } = require('./issue-backend');
13
8
  const { detectWorktree } = require('./detect-worktree');
14
9
  const { DEFAULT_LEASE_TTL_MS } = require('./kernel/lease-enforcer');
15
10
 
16
- const OPERATION_TO_BD = {
17
- create: 'create',
18
- list: 'list',
19
- ready: 'ready',
20
- show: 'show',
21
- search: 'search',
22
- stats: 'status',
23
- close: 'close',
24
- update: 'update',
25
- comment: 'comments',
26
- // KAP-7/KAP-12 derived reads: identity passthroughs to the matching bd subcommand.
27
- // Routed here (not in _issue.js) so the issue command surface carries no bd args.
28
- blocked: 'blocked',
29
- stale: 'stale',
30
- orphans: 'orphans',
31
- lint: 'lint',
32
- // Epic support: identity passthrough to `bd children` for the beads opt-out. The
33
- // kernel default emits the richer `issue.children` rollup; this passthrough only
34
- // keeps the beads path from a null-deref when children is invoked under --issue-backend
35
- // beads (the bd output shape differs and carries no rollup).
36
- children: 'children',
37
- };
38
-
39
- // Beads has no verified release operation; the claim lease lives only in the Kernel
40
- // backend. Surfaced as an explicit error so `forge release <id>` on the beads opt-out
41
- // fails loudly instead of silently mapping to an unrelated bd subcommand.
42
- const BEADS_RELEASE_KERNEL_ONLY_ERROR =
43
- 'forge release <id> is defined for the Kernel issue backend; Beads passthrough has no verified release operation.';
44
-
45
- // `bd children` emits a different payload (no rollup) than the kernel
46
- // `issue.children` envelope this command promises. Reject the beads backend
47
- // explicitly instead of silently proxying a contract-violating shape.
48
- const BEADS_CHILDREN_KERNEL_ONLY_ERROR =
49
- 'forge issue children is defined for the Kernel issue backend; Beads passthrough has no equivalent issue.children rollup.';
50
-
51
- // Lease ownership lives only in the Kernel backend (kernel_claims); Beads has no
52
- // verified lease to check. Reject the beads path explicitly so `forge issue owns <id>`
53
- // fails loudly instead of mapping to an unrelated/absent Beads subcommand.
54
- const BEADS_OWNS_KERNEL_ONLY_ERROR =
55
- 'forge issue owns <id> is defined for the Kernel issue backend; Beads passthrough has no lease-ownership verification.';
56
-
57
- // Active leases live only in the Kernel backend (kernel_claims); Beads has no lease
58
- // table to enumerate. Reject the beads path explicitly so `forge claims` fails loudly
59
- // instead of mapping to an absent Beads subcommand.
60
- const BEADS_CLAIMS_KERNEL_ONLY_ERROR =
61
- 'forge claims is defined for the Kernel issue backend; Beads passthrough has no active-lease listing.';
62
-
63
11
  const OPERATION_METHOD_ALIASES = {
64
12
  'dep.add': 'depAdd',
65
13
  'dep.remove': 'depRemove',
66
14
  };
67
15
 
68
- function getSpawnOptions(projectRoot) {
69
- return {
70
- cwd: projectRoot,
71
- stdio: ['ignore', 'pipe', 'pipe'],
72
- };
73
- }
74
-
75
- function getPathEntries(env = process.env, delimiter = path.delimiter) {
76
- const rawPath = env.PATH || env.Path || '';
77
- return rawPath
78
- .split(delimiter)
79
- .map(entry => entry.trim().replace(/^"(.*)"$/, '$1'))
80
- .filter(Boolean);
81
- }
82
-
83
- function resolveWindowsCommandCandidates(commandNames, deps = {}) {
84
- const env = deps.env || process.env;
85
- const fileExists = deps.existsSync || existsSync;
86
- const resolved = [];
87
- const seen = new Set();
88
- const pathEntries = getPathEntries(env, ';');
89
-
90
- for (const dir of pathEntries) {
91
- for (const commandName of commandNames) {
92
- const fullPath = isWindowsPathEntry(dir)
93
- ? path.win32.join(dir, commandName)
94
- : path.posix.join(dir, commandName);
95
- const dedupeKey = fullPath.toLowerCase();
96
- if (!seen.has(dedupeKey) && fileExists(fullPath)) {
97
- seen.add(dedupeKey);
98
- resolved.push(fullPath);
99
- }
100
- }
101
- }
102
-
103
- return resolved;
104
- }
105
-
106
- function isWindowsPathEntry(entry = '') {
107
- return /^(?:[a-zA-Z]:[\\/]|\\\\)/.test(entry) || entry.includes('\\');
108
- }
109
-
110
- function getBdCommandCandidates(deps = {}) {
111
- const platform = deps.platform || process.platform;
112
- if (platform === 'win32') {
113
- const resolved = resolveWindowsCommandCandidates(['bd.exe', 'bd.cmd'], deps);
114
- return [...new Set([...resolved, 'bd.exe', 'bd'])];
115
- }
116
-
117
- return ['bd'];
118
- }
119
-
120
- function normalizeExecOutput(output) {
121
- if (typeof output === 'string') {
122
- return output;
123
- }
124
-
125
- if (Buffer.isBuffer(output)) {
126
- return output.toString('utf8');
127
- }
128
-
129
- return '';
130
- }
131
-
132
- function hasBdSoftFailure(output) {
133
- return /(^|\n)Error(?: resolving| updating| fetching| adding)?\b/i.test(output);
134
- }
135
-
136
- function hasEmptyShowPayload(operation, output) {
137
- if (operation !== 'show') {
138
- return false;
139
- }
140
-
141
- const trimmedOutput = output.trim();
142
- return trimmedOutput === '' || trimmedOutput === '[]' || trimmedOutput === 'null';
143
- }
144
-
16
+ // Backend-agnostic: a `--help`/`-h` invocation must never reach a store.
145
17
  function isHelpInvocation(args = []) {
146
18
  return args.includes('--help') || args.includes('-h');
147
19
  }
148
20
 
149
- function extractErrorMessage(error) {
150
- if (error?.code === 'ENOENT') {
151
- return 'Beads (bd) command not found. Install or initialize Beads before using forge issues.';
152
- }
153
-
154
- return error?.message?.trim() || 'Beads command failed';
155
- }
156
-
157
- function getCommandErrorMessage(result) {
158
- const output = [result?.stdout, result?.stderr].filter(Boolean).join('\n').trim();
159
- if (output) {
160
- return output;
161
- }
162
-
163
- if (typeof result?.code === 'number') {
164
- return `Beads command failed with exit code ${result.code}`;
165
- }
166
-
167
- return 'Beads command failed';
168
- }
169
-
170
- function buildBdArgs(operation, args) {
171
- if (operation === 'comment') {
172
- return ['comments', 'add', ...args];
173
- }
174
-
175
- // De-bead parity: `forge claim <id>` maps to bd `update <id> --claim`. The
176
- // translation lived in _issue.js; it now lives in the beads layer so the issue
177
- // command surface stays free of bd argv. The issue id is the first positional.
178
- if (operation === 'claim') {
179
- const [issueId, ...rest] = args;
180
- if (!issueId) {
181
- return { error: 'Missing issue id. Usage: forge claim <id> [bd-update-flags]' };
182
- }
183
- return ['update', issueId, '--claim', ...rest];
184
- }
185
-
186
- if (operation === 'dep.add') {
187
- return ['dep', 'add', ...args];
188
- }
189
-
190
- if (operation === 'dep.remove') {
191
- return ['dep', 'remove', ...args];
192
- }
193
-
194
- const bdCommand = OPERATION_TO_BD[operation];
195
- if (!bdCommand) {
196
- return null;
197
- }
198
-
199
- return [bdCommand, ...args];
200
- }
201
-
202
- function shouldCaptureOutput(operation, args, deps = {}) {
203
- if (deps.captureOutput === true) {
204
- return true;
205
- }
206
-
207
- if (isHelpInvocation(args)) {
208
- return true;
209
- }
210
-
211
- if (args.includes('--json')) {
212
- return true;
213
- }
214
-
215
- return operation === 'update' || operation === 'show';
216
- }
217
-
218
- async function runBdCommand(operation, args, projectRoot, deps = {}) {
219
- const spawnBd = deps.spawn || spawn;
220
- const stdoutTarget = deps.stdout || process.stdout;
221
- const stderrTarget = deps.stderr || process.stderr;
222
- const captureOutput = shouldCaptureOutput(operation, args.slice(1), deps);
223
- const commandCandidates = deps.commandCandidates || getBdCommandCandidates(deps);
224
- let lastError;
225
-
226
- for (const command of commandCandidates) {
227
- try {
228
- return await new Promise((resolve, reject) => {
229
- const child = spawnBd(command, args, getSpawnOptions(projectRoot));
230
- let stdout = '';
231
- let stderr = '';
232
-
233
- child.stdout?.setEncoding?.('utf8');
234
- child.stderr?.setEncoding?.('utf8');
235
- child.stdout?.on?.('data', chunk => {
236
- const normalizedChunk = normalizeExecOutput(chunk);
237
- if (captureOutput) {
238
- stdout += normalizedChunk;
239
- } else {
240
- stdoutTarget?.write?.(normalizedChunk);
241
- }
242
- });
243
- child.stderr?.on?.('data', chunk => {
244
- const normalizedChunk = normalizeExecOutput(chunk);
245
- if (captureOutput) {
246
- stderr += normalizedChunk;
247
- }
248
- stderrTarget?.write?.(normalizedChunk);
249
- });
250
-
251
- child.on('error', reject);
252
- child.on('close', code => {
253
- resolve({
254
- code,
255
- stdout,
256
- stderr,
257
- });
258
- });
259
- });
260
- } catch (error) {
261
- lastError = error;
262
- // ENOENT/EINVAL means this candidate is not directly spawnable — try next.
263
- if (error?.code !== 'ENOENT' && error?.code !== 'EINVAL') {
264
- throw error;
265
- }
266
- }
267
- }
268
-
269
- throw lastError;
270
- }
271
-
272
- async function runBeadsOperation(operation, args, context, deps) {
273
- // Beads has no verified release; fail before the init check so the kernel-only
274
- // contract is reported even in an uninitialized repo (parity with the old
275
- // _issue.js release guard, which short-circuited on the beads path).
276
- if (operation === 'release' && !isHelpInvocation(args)) {
277
- return { success: false, error: BEADS_RELEASE_KERNEL_ONLY_ERROR };
278
- }
279
-
280
- // `children` only has a verified shape on the kernel backend (the
281
- // issue.children rollup); fail loudly rather than proxy `bd children`.
282
- if (operation === 'children' && !isHelpInvocation(args)) {
283
- return { success: false, error: BEADS_CHILDREN_KERNEL_ONLY_ERROR };
284
- }
285
-
286
- // `owns` verifies a Kernel lease; Beads has no such lease. Fail loudly rather
287
- // than silently proxy to a non-existent Beads subcommand.
288
- if (operation === 'owns' && !isHelpInvocation(args)) {
289
- return { success: false, error: BEADS_OWNS_KERNEL_ONLY_ERROR };
290
- }
291
-
292
- // `claims` enumerates the Kernel lease table; Beads has none. Fail loudly rather
293
- // than silently proxy to a non-existent Beads subcommand.
294
- if (operation === 'claims' && !isHelpInvocation(args)) {
295
- return { success: false, error: BEADS_CLAIMS_KERNEL_ONLY_ERROR };
296
- }
297
-
298
- const checkInit = deps.isBeadsInitialized || isBeadsInitialized;
299
- if (!isHelpInvocation(args) && !checkInit(context.projectRoot)) {
300
- return {
301
- success: false,
302
- error: 'Beads is not initialized in this project. Run forge setup before using forge issues.',
303
- };
304
- }
305
-
306
- const bdArgs = buildBdArgs(operation, args);
307
- // A non-array result is a translation error (e.g. claim with no issue id).
308
- if (!Array.isArray(bdArgs)) {
309
- return { success: false, error: bdArgs.error };
310
- }
311
- // The reported operation is the logical operation name (comment -> 'comment',
312
- // show -> 'show'), NOT the bd subcommand argv[0] (which is 'comments'/'status').
313
- // The one exception is claim: it maps to `update <id> --claim`, and the historical
314
- // _issue.js beads path reported operation 'update', so preserve that here.
315
- const reportedOperation = operation === 'claim' ? 'update' : operation;
316
- const runCommand = deps.runBdCommand || ((cmdArgs, projectRoot) => runBdCommand(operation, cmdArgs, projectRoot, deps));
317
-
318
- try {
319
- const result = await runCommand(bdArgs, context.projectRoot);
320
- const stdout = normalizeExecOutput(result?.stdout);
321
- const stderr = normalizeExecOutput(result?.stderr);
322
- const combinedOutput = [stdout, stderr].filter(Boolean).join('\n');
323
-
324
- if (result?.code !== 0) {
325
- return {
326
- success: false,
327
- error: getCommandErrorMessage(result),
328
- };
329
- }
330
-
331
- if (hasBdSoftFailure(combinedOutput)) {
332
- return {
333
- success: false,
334
- error: combinedOutput.trim() || 'Beads command failed',
335
- };
336
- }
337
-
338
- if (hasEmptyShowPayload(operation, combinedOutput)) {
339
- return {
340
- success: false,
341
- error: `Issue not found: ${args[0] || 'unknown issue'}`,
342
- };
343
- }
344
-
345
- return {
346
- success: true,
347
- operation: reportedOperation,
348
- output: stdout,
349
- stderr,
350
- };
351
- } catch (error) {
352
- return {
353
- success: false,
354
- error: extractErrorMessage(error),
355
- };
356
- }
357
- }
358
-
359
- function createBeadsIssueBackend(deps = {}) {
360
- return new BeadsIssueAdapter({
361
- runBeadsOperation: (operation, args, context, contextDeps = {}) =>
362
- runBeadsOperation(operation, args, context, {
363
- ...deps,
364
- ...contextDeps,
365
- }),
366
- });
367
- }
368
-
369
21
  // Build the default local broker for the CLI path. createLocalBroker does NOT
370
22
  // construct a driver on its own (`const driver = options.driver`), so when no
371
23
  // driver is injected we build a builtin SQLite driver pointed at the resolved
@@ -373,6 +25,7 @@ function createBeadsIssueBackend(deps = {}) {
373
25
  // on a missing `exec` method before ever reaching the "no tables" case.
374
26
  function createDefaultKernelBroker(brokerContext, deps) {
375
27
  let driver = deps.kernelDriver;
28
+ const ownsDriver = !driver;
376
29
  if (!driver) {
377
30
  const config = buildLocalBrokerConfig({
378
31
  projectRoot: brokerContext.projectRoot,
@@ -383,7 +36,7 @@ function createDefaultKernelBroker(brokerContext, deps) {
383
36
  driver = createBuiltinSQLiteDriver({ databasePath: config.databasePath });
384
37
  }
385
38
 
386
- return createLocalBroker({
39
+ const broker = createLocalBroker({
387
40
  projectRoot: brokerContext.projectRoot,
388
41
  gitCommonDir: deps.gitCommonDir,
389
42
  databasePath: deps.kernelDatabasePath,
@@ -394,6 +47,10 @@ function createDefaultKernelBroker(brokerContext, deps) {
394
47
  // classifier instead of paying the real Windows drive probe per per-op broker.
395
48
  classifyFilesystem: deps.classifyFilesystem,
396
49
  });
50
+ if (ownsDriver && typeof driver.close === 'function') {
51
+ broker.close = () => driver.close();
52
+ }
53
+ return broker;
397
54
  }
398
55
 
399
56
  // Symbol marker for the memoized lazy-init promise. Stored ON the broker object
@@ -430,6 +87,11 @@ function withLazyKernelInit(broker) {
430
87
  async initialize() {
431
88
  return ensureInitialized();
432
89
  },
90
+ async close() {
91
+ if (typeof broker.close === 'function') {
92
+ await broker.close();
93
+ }
94
+ },
433
95
  async runIssueOperation(operation, args = [], context = {}) {
434
96
  await ensureInitialized();
435
97
  return broker.runIssueOperation(operation, args, context);
@@ -442,22 +104,28 @@ function createKernelIssueBackend(context = {}) {
442
104
  const createBroker = deps.createKernelBroker
443
105
  || ((brokerContext) => createDefaultKernelBroker(brokerContext, deps));
444
106
 
107
+ const ownsBroker = !deps.kernelBroker;
445
108
  const broker = deps.kernelBroker || createBroker(context);
446
-
447
- return new KernelIssueAdapter({
448
- broker: withLazyKernelInit(broker),
449
- });
109
+ const managedBroker = withLazyKernelInit(broker);
110
+ const backend = new KernelIssueAdapter({ broker: managedBroker });
111
+ if (ownsBroker && typeof managedBroker?.close === 'function') {
112
+ backend.dispose = () => managedBroker.close();
113
+ }
114
+ return backend;
450
115
  }
451
116
 
452
117
  function createIssueService({ backend } = {}) {
453
- // Default issue backend is now the Kernel. Beads is reachable opt-OUT via the
454
- // selector (--issue-backend beads / FORGE_ISSUE_BACKEND=beads / config), which
455
- // resolves to an explicit `backend` upstream in runIssueOperation. This no-arg
456
- // fallback fires only for a no-backend direct call to createIssueService — the
457
- // CLI path always threads an explicit backend through resolveCommandOpts.
118
+ // The Kernel is the ONLY issue backend. An explicit `backend` is still honored so
119
+ // tests (and any future adapter) can inject one; the no-arg fallback builds the
120
+ // kernel backend, which is also what the CLI path threads in via resolveCommandOpts.
458
121
  const resolvedBackend = backend || createKernelIssueBackend();
459
122
 
460
123
  return {
124
+ async dispose() {
125
+ if (typeof resolvedBackend?.dispose === 'function') {
126
+ await resolvedBackend.dispose();
127
+ }
128
+ },
461
129
  async run(operation, args = [], context = {}) {
462
130
  const methodName = operation === 'show' && typeof resolvedBackend?.show !== 'function'
463
131
  ? 'read'
@@ -531,105 +199,18 @@ function resolveLeaseTtlMs(env = {}) {
531
199
  return ttl;
532
200
  }
533
201
 
534
- // Graceful Beads→Kernel fallback (issue 7f09ae93). When the resolved backend is
535
- // Beads but Beads is NOT initialized in this project AND a Forge Kernel store
536
- // already exists at `<gitCommonDir>/forge/kernel.sqlite`, route the operation to
537
- // the kernel instead of dead-ending on "Beads is not initialized". This unblocks
538
- // upgraders whose repo predates the kernel default (or whose global `forge` binary
539
- // is stale and still resolves Beads): the kernel is the correct default and already
540
- // holds their issues. Returns the resolved kernel paths when the fallback applies,
541
- // or null to keep the Beads path (so a genuine no-store repo still gets the Beads
542
- // not-initialized error). Path resolution reuses buildLocalBrokerConfig so the probed
543
- // location matches exactly what the broker would open. Never throws.
544
- function resolveBeadsFallbackToKernel(projectRoot, deps = {}) {
545
- const checkInit = deps.isBeadsInitialized || isBeadsInitialized;
546
- if (checkInit(projectRoot)) {
547
- // Beads is initialized — honor the explicit Beads selection, no fallback.
548
- return null;
549
- }
550
-
551
- let config;
552
- try {
553
- config = buildLocalBrokerConfig({
554
- projectRoot,
555
- gitCommonDir: deps.gitCommonDir,
556
- databasePath: deps.kernelDatabasePath,
557
- execFileSync: deps.execFileSync,
558
- });
559
- } catch {
560
- // No resolvable kernel location (e.g. projectRoot is not a git repo) — there is
561
- // no safe fallback target, so let the Beads path report its own error.
562
- return null;
563
- }
564
-
565
- const fileExists = deps.existsSync || existsSync;
566
- if (!fileExists(config.databasePath)) {
567
- // No kernel store either — preserve the Beads not-initialized error.
568
- return null;
569
- }
570
-
571
- return { kernelDatabasePath: config.databasePath, gitCommonDir: config.gitCommonDir };
572
- }
573
-
574
202
  async function runIssueOperation(operation, rawArgs, projectRoot, deps = {}) {
575
203
  const injectedService = deps.createService;
576
- let routeToKernel = shouldUseKernelBroker(deps);
577
- let effectiveDeps = deps;
578
-
579
- // Beads→Kernel graceful fallback (7f09ae93). Only considered when a service is not
580
- // injected (an injected service owns backend selection), the selection is not
581
- // already Kernel, and this is not a help invocation (help must never touch a
582
- // backend). When it applies, treat the op as Kernel-routed end-to-end (backend
583
- // construction AND the projection-target seam below).
584
- if (!injectedService && !routeToKernel && !isHelpInvocation(rawArgs)) {
585
- const fallback = resolveBeadsFallbackToKernel(projectRoot, deps);
586
- if (fallback) {
587
- routeToKernel = true;
588
- effectiveDeps = {
589
- ...deps,
590
- useKernelBroker: true,
591
- kernelDatabasePath: fallback.kernelDatabasePath,
592
- gitCommonDir: fallback.gitCommonDir,
593
- };
594
- const notify = deps.warn || ((msg) => process.stderr.write(`${msg}\n`));
595
- notify(
596
- 'Notice: Beads is not initialized, but a Forge Kernel store exists — using the '
597
- + 'kernel backend (the default). Run `forge setup` to finish migrating.',
598
- );
599
- }
600
- }
601
-
602
- const createService = injectedService || (() => {
603
- const context = {
604
- projectRoot,
605
- deps: effectiveDeps,
606
- };
607
-
608
- if (routeToKernel) {
609
- return createIssueService({
610
- backend: createKernelIssueBackend(context),
611
- });
612
- }
613
204
 
614
- const backendDeps = {
615
- isBeadsInitialized: deps.isBeadsInitialized,
616
- runBdCommand: deps.runBdCommand,
617
- spawn: deps.spawn,
618
- platform: deps.platform,
619
- env: deps.env,
620
- };
621
-
622
- return createIssueService({
623
- backend: createBeadsIssueBackend(backendDeps),
624
- });
625
- });
205
+ const createService = injectedService || (() => createIssueService({
206
+ backend: createKernelIssueBackend({ projectRoot, deps }),
207
+ }));
626
208
 
627
209
  const service = createService();
628
210
  // Thread a distinct per-agent actor (and, when present, a stable session id) into the
629
211
  // mutation context so the Kernel's claim idempotency key is scoped per agent rather than
630
212
  // to the shared 'forge' default (kernel d71a824b). Undefined values are omitted so the
631
- // historical default is preserved for no-env callers, and the Beads backend — which
632
- // ignores context.actor/sessionId — is unaffected.
213
+ // historical default is preserved for no-env callers.
633
214
  const actorEnv = deps.env || process.env;
634
215
  const actor = resolveIssueActor(actorEnv);
635
216
  const sessionId = typeof actorEnv.FORGE_SESSION_ID === 'string' && actorEnv.FORGE_SESSION_ID.trim()
@@ -642,56 +223,48 @@ async function runIssueOperation(operation, rawArgs, projectRoot, deps = {}) {
642
223
  const isClaim = operation === 'claim';
643
224
  const worktreeId = isClaim ? resolveWorktreeId(projectRoot, actorEnv, deps) : undefined;
644
225
  const leaseTtlMs = isClaim ? resolveLeaseTtlMs(actorEnv) : undefined;
645
- const result = await service.run(operation, rawArgs, {
646
- projectRoot,
647
- deps,
648
- ...(actor ? { actor } : {}),
649
- ...(sessionId ? { sessionId } : {}),
650
- ...(worktreeId ? { worktreeId } : {}),
651
- ...(leaseTtlMs ? { leaseTtlMs } : {}),
652
- // Kernel mutations enqueue a projection-outbox "dirty marker" that `forge export`
653
- // (the D16 git-JSONL portability projection) drains under target 'jsonl'. The
654
- // broker's legacy primitive default is 'beads', so without steering the target
655
- // here every Kernel-created issue is enqueued as 'beads' and `forge export` — which
656
- // drains 'jsonl' — finds nothing, silently never emitting git-tracked JSONL. Only
657
- // the Kernel path uses this seam (`context.projectionTarget`); Beads ignores it.
658
- ...(routeToKernel ? { projectionTarget: 'jsonl' } : {}),
659
- });
226
+ try {
227
+ const result = await service.run(operation, rawArgs, {
228
+ projectRoot,
229
+ deps,
230
+ ...(actor ? { actor } : {}),
231
+ ...(sessionId ? { sessionId } : {}),
232
+ ...(worktreeId ? { worktreeId } : {}),
233
+ ...(leaseTtlMs ? { leaseTtlMs } : {}),
234
+ // Kernel mutations enqueue a projection-outbox "dirty marker" that `forge export`
235
+ // (the D16 git-JSONL portability projection) drains under target 'jsonl'. The
236
+ // broker's legacy primitive default is still 'beads', so without steering the target
237
+ // here every Kernel-created issue would be enqueued as 'beads' and `forge export` —
238
+ // which drains 'jsonl' — would find nothing, silently never emitting git-tracked
239
+ // JSONL. Unconditional now that the Kernel is the only backend.
240
+ projectionTarget: 'jsonl',
241
+ });
660
242
 
661
- const queueGitHubProjection = deps.enqueueGitHubProjection || deps.queueGitHubProjection;
662
- if (result?.success && typeof queueGitHubProjection === 'function') {
663
- const projectionPlan = createGitHubProjectionPlan(operation, rawArgs);
664
- if (projectionPlan) {
665
- await queueGitHubProjection(projectionPlan, {
666
- operation,
667
- args: rawArgs,
668
- projectRoot,
669
- result,
670
- });
243
+ const queueGitHubProjection = deps.enqueueGitHubProjection || deps.queueGitHubProjection;
244
+ if (result?.success && typeof queueGitHubProjection === 'function') {
245
+ const projectionPlan = createGitHubProjectionPlan(operation, rawArgs);
246
+ if (projectionPlan) {
247
+ await queueGitHubProjection(projectionPlan, {
248
+ operation,
249
+ args: rawArgs,
250
+ projectRoot,
251
+ result,
252
+ });
253
+ }
671
254
  }
672
- }
673
255
 
674
- return result;
256
+ return result;
257
+ } finally {
258
+ if (!injectedService && typeof service.dispose === 'function') {
259
+ try { await service.dispose(); } catch { /* cleanup must not mask the operation outcome */ }
260
+ }
261
+ }
675
262
  }
676
263
 
677
264
  module.exports = {
678
265
  createIssueService,
679
- createBeadsIssueBackend,
680
266
  createKernelIssueBackend,
681
- runBeadsOperation,
682
267
  runIssueOperation,
683
- buildBdArgs,
684
- extractErrorMessage,
685
- getCommandErrorMessage,
686
- getSpawnOptions,
687
- hasEmptyShowPayload,
688
- hasBdSoftFailure,
689
268
  isHelpInvocation,
690
- normalizeExecOutput,
691
- runBdCommand,
692
- getBdCommandCandidates,
693
- getPathEntries,
694
269
  resolveIssueActor,
695
- resolveBeadsFallbackToKernel,
696
- resolveWindowsCommandCandidates,
697
270
  };