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