/** * #302: `org_complete` had no gate at all — a boss could end a run `partial` * with a full backlog and no named reason, and nothing checked whether the * claim was true. `checkCompletion` is the pure decision behind the gate: * given already-gathered facts about the run (nothing it can invent for * itself), it returns a refusal string or `null` (allow). The daemon gathers * the facts (`daemon.ts`'s `onComplete`) and calls this; this module does no * I/O and imports nothing from `daemon.ts`, so it is table-testable without * booting a daemon. * * A free-text blocker is satisfiable by typing anything, so free text alone * is out. But an enum is not honest either — a boss can pick 'time' as * easily as it can type "time". The enum's real value is that it is * MACHINE-CHECKABLE, which free text is not: `budget`/`human`/`time` are * each cross-checked against runtime state the boss does not author. * `external` is the deliberate escape hatch — not checkable, so its * `blockerDetail` is required to be a real, substantive claim instead (it is * recorded verbatim in the run history, attributed to the boss, so a human * reads it). * * Safety property, load-bearing, asserted as test rows not just documented * here: this module constrains ONLY `outcome: 'partial'` by default. * `achieved` and `failed` are never refused by the default (`mode: 'boss'`) * half — a boss can always end the run honestly, so the gate cannot trap an * org in a run it is unable to finish. `mode: 'dag'` (opt-in via * `run_config.completion`) is an ADDITIONAL, stronger constraint on top of * that, not a replacement: it also refuses `achieved` while runnable work * remains, which the default half never does. * * #564 exception: while a BLOCKING `ask_human` question is unanswered, every * outcome is refused except `partial` with blocker `human` — a boss must not * end the run on an answer it made up. That escape keeps the property above: * a run whose human never replies can still be ended, honestly. */ export type CompletionOutcome = 'achieved' | 'partial' | 'failed'; export type CompletionBlocker = 'budget' | 'human' | 'external' | 'time'; export interface CompletionFacts { outcome: CompletionOutcome; blocker?: CompletionBlocker; blockerDetail?: string; /** 'boss' (default): only `outcome: 'partial'` is gated, via `blocker`. * 'dag' (run_config.completion: 'dag', opt-in): additionally refuses * `achieved`/`partial` while runnable work remains — see `hasPendingWork`. */ mode: 'boss' | 'dag'; /** Highest role spend / ceiling across tokens and USD, 0..1. */ maxBudgetFraction: number; /** Unannswered `ask_human` questions plus pending decision gates. */ pendingHumanWaits: number; /** `TaskDag.hasActiveBlock(now)` — every non-terminal task is blocked on a * real-world time still in the future. False when there are no * non-terminal tasks at all (see `hasPendingWork`'s doc). */ hasActiveBlock: boolean; /** Some task in `org_tasks` is not yet terminal (done/failed/split/merged/ * cancelled). Must be checked ALONGSIDE `hasActiveBlock`, not instead of * it: `hasActiveBlock` alone is false on an EMPTY dag too (nothing left to * block on), which would wrongly refuse a run that legitimately finished * every task. */ hasPendingWork: boolean; /** #564: ids of unanswered BLOCKING `ask_human` questions in the org. While * any is open, every outcome is refused except `partial` with blocker * `human` — the one honest way to end a run that is waiting on a person. */ openBlockingQuestions?: string[]; } /** Decide whether an `org_complete` call is honest, given already-gathered * facts. Returns a refusal message, or `null` to allow. */ export declare function checkCompletion(f: CompletionFacts): string | null; /** * The gate above asks "is this claim about the RUN honest?". * `checkTaskEvidence` asks the same question one level down, about a single * item: "may this role close this task, and is there anything but its own * opinion saying it is done?". * * Why: one measured run produced 223 gate verdicts for 15 finished items — * ~15 opinions per item — and still shipped five controls that could not * fail. Three times the verifier FAILED a sha the reviewer had already * APPROVED, and the verifier was right every time. Every one of those * verdicts was an LLM judgment with nothing checking it. * * Ported from Gas Town's `gt done` (internal/cmd/done.go:498-526), which * refuses to retire review work unless the item carries a fresh evidence * comment. Its five tests map onto this runtime as: * 1. posted after work started — STRUCTURAL here, not a check: evidence is * an argument OF the org_task_done call, so it cannot predate the call. * Gas Town needs `attached_at` because its evidence is a comment that * persists on the item independently of the close. * 2. authored by the assignee — `caller` is the RUNTIME's view of which * role invoked the tool (session.ts binds it per role), compared here * against the task's assignee. Deliberately not a self-declared author * field, which an agent could simply write. * 3. machine-parseable prefix — replaced by something stronger: evidence is * a typed {command, exitCode, output} record. There is no prose to * regex, and "an acceptance criterion is a command with an exit code" * (D5) becomes expressible in the type itself. * 4. not machine-generated — n/a, see 3. * 5. head_sha equal to the current HEAD — ported as-is, and the * load-bearing one: evidence gathered before the last commit is STALE. * The work moved; the proof did not. * * What this does NOT do, stated plainly rather than implied: the runtime does * not re-execute the command. A role can still report exitCode 0 for a * command it never ran. What the gate buys is that the proof must name a * runnable command, must be attributable, and must be pinned to the tree * state being closed — so it cannot be recycled across commits, which is the * failure actually observed. Re-executing the acceptance commands from the * daemon is the strictly stronger version and the obvious next step; it needs * a decision about where those commands run (role sandbox vs daemon) that is * out of scope here. * * Opt-in: `run_config.completion_evidence` (default false). Turning it on * refuses completions that previously succeeded — that IS the point — so it * must never be on by default, or every existing org breaks on upgrade. */ /** One acceptance criterion: a command, the exit code it actually returned, * and what it printed. */ export interface EvidenceCheck { command: string; exitCode: number; /** The exit code that means this criterion is met, when that is not 0 — a * 404 from a GET that must not find anything, `git config --get` of a key * that must stay unset, a timeout that must fire (124). The check passes * iff `exitCode === (expectExit ?? 0)`. Declaring it keeps the real exit * code in the record; the alternative roles reached for, `|| true`, * throws it away. */ expectExit?: number; /** One line saying why that non-zero exit is the CORRECT outcome ("404 = * branch not protected"). Required whenever `expectExit` is non-zero: * without it, declaring an expectation and muting a real failure look * identical to everyone downstream. */ expectReason?: string; output?: string; } /** The exit code a check must return to pass. */ export declare const expectedExit: (c: EvidenceCheck) => number; /** True when the check declares a non-zero expected exit. `expectExit: 0` is * the default and declares nothing, so it needs no reason and is not * audited. */ export declare const declaresExpectExit: (c: EvidenceCheck) => boolean; /** How a declared expectation renders wherever a check is shown — the review * packet, the task's stored result, a refusal. The reason rides along with * the code so a reader never has to go looking for it. */ export declare const expectSuffix: (c: EvidenceCheck) => string; /** What `command` aggregates, or null when its exit code is one outcome. */ export declare function aggregateCommand(command: string): string | null; export interface TaskEvidence { /** The commit sha the checks were run against. */ headSha: string; /** The worktree the checks ran in, when the work is not in the org * workspace itself. Pins the staleness check to that worktree's HEAD. */ worktree?: string; checks: EvidenceCheck[]; } /** A commit that is the current state of some local work: a worktree's HEAD * or a local branch's tip. */ export interface LocalHead { sha: string; /** Absolute worktree path, for a worktree HEAD. */ worktree?: string; /** Short branch name, when the head is (or is checked out on) a branch. */ branch?: string; } export interface TaskEvidenceFacts { /** `run_config.completion_evidence`. False (the default) allows everything. */ required: boolean; evidence?: TaskEvidence; /** The workspace's real current commit sha, resolved by the caller. * Undefined when the workspace is not a git repository. */ headSha?: string; /** Every local head of the workspace's repository — each worktree's HEAD * and each local branch tip. Evidence pinned to any of them is current. * When absent, only `headSha` counts. */ heads?: LocalHead[]; /** Whether git knows `sha` as a commit. Tells a typo'd sha apart from * one the tree moved past; when absent every sha is assumed known. */ isKnownCommit?: (sha: string) => boolean; /** Whether the evidence's `worktree` exists on disk; an absent all-caps * path (".../SRC") is then taken for an unfilled placeholder. */ worktreeExists?: boolean; /** `worktree` as the role wrote it, before the caller resolved it against * the workspace. A relative one ("src", "work/src") that resolves to no * worktree names the one worktree whose path ends with it. */ worktreeLabel?: string; /** Role id the runtime saw calling org_task_done. */ caller: string; /** The task's recorded assignee. */ assignee: string; /** The `result` text of the same org_task_done call. Only read when no * evidence arrived, to tell a missing proof from one written into it. */ result?: string; } /** Decide whether a role may close a task, given already-gathered facts. * Returns a refusal message, or `null` to allow. Pure: the caller resolves * the head sha and the assignee. */ export declare function checkTaskEvidence(f: TaskEvidenceFacts): string | null; //# sourceMappingURL=completion-gate.d.ts.map