Handoff Contract (run-contract/v1)
The versioned protocol that binds the parent, worker, and human
roles across proving commands (prove, autoprove) and proof-editing agents.
It has two records — a dispatch record (parent → worker) and a handoff
record (worker → parent/human) — plus a rerun guard that stops a proving
mode from relaunching on the same blocker without new evidence.
This contract is documentation, not runtime code: it defines what the roles
exchange, not an enforcement engine. "Durable" here means each record is
serializable and transferable across a subagent, an inline pass, and a human
handoff — the same record shape regardless of who plays a role. Filesystem
persistence of these records is the separate storage primitive in
run-store.md (run-store/v1, #82A); prove/autoprove --persist
write their runs through it (cycle-engine.md § Run Persistence,
#82B) and cite stored items only as <run_id>#<seq>; prior-run reuse (#82C) records a
local source-note bridging to the prior run's <run_id>#<seq> citations —
Replan cites stay same-run — and carries the prior blocker forward
(cycle-engine.md § Prior-Run Reuse); resume
and inspect remain open under Issue #82.
The contract binds the logical roles, not processes: a host without subagents plays parent and worker inline in the main thread and satisfies the same records (see No-subagent fallback).
It does not invent vocabulary — it names pieces the cycle engine already uses:
the blocker signature, the
pre-flight dispatch block,
the file-baseline/v1
custody chain, the Blocked-Goal Triage classes from
sorry-filling.md, and the review stuck-mode next_action
enum.
Dispatch record (parent → worker)
The concrete instantiation is the Pre-flight Context block; this table is its canonical field contract. Every field is required unless a nullability is given.
| Field | Type | Notes |
|---|---|---|
schema |
const "run-contract/v1" |
record identity |
record |
const "dispatch" |
|
target |
string | file, file:line, or fully-qualified declaration |
scope |
enum sorry | deps | file | changed | project |
proving scope |
mode |
enum prove | autoprove | golf |
dispatching mode |
worker |
enum | null | the dispatched agent: sorry-filler-deep | proof-repair | proof-golfer | axiom-eliminator; null for an inline main-thread pass |
parameters |
object | worker-specific typed inputs ({} when none) — e.g. proof-golfer's search mode + candidate patterns, axiom-eliminator's axiom list, deep-mode safety budgets, proof-repair's structured error |
capabilities |
array of string | tools available to the worker (e.g. lean-lsp, search); may be empty |
owned_files |
array of string | the exclusive-ownership paths — ownership, not changes |
file_baseline |
file-baseline/v1 |
the single baseline record whose files array covers owned_files, computed by the parent immediately before dispatch (the primitive emits one object, not per-file entries) |
prior_blocker |
string | null | the preceding handoff's blocker_signature; null on a first dispatch |
evidence_delta |
array of string | the auditable evidence justifying this (re)dispatch — empty on a first dispatch; each entry names what changed (see Rerun guard) |
budget |
object {max_cycles: integer|null, max_stuck_cycles: integer|null, runtime: duration-string|null} |
null subfield = no explicit bound; runtime is a duration string (e.g. "120m") |
context |
object (below) | the pre-collected LSP starting state (MCP may be unavailable in a worker) |
context carries the pre-flight state — every member is required (never
omitted). Its two nullable members (prior_failure, goal_state) use null
when there is no data, the array members use [], and scratch_location is
always a non-null string (there is always a scratch dir):
context member |
Type | Notes |
|---|---|---|
prior_failure |
string | null | why the previous approach failed; null on a first dispatch |
goal_state |
string | null | lean_goal at the target |
diagnostics |
array of string | lean_diagnostic_messages, summarized |
search_results |
array of {tool: string, query: string, top: array of string} |
prior planning-phase searches |
candidates_tested |
array of {snippet: string, result: string} |
lean_multi_attempt outcomes |
code_actions |
array of string | lean_code_actions for relevant lines |
scratch_location |
string | e.g. /tmp (never repo root) |
Ownership rule: never dispatch concurrent workers with overlapping
owned_files; serialize or keep one in-thread. The single file_baseline
covers the owned set so the worker can check before every mutation and
advance only what it changed
(custody chain).
Worker parameters
parameters is a typed object keyed by worker ({} for an inline pass):
worker |
parameters shape |
|---|---|
sorry-filler-deep |
{fast_pass_error: string, permission_level: string, deep_budget: {scope: string, max_files: integer, max_lines: integer}} |
proof-repair |
{error: {errorType: string, message: string, file: string, line: integer, goal: string, localContext: array of string}} (the structured error the repair targets) |
proof-golfer |
{search_mode: "off" | "quick" | "full", golfable_patterns: array of string, candidate_targets: array of string} |
axiom-eliminator |
{axioms: array of string, permission_level: string} |
Handoff record (worker → parent/human)
Emitted at every stop or stuck boundary — compact enough for a human or another agent to consume in one pass.
| Field | Type | Notes |
|---|---|---|
schema |
const "run-contract/v1" |
|
record |
const "handoff" |
|
target |
string | null | echoes the dispatch target — makes the handoff self-identifying (the rerun guard's same_task reads it). null only in a protocol-error handoff reporting a malformed dispatch (nothing valid to echo). |
scope |
enum | null | echoes the dispatch scope (sorry/deps/file/changed/project); same malformed-dispatch nullability as target |
mode |
enum | null | echoes the dispatch mode (prove/autoprove/golf); same malformed-dispatch nullability as target |
status |
enum solved | stuck | stopped |
|
stop_reason |
enum | null | non-null iff status == stopped: max-stuck | max-cycles | max-runtime | user-stop | queue-empty | protocol-error | operational-error. null for solved/stuck. |
stop_detail |
string | null | non-null iff stop_reason ∈ {protocol-error, operational-error} (e.g. file-baseline drift, malformed dispatch, unavailable checker); null otherwise |
blocker_kind |
enum | null | why a blocker-driven stop happened: proof | false-statement | safety-guard | capability | protocol | operational. Non-null iff the stop was blocker-driven — see Blocker fields below. |
blocker_class |
enum | null | the proof-triage class, non-null iff blocker_kind == proof (sorry-filling.md): definitional-equality | missing-intro-constructor-cases | missing-rewrite | arithmetic | missing-library-lemma | typeclass-coercion-elaboration | needs-helper-lemma. null for a safety-guard/false-statement/etc. blocker (e.g. deep regression, scope exceeded, header-fence, rollback failure). |
blocker_signature |
string | null | the cycle engine's (file, line, primary_error_code_or_text_hash) signature (Stuck Definition). Same nullability as blocker_kind. |
attempted_tools |
array of string | tools/queries tried |
best_candidates |
array of {candidate: string, outcome: string} |
lemmas/tactics tried and how each fared |
failed_avenues |
array of string | approaches ruled out, so a rerun does not repeat them |
evidence |
object {queries: array of string, top_candidates: array of string, attempts: array of {snippet: string, result: string}, goal_delta: string|null, diagnostic_delta: string|null} |
the stuck-handoff evidence: LSP queries attempted, top candidate lemmas returned, lean_multi_attempt outcomes, and the goal / diagnostic change since dispatch (#73 requires reporting goal change; both are qualifying rerun-evidence classes) |
files_owned |
array of string | the ownership set held (echoes the dispatch's owned_files) — distinct from files_changed |
files_changed |
array of string | files the worker actually modified |
file_baseline |
file-baseline/v1 | null |
the final current baseline (adopt/patch rules below); null only in a protocol-error handoff about a malformed dispatch or unusable baseline |
artifacts |
array of {kind: string, content: string} |
worker products the parent consumes — a patch-only worker returns [{"kind": "unified-diff", "content": "..."}] (with files_changed: []); [] when there is no product |
next_action |
enum continue | deep | repair | redraft | golf | stop |
the shipped review stuck-mode vocabulary |
new_evidence_required_for_rerun |
string | null | what must change before a relaunch is justified. Same nullability as blocker_kind. |
Malformed dispatch. So that even an unparseable dispatch yields a valid
handoff, a protocol-error handoff reporting a malformed dispatch echoes
whatever it could parse and sets the rest (target/scope/mode,
file_baseline) to null; stop_detail names the defect. same_task is then
unevaluable, so the operational rerun branch governs the relaunch (a nonempty
evidence_delta resolving stop_detail). In every other handoff these fields
are non-null.
Blocker fields (blocker_kind, blocker_signature,
new_evidence_required_for_rerun) are non-null iff the stop was
blocker-driven — status == stuck, or status == stopped with
stop_reason == max-stuck. blocker_class is non-null only when
blocker_kind == proof (the seven proof-triage classes); a safety-guard
stop (deep regression, deep scope exceeded, header-fence violation, rollback
failure) or a false-statement/capability blocker sets blocker_kind
accordingly and leaves blocker_class null. All blocker fields are null
for status == solved and for every non-blocker stop — budget/user/queue
(max-cycles / max-runtime / user-stop / queue-empty) and operational
aborts (protocol-error / operational-error), which carry their cause in
stop_detail. A queue-empty stop with claims remaining therefore reruns freely
— no blocker_signature for the guard to match.
Custody vs effect. files_owned reports custody (echoing the dispatch's
owned_files); files_changed reports effect (what the worker wrote). The
file_baseline is the single file-baseline/v1 record for the owned set: a
direct-editing worker advances it after each mutation and returns the final
one, which the parent adopts as-is — re-advancing at handoff would bless
drift occurring after the worker's last check. A patch-only worker (e.g.
proof-repair) does not edit: it returns files_changed: [], the parent's own
file_baseline unchanged, and its diff in artifacts as
{"kind": "unified-diff", "content": "..."}; the parent then checks,
applies, and advances the patch itself.
Rerun guard
The predicate is evaluated from the two records — no future blocker is
guessed. The handoff echoes target/scope/mode, so the task triple is
self-identifying:
same_task =
new_dispatch.target == prior_handoff.target
&& new_dispatch.scope == prior_handoff.scope
&& new_dispatch.mode == prior_handoff.modeA relaunch is forbidden when all four hold:
same_task, andprior_handoff.blocker_signatureis non-null (the prior stop was blocker-driven), andnew_dispatch.prior_blocker == prior_handoff.blocker_signature(the same blocker), andnew_dispatch.evidence_deltais empty (nothing new to try).
The non-null condition is load-bearing: a non-blocker stop
(queue-empty/max-cycles/max-runtime/user-stop) has a null signature,
and a null == null match must not forbid the rerun — those reruns are
always allowed. A first dispatch carries prior_blocker: null and an empty evidence_delta, so
it is never forbidden. A qualifying evidence_delta entry is any of:
- a materially changed goal or diagnostic,
- an advanced
file-baseline/v1baseline (accepted new content), - a newly verified candidate lemma,
- changed source, or
- a newly available capability/tool.
When the predicate forbids a relaunch, route to review --mode=stuck,
formalize, or human handoff instead.
Operational and protocol stops. A protocol-error or operational-error
handoff has null blocker fields, so the signature predicate above never
applies — but it must not be relaunched blindly either (the same malformed
dispatch, unavailable checker, or unreconciled drift would just repeat). When the
prior handoff carries a task identity (target/scope/mode non-null), this
rule is scoped to same_task; a malformed-dispatch handoff with a null
identity has no task to compare, so it applies to any paired retry of that stop.
Such a stop may be relaunched only when the new dispatch's evidence_delta is
nonempty and describes how the prior stop_detail was resolved (e.g. baseline
reconciled, checker restored, dispatch corrected). An unrelated task is not a
relaunch. Normal queue/budget/user stops remain freely rerunnable.
This is the single definition of the rule; prove.md, autoprove.md, and
SKILL.md reference it rather than restating the predicate.
Human-in-the-loop
After a clear blocker in an interactive session, the parent presents options
(continue with new evidence / switch to formalize / review --mode=stuck /
stop and hand off) and never assumes autonomous continuation. The handoff
record is the artifact the human reads to choose — the same record a subagent or
inline worker would return.
Blocker-class vocabulary and the stuck review
/lean4:review --mode=stuck reports a Primary blocker class as human
phrases; the handoff record's blocker_class is their kebab-case enum:
| review phrase | blocker_class |
|---|---|
| definitional equality | definitional-equality |
| missing intro-constructor-cases | missing-intro-constructor-cases |
| missing rewrite | missing-rewrite |
| arithmetic | arithmetic |
| missing library lemma | missing-library-lemma |
| typeclass-coercion-elaboration | typeclass-coercion-elaboration |
| needs helper lemma | needs-helper-lemma |
The stuck review block is not itself a complete handoff record — it carries
no schema/record/status, no blocker_signature, and no custody fields. It
supplies the evidence and the blocker vocabulary; the parent wraps that
into a run-contract/v1 handoff record (mapping the phrase to the enum above).
See Also
- cycle-engine.md § Run Contract — roles, delegation expectations, no-subagent fallback
- cycle-engine.md § Stuck Definition — the
blocker_signature - cycle-engine.md § File baselines and drift —
file-baseline/v1custody - sorry-filling.md — Blocked-Goal Triage classes