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.
- package/.forge/hooks/check-tdd.js +79 -5
- package/.forge/hooks/forge-native-hook.js +194 -8
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +28 -0
- package/QUICKSTART.md +6 -2
- package/README.md +3 -1
- package/bin/forge-cmd.js +1 -1
- package/bin/forge.js +95 -19
- package/docs/guides/SETUP.md +4 -1
- package/docs/guides/SUPPORT.md +5 -0
- package/docs/reference/COMMANDS.md +12 -1
- package/docs/reference/shepherd.md +114 -3
- package/lib/activation/ensure-forge-home.js +135 -0
- package/lib/adapters/beads-kernel-compat.js +67 -0
- package/lib/adapters/greptile-review-adapter.js +1 -1
- package/lib/adapters/pr-state-adapter.js +103 -8
- package/lib/adoption-profiles.js +17 -4
- package/lib/agents-config.js +5 -0
- package/lib/beads-detect.js +60 -0
- package/lib/commands/_aliases.js +248 -0
- package/lib/commands/_issue.js +65 -41
- package/lib/commands/_manifest.js +3 -1
- package/lib/commands/_registry.js +14 -0
- package/lib/commands/_resolve-command-opts.js +36 -60
- package/lib/commands/claim.js +2 -4
- package/lib/commands/gate.js +19 -2
- package/lib/commands/hooks.js +294 -12
- package/lib/commands/init.js +26 -20
- package/lib/commands/memory.js +81 -0
- package/lib/commands/migrate.js +0 -161
- package/lib/commands/plan.js +71 -123
- package/lib/commands/pr.js +88 -0
- package/lib/commands/prime.js +8 -1
- package/lib/commands/push.js +66 -0
- package/lib/commands/recall.js +67 -12
- package/lib/commands/recap.js +18 -4
- package/lib/commands/release.js +15 -3
- package/lib/commands/remember.js +86 -20
- package/lib/commands/serve.js +5 -2
- package/lib/commands/setup.js +135 -73
- package/lib/commands/shepherd.js +102 -4
- package/lib/commands/ship.js +40 -4
- 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 +87 -5
- package/lib/core/runtime-graph.js +122 -17
- package/lib/forge-issues.js +13 -464
- package/lib/gate-events.js +54 -55
- package/lib/global-flags.js +30 -0
- package/lib/grounding/context-events.js +230 -0
- package/lib/grounding/read-first.js +112 -0
- package/lib/harness-capability-matrix.js +2 -2
- package/lib/hook-renderer.js +144 -12
- package/lib/issue-backend.js +42 -3
- package/lib/kernel/backing-issue.js +7 -1
- package/lib/kernel/broker.js +28 -0
- package/lib/kernel/migrations.js +30 -2
- package/lib/kernel/owned-kernel.js +43 -0
- package/lib/kernel/schema.js +35 -0
- package/lib/kernel/sqlite-driver.js +187 -1
- package/lib/memory-recall.js +151 -0
- package/lib/orientation.js +303 -6
- package/lib/pr-bundle.js +6 -2
- package/lib/pr-monitor/auto-actions.js +175 -0
- package/lib/pr-monitor/digest.js +206 -0
- 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 +57 -8
- package/lib/pr-monitor/shepherd-lease.js +243 -0
- package/lib/pr-monitor/upsert-sticky.js +169 -0
- package/lib/pr-monitor/watch-lifecycle.js +1 -1
- package/lib/pr-pull.js +113 -17
- package/lib/project-memory.js +8 -0
- package/lib/release-readiness.js +17 -1
- 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 +52 -1
- package/lib/using-forge.js +315 -0
- package/lib/workflow/enforce-stage.js +64 -7
- package/lib/workflow/state-manager.js +23 -23
- package/package.json +2 -2
- 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/pr-auto-actions.js +93 -0
- package/scripts/pr-verdict-label.js +50 -0
- 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/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
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Forge Skill Command -- the unified "forge skill <verb>" noun.
|
|
5
|
+
*
|
|
6
|
+
* "forge skill for <situation>" is a DETERMINISTIC intent-to-skill router: it reads the canonical
|
|
7
|
+
* skill catalog (skills/*\/SKILL.md frontmatter) and prints the best-fit Forge skill(s) plus WHY,
|
|
8
|
+
* as the reasoning fallback for harnesses without a SessionStart hook that can auto-inject the
|
|
9
|
+
* using-forge dispatch skill. It NEVER opens the kernel and NEVER throws.
|
|
10
|
+
*
|
|
11
|
+
* The noun is structured so later waves can add sibling verbs (forge skill eval, forge skill
|
|
12
|
+
* scores) without a new top-level command -- the owner's command-surface rule: unify related
|
|
13
|
+
* commands under one self-explanatory noun instead of scattering verbs.
|
|
14
|
+
*
|
|
15
|
+
* @module commands/skill
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
const { routeSkill, loadSkillCatalog } = require('../using-forge');
|
|
21
|
+
const skillEval = require('../skill-eval');
|
|
22
|
+
|
|
23
|
+
const USAGE = 'Usage: forge skill for "<situation>" [--json]\n' +
|
|
24
|
+
' forge skill eval [name] --static [--json]\n' +
|
|
25
|
+
' forge skill scores [--json]\n' +
|
|
26
|
+
' forge skill coverage [--json]';
|
|
27
|
+
|
|
28
|
+
/** Extract the situation text (all non-flag args after the verb) and json flag. */
|
|
29
|
+
function parseForArgs(rest, flags) {
|
|
30
|
+
const positional = rest.filter(a => typeof a === 'string' && !a.startsWith('--'));
|
|
31
|
+
const json = flags.json === true || flags['--json'] === true || rest.includes('--json');
|
|
32
|
+
return { situation: positional.join(' ').trim(), json };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Render the human-readable routing answer. */
|
|
36
|
+
function formatRouting(result) {
|
|
37
|
+
const lines = ['Best skill for: "' + result.situation + '"', ''];
|
|
38
|
+
if (result.unknown) {
|
|
39
|
+
lines.push(
|
|
40
|
+
'No confident match. This may not need a Forge skill -- or describe it more concretely.',
|
|
41
|
+
'Fallbacks: `forge ready` for what to work on, or the kernel skill to see the whole surface.',
|
|
42
|
+
);
|
|
43
|
+
return lines.join('\n');
|
|
44
|
+
}
|
|
45
|
+
const [top, ...rest] = result.matches;
|
|
46
|
+
lines.push(
|
|
47
|
+
'-> ' + top.name + ' (' + top.why + ')',
|
|
48
|
+
' Announce: "Using ' + top.name + ' to ..." then follow the skill.',
|
|
49
|
+
);
|
|
50
|
+
if (rest.length > 0) {
|
|
51
|
+
lines.push('', 'Also consider:');
|
|
52
|
+
for (const m of rest) lines.push(' - ' + m.name + ' (' + m.why + ')');
|
|
53
|
+
}
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** "forge skill for <situation>" -- deterministic router. */
|
|
58
|
+
function handleFor(rest, flags) {
|
|
59
|
+
const { situation, json } = parseForArgs(rest, flags);
|
|
60
|
+
if (!situation) {
|
|
61
|
+
return { success: false, error: 'Missing situation.\n' + USAGE };
|
|
62
|
+
}
|
|
63
|
+
// Read the canonical catalog from the Forge PACKAGE (not projectRoot): a set-up consumer
|
|
64
|
+
// project has no root skills/, so the routable skills live in the package assets.
|
|
65
|
+
const catalog = loadSkillCatalog();
|
|
66
|
+
const result = routeSkill(situation, { catalog });
|
|
67
|
+
if (json) {
|
|
68
|
+
return { success: true, result, output: JSON.stringify(result, null, 2) + '\n' };
|
|
69
|
+
}
|
|
70
|
+
return { success: true, result, output: formatRouting(result) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Write a scorecard to skills/<name>/evals/scorecard.json (stable 2-space JSON + trailing NL). */
|
|
74
|
+
function writeScorecard(skillsDir, name, card) {
|
|
75
|
+
const dir = path.join(skillsDir, name, 'evals');
|
|
76
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
77
|
+
fs.writeFileSync(path.join(dir, 'scorecard.json'), JSON.stringify(card, null, 2) + '\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* "forge skill eval [name] --static [--json]" -- compute + persist the DETERMINISTIC scorecard(s).
|
|
82
|
+
* All skills when no name. --static is the only tier today (behavioral is W5); it is accepted (and
|
|
83
|
+
* implied) so the flag reads honestly and future tiers can branch here.
|
|
84
|
+
*/
|
|
85
|
+
function handleEval(rest, flags, projectRoot) {
|
|
86
|
+
const json = flags.json === true || flags['--json'] === true || rest.includes('--json');
|
|
87
|
+
const positional = rest.filter(a => typeof a === 'string' && !a.startsWith('--'));
|
|
88
|
+
const name = positional[0];
|
|
89
|
+
const ctx = skillEval.resolveSkillsContext(projectRoot);
|
|
90
|
+
if (!ctx) {
|
|
91
|
+
return { success: false, error: 'No canonical skills/ directory found (looked in the project and the packaged Forge root).' };
|
|
92
|
+
}
|
|
93
|
+
const { skillsDir, catalog } = ctx;
|
|
94
|
+
const targets = name
|
|
95
|
+
? [name]
|
|
96
|
+
: fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
97
|
+
.filter(e => e.isDirectory() && fs.existsSync(path.join(skillsDir, e.name, 'SKILL.md')))
|
|
98
|
+
.map(e => e.name)
|
|
99
|
+
.sort();
|
|
100
|
+
|
|
101
|
+
const written = [];
|
|
102
|
+
const cards = {};
|
|
103
|
+
for (const target of targets) {
|
|
104
|
+
const card = skillEval.buildScorecard({ skillsDir, name: target, catalog });
|
|
105
|
+
if (!card) {
|
|
106
|
+
if (name) return { success: false, error: `Skill '${name}' not found under ${skillsDir}.` };
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
writeScorecard(skillsDir, target, card);
|
|
110
|
+
written.push(target);
|
|
111
|
+
cards[target] = card;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (json) {
|
|
115
|
+
return { success: true, cards, output: JSON.stringify(name ? cards[name] : cards, null, 2) + '\n' };
|
|
116
|
+
}
|
|
117
|
+
const lines = ['Static scorecards written (deterministic tier):'];
|
|
118
|
+
for (const t of written) lines.push(' ' + t + ' composite=' + cards[t].composite);
|
|
119
|
+
lines.push('', 'Behavioral tier (recall/precision/chains/outcome/variance) is W5.');
|
|
120
|
+
return { success: true, cards, output: lines.join('\n') };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Render the worst-first league table from a scorecards map. */
|
|
124
|
+
function formatScores(scorecards, gate) {
|
|
125
|
+
const rows = Object.values(scorecards)
|
|
126
|
+
.map(c => ({
|
|
127
|
+
skill: c.skill,
|
|
128
|
+
composite: c.composite,
|
|
129
|
+
dq: c.static.description_quality.score,
|
|
130
|
+
tok: c.static.token_cost.score,
|
|
131
|
+
caps: c.static.caps.score,
|
|
132
|
+
fixtures: c.fixtures,
|
|
133
|
+
}))
|
|
134
|
+
.sort((a, b) => a.composite - b.composite || a.skill.localeCompare(b.skill));
|
|
135
|
+
|
|
136
|
+
const lines = ['Skill scores (static tier — worst first). Composite = 0.5*desc-quality + 0.3*token-cost + 0.2*caps.', ''];
|
|
137
|
+
lines.push(' COMPOSITE DESC-Q TOKEN CAPS FIXTURES SKILL');
|
|
138
|
+
for (const r of rows) {
|
|
139
|
+
lines.push(
|
|
140
|
+
' ' + String(r.composite).padStart(9) +
|
|
141
|
+
' ' + String(r.dq).padStart(6) +
|
|
142
|
+
' ' + String(r.tok).padStart(5) +
|
|
143
|
+
' ' + String(r.caps).padStart(4) +
|
|
144
|
+
' ' + r.fixtures.padEnd(11) +
|
|
145
|
+
' ' + r.skill,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (gate.warnings.length > 0) {
|
|
149
|
+
lines.push('', 'Router-reachability warnings (paraphrase gap — W5 judge is the fix, not blocking):');
|
|
150
|
+
for (const w of gate.warnings) lines.push(' - ' + w.skill + ': ' + w.detail);
|
|
151
|
+
}
|
|
152
|
+
lines.push('', gate.passed ? 'CI gate: PASS' : 'CI gate: FAIL (' + gate.failures.length + ')');
|
|
153
|
+
for (const f of gate.failures) lines.push(' x ' + f.skill + ': ' + f.kind + ' — ' + f.detail);
|
|
154
|
+
return lines.join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Render the command-coverage section (a summary line + any failures/warnings). */
|
|
158
|
+
function formatCoverage(coverage) {
|
|
159
|
+
const lines = [
|
|
160
|
+
'Skill coverage (every registered command must own a skill or be exempt):',
|
|
161
|
+
' commands=' + coverage.total + ' mapped=' + coverage.mapped + ' exempt=' + coverage.exempt +
|
|
162
|
+
' gaps=' + coverage.failures.length,
|
|
163
|
+
];
|
|
164
|
+
if (coverage.warnings.length > 0) {
|
|
165
|
+
lines.push('', 'Stale coverage.json entries (non-blocking — remove them):');
|
|
166
|
+
for (const w of coverage.warnings) lines.push(' - ' + w.command + ': ' + w.kind);
|
|
167
|
+
}
|
|
168
|
+
lines.push('', coverage.passed ? 'Coverage gate: PASS' : 'Coverage gate: FAIL (' + coverage.failures.length + ')');
|
|
169
|
+
for (const f of coverage.failures) lines.push(' x ' + f.command + ': ' + f.kind + ' — ' + f.detail);
|
|
170
|
+
return lines.join('\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Build the combined gate error string (static scorecard gate + command-coverage gate). */
|
|
174
|
+
function buildScoresError(gate, coverage) {
|
|
175
|
+
const parts = [];
|
|
176
|
+
if (!gate.passed) {
|
|
177
|
+
parts.push('static gate (' + gate.failures.length + '): ' + gate.failures.map(f => f.skill + ' — ' + f.kind).join('; '));
|
|
178
|
+
}
|
|
179
|
+
if (coverage && !coverage.passed) {
|
|
180
|
+
parts.push('coverage gate (' + coverage.failures.length + '): ' + coverage.failures.map(f => f.command + ' — ' + f.kind).join('; '));
|
|
181
|
+
}
|
|
182
|
+
return 'Skill CI gate FAILED — ' + parts.join(' | ');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** "forge skill scores [--json]" -- the league table. Gate state is drift-aware so it agrees with CI. */
|
|
186
|
+
function handleScores(rest, flags, projectRoot) {
|
|
187
|
+
const json = flags.json === true || flags['--json'] === true || rest.includes('--json');
|
|
188
|
+
const ctx = skillEval.resolveSkillsContext(projectRoot);
|
|
189
|
+
if (!ctx) {
|
|
190
|
+
return { success: false, error: 'No canonical skills/ directory found (looked in the project and the packaged Forge root).' };
|
|
191
|
+
}
|
|
192
|
+
const { skillsDir, catalog, source } = ctx;
|
|
193
|
+
const scorecards = skillEval.buildAllScorecards(skillsDir, catalog);
|
|
194
|
+
// Compare the recomputed cards against the COMMITTED artifacts (canonical skills/ AND the
|
|
195
|
+
// .agents/skills mirror). A stale/missing committed scorecard is drift, so the gate reported here
|
|
196
|
+
// is FAIL — matching the CI drift test — instead of a hollow PASS over freshly-rebuilt cards.
|
|
197
|
+
// Gate the mirror by CONTEXT, not existence: a SOURCE checkout (source==='project') is EXPECTED
|
|
198
|
+
// to ship the committed .agents/skills mirror, so pass mirrorDir UNCONDITIONALLY — a deleted or
|
|
199
|
+
// never-checked-out mirror then REPORTS drift instead of silently passing. From the PACKAGED root
|
|
200
|
+
// (source==='package', a consumer install) no mirror ships, so omit the check to avoid false drift.
|
|
201
|
+
const mirrorDir = source === 'project' ? path.join(path.dirname(skillsDir), '.agents', 'skills') : null;
|
|
202
|
+
const drift = skillEval.detectScorecardDrift({ skillsDir, freshCards: scorecards, mirrorDir });
|
|
203
|
+
const gate = skillEval.evaluateGate(scorecards, { drift });
|
|
204
|
+
// Command-coverage gate (§3.3): a registered command with no owning skill (and not exempt) must
|
|
205
|
+
// FAIL scores too, so CI running `forge skill scores` catches a new unrouted command — not only
|
|
206
|
+
// the dedicated `forge skill coverage`.
|
|
207
|
+
const coverage = skillEval.buildCoverageReport(projectRoot);
|
|
208
|
+
// The gate verdict MUST drive the command's exit status: a failing gate (scorecard drift, caps
|
|
209
|
+
// violation, invalid fixtures, OR a coverage gap) returns success:false so the registry runner
|
|
210
|
+
// exits non-zero and a CI job running `forge skill scores` actually FAILS — instead of exiting 0
|
|
211
|
+
// while the output says "gate: FAIL". The full league table + gate detail still ride along.
|
|
212
|
+
const coveragePassed = !coverage || coverage.passed === true;
|
|
213
|
+
const passed = gate.passed === true && coveragePassed;
|
|
214
|
+
const gateError = passed ? undefined : buildScoresError(gate, coverage);
|
|
215
|
+
if (json) {
|
|
216
|
+
return { success: passed, error: gateError, scorecards, gate, coverage, drift, output: JSON.stringify({ scorecards, gate, coverage, drift }, null, 2) + '\n' };
|
|
217
|
+
}
|
|
218
|
+
const text = coverage ? formatScores(scorecards, gate) + '\n\n' + formatCoverage(coverage) : formatScores(scorecards, gate);
|
|
219
|
+
return { success: passed, error: gateError, scorecards, gate, coverage, drift, output: text };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** "forge skill coverage [--json]" -- the dedicated command→skill coverage gate. */
|
|
223
|
+
function handleCoverage(rest, flags, projectRoot) {
|
|
224
|
+
const json = flags.json === true || flags['--json'] === true || rest.includes('--json');
|
|
225
|
+
const report = skillEval.buildCoverageReport(projectRoot);
|
|
226
|
+
if (!report) {
|
|
227
|
+
return { success: false, error: 'No canonical skills/ directory found (looked in the project and the packaged Forge root).' };
|
|
228
|
+
}
|
|
229
|
+
const passed = report.passed === true;
|
|
230
|
+
const gateError = passed
|
|
231
|
+
? undefined
|
|
232
|
+
: 'Skill coverage gate FAILED (' + report.failures.length + '): ' +
|
|
233
|
+
report.failures.map(f => f.command + ' — ' + f.kind).join('; ');
|
|
234
|
+
if (json) {
|
|
235
|
+
return { success: passed, error: gateError, report, output: JSON.stringify(report, null, 2) + '\n' };
|
|
236
|
+
}
|
|
237
|
+
return { success: passed, error: gateError, report, output: formatCoverage(report) };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = {
|
|
241
|
+
name: 'skill',
|
|
242
|
+
description: 'Route, evaluate, score, and coverage-check Forge skills (forge skill for | eval | scores | coverage)',
|
|
243
|
+
usage: USAGE,
|
|
244
|
+
flags: {
|
|
245
|
+
'--json': 'Emit the machine-readable result',
|
|
246
|
+
'--static': 'Score only the deterministic static tier (the only tier today; behavioral is W5)',
|
|
247
|
+
},
|
|
248
|
+
// flags is the LAST declared param so its `= {}` default is trailing (SonarCloud S1788). The
|
|
249
|
+
// registry still passes (args, flags, projectRoot, opts) — the router reads the canonical catalog
|
|
250
|
+
// from the package root; eval/scores read the canonical skills/ source dir.
|
|
251
|
+
handler: (args, flags = {}, projectRoot) => {
|
|
252
|
+
const verb = args[0];
|
|
253
|
+
if (verb === 'for') {
|
|
254
|
+
return handleFor(args.slice(1), flags);
|
|
255
|
+
}
|
|
256
|
+
if (verb === 'eval') {
|
|
257
|
+
return handleEval(args.slice(1), flags, projectRoot);
|
|
258
|
+
}
|
|
259
|
+
if (verb === 'scores') {
|
|
260
|
+
return handleScores(args.slice(1), flags, projectRoot);
|
|
261
|
+
}
|
|
262
|
+
if (verb === 'coverage') {
|
|
263
|
+
return handleCoverage(args.slice(1), flags, projectRoot);
|
|
264
|
+
}
|
|
265
|
+
if (!verb) {
|
|
266
|
+
return { success: false, error: 'Missing verb.\n' + USAGE };
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
success: false,
|
|
270
|
+
error: "Unknown verb '" + verb + "'. Supported: for, eval, scores, coverage.\n" + USAGE,
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
// Exposed for unit tests; not part of the CLI surface.
|
|
274
|
+
_internal: { parseForArgs, formatRouting, handleFor, handleEval, handleScores, handleCoverage, formatScores, formatCoverage },
|
|
275
|
+
};
|
package/lib/commands/status.js
CHANGED
|
@@ -18,7 +18,7 @@ const {
|
|
|
18
18
|
const {
|
|
19
19
|
loadState,
|
|
20
20
|
extractWorkflowStateFromComments,
|
|
21
|
-
|
|
21
|
+
readWorkflowStateFromIssue,
|
|
22
22
|
} = require('../workflow/state-manager');
|
|
23
23
|
const { secureExecFileSync } = require('../shell-utils.js');
|
|
24
24
|
|
|
@@ -140,12 +140,12 @@ function analyzeChecks(context) {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
|
-
* Analyze
|
|
144
|
-
* @param {object}
|
|
145
|
-
* @returns {object}
|
|
143
|
+
* Analyze kernel issue state
|
|
144
|
+
* @param {object} issueRecord - kernel issue record
|
|
145
|
+
* @returns {object} issue analysis
|
|
146
146
|
*/
|
|
147
|
-
function
|
|
148
|
-
if (!
|
|
147
|
+
function analyzeIssue(issueRecord) {
|
|
148
|
+
if (!issueRecord) {
|
|
149
149
|
return {
|
|
150
150
|
hasActiveIssue: false,
|
|
151
151
|
issueStatus: null,
|
|
@@ -155,10 +155,10 @@ function analyzeBeads(beadsIssue) {
|
|
|
155
155
|
|
|
156
156
|
return {
|
|
157
157
|
hasActiveIssue: true,
|
|
158
|
-
issueStatus:
|
|
159
|
-
issueType:
|
|
160
|
-
isInProgress:
|
|
161
|
-
isClosed:
|
|
158
|
+
issueStatus: issueRecord.status,
|
|
159
|
+
issueType: issueRecord.type,
|
|
160
|
+
isInProgress: issueRecord.status === 'in_progress',
|
|
161
|
+
isClosed: issueRecord.status === 'closed',
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
164
|
|
|
@@ -178,7 +178,7 @@ function calculateConfidence(factors, stage) {
|
|
|
178
178
|
files: 30,
|
|
179
179
|
pr: 25,
|
|
180
180
|
checks: 15,
|
|
181
|
-
|
|
181
|
+
issue: 10,
|
|
182
182
|
};
|
|
183
183
|
|
|
184
184
|
// Branch state
|
|
@@ -235,16 +235,16 @@ function calculateConfidence(factors, stage) {
|
|
|
235
235
|
score += weights.checks / 2; // Partial match
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
//
|
|
239
|
-
maxScore += weights.
|
|
240
|
-
if (factors.
|
|
241
|
-
score += weights.
|
|
242
|
-
} else if (factors.
|
|
243
|
-
score += weights.
|
|
244
|
-
} else if (!factors.
|
|
245
|
-
score += weights.
|
|
238
|
+
// Issue state
|
|
239
|
+
maxScore += weights.issue;
|
|
240
|
+
if (factors.issue.isInProgress && stage >= 2 && stage <= 7) {
|
|
241
|
+
score += weights.issue; // Active work
|
|
242
|
+
} else if (factors.issue.isClosed && stage >= 8) {
|
|
243
|
+
score += weights.issue; // Closed for late stages
|
|
244
|
+
} else if (!factors.issue.hasActiveIssue && stage === 1) {
|
|
245
|
+
score += weights.issue; // No issue for fresh start
|
|
246
246
|
} else {
|
|
247
|
-
score += weights.
|
|
247
|
+
score += weights.issue / 2; // Partial match
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
const percentage = Math.round((score / maxScore) * 100);
|
|
@@ -275,21 +275,21 @@ function detectStage(context) {
|
|
|
275
275
|
const fileFactors = analyzeFiles(context);
|
|
276
276
|
const prFactors = analyzePR(context.pr);
|
|
277
277
|
const checkFactors = analyzeChecks(context);
|
|
278
|
-
const
|
|
278
|
+
const issueFactors = analyzeIssue(context.issueRecord);
|
|
279
279
|
|
|
280
280
|
const factors = {
|
|
281
281
|
branch: branchFactors,
|
|
282
282
|
files: fileFactors,
|
|
283
283
|
pr: prFactors,
|
|
284
284
|
checks: checkFactors,
|
|
285
|
-
|
|
285
|
+
issue: issueFactors,
|
|
286
286
|
};
|
|
287
287
|
|
|
288
288
|
// Stage detection logic
|
|
289
289
|
let stage;
|
|
290
290
|
|
|
291
291
|
// Stage 9: PR merged, verify docs
|
|
292
|
-
if (prFactors.prMerged &&
|
|
292
|
+
if (prFactors.prMerged && issueFactors.isClosed) {
|
|
293
293
|
stage = 9;
|
|
294
294
|
}
|
|
295
295
|
// Stage 8: PR approved, ready to merge
|
|
@@ -317,7 +317,7 @@ function detectStage(context) {
|
|
|
317
317
|
stage = 3;
|
|
318
318
|
}
|
|
319
319
|
// Stage 2: Research in progress
|
|
320
|
-
else if (
|
|
320
|
+
else if (issueFactors.issueType === 'research' && issueFactors.isInProgress) {
|
|
321
321
|
stage = 2;
|
|
322
322
|
}
|
|
323
323
|
// Stage 1: Fresh project (default)
|
|
@@ -612,7 +612,7 @@ function resolveWorkflowState(inputs) {
|
|
|
612
612
|
issueId: inputs.issueId,
|
|
613
613
|
issue: inputs.issue,
|
|
614
614
|
comments: inputs.bdComments,
|
|
615
|
-
|
|
615
|
+
preferIssueLookup: inputs.preferIssueLookup,
|
|
616
616
|
});
|
|
617
617
|
|
|
618
618
|
return { workflowState: state, fallbackReason: null };
|
|
@@ -649,7 +649,7 @@ function buildAuthoritativeStatus(workflowState) {
|
|
|
649
649
|
branch: {},
|
|
650
650
|
pr: {},
|
|
651
651
|
checks: { allChecksPass: workflowState.completedStages.includes('validate') },
|
|
652
|
-
|
|
652
|
+
issue: { hasActiveIssue: true },
|
|
653
653
|
},
|
|
654
654
|
};
|
|
655
655
|
}
|
|
@@ -757,9 +757,14 @@ module.exports = {
|
|
|
757
757
|
'--json': 'Emit the machine-readable status envelope',
|
|
758
758
|
},
|
|
759
759
|
handler: async (args, flags, projectRoot) => {
|
|
760
|
+
// Injectable snapshot read, same convention as setup.js's `options.commandRunner`
|
|
761
|
+
// (lib/commands/setup.js:223): a real default with a test-supplied override. Tests
|
|
762
|
+
// need a snapshot with controlled ids and orderings, which a seeded kernel cannot
|
|
763
|
+
// give them — the kernel stamps its own updated_at.
|
|
764
|
+
const readSnapshot = flags?.readStatusSnapshot || readStatusSnapshot;
|
|
760
765
|
const inputs = parseStatusInputs(args, flags);
|
|
761
766
|
const isZeroArgStatus = !inputs.issueId && !inputs.workflowState && !inputs.bdComments;
|
|
762
|
-
inputs.
|
|
767
|
+
inputs.preferIssueLookup = !isZeroArgStatus && Boolean(inputs.issueId || inputs.bdComments);
|
|
763
768
|
if (!inputs.projectRoot && projectRoot) {
|
|
764
769
|
inputs.projectRoot = projectRoot;
|
|
765
770
|
}
|
|
@@ -769,11 +774,11 @@ module.exports = {
|
|
|
769
774
|
|
|
770
775
|
if (isZeroArgStatus) {
|
|
771
776
|
context = detectRepoContext(effectiveProjectRoot);
|
|
772
|
-
// Read from the
|
|
773
|
-
//
|
|
777
|
+
// Read from the Kernel, the only issue backend. Previously hard-wired to
|
|
778
|
+
// the retired Beads
|
|
774
779
|
// issues.jsonl, which is empty on a kernel-default repo — so the flagship
|
|
775
780
|
// view showed "Ready: none" despite a populated kernel (bug 40f35797).
|
|
776
|
-
snapshot = await
|
|
781
|
+
snapshot = await readSnapshot(effectiveProjectRoot, {
|
|
777
782
|
now: inputs.now,
|
|
778
783
|
staleAfterDays: inputs.staleAfterDays,
|
|
779
784
|
issueBackend: inputs.issueBackend,
|
|
@@ -834,7 +839,7 @@ module.exports = {
|
|
|
834
839
|
extractWorkflowStateFromComments,
|
|
835
840
|
resolveWorkflowState,
|
|
836
841
|
parseStatusInputs,
|
|
837
|
-
|
|
842
|
+
readWorkflowStateFromIssue,
|
|
838
843
|
detectStage,
|
|
839
844
|
detectRepoContext,
|
|
840
845
|
discoverCurrentIssue,
|
|
@@ -842,7 +847,7 @@ module.exports = {
|
|
|
842
847
|
analyzeFiles,
|
|
843
848
|
analyzePR,
|
|
844
849
|
analyzeChecks,
|
|
845
|
-
|
|
850
|
+
analyzeIssue,
|
|
846
851
|
calculateConfidence,
|
|
847
852
|
formatStatus,
|
|
848
853
|
extractDesignSlugs,
|
package/lib/commands/test.js
CHANGED
|
@@ -34,12 +34,40 @@ const DIRECT_TEST_CANDIDATES = Object.freeze({
|
|
|
34
34
|
'test/setup-github-sync.test.js',
|
|
35
35
|
'test/setup-runtime-flags.test.js',
|
|
36
36
|
],
|
|
37
|
+
// The second CLI surface (command descriptions + shepherd wiring). Without an
|
|
38
|
+
// entry here it resolves to zero tests and pushes fall back to the full suite.
|
|
39
|
+
'bin/forge-cmd.js': [
|
|
40
|
+
'test/cli/forge-cmd.test.js',
|
|
41
|
+
'test/forge-cmd-shepherd.test.js',
|
|
42
|
+
],
|
|
43
|
+
'bin/forge-preflight.js': ['test/bin/forge-preflight.test.js'],
|
|
37
44
|
'lib/lefthook-check.js': ['test/lefthook-check.test.js', 'test/runtime-health.test.js'],
|
|
38
45
|
'lib/runtime-health.js': ['test/runtime-health.test.js'],
|
|
46
|
+
// skill-eval hosts the accuracy-lint detectors (auditCommandDocumentation /
|
|
47
|
+
// auditRouterPrecision), so edits there must also run their detector suite.
|
|
48
|
+
'lib/skill-eval.js': ['test/skill-eval.test.js', 'test/skill-accuracy.test.js'],
|
|
39
49
|
'lib/upgrade-safety.js': ['test/commands/upgrade.test.js'],
|
|
40
50
|
'scripts/test.js': ['test/scripts/test-runner.test.js'],
|
|
41
51
|
});
|
|
42
52
|
|
|
53
|
+
// Skill sources (canonical `skills/**` and the committed `.agents/skills/**` mirror)
|
|
54
|
+
// map to the fast skill suite. This is what keeps a skills-only PR on the targeted
|
|
55
|
+
// lane instead of the full ~1500-test suite. `skills-sync-drift` guards the mirror.
|
|
56
|
+
const SKILL_TEST_TARGETS = Object.freeze([
|
|
57
|
+
'test/skills-structure.test.js',
|
|
58
|
+
'test/skill-coverage.test.js',
|
|
59
|
+
'test/skill-eval.test.js',
|
|
60
|
+
'test/skill-accuracy.test.js',
|
|
61
|
+
'test/skill-dispatch-parity.test.js',
|
|
62
|
+
'test/using-forge.test.js',
|
|
63
|
+
'test/structural/skills-sync-drift.test.js',
|
|
64
|
+
'test/skills/chain-integrity.test.js',
|
|
65
|
+
'test/skills/skills-sync.test.js',
|
|
66
|
+
'test/skills/stage-skills.test.js',
|
|
67
|
+
'test/skills/using-forge-skill.test.js',
|
|
68
|
+
'test/skills/context-cost.test.js',
|
|
69
|
+
]);
|
|
70
|
+
|
|
43
71
|
/**
|
|
44
72
|
* Detect the package manager by checking which lockfile exists.
|
|
45
73
|
*
|
|
@@ -183,6 +211,10 @@ function getTestCandidatesForChangedFile(file) {
|
|
|
183
211
|
return [file];
|
|
184
212
|
}
|
|
185
213
|
|
|
214
|
+
if (file.startsWith('skills/') || file.startsWith('.agents/skills/')) {
|
|
215
|
+
return [...SKILL_TEST_TARGETS];
|
|
216
|
+
}
|
|
217
|
+
|
|
186
218
|
if (file === 'README.md'
|
|
187
219
|
|| file === 'QUICKSTART.md'
|
|
188
220
|
|| file === 'CHANGELOG.md'
|
package/lib/commands/worktree.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { execFileSync, spawnSync } = require('node:child_process');
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
|
+
const { detectDefaultBranch } = require('../beads-sync-scaffold');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Forge Worktree Command
|
|
@@ -53,6 +54,40 @@ function branchExists(branchName, runFile) {
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* True when a git ref resolves to a commit (branch, tag, remote-tracking ref, or SHA).
|
|
59
|
+
* @param {string} ref - The ref to check
|
|
60
|
+
* @param {string} projectRoot - Repo root to run git in
|
|
61
|
+
* @param {Function} runFile - execFileSync function (for DI)
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
function refExists(ref, projectRoot, runFile) {
|
|
65
|
+
try {
|
|
66
|
+
runFile('git', ['-C', projectRoot, 'rev-parse', '--verify', '--quiet', `${ref}^{commit}`], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
67
|
+
return true;
|
|
68
|
+
} catch (_err) { /* intentional: ref does not resolve */ // NOSONAR S2486
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the base ref a NEW worktree branch should fork from. Defaults to the
|
|
75
|
+
* repo's DEFAULT branch (NOT the checkout's current HEAD) so a worktree created
|
|
76
|
+
* off a WIP branch never silently inherits unrelated commits. Prefers the remote
|
|
77
|
+
* default (`origin/<default>`) when present, else the local default branch, and
|
|
78
|
+
* falls back to the detected name so git surfaces a clear error if neither exists.
|
|
79
|
+
* @param {string} projectRoot - Repo root
|
|
80
|
+
* @param {Function} runFile - execFileSync function (for DI)
|
|
81
|
+
* @returns {string} The base ref to pass to `git worktree add ... <base>`
|
|
82
|
+
*/
|
|
83
|
+
function resolveDefaultBase(projectRoot, runFile) {
|
|
84
|
+
const def = detectDefaultBranch(projectRoot, { _exec: runFile });
|
|
85
|
+
const originRef = `origin/${def}`;
|
|
86
|
+
if (refExists(originRef, projectRoot, runFile)) return originRef;
|
|
87
|
+
if (refExists(def, projectRoot, runFile)) return def;
|
|
88
|
+
return def;
|
|
89
|
+
}
|
|
90
|
+
|
|
56
91
|
/**
|
|
57
92
|
* True when a link failure is a privilege/support problem (Windows without the
|
|
58
93
|
* symlink privilege, restricted FS) rather than a real error. In that case we
|
|
@@ -278,6 +313,14 @@ async function handleCreate(slug, flags, projectRoot, opts) {
|
|
|
278
313
|
|
|
279
314
|
const worktreePath = path.resolve(worktreesDir, slug);
|
|
280
315
|
|
|
316
|
+
// Validate an explicit --base up front so a bad ref errors BEFORE anything is
|
|
317
|
+
// created (no branch, no worktree dir). Base only applies when creating a NEW
|
|
318
|
+
// branch; an existing branch is checked out as-is.
|
|
319
|
+
const explicitBase = flags['--base'] || null;
|
|
320
|
+
if (explicitBase && !refExists(explicitBase, projectRoot, runFile)) {
|
|
321
|
+
return { success: false, error: `Invalid --base: ref '${explicitBase}' not found. Verify it exists (git rev-parse --verify ${explicitBase}).` };
|
|
322
|
+
}
|
|
323
|
+
|
|
281
324
|
// Step 0: Check if worktree already exists
|
|
282
325
|
if (fsApi.existsSync(worktreePath)) {
|
|
283
326
|
// A pre-existing worktree may be checked out on a different branch than the
|
|
@@ -303,12 +346,17 @@ async function handleCreate(slug, flags, projectRoot, opts) {
|
|
|
303
346
|
// Step 1: Ensure .worktrees/ dir exists
|
|
304
347
|
fsApi.mkdirSync(worktreesDir, { recursive: true });
|
|
305
348
|
|
|
306
|
-
// Step 2: Create git worktree
|
|
349
|
+
// Step 2: Create git worktree. For a NEW branch, fork from an explicit --base or
|
|
350
|
+
// the repo's DEFAULT branch — NOT the checkout's current HEAD — so the worktree
|
|
351
|
+
// never silently inherits unrelated WIP commits (B2). An existing branch is
|
|
352
|
+
// checked out as-is (no base applies).
|
|
307
353
|
const hasBranch = branchExists(branchName, runFile);
|
|
354
|
+
let base = null;
|
|
308
355
|
if (hasBranch) {
|
|
309
356
|
runFile('git', ['worktree', 'add', worktreePath, branchName], { stdio: 'pipe' });
|
|
310
357
|
} else {
|
|
311
|
-
|
|
358
|
+
base = explicitBase || resolveDefaultBase(projectRoot, runFile);
|
|
359
|
+
runFile('git', ['worktree', 'add', worktreePath, '-b', branchName, base], { stdio: 'pipe' });
|
|
312
360
|
}
|
|
313
361
|
|
|
314
362
|
// Step 3: Populate node_modules (link to the shared install, else install).
|
|
@@ -329,14 +377,19 @@ async function handleCreate(slug, flags, projectRoot, opts) {
|
|
|
329
377
|
const linkage = await registerWorktreeLinkage({ projectRoot, worktreePath, branch: branchName, issueId, workFolder, opts });
|
|
330
378
|
const backing = await autoFileBackingIssue({ projectRoot, worktreePath, branch: branchName, issueId, opts });
|
|
331
379
|
|
|
380
|
+
// Report the base so the fork point is never silent. `base` is null when an
|
|
381
|
+
// existing branch was checked out (no fork happened).
|
|
382
|
+
const baseNote = base ? `based on ${base}` : `existing branch ${branchName}`;
|
|
332
383
|
return {
|
|
333
384
|
success: true,
|
|
334
385
|
worktreePath,
|
|
335
386
|
branch: branchName,
|
|
387
|
+
base,
|
|
336
388
|
depsLinked: deps.linked,
|
|
337
389
|
depsInstalled: deps.installed,
|
|
338
390
|
linkage,
|
|
339
391
|
backing,
|
|
392
|
+
output: `Created worktree ${worktreePath} on ${branchName} (${baseNote}).`,
|
|
340
393
|
};
|
|
341
394
|
}
|
|
342
395
|
|
|
@@ -353,12 +406,38 @@ async function handleList(projectRoot, opts) {
|
|
|
353
406
|
const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
|
|
354
407
|
driver = (await buildMigratedKernelIssueDeps({ projectRoot })).kernelDriver;
|
|
355
408
|
}
|
|
356
|
-
|
|
409
|
+
// `forge worktree remove` / `forge clean` delete the checkout but do not yet
|
|
410
|
+
// reconcile the kernel_worktrees registry (issue ceeef92f), so the raw registry
|
|
411
|
+
// can still list already-removed paths. Filter to worktrees that still exist on
|
|
412
|
+
// disk so `forge worktree list` never surfaces stale entries agents could act on.
|
|
413
|
+
const existsSync = (opts._fs && opts._fs.existsSync) || require('node:fs').existsSync;
|
|
414
|
+
const worktrees = driver.listWorktrees({}).filter((w) => w && w.path && existsSync(w.path));
|
|
415
|
+
return { success: true, worktrees, output: renderWorktreeList(worktrees) };
|
|
357
416
|
} catch (error) {
|
|
358
417
|
return { success: false, error: error.message, worktrees: [] };
|
|
359
418
|
}
|
|
360
419
|
}
|
|
361
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Render the worktree registry as human-readable text for the CLI. The dispatcher
|
|
423
|
+
* prints only `result.output`, so without this `forge worktree list` is silent.
|
|
424
|
+
* @param {Array<object>} worktrees - Rows from driver.listWorktrees()
|
|
425
|
+
* @returns {string} One line per worktree (path, branch, linked issue, state).
|
|
426
|
+
*/
|
|
427
|
+
function renderWorktreeList(worktrees) {
|
|
428
|
+
if (!Array.isArray(worktrees) || worktrees.length === 0) {
|
|
429
|
+
return 'No worktrees registered.';
|
|
430
|
+
}
|
|
431
|
+
const lines = worktrees.map((w) => {
|
|
432
|
+
const parts = [w.path || '(unknown path)'];
|
|
433
|
+
if (w.branch) parts.push(`branch: ${w.branch}`);
|
|
434
|
+
if (w.issue_id) parts.push(`issue: ${w.issue_id}`);
|
|
435
|
+
if (w.state) parts.push(w.state);
|
|
436
|
+
return ` ${parts.join(' | ')}`;
|
|
437
|
+
});
|
|
438
|
+
return `Registered worktrees (${worktrees.length}):\n${lines.join('\n')}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
362
441
|
/**
|
|
363
442
|
* Handle the "remove" subcommand.
|
|
364
443
|
* @param {string} slug - Worktree slug
|
|
@@ -383,8 +462,8 @@ async function handleRemove(slug, projectRoot, opts) {
|
|
|
383
462
|
}
|
|
384
463
|
|
|
385
464
|
// Long flags that take a value, in both `--flag value` and `--flag=value` forms.
|
|
386
|
-
const WORKTREE_VALUE_FLAGS = ['--branch', '--issue', '--work-folder'];
|
|
387
|
-
const WORKTREE_USAGE_HINT = 'Usage: forge worktree create <slug> [--branch <name>] [--issue <id>] [--work-folder <path>]';
|
|
465
|
+
const WORKTREE_VALUE_FLAGS = ['--branch', '--issue', '--work-folder', '--base'];
|
|
466
|
+
const WORKTREE_USAGE_HINT = 'Usage: forge worktree create <slug> [--branch <name>] [--base <ref>] [--issue <id>] [--work-folder <path>]';
|
|
388
467
|
|
|
389
468
|
function parseWorktreeArgs(args, flags) {
|
|
390
469
|
const positional = [];
|
|
@@ -429,6 +508,7 @@ module.exports = {
|
|
|
429
508
|
usage: 'forge worktree <create|remove|list> <slug>',
|
|
430
509
|
flags: {
|
|
431
510
|
'--branch': 'Custom branch name (default: feat/<slug>)',
|
|
511
|
+
'--base': 'Base ref a new branch forks from (default: the repo default branch, e.g. origin/main)',
|
|
432
512
|
'--issue': 'Kernel issue id to link this worktree to (records issue → worktree)',
|
|
433
513
|
'--work-folder': 'Repo-relative work-folder this issue owns (records worktree → work-folder + drops a .forge-issue marker)',
|
|
434
514
|
},
|
|
@@ -486,5 +566,7 @@ module.exports = {
|
|
|
486
566
|
setupWorktreeDeps,
|
|
487
567
|
runInstall,
|
|
488
568
|
autoFileBackingIssue,
|
|
569
|
+
refExists,
|
|
570
|
+
resolveDefaultBase,
|
|
489
571
|
},
|
|
490
572
|
};
|