@entelligentsia/forgecli 0.1.0 → 0.3.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 (144) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +177 -38
  3. package/dist/bin/argv.js +5 -0
  4. package/dist/bin/argv.js.map +1 -1
  5. package/dist/bin/forge.js +1 -0
  6. package/dist/bin/forge.js.map +1 -1
  7. package/dist/extensions/forgecli/ask-user-tool.d.ts +17 -0
  8. package/dist/extensions/forgecli/ask-user-tool.js +139 -0
  9. package/dist/extensions/forgecli/ask-user-tool.js.map +1 -0
  10. package/dist/extensions/forgecli/forge-commands.d.ts +21 -0
  11. package/dist/extensions/forgecli/forge-commands.js +141 -0
  12. package/dist/extensions/forgecli/forge-commands.js.map +1 -1
  13. package/dist/extensions/forgecli/forge-init.d.ts +26 -0
  14. package/dist/extensions/forgecli/forge-init.js +948 -0
  15. package/dist/extensions/forgecli/forge-init.js.map +1 -0
  16. package/dist/extensions/forgecli/health-check.d.ts +18 -0
  17. package/dist/extensions/forgecli/health-check.js +154 -0
  18. package/dist/extensions/forgecli/health-check.js.map +1 -0
  19. package/dist/extensions/forgecli/hook-dispatcher.d.ts +34 -1
  20. package/dist/extensions/forgecli/hook-dispatcher.js +237 -3
  21. package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
  22. package/dist/extensions/forgecli/index.js +28 -11
  23. package/dist/extensions/forgecli/index.js.map +1 -1
  24. package/dist/extensions/forgecli/init-context.d.ts +99 -0
  25. package/dist/extensions/forgecli/init-context.js +163 -0
  26. package/dist/extensions/forgecli/init-context.js.map +1 -0
  27. package/dist/extensions/forgecli/init-progress.d.ts +39 -0
  28. package/dist/extensions/forgecli/init-progress.js +117 -0
  29. package/dist/extensions/forgecli/init-progress.js.map +1 -0
  30. package/dist/extensions/forgecli/refresh-kb-links.d.ts +18 -0
  31. package/dist/extensions/forgecli/refresh-kb-links.js +228 -0
  32. package/dist/extensions/forgecli/refresh-kb-links.js.map +1 -0
  33. package/dist/extensions/forgecli/store-validator.d.ts +13 -0
  34. package/dist/extensions/forgecli/store-validator.js +35 -0
  35. package/dist/extensions/forgecli/store-validator.js.map +1 -0
  36. package/dist/extensions/forgecli/transition-guard.d.ts +20 -0
  37. package/dist/extensions/forgecli/transition-guard.js +125 -0
  38. package/dist/extensions/forgecli/transition-guard.js.map +1 -0
  39. package/dist/forge-payload/.base-pack/commands/approve.md +22 -0
  40. package/dist/forge-payload/.base-pack/commands/collate.md +22 -0
  41. package/dist/forge-payload/.base-pack/commands/commit.md +22 -0
  42. package/dist/forge-payload/.base-pack/commands/enhance.md +37 -0
  43. package/dist/forge-payload/.base-pack/commands/fix-bug.md +22 -0
  44. package/dist/forge-payload/.base-pack/commands/implement.md +22 -0
  45. package/dist/forge-payload/.base-pack/commands/plan.md +22 -0
  46. package/dist/forge-payload/.base-pack/commands/quiz-agent.md +22 -0
  47. package/dist/forge-payload/.base-pack/commands/retrospective.md +22 -0
  48. package/dist/forge-payload/.base-pack/commands/review-code.md +22 -0
  49. package/dist/forge-payload/.base-pack/commands/review-plan.md +22 -0
  50. package/dist/forge-payload/.base-pack/commands/run-sprint.md +22 -0
  51. package/dist/forge-payload/.base-pack/commands/run-task.md +22 -0
  52. package/dist/forge-payload/.base-pack/commands/sprint-intake.md +22 -0
  53. package/dist/forge-payload/.base-pack/commands/sprint-plan.md +22 -0
  54. package/dist/forge-payload/.base-pack/commands/validate.md +22 -0
  55. package/dist/forge-payload/.claude-plugin/plugin.json +15 -0
  56. package/dist/forge-payload/.init/discovery/discover-database.md +32 -0
  57. package/dist/forge-payload/.init/discovery/discover-processes.md +31 -0
  58. package/dist/forge-payload/.init/discovery/discover-routing.md +31 -0
  59. package/dist/forge-payload/.init/discovery/discover-stack.md +33 -0
  60. package/dist/forge-payload/.init/discovery/discover-testing.md +34 -0
  61. package/dist/forge-payload/.init/generation/generate-kb-doc.md +60 -0
  62. package/dist/forge-payload/.schemas/bug.schema.json +53 -0
  63. package/dist/forge-payload/.schemas/collation-state.schema.json +16 -0
  64. package/dist/forge-payload/.schemas/event-sidecar.schema.json +22 -0
  65. package/dist/forge-payload/.schemas/event.schema.json +32 -0
  66. package/dist/forge-payload/.schemas/feature.schema.json +22 -0
  67. package/dist/forge-payload/.schemas/progress-entry.schema.json +16 -0
  68. package/dist/forge-payload/.schemas/project-context.schema.json +167 -0
  69. package/dist/forge-payload/.schemas/project-overlay.schema.json +25 -0
  70. package/dist/forge-payload/.schemas/sprint.schema.json +27 -0
  71. package/dist/forge-payload/.schemas/structure-versions.schema.json +57 -0
  72. package/dist/forge-payload/.schemas/task.schema.json +58 -0
  73. package/dist/forge-payload/.tools/banners.cjs +435 -0
  74. package/dist/forge-payload/.tools/build-context-pack.cjs +290 -0
  75. package/dist/forge-payload/.tools/build-init-context.cjs +322 -0
  76. package/dist/forge-payload/.tools/build-overlay.cjs +326 -0
  77. package/dist/forge-payload/.tools/build-persona-pack.cjs +226 -0
  78. package/dist/forge-payload/.tools/collate.cjs +1041 -0
  79. package/dist/forge-payload/.tools/generation-manifest.cjs +311 -0
  80. package/dist/forge-payload/.tools/lib/forge-root.cjs +59 -0
  81. package/dist/forge-payload/.tools/lib/paths.cjs +29 -0
  82. package/dist/forge-payload/.tools/lib/pricing.cjs +165 -0
  83. package/dist/forge-payload/.tools/lib/project-root.cjs +32 -0
  84. package/dist/forge-payload/.tools/lib/result.js +40 -0
  85. package/dist/forge-payload/.tools/lib/validate.js +131 -0
  86. package/dist/forge-payload/.tools/manage-config.cjs +340 -0
  87. package/dist/forge-payload/.tools/manage-versions.cjs +365 -0
  88. package/dist/forge-payload/.tools/seed-store.cjs +237 -0
  89. package/dist/forge-payload/.tools/store-cli.cjs +1123 -0
  90. package/dist/forge-payload/.tools/store.cjs +315 -0
  91. package/dist/forge-payload/.tools/substitute-placeholders.cjs +625 -0
  92. package/dist/forge-payload/.tools/validate-store.cjs +522 -0
  93. package/package.json +1 -1
  94. /package/dist/forge-payload/{personas → .base-pack/personas}/architect.md +0 -0
  95. /package/dist/forge-payload/{personas → .base-pack/personas}/bug-fixer.md +0 -0
  96. /package/dist/forge-payload/{personas → .base-pack/personas}/collator.md +0 -0
  97. /package/dist/forge-payload/{personas → .base-pack/personas}/engineer.md +0 -0
  98. /package/dist/forge-payload/{personas → .base-pack/personas}/librarian.md +0 -0
  99. /package/dist/forge-payload/{personas → .base-pack/personas}/orchestrator.md +0 -0
  100. /package/dist/forge-payload/{personas → .base-pack/personas}/product-manager.md +0 -0
  101. /package/dist/forge-payload/{personas → .base-pack/personas}/qa-engineer.md +0 -0
  102. /package/dist/forge-payload/{personas → .base-pack/personas}/supervisor.md +0 -0
  103. /package/dist/forge-payload/{skills → .base-pack/skills}/architect-skills.md +0 -0
  104. /package/dist/forge-payload/{skills → .base-pack/skills}/bug-fixer-skills.md +0 -0
  105. /package/dist/forge-payload/{skills → .base-pack/skills}/collator-skills.md +0 -0
  106. /package/dist/forge-payload/{skills → .base-pack/skills}/engineer-skills.md +0 -0
  107. /package/dist/forge-payload/{skills → .base-pack/skills}/generic-skills.md +0 -0
  108. /package/dist/forge-payload/{skills → .base-pack/skills}/librarian-skills.md +0 -0
  109. /package/dist/forge-payload/{skills → .base-pack/skills}/qa-engineer-skills.md +0 -0
  110. /package/dist/forge-payload/{skills → .base-pack/skills}/store-custodian-skills.md +0 -0
  111. /package/dist/forge-payload/{skills → .base-pack/skills}/supervisor-skills.md +0 -0
  112. /package/dist/forge-payload/{templates → .base-pack/templates}/CODE_REVIEW_TEMPLATE.md +0 -0
  113. /package/dist/forge-payload/{templates → .base-pack/templates}/COST_REPORT_TEMPLATE.md +0 -0
  114. /package/dist/forge-payload/{templates → .base-pack/templates}/PLAN_REVIEW_TEMPLATE.md +0 -0
  115. /package/dist/forge-payload/{templates → .base-pack/templates}/PLAN_SUMMARY_TEMPLATE.json +0 -0
  116. /package/dist/forge-payload/{templates → .base-pack/templates}/PLAN_TEMPLATE.md +0 -0
  117. /package/dist/forge-payload/{templates → .base-pack/templates}/PROGRESS_TEMPLATE.md +0 -0
  118. /package/dist/forge-payload/{templates → .base-pack/templates}/RETROSPECTIVE_TEMPLATE.md +0 -0
  119. /package/dist/forge-payload/{templates → .base-pack/templates}/SPRINT_MANIFEST_TEMPLATE.md +0 -0
  120. /package/dist/forge-payload/{templates → .base-pack/templates}/SPRINT_REQUIREMENTS_TEMPLATE.md +0 -0
  121. /package/dist/forge-payload/{templates → .base-pack/templates}/TASK_PROMPT_TEMPLATE.md +0 -0
  122. /package/dist/forge-payload/{workflows → .base-pack/workflows}/_fragments/context-injection.md +0 -0
  123. /package/dist/forge-payload/{workflows → .base-pack/workflows}/_fragments/event-emission-schema.md +0 -0
  124. /package/dist/forge-payload/{workflows → .base-pack/workflows}/_fragments/finalize.md +0 -0
  125. /package/dist/forge-payload/{workflows → .base-pack/workflows}/_fragments/progress-reporting.md +0 -0
  126. /package/dist/forge-payload/{workflows → .base-pack/workflows}/architect_approve.md +0 -0
  127. /package/dist/forge-payload/{workflows → .base-pack/workflows}/architect_review_sprint_completion.md +0 -0
  128. /package/dist/forge-payload/{workflows → .base-pack/workflows}/architect_sprint_intake.md +0 -0
  129. /package/dist/forge-payload/{workflows → .base-pack/workflows}/architect_sprint_plan.md +0 -0
  130. /package/dist/forge-payload/{workflows → .base-pack/workflows}/collator_agent.md +0 -0
  131. /package/dist/forge-payload/{workflows → .base-pack/workflows}/commit_task.md +0 -0
  132. /package/dist/forge-payload/{workflows → .base-pack/workflows}/fix_bug.md +0 -0
  133. /package/dist/forge-payload/{workflows → .base-pack/workflows}/implement_plan.md +0 -0
  134. /package/dist/forge-payload/{workflows → .base-pack/workflows}/migrate_structural.md +0 -0
  135. /package/dist/forge-payload/{workflows → .base-pack/workflows}/orchestrate_task.md +0 -0
  136. /package/dist/forge-payload/{workflows → .base-pack/workflows}/plan_task.md +0 -0
  137. /package/dist/forge-payload/{workflows → .base-pack/workflows}/quiz_agent.md +0 -0
  138. /package/dist/forge-payload/{workflows → .base-pack/workflows}/review_code.md +0 -0
  139. /package/dist/forge-payload/{workflows → .base-pack/workflows}/review_plan.md +0 -0
  140. /package/dist/forge-payload/{workflows → .base-pack/workflows}/run_sprint.md +0 -0
  141. /package/dist/forge-payload/{workflows → .base-pack/workflows}/sprint_retrospective.md +0 -0
  142. /package/dist/forge-payload/{workflows → .base-pack/workflows}/update_implementation.md +0 -0
  143. /package/dist/forge-payload/{workflows → .base-pack/workflows}/update_plan.md +0 -0
  144. /package/dist/forge-payload/{workflows → .base-pack/workflows}/validate_task.md +0 -0
