@izkac/forgekit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/bin/forge.mjs +100 -0
  2. package/bin/forgekit.mjs +84 -0
  3. package/bin/review.mjs +82 -0
  4. package/package.json +46 -0
  5. package/scripts/prepack.mjs +78 -0
  6. package/scripts/run-tests.mjs +43 -0
  7. package/src/adr.mjs +236 -0
  8. package/src/adr.test.mjs +170 -0
  9. package/src/change.mjs +234 -0
  10. package/src/change.test.mjs +83 -0
  11. package/src/cleanup-sessions.mjs +70 -0
  12. package/src/config.mjs +103 -0
  13. package/src/defer.mjs +75 -0
  14. package/src/doctor.mjs +341 -0
  15. package/src/doctor.test.mjs +114 -0
  16. package/src/init.mjs +575 -0
  17. package/src/install.mjs +777 -0
  18. package/src/install.test.mjs +104 -0
  19. package/src/integrity-check.mjs +58 -0
  20. package/src/integrity.mjs +317 -0
  21. package/src/integrity.test.mjs +296 -0
  22. package/src/lib/workspaces.mjs +55 -0
  23. package/src/lib.mjs +138 -0
  24. package/src/models.defaults.json +41 -0
  25. package/src/new-session.mjs +82 -0
  26. package/src/openspec-overlays/README.md +19 -0
  27. package/src/openspec-overlays/openspec-apply-change-footer.md +14 -0
  28. package/src/openspec-overlays/opsx-apply-completion-step.md +1 -0
  29. package/src/openspec-overlays/opsx-apply-implement-step.md +11 -0
  30. package/src/paths.mjs +92 -0
  31. package/src/plan-engine.mjs +260 -0
  32. package/src/plan-engine.test.mjs +245 -0
  33. package/src/preferences.defaults.json +78 -0
  34. package/src/preferences.mjs +438 -0
  35. package/src/preferences.test.mjs +174 -0
  36. package/src/record-evidence.mjs +204 -0
  37. package/src/record-evidence.test.mjs +260 -0
  38. package/src/resolve-model.mjs +312 -0
  39. package/src/resolve-model.test.mjs +194 -0
  40. package/src/review/carryforward.mjs +413 -0
  41. package/src/review/carryforward.test.mjs +587 -0
  42. package/src/review/cli.test.mjs +117 -0
  43. package/src/review/export.mjs +172 -0
  44. package/src/review/export.test.mjs +197 -0
  45. package/src/review/fixtures/valid-review.json +42 -0
  46. package/src/review/lib.mjs +894 -0
  47. package/src/review/lib.test.mjs +266 -0
  48. package/src/review/merge-tentative.mjs +292 -0
  49. package/src/review/merge-tentative.test.mjs +363 -0
  50. package/src/review/new-review.mjs +200 -0
  51. package/src/review/render.mjs +108 -0
  52. package/src/review/schema-consistency.test.mjs +83 -0
  53. package/src/review/schema.json +196 -0
  54. package/src/review/signals.mjs +144 -0
  55. package/src/review/signals.test.mjs +62 -0
  56. package/src/score-cli.mjs +68 -0
  57. package/src/score.mjs +489 -0
  58. package/src/score.test.mjs +253 -0
  59. package/src/session-reminder.mjs +168 -0
  60. package/src/session-status.mjs +70 -0
  61. package/src/set-models.mjs +186 -0
  62. package/src/set-phase.mjs +177 -0
  63. package/src/set-phase.test.mjs +317 -0
  64. package/src/set-prefs.mjs +294 -0
  65. package/src/spine.mjs +91 -0
  66. package/src/triage-prompt.mjs +175 -0
  67. package/src/triage-prompt.test.mjs +50 -0
  68. package/src/vendor-openspec-overlays.mjs +176 -0
  69. package/src/vendor-openspec-overlays.test.mjs +62 -0
  70. package/vendor/skills/archive-to-adr/SKILL.md +149 -0
  71. package/vendor/skills/forge/SKILL.md +136 -0
  72. package/vendor/skills/forge/phases/brainstorm.md +23 -0
  73. package/vendor/skills/forge/phases/finish.md +87 -0
  74. package/vendor/skills/forge/phases/implement.md +76 -0
  75. package/vendor/skills/forge/phases/plan-openspec.md +40 -0
  76. package/vendor/skills/forge/phases/plan-specs.md +97 -0
  77. package/vendor/skills/forge/phases/review.md +25 -0
  78. package/vendor/skills/forge/phases/verify.md +120 -0
  79. package/vendor/skills/forge/references/forge-layout.md +85 -0
  80. package/vendor/skills/forge/references/pace.md +115 -0
  81. package/vendor/skills/forge/references/plan-routing.md +51 -0
  82. package/vendor/skills/forge/references/runtime-integrity.md +157 -0
  83. package/vendor/skills/forge/references/substantial-work.md +37 -0
  84. package/vendor/skills/forge/references/tdd-core.md +29 -0
  85. package/vendor/skills/forge/references/test-evidence.md +30 -0
  86. package/vendor/skills/forge/references/test-strategy.md +68 -0
  87. package/vendor/skills/forge/skills/NOTICE.md +17 -0
  88. package/vendor/skills/forge/skills/brainstorming/SKILL.md +120 -0
  89. package/vendor/skills/forge/skills/requesting-code-review/SKILL.md +67 -0
  90. package/vendor/skills/forge/skills/requesting-code-review/code-reviewer.md +146 -0
  91. package/vendor/skills/forge/skills/subagent-driven-development/SKILL.md +87 -0
  92. package/vendor/skills/forge/skills/systematic-debugging/SKILL.md +234 -0
  93. package/vendor/skills/forge/skills/systematic-debugging/condition-based-waiting.md +115 -0
  94. package/vendor/skills/forge/skills/systematic-debugging/defense-in-depth.md +122 -0
  95. package/vendor/skills/forge/skills/systematic-debugging/find-polluter.sh +63 -0
  96. package/vendor/skills/forge/skills/systematic-debugging/root-cause-tracing.md +169 -0
  97. package/vendor/skills/forge/skills/test-driven-development/SKILL.md +290 -0
  98. package/vendor/skills/forge/skills/test-driven-development/testing-anti-patterns.md +299 -0
  99. package/vendor/skills/forge/skills/verification-before-completion/SKILL.md +59 -0
  100. package/vendor/skills/forge/subagents/final-reviewer-prompt.md +53 -0
  101. package/vendor/skills/forge/subagents/implementer-prompt.md +38 -0
  102. package/vendor/skills/forge/subagents/task-reviewer-prompt.md +61 -0
  103. package/vendor/skills/git-resolve-adr-conflict/SKILL.md +132 -0
  104. package/vendor/skills/thorough-code-review/SKILL.md +290 -0
  105. package/vendor/skills/thorough-code-review/examples/accepted-risks-janus.md +32 -0
  106. package/vendor/skills/thorough-code-review/examples.md +133 -0
  107. package/vendor/skills/thorough-code-review/reference/accepted-risks.md +26 -0
  108. package/vendor/skills/thorough-code-review/reference/lenses.md +96 -0
  109. package/vendor/skills/thorough-code-review/reference/phase1-scout.md +62 -0
  110. package/vendor/skills/thorough-code-review/reference/phase1c-coverage.md +44 -0
  111. package/vendor/skills/thorough-code-review/reference/phase2-skeptic.md +105 -0
  112. package/vendor/skills/thorough-code-review/reference/report-schema.json +222 -0
  113. package/vendor/skills/thorough-code-review/reference/report-template.md +115 -0
  114. package/vendor/skills/thorough-code-review/reference/severity-rubric.md +49 -0
  115. package/vendor/skills/thorough-code-review/reference/signals-preflight.md +55 -0
  116. package/vendor/templates/adr/README.md +7 -0
  117. package/vendor/templates/adr/decisions.md +141 -0
  118. package/vendor/templates/adr/hooks/check-pending-adrs.mjs +74 -0
  119. package/vendor/templates/adr/hooks/check-pending-adrs.sh +3 -0
  120. package/vendor/templates/adr/hooks/openspec-archive-agent-message.mjs +52 -0
  121. package/vendor/templates/adr/hooks/openspec-archive-agent-message.sh +3 -0
  122. package/vendor/templates/project/claude/commands/forge-apply.md +75 -0
  123. package/vendor/templates/project/claude/commands/forge-brainstorm.md +7 -0
  124. package/vendor/templates/project/claude/commands/forge-build.md +17 -0
  125. package/vendor/templates/project/claude/commands/forge-plan.md +12 -0
  126. package/vendor/templates/project/claude/commands/forge-skip.md +14 -0
  127. package/vendor/templates/project/claude/commands/forge-status.md +16 -0
  128. package/vendor/templates/project/claude/commands/forge.md +16 -0
  129. package/vendor/templates/project/claude/hooks/forge-prompt-hook.mjs +73 -0
  130. package/vendor/templates/project/claude/hooks/forge-session-start.mjs +19 -0
  131. package/vendor/templates/project/claude/hooks/forge-triage-hook.mjs +77 -0
  132. package/vendor/templates/project/claude/rules/forge.md +16 -0
  133. package/vendor/templates/project/codex/rules/forge.md +10 -0
  134. package/vendor/templates/project/cursor/commands/forge-apply.md +75 -0
  135. package/vendor/templates/project/cursor/commands/forge-brainstorm.md +10 -0
  136. package/vendor/templates/project/cursor/commands/forge-build.md +17 -0
  137. package/vendor/templates/project/cursor/commands/forge-plan.md +15 -0
  138. package/vendor/templates/project/cursor/commands/forge-skip.md +14 -0
  139. package/vendor/templates/project/cursor/commands/forge-status.md +16 -0
  140. package/vendor/templates/project/cursor/commands/forge.md +16 -0
  141. package/vendor/templates/project/cursor/hooks/forge-session-start.mjs +30 -0
  142. package/vendor/templates/project/cursor/hooks/forge-session-start.sh +3 -0
  143. package/vendor/templates/project/cursor/rules/forge.mdc +21 -0
