import type { ControllerState } from "./state-machine.js"; export interface RunBlocker { remainingBlockers: string[]; requiredToContinue: string } const NON_SUCCESS_TERMINAL = new Set(["BLOCKED", "FAILED", "CANCELLED", "BUDGET_EXHAUSTED"]); /** * What a person does next, per recorded reason. UltraPi's answer to an unverifiable situation * is to do nothing, which is the right answer and reads as brittleness unless the stop explains * itself. Each line names one concrete action, not a restatement of the failure. */ export const BLOCKER_ACTIONS: Record = { "acceptance-command-required": "Rerun with an executable acceptance check, for example: /ultra . Acceptance: npm test", "acceptance-failed-unfingerprinted": "The acceptance check failed and the failure could not be fingerprinted, so automatic repair would be guesswork. Run the acceptance command yourself to see the output, then rerun /ultra on the narrower problem it shows.", "budget-accounting-incomplete": "Usage could not be accounted for, so the run stopped rather than spend unmeasured. Check /ultra-config status for provider errors, then rerun /ultra.", "budget-exhausted": "Raise the weekly or daily credit budget with /ultra-config, or rerun with a cheaper policy or a narrower scope.", "delegated-run-failed": "The delegated run ended without completing. Check /ultra-config status for provider health, then rerun /ultra.", "dispatch-failed": "The run failed before any agent started, so nothing was changed. Check /ultra-config status, then rerun /ultra.", "project-trust-required": "Trust this project in Pi so UltraPi may run its acceptance command, then rerun /ultra.", "recovery-read-only-handoff": "The interrupted run was handed back read-only and made no changes. Review its findings, then start a fresh /ultra for the remaining work.", "repair-attempts-exhausted": "Repair attempts for this failure are exhausted and the acceptance check still fails. Read the acceptance output, then rerun /ultra on a narrower slice or with a higher policy.", "repair-dispatch-failed": "The repair agent could not be started, so the failing change was left unrepaired. Check /ultra-config status for provider health, then rerun /ultra.", "required-result-contract-failed": "A required worker returned no usable result twice in a row. Rerun /ultra; if it repeats, reduce fan-out or declare a stronger tier for the deciding roles.", "root-attribution-required": "The root turn ended without saying which evidence its changes rest on, so they were not accepted. Rerun /ultra; if it repeats, pass an explicit bounded mode.", "root-model-unavailable": "The required root model is not available in this Pi profile. Enable it in the profile settings, or declare a reachable model in models.tiers, then rerun /ultra.", "stopped-by-user": "The run was stopped before it finished. Rerun /ultra when you are ready to continue.", "verification-command-failed": "The acceptance command could not be executed at all. Check that it runs from a shell in this project, then rerun /ultra.", "worktree-integration-failed": "The reviewed change passed review but could not be integrated into your working tree. Resolve the conflicting state in the repository, then rerun /ultra.", }; /** The floor, so a path nobody anticipated still tells the user something actionable. */ const ACTION_FOR_STATE: Record = { BLOCKED: "UltraPi stopped rather than hand back an unverified result. Run /ultra-config runs to see the last recorded step for this run id, then rerun /ultra with a narrower scope or an explicit acceptance command.", FAILED: "The run ended on an error rather than a decision. Check /ultra-config status for provider health, then rerun /ultra; if it repeats, rerun with mode direct to narrow where it breaks.", CANCELLED: "The run was stopped before it finished. Rerun /ultra when you are ready to continue.", BUDGET_EXHAUSTED: BLOCKER_ACTIONS["budget-exhausted"]!, }; export function isNonSuccessTerminal(state: ControllerState): boolean { return NON_SUCCESS_TERMINAL.has(state); } /** * Resolves an ending into a named blocker and a next action. Returns undefined for states that * are not a non-success ending; for everything else the action is never empty, whether or not * the recorded reason is one this table knows. */ export function runBlocker(state: ControllerState, reason?: string): RunBlocker | undefined { if (!isNonSuccessTerminal(state)) return undefined; const known = reason ? BLOCKER_ACTIONS[reason] : undefined; const fallback = state === "BUDGET_EXHAUSTED" ? "budget-exhausted" : state.toLowerCase(); return { remainingBlockers: [reason ?? fallback], requiredToContinue: known ?? ACTION_FOR_STATE[state]! }; }