import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { suggestRunIds, suggestTeams } from "../../command-completions.ts"; import { commandText, notifyCommandResult, parseRunArgs, parseScalar } from "../command-utils.ts"; import type { RegisterTeamCommandsDeps } from "./shared.ts"; import { handleTeamTool, teamCommandContext } from "./shared.ts"; export function registerRunCommands(pi: ExtensionAPI, deps: RegisterTeamCommandsDeps): void { pi.registerCommand("team-run", { description: "Manually start a pi-crew run (agent may also use the team tool autonomously)", // Round 13 UX: suggest team names for Tab-completion of the first positional arg. getArgumentCompletions: (argumentPrefix: string) => suggestTeams(argumentPrefix), handler: async (args: string, ctx: ExtensionCommandContext) => { const result = await handleTeamTool(parseRunArgs(args), { ...teamCommandContext(ctx), metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(ctx as ExtensionContext, runner, runId), abortForegroundRun: deps.abortForegroundRun, onRunStarted: undefined, }); // A foreground run can span a session switch (e.g. the user opens // an agent view mid-run and the run finishes there). The captured // ctx is stale after the switch: ANY access to it (even reading a // field) throws pi's "This extension ctx is stale ..." error, which // pi then prints into whichever session is active. Probe // defensively and skip the notification when the session moved on // (the result is still visible via /team-status). try { const capturedFile = ctx.sessionManager?.getSessionFile?.(); const currentFile = deps.getCurrentSessionFile?.(); if (capturedFile && currentFile && capturedFile !== currentFile) return; await notifyCommandResult(ctx, commandText(result)); } catch { // Stale ctx during the probe or notify — nothing to do here. } }, }); for (const [name, action, description] of [ ["team-resume", "resume", "Resume a pi-crew run by re-queueing failed/cancelled/skipped/running tasks"], ["team-export", "export", "Export a pi-crew run bundle to artifacts/export"], ["team-cancel", "cancel", "Cancel a pi-crew run"], ] as const) { pi.registerCommand(name, { description, // Round 13 UX: suggest recent run IDs for Tab-completion. getArgumentCompletions: (argumentPrefix: string) => suggestRunIds(argumentPrefix), handler: async (args: string, ctx: ExtensionCommandContext) => { const runId = args.trim() || undefined; const result = await handleTeamTool( { action, runId }, { ...teamCommandContext(ctx), getRunSnapshotCache: deps.getRunSnapshotCache, }, ); await notifyCommandResult(ctx, commandText(result)); }, }); } pi.registerCommand("team-invalidate", { description: "Invalidate the snapshot cache for a run so the UI refreshes immediately: ", getArgumentCompletions: (argumentPrefix: string) => suggestRunIds(argumentPrefix), handler: async (args: string, ctx: ExtensionCommandContext) => { const runId = args.trim() || undefined; if (!runId) { await notifyCommandResult(ctx, "Usage: /team-invalidate "); return; } const result = await handleTeamTool( { action: "invalidate", runId }, { ...teamCommandContext(ctx), getRunSnapshotCache: deps.getRunSnapshotCache, }, ); await notifyCommandResult(ctx, commandText(result)); }, }); pi.registerCommand("team-retry", { description: "Retry failed/cancelled pi-crew tasks: [taskId]", getArgumentCompletions: (argumentPrefix: string) => suggestRunIds(argumentPrefix), handler: async (args: string, ctx: ExtensionCommandContext) => { const tokens = args.trim().split(/\s+/).filter(Boolean); const runId = tokens.shift(); const taskId = tokens.shift(); if (!runId) { await notifyCommandResult(ctx, "Usage: /team-retry [taskId]"); return; } const retryResult = await handleTeamTool( { action: "retry", runId, taskId }, { ...teamCommandContext(ctx), getRunSnapshotCache: deps.getRunSnapshotCache, }, ); await notifyCommandResult(ctx, commandText(retryResult)); }, }); pi.registerCommand("team-respond", { description: "Respond to a waiting pi-crew task: ", getArgumentCompletions: (argumentPrefix: string) => suggestRunIds(argumentPrefix), handler: async (args: string, ctx: ExtensionCommandContext) => { const tokens = args.trim().split(/\s+/).filter(Boolean); const runId = tokens.shift(); const taskToken = tokens[0] === "--all" ? tokens.shift() : tokens.shift(); const taskId = taskToken === "--all" ? undefined : taskToken; const message = tokens.join(" ") || undefined; if (!runId || !taskToken || !message) { await notifyCommandResult(ctx, "Usage: /team-respond …"); return; } const result = await handleTeamTool({ action: "respond", runId, taskId, message }, teamCommandContext(ctx)); await notifyCommandResult(ctx, commandText(result)); }, }); pi.registerCommand("team-follow-up", { description: "Send a follow-up prompt to a pi-crew task: ", getArgumentCompletions: (argumentPrefix: string) => suggestRunIds(argumentPrefix), handler: async (args: string, ctx: ExtensionCommandContext) => { const tokens = args.trim().split(/\s+/).filter(Boolean); const runId = tokens.shift(); const taskId = tokens.shift(); const prompt = tokens.join(" ") || undefined; if (!runId || !taskId || !prompt) { await notifyCommandResult( ctx, "Usage: /team-follow-up . Use /team-respond for waiting-task replies.", ); return; } const result = await handleTeamTool( { action: "api", runId, config: { operation: "follow-up-agent", taskId, prompt }, }, teamCommandContext(ctx), ); await notifyCommandResult(ctx, commandText(result)); }, }); pi.registerCommand("team-api", { description: "Run safe pi-crew API interop operations: [key=value]", getArgumentCompletions: (argumentPrefix: string) => suggestRunIds(argumentPrefix), handler: async (args: string, ctx: ExtensionCommandContext) => { const tokens = args.trim().split(/\s+/).filter(Boolean); const positional = tokens.filter((token) => !token.includes("=") && !token.startsWith("--")); const runIdLessOperations = new Set(["metrics-snapshot"]); const first = positional[0]; const runId = first && runIdLessOperations.has(first) ? undefined : first; const operation = runId ? (positional[1] ?? "read-manifest") : (first ?? "read-manifest"); const config: Record = { operation }; for (const token of tokens.filter((item) => item.includes("="))) { const [key, ...rest] = token.split("="); if (key) config[key] = parseScalar(rest.join("=")); } const result = await handleTeamTool({ action: "api", runId, config }, teamCommandContext(ctx)); await notifyCommandResult(ctx, commandText(result)); }, }); pi.registerCommand("team-goal", { description: "Autonomous goal loop control (defaults to status): [start|status|pause|resume|stop|cancel|reset|step|clear] [goalId] [--objective=...] [--evaluatorModel=...] [--maxTurns=N]", // Suggest the stop-aliases only while completing the FIRST argument: // pi hands `getArgumentCompletions` the whole argument text (there is no // argument-index parameter), so any whitespace in it means the cursor is // already past arg 1 — nothing useful to suggest for goalId/flags. getArgumentCompletions: (argumentPrefix: string) => { if (argumentPrefix.includes(" ")) return []; const prefix = argumentPrefix.trim(); return [ { value: "cancel", label: "cancel", description: "stop the goal loop (alias of stop)" }, { value: "reset", label: "reset", description: "stop the goal loop (alias of stop)" }, ].filter((item) => item.value.startsWith(prefix)); }, handler: async (args: string, ctx: ExtensionCommandContext) => { const tokens = args.trim().split(/\s+/).filter(Boolean); const knownSubs = new Set(["start", "status", "pause", "resume", "stop", "step", "clear", "cancel", "reset"]); const subAction = tokens[0] && knownSubs.has(tokens[0]) ? tokens[0] : "status"; const positional = tokens.filter((token) => !token.includes("=") && !token.startsWith("--") && token !== subAction); const goalId = positional[0]; const config: Record = { subAction }; if (goalId) config.goalId = goalId; for (const token of tokens.filter((item) => item.includes("="))) { const [key, ...rest] = token.split("="); if (key) config[key.replace(/^--/, "")] = parseScalar(rest.join("=")); } const result = await handleTeamTool({ action: "goal", config }, teamCommandContext(ctx)); await notifyCommandResult(ctx, commandText(result)); }, }); pi.registerCommand("workflows", { description: "List all workflows (static + dynamic .dwf.ts)", handler: async (_args: string, ctx: ExtensionCommandContext) => { const result = await handleTeamTool({ action: "workflow-list" }, teamCommandContext(ctx)); await notifyCommandResult(ctx, commandText(result)); }, }); pi.registerCommand("team-metrics", { description: "Show pi-crew metrics snapshot: [filter]", handler: async (args: string, ctx: ExtensionCommandContext) => { const filter = args.trim() || undefined; const result = await handleTeamTool( { action: "api", config: { operation: "metrics-snapshot", filter }, }, { ...teamCommandContext(ctx), metricRegistry: deps.getMetricRegistry?.(), }, ); const text = commandText(result); const trimmed = text.trim(); const hint = !trimmed || trimmed === "[]" ? "\nNo metrics yet — observability may be disabled for this team. Set `observability: true` in the team frontmatter to enable." : ""; await notifyCommandResult(ctx, text + hint); }, }); pi.registerCommand("team-imports", { description: "List imported pi-crew run bundles", handler: async (_args: string, ctx: ExtensionCommandContext) => { const result = await handleTeamTool({ action: "imports" }, teamCommandContext(ctx)); await notifyCommandResult(ctx, commandText(result)); }, }); pi.registerCommand("team-import", { description: "Import a pi-crew run-export.json bundle into local imports", handler: async (args: string, ctx: ExtensionCommandContext) => { const tokens = args.trim().split(/\s+/).filter(Boolean); const pathArg = tokens.find((token) => !token.startsWith("--")); const scope = tokens.includes("--user") ? "user" : "project"; const result = await handleTeamTool({ action: "import", config: { path: pathArg, scope } }, teamCommandContext(ctx)); await notifyCommandResult(ctx, commandText(result)); }, }); }