# 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...

Website: https://harnlang.com/stdlib/agent-judge.html

This page documents Harn, which is pre-1.0. Language, standard library, and CLI APIs may change. If the intended version is unclear, clarify before using this page.

---

# 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`

```text
agent_completion_gate(runtime: HarnessRuntime, options: CompletionGateOptions = {}) -> dict
```

Spread the returned fragment into the loop's base options:

```harn,ignore
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 a `writes` list of `CompletionWriteFact` for the gate to classify.
  `verify` is one `CompletionVerifyVerdict` or 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?}`. `kind` is a `WriteKind`
  string; `"source"` and `"cosmetic"` are load-bearing, anything else is treated
  as non-source.
- `CompletionVerifyVerdict` — `{ok?, findings?, command?}`. `findings` is
  optional red-detail text. `command` optionally 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:

```text
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](./agent-guardrails.md) — the input-side bookend that can
  stop before the first main model turn.
- [Completion control](../llm/completion-control.md#completion-gate-agent_completion_gate)
  — the same gate in the context of the loop's other completion seams
  (`verify_completion`, `verify_completion_judge`, `turn_end_condition`).
- [Host-supplied facts](./fact-intake-seams.md) — the broader
  Harn-owns-mechanism / host-owns-facts pattern this gate follows.
- [Agent governors and detectors](./governors.md) — the budget and stall side of
  the same placement contract.
- [The expressiveness spectrum](../concepts/expressiveness-spectrum.md#level-4-tune-the-loop-by-composing-building-blocks)
  — where the completion gate sits when you compose loop control by hand.

---

## Read next

- [Agent guardrails](https://harnlang.com/stdlib/agent-guardrails.md)
- [Host-supplied facts](https://harnlang.com/stdlib/fact-intake-seams.md)
