forge-workflow 0.1.0-beta.2 → 0.1.0-beta.4

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 (155) hide show
  1. package/.forge/hooks/check-tdd.js +79 -5
  2. package/.forge/hooks/forge-native-hook.js +194 -8
  3. package/AGENTS.md +2 -1
  4. package/CHANGELOG.md +28 -0
  5. package/QUICKSTART.md +6 -2
  6. package/README.md +3 -1
  7. package/bin/forge-cmd.js +1 -1
  8. package/bin/forge.js +95 -19
  9. package/docs/guides/SETUP.md +4 -1
  10. package/docs/guides/SUPPORT.md +5 -0
  11. package/docs/reference/COMMANDS.md +12 -1
  12. package/docs/reference/shepherd.md +114 -3
  13. package/lib/activation/ensure-forge-home.js +135 -0
  14. package/lib/adapters/beads-kernel-compat.js +67 -0
  15. package/lib/adapters/greptile-review-adapter.js +1 -1
  16. package/lib/adapters/pr-state-adapter.js +103 -8
  17. package/lib/adoption-profiles.js +17 -4
  18. package/lib/agents-config.js +5 -0
  19. package/lib/beads-detect.js +60 -0
  20. package/lib/commands/_aliases.js +248 -0
  21. package/lib/commands/_issue.js +65 -41
  22. package/lib/commands/_manifest.js +3 -1
  23. package/lib/commands/_registry.js +14 -0
  24. package/lib/commands/_resolve-command-opts.js +36 -60
  25. package/lib/commands/claim.js +2 -4
  26. package/lib/commands/gate.js +19 -2
  27. package/lib/commands/hooks.js +294 -12
  28. package/lib/commands/init.js +26 -20
  29. package/lib/commands/memory.js +81 -0
  30. package/lib/commands/migrate.js +0 -161
  31. package/lib/commands/plan.js +71 -123
  32. package/lib/commands/pr.js +88 -0
  33. package/lib/commands/prime.js +8 -1
  34. package/lib/commands/push.js +66 -0
  35. package/lib/commands/recall.js +67 -12
  36. package/lib/commands/recap.js +18 -4
  37. package/lib/commands/release.js +15 -3
  38. package/lib/commands/remember.js +86 -20
  39. package/lib/commands/serve.js +5 -2
  40. package/lib/commands/setup.js +135 -73
  41. package/lib/commands/shepherd.js +102 -4
  42. package/lib/commands/ship.js +40 -4
  43. package/lib/commands/skill.js +275 -0
  44. package/lib/commands/status.js +37 -32
  45. package/lib/commands/test.js +32 -0
  46. package/lib/commands/worktree.js +87 -5
  47. package/lib/core/runtime-graph.js +122 -17
  48. package/lib/forge-issues.js +13 -464
  49. package/lib/gate-events.js +54 -55
  50. package/lib/global-flags.js +30 -0
  51. package/lib/grounding/context-events.js +230 -0
  52. package/lib/grounding/read-first.js +112 -0
  53. package/lib/harness-capability-matrix.js +2 -2
  54. package/lib/hook-renderer.js +144 -12
  55. package/lib/issue-backend.js +42 -3
  56. package/lib/kernel/backing-issue.js +7 -1
  57. package/lib/kernel/broker.js +28 -0
  58. package/lib/kernel/migrations.js +30 -2
  59. package/lib/kernel/owned-kernel.js +43 -0
  60. package/lib/kernel/schema.js +35 -0
  61. package/lib/kernel/sqlite-driver.js +187 -1
  62. package/lib/memory-recall.js +151 -0
  63. package/lib/orientation.js +303 -6
  64. package/lib/pr-bundle.js +6 -2
  65. package/lib/pr-monitor/auto-actions.js +175 -0
  66. package/lib/pr-monitor/digest.js +206 -0
  67. package/lib/pr-monitor/reconcile-executor.js +659 -0
  68. package/lib/pr-monitor/reconcile-tick.js +138 -0
  69. package/lib/pr-monitor/reconcile.js +0 -0
  70. package/lib/pr-monitor/render-sticky.js +57 -8
  71. package/lib/pr-monitor/shepherd-lease.js +243 -0
  72. package/lib/pr-monitor/upsert-sticky.js +169 -0
  73. package/lib/pr-monitor/watch-lifecycle.js +1 -1
  74. package/lib/pr-pull.js +113 -17
  75. package/lib/project-memory.js +8 -0
  76. package/lib/release-readiness.js +17 -1
  77. package/lib/rules-sync.js +4 -0
  78. package/lib/runtime-health.js +15 -46
  79. package/lib/skill-eval.js +750 -0
  80. package/lib/status/identity.js +46 -0
  81. package/lib/status/presenter.js +0 -35
  82. package/lib/status/snapshot.js +11 -16
  83. package/lib/upgrade-safety.js +52 -1
  84. package/lib/using-forge.js +315 -0
  85. package/lib/workflow/enforce-stage.js +64 -7
  86. package/lib/workflow/state-manager.js +23 -23
  87. package/package.json +2 -2
  88. package/rules/using-forge.md +24 -0
  89. package/scripts/forge-team/index.sh +0 -5
  90. package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
  91. package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
  92. package/scripts/pr-auto-actions.js +93 -0
  93. package/scripts/pr-verdict-label.js +50 -0
  94. package/scripts/test.js +8 -1
  95. package/skills/claim-safety/SKILL.md +4 -0
  96. package/skills/claim-safety/evals/scorecard.json +41 -0
  97. package/skills/coverage.json +83 -0
  98. package/skills/dev/SKILL.md +4 -0
  99. package/skills/dev/evals/scorecard.json +41 -0
  100. package/skills/gates/SKILL.md +80 -0
  101. package/skills/gates/evals/evals.json +38 -0
  102. package/skills/gates/evals/scorecard.json +41 -0
  103. package/skills/hermes-forge/SKILL.md +1 -0
  104. package/skills/hermes-forge/evals/scorecard.json +41 -0
  105. package/skills/issue-basics/SKILL.md +1 -0
  106. package/skills/issue-basics/evals/scorecard.json +41 -0
  107. package/skills/kernel/SKILL.md +38 -0
  108. package/skills/kernel/evals/scorecard.json +41 -0
  109. package/skills/memory/SKILL.md +16 -1
  110. package/skills/memory/evals/scorecard.json +41 -0
  111. package/skills/parallel-deep-research/SKILL.md +1 -0
  112. package/skills/parallel-deep-research/evals/scorecard.json +41 -0
  113. package/skills/plan/SKILL.md +6 -0
  114. package/skills/plan/evals/scorecard.json +41 -0
  115. package/skills/portability/SKILL.md +47 -0
  116. package/skills/portability/evals/evals.json +34 -0
  117. package/skills/portability/evals/scorecard.json +41 -0
  118. package/skills/research/SKILL.md +1 -0
  119. package/skills/research/evals/scorecard.json +41 -0
  120. package/skills/review/SKILL.md +6 -0
  121. package/skills/review/evals/scorecard.json +41 -0
  122. package/skills/rollback/SKILL.md +1 -0
  123. package/skills/rollback/evals/scorecard.json +41 -0
  124. package/skills/setup/SKILL.md +91 -0
  125. package/skills/setup/evals/evals.json +42 -0
  126. package/skills/setup/evals/scorecard.json +41 -0
  127. package/skills/shepherd/SKILL.md +76 -36
  128. package/skills/shepherd/evals/evals.json +21 -9
  129. package/skills/shepherd/evals/scorecard.json +41 -0
  130. package/skills/ship/SKILL.md +6 -0
  131. package/skills/ship/evals/scorecard.json +41 -0
  132. package/skills/smith/SKILL.md +8 -0
  133. package/skills/smith/evals/scorecard.json +41 -0
  134. package/skills/sonarcloud/SKILL.md +1 -0
  135. package/skills/sonarcloud/evals/scorecard.json +41 -0
  136. package/skills/sonarcloud-analysis/SKILL.md +1 -0
  137. package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
  138. package/skills/status/SKILL.md +3 -0
  139. package/skills/status/evals/scorecard.json +41 -0
  140. package/skills/triage-ready/SKILL.md +2 -0
  141. package/skills/triage-ready/evals/scorecard.json +41 -0
  142. package/skills/using-forge/SKILL.md +104 -0
  143. package/skills/using-forge/evals/scorecard.json +41 -0
  144. package/skills/validate/SKILL.md +4 -0
  145. package/skills/validate/evals/scorecard.json +41 -0
  146. package/skills/verify/SKILL.md +4 -0
  147. package/skills/verify/evals/scorecard.json +41 -0
  148. package/skills/worktree/SKILL.md +87 -0
  149. package/skills/worktree/evals/evals.json +38 -0
  150. package/skills/worktree/evals/scorecard.json +41 -0
  151. package/lib/adapters/beads-issue-adapter.js +0 -127
  152. package/lib/commands/board.js +0 -64
  153. package/lib/status/beads-snapshot.js +0 -145
  154. package/scripts/forge-team/lib/dashboard.sh +0 -316
  155. package/scripts/forge-team/tests/dashboard.test.sh +0 -155
