Completion gate (std/agent/completion_gate)
agent_completion_gate(runtime, options) returns an options fragment for
agent_loop. It checks host-supplied write and verification facts and can add a
bounded LLM judge. Harn owns the decision rules. The host supplies facts such as
which writes changed source and whether verification passed.
agent_completion_gate#
agent_completion_gate(runtime: HarnessRuntime, options: CompletionGateOptions = {}) -> dict
Spread the returned fragment into the loop's base options:
import { agent_completion_gate } from "std/agent/completion_gate"
agent_loop(harness, task, system, base_opts + agent_completion_gate(
harness.runtime, {
facts: fn(ctx) { return host_completion_facts(ctx.session_id) },
verify_command: fn() { return host_run_verify() },
// optional bounded LLM judge, capped at 5 by default
judge: true,
}))
Options#
CompletionGateOptions splits into host-fact callbacks and plain-data policy
knobs. Every field is optional.
| Field | Type | Default | Meaning |
|---|---|---|---|
facts | fn(ctx) -> CompletionFacts | — | The primary fact supplier. ctx is {session_id, task, stop_reason, text, messages}. |
classify_write | fn(path, diff?) -> WriteKind | — | Labels one write when facts returns a writes list without counts. |
verify_command | fn() -> CompletionVerifyVerdict | — | Runs the verifier oracle when facts carries no verify verdict. |
feedback_decorator | fn(reason, feedback, verdict) -> string | — | Decorates a delivered veto using its stable ladder reason and structured verdict. |
require_source_write | bool | true | Enforce the source-write evidence requirement. |
requires_write | bool | per-task fact | Override the "this task needs a source change" fact. |
max_vetoes | int | 3 | Per-session soft-veto budget; 0 disables. |
requirement_contract | CompletionRequirementContract | absent | Declares requirements assessed from the facts callback's typed evidence. Requires facts. |
judge | bool / dict | off | Attach a bounded LLM judge (true or a judge-config dict). |
judge_seam | string | "verify_completion_judge" | Which capped LLM seam the judge rides ("verify_completion_judge" or "turn_end_condition"). |
feedback_templates | dict | defaults | Override feedback by ladder key; repeated failures support {attempts} and {findings}. |
escalation_threshold | int | 3 | Failed-after-write streak required to recommend escalation. |
escalation_target | string | — | Host routing channel copied onto an escalated verdict and event. |
All verifier results are required: one failed result blocks completion, and an
empty result list remains unmeasured. The former veto_combine callback has been
removed so a host cannot replace this rule with custom verdict arbitration.
requirement_contract uses the same typed contract and evidence roles as the
completion judge. The facts callback returns requirement_assessments and
requirement_evidence; Harn joins them to the declared roster. Missing, duplicate,
unsupported and unmet assessments remain pending, even when a separate verifier
passed or the soft-veto budget was spent. The decision receipt includes the
required count, pending count and pending names. An assistant_output requirement
can cite a typed response observation without inventing a tool call or artifact.
The fact types:
CompletionFacts—{consecutive_failed_after_write?, source_write_count?, cosmetic_write_count?, writes?, verify?, verification_failures_converging?, requires_write?, requirement_assessments?, requirement_evidence?}. Supply counts directly, or awriteslist ofCompletionWriteFactfor the gate to classify.verifyis oneCompletionVerifyVerdictor a list of them. When the streak is present, a red verifier uses the streak-aware reasons below; a converging diagnostic set consumes churn credit and suppresses escalation for that decision.CompletionWriteFact—{path?, diff?, kind?}.kindis aWriteKindstring;"source"and"cosmetic"are load-bearing, anything else is treated as non-source.CompletionVerifyVerdict—{ok?, findings?, command?}.findingsis optional red-detail text.commandoptionally names what the oracle ran, so the reading passed to the bounded judge can say which verification passed.
The deterministic ladder#
The gate never keys on a done-sentinel string. It decides purely from write and verifier facts, first match wins:
| Reason | Result | Condition |
|---|---|---|
no_source_write | veto (soft) | task requires a source change, but only cosmetic / zero source writes so far |
verification_after_write_red | veto (strict) | a source write with a red verifier and no streak fact |
failed_verification | veto (strict) | streak-aware red verifier below the threshold, or diagnostic churn is still converging |
repeated_verification_failures | veto (strict) + escalation | non-converging red verifier at or above escalation_threshold |
verified_after_write / verified | allow | verifier is green |
missing_verification | veto (strict) | source written, verifier configured, not yet run — the budget never releases it |
no_workspace_write | allow | task does not require a source change |
veto_budget_exhausted | allow | a soft veto after max_vetoes, converted to an attributable end |
Only source writes count as progress toward done. A cosmetic final write (a
comment, a .md typo) is not evidence: it can't flip an already-green run back
to unverified, and a run that wrote only cosmetics can't claim done. A strict
veto (a source write with a red or missing verifier) is never released by the
veto budget, so a failing build cannot end as verified.
Each deterministic decision emits a judge_decision event with
trigger: "verify_completion", confirm, and the stable reason above. When
the budget converts a soft veto into an allow, the event carries
reason: "veto_budget_exhausted" plus converted_from with the original class.
Streak escalation decisions also carry escalation_recommended and, when
configured, escalation_target. Consumers read the same fields from the
session's judge_decision events.
feedback_decorator runs only after the ladder and veto budget resolve a veto
that will actually be delivered. It receives (reason, feedback, verdict);
the verdict includes any escalation fields, so a host can attach
language-specific repair cues without parsing the default prose.
Degraded mode#
With neither facts nor verify_command, the deterministic gate has nothing
to assert, so it abstains: it
allows, and marks _completion_gate.facts_available = false with a verdict
reason of facts_unavailable. It never fabricates a pass. Any configured LLM
judge still runs, so this is judge-only mode, not no-op mode.
The optional bounded judge#
Set judge to add an LLM check after the deterministic ladder. judge: true
uses defaults. A dict may set the provider, model, system prompt, timeouts, and
invocation cap. The judge uses verify_completion_judge by default; set
judge_seam: "turn_end_condition" to use that completion trigger instead. The default
cap is 5 calls per session. Past the cap, the loop ends with status
completion_unverified. Set max_invocations: 0 to disable the cap. Two
helpers expose the catalog review and the resolved cap:
agent_completion_review(llm, opts) -> dict
agent_verify_completion_judge_cap(judge_cfg, review?) -> int | nil
agent_turn_end_judge_cap(judge_cfg) -> int | nil
agent_turn_end_judge_cap is exported by std/agent/turn_end, which owns the
turn-end condition end to end: whether one is configured, whether its bounded
judge is due at this boundary, and how its caps and invocation counts are
resolved. This module holds the product question "may this turn end?" and
nothing else; make check-turn-end-boundary fails if grading vocabulary
appears in it, or if an eval or bench module reaches into its internals instead
of reading the public result.turn_end_condition block off a finished session.
agent_completion_review reads the session model's catalog row and returns
{scrutiny: "standard"} when the row omits completion_review. The
verification-judge cap prefers an explicit max_invocations, then catalog
max_judge_calls, then 5. Both cap helpers return nil when the cap is
disabled.
What the judge is shown, and what it may refuse on#
The ladder's verifier reading is passed to the judge rather than discarded. The
evidence snapshot carries a typed
verification: {oracle_expected, command, observed, observed_at_evidence_index},
where observed is passed, failed, or not_run, and the judge's prompt
renders it as an explicit deterministic-verification block. not_run covers
both "no oracle was configured" and "the gate could not read facts", so a
facts_unavailable allow can never read as a passing verification.
The judge's verdict carries a gap_class alongside verdict and detail:
missing_artifact, unmet_manner_clause, failed_verification,
unresolved_authorization, or other. It is optional on the wire — an absent
or unrecognized value reads as other.
A continue naming failed_verification is converted to done when, and only
when, all four hold: the deterministic stage actually ran (the directive
receipt's invoked.deterministic), the threaded observed is passed, the
gate's reason is not facts_unavailable, and the named class is
failed_verification. The converted directive carries
converted_from: "failed_verification_contradicted_by_gate". Count conversions
by converted_from; a receipt's trigger names whichever adjudicator answered,
not the boundary that asked.
Every other gap_class vetoes exactly as before. The judge keeps sole authority
over artifact clauses, manner and negative clauses, and authorization, and loses
it only over the one question a deterministic oracle has already answered.
When the judge is not called at all#
Neither judge seam is called when the runtime already holds the answer. All seven of these must hold:
| Clause | Required reading |
|---|---|
| the gate ran | the directive receipt's invoked.deterministic |
| the gate allowed on a proven write | reason is exactly verified_after_write |
| the reading agrees with the reason | threaded observed is passed |
| the turn is the sealed final answer | stop_reason is sentinel |
| there is an answer to hand back | the final response is non-empty |
| nothing is deferred past the turn | pending_tool_batch_effect_count is zero |
| no acceptance row is outstanding | the ledger's pending_count is zero |
No provider call is made and no judge_started event is emitted. The directive
seals as accept with source: "gate" and
outcome: "skipped_verified_after_write", and invoked.turn_end_condition stays
false, so a reader can tell a judge that never ran from one that ran and agreed.
verified is deliberately not verified_after_write. A green verifier with no
source write behind it is an ordinary prose completion and still calls the
judge, as do an unverified path, a red or unrun verifier, a non-sentinel
boundary, and a run with no deterministic gate.
The acceptance-ledger clause is there because the skip does not only skip the
judge. The pending-requirements check that turns a done into a continue lives
inside the structured invocation, so a judge that is never asked takes the ledger
with it. A declared row can only be established as met by an assessment, so with
no assessment in hand every declared row is pending and the judge is asked. What
the clause never does is let the skip fire because a ledger came back met.
The judge cap reads the same ledger. A cap reached over a passing verifier
converts to stop_verified, which seals done; that conversion now also
requires the ledger to be clear, because a cap is a budget rather than an
assessment and the judge may have spent its whole budget refusing the row. With
no rows outstanding the conversion is unchanged and still reports
judge_cap_reached_over_verified_pass.
A done sealed without a judge now records an explicit empty ledger rather than
nil. nil reads the same for "no rows were declared" and "rows were declared
and nobody looked", and only the second is a false completion.
One predicate answers for both slots, read at the single point where the plan
decides whether to announce a judge. The catalog-declared completion_review
light-scrutiny skip is subordinate to it: that rule never reads the verification
at all, so on a typed green terminal it was deciding, from a model's catalog row,
a question the oracle had already answered. It now decides only what the
verification did not, and its receipt still carries source: "catalog" so a
reader can tell the two skips apart. The ledger clause applies to that skip too,
for the same reason: a skipped judge is a skipped pending-requirements check,
whichever rule did the skipping.
Projection receipts name their selected actions and resolved evidence roles in
selected_actions, so a host whose verifier declares no
completion_evidence_role — and whose passing verification is therefore counted
and then dropped from the bounded packet — is visible without arithmetic on the
counts.
See also#
- Agent guardrails — the input-side bookend that can stop before the first main model turn.
- Completion control
— the same gate in the context of the loop's other completion seams
(
verify_completion,verify_completion_judge,turn_end_condition). - Host-supplied facts — the broader Harn-owns-mechanism / host-owns-facts pattern this gate follows.
- Agent governors and detectors — the budget and stall side of the same placement contract.
- The expressiveness spectrum — where the completion gate sits when you compose loop control by hand.