@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.
- package/CHANGELOG.md +138 -0
- package/README.md +177 -38
- package/dist/bin/argv.js +5 -0
- package/dist/bin/argv.js.map +1 -1
- package/dist/bin/forge.js +1 -0
- package/dist/bin/forge.js.map +1 -1
- package/dist/extensions/forgecli/ask-user-tool.d.ts +17 -0
- package/dist/extensions/forgecli/ask-user-tool.js +139 -0
- package/dist/extensions/forgecli/ask-user-tool.js.map +1 -0
- package/dist/extensions/forgecli/forge-commands.d.ts +21 -0
- package/dist/extensions/forgecli/forge-commands.js +141 -0
- package/dist/extensions/forgecli/forge-commands.js.map +1 -1
- package/dist/extensions/forgecli/forge-init.d.ts +26 -0
- package/dist/extensions/forgecli/forge-init.js +948 -0
- package/dist/extensions/forgecli/forge-init.js.map +1 -0
- package/dist/extensions/forgecli/health-check.d.ts +18 -0
- package/dist/extensions/forgecli/health-check.js +154 -0
- package/dist/extensions/forgecli/health-check.js.map +1 -0
- package/dist/extensions/forgecli/hook-dispatcher.d.ts +34 -1
- package/dist/extensions/forgecli/hook-dispatcher.js +237 -3
- package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
- package/dist/extensions/forgecli/index.js +28 -11
- package/dist/extensions/forgecli/index.js.map +1 -1
- package/dist/extensions/forgecli/init-context.d.ts +99 -0
- package/dist/extensions/forgecli/init-context.js +163 -0
- package/dist/extensions/forgecli/init-context.js.map +1 -0
- package/dist/extensions/forgecli/init-progress.d.ts +39 -0
- package/dist/extensions/forgecli/init-progress.js +117 -0
- package/dist/extensions/forgecli/init-progress.js.map +1 -0
- package/dist/extensions/forgecli/refresh-kb-links.d.ts +18 -0
- package/dist/extensions/forgecli/refresh-kb-links.js +228 -0
- package/dist/extensions/forgecli/refresh-kb-links.js.map +1 -0
- package/dist/extensions/forgecli/store-validator.d.ts +13 -0
- package/dist/extensions/forgecli/store-validator.js +35 -0
- package/dist/extensions/forgecli/store-validator.js.map +1 -0
- package/dist/extensions/forgecli/transition-guard.d.ts +20 -0
- package/dist/extensions/forgecli/transition-guard.js +125 -0
- package/dist/extensions/forgecli/transition-guard.js.map +1 -0
- package/dist/forge-payload/.base-pack/commands/approve.md +22 -0
- package/dist/forge-payload/.base-pack/commands/collate.md +22 -0
- package/dist/forge-payload/.base-pack/commands/commit.md +22 -0
- package/dist/forge-payload/.base-pack/commands/enhance.md +37 -0
- package/dist/forge-payload/.base-pack/commands/fix-bug.md +22 -0
- package/dist/forge-payload/.base-pack/commands/implement.md +22 -0
- package/dist/forge-payload/.base-pack/commands/plan.md +22 -0
- package/dist/forge-payload/.base-pack/commands/quiz-agent.md +22 -0
- package/dist/forge-payload/.base-pack/commands/retrospective.md +22 -0
- package/dist/forge-payload/.base-pack/commands/review-code.md +22 -0
- package/dist/forge-payload/.base-pack/commands/review-plan.md +22 -0
- package/dist/forge-payload/.base-pack/commands/run-sprint.md +22 -0
- package/dist/forge-payload/.base-pack/commands/run-task.md +22 -0
- package/dist/forge-payload/.base-pack/commands/sprint-intake.md +22 -0
- package/dist/forge-payload/.base-pack/commands/sprint-plan.md +22 -0
- package/dist/forge-payload/.base-pack/commands/validate.md +22 -0
- package/dist/forge-payload/.claude-plugin/plugin.json +15 -0
- package/dist/forge-payload/.init/discovery/discover-database.md +32 -0
- package/dist/forge-payload/.init/discovery/discover-processes.md +31 -0
- package/dist/forge-payload/.init/discovery/discover-routing.md +31 -0
- package/dist/forge-payload/.init/discovery/discover-stack.md +33 -0
- package/dist/forge-payload/.init/discovery/discover-testing.md +34 -0
- package/dist/forge-payload/.init/generation/generate-kb-doc.md +60 -0
- package/dist/forge-payload/.schemas/bug.schema.json +53 -0
- package/dist/forge-payload/.schemas/collation-state.schema.json +16 -0
- package/dist/forge-payload/.schemas/event-sidecar.schema.json +22 -0
- package/dist/forge-payload/.schemas/event.schema.json +32 -0
- package/dist/forge-payload/.schemas/feature.schema.json +22 -0
- package/dist/forge-payload/.schemas/progress-entry.schema.json +16 -0
- package/dist/forge-payload/.schemas/project-context.schema.json +167 -0
- package/dist/forge-payload/.schemas/project-overlay.schema.json +25 -0
- package/dist/forge-payload/.schemas/sprint.schema.json +27 -0
- package/dist/forge-payload/.schemas/structure-versions.schema.json +57 -0
- package/dist/forge-payload/.schemas/task.schema.json +58 -0
- package/dist/forge-payload/.tools/banners.cjs +435 -0
- package/dist/forge-payload/.tools/build-context-pack.cjs +290 -0
- package/dist/forge-payload/.tools/build-init-context.cjs +322 -0
- package/dist/forge-payload/.tools/build-overlay.cjs +326 -0
- package/dist/forge-payload/.tools/build-persona-pack.cjs +226 -0
- package/dist/forge-payload/.tools/collate.cjs +1041 -0
- package/dist/forge-payload/.tools/generation-manifest.cjs +311 -0
- package/dist/forge-payload/.tools/lib/forge-root.cjs +59 -0
- package/dist/forge-payload/.tools/lib/paths.cjs +29 -0
- package/dist/forge-payload/.tools/lib/pricing.cjs +165 -0
- package/dist/forge-payload/.tools/lib/project-root.cjs +32 -0
- package/dist/forge-payload/.tools/lib/result.js +40 -0
- package/dist/forge-payload/.tools/lib/validate.js +131 -0
- package/dist/forge-payload/.tools/manage-config.cjs +340 -0
- package/dist/forge-payload/.tools/manage-versions.cjs +365 -0
- package/dist/forge-payload/.tools/seed-store.cjs +237 -0
- package/dist/forge-payload/.tools/store-cli.cjs +1123 -0
- package/dist/forge-payload/.tools/store.cjs +315 -0
- package/dist/forge-payload/.tools/substitute-placeholders.cjs +625 -0
- package/dist/forge-payload/.tools/validate-store.cjs +522 -0
- package/package.json +1 -1
- /package/dist/forge-payload/{personas → .base-pack/personas}/architect.md +0 -0
- /package/dist/forge-payload/{personas → .base-pack/personas}/bug-fixer.md +0 -0
- /package/dist/forge-payload/{personas → .base-pack/personas}/collator.md +0 -0
- /package/dist/forge-payload/{personas → .base-pack/personas}/engineer.md +0 -0
- /package/dist/forge-payload/{personas → .base-pack/personas}/librarian.md +0 -0
- /package/dist/forge-payload/{personas → .base-pack/personas}/orchestrator.md +0 -0
- /package/dist/forge-payload/{personas → .base-pack/personas}/product-manager.md +0 -0
- /package/dist/forge-payload/{personas → .base-pack/personas}/qa-engineer.md +0 -0
- /package/dist/forge-payload/{personas → .base-pack/personas}/supervisor.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/architect-skills.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/bug-fixer-skills.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/collator-skills.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/engineer-skills.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/generic-skills.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/librarian-skills.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/qa-engineer-skills.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/store-custodian-skills.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/supervisor-skills.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/CODE_REVIEW_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/COST_REPORT_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/PLAN_REVIEW_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/PLAN_SUMMARY_TEMPLATE.json +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/PLAN_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/PROGRESS_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/RETROSPECTIVE_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/SPRINT_MANIFEST_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/SPRINT_REQUIREMENTS_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/TASK_PROMPT_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/_fragments/context-injection.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/_fragments/event-emission-schema.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/_fragments/finalize.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/_fragments/progress-reporting.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/architect_approve.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/architect_review_sprint_completion.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/architect_sprint_intake.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/architect_sprint_plan.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/collator_agent.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/commit_task.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/fix_bug.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/implement_plan.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/migrate_structural.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/orchestrate_task.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/plan_task.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/quiz_agent.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/review_code.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/review_plan.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/run_sprint.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/sprint_retrospective.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/update_implementation.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/update_plan.md +0 -0
- /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
|