/** * Standalone Commands Extension * * Repo-flavored slash commands that don't wrap a preset: * * /checkpoint [label] Create a named git stash checkpoint right now. * Complements git-checkpoint.ts's per-turn auto-stash * by giving the agent (or user) an on-demand snapshot. * * /handoff [brief] Write a handoff brief to scratch/HANDOFF.md and * notify. Use when ending a session so the next * session (or human) can pick up the thread. * * /proof [target] Kick off a "prove the edit took effect" turn — * nudges the agent to hit the affected parked bay's * endpoint over the compose network and report results. */ import * as fs from "node:fs"; import * as path from "node:path"; import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; function ts(): string { return new Date().toISOString().replace(/[:.]/g, "-"); } export default function standaloneCommands(pi: ExtensionAPI) { pi.registerCommand("checkpoint", { description: "Create a named git stash checkpoint [label]", handler: async (args: string | undefined, ctx: ExtensionCommandContext) => { const label = (args ?? "").trim() || "manual"; try { const create = await pi.exec("git", ["stash", "create"]); const ref = create.stdout.trim(); if (!ref) { ctx.ui.notify("Nothing to checkpoint — working tree is clean.", "info"); return; } const message = `pi-checkpoint: ${label} @ ${new Date().toISOString()}`; await pi.exec("git", ["stash", "store", "-m", message, ref]); ctx.ui.notify(`Checkpoint stored: ${label} (${ref.slice(0, 8)})`, "info"); } catch (err) { ctx.ui.notify(`Checkpoint failed: ${(err as Error).message}`, "error"); } }, }); pi.registerCommand("handoff", { description: "Write a handoff brief to scratch/HANDOFF.md [brief]", handler: async (args: string | undefined, ctx: ExtensionCommandContext) => { const brief = (args ?? "").trim(); const scratchDir = path.join(ctx.cwd, "scratch"); try { fs.mkdirSync(scratchDir, { recursive: true }); } catch (err) { ctx.ui.notify(`Cannot create scratch dir: ${(err as Error).message}`, "error"); return; } const file = path.join(scratchDir, "HANDOFF.md"); const session = ctx.sessionManager.getSessionFile() ?? "(unknown session)"; const header = [ `# Handoff — ${new Date().toISOString()}`, "", `- session: \`${session}\``, "", ].join("\n"); if (!brief) { // No brief provided — ask the agent to write one. const archive = path.join(scratchDir, `HANDOFF-${ts()}.md`); if (fs.existsSync(file)) fs.copyFileSync(file, archive); fs.writeFileSync(file, `${header}\n_Awaiting brief from agent._\n`); pi.sendUserMessage( `Write a handoff brief to \`scratch/HANDOFF.md\` summarising: what was the goal, what shipped, what is left, what to read next, and any open questions. Overwrite the placeholder. Keep it under 30 lines.`, ); return; } fs.writeFileSync(file, `${header}\n${brief}\n`); ctx.ui.notify(`Handoff written: ${path.relative(ctx.cwd, file)}`, "info"); }, }); pi.registerCommand("proof", { description: "Ask the agent to prove the recent edit took effect [target]", handler: async (args: string | undefined, _ctx: ExtensionCommandContext) => { const target = (args ?? "").trim(); const scope = target ? `Focus on: ${target}.` : `Cover every service whose source you touched this session.`; pi.sendUserMessage( [ `Prove the recent edit took effect, per AGENTS.md → "Proving an edit took effect".`, scope, `For each affected service, wait a beat for that codebase's reloader, then \`curl\` it over the compose network (using the recipe's process name/port, e.g. \`curl http://-web:3000/up\`) and quote the response. If the response does not reflect the edit, stop and diagnose.`, ].join(" "), ); }, }); }