Waits for the user to confirm an out-of-band action (e.g. 'did you finish resetting your password in the browser?'). Emits a UserAckCard to the renderer with a prompt and clickable options; returns the chosen option id to the agent.\n\nIMPORTANT — this is a user-wait GATE, not a regular tool. G4 bypasses the 30s TOOL_TIMEOUT_MS ceiling for this call and instead races the user's choice against USER_ACK_TIMEOUT_MS (default 15 min). Include one option with id 'done' to proceed; other option ids (e.g. 'failed', 'cancel') should cause the skill to stop subsequent plan steps.
/** * mcp/skills/waitForUserAck.ts — wait_for_user_ack synthetic tool * * This is NOT a normal tool. It is a registration shim for a first-class * G4 gate — the "user-ack" gate — which is routed specially in * electron/agent/guards/execution.ts:executeStep(). * * Why a tool at all? * ------------------ * Agent plans reference tools by name. Registering wait_for_user_ack in * the MCP tool registry lets the planner emit it as a plan step and the * execution LLM see its Zod schema (so it passes structured params like * { prompt, options }). But the actual `run()` below is never invoked — * G4 detects `meta.isUserWaitGate: true` and routes the step through * runUserAckGate() instead of the normal tool-execution pipeline. * * Why a dedicated gate (not a blocking run())? * -------------------------------------------- * electron/agent/guards/execution.ts:1122 wraps every tool invocation in * Promise.race([skillPromise, timeoutPromise]) against TOOL_TIMEOUT_MS * (default 30 s). An out-of-band SSPR / cloud password reset can take * minutes — a blocking tool run() would be force-killed at 30 s and the * user's eventual click would be orphaned. The user-ack gate runs * OUTSIDE that Promise.race with its own USER_ACK_TIMEOUT_MS (default * 15 min), resolving cleanly on choice or on gate timeout. * * Flow * ---- * 1. Skill emits { tool: "wait_for_user_ack", rationale, params } as a * normal plan step. * 2. G4 sees meta.isUserWaitGate === true and calls runUserAckGate() * with { prompt, options } from params. * 3. runUserAckGate emits "agent:user-ack-required" IPC with the prompt * + options; renderer shows UserAckCard. * 4. User clicks a choice → renderer calls sendUserAckResponse({ choice }). * 5. Gate resolves with { choice } (or { choice: "timeout" } if elapsed). * 6. The choice flows back to the agent's scratchpad as the tool result; * the skill prose branches on "done" vs other values. * * Safety net * ---------- * If G4's routing is broken or a caller invokes run() directly, the stub * below throws so the mistake is loud, not silently ignored. */import { z } from "zod";// -- Meta ---------------------------------------------------------------------export const meta = { name: "wait_for_user_ack", description: "Waits for the user to confirm an out-of-band action (e.g. 'did you " + "finish resetting your password in the browser?'). Emits a UserAckCard " + "to the renderer with a prompt and clickable options; returns the " + "chosen option id to the agent.\n" + "\n" + "IMPORTANT — this is a user-wait GATE, not a regular tool. G4 bypasses " + "the 30s TOOL_TIMEOUT_MS ceiling for this call and instead races the " + "user's choice against USER_ACK_TIMEOUT_MS (default 15 min). Include " + "one option with id 'done' to proceed; other option ids (e.g. 'failed', " + "'cancel') should cause the skill to stop subsequent plan steps.", riskLevel: "low", destructive: false, requiresConsent: false, supportsDryRun: false, affectedScope: ["user"], auditRequired: false, /** * The routing flag. G4's executeStep reads this and dispatches to * runUserAckGate() — the run() below is never invoked on the normal path. */ isUserWaitGate: true, schema: { prompt: z .string() .min(1) .describe( "Short question to show the user in the UserAckCard " + "(e.g. 'Did you complete the password reset in the browser?').", ), options: z .array( z.object({ id: z .string() .min(1) .describe( "Stable identifier returned in the gate result — the agent " + "branches on this value. Use 'done' for the happy-path continue " + "option; 'failed', 'cancel', 'timeout' for the sad paths.", ), label: z .string() .min(1) .describe("Human-readable button text shown in the UserAckCard."), kind: z .enum(["primary", "secondary", "cancel"]) .optional() .describe( "Optional visual hint for the button (primary = green/emerald, " + "secondary = neutral zinc, cancel = muted). Defaults to 'secondary'.", ), }), ) .min(1) .max(4) .describe( "Ordered list of clickable options. Include one 'done' option on " + "the happy path plus 1-3 sad-path options.", ), },} as const;// -- Exported run function ----------------------------------------------------/** * Safety-net stub. The run() function is never invoked on the normal path — * G4's executeStep() detects meta.isUserWaitGate and routes to runUserAckGate() * before the tool-execution block. If this throws, the routing is broken. */export async function run(): Promise<never> { throw new Error( "wait_for_user_ack.run() was invoked directly — this should never happen. " + "G4 is expected to route steps whose tool.meta.isUserWaitGate is true " + "through runUserAckGate() in electron/agent/guards/execution.ts, bypassing " + "the normal tool-execution pipeline. Check G4's executeStep() routing.", );}