/** * Durable Plan mode lifecycle. * * Act mode exposes plan-enter. Plan mode keeps the full tool surface; the * tool-call hook blocks only the built-in file-editing tools until approval. * Markdown drafts are persisted by workspace and chat session; approval must * commit before Act tools are restored. */ import { join } from "node:path"; import { createHash } from "node:crypto"; import { beginPlanAutoCycle, clearPlanAutoTaskContext, endPlanAutoCycle, getPlanAutoSnapshot, markPlanAutoConfirmed, isCurrentPlanAutoGrant, PLAN_AUTO_AUDIT_ENTRY, planAutoStatusLabel, resetPlanAuto, updatePlanAutoTaskContext, type PlanAutoGrantSnapshot, } from "./plan-auto.ts"; import type { ExtensionAPI, ExtensionContext, ToolDefinition, } from "@earendil-works/pi-coding-agent"; import type { AgentMessage, AgentToolResult } from "@earendil-works/pi-agent-core"; import { Key, Text, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"; import { Type } from "typebox"; import { altKey } from "../key-labels.ts"; import type { UserAttentionHandler } from "../notify/user-attention.ts"; import { getPlanTransports, type PlanTransportHandle, type PlanTransportRequest, type PlanTransportResult, } from "../plan-transport.ts"; import { toolCallLine, toolResultLine, resultSummary } from "pi-cockpit/src/quiet-tools.ts"; import { buildPlanDecomposeContract, PlanDecomposeParams } from "./plan-decompose.ts"; import { openPlanConfirmation, type PlanConfirmationModelTransition, type PlanWorkflowConfirmationOptions, } from "./plan-confirm.ts"; import { openPlanEditor } from "./plan-editor.ts"; import { detectDecisionDocuments } from "./plan-sources.ts"; import { PlanApprovalError, PlanStore, prewarmProcessIdentity, type LoadedPlan, type PlanArtifactEntry, type PlanExecutionChoice, type PlanExecutionContextMode, type PlanSessionIdentity, type PlanWorkflowBinding, } from "./plan-store.ts"; import { REFINE_ROLES, openRefinePanel, type RefineAppliesAs, type RefineRole, type RefineSession, } from "./plan-refine.ts"; import { renderRollbackOverlay } from "../tui/plan-rollback-overlay.ts"; import { getVisibleTasks } from "./todo.ts"; import { COMPACTION_LEASE_TIMEOUT_MS, type CompactionArbiter, } from "../compaction/compaction-arbiter.ts"; type Mode = "act" | "plan"; type PlanExecutionMode = PlanExecutionContextMode; export type PlanHandoffStatus = "none" | "todo-required" | "ready"; export type PlanContext = Pick< ExtensionContext, "cwd" | "hasUI" | "ui" | "mode" | "isIdle" | "sessionManager" | "compact" | "model" | "modelRegistry" | "isProjectTrusted" > & Partial>; interface PlanReviewOutcome { approved: boolean; exited: boolean; executionMode?: PlanExecutionMode; executionChoice?: PlanExecutionChoice; executionMessage?: string; discussionMessage?: string; compactDeferred?: boolean; approvalAudit?: PlanApprovalAudit; committedPlan?: LoadedPlan; canHandoff?: () => boolean; } export interface PlanApprovalAudit { source: "human-confirmation" | "human-preauthorization"; generation?: number; revision: number; checksum: string; handoffKey?: string; result: "requested" | "not-approved" | "committed" | "handoff-ready" | "handed-off" | "committed-handoff-stopped"; reason?: string; } type PlanHandoffDelivery = "message" | "tool-result"; export interface PlanToolDetails { action: "enter" | "update" | "review" | "confirm" | "decompose" | "exit" | "status"; mode: Mode; revision: number; path: string; sessionId: string; status: "empty" | "draft" | "approved"; handoffStatus: PlanHandoffStatus; handoffKey?: string; execution?: PlanExecutionChoice; workflowBinding?: PlanWorkflowBinding; approved?: boolean; approvalAudit?: PlanApprovalAudit; planAuto?: { enabled: boolean; generation?: number; confirmPending?: boolean }; error?: string; } export interface PlanCompactionSnapshot { mode: Mode; status: PlanToolDetails["status"]; revision: number; handoffStatus: PlanHandoffStatus; handoffKey?: string; path?: string; /** Approval identity, not an authorization to restart completed execution. */ checksum?: string; } /** Immutable Plan material carried by a Plan-aware deterministic context reset. */ export interface PlanNewContextPayload { status: "draft" | "approved"; revision: number; path: string; handoffKey?: string; checksum?: string; markdown: string; markdownTruncated?: boolean; } export interface LoadedPlanArtifactDocument { entry: PlanArtifactEntry; markdown: string; /** Absolute path of the artifact file under the store's plansDir. */ absolutePath: string; } export interface PlanArtifactSummary { available: boolean; revision: number; status: PlanToolDetails["status"]; } export interface PlanNewContextScheduleInput { executionMessage: string; plan: PlanNewContextPayload; continueAfterReset(): boolean; onCancelled(reason: string): void; } export interface PlanNewContextScheduleReceipt { requestId: number; coalesced: boolean; } interface PlanRuntimeOptions { storeFactory?: (cwd: string, session: PlanSessionIdentity) => PlanStore; hasExecutableTodo?: (handoffKey: string) => boolean; compactionArbiter?: CompactionArbiter; compactionHandoffTimeoutMs?: number; scheduleNewContext?: ( ctx: PlanContext, input: PlanNewContextScheduleInput, ) => PlanNewContextScheduleReceipt; restoreActModel?: (ctx: PlanContext) => Promise; describeModelTransition?: (ctx: PlanContext) => PlanConfirmationModelTransition | undefined; workflowConfirmation?: ( ctx: PlanContext, ) => PlanWorkflowConfirmationOptions | Promise; publishWorkflowPlan?: ( ctx: PlanContext, approved: LoadedPlan, execution: PlanExecutionChoice, ) => Promise; } export interface PlanWorkflowPublicationResult { binding: PlanWorkflowBinding; executionMessage: string; } const STATUS_KEY = "mode"; export const PLAN_TOGGLE_KEY = "alt+shift+p"; export const PLAN_TOGGLE_LABEL = altKey("Shift+P"); const PROPOSED_PLAN_PATTERN = /\s*([\s\S]*?)\s*<\/proposed_plan>/i; const PLAN_ENTER_TOOL = "plan-enter"; const PLAN_DECOMPOSE_TOOL = "plan-decompose"; const PLAN_MODE_TOOL_NAMES = [ "plan-update", "plan-review", "plan-confirm", "plan-exit", "plan-status", ] as const; const ALL_PLAN_TOOL_NAMES = new Set([PLAN_ENTER_TOOL, PLAN_DECOMPOSE_TOOL, ...PLAN_MODE_TOOL_NAMES]); const PlanUpdateParams = Type.Object({ markdown: Type.String({ description: "Complete Markdown text for current.md" }), expectedRevision: Type.Optional( Type.Integer({ minimum: 0, description: "Expected current revision for optimistic concurrency; the save fails if the draft has moved past it. Defaults to the currently loaded revision.", }), ), }); const EmptyPlanParams = Type.Object({}); let mode: Mode = "act"; let extensionApi: ExtensionAPI | undefined; let onPlanModeChanged: ((ctx: PlanContext) => void) | undefined; let storeFactory: (cwd: string, session: PlanSessionIdentity) => PlanStore = (cwd, session) => new PlanStore(cwd, { session }); let currentStore: PlanStore | undefined; let currentStoreKey = ""; let latestPlan: string | undefined; let latestRevision = 0; let latestStatus: PlanToolDetails["status"] = "empty"; let latestHandoffKey: string | undefined; let latestApprovedPath: string | undefined; let latestApprovedChecksum: string | undefined; let latestExecution: PlanExecutionChoice | undefined; let latestWorkflowBinding: PlanWorkflowBinding | undefined; let latestArtifactAvailable = false; let awaitingAction = false; let pendingPlanExitReminder: string | undefined; let pendingPlanEnterNote: string | undefined; let compactionArbiter: CompactionArbiter | undefined; let planCompactionHandoffTimeoutMs = COMPACTION_LEASE_TIMEOUT_MS; let schedulePlanNewContext: PlanRuntimeOptions["scheduleNewContext"]; let restorePlanActModel: NonNullable = async () => true; let describePlanModelTransition: NonNullable = () => undefined; let workflowConfirmation = async (_ctx: PlanContext): Promise => ({ allowNew: false }); let publishWorkflowPlan: ( ctx: PlanContext, approved: LoadedPlan, execution: PlanExecutionChoice, ) => Promise = async () => { throw new Error("Workflow-backed Plan execution is unavailable"); }; export const PLAN_CLEAN_CONTEXT_COMPACTION_MARKER = "[maestro-plan-clean-context]"; /** True only when the clean-context marker is the leading instruction token. */ export function isPlanCleanContextCompactionInstructions( customInstructions: string | undefined, ): boolean { return customInstructions?.trimStart().startsWith(PLAN_CLEAN_CONTEXT_COMPACTION_MARKER) === true; } export interface PlanCleanContextCompactionRequest { summary: string; firstKeptEntryId: string; lifecycleGeneration: number; requestId: number; } interface PlanHandoffRequestIdentity { lifecycleGeneration: number; requestId: number; } interface PlanOperationIdentity { lifecycleGeneration: number; operationId: number; sessionId: string; storeKey: string; store?: PlanStore; } interface PlanCompactHandoff { request: PlanHandoffRequestIdentity; operation: PlanOperationIdentity; planPath: string; markdown: string; executionMessage: string; onDelivered?: () => Promise; /** Set when another host compaction owns the boundary and must settle first. */ waitingForCompactionSettlement?: boolean; } interface PlanContextReplacement { lifecycleGeneration: number; replacement: AgentMessage; baselineMessages?: string[]; } let planLifecycleGeneration = 0; let nextPlanHandoffRequestId = 0; let nextPlanOperationId = 0; let activePlanOperation: PlanOperationIdentity | undefined; let activePlanHandoffRequest: PlanHandoffRequestIdentity | undefined; let pendingPlanCompactHandoff: PlanCompactHandoff | undefined; let activePlanCompactHandoff: PlanCompactHandoff | undefined; let planCompactionSettlementTimer: ReturnType | undefined; let pendingCleanContextCompaction: PlanCleanContextCompactionRequest | undefined; let planContextReplacement: PlanContextReplacement | undefined; // Review & Refine state is scoped to the current draft revision and reset when // the revision changes or the Plan is cleared. let refineSession: RefineSession | undefined; let refineSessionRevision = -1; let refineLatestOutput: string | undefined; let refineLatestRole: RefineRole | undefined; let refineLatestAppliesAs: RefineAppliesAs | undefined; let refineLatestRoleLabel: string | undefined; // The handoff gate reads todos only. Goal state was decoupled from it in 39a5f2dc; // the getters that used to feed it are gone so they cannot be wired back by accident. let hasExecutableTodoForHandoff = (handoffKey: string) => getVisibleTasks().some((task) => task.planHandoffKey === handoffKey && (task.status === "pending" || task.status === "in_progress") && task.blockedBy.length === 0 ); function syncModeStatus(ctx: PlanContext): void { ctx.ui.setStatus("maestro-plan-auto", planAutoStatusLabel(ctx)); ctx.ui.setStatus(STATUS_KEY, mode === "act" ? "ACT" : hasPlan() ? "READY" : "PLAN"); } /** Reuse the existing mode status/listener projection for authorization changes. */ export function refreshPlanModeStatus(ctx: PlanContext): void { syncModeStatus(ctx); onPlanModeChanged?.(ctx); } export function initPlan(pi: ExtensionAPI, options: PlanRuntimeOptions = {}): void { resetRuntimeState(); extensionApi = pi; storeFactory = options.storeFactory ?? ((cwd, session) => new PlanStore(cwd, { session })); hasExecutableTodoForHandoff = options.hasExecutableTodo ?? ((handoffKey) => getVisibleTasks().some((task) => task.planHandoffKey === handoffKey && (task.status === "pending" || task.status === "in_progress") && task.blockedBy.length === 0 )); compactionArbiter = options.compactionArbiter; planCompactionHandoffTimeoutMs = Math.max(1, options.compactionHandoffTimeoutMs ?? COMPACTION_LEASE_TIMEOUT_MS); schedulePlanNewContext = options.scheduleNewContext; restorePlanActModel = options.restoreActModel ?? (async () => true); describePlanModelTransition = options.describeModelTransition ?? (() => undefined); workflowConfirmation = async (ctx) => options.workflowConfirmation?.(ctx) ?? { allowNew: false }; publishWorkflowPlan = options.publishWorkflowPlan ?? (async () => { throw new Error("Workflow-backed Plan execution is unavailable"); }); } export function isPlanMode(): boolean { return mode === "plan"; } export function getMode(): Mode { return mode; } export function hasPlan(): boolean { return Boolean(latestPlan?.trim()); } export function getPlanText(): string { return latestPlan ?? ""; } export function getPlanArtifactSummary(): PlanArtifactSummary { return { available: latestArtifactAvailable, revision: latestRevision, status: latestStatus, }; } export async function loadCurrentPlanArtifacts(ctx: PlanContext): Promise { const store = await ensureStore(ctx); const entries = await store.listArtifacts(); latestArtifactAvailable = entries.length > 0; onPlanModeChanged?.(ctx); return Promise.all(entries.map(async (entry) => ({ entry, markdown: await store.readArtifact(entry), absolutePath: join(store.plansDir, entry.path), }))); } export function clearPlan(): void { clearPlanAutoTaskContext(); latestPlan = undefined; latestRevision = 0; latestStatus = "empty"; latestHandoffKey = undefined; latestApprovedPath = undefined; latestApprovedChecksum = undefined; latestExecution = undefined; latestWorkflowBinding = undefined; latestArtifactAvailable = false; refineSession = undefined; refineSessionRevision = -1; refineLatestOutput = undefined; refineLatestRole = undefined; refineLatestAppliesAs = undefined; refineLatestRoleLabel = undefined; awaitingAction = false; } async function ensureStore(ctx: PlanContext): Promise { const session = currentPlanSession(ctx); const storeKey = `${ctx.cwd}\0${session.id}`; if (!currentStore || currentStoreKey !== storeKey) { currentStore = storeFactory(ctx.cwd, session); currentStoreKey = storeKey; } return currentStore; } function currentPlanSession(ctx: PlanContext): PlanSessionIdentity { const file = ctx.sessionManager.getSessionFile(); const name = ctx.sessionManager.getSessionName(); return { id: ctx.sessionManager.getSessionId(), ...(file ? { file } : {}), ...(name ? { name } : {}), }; } function applyLoadedPlan(loaded: LoadedPlan): void { latestPlan = loaded.markdown || undefined; latestRevision = loaded.manifest.revision; latestStatus = loaded.markdown ? loaded.manifest.status : "empty"; latestHandoffKey = loaded.manifest.handoffKey; latestApprovedPath = loaded.manifest.approvedPath; latestApprovedChecksum = loaded.manifest.approvedChecksum; latestExecution = loaded.manifest.execution; latestWorkflowBinding = loaded.manifest.workflowBinding; latestArtifactAvailable = latestArtifactAvailable || Boolean(loaded.markdown.trim()) || loaded.manifest.approvals.length > 0; awaitingAction = loaded.manifest.status === "approved" && Boolean(loaded.markdown.trim()); } function ensureActToolSurface(): void { if (!extensionApi) return; const active = extensionApi.getActiveTools(); const missing = [...ALL_PLAN_TOOL_NAMES].filter((name) => !active.includes(name)); if (missing.length > 0) extensionApi.setActiveTools([...active, ...missing]); } function activatePlanToolSurface(): void {} function restoreActToolSurface(): void {} async function enterPlanMode(ctx: PlanContext, operation: PlanOperationIdentity): Promise { const store = await ensureStore(ctx); if (!bindPlanOperation(ctx, operation, store)) return false; const loaded = await store.loadQuick(); if (!isCurrentPlanOperation(ctx, operation)) return false; applyLoadedPlan(loaded); pendingPlanExitReminder = undefined; pendingPlanEnterNote = buildPlanEnterNote(); mode = "plan"; beginPlanAutoCycle(ctx, { markdown: latestPlan ?? "", revision: latestRevision }); activatePlanToolSurface(); syncModeStatus(ctx); onPlanModeChanged?.(ctx); ctx.ui.notify(`Plan mode · ${store.currentPath}`, "info"); return true; } function exitPlanMode(ctx: PlanContext, approved = false): void { if (!approved) endPlanAutoCycle(); mode = "act"; pendingPlanEnterNote = undefined; restoreActToolSurface(); syncModeStatus(ctx); } /** Leave Plan mode without approving or discarding the current draft. */ export function exitMode(ctx: PlanContext, operation = beginPlanOperation(ctx)): Mode { if (!isCurrentPlanOperation(ctx, operation, false)) return mode; if (mode === "plan") { exitPlanMode(ctx); ctx.ui.notify("Act mode · draft preserved", "info"); pendingPlanExitReminder = buildPlanExitReminder(); onPlanModeChanged?.(ctx); } return mode; } export async function exitModeAndRestore( ctx: PlanContext, operation = beginPlanOperation(ctx), ): Promise { const wasPlanMode = mode === "plan"; const nextMode = exitMode(ctx, operation); if (wasPlanMode && nextMode === "act") await restorePlanActModel(ctx); return nextMode; } export async function toggleMode(ctx: PlanContext, operation = beginPlanOperation(ctx)): Promise { if (mode === "act") { await enterPlanMode(ctx, operation); if (!isCurrentPlanOperation(ctx, operation)) return mode; return mode; } if (hasPlan() && ctx.hasUI !== false) { const outcome = await reviewPlan(ctx, true, "message", operation); if (!isCurrentPlanOperation(ctx, operation)) return mode; if (!outcome.approved && !outcome.exited) ctx.ui.notify("Staying in Plan mode", "info"); onPlanModeChanged?.(ctx); return mode; } return exitModeAndRestore(ctx, operation); } /** Bind the root UI/UCL to Plan/Act mode transitions. */ export function setPlanModeChangeListener(listener: ((ctx: PlanContext) => void) | undefined): void { onPlanModeChanged = listener; } export async function onSessionStartPlan(ctx: PlanContext): Promise { restoreActToolSurface(); resetRuntimeState(); const operation = beginPlanOperation(ctx); ensureActToolSurface(); syncModeStatus(ctx); prewarmProcessIdentity(); try { const store = await ensureStore(ctx); if (!bindPlanOperation(ctx, operation, store)) return; const loaded = await store.load(); if (!isCurrentPlanOperation(ctx, operation)) return; applyLoadedPlan(loaded); latestArtifactAvailable = await store.hasArtifacts(); if (!isCurrentPlanOperation(ctx, operation)) return; if (loaded.manifest.status === "approved" && loaded.manifest.execution?.backend === "workflow" && (loaded.manifest.workflowBinding?.status !== "bound" || loaded.manifest.workflowBinding.deliveryStatus === "pending")) { await recoverWorkflowBinding(ctx, store, loaded, operation); if (!isCurrentPlanOperation(ctx, operation)) return; } } catch (error) { if (!isCurrentPlanOperation(ctx, operation)) return; currentStore = undefined; currentStoreKey = ""; clearPlan(); ctx.ui.notify(`Plan draft unavailable: ${errorMessage(error)}`, "warning"); } } export function onSessionShutdownPlan(ctx: PlanContext): void { restoreActToolSurface(); resetRuntimeState(); ctx.ui.setStatus(STATUS_KEY, undefined); ctx.ui.setStatus("maestro-plan-auto", undefined); } function beginPlanOperation(ctx: PlanContext): PlanOperationIdentity { const session = currentPlanSession(ctx); const storeKey = `${ctx.cwd}\0${session.id}`; const operation: PlanOperationIdentity = { lifecycleGeneration: planLifecycleGeneration, operationId: ++nextPlanOperationId, sessionId: session.id, storeKey, ...(currentStore && currentStoreKey === storeKey ? { store: currentStore } : {}), }; activePlanOperation = operation; return operation; } function bindPlanOperation( ctx: PlanContext, operation: PlanOperationIdentity, store: PlanStore, ): boolean { if (!isCurrentPlanOperation(ctx, operation, false)) return false; operation.store = store; return isCurrentPlanOperation(ctx, operation); } function planOperationMatchesContext(ctx: PlanContext, operation: PlanOperationIdentity): boolean { try { const session = currentPlanSession(ctx); return session.id === operation.sessionId && `${ctx.cwd}\0${session.id}` === operation.storeKey; } catch { return false; // Pi invalidates context getters after replacement/reload. } } function isCurrentPlanOperation( ctx: PlanContext, operation: PlanOperationIdentity, requireStore = true, ): boolean { return activePlanOperation === operation && planLifecycleGeneration === operation.lifecycleGeneration && planOperationMatchesContext(ctx, operation) && (!requireStore || ( operation.store !== undefined && currentStore === operation.store && currentStoreKey === operation.storeKey )); } async function recoverWorkflowBinding( ctx: PlanContext, store: PlanStore, approved: LoadedPlan, operation: PlanOperationIdentity, ): Promise { const execution = approved.manifest.execution; const handoffKey = approved.manifest.handoffKey; const sourceChecksum = approved.manifest.approvedChecksum; const approvedPath = approved.manifest.approvedPath; if (!execution || execution.backend !== "workflow" || !handoffKey || !sourceChecksum || !approvedPath) return; if (!isCurrentPlanOperation(ctx, operation)) return; const priorBinding = approved.manifest.workflowBinding; let publicationInput = approved; try { if (priorBinding?.status !== "bound") { publicationInput = await store.updateWorkflowBinding(handoffKey, { status: "pending", handoffKey, sourceChecksum, updatedAt: new Date().toISOString(), }); if (!isCurrentPlanOperation(ctx, operation)) return; applyLoadedPlan(publicationInput); } if (!isCurrentPlanOperation(ctx, operation)) return; const published = await publishWorkflowPlan(ctx, publicationInput, execution); if (!isCurrentPlanOperation(ctx, operation)) return; if (published.binding.status !== "bound") { throw new Error("Workflow publisher did not return a bound result"); } const pendingDelivery = withPendingWorkflowDelivery(published.binding); const bound = await store.updateWorkflowBinding(handoffKey, pendingDelivery); if (!isCurrentPlanOperation(ctx, operation)) return; applyLoadedPlan(bound); ctx.ui.notify(`Recovered approved Plan binding to Workflow Session ${published.binding.workflowSessionId}.`, "info"); const planPath = join(bound.plansDir, approvedPath); const executionMessage = `${published.executionMessage}\n\n${buildPlanExecutionContract(planPath, handoffKey, undefined, undefined, bound.manifest.sourceDocuments?.[0])}`; if (!isCurrentPlanOperation(ctx, operation)) return; await deliverImplementation( ctx, planPath, bound.markdown, execution.context, executionMessage, "message", operation, () => acknowledgeWorkflowDelivery(ctx, store, handoffKey, pendingDelivery, operation), ); if (!isCurrentPlanOperation(ctx, operation)) return; } catch (error) { if (!isCurrentPlanOperation(ctx, operation)) return; if (priorBinding?.status !== "bound") { try { const failed = await store.updateWorkflowBinding(handoffKey, { status: "failed", handoffKey, sourceChecksum, error: errorMessage(error), updatedAt: new Date().toISOString(), }); if (!isCurrentPlanOperation(ctx, operation)) return; applyLoadedPlan(failed); } catch (persistError) { if (!isCurrentPlanOperation(ctx, operation)) return; ctx.ui.notify( `Approved Plan Workflow binding retry failed and its failure state could not be persisted: ${errorMessage(persistError)}.`, "warning", ); return; } } if (!isCurrentPlanOperation(ctx, operation)) return; ctx.ui.notify( priorBinding?.status === "bound" ? `Approved Plan Workflow delivery retry failed: ${errorMessage(error)}. The canonical binding is preserved.` : `Approved Plan Workflow binding retry failed: ${errorMessage(error)}. Approval is preserved; execution was not started.`, "warning", ); } } function withPendingWorkflowDelivery(binding: PlanWorkflowBinding): PlanWorkflowBinding { if (binding.status !== "bound" || !binding.requestId) return binding; return { ...binding, deliveryId: binding.deliveryId ?? `${binding.requestId}:implementation`, deliveryStatus: "pending", updatedAt: new Date().toISOString(), }; } async function acknowledgeWorkflowDelivery( ctx: PlanContext, store: PlanStore, handoffKey: string, binding: PlanWorkflowBinding, operation: PlanOperationIdentity, ): Promise { if (binding.status !== "bound" || binding.deliveryStatus !== "pending") return; if (!isCurrentPlanOperation(ctx, operation)) return; const deliveredAt = new Date().toISOString(); try { const delivered = await store.updateWorkflowBinding(handoffKey, { ...binding, deliveryStatus: "delivered", deliveredAt, updatedAt: deliveredAt, }); if (!isCurrentPlanOperation(ctx, operation)) return; applyLoadedPlan(delivered); } catch (error) { if (!isCurrentPlanOperation(ctx, operation)) return; ctx.ui.notify(`Workflow execution handoff was delivered but could not be acknowledged: ${errorMessage(error)}`, "warning"); } } function resetRuntimeState(): void { resetPlanAuto("plan-runtime-reset"); planLifecycleGeneration++; activePlanOperation = undefined; activePlanHandoffRequest = undefined; mode = "act"; currentStore = undefined; currentStoreKey = ""; latestPlan = undefined; latestRevision = 0; latestStatus = "empty"; latestHandoffKey = undefined; latestApprovedPath = undefined; latestApprovedChecksum = undefined; latestExecution = undefined; latestWorkflowBinding = undefined; latestArtifactAvailable = false; refineSession = undefined; refineSessionRevision = -1; refineLatestOutput = undefined; refineLatestRole = undefined; refineLatestAppliesAs = undefined; refineLatestRoleLabel = undefined; awaitingAction = false; pendingPlanExitReminder = undefined; pendingPlanEnterNote = undefined; pendingPlanCompactHandoff = undefined; activePlanCompactHandoff = undefined; if (planCompactionSettlementTimer) clearImmediate(planCompactionSettlementTimer); planCompactionSettlementTimer = undefined; pendingCleanContextCompaction = undefined; planContextReplacement = undefined; } export function onCompactPlan(ctx: PlanContext): void { planContextReplacement = undefined; syncModeStatus(ctx); const handoff = pendingPlanCompactHandoff; if (!handoff?.waitingForCompactionSettlement || !planOperationMatchesContext(ctx, handoff.operation) || !isCurrentPlanOperation(ctx, handoff.operation)) return; // session_compact is emitted while the host still owns its compaction // controller. Defer one macrotask so the host can clear that controller // before the void sendUserMessage call below. if (planCompactionSettlementTimer) return; planCompactionSettlementTimer = setImmediate(() => { planCompactionSettlementTimer = undefined; const current = pendingPlanCompactHandoff; if (!current || current !== handoff || !current.waitingForCompactionSettlement || !planOperationMatchesContext(ctx, current.operation) || !isCurrentPlanOperation(ctx, current.operation)) return; current.waitingForCompactionSettlement = false; pendingPlanCompactHandoff = undefined; deliverPlanHandoff(ctx, current); }); planCompactionSettlementTimer.unref?.(); } export function onContextPlan(messages: AgentMessage[]): { messages: AgentMessage[] } | undefined { const manifest = planContextReplacement; if (!manifest) return undefined; if (manifest.lifecycleGeneration !== planLifecycleGeneration) { planContextReplacement = undefined; return undefined; } const serialized = messages.map((message) => JSON.stringify(message)); if (!manifest.baselineMessages) { manifest.baselineMessages = serialized; return { messages: [manifest.replacement] }; } const baselineMatches = manifest.baselineMessages.length <= serialized.length && manifest.baselineMessages.every((message, index) => message === serialized[index]); if (!baselineMatches) { // Preserve the newest unmatched user turn while rebasing the dropped prefix. // This avoids resurrecting rewritten history without hiding the request that // caused a branch or host-prefix change. let suffixStart = -1; for (let index = messages.length - 1; index >= 0; index--) { if (messages[index]?.role !== "user") continue; if (index >= manifest.baselineMessages.length || serialized[index] !== manifest.baselineMessages[index]) { suffixStart = index; break; } } if (suffixStart < 0) suffixStart = messages.length; manifest.baselineMessages = serialized.slice(0, suffixStart); return { messages: [manifest.replacement, ...messages.slice(suffixStart)] }; } return { messages: [manifest.replacement, ...messages.slice(manifest.baselineMessages.length)], }; } export function projectPlanContextForCompaction(messages: AgentMessage[]): { messages: AgentMessage[]; firstKeptEntryId: string; } | undefined { const manifest = planContextReplacement; const projected = onContextPlan(messages); if (!manifest || !projected || manifest.lifecycleGeneration !== planLifecycleGeneration) return undefined; return { messages: projected.messages, firstKeptEntryId: `maestro-plan-replacement-${manifest.lifecycleGeneration}`, }; } export const PLAN_MODE_REMINDER_TYPE = "plan-mode-reminder"; export function onBeforeAgentStartPlan(_event: { systemPrompt: string }): { message: { customType: string; content: string; display: boolean }; } | undefined { if (pendingPlanEnterNote) { const note = pendingPlanEnterNote; pendingPlanEnterNote = undefined; return { message: { customType: PLAN_MODE_REMINDER_TYPE, content: note, display: false, }, }; } if (!pendingPlanExitReminder) return; const reminder = pendingPlanExitReminder; pendingPlanExitReminder = undefined; return { message: { customType: PLAN_MODE_REMINDER_TYPE, content: reminder, display: false, }, }; } export function onToolCallPlan(event: { toolName: string; input: Record; }, _bypassHandoff = false): { block: true; reason: string } | undefined { if (mode !== "plan") return undefined; // Plan mode blocks only the built-in file-editing tools. Everything else — // bash, run-control, todo, teammate, MCP, unknown tools — stays available. // A lexical mutation scanner is an incomplete guard anyway, and a long // blocklist false-blocks read-only tools (e.g. mcp search); the plan-mode // prompt carries the read-only planning convention instead. if (["edit", "write", "notebookedit", "notebook_edit"].includes(event.toolName.toLowerCase())) { return planMutationBlock(event.toolName); } return undefined; } function planMutationBlock(operation: string, detail?: string): { block: true; reason: string } { return { block: true, reason: `Plan mode is read-only before approval; ${operation} is blocked${detail ? ` (${detail})` : ""}. Approve or exit the Plan first.`, }; } export async function onAgentEndPlan(event: { messages: unknown[] }, ctx: PlanContext): Promise { if (mode !== "plan" || latestStatus === "approved") return; const proposedPlan = extractProposedPlan(latestAssistantText(event.messages)); if (!proposedPlan) return; const operation = beginPlanOperation(ctx); try { const store = await ensureStore(ctx); if (!bindPlanOperation(ctx, operation, store)) return; const saved = await store.saveDraft(proposedPlan, latestRevision); if (!isCurrentPlanOperation(ctx, operation)) return; applyLoadedPlan(saved); updatePlanAutoTaskContext(ctx, { markdown: saved.markdown, revision: latestRevision }); latestArtifactAvailable = Boolean(saved.markdown.trim()) || latestArtifactAvailable; syncModeStatus(ctx); onPlanModeChanged?.(ctx); ctx.ui.notify("Compatibility plan captured to current.md. Use plan-review or plan-confirm.", "info"); } catch (error) { if (!isCurrentPlanOperation(ctx, operation, operation.store !== undefined)) return; ctx.ui.notify(`Plan compatibility capture failed: ${errorMessage(error)}`, "warning"); } } async function savePlan( ctx: PlanContext, markdown: string, expectedRevision: number, operation: PlanOperationIdentity, ): Promise { const store = await ensureStore(ctx); if (!bindPlanOperation(ctx, operation, store)) return undefined; const saved = await store.saveDraft(markdown, expectedRevision); if (!isCurrentPlanOperation(ctx, operation)) return undefined; applyLoadedPlan(saved); if (!saved.markdown.trim()) clearPlanAutoTaskContext(); updatePlanAutoTaskContext(ctx, { markdown: saved.markdown, revision: latestRevision }); latestArtifactAvailable = Boolean(saved.markdown.trim()) || latestArtifactAvailable; syncModeStatus(ctx); onPlanModeChanged?.(ctx); return saved; } function isRecordValue(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function isPlanTransportResult(value: unknown): value is PlanTransportResult { if (!isRecordValue(value) || (value.status !== "decision" && value.status !== "edited" && value.status !== "cancelled")) return false; if (value.status === "cancelled") return true; if (value.status === "edited") { return typeof value.markdown === "string" && typeof value.expectedRevision === "number" && Number.isInteger(value.expectedRevision) && value.expectedRevision >= 0; } const decision = value.decision; if (!isRecordValue(decision) || !["execute", "modify", "continue", "refine", "rollback", "exit-plan", "close"].includes(String(decision.action))) return false; if (decision.discussion !== undefined && typeof decision.discussion !== "string") return false; const execution = decision.execution; if (execution === undefined) return true; if (!isRecordValue(execution) || (execution.backend !== "standalone" && execution.backend !== "workflow") || (execution.context !== "current" && execution.context !== "compact")) return false; if (execution.sourceDocument !== undefined && typeof execution.sourceDocument !== "string") return false; return execution.backend === "standalone" ? execution.workflowTarget === undefined : execution.workflowTarget === "current" || execution.workflowTarget === "new"; } function openPlanTransport(request: PlanTransportRequest): PlanTransportHandle | undefined { for (const transport of getPlanTransports()) { try { const handle = transport.open(request); if (handle) return handle; } catch (error) { console.error(`[maestro] Plan transport open failed: ${error instanceof Error ? error.message : String(error)}`); } } return undefined; } async function racePlanTransport( local: (signal: AbortSignal) => Promise, remote: PlanTransportHandle, signal: AbortSignal, ): Promise<{ source: "local"; value: T } | { source: "remote"; result: PlanTransportResult }> { const localController = new AbortController(); const localSignal = AbortSignal.any([signal, localController.signal]); let localPending = true; let remotePending = true; const localPromise = local(localSignal).then((value) => ({ source: "local" as const, value })); const remotePromise: Promise< | { source: "remote"; result: PlanTransportResult } | { source: "remote-error" } > = remote.promise.then( (result): { source: "remote"; result: PlanTransportResult } => ({ source: "remote", result: isPlanTransportResult(result) ? result : { status: "cancelled" }, }), () => ({ source: "remote-error" as const }), ); while (localPending || remotePending) { const candidates: Array> = []; if (localPending) candidates.push(localPromise); if (remotePending) candidates.push(remotePromise); const winner = await Promise.race(candidates); if (winner.source === "local") { localPending = false; try { await remote.cancel("tui_answered"); } catch { /* transport cleanup is best-effort */ } return winner; } remotePending = false; if (winner.source === "remote-error") { // A remote transport error only removes that projection. Keep the local // TUI alive; a reconnect replays the same request identity. continue; } if (winner.result.status === "cancelled") { localController.abort(); void localPromise.catch(() => undefined); return winner; } localController.abort(); void localPromise.catch(() => undefined); return winner; } return await localPromise; } function transportRequest( ctx: PlanContext, operation: PlanOperationIdentity, kind: PlanTransportRequest["kind"], signal: AbortSignal, options: Omit, ): PlanTransportRequest { return { kind, sessionId: operation.sessionId, operationId: operation.operationId, cwd: ctx.cwd, mode: ctx.mode, sessionFile: ctx.sessionManager.getSessionFile(), markdown: latestPlan ?? "", revision: latestRevision, pathLabel: operation.store?.currentPath ?? "current.md", signal, ...options, }; } async function reviewPlan( ctx: PlanContext, allowConfirm: boolean, handoffDelivery: PlanHandoffDelivery, operation: PlanOperationIdentity, signal?: AbortSignal, onUserAttention?: UserAttentionHandler, autoGrant?: PlanAutoGrantSnapshot, ): Promise { signal = signal && ctx.signal ? AbortSignal.any([signal, ctx.signal]) : signal ?? ctx.signal ?? new AbortController().signal; if (autoGrant) { const store = await ensureStore(ctx); if (!bindPlanOperation(ctx, operation, store)) return { approved: false, exited: false }; return approveAndImplement(ctx, store, { backend: "standalone", context: "current" }, handoffDelivery, operation, signal, autoGrant); } if (signal.aborted) return { approved: false, exited: false }; if (!ctx.hasUI) { if (isCurrentPlanOperation(ctx, operation, false)) { ctx.ui.notify("Plan review requires an interactive UI.", "warning"); } return { approved: false, exited: false }; } const store = await ensureStore(ctx); if (!bindPlanOperation(ctx, operation, store)) return { approved: false, exited: false }; if (mode !== "plan") { await enterPlanMode(ctx, operation); if (!isCurrentPlanOperation(ctx, operation)) return { approved: false, exited: false }; } if (!allowConfirm) { onUserAttention?.({ id: `plan-review:${operation.sessionId}:${operation.operationId}`, kind: "plan-review", }, ctx); const remote = openPlanTransport(transportRequest(ctx, operation, "review", signal, { availableActions: [], decisionDocuments: [], drafts: [], })); if (remote) { const raced = await racePlanTransport( (localSignal) => editPlan(ctx, store.currentPath, operation, localSignal).then(() => undefined), remote, signal, ); if (raced.source === "remote") { if (raced.result.status === "edited") { await savePlan(ctx, raced.result.markdown, raced.result.expectedRevision, operation); } return { approved: false, exited: false }; } if (!isCurrentPlanOperation(ctx, operation)) return { approved: false, exited: false }; return { approved: false, exited: false }; } await editPlan(ctx, store.currentPath, operation, signal); if (!isCurrentPlanOperation(ctx, operation)) return { approved: false, exited: false }; return { approved: false, exited: false }; } let attentionIndex = 0; while (isCurrentPlanOperation(ctx, operation)) { const workflow = await workflowConfirmation(ctx); if (!isCurrentPlanOperation(ctx, operation)) return { approved: false, exited: false }; let drafts: { revision: number; archivedAt: string; checksum: string }[]; try { drafts = (await store.listDrafts()).map((entry) => ({ revision: entry.revision, archivedAt: entry.archivedAt, checksum: entry.checksum, })); } catch { drafts = []; } if (!isCurrentPlanOperation(ctx, operation)) return { approved: false, exited: false }; onUserAttention?.({ id: `plan-confirm:${operation.sessionId}:${operation.operationId}:${++attentionIndex}`, kind: "plan-confirm", }, ctx); const decisionDocuments = await detectDecisionDocuments(latestPlan ?? "", ctx.cwd).catch(() => []); if (!isCurrentPlanOperation(ctx, operation)) return { approved: false, exited: false }; const remote = openPlanTransport(transportRequest(ctx, operation, "confirm", signal, { availableActions: [ "execute", "modify", "refine", ...(drafts.length > 0 ? ["rollback" as const] : []), "continue", "exit-plan", ], defaultExecution: latestExecution, workflow, modelTransition: describePlanModelTransition(ctx), decisionDocuments, drafts, })); let decision: Awaited>; if (remote) { const raced = await racePlanTransport( (localSignal) => openPlanConfirmation(ctx, { markdown: latestPlan ?? "", pathLabel: store.currentPath, canCompactContext: true, contextPercent: ctx.getContextUsage?.()?.percent ?? undefined, defaultExecution: latestExecution, workflow, modelTransition: describePlanModelTransition(ctx), decisionDocuments, drafts, signal: localSignal, }), remote, signal, ); if (raced.source === "remote") { if (raced.result.status === "edited") { await savePlan(ctx, raced.result.markdown, raced.result.expectedRevision, operation); continue; } if (raced.result.status === "cancelled") return { approved: false, exited: false }; decision = raced.result.decision; } else { decision = raced.value; } } else { decision = await openPlanConfirmation(ctx, { markdown: latestPlan ?? "", pathLabel: store.currentPath, canCompactContext: true, contextPercent: ctx.getContextUsage?.()?.percent ?? undefined, defaultExecution: latestExecution, workflow, modelTransition: describePlanModelTransition(ctx), decisionDocuments, drafts, signal, }); } if (!isCurrentPlanOperation(ctx, operation)) return { approved: false, exited: false }; const action = decision.action; if (action === "modify") { refineSession = undefined; refineSessionRevision = -1; refineLatestOutput = undefined; refineLatestRole = undefined; refineLatestAppliesAs = undefined; refineLatestRoleLabel = undefined; await editPlan(ctx, store.currentPath, operation, signal); if (!isCurrentPlanOperation(ctx, operation)) return { approved: false, exited: false }; continue; } if (action === "refine") { if (!extensionApi) { ctx.ui.notify("Review & Refine is unavailable: extension API is not initialized.", "warning"); continue; } if (refineSessionRevision !== latestRevision) { refineSession = undefined; refineSessionRevision = latestRevision; refineLatestOutput = undefined; refineLatestRole = undefined; refineLatestAppliesAs = undefined; refineLatestRoleLabel = undefined; } const initialRole: RefineRole = refineSession?.currentRole ?? "reviewer"; const reviewedRevision = latestRevision; const outcome = await openRefinePanel(extensionApi, ctx, { markdown: latestPlan ?? "", revision: reviewedRevision, initialRole, session: refineSession, signal, async onOutput(turn) { await store.saveReviewArtifact({ revision: reviewedRevision, role: turn.role, markdown: turn.output, createdAt: turn.createdAt, }); if (!isCurrentPlanOperation(ctx, operation)) return; latestArtifactAvailable = true; onPlanModeChanged?.(ctx); }, }); if (!isCurrentPlanOperation(ctx, operation)) return { approved: false, exited: false }; refineSession = outcome.session; refineSessionRevision = latestRevision; refineLatestOutput = outcome.latestOutput; refineLatestRole = outcome.latestRole; refineLatestAppliesAs = outcome.latestAppliesAs; refineLatestRoleLabel = outcome.latestRole ? REFINE_ROLES[outcome.latestRole].label : undefined; if (outcome.action === "apply") { const output = refineSessionRevision === latestRevision ? refineLatestOutput : undefined; if (!output) { ctx.ui.notify("No refine result is available to apply; run review/refine first.", "warning"); continue; } if (refineLatestAppliesAs === "draft") { // Optimizer-style roles return a full draft: write it directly as the new current.md. let saved: LoadedPlan | undefined; try { saved = await savePlan(ctx, output, latestRevision, operation); } catch (error) { ctx.ui.notify(`Failed to save refined plan: ${error instanceof Error ? error.message : String(error)}`, "warning"); } if (!isCurrentPlanOperation(ctx, operation)) return { approved: false, exited: false }; if (saved) { // Only a committed PlanStore write may report the business terminal. try { await outcome.finalize?.({ action: "apply", turnId: refineSession.turns.at(-1)?.turnId, terminal: "applied", }); } catch (error) { console.error(`[maestro] Failed to finalize applied refine: ${error instanceof Error ? error.message : String(error)}`); } refineLatestOutput = undefined; refineLatestRole = undefined; refineLatestAppliesAs = undefined; refineLatestRoleLabel = undefined; refineSession = undefined; refineSessionRevision = -1; ctx.ui.notify("Refine result applied to the Plan draft.", "info"); } else { await outcome.finalize?.({ action: "apply", terminal: "failed", }); } continue; } // Reviewer/decomposer/brainstormer output continues the Plan discussion. const feedback = buildRefineFeedbackMessage(output, refineLatestRoleLabel); refineLatestOutput = undefined; refineLatestRole = undefined; refineLatestAppliesAs = undefined; refineLatestRoleLabel = undefined; const discussionResult = deliverPlanDiscussion(ctx, handoffDelivery, feedback); await outcome.finalize?.({ action: "cancel", terminal: "cancelled" }); return discussionResult; } if (outcome.action === "discard" || outcome.action === "cancel") { refineLatestOutput = undefined; refineLatestRole = undefined; refineLatestAppliesAs = undefined; refineLatestRoleLabel = undefined; // The mirror must learn the session ended even when the user walks away. await outcome.finalize?.({ action: outcome.action, terminal: outcome.action === "discard" ? "discarded" : "cancelled", }); if (outcome.action === "discard") { ctx.ui.notify("Refine result discarded; returning to the Plan preview.", "info"); } // An aborted signal is not a user decision; re-opening the confirm // overlay in a dead turn would strand the user on it. if (signal.aborted) return { approved: false, exited: false }; } continue; } if (action === "rollback") { const entries = await store.listDrafts(); if (!isCurrentPlanOperation(ctx, operation)) return { approved: false, exited: false }; if (entries.length === 0) { ctx.ui.notify("No archived drafts are available to roll back to.", "warning"); continue; } const rollback = await renderRollbackOverlay(ctx, { drafts: entries, readDraft: (path) => store.readDraft(path), signal, }); if (!isCurrentPlanOperation(ctx, operation)) return { approved: false, exited: false }; if (rollback.action === "restore" && rollback.selected) { try { const restored = await store.restoreDraft(rollback.selected.revision, latestRevision); if (!isCurrentPlanOperation(ctx, operation)) return { approved: false, exited: false }; applyLoadedPlan(restored); updatePlanAutoTaskContext(ctx, { markdown: restored.markdown, revision: latestRevision }); refineSession = undefined; refineSessionRevision = -1; refineLatestOutput = undefined; refineLatestRole = undefined; refineLatestAppliesAs = undefined; refineLatestRoleLabel = undefined; syncModeStatus(ctx); ctx.ui.notify(`Restored Plan draft revision ${rollback.selected.revision} as r${restored.manifest.revision}.`, "info"); } catch (error) { ctx.ui.notify(`Rollback failed: ${errorMessage(error)}`, "warning"); } } continue; } if (action === "continue") { const discussion = decision.discussion !== undefined ? decision.discussion : await ctx.ui.input( "Continue discussing the Plan", "Enter feedback or a question", { signal }, ); if (!isCurrentPlanOperation(ctx, operation)) return { approved: false, exited: false }; if (!discussion?.trim()) continue; return deliverPlanDiscussion(ctx, handoffDelivery, discussion.trim()); } if (action === "close") { abortPlanTurn(ctx); return { approved: false, exited: false }; } if (action === "exit-plan") { await exitModeAndRestore(ctx, operation); return { approved: false, exited: true }; } if (action !== "execute") { abortPlanTurn(ctx); ctx.ui.notify("Plan confirmation closed without an explicit Execute decision.", "warning"); return { approved: false, exited: false }; } const executionChoice = decision.execution ?? { backend: "standalone", context: "current" }; return approveAndImplement(ctx, store, executionChoice, handoffDelivery, operation, signal); } return { approved: false, exited: false }; } /** Both human UI decisions and host preauthorization use this one approval transaction. */ async function approveAndImplement( ctx: PlanContext, store: PlanStore, executionChoice: PlanExecutionChoice, handoffDelivery: PlanHandoffDelivery, operation: PlanOperationIdentity, signal: AbortSignal, autoGrant?: PlanAutoGrantSnapshot, ): Promise { const markdown = latestPlan ?? ""; const revision = latestRevision; const consumptionGrant = autoGrant ?? getPlanAutoSnapshot(ctx); const fresh = () => !signal.aborted && isCurrentPlanOperation(ctx, operation) && (!autoGrant || isCurrentPlanAutoGrant(autoGrant, ctx)); const assertCurrent = () => { if (!fresh() || (autoGrant && !getPlanAutoSnapshot(ctx)?.confirmPending)) { throw new Error("Plan approval cancelled, superseded, or preauthorization revoked/consumed"); } }; let audit: PlanApprovalAudit = { source: autoGrant ? "human-preauthorization" : "human-confirmation", ...(autoGrant ? { generation: autoGrant.generation } : {}), revision, checksum: createHash("sha256").update(markdown).digest("hex"), result: "requested", }; let approved: LoadedPlan; try { assertCurrent(); if (!markdown.trim()) throw new Error("Cannot approve an empty Plan"); if (autoGrant) appendApprovalAudit(ctx, operation, audit); approved = await store.approve(markdown, revision, { execution: executionChoice, assertCurrent, onCommitted(committed) { // Consume after manifest rename, not after cleanup. This also fences a // duplicate confirm captured while cleanup is still in flight. if (consumptionGrant) markPlanAutoConfirmed(ctx, consumptionGrant, { markdown: committed.markdown, revision: committed.manifest.revision, handoffKey: committed.manifest.handoffKey, }); }, }); } catch (error) { audit = { ...audit, result: "not-approved", reason: errorMessage(error) }; if (isCurrentPlanOperation(ctx, operation)) { const loaded = await store.load(); if (isCurrentPlanOperation(ctx, operation)) { applyLoadedPlan(loaded); updatePlanAutoTaskContext(ctx, { markdown: loaded.markdown, revision: latestRevision }); const draftNote = error instanceof PlanApprovalError && error.draftPersisted ? ` Your draft is intact at revision ${error.revision}; retry the approval.` : ""; ctx.ui.notify(`Plan approval failed: ${errorMessage(error)}${draftNote}`, "warning"); } } try { appendApprovalAudit(ctx, operation, audit); } catch { /* tool result retains the outcome */ } return { approved: false, exited: false, approvalAudit: audit }; } audit = { ...audit, revision: approved.manifest.revision, checksum: approved.manifest.approvedChecksum!, handoffKey: approved.manifest.handoffKey, result: "committed" }; if (isCurrentPlanOperation(ctx, operation)) applyLoadedPlan(approved); const stopped = (reason: string): PlanReviewOutcome => { const stoppedAudit: PlanApprovalAudit = { ...audit, result: "committed-handoff-stopped", reason }; try { appendApprovalAudit(ctx, operation, stoppedAudit); } catch { /* tool result retains the outcome */ } return { approved: true, exited: mode === "act", executionChoice, executionMode: executionChoice.context, committedPlan: approved, approvalAudit: stoppedAudit }; }; if (!fresh()) return stopped("Approval committed; execution was not started because the operation, signal, or preauthorization changed."); try { appendApprovalAudit(ctx, operation, audit); const executionMessage = await startImplementation(ctx, store, approved, executionChoice, handoffDelivery, operation, fresh, autoGrant !== undefined); if (!fresh()) return stopped("Approval committed; execution was not started because the operation, signal, or preauthorization changed."); const compactDeferred = pendingPlanCompactHandoff?.operation === operation; return { approved: true, exited: true, executionMode: executionChoice.context, executionChoice, executionMessage, compactDeferred, committedPlan: approved, ...(autoGrant ? { canHandoff: fresh } : {}), approvalAudit: { ...audit, result: compactDeferred || handoffDelivery === "tool-result" ? "handoff-ready" : "handed-off" } }; } catch (error) { return stopped(`Approval committed; execution was not started: ${errorMessage(error)}`); } } function appendApprovalAudit(ctx: PlanContext, operation: PlanOperationIdentity, audit: PlanApprovalAudit): void { // Never append an old session's outcome to a newly selected chat. The tool // result carries immutable approval identity even when the operation is stale. if (planLifecycleGeneration !== operation.lifecycleGeneration || !planOperationMatchesContext(ctx, operation)) return; extensionApi?.appendEntry?.(PLAN_AUTO_AUDIT_ENTRY, { action: "approval", ...audit, sessionId: operation.sessionId, storeKey: operation.storeKey, replayable: false, }); } function buildRefineFeedbackMessage(output: string, roleLabel?: string): string { return [ "## Refine Feedback (Plan)", `The following is the Review & Refine output${roleLabel ? ` from ${roleLabel}` : ""} for the current Plan draft. Revise the Plan with plan-update to incorporate the valid findings, then present it again with plan-confirm. If you disagree with a point, explain why in the discussion.`, "", output, ].join("\n"); } function deliverPlanDiscussion( ctx: PlanContext, handoffDelivery: PlanHandoffDelivery, discussion: string, ): PlanReviewOutcome { if (handoffDelivery === "tool-result") { return { approved: false, exited: false, discussionMessage: discussion }; } queuePlanDiscussion(ctx, discussion); return { approved: false, exited: false }; } function queuePlanDiscussion(ctx: PlanContext, discussion: string): void { const busy = ctx.isIdle?.() === false; extensionApi?.sendUserMessage( discussion, busy ? { deliverAs: "followUp" } : undefined, ); if (busy) abortPlanTurn(ctx); } function buildPlanExitMessage(): string { const path = currentStore?.currentPath ?? "the persisted Plan draft"; return [ "## Exited Plan Mode", "The user intentionally exited Plan mode without approving the draft.", `Act mode is now active, and the draft remains preserved at ${path}.`, "This is not an approval failure. Do not call Plan-only tools unless the user explicitly re-enters Plan mode.", ].join("\n"); } function buildPlanExitReminder(): string { return `\n${buildPlanExitMessage()}\n`; } function abortPlanTurn(ctx: PlanContext): void { try { ctx.abort?.(); } catch { // 关闭确认页不应因宿主中断失败而阻塞。 } } async function editPlan( ctx: PlanContext, pathLabel: string, operation: PlanOperationIdentity, signal?: AbortSignal, ): Promise { await openPlanEditor(ctx, { markdown: latestPlan ?? "", revision: latestRevision, allowConfirm: false, pathLabel, signal, async onSave(markdown, expectedRevision) { if (!isCurrentPlanOperation(ctx, operation)) throw new Error("Plan operation was superseded"); const saved = await savePlan(ctx, markdown, expectedRevision, operation); if (!isCurrentPlanOperation(ctx, operation) || !saved) throw new Error("Plan operation was superseded"); return saved.manifest.revision; }, async onConfirm() {}, }); if (!isCurrentPlanOperation(ctx, operation)) return; } export function consumePlanCleanContextCompaction(): PlanCleanContextCompactionRequest | undefined { const request = pendingCleanContextCompaction; if (!request || !isCurrentPlanHandoff(request)) { pendingCleanContextCompaction = undefined; return undefined; } pendingCleanContextCompaction = undefined; return request; } function beginPlanHandoff(): PlanHandoffRequestIdentity { const request = { lifecycleGeneration: planLifecycleGeneration, requestId: ++nextPlanHandoffRequestId, }; activePlanHandoffRequest = request; return request; } function isCurrentPlanHandoff(request: PlanHandoffRequestIdentity): boolean { return request.lifecycleGeneration === planLifecycleGeneration && request.requestId === activePlanHandoffRequest?.requestId && request.lifecycleGeneration === activePlanHandoffRequest.lifecycleGeneration; } function finishPlanHandoff(request: PlanHandoffRequestIdentity): boolean { if (!isCurrentPlanHandoff(request)) return false; activePlanHandoffRequest = undefined; if (pendingPlanCompactHandoff?.request.requestId === request.requestId && pendingPlanCompactHandoff.request.lifecycleGeneration === request.lifecycleGeneration) { pendingPlanCompactHandoff = undefined; } if (activePlanCompactHandoff?.request.requestId === request.requestId && activePlanCompactHandoff.request.lifecycleGeneration === request.lifecycleGeneration) { activePlanCompactHandoff = undefined; } if (pendingCleanContextCompaction?.requestId === request.requestId && pendingCleanContextCompaction.lifecycleGeneration === request.lifecycleGeneration) { pendingCleanContextCompaction = undefined; } return true; } export function applyPlanContextToCompaction( preparation: { messagesToSummarize: AgentMessage[]; turnPrefixMessages: AgentMessage[]; firstKeptEntryId: string; previousSummary?: string; }, branchMessages: AgentMessage[], ): boolean { const projected = projectPlanContextForCompaction(branchMessages); if (!projected) return false; preparation.messagesToSummarize = projected.messages; preparation.turnPrefixMessages = []; preparation.firstKeptEntryId = projected.firstKeptEntryId; preparation.previousSummary = undefined; return true; } function activatePlanContextReplacement(summary: string): void { const text = [ "The conversation history before this point was intentionally reset after explicit user approval.", "", "", summary, "", ].join("\n"); planContextReplacement = { lifecycleGeneration: planLifecycleGeneration, replacement: { role: "user", content: [{ type: "text", text }], timestamp: Date.now(), } as AgentMessage, }; } function canReplaceContextAfterCompactionFailure(error: unknown): boolean { const message = errorMessage(error); return message.includes("Nothing to compact") || message.includes("Already compacted"); } async function startImplementation( ctx: PlanContext, store: PlanStore, approved: LoadedPlan, executionChoice: PlanExecutionChoice, handoffDelivery: PlanHandoffDelivery, operation: PlanOperationIdentity, canHandoff: () => boolean = () => isCurrentPlanOperation(ctx, operation), preauthorized = false, ): Promise { if (!canHandoff()) return undefined; const markdown = approved.markdown; const planPath = approved.manifest.approvedPath ? join(approved.plansDir, approved.manifest.approvedPath) : approved.currentPath; const executionMode = executionChoice.context; const restored = await restorePlanActModel(ctx); if (!canHandoff()) return undefined; if (preauthorized && !restored) throw new Error("Act model restoration failed"); exitPlanMode(ctx, true); latestPlan = markdown; latestStatus = "approved"; ctx.ui.notify("Plan approved · Act mode active", "info"); const refineFeedback = refineSessionRevision === approved.manifest.revision && refineLatestAppliesAs === "feedback" ? refineLatestOutput : undefined; const executionContract = buildPlanExecutionContract( planPath, latestHandoffKey, refineFeedback, refineLatestRoleLabel, approved.manifest.sourceDocuments?.[0], preauthorized, ); let executionMessage = executionContract; let workflowDelivery: { handoffKey: string; binding: PlanWorkflowBinding } | undefined; if (executionChoice.backend === "workflow") { try { const publication = await publishWorkflowPlan(ctx, approved, executionChoice); if (!canHandoff()) return undefined; if (publication.binding.status !== "bound") { throw new Error("Workflow publisher returned a non-bound result"); } const pendingDelivery = withPendingWorkflowDelivery(publication.binding); const bound = await store.updateWorkflowBinding(publication.binding.handoffKey, pendingDelivery); if (!canHandoff()) return undefined; applyLoadedPlan(bound); workflowDelivery = { handoffKey: publication.binding.handoffKey, binding: bound.manifest.workflowBinding!, }; executionMessage = `${publication.executionMessage}\n\n${executionContract}`; ctx.ui.notify( `Approved Plan bound to Workflow Session ${publication.binding.workflowSessionId}`, "info", ); } catch (error) { if (!canHandoff()) return undefined; const message = errorMessage(error); const handoffKey = approved.manifest.handoffKey; const checksum = approved.manifest.approvedChecksum; if (handoffKey && checksum) { const failed: PlanWorkflowBinding = { status: "failed", handoffKey, sourceChecksum: checksum, error: message.slice(0, 2000), updatedAt: new Date().toISOString(), }; try { const failedPlan = await store.updateWorkflowBinding(handoffKey, failed); if (!canHandoff()) return undefined; applyLoadedPlan(failedPlan); } catch (bindingError) { if (!canHandoff()) return undefined; ctx.ui.notify(`Workflow binding state could not be persisted: ${errorMessage(bindingError)}`, "error"); } } if (!canHandoff()) return undefined; const failureMessage = `Plan approved, but Workflow binding failed; execution was not started: ${message}`; ctx.ui.notify(failureMessage, "error"); return failureMessage; } } if (!canHandoff()) return undefined; const delivered = await deliverImplementation( ctx, planPath, markdown, executionMode, executionMessage, handoffDelivery, operation, workflowDelivery ? () => acknowledgeWorkflowDelivery( ctx, store, workflowDelivery!.handoffKey, workflowDelivery!.binding, operation, ) : undefined, ); return isCurrentPlanOperation(ctx, operation) ? delivered : undefined; } function buildPlanExecutionContract( planPath: string, handoffKey?: string, refineFeedback?: string, refineRoleLabel?: string, sourceDocument?: string, preauthorized = false, ): string { const base = [ preauthorized ? "The user preauthorized this Plan-confirm through Plan-auto; no per-confirm click was made. Immediate implementation of this approved Plan is authorized." : "The user selected Execute and explicitly authorized immediate implementation of the approved Plan.", "Begin execution now. Do not ask the user to trigger implementation again.", "The approved Plan is already in the current context for current-context execution; after a deterministic reset, reload it from the source path below before decomposition.", `Plan source: ${planPath}`, ...(sourceDocument ? [`Decision document: ${sourceDocument} — honor its locked decisions; do not re-litigate alternatives.`] : []), "Before modifying the project:", "1. Load the knowledge/spec system (Knowledge Gate) before any project-related work:", " - Make `maestro search \"<1-3 task keywords from this Plan>\" [--type spec|knowhow] --json` the first project-related call.", " - Load every relevant governing hit with `maestro load --type --id `; search is exposure only, loading records consumption.", " - Follow the injected knowledge_context; re-search when entering a new subsystem or before an architecture decision.", "2. Reconcile the Plan with every user requirement; do not shrink or reinterpret the approved scope.", " - Distinguish user-required outcomes and explicit locked constraints from agent-selected implementation means. Prefer the smallest end-to-end path using existing authorities; risk enumeration alone does not make auxiliary infrastructure a delivery prerequisite.", " - Explain each blocking dependency with a requirement or confirmed material defect. If a simpler equivalent implementation is supported by evidence, reconcile it with the approved Plan; obtain renewed approval for scope, explicit locked-decision, or safety-boundary changes. Never silently drop acceptance conditions or bypass existing gates.", " - Carry cumulative failed-attempt counts by shared interface/state machine and delivery blocker across workers, task names, approaches, and resets. After three unsuccessful attempts, stop the correction chain and report the effect on the requested outcome plus a simpler option or need for redesign/user guidance.", "3. Decompose the Plan into an ordered Todo dependency graph before implementation.", ...(handoffKey ? [ ` - For complex approved work, call plan-decompose with planHandoffKey: "${handoffKey}"; follow the returned decomposition prompt in the current main flow, then create the Todo batch it specifies.`, ` - For simple work, create the Todo directly with planHandoffKey: "${handoffKey}".`, ] : [" - For complex approved work, use plan-decompose before creating Todos."]), "4. Goal creation is optional. Do not create a Goal solely because the Plan was approved. Create one only when this work genuinely needs cross-turn persistence, a user-requested budget, or independent completion verification and no active Workflow Session already owns execution.", " - When Todos are used, attach a Goal only to a key Todo with verifiable acceptance criteria; never create one for every Todo. A Todo without a Goal completes through its own acceptance criteria.", "5. Prefer the teammate tool to delegate independent Todo work; use direct execution only when delegation would not help.", "6. Execute the Todo sequence; activating a Todo switches to its quality-gate Goal only when that Todo explicitly has one, and only that Todo waits for Goal verification.", "7. After implementation and verification, assess the task execution for reusable knowledge before declaring completion:", " - Stage only a non-obvious pitfall, failure lesson, trade-off, or newly established prescriptive constraint through the active Run/Session knowledge workflow; do not duplicate facts already auto-staged from accepted decisions or locked constraints.", " - If nothing meets the knowledge quality bar, explicitly report zero knowledge candidates. Never fabricate a candidate merely to complete the process.", ]; if (refineFeedback) { base.push( `8. Review & Refine feedback${refineRoleLabel ? ` from ${refineRoleLabel}` : ""}: incorporate valid findings during execution.`, "", refineFeedback, "", ); } return base.join("\n"); } async function deliverImplementation( ctx: PlanContext, planPath: string, markdown: string, executionMode: PlanExecutionChoice["context"], executionMessage: string, handoffDelivery: PlanHandoffDelivery, operation: PlanOperationIdentity, onDelivered?: () => Promise, ): Promise { if (!isCurrentPlanOperation(ctx, operation)) return undefined; if (executionMode === "compact") { const handoff: PlanCompactHandoff = { request: beginPlanHandoff(), operation, planPath, markdown, executionMessage, ...(onDelivered ? { onDelivered } : {}), }; if (handoffDelivery === "tool-result") { pendingPlanCompactHandoff = handoff; ctx.ui.notify("Plan compaction queued for the end of the current turn…", "info"); return undefined; } startPlanCompaction(ctx, handoff); return undefined; } if (handoffDelivery === "tool-result") { await onDelivered?.(); return isCurrentPlanOperation(ctx, operation) ? executionMessage : undefined; } sendImplementationMessage(ctx, executionMessage); await onDelivered?.(); if (!isCurrentPlanOperation(ctx, operation)) return undefined; } export function hasPendingPlanCompactHandoff(ctx: PlanContext): boolean { const handoff = pendingPlanCompactHandoff ?? activePlanCompactHandoff; return Boolean(handoff && planOperationMatchesContext(ctx, handoff.operation) && isCurrentPlanOperation(ctx, handoff.operation)); } export function onAgentSettledPlan(ctx: PlanContext): void { // A compaction handoff already owns the current host boundary. A second // settlement must not start a duplicate compaction or execution message. if (activePlanCompactHandoff && planOperationMatchesContext(ctx, activePlanCompactHandoff.operation)) { if (isCurrentPlanOperation(ctx, activePlanCompactHandoff.operation)) return; const stale = activePlanCompactHandoff; activePlanCompactHandoff = undefined; if (isCurrentPlanHandoff(stale.request)) finishPlanHandoff(stale.request); } const handoff = pendingPlanCompactHandoff; if (!handoff || !planOperationMatchesContext(ctx, handoff.operation)) return; if (!isCurrentPlanOperation(ctx, handoff.operation)) { pendingPlanCompactHandoff = undefined; finishPlanHandoff(handoff.request); return; } if (handoff.waitingForCompactionSettlement) { // The host's next settled boundary is the first point at which a denied // compaction can safely fall back to the current context. Keep waiting if // an owner/tombstone still proves that compaction is unresolved. if (compactionArbiter?.currentOwner() || compactionArbiter?.timeoutTombstone()) return; handoff.waitingForCompactionSettlement = false; pendingPlanCompactHandoff = undefined; deliverPlanHandoff(ctx, handoff); return; } startPlanCompaction(ctx, handoff); } /** Queue an approved Plan message only at a host boundary known to be safe. */ function deliverPlanHandoff(ctx: PlanContext, handoff: PlanCompactHandoff): boolean { if (!isCurrentPlanOperation(ctx, handoff.operation) || !isCurrentPlanHandoff(handoff.request)) return false; try { sendImplementationMessage(ctx, handoff.executionMessage); } catch (error) { // A synchronous rejection is not an acceptance signal. Preserve the // request so a later settled boundary can retry it. handoff.waitingForCompactionSettlement = true; if (activePlanCompactHandoff === handoff) activePlanCompactHandoff = undefined; pendingPlanCompactHandoff = handoff; ctx.ui.notify(`Plan execution handoff could not be queued: ${errorMessage(error)}`, "error"); return false; } if (!finishPlanHandoff(handoff.request)) return false; void handoff.onDelivered?.(); return true; } function startPlanCompaction(ctx: PlanContext, handoff: PlanCompactHandoff): void { const { request, operation, planPath, markdown, executionMessage } = handoff; if (schedulePlanNewContext) { if (pendingPlanCompactHandoff === handoff) pendingPlanCompactHandoff = undefined; activePlanCompactHandoff = handoff; try { const receipt = schedulePlanNewContext(ctx, { executionMessage, plan: { status: "approved", revision: latestRevision, path: planPath, ...(latestHandoffKey ? { handoffKey: latestHandoffKey } : {}), ...(latestApprovedChecksum ? { checksum: latestApprovedChecksum } : {}), markdown, }, continueAfterReset: () => deliverPlanHandoff(ctx, handoff), onCancelled(reason) { if (activePlanCompactHandoff === handoff) activePlanCompactHandoff = undefined; if (!isCurrentPlanOperation(ctx, operation) || !isCurrentPlanHandoff(request)) return; finishPlanHandoff(request); ctx.ui.notify(`Plan New Context handoff was cancelled; execution was not started: ${reason}`, "warning"); }, }); ctx.ui.notify( receipt.coalesced ? `Plan execution joined New Context request ${receipt.requestId}; the Plan conversation will be checkpointed before execution.` : `Plan New Context request ${receipt.requestId} scheduled; the Plan conversation will be checkpointed before execution.`, "info", ); } catch (error) { if (activePlanCompactHandoff === handoff) activePlanCompactHandoff = undefined; if (isCurrentPlanOperation(ctx, operation) && isCurrentPlanHandoff(request)) { ctx.ui.notify(`Plan New Context could not be scheduled; executing with the current context: ${errorMessage(error)}`, "warning"); deliverPlanHandoff(ctx, handoff); } } return; } const lease = compactionArbiter?.request("plan-handoff", { owner: "plan-handoff", reason: "preserve-approved-plan", }); if (compactionArbiter && !lease) { // Never consume the handoff while another compaction (or its timeout // tombstone) may still own the host controller. The next authoritative // settlement retries the current-context delivery exactly once. const firstDeferral = !handoff.waitingForCompactionSettlement; handoff.waitingForCompactionSettlement = true; pendingPlanCompactHandoff = handoff; if (firstDeferral) { ctx.ui.notify("Compaction is already in progress; Plan execution is deferred until it settles.", "warning"); } return; } // Request ownership is now established; only this transition may consume a // pending handoff and expose the active compaction to settlement guards. if (pendingPlanCompactHandoff === handoff) pendingPlanCompactHandoff = undefined; activePlanCompactHandoff = handoff; let delivered = false; let watchdog: ReturnType | undefined; const deliver = (releaseLease = true) => { if (delivered) return false; delivered = true; if (watchdog) clearTimeout(watchdog); if (releaseLease) lease?.release(); if (!isCurrentPlanOperation(ctx, operation) || !isCurrentPlanHandoff(request)) { if (activePlanCompactHandoff === handoff) activePlanCompactHandoff = undefined; if (isCurrentPlanHandoff(request)) finishPlanHandoff(request); return false; } const queued = deliverPlanHandoff(ctx, handoff); if (!queued) delivered = false; return queued; }; const compactionInstructions = [ "Treat the following approved Plan as the authoritative execution contract.", `Preserve its source path, locked boundaries, risks, acceptance checks, and current execution position: ${planPath}`, "", markdown, ].join("\n"); ctx.ui.notify("Compacting context with the approved Plan preserved…", "info"); watchdog = setTimeout(() => { if (isCurrentPlanOperation(ctx, operation) && isCurrentPlanHandoff(request)) { // Wall-clock expiry does not prove that the host compaction stopped. Keep // the handoff and wait for onComplete/onError, both invoked after the // host clears its compaction controller. ctx.ui.notify("Plan compaction did not finish in time; waiting for the host to settle before execution.", "warning"); } }, planCompactionHandoffTimeoutMs); watchdog.unref?.(); try { ctx.compact({ customInstructions: lease?.tagInstructions(compactionInstructions) ?? compactionInstructions, onComplete() { deliver(); }, onError(error) { if (isCurrentPlanOperation(ctx, operation) && isCurrentPlanHandoff(request)) { ctx.ui.notify(`Compaction failed; executing with the current context: ${error.message}`, "warning"); } deliver(); }, }); } catch (error) { if (isCurrentPlanOperation(ctx, operation) && isCurrentPlanHandoff(request)) { ctx.ui.notify(`Compaction failed; executing with the current context: ${errorMessage(error)}`, "warning"); } deliver(); } } function sendImplementationMessage(ctx: PlanContext, message: string): void { if (!extensionApi) throw new Error("Plan extension API is not initialized"); const opts = ctx.isIdle?.() === false ? { deliverAs: "followUp" as const } : undefined; extensionApi.sendUserMessage(message, opts); } function currentDetails(action: PlanToolDetails["action"]): PlanToolDetails { return { action, mode, revision: latestRevision, path: currentStore?.currentPath ?? "", sessionId: currentStore?.sessionId ?? "", status: latestStatus, handoffStatus: getPlanHandoffStatus(), ...(latestHandoffKey ? { handoffKey: latestHandoffKey } : {}), ...(latestExecution ? { execution: latestExecution } : {}), ...(latestWorkflowBinding ? { workflowBinding: latestWorkflowBinding } : {}), }; } /** * Detached Plan state for compaction metadata and prompts, mirroring * getTodoCompactionSnapshot. * * Only one-shot notes reach the model via before_agent_start.message, so after a * compaction the model's only trace of an approved Plan is whatever prose survived the * summary — including the handoff key, which nothing injects and which the todo tool * needs verbatim. The plan body is deliberately left out: it lives at `path` and is * reloaded from the store, so this carries identity and reload metadata, the same way * the checkpoint treats skills. */ export function getPlanCompactionSnapshot(): PlanCompactionSnapshot { return { mode, status: latestStatus, revision: latestRevision, handoffStatus: getPlanHandoffStatus(), ...(latestHandoffKey ? { handoffKey: latestHandoffKey } : {}), ...(currentStore ? { path: latestStatus === "approved" && latestApprovedPath ? join(currentStore.plansDir, latestApprovedPath) : currentStore.currentPath, } : {}), ...(latestStatus === "approved" && latestApprovedChecksum ? { checksum: latestApprovedChecksum } : {}), }; } /** Return the current Plan material for a deterministic context-reset handoff. */ export function getPlanNewContextPayload(): PlanNewContextPayload | undefined { if (!latestPlan || latestStatus === "empty" || !currentStore) return undefined; const path = latestStatus === "approved" && latestApprovedPath ? join(currentStore.plansDir, latestApprovedPath) : currentStore.currentPath; return { status: latestStatus, revision: latestRevision, path, ...(latestHandoffKey ? { handoffKey: latestHandoffKey } : {}), ...(latestApprovedChecksum ? { checksum: latestApprovedChecksum } : {}), markdown: latestPlan, }; } export function getPlanHandoffStatus(): PlanHandoffStatus { if (latestStatus !== "approved") return "none"; if (!latestHandoffKey || !hasExecutableTodoForHandoff(latestHandoffKey)) return "todo-required"; return "ready"; } function result( text: string, details: PlanToolDetails, isError = false, terminate = false, ): AgentToolResult { return { content: [{ type: "text", text }], details, ...(isError ? { isError: true } : {}), ...(terminate ? { terminate: true } : {}), } as unknown as AgentToolResult; } function requirePlanMode(action: PlanToolDetails["action"]): AgentToolResult | undefined { if (mode === "plan") return; return result(`plan-${action} requires Plan mode. Call plan-enter first.`, { ...currentDetails(action), error: "E_PLAN_MODE_REQUIRED", }, true); } function supersededResult(action: PlanToolDetails["action"]): AgentToolResult { return result("Plan operation was superseded by a newer Plan operation.", { ...currentDetails(action), error: "E_PLAN_OPERATION_SUPERSEDED", }, true); } export function registerPlanTools( pi: ExtensionAPI, options: { onUserAttention?: UserAttentionHandler } = {}, ): void { const enterTool: ToolDefinition = { name: PLAN_ENTER_TOOL, label: "Plan Enter", description: "Enter durable Plan mode and load this chat session's current.md draft. Use only when the user explicitly requests Plan mode or a concrete unresolved decision or gated change requires user approval not already given. Internal task planning does not require this tool.", promptSnippet: "Enter Plan mode for an explicit user request for Plan mode or an unresolved approval point, not merely to organize implementation steps. Otherwise stay in Act and implement the authorized scope.", promptGuidelines: [ "For agent-initiated entry, before calling plan-enter, state the specific decision or authorization needed and why the current user request does not settle it. Task size, multiple files, investigation, tests, and implementation checklists alone are not approval points.", "Use read-only investigation in Act to resolve technical uncertainty. For one user-owned choice, ask one focused question with ask-user-question; use Plan mode when that choice changes the broader approach, not automatically for every question.", "A clear request to implement a diagnosed fix or proposed display change normally authorizes in-scope edits. Do not seek duplicate approval; preserve existing permission gates, failure stop conditions, and explicit locked constraints. Do not invent data semantics or silently narrow the requested result to avoid asking.", ], parameters: EmptyPlanParams, async execute(_id, _params, _signal, _onUpdate, ctx) { const operation = beginPlanOperation(ctx); if (mode !== "plan") await enterPlanMode(ctx, operation); if (!isCurrentPlanOperation(ctx, operation)) return supersededResult("enter"); return result(`Plan mode active. Draft: ${currentStore?.currentPath ?? ""}`, currentDetails("enter")); }, renderShell: "self", renderCall(_args, theme, ctx) { if (ctx?.isPartial === false) return new Text("", 0, 0); return toolCallLine(theme, "plan", "enter"); }, renderResult(result, opts, theme, ctx) { if (opts.isPartial) return new Text("", 0, 0); const block = result.content.find((c) => c.type === "text"); const text = block && "text" in block ? block.text : ""; const isError = ctx.isError || (result as { isError?: boolean }).isError === true; return toolResultLine(theme, { name: "plan", ok: !isError, arg: "enter", summary: resultSummary(result), expanded: opts.expanded, detail: text }); }, }; const updateTool: ToolDefinition = { name: "plan-update", label: "Plan Update", description: "Replace this chat session's current.md draft with complete Markdown. expectedRevision defaults to the currently loaded revision.", promptSnippet: "Use plan-update to persist the decision-complete, execution-ready Markdown Plan before review.", parameters: PlanUpdateParams, async execute(_id, params, _signal, _onUpdate, ctx) { const operation = beginPlanOperation(ctx); const blocked = requirePlanMode("update"); if (blocked) return blocked; const expectedRevision = params.expectedRevision ?? latestRevision; try { const saved = await savePlan(ctx, params.markdown, expectedRevision, operation); if (!isCurrentPlanOperation(ctx, operation) || !saved) return supersededResult("update"); return result(`Plan draft saved at revision ${saved.manifest.revision}.`, currentDetails("update")); } catch (error) { if (!isCurrentPlanOperation(ctx, operation, operation.store !== undefined)) return supersededResult("update"); return result(errorMessage(error), { ...currentDetails("update"), error: errorMessage(error) }, true); } }, renderShell: "self", renderCall(_args, theme, ctx) { if (ctx?.isPartial === false) return new Text("", 0, 0); return toolCallLine(theme, "plan", "update"); }, renderResult(result, opts, theme, ctx) { if (opts.isPartial) return new Text("", 0, 0); const block = result.content.find((c) => c.type === "text"); const text = block && "text" in block ? block.text : ""; const isError = ctx.isError || (result as { isError?: boolean }).isError === true; return toolResultLine(theme, { name: "plan", ok: !isError, arg: "update", summary: resultSummary(result), expanded: opts.expanded, detail: text }); }, }; const reviewTool: ToolDefinition = { name: "plan-review", label: "Plan Draft Editor", description: "Open the full-screen editable Markdown draft in an interactive UI. Save or cancel without entering Act mode. Use plan-confirm for Review & Refine (role-based AI review and refinement).", parameters: EmptyPlanParams, executionMode: "sequential", async execute(_id, _params, signal, _onUpdate, ctx) { const operation = beginPlanOperation(ctx); const blocked = requirePlanMode("review"); if (blocked) return blocked; await reviewPlan(ctx, false, "message", operation, signal, options.onUserAttention); if (!isCurrentPlanOperation(ctx, operation)) return supersededResult("review"); return result("Plan review closed; Plan mode remains active.", currentDetails("review")); }, renderShell: "self", renderCall(_args, theme, ctx) { if (ctx?.isPartial === false) return new Text("", 0, 0); return toolCallLine(theme, "plan", "review"); }, renderResult(result, opts, theme, ctx) { if (opts.isPartial) return new Text("", 0, 0); const block = result.content.find((c) => c.type === "text"); const text = block && "text" in block ? block.text : ""; const isError = ctx.isError || (result as { isError?: boolean }).isError === true; return toolResultLine(theme, { name: "plan", ok: !isError, arg: "review", summary: resultSummary(result), expanded: opts.expanded, detail: text }); }, }; const confirmTool: ToolDefinition = { name: "plan-confirm", label: "Plan Confirm", description: "Only a valid human Plan-auto preauthorization bypasses the UI, approves this exact draft, and executes standalone/current without inheriting Workflow, New Context, or decision documents. Otherwise present the Markdown Plan in an interactive UI with choices to execute, modify, discuss, run role-based Review & Refine, or exit. Current-context execution returns through the tool result; New Context execution settles the turn, saves the Plan conversation as a checkpoint, resets deterministically, then resumes automatically.", promptSnippet: "Standard presentation step after plan-update. Default is manual; only an explicit human Plan-auto grant preauthorizes this confirm and immediate standalone/current execution. The user controls approval and may run role-based Review & Refine; choosing Execute authorizes immediate implementation, with New Context execution preserving a checkpoint and resuming automatically after turn settlement.", parameters: EmptyPlanParams, executionMode: "sequential", async execute(_id, _params, signal, _onUpdate, ctx) { const operation = beginPlanOperation(ctx); const blocked = requirePlanMode("confirm"); if (blocked) return blocked; const snapshot = getPlanAutoSnapshot(ctx); const autoGrant = snapshot?.confirmPending ? snapshot : undefined; const outcome = await reviewPlan(ctx, true, "tool-result", operation, signal, options.onUserAttention, autoGrant); if (isCurrentPlanOperation(ctx, operation)) onPlanModeChanged?.(ctx); if (outcome.canHandoff && !outcome.canHandoff()) { outcome.executionMessage = undefined; outcome.approvalAudit = { ...outcome.approvalAudit!, result: "committed-handoff-stopped", reason: "Approval committed; execution was not started because preauthorization or context changed before tool-result delivery." }; } else if (outcome.approvalAudit?.result === "handoff-ready" && !outcome.compactDeferred) { outcome.approvalAudit = { ...outcome.approvalAudit, result: "handed-off" }; } if (outcome.approvalAudit) { try { appendApprovalAudit(ctx, operation, outcome.approvalAudit); } catch { /* result retains the outcome */ } } // The host audit append is synchronous, but may notify lifecycle listeners. // No await or host callback remains between this fence and result delivery. if (outcome.canHandoff && !outcome.canHandoff() && outcome.executionMessage) { outcome.executionMessage = undefined; outcome.approvalAudit = { ...outcome.approvalAudit!, result: "committed-handoff-stopped", reason: "Approval committed; execution was not started because preauthorization or context changed during audit delivery." }; try { appendApprovalAudit(ctx, operation, outcome.approvalAudit); } catch { /* result retains the outcome */ } } if (!isCurrentPlanOperation(ctx, operation) && !outcome.approvalAudit) return supersededResult("confirm"); if (outcome.compactDeferred) { try { ctx.abort?.(); } catch (error) { ctx.ui.notify(`Plan could not stop the current tool batch before compaction: ${errorMessage(error)}`, "warning"); } } const summary = outcome.approvalAudit?.result === "committed-handoff-stopped" ? `Plan approval is committed and preserved; execution was not started. ${outcome.approvalAudit.reason ?? ""}${autoGrant ? " No automatic retry or recovery handoff will run." : ""}` : outcome.approved ? outcome.executionChoice?.backend === "workflow" && latestWorkflowBinding?.status === "failed" ? "Plan approved; Workflow binding failed and execution was not started." : outcome.compactDeferred ? `Plan approved; Act mode restored (New Context, ${outcome.executionChoice?.backend ?? "standalone"}). The Plan conversation will be saved as a checkpoint after this turn settles, then execution resumes automatically.` : `Plan approved; Act mode restored (${outcome.executionMode ?? "current"} context, ${outcome.executionChoice?.backend ?? "standalone"}).` : outcome.exited ? buildPlanExitMessage() : outcome.discussionMessage ? "Plan feedback returned; Plan mode remains active." : "Plan not approved; Plan mode remains active."; const toolMessage = outcome.executionMessage ?? outcome.discussionMessage; const text = toolMessage ? `${summary}\n\n${toolMessage}` : summary; return result(text, { ...currentDetails("confirm"), ...(outcome.committedPlan ? { revision: outcome.committedPlan.manifest.revision, status: "approved" as const, sessionId: outcome.committedPlan.manifest.sessionId ?? operation.sessionId, path: outcome.committedPlan.currentPath, handoffKey: outcome.committedPlan.manifest.handoffKey, execution: outcome.committedPlan.manifest.execution, } : {}), approved: outcome.approved, ...(outcome.approvalAudit ? { approvalAudit: outcome.approvalAudit } : {}), ...(!isCurrentPlanOperation(ctx, operation) ? { error: "E_PLAN_OPERATION_SUPERSEDED" } : {}), }, !isCurrentPlanOperation(ctx, operation), outcome.compactDeferred === true); }, renderShell: "self", renderCall(_args, theme, ctx) { if (ctx?.isPartial === false) return new Text("", 0, 0); return toolCallLine(theme, "plan", "confirm"); }, renderResult(result, opts, theme, ctx) { if (opts.isPartial) return new Text("", 0, 0); const block = result.content.find((c) => c.type === "text"); const text = block && "text" in block ? block.text : ""; const isError = ctx.isError || (result as { isError?: boolean }).isError === true; return toolResultLine(theme, { name: "plan", ok: !isError, arg: "confirm", summary: resultSummary(result), expanded: opts.expanded, detail: text }); }, }; const decomposeTool: ToolDefinition = { name: PLAN_DECOMPOSE_TOOL, label: "Plan Decompose", description: "Inject the main-flow decomposition prompt for the currently approved Plan. Requires Act mode and the exact approved handoff key; creates no files, Todos, messages, or agents.", promptSnippet: "For complex approved work, call plan-decompose with the exact planHandoffKey, then let the current main flow build the complete Todo graph from its returned prompt.", parameters: PlanDecomposeParams, async execute(_id, params, _signal, _onUpdate, ctx) { const operation = beginPlanOperation(ctx); if (mode !== "act") { return result("plan-decompose requires Act mode after Plan approval.", { ...currentDetails("decompose"), error: "E_PLAN_ACT_MODE_REQUIRED", }, true); } try { const store = await ensureStore(ctx); if (!bindPlanOperation(ctx, operation, store)) return supersededResult("decompose"); const loaded = await store.loadApprovedSnapshotReadOnly(); if (!isCurrentPlanOperation(ctx, operation)) return supersededResult("decompose"); const manifest = loaded.manifest; if ( manifest.status !== "approved" || !manifest.handoffKey || !manifest.approvedPath || !manifest.approvedChecksum ) { return result("plan-decompose requires a currently approved Plan with complete approval identity.", { ...currentDetails("decompose"), error: "E_PLAN_APPROVAL_REQUIRED", }, true); } if (params.planHandoffKey !== manifest.handoffKey) { return result("plan-decompose requires the exact handoff key of the currently approved Plan.", { ...currentDetails("decompose"), error: "E_PLAN_HANDOFF_KEY_MISMATCH", }, true); } const contract = buildPlanDecomposeContract({ planHandoffKey: params.planHandoffKey, approvedPlanPath: loaded.approvedPath, approvedChecksum: manifest.approvedChecksum, }); if (!isCurrentPlanOperation(ctx, operation)) return supersededResult("decompose"); return result(contract, currentDetails("decompose")); } catch (error) { if (!isCurrentPlanOperation(ctx, operation, operation.store !== undefined)) return supersededResult("decompose"); if (errorMessage(error) === "Plan has no complete approved snapshot") { return result("plan-decompose requires a currently approved Plan with complete approval identity.", { ...currentDetails("decompose"), error: "E_PLAN_APPROVAL_REQUIRED", }, true); } return result(errorMessage(error), { ...currentDetails("decompose"), error: "E_PLAN_DECOMPOSE_INVALID", }, true); } }, renderShell: "self", renderCall(_args, theme, ctx) { if (ctx?.isPartial === false) return new Text("", 0, 0); return toolCallLine(theme, "plan", "decompose"); }, renderResult(result, opts, theme, ctx) { if (opts.isPartial) return new Text("", 0, 0); const block = result.content.find((c) => c.type === "text"); const text = block && "text" in block ? block.text : ""; const isError = ctx.isError || (result as { isError?: boolean }).isError === true; return toolResultLine(theme, { name: "plan", ok: !isError, arg: "decompose", summary: resultSummary(result), expanded: opts.expanded, detail: text }); }, }; const exitTool: ToolDefinition = { name: "plan-exit", label: "Plan Exit", description: "Exit Plan mode without deleting the persisted draft and return to Act mode.", parameters: EmptyPlanParams, async execute(_id, _params, _signal, _onUpdate, ctx) { const operation = beginPlanOperation(ctx); const blocked = requirePlanMode("exit"); if (blocked) return blocked; await exitModeAndRestore(ctx, operation); return result(buildPlanExitMessage(), currentDetails("exit")); }, renderShell: "self", renderCall(_args, theme, ctx) { if (ctx?.isPartial === false) return new Text("", 0, 0); return toolCallLine(theme, "plan", "exit"); }, renderResult(result, opts, theme, ctx) { if (opts.isPartial) return new Text("", 0, 0); const block = result.content.find((c) => c.type === "text"); const text = block && "text" in block ? block.text : ""; const isError = ctx.isError || (result as { isError?: boolean }).isError === true; return toolResultLine(theme, { name: "plan", ok: !isError, arg: "exit", summary: resultSummary(result), expanded: opts.expanded, detail: text }); }, }; const statusTool: ToolDefinition = { name: "plan-status", label: "Plan Status", description: "Return the draft path, revision, approval status, and handoff state while Plan mode is active.", parameters: EmptyPlanParams, async execute(_id, _params, _signal, _onUpdate, ctx) { beginPlanOperation(ctx); const blocked = requirePlanMode("status"); if (blocked) return blocked; const snapshot = getPlanAutoSnapshot(ctx); const details = { ...currentDetails("status"), planAuto: { enabled: !!snapshot, ...(snapshot ? { generation: snapshot.generation, confirmPending: snapshot.confirmPending } : {}), } }; return result(`${details.mode} · ${details.status} · r${details.revision} · ${details.sessionId} · ${details.path} · ${planAutoStatusLabel(ctx)}`, details); }, renderShell: "self", renderCall(_args, theme, ctx) { if (ctx?.isPartial === false) return new Text("", 0, 0); return toolCallLine(theme, "plan", "status"); }, renderResult(result, opts, theme, ctx) { if (opts.isPartial) return new Text("", 0, 0); const block = result.content.find((c) => c.type === "text"); const text = block && "text" in block ? block.text : ""; const isError = ctx.isError || (result as { isError?: boolean }).isError === true; return toolResultLine(theme, { name: "plan", ok: !isError, arg: "status", summary: resultSummary(result), expanded: opts.expanded, detail: text }); }, }; for (const tool of [enterTool, updateTool, reviewTool, confirmTool, decomposeTool, exitTool, statusTool]) { pi.registerTool(tool as ToolDefinition); } } function extractProposedPlan(text: string): string | undefined { return PROPOSED_PLAN_PATTERN.exec(text)?.[1]?.trim() || undefined; } function latestAssistantText(messages: unknown): string { if (!Array.isArray(messages)) return ""; for (let index = messages.length - 1; index >= 0; index--) { const entry = messages[index] as Record; const message = (entry?.message as Record) ?? entry; if (message?.role !== "assistant") continue; const text = contentText(message.content); if (text) return text; } return ""; } function contentText(content: unknown): string { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; return content.map((block) => { const value = block as { type?: string; text?: string }; return value.type === "text" && typeof value.text === "string" ? value.text : ""; }).filter(Boolean).join("\n"); } function buildPlanEnterNote(): string { return [ "", "## Plan Mode Just Activated", "Plan mode was activated (via user toggle or plan-enter). The tool surface is UNCHANGED for prompt-cache stability,", "and the tool-call hook blocks only the built-in file-editing tools (Write/Edit) until approval.", "Keep planning read-only by convention: research, search, and inspect freely, but do not mutate", "project files, Workflow/Todo/Goal state, or external systems while shaping the draft.", "- Plan tools: plan-update, plan-review, plan-confirm, plan-exit, plan-status.", "", "Workflow: research → plan-update (persist draft) → plan-confirm (present to user) in the same turn.", "plan-confirm defaults to a human choice (execute, modify, discuss, or exit). Only a valid human /plan-auto grant preauthorizes the next confirm and immediate standalone/current execution; plan-update and plan-review never automatically approve.", "If the user wants changes: plan-update again, then plan-confirm again.", "plan-exit leaves Plan mode without approving; the draft is preserved.", "If the user challenges an unnecessary Plan, acknowledge the mismatch. If their intent is unclear, ask one focused question with ask-user-question about abandoning planning; on an explicit request to abandon it, use plan-exit. Do not force a full draft and confirmation solely because Plan mode was entered. Exit is not approval and does not waive any other gate.", "", // Restored in full rather than condensed: 896d036f thinned this guidance on the // premise that the tool panel was "the authoritative signal" for Plan mode, and // 2e7c19b2 then removed the panel change altogether. This note is now the only // channel, and being one-time it extends the cached prefix once instead of // invalidating it per turn — so there is no cache argument for keeping it terse. "Planning quality:", "- Ground every decision in codebase evidence, not assumption.", "- Align every user requirement with a planned outcome and a verifiable acceptance check; separate user-required outcomes and explicit locked constraints from agent-selected implementation means.", "- Prefer the smallest end-to-end path using existing authorities. Justify every blocking dependency by a requirement or confirmed material defect; risks and optional diagnostics/provenance/hardening are not automatic delivery gates. Make shared authorities available before their consumers need production-path acceptance.", "- Require every Plan to include an end-of-execution knowledge outcome: after implementation and", " verification, assess the task execution for a reusable pitfall, failure lesson, trade-off, or", " prescriptive constraint. Plan candidate staging only when the quality bar is met; otherwise require", " an explicit zero-candidate result. Actual knowledge writes happen after approval in Act/Run mode.", "", "Agent boundary (Plan-mode work stays read-only — dispatch only read-only roles):", "- Never use plan-exit to bypass Plan-mode boundaries. Stay in Plan mode and reshape the work", " to the read-only planning boundary; use plan-exit only when the user wants to abandon planning.", "- Standard flow (broad or uncertain scope): dispatch `explorer` ONCE with batched parallel", " prompts; unless the cross-module escalation below applies, dispatch one `planner`, passing", " each explorer result's exact `agent://` publication ID as an immutable briefing reference", " (use a correlation ID only when latest-turn semantics are intentional), plus your distilled", " evidence, resolved requirements, constraints, and every known red line (architecture invariants,", " compatibility rules) as a mandatory first-round checklist so the plan passes review in one", " round. Never re-dispatch planning from scratch; send targeted revision instructions to the same", " planner instead.", "- Required-result barrier: if a required explorer/planner dispatch moves to background or a bounded", " wait times out, do not begin final synthesis or call plan-update / plan-confirm. Wait for automatic", " delivery or use one bounded observe wait until every required result is result-ready, then ensure", " each result's content is present in the current context (inline or loaded from its exact immutable", " publication ID) before composing or persisting the Plan.", "- Lightweight flow (small, well-understood task): skip agent exploration entirely — inspect the", " code yourself with read/search tools, then either author the draft via plan-update directly or", " make one `planner` call with that content injected. After spot-checking the result, go straight", " to ask-user-question / plan-confirm.", "- Planner budget: the planner may call at most ONE nested read-only agent (prefer a pressure", " review of its near-final draft on architecturally risky changes) and must apply the findings by", " revising its own draft — no repeated review rounds, no delegate chains.", "- Multi-planner escalation (cross-module scope only): when explorer evidence shows real module", " boundaries (>= 2 modules, interface contracts are the core risk), you may dispatch up to 3", " planners in parallel — one per module, each with maxNestingDepth: 0 (sub-planners get no nested", " agents) and a briefing carrying only that module's exact immutable explorer publication IDs", " and distilled evidence; never use a task-name URI as durable briefing. Root owns cross-module", " integration and synthesizes the final unified Plan (or designates one lead planner", " for integration). Single-module scope stays one planner.", "- Do not use implementation-capable agents in Plan mode. The root agent owns user interaction,", " evidence spot-checking, contract validation, plan-update, and plan-confirm; persist the returned", " Markdown only after checking it against the planner role contract, and return incomplete drafts", " to the same planner for targeted revision (except in the lightweight flow, where you may author", " the small draft yourself).", "- Use ask-user-question only for genuine unresolved user-owned choices. Ask one focused question", " when one choice remains; group related questions only when needed. Do not pad a batch or re-ask", " settled requirements, and do not ask questions as plain assistant text.", "- Root performs one contract spot-check covering scope, boundaries, non-goals, requirements, and", " acceptance checks. Send concrete gaps to the same planner as targeted revisions without starting", " another review chain; unresolved risks must stay visible.", "- Keep the final Markdown decision-complete and directly consumable by an execution agent. Omit", " interview logs, delegate transcripts, and generic boilerplate. Validate the planner's draft", " against the planner role contract before persisting it; return incomplete drafts for revision.", "- Approval decomposes the locked Plan into an ordered Todo graph. Goal creation is optional:", " never create one solely because the Plan was approved; use one only when cross-turn persistence,", " a user-requested budget, or independent completion verification is genuinely needed and no active", " Workflow Session already owns execution. A Todo may carry a Goal as a key quality gate, but most", " Todos should complete without one.", "", // Injected as a one-time custom message (before_agent_start.message) so the // system prompt prefix stays stable for prompt-cache reuse. "This is a one-time notification injected at the next turn boundary. Subsequent turns will not re-announce Plan mode — this reminder is the only signal you get.", "", ].join("\n"); } export function registerPlanCommand(pi: ExtensionAPI): void { pi.registerCommand("plan", { description: "Plan mode: /plan [|exit|show|approve|clear|tools]", getArgumentCompletions(prefix: string) { const options = [ { value: "exit", label: "exit", description: "Leave Plan mode and preserve draft" }, { value: "show", label: "show", description: "Open editable Plan review" }, { value: "approve", label: "approve", description: "Review, approve and implement" }, { value: "clear", label: "clear", description: "Clear current Markdown draft" }, { value: "tools", label: "tools", description: "Show active Plan tools" }, ]; const lower = prefix.trim().toLowerCase(); return lower ? options.filter((option) => option.value.startsWith(lower)) : options; }, async handler(args: string, ctx: PlanContext) { const operation = beginPlanOperation(ctx); const trimmed = args.trim(); const command = trimmed.toLowerCase(); if (command === "exit" || command === "off") { if (isPlanMode()) { await exitModeAndRestore(ctx, operation); } else if (isCurrentPlanOperation(ctx, operation, false)) { ctx.ui.notify("Act mode · draft preserved", "info"); } return; } if (command === "show") { if (!isPlanMode()) { await enterPlanMode(ctx, operation); if (!isCurrentPlanOperation(ctx, operation)) return; } await reviewPlan(ctx, false, "message", operation); if (!isCurrentPlanOperation(ctx, operation)) return; return; } if (command === "approve") { if (!isPlanMode()) { await enterPlanMode(ctx, operation); if (!isCurrentPlanOperation(ctx, operation)) return; } if (!hasPlan()) { ctx.ui.notify("No Plan draft to approve.", "warning"); return; } await reviewPlan(ctx, true, "message", operation); if (!isCurrentPlanOperation(ctx, operation)) return; onPlanModeChanged?.(ctx); return; } if (command === "clear") { if (!isPlanMode()) { await enterPlanMode(ctx, operation); if (!isCurrentPlanOperation(ctx, operation)) return; } const saved = await savePlan(ctx, "", latestRevision, operation); if (!isCurrentPlanOperation(ctx, operation) || !saved) return; ctx.ui.notify("Plan draft cleared.", "info"); return; } if (command === "tools") { if (isCurrentPlanOperation(ctx, operation, operation.store !== undefined)) { ctx.ui.notify(isPlanMode() ? PLAN_MODE_TOOL_NAMES.join(", ") : PLAN_ENTER_TOOL, "info"); } return; } if (trimmed) { if (!isPlanMode()) { await enterPlanMode(ctx, operation); if (!isCurrentPlanOperation(ctx, operation)) return; } if (ctx.isIdle?.() === false) { ctx.ui.notify("Plan mode active. Planning prompt was not queued because the agent is still busy.", "warning"); return; } extensionApi?.sendUserMessage(trimmed); return; } await toggleMode(ctx, operation); if (!isCurrentPlanOperation(ctx, operation, operation.store !== undefined)) return; }, }); } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); }