@izkac/forgekit 0.3.12 → 0.3.13
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/bin/forge.mjs +107 -107
- package/bin/forgekit.mjs +83 -83
- package/bin/review.mjs +81 -81
- package/package.json +1 -1
- package/scripts/prepack.mjs +78 -78
- package/scripts/run-tests.mjs +50 -50
- package/src/adr.mjs +236 -236
- package/src/adr.test.mjs +170 -170
- package/src/change.mjs +327 -234
- package/src/change.test.mjs +145 -83
- package/src/cleanup-sessions.mjs +84 -84
- package/src/config.mjs +103 -103
- package/src/defer.mjs +75 -75
- package/src/doctor.mjs +350 -341
- package/src/doctor.test.mjs +114 -114
- package/src/init.mjs +680 -621
- package/src/install.mjs +815 -815
- package/src/install.test.mjs +180 -180
- package/src/integrity-check.mjs +60 -60
- package/src/integrity.mjs +682 -682
- package/src/integrity.test.mjs +566 -566
- package/src/lib.mjs +143 -143
- package/src/models.defaults.json +41 -41
- package/src/new-session.mjs +99 -99
- package/src/openspec-overlays/README.md +19 -19
- package/src/openspec-overlays/openspec-apply-change-footer.md +14 -14
- package/src/openspec-overlays/opsx-apply-completion-step.md +1 -1
- package/src/openspec-overlays/opsx-apply-implement-step.md +11 -11
- package/src/paths.mjs +92 -92
- package/src/plan-engine.mjs +321 -278
- package/src/plan-engine.test.mjs +447 -283
- package/src/preferences.defaults.json +78 -78
- package/src/preferences.mjs +438 -438
- package/src/preferences.test.mjs +174 -174
- package/src/record-evidence.mjs +204 -204
- package/src/resolve-model.mjs +312 -312
- package/src/resolve-model.test.mjs +194 -194
- package/src/review/cli.test.mjs +117 -117
- package/src/review/export.mjs +172 -172
- package/src/review/export.test.mjs +197 -197
- package/src/review/fixtures/valid-review.json +42 -42
- package/src/review/lib.mjs +894 -894
- package/src/review/lib.test.mjs +266 -266
- package/src/review/schema.json +196 -196
- package/src/review/signals.test.mjs +62 -62
- package/src/score-cli.mjs +68 -68
- package/src/score.mjs +568 -568
- package/src/score.test.mjs +366 -366
- package/src/session-reminder.mjs +207 -207
- package/src/session-status.mjs +70 -70
- package/src/set-models.mjs +186 -186
- package/src/set-phase.mjs +205 -205
- package/src/set-prefs.mjs +294 -294
- package/src/specs-sync.mjs +234 -0
- package/src/specs-sync.test.mjs +114 -0
- package/src/spine.mjs +93 -93
- package/src/triage-prompt.mjs +175 -175
- package/src/triage-prompt.test.mjs +50 -50
- package/src/vendor-openspec-overlays.mjs +176 -176
- package/src/vendor-openspec-overlays.test.mjs +62 -62
- package/vendor/skills/archive-to-adr/SKILL.md +149 -149
- package/vendor/skills/forge/SKILL.md +136 -136
- package/vendor/skills/forge/docs/forge.md +650 -647
- package/vendor/skills/forge/phases/brainstorm.md +23 -23
- package/vendor/skills/forge/phases/finish.md +90 -87
- package/vendor/skills/forge/phases/implement.md +77 -77
- package/vendor/skills/forge/phases/plan-openspec.md +60 -60
- package/vendor/skills/forge/phases/plan-specs.md +163 -117
- package/vendor/skills/forge/phases/review.md +25 -25
- package/vendor/skills/forge/phases/verify.md +124 -124
- package/vendor/skills/forge/references/forge-layout.md +85 -85
- package/vendor/skills/forge/references/pace.md +115 -115
- package/vendor/skills/forge/references/plan-routing.md +52 -51
- package/vendor/skills/forge/references/runtime-integrity.md +232 -232
- package/vendor/skills/forge/references/substantial-work.md +37 -37
- package/vendor/skills/forge/references/test-evidence.md +30 -30
- package/vendor/skills/forge/references/test-strategy.md +68 -68
- package/vendor/skills/forge/skills/subagent-driven-development/SKILL.md +87 -87
- package/vendor/skills/forge/subagents/final-reviewer-prompt.md +56 -56
- package/vendor/skills/forge/subagents/implementer-prompt.md +38 -38
- package/vendor/skills/git-resolve-adr-conflict/SKILL.md +132 -132
- package/vendor/skills/thorough-code-review/SKILL.md +290 -290
- package/vendor/skills/thorough-code-review/examples.md +133 -133
- package/vendor/skills/thorough-code-review/reference/accepted-risks.md +26 -26
- package/vendor/skills/thorough-code-review/reference/lenses.md +96 -96
- package/vendor/skills/thorough-code-review/reference/phase1-scout.md +62 -62
- package/vendor/skills/thorough-code-review/reference/phase2-skeptic.md +105 -105
- package/vendor/skills/thorough-code-review/reference/report-schema.json +222 -222
- package/vendor/skills/thorough-code-review/reference/report-template.md +115 -115
- package/vendor/skills/thorough-code-review/reference/severity-rubric.md +49 -49
- package/vendor/skills/thorough-code-review/reference/signals-preflight.md +55 -55
- package/vendor/templates/adr/README.md +7 -7
- package/vendor/templates/adr/decisions.md +141 -141
- package/vendor/templates/adr/hooks/check-pending-adrs.mjs +74 -74
- package/vendor/templates/adr/hooks/check-pending-adrs.sh +3 -3
- package/vendor/templates/adr/hooks/openspec-archive-agent-message.mjs +52 -52
- package/vendor/templates/adr/hooks/openspec-archive-agent-message.sh +3 -3
- package/vendor/templates/project/claude/commands/forge-apply.md +75 -75
- package/vendor/templates/project/claude/commands/forge-brainstorm.md +7 -7
- package/vendor/templates/project/claude/commands/forge-build.md +17 -17
- package/vendor/templates/project/claude/commands/forge-plan.md +12 -12
- package/vendor/templates/project/claude/commands/forge-skip.md +14 -14
- package/vendor/templates/project/claude/commands/forge-status.md +16 -16
- package/vendor/templates/project/claude/commands/forge.md +16 -16
- package/vendor/templates/project/claude/hooks/forge-prompt-hook.mjs +73 -73
- package/vendor/templates/project/claude/hooks/forge-session-start.mjs +19 -19
- package/vendor/templates/project/claude/hooks/forge-triage-hook.mjs +77 -77
- package/vendor/templates/project/cursor/commands/forge-apply.md +75 -75
- package/vendor/templates/project/cursor/commands/forge-brainstorm.md +10 -10
- package/vendor/templates/project/cursor/commands/forge-build.md +17 -17
- package/vendor/templates/project/cursor/commands/forge-plan.md +15 -15
- package/vendor/templates/project/cursor/commands/forge-skip.md +14 -14
- package/vendor/templates/project/cursor/commands/forge-status.md +16 -16
- package/vendor/templates/project/cursor/commands/forge.md +16 -16
- package/vendor/templates/project/cursor/hooks/forge-session-start.mjs +30 -30
- package/vendor/templates/project/cursor/hooks/forge-session-start.sh +3 -3
package/src/plan-engine.mjs
CHANGED
|
@@ -1,278 +1,321 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Planning engine resolution + scaffolding for forgekit.
|
|
4
|
-
*
|
|
5
|
-
* Engines:
|
|
6
|
-
* openspec — vendor OpenSpec CLI (`openspec/changes/<name>/`)
|
|
7
|
-
* specs — built-in markdown engine (`specs/changes/<name>/`), same inner
|
|
8
|
-
* layout as OpenSpec so phases / archive→ADR / later migration
|
|
9
|
-
* stay compatible.
|
|
10
|
-
*
|
|
11
|
-
* User default: ~/.forgekit/config.json → { plan: { engine } }
|
|
12
|
-
* Project: <repo>/.forge/config.json → { plan: { engine, dir } }
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import fs from 'node:fs';
|
|
16
|
-
import os from 'node:os';
|
|
17
|
-
import path from 'node:path';
|
|
18
|
-
import { spawnSync } from 'node:child_process';
|
|
19
|
-
import {
|
|
20
|
-
loadProjectConfig,
|
|
21
|
-
loadUserConfig,
|
|
22
|
-
saveProjectConfig,
|
|
23
|
-
saveUserConfig,
|
|
24
|
-
} from './config.mjs';
|
|
25
|
-
|
|
26
|
-
export const PLAN_ENGINES = Object.freeze(['openspec', 'specs']);
|
|
27
|
-
export const DEFAULT_SPECS_DIR = 'specs';
|
|
28
|
-
|
|
29
|
-
export const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (!
|
|
46
|
-
throw new Error(
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
*/
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
*
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
* @
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Planning engine resolution + scaffolding for forgekit.
|
|
4
|
+
*
|
|
5
|
+
* Engines:
|
|
6
|
+
* openspec — vendor OpenSpec CLI (`openspec/changes/<name>/`)
|
|
7
|
+
* specs — built-in markdown engine (`specs/changes/<name>/`), same inner
|
|
8
|
+
* layout as OpenSpec so phases / archive→ADR / later migration
|
|
9
|
+
* stay compatible.
|
|
10
|
+
*
|
|
11
|
+
* User default: ~/.forgekit/config.json → { plan: { engine } }
|
|
12
|
+
* Project: <repo>/.forge/config.json → { plan: { engine, dir } }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import {
|
|
20
|
+
loadProjectConfig,
|
|
21
|
+
loadUserConfig,
|
|
22
|
+
saveProjectConfig,
|
|
23
|
+
saveUserConfig,
|
|
24
|
+
} from './config.mjs';
|
|
25
|
+
|
|
26
|
+
export const PLAN_ENGINES = Object.freeze(['openspec', 'specs']);
|
|
27
|
+
export const DEFAULT_SPECS_DIR = 'specs';
|
|
28
|
+
/** Default OpenSpec engine root (symmetric with DEFAULT_SPECS_DIR). */
|
|
29
|
+
export const DEFAULT_OPENSPEC_DIR = 'openspec';
|
|
30
|
+
|
|
31
|
+
export const OPENSPEC_PACKAGE = '@fission-ai/openspec';
|
|
32
|
+
export const OPENSPEC_INSTALL_CMD = `npm install -g ${OPENSPEC_PACKAGE}`;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Relative engine-root path inside the repo (no `..`, no absolute).
|
|
36
|
+
* @param {string} dir
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
export function normalizePlanDir(dir) {
|
|
40
|
+
const cleaned = String(dir ?? '')
|
|
41
|
+
.trim()
|
|
42
|
+
.replace(/\\/g, '/')
|
|
43
|
+
.replace(/^\.\/+/, '')
|
|
44
|
+
.replace(/\/+$/, '');
|
|
45
|
+
if (!cleaned) {
|
|
46
|
+
throw new Error('plan.dir must be a non-empty relative path (e.g. specs or openspec)');
|
|
47
|
+
}
|
|
48
|
+
if (path.isAbsolute(cleaned) || cleaned.startsWith('/') || cleaned.includes('..')) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`plan.dir must be a relative path within the repo (got: ${dir}). Example: openspec`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return cleaned;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {unknown} value
|
|
58
|
+
* @returns {{ engine?: string, dir?: string } | null}
|
|
59
|
+
*/
|
|
60
|
+
function asPlan(value) {
|
|
61
|
+
if (!value || typeof value !== 'object') return null;
|
|
62
|
+
return /** @type {{ engine?: string, dir?: string }} */ (value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {string} engine
|
|
67
|
+
*/
|
|
68
|
+
export function assertPlanEngine(engine) {
|
|
69
|
+
if (!PLAN_ENGINES.includes(engine)) {
|
|
70
|
+
throw new Error(`Unknown plan engine: ${engine}. Known: ${PLAN_ENGINES.join(', ')}`);
|
|
71
|
+
}
|
|
72
|
+
return engine;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} cwd
|
|
77
|
+
* @returns {boolean}
|
|
78
|
+
*/
|
|
79
|
+
export function hasOpenSpecConfig(cwd) {
|
|
80
|
+
return fs.existsSync(path.join(cwd, 'openspec', 'config.yaml'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} [home]
|
|
85
|
+
* @returns {string | null}
|
|
86
|
+
*/
|
|
87
|
+
export function loadUserPlanEngine(home = os.homedir()) {
|
|
88
|
+
const cfg = loadUserConfig(home);
|
|
89
|
+
const engine = asPlan(cfg.plan)?.engine;
|
|
90
|
+
return PLAN_ENGINES.includes(engine) ? engine : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Merge { plan: { engine } } into ~/.forgekit/config.json, preserving other keys.
|
|
95
|
+
* @param {string} engine
|
|
96
|
+
* @param {string} [home]
|
|
97
|
+
*/
|
|
98
|
+
export function saveUserPlanEngine(engine, home = os.homedir()) {
|
|
99
|
+
assertPlanEngine(engine);
|
|
100
|
+
return saveUserConfig({ plan: { engine } }, home);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Merge { plan: { engine, dir? } } into <cwd>/.forge/config.json, preserving adr etc.
|
|
105
|
+
* @param {string} cwd
|
|
106
|
+
* @param {{ engine: string, dir?: string }} plan
|
|
107
|
+
*/
|
|
108
|
+
export function writeProjectPlanConfig(cwd, plan) {
|
|
109
|
+
assertPlanEngine(plan.engine);
|
|
110
|
+
const current = loadProjectConfig(cwd);
|
|
111
|
+
const curPlan = asPlan(current.plan);
|
|
112
|
+
/** @type {{ engine: string, dir?: string }} */
|
|
113
|
+
const nextPlan = { engine: plan.engine };
|
|
114
|
+
if (plan.engine === 'specs') {
|
|
115
|
+
nextPlan.dir = normalizePlanDir(plan.dir ?? curPlan?.dir ?? DEFAULT_SPECS_DIR);
|
|
116
|
+
} else if (plan.dir) {
|
|
117
|
+
// Optional override when recording openspec (rarely needed).
|
|
118
|
+
nextPlan.dir = normalizePlanDir(plan.dir);
|
|
119
|
+
}
|
|
120
|
+
return saveProjectConfig(cwd, { plan: nextPlan }, { replaceKeys: ['plan'] });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Effective plan engine for a project.
|
|
125
|
+
*
|
|
126
|
+
* Precedence: project config → openspec/config.yaml detection → user default
|
|
127
|
+
* → 'openspec'.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} cwd
|
|
130
|
+
* @param {{ home?: string, useUserDefault?: boolean }} [opts]
|
|
131
|
+
* @returns {{ engine: string, dir: string, source: string }}
|
|
132
|
+
*/
|
|
133
|
+
export function resolveProjectPlanEngine(cwd, opts = {}) {
|
|
134
|
+
const project = loadProjectConfig(cwd);
|
|
135
|
+
const projectPlan = asPlan(project.plan);
|
|
136
|
+
if (projectPlan && PLAN_ENGINES.includes(projectPlan.engine)) {
|
|
137
|
+
const fallbackDir =
|
|
138
|
+
projectPlan.engine === 'openspec' ? DEFAULT_OPENSPEC_DIR : DEFAULT_SPECS_DIR;
|
|
139
|
+
return {
|
|
140
|
+
engine: /** @type {string} */ (projectPlan.engine),
|
|
141
|
+
dir: projectPlan.dir ?? fallbackDir,
|
|
142
|
+
source: 'project',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (hasOpenSpecConfig(cwd)) {
|
|
146
|
+
return { engine: 'openspec', dir: DEFAULT_OPENSPEC_DIR, source: 'detected' };
|
|
147
|
+
}
|
|
148
|
+
if (opts.useUserDefault !== false) {
|
|
149
|
+
const userEngine = loadUserPlanEngine(opts.home);
|
|
150
|
+
if (userEngine) {
|
|
151
|
+
return {
|
|
152
|
+
engine: userEngine,
|
|
153
|
+
dir: userEngine === 'openspec' ? DEFAULT_OPENSPEC_DIR : DEFAULT_SPECS_DIR,
|
|
154
|
+
source: 'user',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { engine: 'openspec', dir: DEFAULT_OPENSPEC_DIR, source: 'default' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const SPECS_README = (dir) => `# \`${dir}/\` — Forge specs (built-in planning engine)
|
|
162
|
+
|
|
163
|
+
OpenSpec-compatible change tracking without the OpenSpec CLI. Same **format**
|
|
164
|
+
as OpenSpec (proposal / design / tasks / delta specs / main catalog). Managed
|
|
165
|
+
by the Forge workflow (see the \`forge\` skill, \`phases/plan-specs.md\`).
|
|
166
|
+
|
|
167
|
+
\`\`\`
|
|
168
|
+
${dir}/
|
|
169
|
+
specs/<capability>/spec.md # source of truth (current behavior)
|
|
170
|
+
changes/<change-name>/
|
|
171
|
+
proposal.md # Why / What Changes / Capabilities / Impact
|
|
172
|
+
design.md # optional — context, decisions, risks
|
|
173
|
+
tasks.md # ## groups with - [ ] task checkboxes
|
|
174
|
+
specs/<capability>/spec.md # DELTA specs (ADDED / MODIFIED / REMOVED)
|
|
175
|
+
changes/archive/YYYY-MM-DD-<change-name>/
|
|
176
|
+
\`\`\`
|
|
177
|
+
|
|
178
|
+
Conventions (kept identical to OpenSpec so migration stays trivial):
|
|
179
|
+
|
|
180
|
+
- One change per unit of substantial work; kebab-case change names.
|
|
181
|
+
- Delta specs live under \`changes/<name>/specs/\` — **not** a \`deltas/\` folder.
|
|
182
|
+
- \`tasks.md\` uses \`##\` section groups and \`- [ ]\` checkboxes; Forge counts
|
|
183
|
+
and reviews per group.
|
|
184
|
+
- On archive (\`forge change archive\`), deltas merge into \`${dir}/specs/\`, then
|
|
185
|
+
the change folder moves under \`changes/archive/\`.
|
|
186
|
+
|
|
187
|
+
Switching from OpenSpec without moving files: set
|
|
188
|
+
\`.forge/config.json\` → \`{ "plan": { "engine": "specs", "dir": "openspec" } }\`
|
|
189
|
+
(or \`forge init --no-openspec --plan-dir openspec\`).
|
|
190
|
+
|
|
191
|
+
Migrating the other way: run \`openspec init\`, keep using the same tree if
|
|
192
|
+
\`dir\` already points at \`openspec/\`.
|
|
193
|
+
`;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Scaffold the specs engine directory structure.
|
|
197
|
+
* @param {string} cwd
|
|
198
|
+
* @param {{ dir?: string, force?: boolean }} [opts]
|
|
199
|
+
*/
|
|
200
|
+
export function scaffoldSpecs(cwd, opts = {}) {
|
|
201
|
+
const dir = normalizePlanDir(opts.dir ?? DEFAULT_SPECS_DIR);
|
|
202
|
+
/** @type {{ file: string, status: string }[]} */
|
|
203
|
+
const files = [];
|
|
204
|
+
|
|
205
|
+
const ensureDir = (rel) => {
|
|
206
|
+
fs.mkdirSync(path.join(cwd, ...rel.split('/')), { recursive: true });
|
|
207
|
+
};
|
|
208
|
+
const writeFile = (rel, body) => {
|
|
209
|
+
const dest = path.join(cwd, ...rel.split('/'));
|
|
210
|
+
if (fs.existsSync(dest) && !opts.force) {
|
|
211
|
+
files.push({ file: rel, status: 'skipped' });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
fs.writeFileSync(dest, body, 'utf8');
|
|
215
|
+
files.push({ file: rel, status: 'written' });
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
ensureDir(`${dir}/changes/archive`);
|
|
219
|
+
ensureDir(`${dir}/specs`);
|
|
220
|
+
writeFile(`${dir}/README.md`, SPECS_README(dir));
|
|
221
|
+
writeFile(`${dir}/changes/archive/.gitkeep`, '');
|
|
222
|
+
writeFile(`${dir}/specs/.gitkeep`, '');
|
|
223
|
+
|
|
224
|
+
return { dir, files };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @returns {{ ok: boolean, version: string | null }}
|
|
229
|
+
*/
|
|
230
|
+
export function checkOpenSpecCliQuick(runCommand) {
|
|
231
|
+
const run =
|
|
232
|
+
runCommand ??
|
|
233
|
+
((cmd, args) => {
|
|
234
|
+
let result = spawnSync(cmd, args, { encoding: 'utf8', shell: false });
|
|
235
|
+
if (result.error && process.platform === 'win32') {
|
|
236
|
+
result = spawnSync([cmd, ...args].join(' '), { encoding: 'utf8', shell: true });
|
|
237
|
+
}
|
|
238
|
+
return { status: result.status, stdout: result.stdout || '' };
|
|
239
|
+
});
|
|
240
|
+
const attempt = run('openspec', ['--version']);
|
|
241
|
+
if (attempt.status === 0) {
|
|
242
|
+
return {
|
|
243
|
+
ok: true,
|
|
244
|
+
version: String(attempt.stdout || '').trim().split(/\r?\n/)[0] || 'unknown',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return { ok: false, version: null };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Map forgekit environment ids → OpenSpec tool ids (mostly identity).
|
|
252
|
+
* @type {Record<string, string>}
|
|
253
|
+
*/
|
|
254
|
+
const OPENSPEC_TOOL_ID = { copilot: 'github-copilot' };
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* @param {string[]} agentIds forgekit environment ids
|
|
258
|
+
* @returns {string[]} OpenSpec tool ids
|
|
259
|
+
*/
|
|
260
|
+
export function toOpenSpecToolIds(agentIds) {
|
|
261
|
+
return agentIds.map((id) => OPENSPEC_TOOL_ID[id] ?? id);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Install the OpenSpec CLI (if missing) and run `openspec init` in the project.
|
|
266
|
+
* Interactive: inherits stdio so `openspec init` can prompt — unless `tools`
|
|
267
|
+
* is given, in which case those tools are configured non-interactively
|
|
268
|
+
* (`openspec init --tools <ids>`), skipping OpenSpec's own tool picker.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} cwd
|
|
271
|
+
* @param {{ runCommand?: Function, interactive?: boolean, tools?: string[] }} [opts]
|
|
272
|
+
* @returns {{ ok: boolean, steps: { step: string, ok: boolean, detail?: string }[] }}
|
|
273
|
+
*/
|
|
274
|
+
export function setupOpenSpec(cwd, opts = {}) {
|
|
275
|
+
/** @type {{ step: string, ok: boolean, detail?: string }[]} */
|
|
276
|
+
const steps = [];
|
|
277
|
+
|
|
278
|
+
const runInherit =
|
|
279
|
+
opts.runCommand ??
|
|
280
|
+
((cmd, args) => {
|
|
281
|
+
let result = spawnSync(cmd, args, { stdio: 'inherit', shell: false, cwd });
|
|
282
|
+
if (result.error && process.platform === 'win32') {
|
|
283
|
+
result = spawnSync([cmd, ...args].join(' '), {
|
|
284
|
+
stdio: 'inherit',
|
|
285
|
+
shell: true,
|
|
286
|
+
cwd,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return { status: result.status, error: result.error };
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const cli = checkOpenSpecCliQuick(opts.runCommand);
|
|
293
|
+
if (!cli.ok) {
|
|
294
|
+
const install = runInherit('npm', ['install', '-g', OPENSPEC_PACKAGE]);
|
|
295
|
+
const ok = install.status === 0;
|
|
296
|
+
steps.push({
|
|
297
|
+
step: OPENSPEC_INSTALL_CMD,
|
|
298
|
+
ok,
|
|
299
|
+
detail: ok ? undefined : String(install.error ?? `exit ${install.status}`),
|
|
300
|
+
});
|
|
301
|
+
if (!ok) return { ok: false, steps };
|
|
302
|
+
} else {
|
|
303
|
+
steps.push({ step: `openspec CLI present (${cli.version})`, ok: true });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (hasOpenSpecConfig(cwd)) {
|
|
307
|
+
steps.push({ step: 'openspec/config.yaml already present', ok: true });
|
|
308
|
+
return { ok: true, steps };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const tools = opts.tools?.length ? toOpenSpecToolIds(opts.tools) : null;
|
|
312
|
+
const initArgs = tools ? ['init', '--tools', tools.join(',')] : ['init'];
|
|
313
|
+
const init = runInherit('openspec', initArgs);
|
|
314
|
+
const initOk = init.status === 0;
|
|
315
|
+
steps.push({
|
|
316
|
+
step: `openspec ${initArgs.join(' ')}`,
|
|
317
|
+
ok: initOk,
|
|
318
|
+
detail: initOk ? undefined : String(init.error ?? `exit ${init.status}`),
|
|
319
|
+
});
|
|
320
|
+
return { ok: initOk && hasOpenSpecConfig(cwd), steps };
|
|
321
|
+
}
|