// src/tools/contracts.ts — the tool CONTRACT TEXT, single-sourced. The hosts // (pi TypeBox tools, opencode def tools) reference these instead of // hand-writing descriptions — the contract has drifted before (the ready // flag, the done→approved transition) and must be fixed in ONE place. // // The transition list and the specification contract are GENERATED from the // item-contract table (AUTOPILOT-128) — never hand-written here again. The // written text used to name `approved→active` while the map also allowed // `approved→blocked`; a test (test/framework/item-contract.test.ts) now FAILS // if the documented list and the map disagree. import { STATUS_TRANSITIONS, renderTransitionList, renderSpecContract, approvalFieldNames } from "../framework/item-contract.ts"; const TRANSITION_DOC = renderTransitionList(STATUS_TRANSITIONS); const SPEC_DOC = renderSpecContract(); // The approval gate's attribute list — DERIVED from the item-contract table, so // it can never name a subset the validator would refuse beside it. const APPROVAL_FIELDS = approvalFieldNames().join("/"); export const CONTRACTS = { /** The `model` parameter shared by queue_add / queue_update / queue_dispatch / * queue_review (KEY: AUTOPILOT-79/80/110) — one description, both hosts. */ model_param: "model override 'provider/id[:thinking]' (the escape hatch); omitted = falls back to a `tier`, then the level map's default tier (`models.default`/`models.reviewDefault` in the ACTUATOR's config, actuator.config.json), then autopilot.config.json's top-level `model` default, then to inheriting the session's model. " + "On queue_add/queue_update/queue_dispatch this is the item's WORKER model — recorded on the item and passed to every worker spawn (dispatch, auto-dispatch, re-dispatch, spec pass) AND to the auto-dispatched reviewer. " + "On queue_review it is a per-call REVIEWER model override — NOT recorded on the item (a reviewer's model choice is not a fact about the item's work). " + "Every spawn resolves the same way: explicit param > the item's model > call tier > item tier > config `model` > config default tier > inherit. Not validated against the model registry; the runtime rejects an unknown model at launch.", /** The `tier` parameter shared by queue_add / queue_update / queue_dispatch / * queue_review (KEY: AUTOPILOT-110) — one description, both hosts. */ tier_param: "capability LEVEL (a name defined under the ACTUATOR's level map — `actuator.config.json` → `models`, e.g. high|medium|low; autopilot.config.json's own `models` is a fallback), resolved to a model + optional thinking level at dispatch time — the ONE place to re-point model choices without hardcoding vendor ids. " + "On queue_add/queue_update/queue_dispatch this is the item's WORKER tier (recorded on the item); on queue_review it is a per-call REVIEWER tier (NOT recorded — a reviewer's capability choice is not a fact about the work). " + "Reviewing and implementing have SEPARATE config defaults (`models.reviewDefault` vs `models.default`). " + "Precedence: an explicit `model` still wins, then the call's `tier`, then the item's `tier`, then config `model`, then the config default tier, then inherit. " + "An UNKNOWN level is REFUSED loudly, listing the configured levels — never a silent fallback to inherit.", /** The approval-gate clause the hosts attach to the queue_add/queue_update * `status`/`scope`/`cwd` parameters (KEY: AUTOPILOT-128). The field list is * DERIVED from the item-contract table via approvalFieldNames() — never * hand-written here — so a parameter text can never name a subset of the * attributes approval actually requires (the stale "scope + cwd" form this * replaces). The item-contract test asserts it names EXACTLY the table's * approval fields, in table order. */ approval_gate_param: `required to reach \`approved\` — the full specification contract (${APPROVAL_FIELDS}, every gap named on refusal) is stated in this tool's description`, /** The `after` parameter shared by queue_add / queue_update (KEY: AUTOPILOT-81). */ after_param: "dependency keys this item is BLOCKED ON until they reach `done` (array of existing keys, or a comma-separated string). " + "Validated at write: every key must already exist, no self-reference, no cycle — a bad value is REJECTED with a clear reason, nothing is written. " + "An approved item with an unmet `after` is WAITING, not ready: it is excluded from auto-dispatch, auto-recovery, human re-dispatch, spec-pass dispatch, and queue_dispatch refuses it by name — overrideCapacity does NOT override this; the only way past it is editing `after`. " + "`done` OR `rejected` is the release condition (not human-review, not shippedAt — shipping can be a deliberate, later step): when the last dependency completes, a parked dependent is released automatically back to the status it was PARKED FROM (`blockedFrom`) — a park from `proposal` returns to `proposal` (the human approval gate is never skipped), a park from `approved` returns to `approved` (a plain `approved` dependent just becomes ready, no store change needed). " + "Rejecting a dependency does NOT auto-reject its dependents — they stay waiting and are named once in the response. Pass an empty array to clear.", queue_list: "List the orchestrator task queue. Filter by status and/or last-change timestamp; compact view by default " + "(heavy detail fields only with includeNotes). Returns per-status counts + fleet occupancy + matching items.", queue_add: "Add a new queue item (proposal by default, or approved). Notes/scope are free-form — no schema constraints on content. " + "evidence/value/urgency/risk are captured IMMEDIATELY, at proposal time, if supplied — they are not deferred to approval and are not silently dropped; " + "a fully-specified proposal (title+scope+evidence+value+urgency+risk) persists all of it on this call, read it back with queue_list/queue_get to confirm. " + "Approval (status=approved) is GATED by the specification contract — a refusal names EVERY gap and how to fix it; a plain proposal is NOT gated (intake must be able to propose before it is scoped). " + SPEC_DOC + " " + "KEY ALLOCATION is deterministic: pass `cwd` and the series resolves automatically (registry → history → repo-name slug). A proposal without cwd gets a provisional Q- handle, renamed into the repo's real series at approval. " + "Pass explicit `series` only when starting a NEW workstream; explicit `key` for semantic suffixes (must be unique). Hand-numbering collides — do not hand-pick numbers. " + "DEPENDENCIES: pass `after` (see its own description) to declare this item BLOCKED on other keys until they reach done.", queue_update: "Update a queue item: status (validated transitions: " + TRANSITION_DOC + "; NO-CHANGE is the non-deliverable terminal outcome — a correct refusal/withdrawal, never re-dispatched); " + SPEC_DOC + " " + "blocked REQUIRES a blocker reason (parked/serialized/merge/decision). `serialized` ADDITIONALLY requires a non-empty `after`: a serialized park with no dependency can never release (nothing to satisfy) — use `parked`/`decision` for an indefinite human deferral. A park records WHERE it came from (`blockedFrom`), so the automatic dependency release restores that status (proposal → proposal, approved → approved, failed/human-review → that lane) instead of always `approved`. " + "REVIEWING A COMPLETED-BUT-PARKED ITEM (blocked→ai-review): give a finished item a route to review that does NOT transit the auto-dispatchable `approved` state — {status:'ai-review'}. It is EVIDENCE-GATED: the item must carry a completed run (`runId`/`lastRunId`) or a recorded branch/sha (`runWorktree`/`landedEvidence`/`shippedAt`), read from the item's own record. If the framework lost the record, pass `completedRun:''` in the SAME write to record it. An item with NO completed run is REFUSED (a park must not become a way to mark unfinished work reviewable). " + "PARKING A FAILED ITEM (failed→blocked): auto-recovery re-dispatches failed items on a timer and reads the STATUS, not your notes — so when a retry CANNOT fix the failure (an interactive prompt no headless run can answer, a missing credential, a decision only the user can make), move it: {status:'blocked', blocker:'decision'}. A blocked item is never auto-re-dispatched and the hold is ticked once. RELEASE it when the blocker is resolved: {status:'approved'} (this clears the blocker + re-arms auto-recovery), then queue_dispatch. " + "RE-DISPATCHING A human-review ITEM (human-review→active): REQUIRES `findings` (non-empty) — {status:'active', findings:''}. This is the SAME structured handoff the decision panel's redispatch action writes; the harness's human re-dispatch sweep reads it and spawns the redo with those findings on its next pass. Do NOT call queue_dispatch afterward — it refuses active-status items by design; this transition alone is the complete re-dispatch. " + "the active→ai-review/failed flips are event-driven — do NOT set them by hand (the ONE deliberate exception is blocked→ai-review above, which is evidence-gated). " + "OVERRIDING A FAILURE VERDICT: pass `overrideReason` — it is RECORDED on the item (append-only overrides[]) instead of living as prose in notes, so a pattern of overrides stays visible. " + "DEPENDENCIES: pass `after` (see its own description) to declare/edit this item's blocking keys; rejecting an item other items depend on does NOT auto-reject them — the response names them, still waiting.", queue_dispatch: "Dispatch a queue item: spawns the worker (same executor as the subagent tool; fresh context, worktree isolation) AND records " + "approved→active with the run id — atomically. Call with the key of an approved item and the scoped worker prompt. " + "High-risk items: still surface the final checkpoint BEFORE calling this. Returns the run id. " + "CAPACITY: this REFUSES when the fleet is at/above maxSlots (the same cap the harness honours) — nothing is spawned and the refusal names the occupancy. " + "Override deliberately with overrideCapacity=true (the dispatch proceeds and is marked over-capacity), or raise the cap with /autopilot capacity . " + "MERGE FINISHERS: pass dispatchClass='finisher' AND finisherSource= when the run writes into the cwd's CHECKOUT (cherry-pick/merge into main) instead of its own worktree — the dispatch records the cwd's HEAD plus that source as a baseline, and success is then judged on the source entering the cwd's history, so a runtime 'no edits in the worktree' failure verdict is overridden (and the override recorded) instead of re-dispatching a merge that already landed. Without finisherSource there is NO evidence and the runtime verdict stands. " + "DEPENDENCIES (AUTOPILOT-81): REFUSES an item still WAITING on an unmet `after` dependency, naming the unmet keys — overrideCapacity does NOT reach this refusal; edit `after` instead.", queue_review: "Dispatch the reviewer for an `ai-review` item: spawns the reviewer subagent (read-only, no worktree) via the same executor, records the " + "reviewerRunId on the item, and emits orch:reviewer-dispatched. When the reviewer completes, the verdict line ('Verdict: PASS | FAIL | NO-CHANGE') " + "is parsed and the item auto-transitions (PASS → human-review awaiting your approval, FAIL → active re-dispatch, cap → failed, NO-CHANGE → no-change — the non-deliverable terminal outcome: the correct thing was to make NO change, so it is NEVER re-dispatched) — see the queue model. " + "The reviewer does NOT re-run the acceptance suite: it AUDITS the worker's captured acceptance artifact and RE-DERIVES cheap static facts by a different method. " + "Its report MUST carry the required contract fields (AUTOPILOT-108/114): a COVERAGE STATEMENT naming each load-bearing claim VERIFIED INDEPENDENTLY / ACCEPTED FROM WORKER'S EVIDENCE / COULD NOT CHECK, a `Verdict reason` (which findings drove the verdict), a `Findings` section giving EVERY finding a FAIL/NOTE disposition with every NOTE justified as OUT of this deliverable's territory, and — on a FAIL — a `Do not rebuild` statement. A report that omits ANY of them FAILS validation and is recorded as a review-contract gap. " + "The verdict is decided by the finding's TERRITORY (AUTOPILOT-114): a finding against THIS deliverable's own claims/docs/tests — the documentation case — or a PARTIAL ROLLOUT of something the item introduced is a FAIL, not a note. Returns the run id.", queue_steer: "Steer a RUNNING worker or reviewer (dispatched via queue_dispatch/queue_review). Writes a steer request to the subagent control " + "channel and VERIFIES the child's acknowledgment. Headless children do not support steering (supported:false capability or silent " + "no-ack) — the tool reports that honestly instead of claiming delivery. If the run dir is gone, the run completed/died — stop + " + "re-dispatch instead.", flag_for_review: "Flag that you believe the current task is complete and ready for the user's judgment. Call this ONLY when you believe the work is " + "done — not after every turn, not when you have a question. Reason explicitly about risk (what happens if wrong) and blast radius " + "(what breaks) before flagging. self_reviewed=true when an automated review passed (commit-hook / reviewer-subagent), else false. " + "NARROWED ROLE: a queue item that PASSES agent review is flagged AUTOMATICALLY by the harness — with pointer-rich review targets AND " + "the reviewer's own caveats (what it could not verify) as residual_risks. Call this tool for what that path does NOT cover: work with " + "NO queue item (a doc/analysis/plan produced directly in session), a queue item that never reached a PASS (review FAIL/cap, " + "budget-capped, unparseable verdict) that you are surfacing as-is, or an honest non-reviewed handover (self_reviewed=false / " + "review_method 'none'|'commit-hook'). Re-flagging an already-flagged queue_key logs the entry in full but MERGES the duplicate " + "desktop notification into the first (entries carry origin: automatic | orchestrator-authored).", } as const;