import { type AgentToolResult, type ExtensionContext } from "@earendil-works/pi-coding-agent"; import { GOAL_AUDIT_ENTRY, detailedSummary, formatAuditUsage, goalDetails } from "./goal-format.ts"; import { buildCompletionReport, buildTaskSummary, taskCompletionBlockWarning, validateGoalCompletion, } from "./goal-policy.ts"; import { loadGoalSettings } from "./goal-settings.ts"; import { runGoalCompletionAuditor, appendAuditUsageEntry, type GoalAuditorResult } from "./goal-auditor.ts"; import { nowIso, type GoalRecord } from "./goal-record.ts"; import { latestEventsForGoal, goalRuntimeEvents } from "./goal-ledger.ts"; import { mergeGoalPromptFromDisk } from "./storage/goal-files.ts"; import { renderGoalChangeManifest } from "./goal-change-delta.ts"; import { deleteChangeBaseline } from "./goal-change-baseline.ts"; import { showEscapeDialog, type EscapeDialogResult } from "./widgets/goal-escape-dialog.ts"; import type { GoalCore } from "./goal-state.ts"; import type { GoalMutationOutcome } from "./goal-service.ts"; import type { SubagentDelegationUsage } from "@xzzpig/pi-subagents/delegation"; /** * Project pi-subagents child usage into pi's tool-result `Usage` shape so the * host accounts the audit spend in this session (footer `$`, `/session` Cost, * `getSessionStats`). pi only counts a tool result that carries `totalTokens` * and an object `cost` with a `total`; pi-subagents' own `Usage` reports `cost` * as a plain number, so it cannot be attached as-is. Absent usage leaves the * result untouched, so every completion branch keeps its previous shape when * the delegation reported no usage. */ function withAuditorUsage>(result: T, usage: SubagentDelegationUsage | undefined): T { if (!usage) return result; return { ...result, usage: { input: usage.input, output: usage.output, cacheRead: usage.cacheRead, cacheWrite: usage.cacheWrite, totalTokens: usage.input + usage.output + usage.cacheRead + usage.cacheWrite, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: usage.cost }, }, }; } /** Total child tokens the auditor burned, across every counter the child reported. */ function auditorTokens(usage: SubagentDelegationUsage | undefined): number { if (!usage) return 0; return usage.input + usage.output + usage.cacheRead + usage.cacheWrite; } /** Audit-card line for the child spend, or undefined when the child reported none. */ function auditorUsageLine(usage: SubagentDelegationUsage | undefined): string | undefined { if (!usage) return undefined; return formatAuditUsage({ tokens: auditorTokens(usage), costUsd: usage.cost, turns: usage.turns }); } // update_goal(complete) execution path: validates the completable state, // runs the independent auditor (or the disabled/legacy-skip branches), and // commits through the single completion transaction. The auditor derives the // requirements from the objective and any verification contract and inspects // actual workspace evidence. An optional completion_summary is forwarded as an // UNTRUSTED executor claim — never evidence and never an approval bypass. export async function runGoalCompletionFlow(core: GoalCore, ctx: ExtensionContext, completionSummary?: string): Promise> { const flushError = core.goalService.flushForAudit(ctx); if (flushError) return {content: [{type: "text", text: flushError}], details: goalDetails(core.state.goal)}; core.reconcileFocusedGoalFromDisk(ctx); // -- Completion -- const completionGate = validateGoalCompletion({ goal: core.state.goal, runningGoalId: core.runningGoalId }); if (!completionGate.ok) { return { content: [{ type: "text", text: completionGate.message }], details: goalDetails(core.state.goal), }; } if (!core.state.goal) throw new Error("Goal disappeared during completion validation."); // Task gate: warn if blockCompletion is enabled and tasks remain pending const disableTasksSettings = loadGoalSettings(ctx.cwd).disableTasks; if (!disableTasksSettings) { const taskWarning = core.state.goal.taskList ? taskCompletionBlockWarning(core.state.goal.taskList) : null; if (taskWarning) { return { content: [{ type: "text", text: taskWarning }], details: goalDetails(core.state.goal), }; } } const auditTarget = mergeGoalPromptFromDisk(ctx, core.state.goal); const completionFocus = core.focusedOperationToken(auditTarget.id); // Append ledger: completion requested try { core.goalService.appendEvents(ctx, [{ type: "completion_requested", goalId: auditTarget.id, at: nowIso(), }]); } catch { // Ledger append failure should not block completion } const settings = loadGoalSettings(ctx.cwd); const auditorSettings = settings.auditor; const auditorLabel = auditorSettings?.provider || auditorSettings?.model || auditorSettings?.thinkingLevel ? `${auditorSettings?.agent ?? "goal-auditor"} (${auditorSettings?.provider ?? "default"}/${auditorSettings?.model ?? "default"}${auditorSettings?.thinkingLevel ? `:${auditorSettings.thinkingLevel}` : ""})` : (auditorSettings?.agent ?? "goal-auditor"); /** * Single transaction for every successful completion commit — audit-approved, * globally disabled, legacy per-goal skipped, or user-bypassed via Escape. * Deferred archival: sets the goal complete in memory + writes the active * file WITHOUT archiving; archival happens at turn_end so the agent can * recognise the outcome before the goal is archived. * * Returns a discriminated result. When GoalService.apply fails (stale focus, * missing file, write failure, or invalid lifecycle state), it returns * { ok: false, message, terminate: false } — never a completed report and * never a termination request (follow-up Stage 3). */ type CompletionCommitResult = AgentToolResult & { ok: boolean }; function commitGoalCompletion(core: GoalCore, ctx: ExtensionContext, opts: { goal: GoalRecord; completionFocus: { goalId: string; revision: number }; auditorReport?: string | null; auditSkippedReason?: string | null; terminate?: boolean; trailing?: string[]; }): CompletionCommitResult { core.accountProgress(ctx); core.auditProgress = null; core.goalWidgetComponentRef.current?.invalidate(); let completeResult: GoalMutationOutcome; try { completeResult = core.goalService.apply(ctx, { reconcile: false, focusToken: opts.completionFocus, mutate: () => ({ ...opts.goal, status: "complete" as const, stopReason: "agent" as const, updatedAt: nowIso() }), }); } catch (err) { // The authoritative file write throws on failure; surface it as a typed // mutation outcome so the caller can inspect it instead of crashing. completeResult = { ok: false, message: err instanceof Error ? err.message : String(err) }; } if (!completeResult.ok) { return { ok: false, content: [{ type: "text", text: `Goal completion failed: ${completeResult.message ?? "the state mutation was rejected"}. The goal was not completed.` }], details: goalDetails(core.state.goal), terminate: false, }; } if (completeResult.goal) core.runtime.markTurnStopped(completeResult.goal.id); // Baseline lifecycle: the approved completion transaction committed, so the // execution-window baseline is done. A rejected or failed audit never reaches // this point, so the baseline survives for a retry. if (completeResult.goal) deleteChangeBaseline(ctx, completeResult.goal.id); core.updateUI(ctx); const text = buildCompletionReport({ detailedSummary: detailedSummary(core.state.goal), auditorReport: opts.auditorReport, auditSkippedReason: opts.auditSkippedReason, taskSummary: core.state.goal?.taskList ? buildTaskSummary(core.state.goal.taskList) : null, }); return { ok: true, content: [{ type: "text", text: opts.trailing?.length ? [text, "", ...opts.trailing].join("\n") : text }], details: goalDetails(core.state.goal), ...(opts.terminate === false ? {} : { terminate: true }), }; } // Check if auditor is disabled per-goal (legacy persisted skipAuditor:true // records remain readable and honored for compatibility; no model tool or // task dialog creates new per-goal bypass state). if (auditTarget.skipAuditor) { core.auditMessages.enqueue(ctx, { customType: GOAL_AUDIT_ENTRY, content: `Goal completed — per-goal auditor disabled.`, display: true, details: { phase: "skipped", goalId: auditTarget.id, auditor: auditorLabel }, }); try { core.goalService.appendEvents(ctx, [{ type: "audit_skipped", goalId: auditTarget.id, reason: "disabled", provider: auditorSettings?.provider, model: auditorSettings?.model, thinkingLevel: auditorSettings?.thinkingLevel, at: nowIso(), }]); } catch { // Ledger append failure should not block completion } return commitGoalCompletion(core, ctx, { goal: auditTarget, completionFocus, auditSkippedReason: "per-goal auditor disabled", }); } // settings.auditor.disabled is an explicit user-owned setting: completion skips // the auditor, records audit_skipped, and proceeds through the normal // deferred-completion path. No model-side bypass flag is required. if (auditorSettings?.disabled === true) { core.auditMessages.enqueue(ctx, { customType: GOAL_AUDIT_ENTRY, content: `Goal completed — auditor disabled in settings.`, display: true, details: { phase: "skipped", goalId: auditTarget.id, auditor: auditorLabel }, }); try { core.goalService.appendEvents(ctx, [{ type: "audit_skipped", goalId: auditTarget.id, reason: "disabled", provider: auditorSettings?.provider, model: auditorSettings?.model, thinkingLevel: auditorSettings?.thinkingLevel, at: nowIso(), }]); } catch { // Ledger append failure should not block completion } return commitGoalCompletion(core, ctx, { goal: auditTarget, completionFocus, auditSkippedReason: "auditor disabled in settings", }); } // Auditor is enabled — run the normal audit flow core.auditMessages.enqueue(ctx, { customType: GOAL_AUDIT_ENTRY, content: [ "Auditor: I am starting the independent completion audit.", `Goal id: ${auditTarget.id}`, `Auditor model: ${auditorLabel}`, ].filter((line): line is string => line !== undefined).join("\n"), display: true, details: { phase: "started", goalId: auditTarget.id, auditor: auditorLabel }, }); if (!core.isFocusedOperationCurrent(completionFocus)) { return core.focusedOperationCancelledResult("Goal completion", completionFocus); } // Append ledger: audit started try { core.goalService.appendEvents(ctx, [{ type: "audit_started", goalId: auditTarget.id, provider: auditorSettings?.provider, model: auditorSettings?.model, thinkingLevel: auditorSettings?.thinkingLevel, at: nowIso(), }]); } catch { // Ledger append failure should not block completion } // Set up the high-level dashboard before dispatching the delegated child. const auditStartedAt = Date.now(); core.auditProgress = { recentOutput: [], phase: "running", elapsedMs: 0, auditorLabel, }; // Start animation timer for the spinner in the auditor widget core.stopAuditAnimation(); core.auditAnimationTimer = setInterval(() => { if (!core.auditProgress) { core.stopAuditAnimation(); return; } core.auditProgress.elapsedMs = Date.now() - auditStartedAt; core.goalWidgetComponentRef.current?.invalidate(); }, 80); core.auditAnimationTimer?.unref?.(); // Create a dedicated AbortController for the audit so Escape can cancel the // exact structured-delegation attempt without touching other child runs. core.auditAbortController?.abort(); const completionAuditController = new AbortController(); core.auditAbortController = completionAuditController; // P1-6: warm start — seed the auditor with the parent-rendered ledger tail // (recent lifecycle + task evidence) so it does not re-derive session facts. // auditor.warmContext: false skips the injection entirely. const ledger = goalRuntimeEvents(ctx, auditTarget.id); const warmTail = settings.auditor?.warmContext === false ? [] : latestEventsForGoal(ledger, auditTarget.id, 8); const warmContext = warmTail.length > 0 ? `Recent goal events (from the shared ledger):\n${warmTail.map((e) => `- ${e.at} ${e.type}${"taskId" in e ? ` (task ${e.taskId})` : ""}${"evidence" in e && e.evidence ? ` evidence: ${e.evidence}` : ""}`).join("\n")}` : null; // Change manifest: a machine-collected index of what changed in this goal's // execution window, so the fresh-context auditor can aim its inspection // instead of re-deriving the change set. Best-effort by construction — with // no baseline (off, non-git, capture failed) this is null and the audit input // is exactly what it was before this feature. It is evidence about the // workspace, never a substitute for the executor claim's untrusted marking. const changeManifest = await renderGoalChangeManifest(ctx, auditTarget.id); let auditor: GoalAuditorResult; try { auditor = await (core.dependencies.runCompletionAuditor ?? runGoalCompletionAuditor)({ ctx, events: core.pi.events, goal: auditTarget, detailedSummary: detailedSummary(auditTarget), completionSummary: completionSummary?.trim() || undefined, settings, warmContext, changeManifest, signal: completionAuditController.signal, // Esc interrupts the audit without killing the child: the audit is // parked and the SAME delegated subagent resumes when the user chooses // to continue it (see the interrupted branch below). parkOnAbort: true, onProgress: (progress) => { core.auditProgress = { ...progress, auditorLabel, elapsedMs: Date.now() - auditStartedAt, }; core.goalWidgetComponentRef.current?.invalidate(); }, }); } catch (error) { auditor = { approved: false, disapproved: true, output: "", error: `Goal auditor failed unexpectedly: ${error instanceof Error ? error.message : String(error)}`, }; } finally { if (core.auditAbortController === completionAuditController) core.auditAbortController = null; core.stopAuditAnimation(); } // Child-session accounting. The delegated auditor runs in its own pi session, // so its spend never reaches this session's own turn accounting. Record it as // a separate ledger entry (goal.usage.tokensUsed stays parent-turn-only, so no // token budget changes) and surface the same numbers on the audit card below. appendAuditUsageEntry(core, ctx, auditTarget.id, auditor.usage); if (!core.isFocusedOperationCurrent(completionFocus)) { core.auditProgress = null; core.goalWidgetComponentRef.current?.invalidate(); return withAuditorUsage(core.focusedOperationCancelledResult("Goal completion", completionFocus), auditor.usage); } // Escape parked the audit: the delegated child is still running and the // attempt's terminal result is captured. Ask the user whether to finish the // audit with the SAME subagent or complete without it; the child is only // cancelled when the user opts out (or focus moved on). if (auditor.interrupted && auditor.session) { core.auditProgress = null; core.goalWidgetComponentRef.current?.invalidate(); core.updateUI(ctx); core.enterGoalModal(); let userChoice: EscapeDialogResult; try { userChoice = await showEscapeDialog(ctx, auditTarget.objective); } finally { core.exitGoalModal(); } // Consume the transient abort state recorded by the low-level callback. core.auditAborted = false; if (!core.isFocusedOperationCurrent(completionFocus)) { // Focus moved on while the dialog was open: kill the parked child so it // does not keep running unattended, then return the focus result. const cancelled = await auditor.session.cancel(); appendAuditUsageEntry(core, ctx, auditTarget.id, cancelled.usage); return withAuditorUsage(core.focusedOperationCancelledResult("Goal completion", completionFocus), cancelled.usage); } if (userChoice === "complete_without_audit") { // The child was kept alive across the dialog; cancel it now. const cancelled = await auditor.session.cancel(); appendAuditUsageEntry(core, ctx, auditTarget.id, cancelled.usage); // ── Mark complete without audit ──────────────────────────── core.auditMessages.enqueue(ctx, { customType: GOAL_AUDIT_ENTRY, content: `Goal completed — user bypassed audit via Escape.`, display: true, details: { phase: "skipped", goalId: auditTarget.id, auditor: auditorLabel }, }); // The one canonical ledger outcome for this choice. try { core.goalService.appendEvents(ctx, [{ type: "audit_skipped", goalId: auditTarget.id, reason: "user_aborted", provider: auditorSettings?.provider, model: auditorSettings?.model, thinkingLevel: auditorSettings?.thinkingLevel, at: nowIso(), }]); } catch { // Ledger append failure should not block completion } // Deferred archival: set goal complete in memory + write the active file // WITHOUT archiving; archival happens at turn_end so the agent can // recognise the skipped audit before the goal is archived. return withAuditorUsage(commitGoalCompletion(core, ctx, { goal: auditTarget, completionFocus, auditSkippedReason: "auditor bypassed (user pressed Escape during audit)", terminate: false, trailing: ["The goal is complete. Provide a final summary of what was accomplished."], }), cancelled.usage); } // ── Continue audit with the SAME delegated subagent ───────────── // Re-arm the audit display, then keep waiting on the original attempt. // Its verdict drives the shared approve/disapprove handling below. const resumedAt = Date.now(); core.auditProgress = { recentOutput: [], phase: "running", elapsedMs: 0, auditorLabel, }; core.stopAuditAnimation(); core.auditAnimationTimer = setInterval(() => { if (!core.auditProgress) { core.stopAuditAnimation(); return; } core.auditProgress.elapsedMs = Date.now() - resumedAt; core.goalWidgetComponentRef.current?.invalidate(); }, 80); core.auditAnimationTimer?.unref?.(); try { auditor = await auditor.session.resume(); } catch (error) { auditor = { approved: false, disapproved: true, output: "", error: `Goal auditor failed unexpectedly: ${error instanceof Error ? error.message : String(error)}`, }; } finally { core.stopAuditAnimation(); } // The resumed attempt is terminal: account its spend, then re-check focus // (it may have moved while the resumed audit was running) before verdicts. appendAuditUsageEntry(core, ctx, auditTarget.id, auditor.usage); if (!core.isFocusedOperationCurrent(completionFocus)) { core.auditProgress = null; core.goalWidgetComponentRef.current?.invalidate(); return withAuditorUsage(core.focusedOperationCancelledResult("Goal completion", completionFocus), auditor.usage); } } else if (auditor.error === "Auditor aborted.") { // Non-parkable fallback (e.g. a dependency stub that resolved the audit // with the sentinel error but no interrupted session): show the same // dialog; "continue" keeps the goal active without a resumed audit. core.auditProgress = null; core.goalWidgetComponentRef.current?.invalidate(); core.updateUI(ctx); core.enterGoalModal(); let userChoice: EscapeDialogResult; try { userChoice = await showEscapeDialog(ctx, auditTarget.objective); } finally { core.exitGoalModal(); } // Consume the transient abort state recorded by the low-level callback. core.auditAborted = false; if (!core.isFocusedOperationCurrent(completionFocus)) { return withAuditorUsage(core.focusedOperationCancelledResult("Goal completion", completionFocus), auditor.usage); } if (userChoice === "complete_without_audit") { // ── Mark complete without audit ──────────────────────────── core.auditMessages.enqueue(ctx, { customType: GOAL_AUDIT_ENTRY, content: `Goal completed — user bypassed audit via Escape.`, display: true, details: { phase: "skipped", goalId: auditTarget.id, auditor: auditorLabel }, }); // The one canonical ledger outcome for this choice. try { core.goalService.appendEvents(ctx, [{ type: "audit_skipped", goalId: auditTarget.id, reason: "user_aborted", provider: auditorSettings?.provider, model: auditorSettings?.model, thinkingLevel: auditorSettings?.thinkingLevel, at: nowIso(), }]); } catch { // Ledger append failure should not block completion } // Deferred archival: set goal complete in memory + write the active file // WITHOUT archiving; archival happens at turn_end so the agent can // recognise the skipped audit before the goal is archived. return withAuditorUsage(commitGoalCompletion(core, ctx, { goal: auditTarget, completionFocus, auditSkippedReason: "auditor bypassed (user pressed Escape during audit)", terminate: false, trailing: ["The goal is complete. Provide a final summary of what was accomplished."], }), auditor.usage); } // ── Continue working ──────────────────────────────────────── // The goal stays active: no pause, no stop marker, no skip event. core.goalWidgetComponentRef.current?.invalidate(); core.updateUI(ctx); return withAuditorUsage({ content: [{ type: "text", text: "Audit aborted — the goal remains active and work continues." }], details: goalDetails(auditTarget), }, auditor.usage); } // Show final audit output briefly before clearing if (core.auditProgress && auditor.output) { const outputLines = auditor.output.split("\n").slice(0, 8); core.auditProgress = { ...core.auditProgress, phase: "done", recentOutput: outputLines, elapsedMs: Date.now() - auditStartedAt, }; core.goalWidgetComponentRef.current?.invalidate(); } // Append ledger: audit result const verdict = auditor.approved ? "approved" : auditor.error ? "error" : "disapproved" as const; try { core.goalService.appendEvents(ctx, [{ type: "audit_result", goalId: auditTarget.id, verdict, report: auditor.output || "Auditor produced no output.", at: nowIso(), }]); } catch { // Ledger append failure should not block completion } if (!auditor.approved) { // Clear auditor progress to restore normal widget state, then show the // §15.4 result card briefly so the required next work is visible before // the normal dashboard returns (the goal stays open). core.auditProgress = null; core.setAuditResult(auditor.error ? "error" : "disapproved", auditor.output || "Auditor produced no output."); core.goalWidgetComponentRef.current?.invalidate(); const rejectionText = [ "Goal audit rejected.", "", "Goal completion rejected by independent auditor.", auditor.model ? `Auditor model: ${auditor.model}${auditor.thinkingLevel ? `:${auditor.thinkingLevel}` : ""}` : undefined, auditor.error ? `Auditor error: ${auditor.error}` : undefined, auditorUsageLine(auditor.usage), "", auditor.output || "Auditor produced no approval marker.", // Operator-configured closing note (settings auditor.feedbackNotes): // fixed guidance the executor receives alongside every rejection. ...(settings.auditor?.feedbackNotes ? ["", "Operator note:", settings.auditor.feedbackNotes] : []), ].filter((line): line is string => line !== undefined).join("\n"); core.auditMessages.enqueue(ctx, { customType: GOAL_AUDIT_ENTRY, content: rejectionText, display: true, details: { phase: "rejected", goalId: auditTarget.id, auditor: auditor.model }, }); return withAuditorUsage({ content: [{ type: "text", text: rejectionText }], details: goalDetails(core.state.goal), }, auditor.usage); } const approvalText = [ "Auditor: I approve this completion claim.", auditor.model ? `Auditor model: ${auditor.model}${auditor.thinkingLevel ? `:${auditor.thinkingLevel}` : ""}` : undefined, auditorUsageLine(auditor.usage), "", auditor.output || "Auditor approved completion.", ].filter((line): line is string => line !== undefined).join("\n"); core.auditMessages.enqueue(ctx, { customType: GOAL_AUDIT_ENTRY, content: approvalText, display: true, details: { phase: "approved", goalId: auditTarget.id, auditor: auditor.model }, }); // §15.4: the approval card shows during the deferred archival window. core.setAuditResult("approved", auditor.output || "Auditor approved completion."); // Account for any remaining elapsed time. // Deferred archival happens inside commitGoalCompletion; archival occurs at // turn_end so the agent can see the auditor approval before the goal is // archived. return withAuditorUsage(commitGoalCompletion(core, ctx, { goal: auditTarget, completionFocus, auditorReport: auditor.output, }), auditor.usage); }