# Commands And Feedback This document is the normative SDK contract for plugin commands that may launch background work. ## Contract Every human menu click follows this sequence: ```text menu item -> command id -> registered command handler -> optional task -> feedback surfaces ``` - A manifest menu item declares a `command` id; it does not run tools directly. - The command handler is the only place that decides whether work should start. - A launched process is represented by a background task returned from `ctx.services.tasks.runCommand()`. - A skipped command is represented by the command handler return value and logs, not by a fake task. - Feedback objects must be stable enough for agents and programmatic callers to parse, and their messages must be readable by humans. - The current human menu-click UI does not guarantee display of a command handler return value. Use logs, Work/task state or plugin-owned UI/files when a human needs visible feedback today. ## Preconditions Check preconditions before calling `runCommand()`: - `ctx.services` exists. Plugins need the `tooling` capability for the service facade. - `ctx.services.workspace.getCurrentProject()` returned a project, or the caller supplied a valid `projectId`. - Required executables, environment variables and workspace files were validated, preferably with `ctx.services.tooling.diagnose()`. - `ownerAgentId` is a real agent id when the work is owned by an agent workflow. Do not use all-zero placeholders in production. - `cwd` is relative to the project root. - `command` and `args` are separate values. Do not shell-join user input. If any required precondition fails, return a skipped result: ```ts return { status: "skipped", reason: "missing-owner-agent", message: "Configure an owner agent before launching the hello-plugin task." }; ``` ## Feedback Surfaces Command handlers should return one of these shapes, or a plugin-specific object with equivalent fields: ```ts type CommandFeedback = | { status: "skipped"; reason: string; message: string } | { status: "launched"; taskId: string; state: string; message: string }; ``` Use the same `status` vocabulary consistently: - `skipped`: no background task was created. - `launched`: a task was created; inspect the task state for later progress. - `failed`: the handler itself failed before it could return normally. Visible surfaces are intentionally distinct: - Command return value: immediate feedback for programmatic callers, agents and future host surfaces. It is not a guaranteed visible UI surface for current human menu clicks. - Plugin logs: diagnostics for developers and operators. - Work/background-task surfaces: only for tasks actually launched through `runCommand()`. - Plugin layouts or files: optional plugin-owned user feedback. `recordOnly: true` affects completion delivery to the owning agent. It does not mean hidden, skipped or UI-silent. A record-only command task can still appear in Work and can still emit background-task events. ## Best-Effort Watches `ctx.services.workspace.watch()` is not a baseline precondition for commands. The host may reject it until workspace watch support is delivered. Treat watch setup as optional and non-fatal: ```ts try { const watch = await ctx.services.workspace.watch(".ideai", refresh, project.id); ctx.subscriptions.push(watch); } catch (error) { ctx.logger.info("workspace watch unavailable", { error }); } ``` Commands that depend on fresh workspace state should refresh or re-read that state when invoked instead of assuming a watch was installed at activation. ## Example ```ts const project = await ctx.services?.workspace.getCurrentProject(); if (!project) { return { status: "skipped", reason: "no-focused-project", message: "Open a project before running this command." }; } const ownerAgentId = await ctx.storage?.get("myPlugin.ownerAgentId"); if (!ownerAgentId) { return { status: "skipped", reason: "missing-owner-agent", message: "Configure an owner agent before launching this task." }; } const task = await ctx.services.tasks.runCommand({ projectId: project.id, ownerAgentId, label: "Run my tool", command: "npm", args: ["--version"], cwd: ".", recordOnly: true }); return { status: "launched", taskId: task.taskId, state: task.state, message: "Started Run my tool." }; ``` ## Counterexample Do not launch a shell command just to produce feedback: ```ts await ctx.services?.tasks.runCommand({ ownerAgentId: "00000000-0000-0000-0000-000000000000", label: "Skipped: missing config", command: "echo", args: ["missing config"], recordOnly: true }); ``` This creates misleading Work history, uses a placeholder owner and turns a precondition failure into a fake task. Return `status: "skipped"` instead.