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.
- package/AGENTS.md +1 -1
- package/bin/forge-cmd.js +1 -1
- package/bin/forge.js +5 -0
- package/docs/reference/COMMANDS.md +3 -1
- package/docs/reference/shepherd.md +72 -1
- package/lib/adapters/greptile-review-adapter.js +1 -1
- package/lib/adapters/pr-state-adapter.js +103 -8
- package/lib/agents-config.js +5 -0
- package/lib/commands/_issue.js +31 -46
- package/lib/commands/_manifest.js +1 -1
- package/lib/commands/_resolve-command-opts.js +36 -29
- package/lib/commands/claim.js +2 -4
- package/lib/commands/hooks.js +155 -8
- package/lib/commands/plan.js +23 -115
- package/lib/commands/prime.js +8 -1
- package/lib/commands/release.js +1 -2
- package/lib/commands/serve.js +5 -2
- package/lib/commands/setup.js +0 -1
- package/lib/commands/shepherd.js +36 -3
- package/lib/commands/skill.js +275 -0
- package/lib/commands/status.js +37 -32
- package/lib/commands/test.js +32 -0
- package/lib/commands/worktree.js +27 -1
- package/lib/core/runtime-graph.js +88 -14
- package/lib/forge-issues.js +13 -464
- package/lib/harness-capability-matrix.js +2 -2
- package/lib/hook-renderer.js +54 -12
- package/lib/issue-backend.js +42 -3
- package/lib/kernel/broker.js +28 -0
- package/lib/kernel/migrations.js +30 -2
- package/lib/kernel/schema.js +35 -0
- package/lib/kernel/sqlite-driver.js +150 -0
- package/lib/memory-recall.js +151 -0
- package/lib/orientation.js +303 -6
- package/lib/pr-bundle.js +6 -2
- package/lib/pr-monitor/reconcile-executor.js +659 -0
- package/lib/pr-monitor/reconcile-tick.js +138 -0
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-sticky.js +14 -0
- package/lib/pr-monitor/shepherd-lease.js +243 -0
- package/lib/pr-monitor/upsert-sticky.js +1 -1
- package/lib/pr-monitor/watch-lifecycle.js +1 -1
- package/lib/pr-pull.js +70 -15
- package/lib/project-memory.js +8 -0
- package/lib/rules-sync.js +4 -0
- package/lib/runtime-health.js +15 -46
- package/lib/skill-eval.js +750 -0
- package/lib/status/identity.js +46 -0
- package/lib/status/presenter.js +0 -35
- package/lib/status/snapshot.js +11 -16
- package/lib/upgrade-safety.js +8 -9
- package/lib/using-forge.js +315 -0
- package/lib/workflow/enforce-stage.js +5 -5
- package/lib/workflow/state-manager.js +23 -23
- package/package.json +1 -1
- package/rules/using-forge.md +24 -0
- package/scripts/forge-team/index.sh +0 -5
- package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
- package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
- package/scripts/test.js +8 -1
- package/skills/claim-safety/SKILL.md +4 -0
- package/skills/claim-safety/evals/scorecard.json +41 -0
- package/skills/coverage.json +83 -0
- package/skills/dev/SKILL.md +4 -0
- package/skills/dev/evals/scorecard.json +41 -0
- package/skills/gates/SKILL.md +80 -0
- package/skills/gates/evals/evals.json +38 -0
- package/skills/gates/evals/scorecard.json +41 -0
- package/skills/hermes-forge/SKILL.md +1 -0
- package/skills/hermes-forge/evals/scorecard.json +41 -0
- package/skills/issue-basics/SKILL.md +1 -0
- package/skills/issue-basics/evals/scorecard.json +41 -0
- package/skills/kernel/SKILL.md +38 -0
- package/skills/kernel/evals/scorecard.json +41 -0
- package/skills/memory/SKILL.md +16 -1
- package/skills/memory/evals/scorecard.json +41 -0
- package/skills/parallel-deep-research/SKILL.md +1 -0
- package/skills/parallel-deep-research/evals/scorecard.json +41 -0
- package/skills/plan/SKILL.md +6 -0
- package/skills/plan/evals/scorecard.json +41 -0
- package/skills/portability/SKILL.md +47 -0
- package/skills/portability/evals/evals.json +34 -0
- package/skills/portability/evals/scorecard.json +41 -0
- package/skills/research/SKILL.md +1 -0
- package/skills/research/evals/scorecard.json +41 -0
- package/skills/review/SKILL.md +6 -0
- package/skills/review/evals/scorecard.json +41 -0
- package/skills/rollback/SKILL.md +1 -0
- package/skills/rollback/evals/scorecard.json +41 -0
- package/skills/setup/SKILL.md +91 -0
- package/skills/setup/evals/evals.json +42 -0
- package/skills/setup/evals/scorecard.json +41 -0
- package/skills/shepherd/SKILL.md +76 -36
- package/skills/shepherd/evals/evals.json +21 -9
- package/skills/shepherd/evals/scorecard.json +41 -0
- package/skills/ship/SKILL.md +6 -0
- package/skills/ship/evals/scorecard.json +41 -0
- package/skills/smith/SKILL.md +8 -0
- package/skills/smith/evals/scorecard.json +41 -0
- package/skills/sonarcloud/SKILL.md +1 -0
- package/skills/sonarcloud/evals/scorecard.json +41 -0
- package/skills/sonarcloud-analysis/SKILL.md +1 -0
- package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
- package/skills/status/SKILL.md +3 -0
- package/skills/status/evals/scorecard.json +41 -0
- package/skills/triage-ready/SKILL.md +2 -0
- package/skills/triage-ready/evals/scorecard.json +41 -0
- package/skills/using-forge/SKILL.md +104 -0
- package/skills/using-forge/evals/scorecard.json +41 -0
- package/skills/validate/SKILL.md +4 -0
- package/skills/validate/evals/scorecard.json +41 -0
- package/skills/verify/SKILL.md +4 -0
- package/skills/verify/evals/scorecard.json +41 -0
- package/skills/worktree/SKILL.md +87 -0
- package/skills/worktree/evals/evals.json +38 -0
- package/skills/worktree/evals/scorecard.json +41 -0
- package/lib/adapters/beads-issue-adapter.js +0 -127
- package/lib/beads-nudge.js +0 -91
- package/lib/commands/board.js +0 -64
- package/lib/status/beads-snapshot.js +0 -145
- package/scripts/forge-team/lib/dashboard.sh +0 -316
- 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
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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 (!
|
|
847
|
+
if (!knownIds.has(id)) {
|
|
794
848
|
hasUnknown = true;
|
|
795
849
|
errors.push({
|
|
796
|
-
code:
|
|
797
|
-
message: `Unknown
|
|
850
|
+
code: unknownCode,
|
|
851
|
+
message: `Unknown ${noun} '${id}' in ${CONFIG_SOURCE}.`,
|
|
798
852
|
});
|
|
799
853
|
}
|
|
800
854
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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,
|
package/lib/forge-issues.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
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
|
-
|
|
623
|
-
|
|
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
|
|
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
|
|
656
|
-
// drains 'jsonl' —
|
|
657
|
-
//
|
|
658
|
-
|
|
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
|
};
|