import { lstatSync } from "node:fs"; import path from "node:path"; import type { ExtensionAPI, ExtensionCommandContext, RegisteredCommand, } from "@earendil-works/pi-coding-agent"; import { boardSummaryBlock } from "./board-summary.ts"; import { runDoctor } from "./doctor.ts"; import { applyInitialization, inspectInitialization } from "./init.ts"; import { loadSystemRenderer } from "./loaders.js"; import { openHtmlFile } from "./open-browser.ts"; import { openEditableFile } from "./open-editor.ts"; import { loadProjectIndex } from "./project-index.ts"; import { systemArgumentCompletions } from "./system-command-meta.ts"; import { systemPromptFor } from "./system-prompts.ts"; const LIFECYCLE = new Set([ "research", "spec", "spec-verify", "assess", "phase", "plan", "issue", "exec", ]); const BUNDLE_SUBCOMMANDS = new Set([ "research", "spec", "phase", "plan", "issue", ]); const NON_LIFECYCLE = new Set([ "init", "doctor", "view", "book", "board", "edit", ]); type CommandOptions = Omit; type SystemCommandHooks = Readonly<{ beginLifecycle?: () => void; activateBundleTool?: () => void; activateBoardProposal?: (cwd: string) => void | Promise; activateBoardUpdate?: (cwd: string) => void | Promise; activateBundleMigration?: () => void; openBoard?: (cwd: string) => Promise; }>; export function createSystemCommand( pi: ExtensionAPI, hooks: SystemCommandHooks = {}, ): CommandOptions { return { description: "The System project governance", getArgumentCompletions: systemArgumentCompletions, handler: async (args: string, ctx: ExtensionCommandContext) => { const parsed = parseSystemArgs(args); if (!parsed.subcommand) { ctx.ui.notify(systemHelp(), "info"); return; } if (parsed.subcommand === "init") { await runSystemInit( pi, hooks.activateBundleMigration, parsed.rest, ctx, ); return; } if (parsed.subcommand === "view") { const selectors = parsed.rest.trim().split(/\s+/).filter(Boolean); if (selectors.length !== 1) { ctx.ui.notify("Usage: /system view ", "error"); return; } const output = await renderSystemView(ctx.cwd, selectors[0] as string); ctx.ui.notify(await openNotice("System view", output), "info"); return; } if (parsed.subcommand === "edit") { const selectors = parsed.rest.trim().split(/\s+/).filter(Boolean); if (selectors.length !== 1) { ctx.ui.notify("Usage: /system edit ", "error"); return; } const output = await editSystemDocument( ctx.cwd, selectors[0] as string, ); ctx.ui.notify(output, output.includes("opened:") ? "info" : "error"); return; } if (parsed.subcommand === "book") { if (parsed.rest.trim()) { ctx.ui.notify("Usage: /system book", "error"); return; } const output = await renderSystemBook(ctx.cwd); ctx.ui.notify(await openNotice("System book", output), "info"); return; } if (parsed.subcommand === "board") { if (parsed.rest.trim()) { ctx.ui.notify("Usage: /system board", "error"); return; } if (!hooks.openBoard) throw new Error("System board runtime is unavailable"); ctx.ui.notify(await hooks.openBoard(ctx.cwd), "info"); return; } if (parsed.subcommand === "doctor") { const result = runDoctor(ctx.cwd); ctx.ui.notify( formatDoctor(result), result.exitCode === 0 ? "info" : "warning", ); return; } hooks.beginLifecycle?.(); if (BUNDLE_SUBCOMMANDS.has(parsed.subcommand)) hooks.activateBundleTool?.(); if (parsed.subcommand === "assess") await hooks.activateBoardProposal?.(ctx.cwd); if (parsed.subcommand === "exec") await hooks.activateBoardUpdate?.(ctx.cwd); pi.sendUserMessage( systemPromptFor( parsed.subcommand, parsed.rest, boardSummaryBlock(ctx.cwd), ), ); if (ctx.mode === "print" || ctx.mode === "json") { await new Promise((resolve) => setTimeout(resolve, 0)); await ctx.waitForIdle(); } }, }; } function parseSystemArgs(args: string): { subcommand?: string; rest: string } { const trimmed = args.trim(); if (!trimmed) return { rest: "" }; const [raw = "", ...rest] = trimmed.split(/\s+/); const subcommand = raw.toLowerCase(); if (!LIFECYCLE.has(subcommand) && !NON_LIFECYCLE.has(subcommand)) return { rest: "" }; return { subcommand, rest: rest.join(" ") }; } function systemHelp(): string { return [ "/system ", "init [PROJECT-PREFIX]", "doctor", "view ", "edit ", "book", "board", "research [instructions]", "spec [instructions]", "spec-verify [instructions]", "assess [instructions]", "phase [instructions]", "plan [instructions]", "issue [instructions]", "exec [instructions]", ].join("\n"); } async function runSystemInit( pi: ExtensionAPI, activateBundleMigration: (() => void) | undefined, args: string, ctx: ExtensionCommandContext, ): Promise { let requested = args.trim() || undefined; let inspection = inspectInitialization(ctx.cwd, requested); requested ??= inspection.prefix; if (!requested) { requested = await ctx.ui.input("Project prefix", "SYS"); if (!requested) { ctx.ui.notify("/system init cancelled", "info"); return; } inspection = inspectInitialization(ctx.cwd, requested); } const configMigration = inspection.rewrites.find((item) => item.startsWith(".system/config.json "), ); const summary = [ `Root: ${inspection.root}`, `Prefix: ${inspection.prefix}`, ...inspection.creates.map((item) => `Create: ${item}`), ...inspection.cores .filter((item) => item.state === "missing") .map((item) => `Create: ${item.path}`), ...inspection.rewrites.map((item) => `Update: ${item}`), ].join("\n"); if (!(await ctx.ui.confirm("Initialize The System?", summary))) { ctx.ui.notify("/system init cancelled", "info"); return; } const reloadRequired = inspection.creates.length > 0 || inspection.rewrites.length > 0 || inspection.cores.some((core) => core.state === "missing"); const result = applyInitialization(ctx.cwd, requested, true); ctx.ui.notify( [ `Initialized The System at ${result.root}`, `Prefix: ${result.prefix}`, ...result.changed.map((item) => `Wrote: ${item}`), ...(reloadRequired ? ["Run /reload to activate System format skills."] : []), ].join("\n"), "info", ); if ( !configMigration || !(await ctx.ui.confirm( "Should System bundles be migrated by agent?", `Machine schema updated mechanically.\n${configMigration}`, )) ) return; await ctx.waitForIdle(); activateBundleMigration?.(); pi.sendUserMessage( systemPromptFor( "init", [ "The human approved agent-led bundle migration.", "Machine config and board schema markers are already current; do not edit them.", `Source: ${configMigration}`, "Inspect existing bundles and migrate obsolete structure and content to the current contracts.", ].join("\n"), ), ); } function formatDoctor(result: ReturnType): string { if (result.exitCode === 0) return `System doctor OK\nRoot: ${result.root}`; return [ `System doctor found ${result.diagnostics.length} issue(s)`, `Root: ${result.root}`, ...result.references.map((item) => `Ref: ${item.path} -> ${item.spec}`), ...result.diagnostics.map( (item) => `${item.code} ${item.path}: ${item.message}`, ), ].join("\n"); } async function renderSystemView( cwd: string, selector: string, ): Promise { const index = loadProjectIndex(cwd); const document = resolveDocument(index.root, index.documents, selector); const { renderSystemViewSite } = await loadSystemRenderer(); return renderSystemViewSite(index, document).file; } async function renderSystemBook(cwd: string): Promise { const { renderSystemBookSite } = await loadSystemRenderer(); return renderSystemBookSite(loadProjectIndex(cwd)).file; } async function editSystemDocument( cwd: string, selector: string, ): Promise { const index = loadProjectIndex(cwd); const file = editableSystemPath(index.root, index.documents, selector); const opened = await openEditableFile(file); if (opened.ok) return `System document opened: ${file}\nEditor: ${opened.command}`; return `System document not opened: ${file}\nEditor: ${opened.error}`; } function editableSystemPath( root: string, documents: ReturnType["documents"], selector: string, ): string { const normalized = selector.split(path.sep).join("/"); if (normalized.includes("/") || normalized.endsWith(".md")) { const target = path.resolve(root, selector); const relative = path.relative(root, target).split(path.sep).join("/"); if ( relative.startsWith("../") || relative === ".." || !relative.startsWith(".system/") ) throw new Error(`System edit path must be inside .system: ${selector}`); const stats = lstatSync(target, { throwIfNoEntry: false }); if (!stats?.isFile() || stats.isSymbolicLink()) throw new Error(`No editable System file: ${selector}`); return target; } return path.join(root, resolveDocument(root, documents, selector).path); } function resolveDocument( root: string, documents: ReturnType["documents"], selector: string, ) { const normalized = selector.split(path.sep).join("/"); if (normalized.includes("/") || normalized.endsWith(".md")) { const relative = path .relative(root, path.resolve(root, selector)) .split(path.sep) .join("/"); const found = documents.find((document) => document.path === relative); if (!found) throw new Error(`No System document path: ${selector}`); return found; } const byId = documents.find((document) => document.metadata.id === selector); if (byId) return byId; const bySlug = documents.filter((document) => { const bundle = document.path.split("/")[2] ?? ""; return bundle.endsWith(`-${selector}`); }); if (bySlug.length === 1 && bySlug[0]) return bySlug[0]; if (bySlug.length > 1) throw new Error(`Ambiguous System slug: ${selector}`); throw new Error(`No System document: ${selector}`); } async function openNotice(label: string, output: string): Promise { const opened = await openHtmlFile(output); if (opened.ok) return `${label} opened: ${opened.url}\nFile: ${output}\nBrowser: ${opened.command}`; return `${label} written: ${output}\nOpen: ${opened.url}\nBrowser: ${opened.error}`; }