/** * User-facing slash commands with `/` autocomplete. * These call the same functions as the LLM tools — no workflow_* typing required. */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent" import { DISPATCH_KINDS, formatResult, parseNumFlag, parseOperationArgs, type DispatchKind, type ExecuteOperation, type Operation, type ToolResult, } from "@naxodev/apnea" import { executePiOperation, PI_OPERATIONS } from "./runtime.ts" export const SUBS = [ ...PI_OPERATIONS.map((o) => o.verb), "resume", "help", ] as const satisfies readonly string[] function notify( ctx: { ui: { notify: (m: string, l?: "info" | "error" | "warning") => void } }, r: ToolResult, ) { const text = formatResult(r) ctx.ui.notify(text, r.ok ? "info" : "error") } function orchestratorKickMessage( kind: "start" | "resume", goal?: string, ): string { if (kind === "start") { return [ "You are the Apnea orchestrator for this run. Schedule only — no product code.", goal ? `Goal: ${goal}` : null, "State is step=planning with no pending dispatch.", 'Immediately call dispatch_role with kind="plan", then workflow_wait.', "Then continue the loop: plan_review → phase_package → code → code_review → workflow_commit_phase → … → pr_description.", "Use workflow_status if unsure. Never call workflow_reset_rounds. Escalate timeouts/caps/dirty-reviewer to the human.", ] .filter(Boolean) .join("\n") } return [ "You are the Apnea orchestrator resuming this run. Schedule only — no product code.", "Call workflow_status (and workflow_start action=resume if needed).", "If a pending artifact exists, workflow_wait. Else dispatch the legal kind for the current step.", "Never auto-invent steps; never workflow_reset_rounds; never product code.", ].join("\n") } function helpText(operations: readonly Operation[]): string { // verb + usage on one line (what to type), summary indented below (what // it does) — usage is what regressed when helpText() first went generated: // it rendered only the LLM-facing summary and dropped the flag syntax. const lines = operations.map((o) => { const invocation = o.usage ? `${o.verb} ${o.usage}` : o.verb return ` /apnea ${invocation}\n ${o.summary}` }) return [ "Apnea commands (tools remain for the model; you use /apnea):", ...lines, " /apnea resume # resume an existing run", ` dispatch kinds: ${DISPATCH_KINDS.join(" | ")}`, " /apnea help", ].join("\n") } export function registerApneaCommands( pi: ExtensionAPI, operations: readonly Operation[] = PI_OPERATIONS, execute: ExecuteOperation = executePiOperation, ): void { const run = ( signal: AbortSignal | undefined, verb: string, params: Record, ) => { const operation = operations.find((candidate) => candidate.verb === verb) if (!operation) throw new Error(`Missing Apnea operation: ${verb}`) return execute(operation.verb, params, { signal }) } const kick = (kind: "start" | "resume", goal?: string) => { pi.sendUserMessage(orchestratorKickMessage(kind, goal)) } pi.registerCommand("apnea", { description: "Apnea workflow: start, status, dispatch, wait, commit, …", getArgumentCompletions: (prefix: string) => { const parts = prefix.trimStart().split(/\s+/) // completing first token (subcommand) if (parts.length <= 1) { const p = parts[0] ?? "" const hits = SUBS.filter((s) => s.startsWith(p)).map((s) => ({ value: s, label: s, })) return hits.length ? hits : null } const sub = parts[0] if (sub === "dispatch") { const p = parts[1] ?? "" // Complete the kind token only — Pi replaces the last token. const hits = DISPATCH_KINDS.filter((k) => k.startsWith(p)).map((k) => ({ value: k, label: k, })) return hits.length ? hits : null } if (sub === "setup") { const p = parts[1] ?? "" const flags = ["--project", "--force", "--agents-md"] const hits = flags .filter((f) => f.startsWith(p) || p === "") .map((f) => ({ value: f, label: f })) return hits.length ? hits : null } if (sub === "start" && parts.length === 2 && parts[1]?.startsWith("--")) { const flags = ["--allow-dirty", "--slug="] const p = parts[1] ?? "" const hits = flags .filter((f) => f.startsWith(p)) .map((f) => ({ value: f, label: f })) return hits.length ? hits : null } if (sub === "commit") { const p = parts[1] ?? "" if (p.startsWith("-") || p === "") { const hits = ["--done"] .filter((f) => f.startsWith(p) || p === "") .map((f) => ({ value: f, label: f })) return hits.length ? hits : null } } if (sub === "reset-rounds") { const gates = [ "plan_review", "phase-01/code_review", "phase-02/code_review", ] const p = parts[1] ?? "" const hits = gates .filter((g) => g.startsWith(p)) .map((g) => ({ value: g, label: g })) return hits.length ? hits : null } return null }, handler: async (args, ctx) => { const raw = args.trim() if (!raw || raw === "help") { ctx.ui.notify(helpText(operations), "info") return } const tokens = raw.split(/\s+/).filter(Boolean) const sub = tokens[0]! try { const parsed = parseOperationArgs(sub, tokens.slice(1), { surface: "slash", }) if (!parsed.ok) { ctx.ui.notify(parsed.message, "error") return } const { flags, values, positional: rest } = parsed switch (sub) { case "help": ctx.ui.notify(helpText(operations), "info") return case "setup": notify( ctx, await run(ctx.signal, "setup", { project: flags.has("project"), force: flags.has("force"), agents_md: flags.has("agents-md"), }), ) return case "start": { // goal is everything not a flag; support --slug=x const slug = values.get("slug") const goal = rest.join(" ").trim() if (!goal) { ctx.ui.notify( "Usage: /apnea start [--allow-dirty] [--slug=name]", "error", ) return } const r = await run(ctx.signal, "start", { goal, slug, allow_dirty: flags.has("allow-dirty"), action: "start", }) notify(ctx, r) if (r.ok) kick("start", goal) return } case "resume": { const r = await run(ctx.signal, "start", { goal: "", action: "resume", }) notify(ctx, r) if (r.ok) kick("resume") return } case "abandon": notify( ctx, await run(ctx.signal, "abandon", { confirm: values.get("confirm"), stop_panes: flags.has("stop-panes") || undefined, stopped_work: flags.has("stopped-work") || undefined, acknowledge_corrupt: flags.has("acknowledge-corrupt") || undefined, }), ) return case "status": notify(ctx, await run(ctx.signal, "status", {})) return case "wait": { // `--timeout` and `--budget` are the same knob: how long THIS // call blocks. The role's deadline comes from config, stamped // at dispatch. Same flags as `apnea wait`; only the default // differs, because this surface has no host shell to time out. const poll = parseNumFlag(values, "poll") if (!poll.ok) { ctx.ui.notify( `Usage: /apnea wait [--poll=] (got --poll=${poll.raw})`, "error", ) return } const budget = parseNumFlag(values, "budget") if (!budget.ok) { ctx.ui.notify( `Usage: /apnea wait [--budget=] (got --budget=${budget.raw})`, "error", ) return } const timeout = parseNumFlag(values, "timeout") if (!timeout.ok) { ctx.ui.notify( `Usage: /apnea wait [--timeout=] (got --timeout=${timeout.raw})`, "error", ) return } const r = await run(ctx.signal, "wait", { poll_ms: poll.value, // Unbounded by default, like the Pi tool in `index.ts`: // `/apnea` runs inside Pi, which has no shell timeout, so // the CLI's chunking default would end the wait with a // green "still waiting" toast and nothing left polling. budget_ms: budget.value ?? timeout.value ?? Number.MAX_SAFE_INTEGER, }) notify(ctx, r) return } case "dispatch": { const kind = rest[0] as DispatchKind | undefined if (!kind || !DISPATCH_KINDS.includes(kind)) { ctx.ui.notify( `Usage: /apnea dispatch <${DISPATCH_KINDS.join("|")}> [--rework] [--redeliver]`, "error", ) return } notify( ctx, await run(ctx.signal, "dispatch", { kind, rework: flags.has("rework"), redeliver: flags.has("redeliver"), }), ) return } case "commit": { // `parseFlags` already strips `--` tokens into `flags`/`values`; // `rest` never contains them, so no filter is needed here. const message = rest.join(" ").trim() || undefined notify( ctx, await run(ctx.signal, "commit", { message, no_remaining_phases: flags.has("done") || undefined, }), ) return } case "reset-rounds": { const gate = rest[0] if (!gate) { ctx.ui.notify("Usage: /apnea reset-rounds ", "error") return } notify(ctx, await run(ctx.signal, "reset-rounds", { gate })) return } default: ctx.ui.notify( `Unknown subcommand: ${sub}\n${helpText(operations)}`, "error", ) } } catch (e) { ctx.ui.notify(e instanceof Error ? e.message : String(e), "error") } }, }) // Short aliases that also show in `/` autocomplete pi.registerCommand("apnea-status", { description: "Apnea: read-only run status (alias of /apnea status)", handler: async (_args, ctx) => notify(ctx, await run(ctx.signal, "status", {})), }) pi.registerCommand("apnea-start", { description: "Apnea: start a run — /apnea-start ", handler: async (args, ctx) => { const goal = args.trim() if (!goal) { ctx.ui.notify("Usage: /apnea-start ", "error") return } const r = await run(ctx.signal, "start", { goal, action: "start" }) notify(ctx, r) if (r.ok) kick("start", goal) }, }) }