package/src/init.mjs ADDED
@@ -0,0 +1,575 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Wire Forge into a project: commands, thin rules, hooks, .forge gitignore.
4
+ *
5
+ * Usage:
6
+ * forge init # interactive agent picker
7
+ * forge init --cursor --claude
8
+ * forge init --all
9
+ * forge init --overlay # also apply OpenSpec vendor overlays
10
+ */
11
+
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import readline from 'node:readline/promises';
15
+ import { fileURLToPath, pathToFileURL } from 'node:url';
16
+ import { spawnSync } from 'node:child_process';
17
+ import { stdin as input, stdout as output } from 'node:process';
18
+ import {
19
+ DEFAULT_ADR_DIR,
20
+ disableProjectAdr,
21
+ loadUserConfig,
22
+ normalizeAdrDir,
23
+ scaffoldAdr,
24
+ } from './adr.mjs';
25
+ import {
26
+ DEFAULT_SPECS_DIR,
27
+ hasOpenSpecConfig,
28
+ loadUserPlanEngine,
29
+ scaffoldSpecs,
30
+ setupOpenSpec,
31
+ writeProjectPlanConfig,
32
+ } from './plan-engine.mjs';
33
+ import { resolveAsset } from './paths.mjs';
34
+
35
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
36
+
37
+ /**
38
+ * @param {string[]} argv
39
+ */
40
+ export function parseArgs(argv) {
41
+ const opts = {
42
+ all: false,
43
+ help: false,
44
+ force: false,
45
+ overlay: false,
46
+ agents: /** @type {string[]} */ ([]),
47
+ cwd: process.cwd(),
48
+ /** @type {boolean | null} */
49
+ adr: /** @type {boolean | null} */ (null),
50
+ adrDir: /** @type {string | null} */ (null),
51
+ /** @type {boolean | null} true=openspec, false=specs, null=detect/prompt */
52
+ openspec: /** @type {boolean | null} */ (null),
53
+ };
54
+
55
+ for (let i = 0; i < argv.length; i += 1) {
56
+ const arg = argv[i];
57
+ if (arg === '--all') opts.all = true;
58
+ else if (arg === '--force' || arg === '-f') opts.force = true;
59
+ else if (arg === '--overlay') opts.overlay = true;
60
+ else if (arg === '--cwd') opts.cwd = argv[++i];
61
+ else if (arg === '--help' || arg === '-h') opts.help = true;
62
+ else if (arg === '--cursor') opts.agents.push('cursor');
63
+ else if (arg === '--claude' || arg === '--claude-code') opts.agents.push('claude');
64
+ else if (arg === '--codex') opts.agents.push('codex');
65
+ else if (arg === '--adr') opts.adr = true;
66
+ else if (arg === '--no-adr') opts.adr = false;
67
+ else if (arg === '--adr-dir') opts.adrDir = argv[++i];
68
+ else if (arg === '--openspec') opts.openspec = true;
69
+ else if (arg === '--no-openspec') opts.openspec = false;
70
+ else throw new Error(`Unknown argument: ${arg}`);
71
+ }
72
+
73
+ return opts;
74
+ }
75
+
76
+ function printHelp() {
77
+ process.stdout.write(`Usage: forge init [options]
78
+
79
+ Wire Forge commands, thin rules, and hooks into the current project.
80
+
81
+ Options:
82
+ --cursor Cursor (.cursor/commands, rules, hooks)
83
+ --claude Claude Code (.claude/commands, rules, hooks)
84
+ --codex Codex CLI (.codex/rules)
85
+ --all All of the above
86
+ --openspec Plan with OpenSpec (offer install + \`openspec init\` if missing)
87
+ --no-openspec Plan with the built-in specs engine (${DEFAULT_SPECS_DIR}/changes/)
88
+ --adr Enable ADRs (scaffold decisions.md + ADR dir + hooks)
89
+ --no-adr Disable ADRs for this project
90
+ --adr-dir <path> ADR directory (default: ${DEFAULT_ADR_DIR} or ~/.forgekit preference)
91
+ --overlay Also run \`forge overlay\` (OpenSpec vendor patches)
92
+ --force, -f Overwrite existing template files
93
+ --cwd <path> Project root (default: cwd)
94
+ --help
95
+
96
+ Requires the Forge skill already installed (\`forge install\`) for agents
97
+ to load skill content. Init only adds project-local wiring.
98
+
99
+ Interactive (TTY): when --openspec/--no-openspec omitted and OpenSpec is not
100
+ already set up, offers to install + set it up (decline = built-in specs
101
+ engine). When --adr/--no-adr omitted, asks whether to use ADRs and for the
102
+ directory inside the repo.
103
+ `);
104
+ }
105
+
106
+ /**
107
+ * @returns {string}
108
+ */
109
+ export function resolveTemplatesRoot() {
110
+ return resolveAsset('templates/project');
111
+ }
112
+
113
+ /**
114
+ * @param {string} src
115
+ * @param {string} dest
116
+ * @param {{ force?: boolean }} opts
117
+ */
118
+ function copyFile(src, dest, opts) {
119
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
120
+ if (fs.existsSync(dest) && !opts.force) {
121
+ return 'skipped';
122
+ }
123
+ fs.copyFileSync(src, dest);
124
+ return 'written';
125
+ }
126
+
127
+ /**
128
+ * @param {string} srcDir
129
+ * @param {string} destDir
130
+ * @param {{ force?: boolean }} opts
131
+ */
132
+ function copyDirFiles(srcDir, destDir, opts) {
133
+ /** @type {{ file: string, status: string }[]} */
134
+ const out = [];
135
+ if (!fs.existsSync(srcDir)) return out;
136
+ for (const name of fs.readdirSync(srcDir)) {
137
+ const from = path.join(srcDir, name);
138
+ if (!fs.statSync(from).isFile()) continue;
139
+ const to = path.join(destDir, name);
140
+ out.push({ file: path.relative(opts.cwd ?? destDir, to) || to, status: copyFile(from, to, opts) });
141
+ }
142
+ return out;
143
+ }
144
+
145
+ /**
146
+ * Ensure .forge/.gitignore exists.
147
+ * @param {string} cwd
148
+ */
149
+ export function ensureForgeGitignore(cwd) {
150
+ const forgeDir = path.join(cwd, '.forge');
151
+ fs.mkdirSync(forgeDir, { recursive: true });
152
+ const gi = path.join(forgeDir, '.gitignore');
153
+ const body = `# Forge session scratch — keep layout docs + committed project config
154
+ *
155
+ !.gitignore
156
+ !README.md
157
+ !config.json
158
+ `;
159
+ if (!fs.existsSync(gi)) {
160
+ fs.writeFileSync(gi, body, 'utf8');
161
+ return 'written';
162
+ }
163
+ // Upgrade older scaffolds that omit config.json
164
+ const existing = fs.readFileSync(gi, 'utf8');
165
+ if (!existing.includes('!config.json')) {
166
+ const next = existing.trimEnd().endsWith('!README.md')
167
+ ? `${existing.trimEnd()}\n!config.json\n`
168
+ : `${existing.trimEnd()}\n!config.json\n`;
169
+ fs.writeFileSync(gi, next, 'utf8');
170
+ return 'updated';
171
+ }
172
+ return 'exists';
173
+ }
174
+
175
+ /**
176
+ * Write a short project README under .forge/ if missing.
177
+ * @param {string} cwd
178
+ */
179
+ export function ensureForgeReadme(cwd) {
180
+ const readme = path.join(cwd, '.forge', 'README.md');
181
+ if (fs.existsSync(readme)) return 'exists';
182
+ fs.writeFileSync(
183
+ readme,
184
+ `# \`.forge/\` — Forge session scratch
185
+
186
+ Per-checkout, **gitignored** workspace for Forge session orchestration.
187
+ Canonical specs live in \`openspec/\`; this directory holds session-local artefacts only.
188
+
189
+ \`\`\`bash
190
+ forge new <slug>
191
+ forge status
192
+ forge prefs
193
+ forge models
194
+ forge cleanup
195
+ \`\`\`
196
+
197
+ See the Forge skill and forgekit docs for the full workflow.
198
+ `,
199
+ 'utf8',
200
+ );
201
+ return 'written';
202
+ }
203
+
204
+ /**
205
+ * Append hook registrations into Claude settings.json if present / create stub note.
206
+ * @param {string} cwd
207
+ * @param {{ force?: boolean }} opts
208
+ */
209
+ export function ensureClaudeHookHints(cwd, opts) {
210
+ const settingsPath = path.join(cwd, '.claude', 'settings.json');
211
+ const notePath = path.join(cwd, '.claude', 'forge-hooks.snippet.json');
212
+ const snippet = {
213
+ _comment:
214
+ 'Merge these hooks into .claude/settings.json (SessionStart + UserPromptSubmit). Paths assume forge CLI is on PATH.',
215
+ hooks: {
216
+ SessionStart: [
217
+ {
218
+ hooks: [
219
+ {
220
+ type: 'command',
221
+ command: 'node "${CLAUDE_PROJECT_DIR}/.claude/hooks/forge-session-start.mjs"',
222
+ },
223
+ ],
224
+ },
225
+ ],
226
+ UserPromptSubmit: [
227
+ {
228
+ hooks: [
229
+ {
230
+ type: 'command',
231
+ command: 'node "${CLAUDE_PROJECT_DIR}/.claude/hooks/forge-triage-hook.mjs"',
232
+ },
233
+ {
234
+ type: 'command',
235
+ command: 'node "${CLAUDE_PROJECT_DIR}/.claude/hooks/forge-prompt-hook.mjs"',
236
+ },
237
+ ],
238
+ },
239
+ ],
240
+ },
241
+ };
242
+ fs.mkdirSync(path.dirname(notePath), { recursive: true });
243
+ if (!fs.existsSync(notePath) || opts.force) {
244
+ fs.writeFileSync(notePath, `${JSON.stringify(snippet, null, 2)}\n`, 'utf8');
245
+ }
246
+ return {
247
+ settingsExists: fs.existsSync(settingsPath),
248
+ snippet: notePath,
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Write Cursor hooks.json snippet.
254
+ * @param {string} cwd
255
+ * @param {{ force?: boolean }} opts
256
+ */
257
+ export function ensureCursorHookHints(cwd, opts) {
258
+ const notePath = path.join(cwd, '.cursor', 'forge-hooks.snippet.json');
259
+ const snippet = {
260
+ _comment:
261
+ 'Merge into .cursor/hooks.json. Requires Node on PATH (forge-session-start.mjs).',
262
+ version: 1,
263
+ hooks: {
264
+ sessionStart: [
265
+ {
266
+ command: 'node .cursor/hooks/forge-session-start.mjs',
267
+ },
268
+ ],
269
+ },
270
+ };
271
+ fs.mkdirSync(path.dirname(notePath), { recursive: true });
272
+ if (!fs.existsSync(notePath) || opts.force) {
273
+ fs.writeFileSync(notePath, `${JSON.stringify(snippet, null, 2)}\n`, 'utf8');
274
+ }
275
+ return notePath;
276
+ }
277
+
278
+ /**
279
+ * @param {string[]} selected
280
+ * @param {{ cwd: string, force?: boolean, overlay?: boolean, templatesRoot?: string, adr?: boolean | null, adrDir?: string | null, home?: string, planEngine?: string | null }} opts
281
+ */
282
+ export function initProject(selected, opts) {
283
+ const templates = opts.templatesRoot ?? resolveTemplatesRoot();
284
+ const cwd = opts.cwd;
285
+ /** @type {Record<string, unknown>} */
286
+ const report = {
287
+ cwd,
288
+ gitignore: ensureForgeGitignore(cwd),
289
+ readme: ensureForgeReadme(cwd),
290
+ files: /** @type {{ file: string, status: string }[]} */ ([]),
291
+ };
292
+
293
+ const copyOpts = { force: opts.force, cwd };
294
+
295
+ if (selected.includes('cursor')) {
296
+ report.files.push(
297
+ ...copyDirFiles(
298
+ path.join(templates, 'cursor', 'commands'),
299
+ path.join(cwd, '.cursor', 'commands'),
300
+ copyOpts,
301
+ ),
302
+ ...copyDirFiles(
303
+ path.join(templates, 'cursor', 'rules'),
304
+ path.join(cwd, '.cursor', 'rules'),
305
+ copyOpts,
306
+ ),
307
+ ...copyDirFiles(
308
+ path.join(templates, 'cursor', 'hooks'),
309
+ path.join(cwd, '.cursor', 'hooks'),
310
+ copyOpts,
311
+ ),
312
+ );
313
+ report.cursorHookSnippet = ensureCursorHookHints(cwd, copyOpts);
314
+ }
315
+
316
+ if (selected.includes('claude')) {
317
+ report.files.push(
318
+ ...copyDirFiles(
319
+ path.join(templates, 'claude', 'commands'),
320
+ path.join(cwd, '.claude', 'commands'),
321
+ copyOpts,
322
+ ),
323
+ ...copyDirFiles(
324
+ path.join(templates, 'claude', 'rules'),
325
+ path.join(cwd, '.claude', 'rules'),
326
+ copyOpts,
327
+ ),
328
+ ...copyDirFiles(
329
+ path.join(templates, 'claude', 'hooks'),
330
+ path.join(cwd, '.claude', 'hooks'),
331
+ copyOpts,
332
+ ),
333
+ );
334
+ report.claudeHooks = ensureClaudeHookHints(cwd, copyOpts);
335
+ }
336
+
337
+ if (selected.includes('codex')) {
338
+ report.files.push(
339
+ ...copyDirFiles(
340
+ path.join(templates, 'codex', 'rules'),
341
+ path.join(cwd, '.codex', 'rules'),
342
+ copyOpts,
343
+ ),
344
+ );
345
+ }
346
+
347
+ if (opts.planEngine === 'specs') {
348
+ const scaffold = scaffoldSpecs(cwd, { force: opts.force });
349
+ const config = writeProjectPlanConfig(cwd, {
350
+ engine: 'specs',
351
+ dir: scaffold.dir,
352
+ });
353
+ report.plan = { engine: 'specs', dir: scaffold.dir, files: scaffold.files, config };
354
+ } else if (opts.planEngine === 'openspec') {
355
+ const config = writeProjectPlanConfig(cwd, { engine: 'openspec' });
356
+ report.plan = {
357
+ engine: 'openspec',
358
+ configured: hasOpenSpecConfig(cwd),
359
+ config,
360
+ };
361
+ }
362
+
363
+ if (opts.adr === true) {
364
+ const user = loadUserConfig(opts.home);
365
+ const dir = normalizeAdrDir(
366
+ opts.adrDir ?? user.adr?.dir ?? DEFAULT_ADR_DIR,
367
+ );
368
+ report.adr = scaffoldAdr(cwd, {
369
+ dir,
370
+ force: opts.force,
371
+ hooks: true,
372
+ });
373
+ } else if (opts.adr === false) {
374
+ report.adr = { config: disableProjectAdr(cwd), enabled: false };
375
+ }
376
+
377
+ if (opts.overlay) {
378
+ const overlayScript = path.join(__dirname, 'vendor-openspec-overlays.mjs');
379
+ const r = spawnSync(process.execPath, [overlayScript], {
380
+ cwd,
381
+ encoding: 'utf8',
382
+ });
383
+ report.overlay = {
384
+ status: r.status,
385
+ stdout: r.stdout?.trim() || '',
386
+ stderr: r.stderr?.trim() || '',
387
+ };
388
+ }
389
+
390
+ return report;
391
+ }
392
+
393
+ async function promptAgents() {
394
+ const rl = readline.createInterface({ input, output });
395
+ try {
396
+ process.stdout.write(`Init Forge project wiring for which environments?\n`);
397
+ process.stdout.write(` 1) Cursor\n`);
398
+ process.stdout.write(` 2) Claude Code\n`);
399
+ process.stdout.write(` 3) Codex CLI\n`);
400
+ process.stdout.write(` 4) All\n`);
401
+ process.stdout.write(`Enter numbers separated by commas (e.g. 1,2) or 4 for all: `);
402
+ const answer = (await rl.question('')).trim();
403
+ if (!answer || answer === '4') return ['cursor', 'claude', 'codex'];
404
+ const map = { 1: 'cursor', 2: 'claude', 3: 'codex' };
405
+ const picked = [
406
+ ...new Set(
407
+ answer
408
+ .split(/[,\s]+/)
409
+ .map((s) => map[s.trim()])
410
+ .filter(Boolean),
411
+ ),
412
+ ];
413
+ if (picked.length === 0) throw new Error('No agents selected');
414
+ return picked;
415
+ } finally {
416
+ rl.close();
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Offer to install + set up OpenSpec in this project.
422
+ * @returns {Promise<boolean>} true = user accepted OpenSpec setup
423
+ */
424
+ async function promptOpenSpecSetup() {
425
+ const rl = readline.createInterface({ input, output });
426
+ try {
427
+ const yn = (
428
+ await rl.question(
429
+ 'OpenSpec is not set up in this project. Install and set it up now? [Y/n] (n = built-in specs engine) ',
430
+ )
431
+ )
432
+ .trim()
433
+ .toLowerCase();
434
+ return !(yn === 'n' || yn === 'no');
435
+ } finally {
436
+ rl.close();
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Resolve the planning engine for `forge init`, offering OpenSpec setup when needed.
442
+ * @param {{ cwd: string, openspec: boolean | null }} opts
443
+ * @returns {Promise<string>} 'openspec' | 'specs'
444
+ */
445
+ async function resolveInitPlanEngine(opts) {
446
+ const configured = hasOpenSpecConfig(opts.cwd);
447
+
448
+ if (opts.openspec === false) return 'specs';
449
+
450
+ if (opts.openspec === true) {
451
+ if (!configured && process.stdin.isTTY) {
452
+ const setup = setupOpenSpec(opts.cwd);
453
+ for (const s of setup.steps) {
454
+ process.stdout.write(` [${s.ok ? 'ok' : 'FAIL'}] ${s.step}${s.detail ? ` — ${s.detail}` : ''}\n`);
455
+ }
456
+ if (!setup.ok) {
457
+ process.stderr.write(
458
+ 'OpenSpec setup failed — engine recorded as openspec; re-run `forge doctor --install` or `openspec init` manually.\n',
459
+ );
460
+ }
461
+ }
462
+ return 'openspec';
463
+ }
464
+
465
+ if (configured) return 'openspec';
466
+
467
+ const userDefault = loadUserPlanEngine();
468
+ if (userDefault === 'specs') return 'specs';
469
+
470
+ // Default (or user prefers openspec) but project has no OpenSpec yet
471
+ if (!process.stdin.isTTY) {
472
+ return userDefault === 'openspec' ? 'openspec' : 'specs';
473
+ }
474
+
475
+ const accepted = await promptOpenSpecSetup();
476
+ if (!accepted) return 'specs';
477
+
478
+ const setup = setupOpenSpec(opts.cwd);
479
+ for (const s of setup.steps) {
480
+ process.stdout.write(` [${s.ok ? 'ok' : 'FAIL'}] ${s.step}${s.detail ? ` — ${s.detail}` : ''}\n`);
481
+ }
482
+ if (!setup.ok) {
483
+ process.stderr.write(
484
+ 'OpenSpec setup failed — falling back to the built-in specs engine. You can switch later with `forge init --openspec --force`.\n',
485
+ );
486
+ return 'specs';
487
+ }
488
+ return 'openspec';
489
+ }
490
+
491
+ /**
492
+ * @param {string} [defaultDir]
493
+ * @returns {Promise<{ enabled: boolean, dir: string }>}
494
+ */
495
+ async function promptAdrForInit(defaultDir = DEFAULT_ADR_DIR) {
496
+ const rl = readline.createInterface({ input, output });
497
+ let enabled = false;
498
+ try {
499
+ const yn = (
500
+ await rl.question(
501
+ 'Use Architecture Decision Records (ADRs) in this project? [y/N] ',
502
+ )
503
+ )
504
+ .trim()
505
+ .toLowerCase();
506
+ enabled = yn === 'y' || yn === 'yes';
507
+ if (!enabled) return { enabled: false, dir: defaultDir };
508
+ const dirAnswer = (
509
+ await rl.question(`ADR directory inside the repo [${defaultDir}]: `)
510
+ ).trim();
511
+ return { enabled: true, dir: normalizeAdrDir(dirAnswer || defaultDir) };
512
+ } finally {
513
+ rl.close();
514
+ }
515
+ }
516
+
517
+ async function main(argv = process.argv.slice(2)) {
518
+ const opts = parseArgs(argv);
519
+ if (opts.help) {
520
+ printHelp();
521
+ return 0;
522
+ }
523
+
524
+ let selected = opts.all ? ['cursor', 'claude', 'codex'] : [...new Set(opts.agents)];
525
+ if (selected.length === 0) {
526
+ if (!process.stdin.isTTY) {
527
+ process.stderr.write(
528
+ 'No agents specified. Pass --cursor/--claude/--codex/--all, or run in a TTY.\n',
529
+ );
530
+ return 1;
531
+ }
532
+ selected = await promptAgents();
533
+ }
534
+
535
+ const planEngine = await resolveInitPlanEngine({
536
+ cwd: opts.cwd,
537
+ openspec: opts.openspec,
538
+ });
539
+
540
+ let adr = opts.adr;
541
+ let adrDir = opts.adrDir;
542
+ if (adr === null) {
543
+ const user = loadUserConfig();
544
+ const defaultDir = user.adr?.dir ?? DEFAULT_ADR_DIR;
545
+ if (process.stdin.isTTY) {
546
+ const picked = await promptAdrForInit(defaultDir);
547
+ adr = picked.enabled;
548
+ adrDir = picked.dir;
549
+ } else if (user.adr?.enabled === true) {
550
+ adr = true;
551
+ adrDir = user.adr.dir ?? DEFAULT_ADR_DIR;
552
+ } else {
553
+ adr = false;
554
+ }
555
+ }
556
+
557
+ const report = initProject(selected, { ...opts, adr, adrDir, planEngine });
558
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
559
+ process.stdout.write(
560
+ `\nMerge hook snippets into settings if needed, ensure \`forge\` is on PATH, then open the project in your agent.\n`,
561
+ );
562
+ return 0;
563
+ }
564
+
565
+ const isDirect =
566
+ process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url;
567
+
568
+ if (isDirect) {
569
+ main()
570
+ .then((code) => process.exit(code))
571
+ .catch((err) => {
572
+ process.stderr.write(`${err.message || err}\n`);
573
+ process.exit(1);
574
+ });
575
+ }