@@ -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
@@ -450,11 +102,9 @@ function createKernelIssueBackend(context = {}) {
450
102
  }
451
103
 
452
104
  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.
105
+ // The Kernel is the ONLY issue backend. An explicit `backend` is still honored so
106
+ // tests (and any future adapter) can inject one; the no-arg fallback builds the
107
+ // kernel backend, which is also what the CLI path threads in via resolveCommandOpts.
458
108
  const resolvedBackend = backend || createKernelIssueBackend();
459
109
 
460
110
  return {
@@ -531,105 +181,18 @@ function resolveLeaseTtlMs(env = {}) {
531
181
  return ttl;
532
182
  }
533
183
 
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
184
  async function runIssueOperation(operation, rawArgs, projectRoot, deps = {}) {
575
185
  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
-
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
186
 
622
- return createIssueService({
623
- backend: createBeadsIssueBackend(backendDeps),
624
- });
625
- });
187
+ const createService = injectedService || (() => createIssueService({
188
+ backend: createKernelIssueBackend({ projectRoot, deps }),
189
+ }));
626
190
 
627
191
  const service = createService();
628
192
  // Thread a distinct per-agent actor (and, when present, a stable session id) into the
629
193
  // mutation context so the Kernel's claim idempotency key is scoped per agent rather than