@@ -0,0 +1,948 @@
1
+ // forge-init.ts — /forge:init command handler — FORGE-S17-T02
2
+ //
3
+ // Full 4-phase init flow:
4
+ // Phase 1 — Collect: 5 parallel discovery scans → .forge/config.json
5
+ // Phase 2 — Discover: 7 parallel KB doc generation + project-context.json
6
+ // Phase 3 — Materialize: substitute-placeholders → .forge/{personas,skills,workflows,templates}
7
+ // Phase 4 — Register: 11 deterministic steps → versioning, packs, store, Tomoshibi
8
+ //
9
+ // Per INIT_PARITY_SPEC.md and PLAN.md (rev 2) phases A–G.
10
+ //
11
+ // Iron Laws:
12
+ // - Iron Law 1: no edits to forge/ or pi-mono/
13
+ // - Iron Law 6: execFile with argv arrays — no shell-string interpolation
14
+ // - Iron Law 7: silent continuation past failures is never acceptable
15
+ //
16
+ // Sub-decision bindings (from PLAN.md):
17
+ // #1: Marketplace skills — advisory only; write installedSkills: []
18
+ // #3: Parallel dispatch — vendored subagent via ctx.sendUserMessage instruction
19
+ // #4: /forge:enhance — sentinel + advisory only; no sendUserMessage dispatch
20
+ // #5: Tomoshibi — runRefreshKbLinks() native TS port; no shell-out
21
+ // #9: Health check — runHealthCheck() direct call; NOT via sendUserMessage
22
+ import { execFile } from "node:child_process";
23
+ import * as fs from "node:fs";
24
+ import * as path from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+ import { promisify } from "node:util";
27
+ import { runHealthCheck } from "./health-check.js";
28
+ import { buildProjectContext, computeCalibrationBaseline, discoverProjectName, validateProjectContext, writeProjectContext, } from "./init-context.js";
29
+ import { deleteInitProgress, readInitProgress, writeInitProgress } from "./init-progress.js";
30
+ import { getRefreshKbLinksHandler } from "./refresh-kb-links.js";
31
+ const execFileAsync = promisify(execFile);
32
+ // ── Bundle path resolution ─────────────────────────────────────────────────
33
+ const _EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
34
+ // dist/extensions/forgecli/ → dist/ → <pkg-root>/
35
+ const _DIST_DIR = path.resolve(_EXTENSION_DIR, "..", "..");
36
+ const _PKG_ROOT = path.resolve(_DIST_DIR, "..");
37
+ /** Get the bundled forge-payload root (dist/forge-payload/) */
38
+ export function getBundledPayloadRoot() {
39
+ return path.join(_PKG_ROOT, "dist", "forge-payload");
40
+ }
41
+ /** Get the bundled tools directory (dist/forge-payload/.tools/) */
42
+ export function getBundledToolsRoot() {
43
+ return path.join(getBundledPayloadRoot(), ".tools");
44
+ }
45
+ /**
46
+ * Resolve the absolute path to dist/forge-payload/.tools and validate it
47
+ * contains store-cli.cjs. Throws if the directory is missing or incomplete.
48
+ * Exported for test access and for Phase-4 pi-aware forgeRoot stamp.
49
+ */
50
+ export function resolveBundleToolsRoot() {
51
+ const toolsRoot = getBundledToolsRoot();
52
+ const storeCli = path.join(toolsRoot, "store-cli.cjs");
53
+ if (!fs.existsSync(storeCli)) {
54
+ throw new Error(`resolveBundleToolsRoot: bundled tools dir missing store-cli.cjs — expected at ${storeCli}. ` +
55
+ "Run 'npm run build' to populate dist/forge-payload/.tools/.");
56
+ }
57
+ return toolsRoot;
58
+ }
59
+ /**
60
+ * Detect pi runtime. forge-init.ts is only ever called from the forgecli pi
61
+ * extension (registerForgeInit is invoked during extension load by pi). There
62
+ * is no Claude Code execution path. Therefore this always returns true.
63
+ *
64
+ * We keep the guard explicit rather than hardcoding `true` so that if a future
65
+ * Claude Code path is added it is obvious where to insert the condition.
66
+ *
67
+ * Heuristic: PI_CODING_AGENT_DIR env set → definitely pi. Otherwise assume pi
68
+ * (our only caller). Only false if explicitly opted-out via env flag in a
69
+ * hypothetical future Claude Code integration.
70
+ * Exported for test access.
71
+ */
72
+ export function isPiRuntime() {
73
+ // If the caller explicitly overrides via env, respect it (test escape hatch only).
74
+ if (process.env.FORGE_INIT_CLAUDE_CODE_MODE === "1")
75
+ return false;
76
+ return true;
77
+ }
78
+ /** Get the bundled forge version from .claude-plugin/plugin.json */
79
+ function getBundledForgeVersion() {
80
+ try {
81
+ const pluginPath = path.join(getBundledPayloadRoot(), ".claude-plugin", "plugin.json");
82
+ const raw = fs.readFileSync(pluginPath, "utf8");
83
+ const plugin = JSON.parse(raw);
84
+ return typeof plugin.version === "string" ? plugin.version : "0.0.0";
85
+ }
86
+ catch {
87
+ return "0.0.0";
88
+ }
89
+ }
90
+ // ── Session-scoped banner state ────────────────────────────────────────────
91
+ // Prevents re-rendering the hero banner on resume within the same session.
92
+ let heroBannerShown = false;
93
+ // ── Non-interactive mode ───────────────────────────────────────────────────
94
+ /**
95
+ * Returns true when running in non-interactive / CI mode.
96
+ *
97
+ * Activated by either:
98
+ * - `FORGE_YES=1` — ergonomic shell shorthand (FORGE-S18-T01)
99
+ * - `FORGE_NON_INTERACTIVE=1` — set by `forge --non-interactive` flag
100
+ *
101
+ * When active, every Y/N gate resolves to its documented default without
102
+ * emitting a model-text prompt.
103
+ */
104
+ function isNonInteractive() {
105
+ return process.env.FORGE_YES === "1" || process.env.FORGE_NON_INTERACTIVE === "1";
106
+ }
107
+ function parseInitFlags(args) {
108
+ const parts = args.trim().split(/\s+/).filter(Boolean);
109
+ const hasFast = parts.includes("--fast");
110
+ const hasFull = parts.includes("--full");
111
+ // Find trailing numeric phase arg
112
+ let startPhase = null;
113
+ let invalidPhase = false;
114
+ for (let i = 0; i < parts.length; i++) {
115
+ const p = parts[i];
116
+ if (p === "--fast" || p === "--full")
117
+ continue;
118
+ const n = parseInt(p, 10);
119
+ if (!Number.isNaN(n)) {
120
+ if (n >= 1 && n <= 4) {
121
+ startPhase = n;
122
+ }
123
+ else {
124
+ invalidPhase = true;
125
+ }
126
+ }
127
+ }
128
+ return {
129
+ fast: hasFast,
130
+ full: hasFull,
131
+ startPhase,
132
+ conflict: hasFast && hasFull,
133
+ invalidPhase,
134
+ };
135
+ }
136
+ // ── Tool invocation helpers ────────────────────────────────────────────────
137
+ async function runTool(toolPath, argv, cwd, timeout = 30000) {
138
+ try {
139
+ await execFileAsync("node", [toolPath, ...argv], {
140
+ cwd,
141
+ timeout,
142
+ encoding: "utf8",
143
+ });
144
+ }
145
+ catch (err) {
146
+ const e = err;
147
+ throw new Error(`Tool ${path.basename(toolPath)} failed: ${e.stderr?.trim() || e.message || "unknown error"}`);
148
+ }
149
+ }
150
+ async function runToolAdvisory(toolPath, argv, cwd, ctx, label, timeout = 30000) {
151
+ try {
152
+ await runTool(toolPath, argv, cwd, timeout);
153
+ return true;
154
+ }
155
+ catch (err) {
156
+ const e = err;
157
+ ctx.ui.notify(`△ ${label}: ${e.message ?? "failed"} — proceeding.`, "warning");
158
+ return false;
159
+ }
160
+ }
161
+ // ── Discovery prompt text ──────────────────────────────────────────────────
162
+ function buildPhase1PromptText(bundleRoot, projectName) {
163
+ const discoveryDir = path.join(bundleRoot, ".init", "discovery");
164
+ const topics = ["stack", "processes", "database", "routing", "testing"];
165
+ const topicLines = topics.map((t) => ` • ${path.join(discoveryDir, `discover-${t}.md`)}`).join("\n");
166
+ return `## /forge:init Phase 1 — Collect: 5 parallel discovery scans
167
+
168
+ Project: ${projectName}
169
+
170
+ Please use the **subagent** tool to run 5 discovery scans in parallel (mode: "parallel").
171
+
172
+ Each subagent should:
173
+ 1. Read the discovery prompt file at its assigned path (shown below)
174
+ 2. Analyze the current project codebase
175
+ 3. Return structured findings as JSON
176
+
177
+ Discovery prompt files:
178
+ ${topicLines}
179
+
180
+ Run all 5 concurrently with mode: "parallel". Collect all results before proceeding.
181
+ After all 5 complete, synthesize the findings into a unified config and write .forge/config.json.
182
+
183
+ Required .forge/config.json structure:
184
+ {
185
+ "version": "1",
186
+ "project": { "name": "${projectName}", "prefix": "<UPPERCASE_ABBREV>" },
187
+ "stack": { "primary": [...], "test": <framework>, "build": <tool>, "lint": <tool> },
188
+ "commands": { "test": "<test cmd>", "build": "<build cmd>", "lint": "<lint cmd>" },
189
+ "paths": {
190
+ "engineering": "engineering",
191
+ "store": ".forge/store",
192
+ "workflows": ".forge/workflows",
193
+ "commands": ".claude/commands/forge",
194
+ "templates": ".forge/templates"
195
+ }
196
+ }
197
+
198
+ Write the config with: node "${path.join(bundleRoot, ".tools/manage-config.cjs")}" set <key> <value>
199
+ Or write .forge/config.json directly as valid JSON.`;
200
+ }
201
+ function buildPhase2PromptText(bundleRoot, kbPath, projectName) {
202
+ const generateKbDocPath = path.join(bundleRoot, ".init", "generation", "generate-kb-doc.md");
203
+ const docs = ["stack", "processes", "database", "routing", "deployment", "entity-model", "stack-checklist"];
204
+ const docLines = docs.map((d) => ` • ${kbPath}/architecture/${d}.md`).join("\n");
205
+ return `## /forge:init Phase 2 — Discover: 7 parallel KB doc generation
206
+
207
+ Project: ${projectName}
208
+ KB path: ${kbPath}/
209
+ Rulebook: ${generateKbDocPath}
210
+
211
+ Please use the **subagent** tool to generate 7 knowledge-base documents in parallel (mode: "parallel").
212
+
213
+ Each subagent should:
214
+ 1. Read the rulebook at: ${generateKbDocPath}
215
+ 2. Analyze the project codebase for its assigned topic
216
+ 3. Write the resulting document to its assigned output file
217
+
218
+ Documents to generate:
219
+ ${docLines}
220
+
221
+ Run all 7 concurrently with mode: "parallel".
222
+ After all complete, check for any that returned "FAILED:" in their output — retry those once.
223
+
224
+ Also create these index files after generation:
225
+ - ${kbPath}/architecture/INDEX.md
226
+ - ${kbPath}/business-domain/INDEX.md
227
+ - ${kbPath}/MASTER_INDEX.md (scaffold)`;
228
+ }
229
+ // ── Phase 4 helper — .gitignore update ────────────────────────────────────
230
+ function updateGitignore(cwd, ctx) {
231
+ const gitignorePath = path.join(cwd, ".gitignore");
232
+ if (!fs.existsSync(gitignorePath)) {
233
+ // No gitignore — skip
234
+ return;
235
+ }
236
+ let content;
237
+ try {
238
+ content = fs.readFileSync(gitignorePath, "utf8");
239
+ }
240
+ catch {
241
+ return;
242
+ }
243
+ const IGNORE_PATTERNS = [".forge/store/events/", ".forge/store/events", ".forge/store/", ".forge/"];
244
+ const lines = content.split("\n");
245
+ const alreadyIgnored = lines.some((line) => {
246
+ const trimmed = line.trim();
247
+ if (!trimmed || trimmed.startsWith("#"))
248
+ return false;
249
+ return IGNORE_PATTERNS.some((pat) => trimmed.includes(pat));
250
+ });
251
+ if (alreadyIgnored) {
252
+ ctx.ui.notify("〇 .forge/store/events/ already gitignored — skipped.", "info");
253
+ return;
254
+ }
255
+ const toAppend = "\n# Forge — transient agent event logs (one file per phase, do not commit)\n.forge/store/events/\n";
256
+ try {
257
+ fs.appendFileSync(gitignorePath, toAppend, "utf8");
258
+ ctx.ui.notify("〇 Appended .forge/store/events/ to .gitignore.", "info");
259
+ }
260
+ catch {
261
+ ctx.ui.notify("△ Could not update .gitignore — update manually.", "warning");
262
+ }
263
+ }
264
+ // ── Phase 4 helper — agent instruction file linking ────────────────────────
265
+ async function linkAgentInstructionFile(cwd, kbPath, projectName, ctx) {
266
+ const INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md", "CLAUDE.local.md", ".cursorrules"];
267
+ const existing = INSTRUCTION_FILES.filter((f) => fs.existsSync(path.join(cwd, f)));
268
+ if (existing.length > 0) {
269
+ // Already exists — do NOT modify (per spec step 4-11: avoid KB-link bloat)
270
+ return;
271
+ }
272
+ // None exist — prompt to create minimal CLAUDE.md (G4: bypassed in non-interactive mode)
273
+ const ok = isNonInteractive()
274
+ ? true
275
+ : await ctx.ui.confirm("Create CLAUDE.md?", `No agent instruction file found at project root.\nCreate a minimal CLAUDE.md with links to the Forge knowledge base? [Y/n]`);
276
+ if (!ok) {
277
+ ctx.ui.notify("〇 KB not linked — run /forge:refresh-kb-links after creating CLAUDE.md.", "info");
278
+ return;
279
+ }
280
+ const claudeMdPath = path.join(cwd, "CLAUDE.md");
281
+ const content = [
282
+ `# ${projectName}`,
283
+ ``,
284
+ `## Forge Knowledge Base`,
285
+ ``,
286
+ `| Index | Contents |`,
287
+ `|-------|----------|`,
288
+ `| [MASTER_INDEX](${kbPath}/MASTER_INDEX.md) | All sprints, tasks, bugs, and features |`,
289
+ `| [Architecture](${kbPath}/architecture/INDEX.md) | Stack, processes, database, routing, deployment |`,
290
+ `| [Business Domain](${kbPath}/business-domain/INDEX.md) | Entity model and domain concepts |`,
291
+ ``,
292
+ `## Forge Workflows`,
293
+ ``,
294
+ `| Workflow | Purpose |`,
295
+ `|----------|---------|`,
296
+ `| /forge:plan | Research codebase, produce implementation plan |`,
297
+ `| /forge:implement | Execute approved plan, make code changes |`,
298
+ `| /forge:validate | Validate task implementation against acceptance criteria |`,
299
+ `| /forge:approve | Final architect approval gate |`,
300
+ `| /forge:commit | Stage and commit completed task artifacts |`,
301
+ `| /forge:fix-bug | Triage, diagnose, and fix a bug |`,
302
+ `| /forge:run-task | Full plan-implement-review-commit pipeline |`,
303
+ `| /forge:run-sprint | Execute all tasks in a sprint |`,
304
+ `| /forge:sprint-plan | Decompose sprint requirements into tasks |`,
305
+ `| /forge:sprint-intake | Elicit and structure requirements for a new sprint |`,
306
+ ``,
307
+ `---`,
308
+ `_Generated by /forge:init. Run /forge:refresh-kb-links to update._`,
309
+ ``,
310
+ ].join("\n");
311
+ try {
312
+ fs.writeFileSync(claudeMdPath, content, "utf8");
313
+ ctx.ui.notify("〇 Created CLAUDE.md with KB links.", "info");
314
+ }
315
+ catch (err) {
316
+ const e = err;
317
+ ctx.ui.notify(`△ Could not create CLAUDE.md: ${e.message ?? "unknown"}`, "warning");
318
+ }
319
+ }
320
+ // ── Main command registration ──────────────────────────────────────────────
321
+ export function registerForgeInit(pi) {
322
+ // Capture pi.sendUserMessage in closure — ExtensionCommandContext does not
323
+ // have sendUserMessage; it is on ExtensionAPI per pi types.ts:1187.
324
+ //
325
+ // FIX BUG-017 / BUG-023: all sendUserMessage calls during a command handler
326
+ // execution (which is itself an active agent turn) MUST carry deliverAs: "steer"
327
+ // to avoid the "Agent is already processing" runtime error. The command handler
328
+ // runs inside a turn boundary; raw sendUserMessage() without deliverAs throws.
329
+ const sendToAgent = (text) => pi.sendUserMessage(text, { deliverAs: "steer" });
330
+ pi.registerCommand("forge:init", {
331
+ description: "Bootstrap a new Forge SDLC project at the current working directory",
332
+ async handler(args, ctx) {
333
+ const cwd = process.cwd();
334
+ const bundleRoot = getBundledPayloadRoot();
335
+ const toolsRoot = getBundledToolsRoot();
336
+ const bundledVersion = getBundledForgeVersion();
337
+ // kbPathFinal is resolved in Phase 4 but used in post-phase report.
338
+ // Declare at handler scope so post-phase code can read it.
339
+ let kbPathFinal = "engineering";
340
+ // ── 1. Flag parsing ────────────────────────────────────────────────
341
+ const flags = parseInitFlags(args);
342
+ if (flags.conflict) {
343
+ ctx.ui.notify("× Conflicting flags: --fast and --full cannot be combined.", "error");
344
+ return;
345
+ }
346
+ // ── 2. Resume detection ────────────────────────────────────────────
347
+ const progressResult = readInitProgress(cwd);
348
+ let startPhase = flags.startPhase ?? 1;
349
+ if (progressResult.kind === "malformed") {
350
+ ctx.ui.notify("△ init-progress.json is malformed — deleting and starting fresh.", "warning");
351
+ deleteInitProgress(cwd);
352
+ }
353
+ else if (progressResult.kind === "stale") {
354
+ // Silently delete stale checkpoint and proceed fresh
355
+ deleteInitProgress(cwd);
356
+ }
357
+ else if (progressResult.kind === "valid") {
358
+ const lastPhase = progressResult.progress.lastPhase;
359
+ const nextPhase = Math.min(lastPhase + 1, 4);
360
+ const resumeBanner = `〇 Previous init detected — last completed phase: ${lastPhase} of 4\n` +
361
+ `Resume from Phase ${nextPhase}?`;
362
+ // G1: in non-interactive mode, default to not resuming (start fresh)
363
+ const shouldResume = isNonInteractive()
364
+ ? false
365
+ : await ctx.ui.confirm("Resume /forge:init?", resumeBanner);
366
+ if (shouldResume) {
367
+ startPhase = nextPhase;
368
+ // Skip hero banner on resume (session-scoped gate)
369
+ heroBannerShown = true;
370
+ }
371
+ else {
372
+ deleteInitProgress(cwd);
373
+ startPhase = 1;
374
+ }
375
+ }
376
+ // Override startPhase from flags if --fast/--full N or direct phase arg
377
+ if (flags.startPhase !== null) {
378
+ startPhase = flags.startPhase;
379
+ }
380
+ if (flags.invalidPhase) {
381
+ // Invalid phase specified — re-prompt via pre-flight (fall through to pre-flight)
382
+ startPhase = 1;
383
+ }
384
+ // ── 3. Hero banner (once per session) ────────────────────────────
385
+ if (!heroBannerShown) {
386
+ heroBannerShown = true;
387
+ const bannersTool = path.join(toolsRoot, "banners.cjs");
388
+ if (fs.existsSync(bannersTool)) {
389
+ await execFileAsync("node", [bannersTool, "forge"], {
390
+ cwd,
391
+ timeout: 5000,
392
+ }).catch(() => {
393
+ /* non-fatal */
394
+ });
395
+ await execFileAsync("node", [bannersTool, "--subtitle", `AI SDLC bootstrapper · forge:init v${bundledVersion}`], { cwd, timeout: 5000 }).catch(() => {
396
+ /* non-fatal */
397
+ });
398
+ }
399
+ }
400
+ // ── 4. Flag acknowledgement (--fast or --full, no phase jump) ────
401
+ if ((flags.fast || flags.full) && flags.startPhase === null) {
402
+ const mode = flags.fast ? "--fast" : "--full";
403
+ ctx.ui.notify(`〇 ${mode} — running all 4 phases sequentially (functionally equivalent).`, "info");
404
+ }
405
+ // ── 5. Pre-flight plan (unless jumping to a specific phase) ───────
406
+ const projectName = discoverProjectName(cwd);
407
+ if (flags.startPhase === null || flags.invalidPhase) {
408
+ const preflightSummary = `Forge Init — ${projectName}\n\n` +
409
+ `4 phases will run in this session (~45 seconds non-interactive):\n\n` +
410
+ ` 1 Collect — 5 parallel discovery scans → config.json\n` +
411
+ ` KB folder prompt (interactive)\n` +
412
+ ` 2 Discover — KB doc generation (LLM fan-out) + project-context.json\n` +
413
+ ` 3 Materialize — substitute-placeholders.cjs → fully functional workflows\n` +
414
+ ` 4 Register — versioning, manifest, cache, store entries, Tomoshibi\n\n` +
415
+ `Phase 1 is interactive (KB folder name prompt). Phases 2–4 are non-interactive\n` +
416
+ `and complete in under 45 seconds.`;
417
+ // G2: skip pre-flight confirm in non-interactive mode (proceed directly to Phase 1)
418
+ if (!isNonInteractive()) {
419
+ const proceed = await ctx.ui.confirm("Start /forge:init?", preflightSummary);
420
+ if (!proceed) {
421
+ ctx.ui.notify("〇 /forge:init cancelled.", "info");
422
+ return;
423
+ }
424
+ }
425
+ }
426
+ // ── Phase 1 — Collect ─────────────────────────────────────────────
427
+ if (startPhase <= 1) {
428
+ ctx.ui.setStatus?.("forge:init", "Phase 1/4: Collect");
429
+ const bannersTool = path.join(toolsRoot, "banners.cjs");
430
+ if (fs.existsSync(bannersTool)) {
431
+ await execFileAsync("node", [bannersTool, "--phase", "1", "4", "Collect", "north"], {
432
+ cwd,
433
+ timeout: 5000,
434
+ }).catch(() => {
435
+ /* non-fatal */
436
+ });
437
+ }
438
+ ctx.ui.notify("Running 5 discovery scans in parallel...", "info");
439
+ // Dispatch 5 discovery subagents via sendUserMessage instruction
440
+ // (model invokes the subagent tool with mode: "parallel")
441
+ const phase1Prompt = buildPhase1PromptText(bundleRoot, projectName);
442
+ sendToAgent(phase1Prompt);
443
+ await ctx.waitForIdle();
444
+ // KB folder prompt (spec §7, F2) — G3: skipped in non-interactive mode (default: "engineering")
445
+ if (!isNonInteractive()) {
446
+ const kbDescription = `Forge will create a folder for architecture docs, sprints, bugs, and features.\n` +
447
+ `Default name: engineering/\n\n` +
448
+ `Does "engineering" conflict with an existing folder in this project?`;
449
+ const hasConflict = await ctx.ui.confirm("Engineering folder name?", kbDescription);
450
+ if (hasConflict) {
451
+ const customName = await ctx.ui.input("Engineering folder name?", "Enter preferred folder name (e.g. ai-docs, .forge-kb, docs/ai): ");
452
+ if (customName && customName.trim()) {
453
+ const manageConfigToolEarly = path.join(toolsRoot, "manage-config.cjs");
454
+ if (fs.existsSync(manageConfigToolEarly)) {
455
+ await runToolAdvisory(manageConfigToolEarly, ["set", "paths.engineering", customName.trim()], cwd, ctx, "manage-config paths.engineering");
456
+ }
457
+ }
458
+ }
459
+ }
460
+ // Marketplace skills advisory (sub-decision #1)
461
+ ctx.ui.notify("〇 Marketplace skills auto-recommendation is Claude-Code-only. " +
462
+ "Pi users install extensions manually. Writing installedSkills: []", "info");
463
+ // Write installedSkills: []
464
+ const manageConfigTool = path.join(toolsRoot, "manage-config.cjs");
465
+ if (fs.existsSync(manageConfigTool)) {
466
+ await runToolAdvisory(manageConfigTool, ["set", "installedSkills", "[]"], cwd, ctx, "manage-config installedSkills");
467
+ // Write mode = "full"
468
+ await runToolAdvisory(manageConfigTool, ["set", "mode", "full"], cwd, ctx, "manage-config mode");
469
+ }
470
+ writeInitProgress(cwd, 1);
471
+ ctx.ui.notify("〇 Phase 1 complete.", "info");
472
+ }
473
+ // ── Phase 2 — Discover ────────────────────────────────────────────
474
+ if (startPhase <= 2) {
475
+ ctx.ui.setStatus?.("forge:init", "Phase 2/4: Discover");
476
+ const bannersTool = path.join(toolsRoot, "banners.cjs");
477
+ if (fs.existsSync(bannersTool)) {
478
+ await execFileAsync("node", [bannersTool, "--phase", "2", "4", "Discover", "oracle"], {
479
+ cwd,
480
+ timeout: 5000,
481
+ }).catch(() => {
482
+ /* non-fatal */
483
+ });
484
+ }
485
+ // Read KB_PATH from config
486
+ let kbPath = "engineering";
487
+ try {
488
+ const configRaw = fs.readFileSync(path.join(cwd, ".forge", "config.json"), "utf8");
489
+ const config = JSON.parse(configRaw);
490
+ const paths = config.paths;
491
+ if (paths && typeof paths.engineering === "string" && paths.engineering) {
492
+ kbPath = paths.engineering;
493
+ }
494
+ }
495
+ catch {
496
+ // Use default
497
+ }
498
+ // Directory scaffolding
499
+ const dirs = [
500
+ path.join(cwd, kbPath),
501
+ path.join(cwd, kbPath, "architecture"),
502
+ path.join(cwd, kbPath, "business-domain"),
503
+ path.join(cwd, kbPath, "sprints"),
504
+ path.join(cwd, ".forge", "store"),
505
+ path.join(cwd, ".forge", "cache"),
506
+ ];
507
+ for (const dir of dirs) {
508
+ try {
509
+ fs.mkdirSync(dir, { recursive: true });
510
+ // Write .gitkeep for empty dirs
511
+ const keepPath = path.join(dir, ".gitkeep");
512
+ if (!fs.existsSync(keepPath)) {
513
+ fs.writeFileSync(keepPath, "", "utf8");
514
+ }
515
+ }
516
+ catch {
517
+ // Non-fatal
518
+ }
519
+ }
520
+ // Dispatch 7 parallel KB doc subagents
521
+ const phase2Prompt = buildPhase2PromptText(bundleRoot, kbPath, projectName);
522
+ sendToAgent(phase2Prompt);
523
+ await ctx.waitForIdle();
524
+ // Construct project-context.json
525
+ let kbPathResolved = kbPath;
526
+ let prefix = "";
527
+ try {
528
+ const configRaw = fs.readFileSync(path.join(cwd, ".forge", "config.json"), "utf8");
529
+ const config = JSON.parse(configRaw);
530
+ const proj = config.project;
531
+ if (proj && typeof proj.prefix === "string")
532
+ prefix = proj.prefix;
533
+ const paths = config.paths;
534
+ if (paths && typeof paths.engineering === "string")
535
+ kbPathResolved = paths.engineering;
536
+ }
537
+ catch {
538
+ // Use defaults
539
+ }
540
+ // Read config for full project context construction
541
+ let configForContext = {};
542
+ try {
543
+ const raw = fs.readFileSync(path.join(cwd, ".forge", "config.json"), "utf8");
544
+ configForContext = JSON.parse(raw);
545
+ }
546
+ catch {
547
+ // empty config
548
+ }
549
+ const projectCtx = buildProjectContext({
550
+ projectName: configForContext.project?.name ?? projectName,
551
+ prefix,
552
+ kbPath: kbPathResolved,
553
+ }, configForContext);
554
+ try {
555
+ validateProjectContext(projectCtx);
556
+ writeProjectContext(cwd, projectCtx);
557
+ ctx.ui.notify("〇 project-context.json written.", "info");
558
+ }
559
+ catch (err) {
560
+ const e = err;
561
+ ctx.ui.notify(`△ project-context.json validation failed: ${e.message ?? "unknown"} — proceeding.`, "warning");
562
+ }
563
+ // Calibration baseline
564
+ const baseline = computeCalibrationBaseline(cwd, kbPathResolved, bundledVersion);
565
+ const manageConfigTool = path.join(toolsRoot, "manage-config.cjs");
566
+ if (fs.existsSync(manageConfigTool)) {
567
+ await runToolAdvisory(manageConfigTool, ["set", "calibrationBaseline", JSON.stringify(baseline)], cwd, ctx, "manage-config calibrationBaseline");
568
+ }
569
+ writeInitProgress(cwd, 2);
570
+ ctx.ui.notify("〇 Phase 2 complete.", "info");
571
+ }
572
+ // ── Phase 3 — Materialize ─────────────────────────────────────────
573
+ if (startPhase <= 3) {
574
+ ctx.ui.setStatus?.("forge:init", "Phase 3/4: Materialize");
575
+ const bannersTool = path.join(toolsRoot, "banners.cjs");
576
+ if (fs.existsSync(bannersTool)) {
577
+ await execFileAsync("node", [bannersTool, "--phase", "3", "4", "Materialize", "supervisor"], {
578
+ cwd,
579
+ timeout: 5000,
580
+ }).catch(() => {
581
+ /* non-fatal */
582
+ });
583
+ }
584
+ const buildInitContextTool = path.join(toolsRoot, "build-init-context.cjs");
585
+ const substituteTool = path.join(toolsRoot, "substitute-placeholders.cjs");
586
+ const buildOverlayTool = path.join(toolsRoot, "build-overlay.cjs");
587
+ const basePackDir = path.join(bundleRoot, ".base-pack");
588
+ // 3a: build-init-context.cjs first build
589
+ if (fs.existsSync(buildInitContextTool)) {
590
+ await runToolAdvisory(buildInitContextTool, [
591
+ "--config",
592
+ path.join(cwd, ".forge", "config.json"),
593
+ "--personas",
594
+ path.join(cwd, ".forge", "personas"),
595
+ "--templates",
596
+ path.join(cwd, ".forge", "templates"),
597
+ "--kb",
598
+ cwd,
599
+ "--out",
600
+ path.join(cwd, ".forge", "init-context.md"),
601
+ "--json-out",
602
+ path.join(cwd, ".forge", "init-context.json"),
603
+ ], cwd, ctx, "build-init-context", 30000);
604
+ }
605
+ // 3b: substitute-placeholders.cjs — base-pack materialisation
606
+ if (fs.existsSync(substituteTool) && fs.existsSync(basePackDir)) {
607
+ await runToolAdvisory(substituteTool, [
608
+ "--forge-root",
609
+ bundleRoot,
610
+ "--base-pack",
611
+ basePackDir,
612
+ "--config",
613
+ path.join(cwd, ".forge", "config.json"),
614
+ "--context",
615
+ path.join(cwd, ".forge", "init-context.json"),
616
+ "--out",
617
+ cwd,
618
+ ], cwd, ctx, "substitute-placeholders", 60000);
619
+ }
620
+ // 3c: build-overlay.cjs smoke test (exit 1 is advisory)
621
+ if (fs.existsSync(buildOverlayTool)) {
622
+ await runToolAdvisory(buildOverlayTool, ["--task", "INIT-SMOKE-TEST", "--format", "json"], cwd, ctx, "build-overlay smoke (advisory)", 15000);
623
+ }
624
+ writeInitProgress(cwd, 3);
625
+ ctx.ui.notify("〇 Phase 3 complete.", "info");
626
+ }
627
+ // ── Phase 4 — Register ────────────────────────────────────────────
628
+ if (startPhase <= 4) {
629
+ ctx.ui.setStatus?.("forge:init", "Phase 4/4: Register");
630
+ const bannersTool = path.join(toolsRoot, "banners.cjs");
631
+ if (fs.existsSync(bannersTool)) {
632
+ await execFileAsync("node", [bannersTool, "--phase", "4", "4", "Register", "forge"], {
633
+ cwd,
634
+ timeout: 5000,
635
+ }).catch(() => {
636
+ /* non-fatal */
637
+ });
638
+ }
639
+ const manageConfigTool = path.join(toolsRoot, "manage-config.cjs");
640
+ const manageVersionsTool = path.join(toolsRoot, "manage-versions.cjs");
641
+ const generationManifestTool = path.join(toolsRoot, "generation-manifest.cjs");
642
+ const buildPersonaPackTool = path.join(toolsRoot, "build-persona-pack.cjs");
643
+ const buildContextPackTool = path.join(toolsRoot, "build-context-pack.cjs");
644
+ const buildInitContextTool = path.join(toolsRoot, "build-init-context.cjs");
645
+ const seedStoreTool = path.join(toolsRoot, "seed-store.cjs");
646
+ // Step 4-1: write paths.forgeRoot + copy schemas
647
+ // BUG-024 fix: under pi runtime stamp paths.forgeRoot to the bundled
648
+ // tools directory (dist/forge-payload/.tools/) which is the path where
649
+ // store-cli.cjs and all other tools live. This is always-pi when
650
+ // forge-init.ts runs (isPiRuntime() === true). Under a hypothetical
651
+ // future Claude Code path we fall back to bundleRoot.
652
+ if (fs.existsSync(manageConfigTool)) {
653
+ // BUG-024: under pi runtime resolve bundled tools path and validate
654
+ // store-cli.cjs is present before stamping. manageConfigTool guard
655
+ // ensures the validation block only runs when we're about to actually
656
+ // invoke manage-config — avoids false-positive errors in test contexts
657
+ // where fs is fully mocked and tools don't exist on disk.
658
+ let forgeRootToStamp;
659
+ if (isPiRuntime()) {
660
+ const toolsRoot = getBundledToolsRoot();
661
+ const storeCli = path.join(toolsRoot, "store-cli.cjs");
662
+ if (!fs.existsSync(storeCli)) {
663
+ ctx.ui.notify(`× step 4-1 paths.forgeRoot: store-cli.cjs missing from bundled tools (expected: ${storeCli}). ` +
664
+ "Run 'npm run build' to populate dist/forge-payload/.tools/. Aborting Phase 4.", "error");
665
+ return;
666
+ }
667
+ forgeRootToStamp = toolsRoot;
668
+ }
669
+ else {
670
+ // Claude Code path (not active today — preserved for future use)
671
+ forgeRootToStamp = bundleRoot;
672
+ }
673
+ await runToolAdvisory(manageConfigTool, ["set", "paths.forgeRoot", forgeRootToStamp], cwd, ctx, "step 4-1 paths.forgeRoot");
674
+ }
675
+ const schemasSrc = path.join(bundleRoot, ".schemas");
676
+ const schemasDest = path.join(cwd, ".forge", "schemas");
677
+ fs.mkdirSync(schemasDest, { recursive: true });
678
+ if (fs.existsSync(schemasSrc)) {
679
+ const schemaFiles = fs.readdirSync(schemasSrc).filter((f) => f.endsWith(".json"));
680
+ for (const f of schemaFiles) {
681
+ try {
682
+ fs.copyFileSync(path.join(schemasSrc, f), path.join(schemasDest, f));
683
+ }
684
+ catch {
685
+ // non-fatal
686
+ }
687
+ }
688
+ ctx.ui.notify(`〇 Copied ${schemaFiles.length} schema files to .forge/schemas/.`, "info");
689
+ }
690
+ // Step 4-1b: enhancement substrate
691
+ const enhancementsDir = path.join(cwd, ".forge", "enhancements");
692
+ fs.mkdirSync(enhancementsDir, { recursive: true });
693
+ const overlaySchemaPath = path.join(schemasSrc, "project-overlay.schema.json");
694
+ if (fs.existsSync(overlaySchemaPath)) {
695
+ try {
696
+ fs.copyFileSync(overlaySchemaPath, path.join(schemasDest, "project-overlay.schema.json"));
697
+ }
698
+ catch {
699
+ // non-fatal
700
+ }
701
+ }
702
+ // Step 4-2: manage-versions init
703
+ if (fs.existsSync(manageVersionsTool)) {
704
+ await runToolAdvisory(manageVersionsTool, ["init"], cwd, ctx, "step 4-2 manage-versions");
705
+ }
706
+ // Step 4-3: generation-manifest record-all
707
+ if (fs.existsSync(generationManifestTool)) {
708
+ await runToolAdvisory(generationManifestTool, ["record-all"], cwd, ctx, "step 4-3 generation-manifest", 30000);
709
+ }
710
+ // Step 4-4: build-persona-pack
711
+ if (fs.existsSync(buildPersonaPackTool)) {
712
+ await runToolAdvisory(buildPersonaPackTool, ["--out", path.join(cwd, ".forge", "cache", "persona-pack.json")], cwd, ctx, "step 4-4 build-persona-pack", 30000);
713
+ }
714
+ // Step 4-5: build-context-pack
715
+ try {
716
+ const raw = fs.readFileSync(path.join(cwd, ".forge", "config.json"), "utf8");
717
+ const cfg = JSON.parse(raw);
718
+ const p = cfg.paths;
719
+ if (p && typeof p.engineering === "string")
720
+ kbPathFinal = p.engineering;
721
+ }
722
+ catch {
723
+ // use default "engineering"
724
+ }
725
+ if (fs.existsSync(buildContextPackTool)) {
726
+ await runToolAdvisory(buildContextPackTool, [
727
+ "--arch-dir",
728
+ path.join(cwd, kbPathFinal, "architecture"),
729
+ "--out-md",
730
+ path.join(cwd, ".forge", "cache", "context-pack.md"),
731
+ "--out-json",
732
+ path.join(cwd, ".forge", "cache", "context-pack.json"),
733
+ ], cwd, ctx, "step 4-5 build-context-pack", 30000);
734
+ }
735
+ // Step 4-6: build-init-context final rebuild
736
+ if (fs.existsSync(buildInitContextTool)) {
737
+ await runToolAdvisory(buildInitContextTool, [
738
+ "--config",
739
+ path.join(cwd, ".forge", "config.json"),
740
+ "--personas",
741
+ path.join(cwd, ".forge", "personas"),
742
+ "--templates",
743
+ path.join(cwd, ".forge", "templates"),
744
+ "--kb",
745
+ cwd,
746
+ "--out",
747
+ path.join(cwd, ".forge", "init-context.md"),
748
+ "--json-out",
749
+ path.join(cwd, ".forge", "init-context.json"),
750
+ ], cwd, ctx, "step 4-6 build-init-context final", 30000);
751
+ }
752
+ // Step 4-7: seed-store
753
+ if (fs.existsSync(seedStoreTool)) {
754
+ await runToolAdvisory(seedStoreTool, [], cwd, ctx, "step 4-7 seed-store", 30000);
755
+ }
756
+ // Step 4-8: update-check cache baseline
757
+ const updateCachePath = path.join(cwd, ".forge", "update-check-cache.json");
758
+ try {
759
+ const pluginPath = path.join(bundleRoot, ".claude-plugin", "plugin.json");
760
+ const pluginRaw = fs.readFileSync(pluginPath, "utf8");
761
+ const plugin = JSON.parse(pluginRaw);
762
+ const cache = {
763
+ lastChecked: new Date().toISOString(),
764
+ installedVersion: plugin.version ?? bundledVersion,
765
+ latestVersion: plugin.version ?? bundledVersion,
766
+ upToDate: true,
767
+ };
768
+ fs.writeFileSync(updateCachePath, JSON.stringify(cache, null, 2) + "\n", "utf8");
769
+ ctx.ui.notify("〇 Update-check cache baseline written.", "info");
770
+ }
771
+ catch {
772
+ ctx.ui.notify("△ Could not write update-check cache — non-fatal.", "warning");
773
+ }
774
+ // Step 4-9: Tomoshibi — invoke refresh-kb-links handler directly
775
+ try {
776
+ const refreshHandler = getRefreshKbLinksHandler();
777
+ const refreshResult = await refreshHandler(cwd);
778
+ for (const msg of refreshResult.messages) {
779
+ ctx.ui.notify(msg, "info");
780
+ }
781
+ if (refreshResult.filesUpdated === 0) {
782
+ ctx.ui.notify("△ Run /forge:refresh-kb-links manually after init completes " +
783
+ "(no agent instruction files found).", "info");
784
+ }
785
+ }
786
+ catch (err) {
787
+ const e = err;
788
+ ctx.ui.notify(`△ Tomoshibi (refresh-kb-links) failed: ${e.message ?? "unknown"} — ` +
789
+ "Run /forge:refresh-kb-links manually after init completes.", "warning");
790
+ }
791
+ // Step 4-10: .gitignore update
792
+ updateGitignore(cwd, ctx);
793
+ // Step 4-10b: BUG-025 fix — remove Claude-Code-only .claude/commands/ artifact.
794
+ // substitute-placeholders.cjs (Phase 3) unconditionally writes command .md files
795
+ // to .claude/commands/<prefix>/ regardless of runtime. Under pi runtime pi never
796
+ // scans .claude/commands/ (commands are discovered via programmatic registerCommand
797
+ // in registerAllForgeCommands). Delete the directory so it does not pollute the
798
+ // project root. This runs in Phase 4 so it handles both same-session and resumed
799
+ // inits (where Phase 3 ran in a prior session).
800
+ if (isPiRuntime()) {
801
+ let commandsPrefix = "forge";
802
+ try {
803
+ const cfgRaw = fs.readFileSync(path.join(cwd, ".forge", "config.json"), "utf8");
804
+ const cfg = JSON.parse(cfgRaw);
805
+ const proj = cfg.project;
806
+ if (proj && typeof proj.prefix === "string" && proj.prefix) {
807
+ commandsPrefix = proj.prefix.toLowerCase();
808
+ }
809
+ }
810
+ catch {
811
+ // fall back to "forge"
812
+ }
813
+ const claudeCommandsDir = path.join(cwd, ".claude", "commands", commandsPrefix);
814
+ if (fs.existsSync(claudeCommandsDir)) {
815
+ try {
816
+ fs.rmSync(claudeCommandsDir, { recursive: true, force: true });
817
+ // Remove empty ancestor dirs best-effort
818
+ const parentDir = path.join(cwd, ".claude", "commands");
819
+ try {
820
+ if (fs.readdirSync(parentDir).length === 0) {
821
+ fs.rmdirSync(parentDir);
822
+ const grandparent = path.join(cwd, ".claude");
823
+ if (fs.readdirSync(grandparent).length === 0)
824
+ fs.rmdirSync(grandparent);
825
+ }
826
+ }
827
+ catch {
828
+ // best-effort
829
+ }
830
+ }
831
+ catch (err) {
832
+ const e = err;
833
+ ctx.ui.notify(`△ Could not remove .claude/commands/${commandsPrefix}/: ${e.message ?? "unknown"} — non-fatal.`, "warning");
834
+ }
835
+ }
836
+ }
837
+ // Step 4-11: agent instruction file linking
838
+ await linkAgentInstructionFile(cwd, kbPathFinal, projectName, ctx);
839
+ // Completion — delete init-progress
840
+ deleteInitProgress(cwd);
841
+ ctx.ui.notify("〇 Phase 4 complete — /forge:init done.", "info");
842
+ }
843
+ // ── Post-Phase-4: health check ────────────────────────────────────
844
+ ctx.ui.setStatus?.("forge:init", "Post-init: health check");
845
+ const healthResult = await runHealthCheck(cwd, bundleRoot);
846
+ if (healthResult.clean) {
847
+ ctx.ui.notify("〇 /forge:health: clean.", "info");
848
+ }
849
+ else {
850
+ ctx.ui.notify(`△ /forge:health: ${healthResult.gaps.length} gap(s) detected — see console output.`, "warning");
851
+ for (const gap of healthResult.gaps) {
852
+ ctx.ui.notify(` · ${gap.check}: ${gap.message}`, "info");
853
+ }
854
+ }
855
+ // ── post-init sentinel ────────────────────────────────────────────
856
+ const sentinelPath = path.join(cwd, ".forge", "cache", "post-init-enhancement-triggered");
857
+ if (!fs.existsSync(sentinelPath)) {
858
+ try {
859
+ fs.mkdirSync(path.dirname(sentinelPath), { recursive: true });
860
+ fs.writeFileSync(sentinelPath, new Date().toISOString() + "\n", "utf8");
861
+ ctx.ui.notify("〇 /forge:enhance — full implementation in S18+. " +
862
+ "Sentinel written; auto-trigger will fire when it lands.", "info");
863
+ }
864
+ catch {
865
+ // non-fatal
866
+ }
867
+ }
868
+ // ── Report ────────────────────────────────────────────────────────
869
+ // FIX BUG-020: read kbPathFinal from config.json at report time so
870
+ // a custom KB folder chosen in Phase 1 is reflected here. kbPathFinal
871
+ // is only updated inside the Phase-4 block, so if init was resumed
872
+ // from Phase 1-3 we still get the right value.
873
+ try {
874
+ const cfgRaw = fs.readFileSync(path.join(cwd, ".forge", "config.json"), "utf8");
875
+ const cfg = JSON.parse(cfgRaw);
876
+ const p = cfg.paths;
877
+ if (p && typeof p.engineering === "string" && p.engineering) {
878
+ kbPathFinal = p.engineering;
879
+ }
880
+ }
881
+ catch {
882
+ // use default "engineering" already set
883
+ }
884
+ ctx.ui.setStatus?.("forge:init", undefined);
885
+ const kbPath_ = kbPathFinal;
886
+ // FIX BUG-022 (product call): surface gap details in Report.
887
+ // Conservative path: always include gap list in the Report.
888
+ // Exit non-zero (via notify "error") only for blocking (severity: "error") gaps.
889
+ // Warning-severity gaps are advisory; init is considered successful.
890
+ //
891
+ // Rationale: exiting non-zero for any gap would break common fresh-init
892
+ // flows where KB docs haven't been generated yet (kb-freshness warning).
893
+ // Error gaps (e.g. config missing) indicate structural failure and must
894
+ // surface clearly.
895
+ const criticalGaps = healthResult.gaps.filter((g) => g.severity === "error");
896
+ const warningGaps = healthResult.gaps.filter((g) => g.severity === "warning");
897
+ let healthSection = `Health: ${healthResult.summary}`;
898
+ if (healthResult.gaps.length > 0) {
899
+ const gapLines = healthResult.gaps
900
+ .map((g) => ` [${g.severity.toUpperCase()}] ${g.check}: ${g.message}`)
901
+ .join("\n");
902
+ healthSection += `\n\nGap detail:\n${gapLines}`;
903
+ }
904
+ if (warningGaps.length > 0) {
905
+ healthSection += `\n\nWarning gaps are advisory. Run /forge:health anytime to recheck.`;
906
+ }
907
+ if (criticalGaps.length > 0) {
908
+ healthSection += `\n\n× CRITICAL: ${criticalGaps.length} blocking gap(s) — review the detail above and re-run /forge:init.`;
909
+ ctx.ui.notify(`× /forge:init: ${criticalGaps.length} critical gap(s) require attention — see Report.`, "error");
910
+ }
911
+ const report = [
912
+ ``,
913
+ `╔══════════════════════════════════════════════════════════════╗`,
914
+ `║ /forge:init complete ║`,
915
+ `╚══════════════════════════════════════════════════════════════╝`,
916
+ ``,
917
+ `Project: ${projectName}`,
918
+ `Bundle: forge v${bundledVersion}`,
919
+ ``,
920
+ `Knowledge base: ${kbPath_}/`,
921
+ `Personas: .forge/personas/`,
922
+ `Skills: .forge/skills/`,
923
+ `Workflows: .forge/workflows/`,
924
+ `Templates: .forge/templates/`,
925
+ ``,
926
+ healthSection,
927
+ ``,
928
+ `Next steps:`,
929
+ ` 1. Run /forge:sprint-intake to start your first sprint`,
930
+ ` 2. Run /forge:health anytime to check project health`,
931
+ ` 3. Run /forge:refresh-kb-links to update agent instruction file links`,
932
+ ``,
933
+ `Note: Marketplace skills auto-recommendation is Claude-Code-only.`,
934
+ `Pi users install extensions manually.`,
935
+ ``,
936
+ // BUG-025: explain slash command registration under pi runtime
937
+ ...(isPiRuntime()
938
+ ? [
939
+ `Note: Slash commands registered programmatically (pi runtime); skipping .claude/commands/ Claude-Code-only artifact.`,
940
+ ``,
941
+ ]
942
+ : []),
943
+ ].join("\n");
944
+ sendToAgent(report);
945
+ },
946
+ });
947
+ }
948
+ //# sourceMappingURL=forge-init.js.map