done until a check passesharness, no loop, no graph. Those remain ADR 0016/0017's and the
API-options note.done when the model stops asking for tools. Nothing checks
whether the work is actually finished. "Don't tell me it's done until the tests pass" is the most
requested guarantee a harness can give, and toolnexus cannot express it.docs/spikes/0010-completion-verifier.mjs, run against the shipped client and §7D
runtime.js only. The delegation finding (below) rests on §7D, which
is normative for all seven ports, so I expect it to hold everywhere — but it is an argument from
the contract, not seven measurements. My weakest claim is the failure-feedback shape: the
spike feeds the reason back as a plain user message, which is the obvious choice and not
necessarily the right one (see Open questions).Hooks has four members (js/src/client.ts:128-137). beforeLLM can rewrite messages and tools;
beforeTool can short-circuit a tool; afterTool can replace a result. afterLLM is
observe-only — its return type is void | Promise<void> (:132). There is no way for a host to
say "this run is not finished, keep going". The done-decision is closed.
Spike Q1: a host wraps run() in its own retry loop, threading history and appending the failure
reason. It works:
works: true | verified on attempt 3
Any proposal must therefore justify itself on something other than "otherwise impossible".
Spike Q2. A host-side retry loop lives at the call site. §7D's task tool fuses
spawn→wake→wait→close inside a parent's turn — the host is not there. So when a parent delegates
to an agent whose completion the host was guarding:
child run status: done | text: "patch v0"
did ANY verification happen inside the delegated run? NO
The child claims done on its first, unverified attempt, and the parent believes it. A guarantee
that evaporates under delegation is not a guarantee — and delegation is the case toolnexus exists
to make cheap. Declared on the agent, the gate travels with it: every caller gets it, including
task, including an A2A peer calling in through serve.
Spike Q3, both directions:
passing case : status=done attempts=3 stoppedBy=null
failing case : status=incomplete attempts=3
stoppedBy="completion.verify failed 3×: still red"
Add one optional member to the agent/client options. Nothing else moves.
completion?: {
verify: (run: RunResult) => { ok: boolean, reason?: string } | Promise<…>
maxAttempts: number // REQUIRED — no default; an unbounded gate is a runaway loop
}
Six rules, and they are the whole contract:
done. Never on pending,
incomplete, interrupted, closed, timeout or error — those already have a reason, and
re-gating them would let a verifier override a budget stop or a suspension.{ok, reason} and does exactly two
things: continue, or stop. It has no idea what "verified" means, so the loop stays
domain-neutral — the property that keeps it portable across seven languages.reason, and the run continues. The agent
gets to fix its own work; that is the entire value over a host-side assertion.status:"incomplete" with the reason
named. No new status string — SPEC.md:785-787 pins TaskStatus as identical across ports,
and a seventh value would need the same pinning and the same fixtures for no gain.pending (blocked on a human) and incomplete (blocked on a check)
collapse into one status, and a caller can no longer tell whether it owes an Answer or a code
change.Excluded — harness, loop, graph. This ADR adds an option, not a vocabulary. The placement
rule those discussions produced ("put each thing at the scope where it varies") is what puts
completion on the agent rather than on a call: it varies with the problem, not the invocation.
Excluded — a protocol/invariant verifier. ADR 0016's revision proposed loop.verify() over the
transition trace. On reflection it has no user I can name: port implementers already get it from the
276 conformance traces, and no host has asked. The discipline it borrowed — properties are named,
and an absence carries its reason — belongs in the conformance suite, not on a public API.
Deferred — verification as a graded score rather than a boolean. Real review is not binary. But
a threshold is a domain decision, and encoding one in the loop is exactly the domain-awareness rule
3 exists to prevent. A host that wants grading returns ok: score > 0.8 today.
maxAttempts is required, so the pathological case — a verifier that
never passes — costs a known number of turns and stops with its reason named.incomplete. Worth stating
in the spec so nobody is surprised by the interaction between two limits.stoppedBy becomes worth having. §7D already requires a limit stop to be loud and named, but
the reason is prose in text. A field is the small, useful version — and it serves budget stops
as much as this.SPEC.md §8 should say plainly that the done-decision is closed and a host
wanting a completion guarantee must own the loop itself — and that such a guarantee does not
survive task delegation. That last sentence is the one worth writing regardless of the
outcome, because the failure mode is silent.Q1 — What does the verifier see? — ANSWERED (owner, 2026-08-16): plan/todo/goal state.
The spike passes the whole RunResult, which is enough for "did it answer" but not for "is the work
finished". The resolution is not to widen the input but to give the agent structured task state
and verify against it: not done until every todo is checked off.
This is better than a workspace handle for the reason that governs this whole design — it is a structural check, not a domain one. The loop counts unchecked items; it never learns what a todo means, so rule 3 (the loop never interprets the verifier) holds by construction. It also means the library ships a useful default verifier instead of a gate that every host must supply domain code to make meaningful.
Consequence: plan/todo/goal stop being speculative primitives and become the substrate this ADR depends on. A host may still close over anything it likes for a domain check — that path is unchanged and needs no defined input.
Q2 — How is the failure fed back? The spike appends a user message. That is the obvious shape
and it is load-bearing for parity, since seven ports must produce identical bytes. It also interacts
with the canonical-transcript change (openspec/changes/add-canonical-transcript), which is
re-plumbing exactly this representation — so this should land after it, or agree with it.
Q3 — Does the gate re-run after a durable resume? A run that suspended, was answered, and now
completes: does it verify? I think yes, since it is reaching done for the first time — but §10's
rewind-to-checkpoint interacts here and it needs pinning.
Per the prime directive this ADR is the discussion, not the change. If accepted, the work is a small
OpenSpec change against a new completion-gate capability (or agent-runtime), with the six rules
as requirements, the absent-option byte-identity as the first conformance scenario, a shared fixture
whose verifier fails twice then passes, and a seven-port parity checklist.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |