forge-workflow 0.1.0-beta.3 → 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 (122) hide show
  1. package/AGENTS.md +1 -1
  2. package/bin/forge-cmd.js +1 -1
  3. package/bin/forge.js +5 -0
  4. package/docs/reference/COMMANDS.md +3 -1
  5. package/docs/reference/shepherd.md +72 -1
  6. package/lib/adapters/greptile-review-adapter.js +1 -1
  7. package/lib/adapters/pr-state-adapter.js +103 -8
  8. package/lib/agents-config.js +5 -0
  9. package/lib/commands/_issue.js +31 -46
  10. package/lib/commands/_manifest.js +1 -1
  11. package/lib/commands/_resolve-command-opts.js +36 -29
  12. package/lib/commands/claim.js +2 -4
  13. package/lib/commands/hooks.js +155 -8
  14. package/lib/commands/plan.js +23 -115
  15. package/lib/commands/prime.js +8 -1
  16. package/lib/commands/release.js +1 -2
  17. package/lib/commands/serve.js +5 -2
  18. package/lib/commands/setup.js +0 -1
  19. package/lib/commands/shepherd.js +36 -3
  20. package/lib/commands/skill.js +275 -0
  21. package/lib/commands/status.js +37 -32
  22. package/lib/commands/test.js +32 -0
  23. package/lib/commands/worktree.js +27 -1
  24. package/lib/core/runtime-graph.js +88 -14
  25. package/lib/forge-issues.js +13 -464
  26. package/lib/harness-capability-matrix.js +2 -2
  27. package/lib/hook-renderer.js +54 -12
  28. package/lib/issue-backend.js +42 -3
  29. package/lib/kernel/broker.js +28 -0
  30. package/lib/kernel/migrations.js +30 -2
  31. package/lib/kernel/schema.js +35 -0
  32. package/lib/kernel/sqlite-driver.js +150 -0
  33. package/lib/memory-recall.js +151 -0
  34. package/lib/orientation.js +303 -6
  35. package/lib/pr-bundle.js +6 -2
  36. package/lib/pr-monitor/reconcile-executor.js +659 -0
  37. package/lib/pr-monitor/reconcile-tick.js +138 -0
  38. package/lib/pr-monitor/reconcile.js +0 -0
  39. package/lib/pr-monitor/render-sticky.js +14 -0
  40. package/lib/pr-monitor/shepherd-lease.js +243 -0
  41. package/lib/pr-monitor/upsert-sticky.js +1 -1
  42. package/lib/pr-monitor/watch-lifecycle.js +1 -1
  43. package/lib/pr-pull.js +70 -15
  44. package/lib/project-memory.js +8 -0
  45. package/lib/rules-sync.js +4 -0
  46. package/lib/runtime-health.js +15 -46
  47. package/lib/skill-eval.js +750 -0
  48. package/lib/status/identity.js +46 -0
  49. package/lib/status/presenter.js +0 -35
  50. package/lib/status/snapshot.js +11 -16
  51. package/lib/upgrade-safety.js +8 -9
  52. package/lib/using-forge.js +315 -0
  53. package/lib/workflow/enforce-stage.js +5 -5
  54. package/lib/workflow/state-manager.js +23 -23
  55. package/package.json +1 -1
  56. package/rules/using-forge.md +24 -0
  57. package/scripts/forge-team/index.sh +0 -5
  58. package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
  59. package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
  60. package/scripts/test.js +8 -1
  61. package/skills/claim-safety/SKILL.md +4 -0
  62. package/skills/claim-safety/evals/scorecard.json +41 -0
  63. package/skills/coverage.json +83 -0
  64. package/skills/dev/SKILL.md +4 -0
  65. package/skills/dev/evals/scorecard.json +41 -0
  66. package/skills/gates/SKILL.md +80 -0
  67. package/skills/gates/evals/evals.json +38 -0
  68. package/skills/gates/evals/scorecard.json +41 -0
  69. package/skills/hermes-forge/SKILL.md +1 -0
  70. package/skills/hermes-forge/evals/scorecard.json +41 -0
  71. package/skills/issue-basics/SKILL.md +1 -0
  72. package/skills/issue-basics/evals/scorecard.json +41 -0
  73. package/skills/kernel/SKILL.md +38 -0
  74. package/skills/kernel/evals/scorecard.json +41 -0
  75. package/skills/memory/SKILL.md +16 -1
  76. package/skills/memory/evals/scorecard.json +41 -0
  77. package/skills/parallel-deep-research/SKILL.md +1 -0
  78. package/skills/parallel-deep-research/evals/scorecard.json +41 -0
  79. package/skills/plan/SKILL.md +6 -0
  80. package/skills/plan/evals/scorecard.json +41 -0
  81. package/skills/portability/SKILL.md +47 -0
  82. package/skills/portability/evals/evals.json +34 -0
  83. package/skills/portability/evals/scorecard.json +41 -0
  84. package/skills/research/SKILL.md +1 -0
  85. package/skills/research/evals/scorecard.json +41 -0
  86. package/skills/review/SKILL.md +6 -0
  87. package/skills/review/evals/scorecard.json +41 -0
  88. package/skills/rollback/SKILL.md +1 -0
  89. package/skills/rollback/evals/scorecard.json +41 -0
  90. package/skills/setup/SKILL.md +91 -0
  91. package/skills/setup/evals/evals.json +42 -0
  92. package/skills/setup/evals/scorecard.json +41 -0
  93. package/skills/shepherd/SKILL.md +76 -36
  94. package/skills/shepherd/evals/evals.json +21 -9
  95. package/skills/shepherd/evals/scorecard.json +41 -0
  96. package/skills/ship/SKILL.md +6 -0
  97. package/skills/ship/evals/scorecard.json +41 -0
  98. package/skills/smith/SKILL.md +8 -0
  99. package/skills/smith/evals/scorecard.json +41 -0
  100. package/skills/sonarcloud/SKILL.md +1 -0
  101. package/skills/sonarcloud/evals/scorecard.json +41 -0
  102. package/skills/sonarcloud-analysis/SKILL.md +1 -0
  103. package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
  104. package/skills/status/SKILL.md +3 -0
  105. package/skills/status/evals/scorecard.json +41 -0
  106. package/skills/triage-ready/SKILL.md +2 -0
  107. package/skills/triage-ready/evals/scorecard.json +41 -0
  108. package/skills/using-forge/SKILL.md +104 -0
  109. package/skills/using-forge/evals/scorecard.json +41 -0
  110. package/skills/validate/SKILL.md +4 -0
  111. package/skills/validate/evals/scorecard.json +41 -0
  112. package/skills/verify/SKILL.md +4 -0
  113. package/skills/verify/evals/scorecard.json +41 -0
  114. package/skills/worktree/SKILL.md +87 -0
  115. package/skills/worktree/evals/evals.json +38 -0
  116. package/skills/worktree/evals/scorecard.json +41 -0
  117. package/lib/adapters/beads-issue-adapter.js +0 -127
  118. package/lib/beads-nudge.js +0 -91
  119. package/lib/commands/board.js +0 -64
  120. package/lib/status/beads-snapshot.js +0 -145
  121. package/scripts/forge-team/lib/dashboard.sh +0 -316
  122. package/scripts/forge-team/tests/dashboard.test.sh +0 -155
@@ -161,6 +161,55 @@ const PLAN_SUBSKILL_DEFINITIONS = [
161
161
 
162
162
  const PLAN_SUBSKILL_IDS = new Set(PLAN_SUBSKILL_DEFINITIONS.map(definition => definition.id));
163
163
 
164
+ // The `smith` orchestrator composes the stage skills end-to-end. Its sub-skills
165
+ // are the stage skill NAMES (each a real `skills/<name>/` dir), unlike plan's
166
+ // fine-grained internal planning phases. Both resolve through the ONE generic
167
+ // registry below (keyed by owning skill), so composition is no longer hardcoded
168
+ // to plan. See docs/work kernel-native-skills composition epic (a0776e61) +
169
+ // smith-orchestrator (7da81cbd).
170
+ const SMITH_SUBSKILL_DEFINITIONS = ['plan', 'dev', 'validate', 'ship', 'review', 'verify']
171
+ .map(stage => ({ id: stage, label: `${stage} stage`, owner: 'smith' }));
172
+
173
+ // Skills that `plan` COMPOSES at the whole-skill level (its SKILL.md `subskills:`
174
+ // frontmatter), distinct from the fine-grained plan.* planning micro-phases in
175
+ // PLAN_SUBSKILL_DEFINITIONS. A composed whole-skill is an advertised sub-skill
176
+ // that must both VALIDATE and RESOLVE, but it is NOT a partialInvocation
177
+ // micro-phase (it maps to no runtime-graph action), so it lives OUTSIDE
178
+ // PLAN_SUBSKILL_IDS (which governs planning.template.only/skip).
179
+ const PLAN_COMPOSED_SUBSKILLS = ['research'];
180
+ const PLAN_COMPOSED_SUBSKILL_DEFINITIONS = PLAN_COMPOSED_SUBSKILLS.map(id => ({
181
+ id,
182
+ label: `${id.charAt(0).toUpperCase()}${id.slice(1)} (composed skill)`,
183
+ kind: 'composed-skill',
184
+ owner: 'plan',
185
+ }));
186
+
187
+ // Generic per-skill sub-skill registry, keyed by owning skill id. Generalizes
188
+ // plan's previously hardcoded PLAN_SUBSKILL_DEFINITIONS / validatePlanSubSkillList
189
+ // so any composing skill (plan, smith, ...) declares its sub-skill set through a
190
+ // single mechanism. This registry is the SINGLE source for BOTH validation (ids)
191
+ // and resolution (definitions), so getSubSkillDefinitions and validateSubSkillList
192
+ // can never disagree. plan's set = its planning micro-phases + composed whole-skills.
193
+ const SUBSKILL_REGISTRY = {
194
+ plan: [...PLAN_SUBSKILL_DEFINITIONS, ...PLAN_COMPOSED_SUBSKILL_DEFINITIONS],
195
+ smith: SMITH_SUBSKILL_DEFINITIONS,
196
+ };
197
+
198
+ // Validation id sets DERIVED from the registry definitions — guarantees
199
+ // validate-vs-resolve parity for every owner (a subskill that validates also
200
+ // resolves, and vice versa).
201
+ const SUBSKILL_IDS_BY_OWNER = Object.fromEntries(
202
+ Object.entries(SUBSKILL_REGISTRY).map(([owner, defs]) => [owner, new Set(defs.map(d => d.id))])
203
+ );
204
+
205
+ function getSubSkillDefinitions(owner) {
206
+ return SUBSKILL_REGISTRY[owner] ? [...SUBSKILL_REGISTRY[owner]] : [];
207
+ }
208
+
209
+ function getSubSkillIds(owner) {
210
+ return new Set(SUBSKILL_IDS_BY_OWNER[owner] || []);
211
+ }
212
+
164
213
  function planningSubSkillFromDefinition(definition) {
165
214
  return PlanningSubSkill({
166
215
  id: definition.id,
@@ -777,31 +826,51 @@ function assertStringList(value, errors, code, message) {
777
826
  return valid;
778
827
  }
779
828
 
780
- function validatePlanSubSkillList(value, errors, path) {
781
- if (!assertStringList(
782
- value,
783
- errors,
784
- 'INVALID_PLAN_SUBSKILL_LIST',
785
- `${path} must be a list of planning sub-skill IDs.`
786
- )) {
829
+ function subSkillCodesForOwner(owner) {
830
+ return owner === 'plan'
831
+ ? { listCode: 'INVALID_PLAN_SUBSKILL_LIST', unknownCode: 'UNKNOWN_PLAN_SUBSKILL', noun: 'planning sub-skill' }
832
+ : { listCode: 'INVALID_SUBSKILL_LIST', unknownCode: 'UNKNOWN_SUBSKILL', noun: `${owner} sub-skill` };
833
+ }
834
+
835
+ // Core list validator against an explicit known-id Set. The two callers below
836
+ // deliberately pass DIFFERENT id sets so the two contracts never leak into each
837
+ // other (see validatePlanSubSkillList).
838
+ function validateAgainstKnownIds(knownIds, codes, value, errors, path) {
839
+ const { listCode, unknownCode, noun } = codes;
840
+ if (!assertStringList(value, errors, listCode, `${path} must be a list of ${noun} IDs.`)) {
787
841
  return undefined;
788
842
  }
789
843
 
790
844
  const normalized = value.map(item => item.trim());
791
845
  let hasUnknown = false;
792
846
  for (const id of normalized) {
793
- if (!PLAN_SUBSKILL_IDS.has(id)) {
847
+ if (!knownIds.has(id)) {
794
848
  hasUnknown = true;
795
849
  errors.push({
796
- code: 'UNKNOWN_PLAN_SUBSKILL',
797
- message: `Unknown planning sub-skill '${id}' in ${CONFIG_SOURCE}.`,
850
+ code: unknownCode,
851
+ message: `Unknown ${noun} '${id}' in ${CONFIG_SOURCE}.`,
798
852
  });
799
853
  }
800
854
  }
801
- if (hasUnknown) {
802
- return undefined;
803
- }
804
- return normalized;
855
+ return hasUnknown ? undefined : normalized;
856
+ }
857
+
858
+ // Generic COMPOSITION validator, keyed by owning skill — validates a skill's
859
+ // ADVERTISED (SKILL.md frontmatter) subskills. For `plan` the known-id set is the
860
+ // composition union (plan.* micro-phases + composed whole-skills like research).
861
+ function validateSubSkillList(owner, value, errors, path) {
862
+ const knownIds = SUBSKILL_IDS_BY_OWNER[owner] || new Set();
863
+ return validateAgainstKnownIds(knownIds, subSkillCodesForOwner(owner), value, errors, path);
864
+ }
865
+
866
+ // partialInvocation validator (.forge/config.yaml planning.template.only/skip).
867
+ // SEPARATE contract from composition: it MUST accept ONLY the fine-grained plan.*
868
+ // MICRO-PHASE ids (PLAN_SUBSKILL_IDS), never composed whole-skill ids like
869
+ // `research` — a composed skill maps to no runtime-graph action, so `only:
870
+ // [research]` would be silent dead config. Validates against PLAN_SUBSKILL_IDS,
871
+ // NOT the composition union, so it fails closed on composed ids.
872
+ function validatePlanSubSkillList(value, errors, path) {
873
+ return validateAgainstKnownIds(PLAN_SUBSKILL_IDS, subSkillCodesForOwner('plan'), value, errors, path);
805
874
  }
806
875
 
807
876
  function applyPlanningMode(template, nextTemplate, errors) {
@@ -968,6 +1037,11 @@ module.exports = {
968
1037
  PlanningSubSkill,
969
1038
  Role,
970
1039
  ROLE_IDS,
1040
+ SUBSKILL_REGISTRY,
1041
+ getSubSkillDefinitions,
1042
+ getSubSkillIds,
1043
+ validateSubSkillList,
1044
+ validatePlanSubSkillList,
971
1045
  loadRuntimeGraphConfig,
972
1046
  lintRuntimeGraphConfig,
973
1047
  resolveRuntimeGraph,
@@ -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
  };