630
194
  // 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.
195
+ // historical default is preserved for no-env callers.
633
196
  const actorEnv = deps.env || process.env;
634
197
  const actor = resolveIssueActor(actorEnv);
635
198
  const sessionId = typeof actorEnv.FORGE_SESSION_ID === 'string' && actorEnv.FORGE_SESSION_ID.trim()
@@ -651,11 +214,11 @@ async function runIssueOperation(operation, rawArgs, projectRoot, deps = {}) {
651
214
  ...(leaseTtlMs ? { leaseTtlMs } : {}),
652
215
  // Kernel mutations enqueue a projection-outbox "dirty marker" that `forge export`
653
216
  // (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' } : {}),
217
+ // broker's legacy primitive default is still 'beads', so without steering the target
218
+ // here every Kernel-created issue would be enqueued as 'beads' and `forge export` —
219
+ // which drains 'jsonl' — would find nothing, silently never emitting git-tracked
220
+ // JSONL. Unconditional now that the Kernel is the only backend.
221
+ projectionTarget: 'jsonl',
659
222
  });
660
223
 
661
224
  const queueGitHubProjection = deps.enqueueGitHubProjection || deps.queueGitHubProjection;
@@ -676,22 +239,8 @@ async function runIssueOperation(operation, rawArgs, projectRoot, deps = {}) {
676
239
 
677
240
  module.exports = {
678
241
  createIssueService,
679
- createBeadsIssueBackend,
680
242
  createKernelIssueBackend,
681
- runBeadsOperation,
682
243
  runIssueOperation,
683
- buildBdArgs,
684
- extractErrorMessage,
685
- getCommandErrorMessage,
686
- getSpawnOptions,
687
- hasEmptyShowPayload,
688
- hasBdSoftFailure,
689
244
  isHelpInvocation,
690
- normalizeExecOutput,
691
- runBdCommand,
692
- getBdCommandCandidates,
693
- getPathEntries,
694
245
  resolveIssueActor,
695
- resolveBeadsFallbackToKernel,
696
- resolveWindowsCommandCandidates,
697
246
  };
@@ -23,7 +23,7 @@
23
23
  * runtime graph + `.forge/config.yaml`; this module only records/reads the events.
24
24
  */
25
25
 
26
- const { buildMigratedKernelIssueDeps } = require('./kernel/cli-broker-factory');
26
+ const { resolveOwnedKernel, closeIfOwned } = require('./kernel/owned-kernel');
27
27
  const { resolveIssueActor } = require('./forge-issues');
28
28
 
29
29
  const GATE_APPROVED_EVENT = 'gate.approved';
@@ -37,22 +37,12 @@ const GATE_EVENT_TYPES = {
37
37
  const ISSUE_ENTITY_TYPE = 'issue';
38
38
  const GATE_EVENT_ORIGIN = 'cli';
39
39
 
40
- /**
41
- * Resolve the kernel broker + driver + config. Tests (and the orchestrator) inject
42
- * a shared, already-migrated kernel via `deps`; the CLI path builds a fresh one for
43
- * the (short-lived) process.
44
- *
45
- * @param {string} projectRoot
46
- * @param {{ kernelBroker?: Object, kernelDriver?: Object }} [deps]
47
- * @returns {Promise<{ broker: Object, driver: Object, config: Object }>}
48
- */
49
- async function resolveGateKernel(projectRoot, deps = {}) {
50
- if (deps.kernelBroker && deps.kernelDriver) {
51
- return { broker: deps.kernelBroker, driver: deps.kernelDriver, config: deps.kernelBroker.config };
52
- }
53
- const built = await buildMigratedKernelIssueDeps({ projectRoot });
54
- return { broker: built.kernelBroker, driver: built.kernelDriver, config: built.kernelBroker.config };
55
- }
40
+ // Kernel lifecycle (resolve + close-what-you-built) is shared with
41
+ // grounding/context-events via lib/kernel/owned-kernel. A gate read/append that
42
+ // builds its own kernel must close it an unclosed SQLite handle leaks and, on
43
+ // Windows, locks the DB directory (EBUSY on rmSync, kernel issue e62e4bde). Safe
44
+ // here today because gate events only fire on explicit `forge gate` commands,
45
+ // but the leak is identical so we close proactively via closeIfOwned.
56
46
 
57
47
  /**
58
48
  * Idempotency key for a gate event. Scoped to issue + gate + actor + decision so a
@@ -110,44 +100,49 @@ async function recordGateEvent(projectRoot, params = {}) {
110
100
  }
111
101
 
112
102
  const actor = resolveIssueActor(env || process.env) || 'forge';
113
- const { driver, config } = await resolveGateKernel(projectRoot, deps);
114
-
115
- const entity = await driver.loadKernelEntity(ISSUE_ENTITY_TYPE, issueId, {}, config);
116
- if (!entity) {
117
- return { ok: false, issueMissing: true, actor };
118
- }
103
+ const kernel = await resolveOwnedKernel(projectRoot, deps);
104
+ const { driver, config } = kernel;
119
105
 
120
- const idempotencyKey = gateIdempotencyKey(eventType, issueId, gateId, actor);
106
+ try {
107
+ const entity = await driver.loadKernelEntity(ISSUE_ENTITY_TYPE, issueId, {}, config);
108
+ if (!entity) {
109
+ return { ok: false, issueMissing: true, actor };
110
+ }
121
111
 
122
- const existing = await driver.loadKernelEventByIdempotencyKey(idempotencyKey, {}, config);
123
- if (existing) {
124
- return { ok: true, duplicate: true, event: parseGateEvent(existing), actor };
125
- }
112
+ const idempotencyKey = gateIdempotencyKey(eventType, issueId, gateId, actor);
126
113
 
127
- const payload = { gate: gateId, actor };
128
- if (typeof reason === 'string' && reason.length > 0) payload.reason = reason;
129
-
130
- const event = {
131
- entity_type: ISSUE_ENTITY_TYPE,
132
- entity_id: issueId,
133
- event_type: eventType,
134
- idempotency_key: idempotencyKey,
135
- expected_revision: 0,
136
- actor,
137
- origin: GATE_EVENT_ORIGIN,
138
- payload,
139
- created_at: now || new Date().toISOString(),
140
- };
114
+ const existing = await driver.loadKernelEventByIdempotencyKey(idempotencyKey, {}, config);
115
+ if (existing) {
116
+ return { ok: true, duplicate: true, event: parseGateEvent(existing), actor };
117
+ }
141
118
 
142
- try {
143
- const inserted = await driver.insertKernelEvent(event, {}, config);
144
- return { ok: true, duplicate: false, event: parseGateEvent(inserted), actor };
145
- } catch (error) {
146
- if (isIdempotencyRace(error)) {
147
- const winner = await driver.loadKernelEventByIdempotencyKey(idempotencyKey, {}, config);
148
- return { ok: true, duplicate: true, event: winner ? parseGateEvent(winner) : parseGateEvent(event), actor };
119
+ const payload = { gate: gateId, actor };
120
+ if (typeof reason === 'string' && reason.length > 0) payload.reason = reason;
121
+
122
+ const event = {
123
+ entity_type: ISSUE_ENTITY_TYPE,
124
+ entity_id: issueId,
125
+ event_type: eventType,
126
+ idempotency_key: idempotencyKey,
127
+ expected_revision: 0,
128
+ actor,
129
+ origin: GATE_EVENT_ORIGIN,
130
+ payload,
131
+ created_at: now || new Date().toISOString(),
132
+ };
133
+
134
+ try {
135
+ const inserted = await driver.insertKernelEvent(event, {}, config);
136
+ return { ok: true, duplicate: false, event: parseGateEvent(inserted), actor };
137
+ } catch (error) {
138
+ if (isIdempotencyRace(error)) {
139
+ const winner = await driver.loadKernelEventByIdempotencyKey(idempotencyKey, {}, config);
140
+ return { ok: true, duplicate: true, event: winner ? parseGateEvent(winner) : parseGateEvent(event), actor };
141
+ }
142
+ throw error;
149
143
  }
150
- throw error;
144
+ } finally {
145
+ closeIfOwned(kernel);
151
146
  }
152
147
  }
153
148
 
@@ -160,11 +155,15 @@ async function recordGateEvent(projectRoot, params = {}) {
160
155
  * @returns {Promise<Array<{ event_type: string, gate: string, actor: string, created_at: string, reason?: string }>>}
161
156
  */
162
157
  async function listGateEvents(projectRoot, issueId, options = {}) {
163
- const { driver, config } = await resolveGateKernel(projectRoot, options.deps);
164
- const rows = await driver.listKernelEvents(ISSUE_ENTITY_TYPE, issueId, {}, config);
165
- return (rows || [])
166
- .filter(row => typeof row.event_type === 'string' && row.event_type.startsWith('gate.'))
167
- .map(parseGateEvent);
158
+ const kernel = await resolveOwnedKernel(projectRoot, options.deps);
159
+ try {
160
+ const rows = await kernel.driver.listKernelEvents(ISSUE_ENTITY_TYPE, issueId, {}, kernel.config);
161
+ return (rows || [])
162
+ .filter(row => typeof row.event_type === 'string' && row.event_type.startsWith('gate.'))
163
+ .map(parseGateEvent);
164
+ } finally {
165
+ closeIfOwned(kernel);
166
+ }
168
167
  }
169
168
 
170
169
  /**