DEV Community

Cover image for Your coding agents don't need more messages — they need a handoff contract
tia-forge
tia-forge

Posted on Originally published at hubwiz.com

Your coding agents don't need more messages — they need a handoff contract

I run several coding agents at once. The part that keeps costing me time is not the code they write — it is the note I hand from one to the next.

When I write that note by hand, I write what hurts: "CSV export loses the filters." That sentence is accurate and completely useless to an executor. It does not say which revision to start from, which command reproduces it, what output counts as correct, or who is allowed to say it is done.

That is the whole thesis of this post, and it comes from four October 2026 write-ups by people who hit the same wall. Everything factual below is theirs; I reproduced none of it, so treat the numbers as claims with sources. Sources and disclosures are at the end.

A ticket describes the symptom. A contract describes the job.

The author of the first write-up puts it in one line: a ticket can accurately describe a bug while leaving the executor unable to start.

His framing is more useful than a tool comparison, because it applies before any tool enters the picture: compare responsibilities, not labels. A task tracker can be part of a well-managed execution system. An agent runner can be badly managed. The difference is whether the handoff specifies four things.

Dimension The question it must answer How it usually gets faked
Context Which exact starting revision? Which behavior, constraints, allowed files? "Just change it on main"; a long conversation instead of a short, current spec
Runner Who is the authorized operator? Is the environment actually ready? Naming an agent when nobody installed the deps or can grant repo access
Evidence Patch revision, commands actually run, results, and untested cases? "done" — a state label, not a verification record
Acceptance Who decides the result satisfies the requirement? The executor quietly swapping the requirement for an easier reading

Note the asymmetry: three of these four are things an agent cannot invent for itself. Acceptance in particular has to be a person or a role that is not the implementer, which is why "the agent said it was done" is not a verification story.

The template

The original template is for a CSV repair. Here it is generalized, with the author's own warning intact: values in angle brackets must come from the real project, and command names must not be invented.

task:      Preserve the selected filters in CSV export
context:
  repository:    <authorized repository>
  base_revision: <verified commit>
  reproduction:  Select status=open, then export CSV
  expected:      Export contains exactly the rows matching the filter
  fixtures:      <sanitized fixture path>
scope:
  allowed:  Export query and focused regression coverage
  excluded: Authentication, schema migrations, unrelated UI changes
execution:
  operator:      <developer or authorized agent operator>
  setup_command: <verified command from this repository>
  test_command:  <verified command from this repository>
  permissions:   Existing authorized development access only
delivery:
  patch_revision:        required
  commands_and_results:  required
  sample_output:         sanitized CSV from the fixture
  untested_cases:        required, even when empty
acceptance:
  product_owner: Confirms filtering semantics
  engineer:      Reviews implementation and regression evidence
Enter fullscreen mode Exit fullscreen mode

Three rules ride along with it, and the second one is the one people break in a hurry:

  1. Do not paste credentials into the contract. The executor uses their own authorized tools and access.
  2. An unavailable environment is an execution blocker, not an invitation to share an account.
  3. The template is a proposed operating practice — not evidence that any platform fills in every field automatically.

untested_cases: required, even when empty deserves its own sentence. Empty and "none" are different claims: the first is missing information, the second is a checkable statement.

What one handoff actually looks like

The walkthrough in the source is worth keeping whole, because it shows where a human belongs:

  1. The product owner builds a fixture: three records, two open and one closed. The expected filtered export is exactly two rows.
  2. An engineer confirms the behavior and points at the export path.
  3. The executor first reproduces the failure at the recorded revision. If it does not reproduce, the next useful delivery is not a patch — it is the attempted command, environment details, and observed output. Editing before resolving that mismatch risks fixing a different problem.
  4. Only then: a scoped change, plus coverage for the filtered export. The unfiltered export gets checked too, because repairing one path can silently narrow the other.
  5. The delivery attaches the patch revision and the real output of the verified test command.
  6. Two reviewers, separately: the engineer reads the diff and the regression evidence; the product owner looks at the sanitized sample and confirms that "filtered" means what users think it means.
  7. If either review fails, the same task records the reason and the next attempt.

Honest note from the original: those are proposed checks. No passing results were claimed. That restraint is part of the standard, not a weakness of the article.

The portable version: five fields, five sections of evidence

The follow-up post targets a messier reality: one teammate uses a terminal agent, another uses an editor assistant. What transfers between them is the task contract — inputs, allowed changes, required evidence, a reviewer. Conversation history is optional context.

outcome:    reject invalid worker-count configuration before startup
scope:
  allowed:  config parser and focused parser tests
  excluded: deployment, credentials, queue architecture
inputs:
  revision:     record the real base commit
  config_key:   replace with the actual key
  reproduction: supply a synthetic invalid value
acceptance:
  - omitted value preserves the current default
  - valid positive integer preserves behavior
  - zero, negative, and non-integer values are rejected
  - error names the key without printing secret configuration
stop_when:
  - the parser contract conflicts with documented behavior
  - the change requires a production configuration update
  - unrelated failures prevent a trustworthy comparison
Enter fullscreen mode Exit fullscreen mode

stop_when is the field I stole immediately. It gives the executor permission to stop and a reason to record, which is exactly what you want from an autonomous worker that is about to guess.

The evidence protocol is five lines:

Before:      command, exit code, observed output, base revision
Change:      files and the reason each file is necessary
After:       same reproduction, focused tests, actual exit codes
Uncertainty: checks not run, and why
Review:      commit reference and named acceptance owner
Enter fullscreen mode Exit fullscreen mode

Two sentences from that post are worth pinning above a desk:

"The model says validation is fixed" is a hypothesis. A command and its output are evidence that a check ran.

…and the reviewer still has to judge whether that check covers the intended failure.

Two gates that get skipped:

  • Eligibility comes before capability. Access is a gate: the runner must already be authorized. A teammate with a strong model and no repository permission is not eligible just because someone could paste them a token.
  • Honest non-results. In the article's example the executor discovers that a worker count of zero deliberately means automatic sizing. The correct move is to stop, return the fact, and revise the requirement — not to change working behavior to satisfy an invented test.

Pre-acceptance checks: verify the recorded revision, reproduce the named cases, look for accidental configuration disclosure in the error message, and confirm no deployment or unrelated cleanup slipped into the diff.

Making the handoff an object (and holding the writes)

The third write-up takes the opposite route: no template, a document model.

Its observation is blunt — passing messages between agents is straightforward; keeping track of requirements, decisions, results, validation, and unfinished work is not. A research agent handing off to a coding agent cannot send a summary: the receiver needs the original requirements, the constraints, the decisions already made, the results, the open questions, and the next steps.

So Work Order in, Work Report back, with one model and one set of validation rules behind three entry points — a TypeScript package, an MCP server (so Claude Code and friends can create/accept/report/review through tools), and a Python implementation that runs the same shared test cases and reports the same findings for the same documents.

The interesting part is the MCP gateway in the middle. The worker does not touch real tools directly; every call goes through the gateway, which enforces that order's allowed tools and budget on each call and records the call as evidence.

Version 0.7 added a side-effect gate — staged tools:

Staged tools: the worker's reads run straight through the gateway, but its gmail/send call is held, logged, and answered with a placeholder; the worker lists it in the report; on approve the gateway runs it once, in order

Reads pass through, writes are held, and only an approval makes them run — once. (Image: the source post at dev.to, rehosted.)

Mechanics worth copying:

  • Reads run normally. Only staged calls wait, so the worker can still research.
  • Writes are held. The gateway counts and logs the call with its arguments but does not forward it; the worker gets a placeholder like pending:gmail/send#3 and lists it under effects in its report.
  • The manager decides. Approve → the gateway runs the held calls once, in order. Send it back → they are dropped, and nothing has to be undone because nothing ran.

Four details the author says only showed up in real workflows:

  1. Partial approval. Approve the work but run only chosen effects: effects: ["gmail/send#3"].
  2. Dependent effects. A later held call can reference an earlier one — {"$ref": "github/create_issue#3", "pointer": "/structuredContent/number"} — and the gateway fills it in at execution time.
  3. At most once, by reconciliation. Before running anything, the gateway checks every effect in the report against what it actually logged. Mismatch → nothing runs. Re-running --commit never double-sends, even after a crash.
  4. Sub-agents cannot route around it. A worker cannot hand a staged tool to a sub-agent: the sub-agent's writes would execute when the worker approves, before the manager has seen anything.

There is also progress() for in-flight notes and trackRecord() — a worker's history of first-time approvals, rework, failures, expiries, and budget spent. Judge a worker by that, not by its self-report.

Disclosure: the author is building these packages, so read it as "someone doing the thing", not neutral review.

The ledger: derive health, never store it

Writing a contract solves what to hand over. The Agent Board write-up solves the other half: whether you can still see the whole board afterwards.

The problem statement is three questions you cannot answer when agents run in parallel: is it still working, is it stuck, and how much of the subscription is it burning? The original requirement was blunter — one always-accurate page. The author then names why that is hard: this is a distributed systems problem. Agents start and die outside any single process, report asynchronously, and lie by omission when they crash.

His answer: the board is a projection; the ledger is the only source of truth. Four invariants:

  • Register before spawn. The task row exists before the process does, so nothing started through the supported path can run unrecorded.
  • Unknown stays unknown. No estimated percentages, no inferred exit codes, no guessed usage. A Claude session without a reliable PID shows unknown and its Stop button is disabled.
  • Derive, do not store, health. Status is a fact written by a producer; health is computed at read time from status plus idle time.
  • Never block the agent. Instrumentation fails open: a busy ledger drops an event rather than adding latency.

Three producers, one append-only ledger

  • Launch wrapper. Writes the row, then spawns the agent in JSON-event mode; heartbeats every 15 s while the child lives; records the exit code; a spawn failure is recorded as failed with exit 127 instead of vanishing; signal forwarding is installed before the prompt is sent, so stop reaches the child. A test asserts the ordering: a fake agent blocks before its first event while the test polls the ledger and asserts the row already exists.
  • Lifecycle hooks. Session start/stop, prompt submit, helper start/stop, tool events before and after each call. Lifecycle hooks run synchronously, tool hooks asynchronously. Each hook gets a 25 ms lock wait and a 150 ms work budget, targeting an exit under 200 ms; on timeout it logs and drops the event; it always exits zero and never writes to stdout — anything printed would be injected into the agent's context.
  • Reconciler, every 5 s. A running task whose process is gone with no exit becomes lost. A running task idle for more than 600 s becomes stalled, flipping back on the next event. It also scans the process table for Codex processes that never went through the wrapper, walks up to three parent levels to avoid double counting, and records them as untracked, keyed by PID.

Seven recorded statuses and four derived health bands

Seven statuses; health is a read-time function, so the board cannot drift from the ledger. (Redrawn from the source's section 5.)

Progress exists only when there is a denominator

My favourite rule in the whole write-up: percent complete is shown only when a denominator was declared. Runs launched with a step count ask the agent to emit PROGRESS n/total, parsed from messages; Claude tasks report checklist items; everything else says "progress not reported". Even then it is the agent's own claim — the timeline is the evidence.

Admission control: one writer, two slots

Concurrency per repository is bounded by kernel advisory locks, with the lock root derived from a hash of the checkout path. A writing run takes an exclusive writer lock, then one of two slot locks, tried without blocking. No sentinel files, no sleeps, no polling loops — a crashed holder releases when the kernel closes the file.

The cap of two came from evidence, not taste: on day one, with everything at high effort, the Codex weekly allowance dropped to 11% left, and a third concurrent stream stalled while two ran. Runs also carry a token budget — 40 million by default — counted from the agent's own token events; over budget, the run is stopped and exits 125.

What the ledger actually recorded (2026-09-08 → 10-23)

Measure Value
Task rows / events 28,149 / 540,175
By source 8,331 Codex, 7,267 Claude sessions, 12,373 Claude helpers, 178 other
Wrapper-launched Codex runs 1,380 done, 116 failed, 267 lost
Codex processes found outside the wrapper 6,568 — all ended as lost
Mean wall time, done runs 7.95 min over 803 runs
Review verdicts 160 APPROVE, 337 FIX, 87 INVALID
Tests 188 methods in 22 Python files + 12 browser scripts

The largest number is the useful one: most Codex rows were never launched by the wrapper at all. They are processes the reconciler caught running on their own, recorded as untracked, then lost. That is the detector working — and a measure of how much agent activity is otherwise invisible.

Five failures worth stealing

  1. The kill chain reaction. Killing a reviewer produced a signal exit → parsed as INVALID → triggered the fix loop → spawned a new worker and a new review. Each kill produced two or three more tasks. Fix: a reviewer stopped by a signal ends the chain with a STOPPED verdict.
  2. Budget burn, itemized. Day one: 36 tasks, ~24 agent hours in 36 hours, all high effort. The breakdown: re-reading context ~25%, oversized command output 18%, repeated aggregate verification 12%, high effort on routine work 12%. Roles, bounded packets, a release window and the two-run cap all came out of that.
  3. Phantom helpers. Helpers that never closed showed as stalled for hundreds of hours and flooded the view. Aggregation by project plus a 15-minute idle cutoff replaced one sprite per helper.
  4. Lock contention. Builds in the board's own repo failed with database is locked while a rendering service held the ledger. Operating rule: pause that service during builds.
  5. Ready before verified. UI work was reported ready before anyone opened it in a real browser with real data. New rule: no UI is ready until someone has looked at a screenshot.

The review gate: packet, grammar, one fix round

Implementation runs as the role worker, followed by an isolated review. The reviewer gets a packet, not a conversation: the task-local diff, at most 200 lines of test output, the worker report, and the path to the original brief — inside a 256 KB budget. When the budget is tight, unchanged context and non-test file bodies are dropped, with recovery paths the reviewer can open.

The verdict grammar is strict: exactly APPROVE, or a numbered list of fixes. Anything else is classified INVALID. A FIX returns to the worker once; a second review follows; still unresolved → exit 2, surfaced in a human queue. Nothing escalates automatically. Reviewers can be two different models, in which case both must approve and disagreements are labelled.

584 verdicts on record: 160 APPROVE, 337 FIX, 87 INVALID. Fix requests outnumber approvals two to one — and the author says that is the point of the loop, not a bad sign.

The human queue is computed, not curated, from five sources (tasks failed in the last two hours, reviews still asking for changes, manually raised needs, needs raised by a live Claude session, work items waiting on the owner), with three suppression rules (acknowledged after the latest signal; a later run in the same directory or with the same title succeeded; dedupe per task, keep the newest signal). Display cap: five per lane. In the author's words — needs you, maximum five, real and current only.

Where the "discussion" part goes wrong

Handoffs are half the problem. The other half is agents disagreeing about a plan.

A vendor write-up (CEO's column, so read it as an argument) describes a consensus cycle: agents share draft plans, surface conflicts, revise, repeat. It settles many disagreements — and when it cannot, the system escalates to a human. Then the important observation: the safe thing has a cost curve. One team escalating occasionally is governance; fifty teams escalating every unresolved disagreement is a flooded queue, and flooded people approve by default. That is automation bias (Parasuraman & Manzey 2010; the EU AI Act names it in Article 14). A rubber stamp and a working control produce the same output while conditions are normal.

So the constraint is not agent capacity — that is cheap and getting cheaper. It is the handful of people who understand the operation well enough to settle an argument.

The triage that follows is the part I actually use. Take the last twenty decisions your team pushed to a person and sort them:

  • Pile one: the parties disagreed about what will happen — different data, different history, different confidence in the same lever. This pile does not need a person's judgement; it needs both parties to see the same evidence.
  • Pile two: they agreed on what will happen and disagreed about what to want — throughput against energy cost, this customer's order against that one's. This pile is where a person's day should go.

If your queue is mostly pile one, you are running an evidence problem through a governance process, and adding approvers will not fix it.

The proposed split is clean: agents strategize, mathematics closes the deal. Each agent decides only two things — eight weights over delivery slots, and a walk-away floor:

U(o) = w1·u1(o1) + w2·u2(o2) + … + w8·u8(o8)   accept only if U(o) ≥ r
Enter fullscreen mode Exit fullscreen mode

The agreement itself is produced by a bargaining engine running alternating offers. No language model makes an offer, concedes a point, or declares a deal; the same positions in produce the same agreement out, auditable to the individual offer, settling in about a second — and when there is genuinely nothing to trade, the agents walk away, which is also correct.

That gives you a third stage between "force consensus" and "page a human": a mediator proposes candidate settlements, filtered so nothing breaching a constraint is ever offered; each agent judges only whether a candidate beats the one on the table, and any agent can veto — without revealing its weights. What reaches the person is not two drafts and a deadlock notice, but a short list of what could not be settled and who vetoed what.

The companion piece on roles adds four short rules that keep a "team" from becoming a demo:

  • A role is one job and one checkable output. "Research competitors and return a claims table with a source column" is a role. "Help with marketing" is not.
  • Boundaries belong in tools, not prompts. A reviewer reads the diff and posts comments; it cannot merge. Instructions can be ignored or misread; a missing tool cannot be used.
  • Make the handoff explicit: name the artefact, the reason it moved, and what the receiver must check — and log the handoff, so you can see where a decision changed hands.
  • Cap the coordinator: max reassignments, a time limit, and a rule for what happens after a task fails twice. It should also point at evidence per task, not "all complete".

And the sequencing advice I keep repeating to myself: start with two roles and one handoff, and get it boring before adding a third.

Putting the three layers together

Three layers: handoff contract (revision, commands, expected output, acceptor, open items), ledger (append-only, health derived), gates (packet, APPROVE or numbered fixes, one fix round, then a human)

Handoff contract for the next agent; ledger for the machine; gate before side effects. (Drawn by the author from the four sources above.)

Layer Artifact Written by Read by If missing
Handoff HANDOFF.md (five fields + five evidence sections) Upstream owner or upstream agent The next agent's start script The next agent guesses, or asks you — which is where we started
Ledger tasks.tsv or SQLite, append-only Wrapper, hooks, reconciler Board, gates, coordinator No answer to "who is running, stuck how long, spent what"; crashes lie by omission
Gate Review packet + verdict + approval Independent reviewer or authorized human Merge, deploy, anything with consequences You reject the report after the email has already gone out

Three things actually get automated here — none of them is "make the model smarter":

  1. Generate the contract at the end of the upstream task: template plus a validator that checks the four fields are present, acceptor ≠ implementer, and base_commit actually exists.
  2. Consume the contract at the start of the downstream task: the launcher injects HANDOFF.md into the first turn, instead of you pasting it into a chat box.
  3. Gate before the side effect: not "reject the report afterwards", but "hold the write until approved". That is exactly why staged tools matters — a rejection leaves nothing to undo.

A three-week rollout with pass/fail criteria

Week 1 — contract plus validator. HANDOFF.md in the repo, a ~30-line script. Pass: ten consecutive tasks where no downstream agent comes back asking for missing information.

Week 2 — wrapper plus reconciler. Register the row before spawn; heartbeat every 15 s; mark lost when a PID disappears without an exit; mark stalled past 10 minutes idle. Pass: you can answer "who is running, stuck how long, spent what" at any moment, and unknown was never silently filled with 0.

Week 3 — reviewer plus grammar plus one fix round. Packet, not conversation; verdicts limited to APPROVE or a numbered list; a single fix round; a human queue capped at five. Pass: the fix/approve ratio is stable, and the queue stays under five — if it does not, fix the process before adding approvers.

The discussion layer goes after week 2: give plans an independent read-only reviewer, cap the loop at two rounds, and escalate — otherwise you build the flooded approver from the previous section.

Cautions

  • The ledger only covers runs that go through the wrapper. Those 6,568 outside processes are the proof: you must assume a share of activity is invisible.
  • Heartbeats prove liveness, not progress.
  • Lifecycle hooks cannot get a reliable PID for Claude sessions, so the board cannot stop them; progress counts are self-reported; budgets come from the agent's own token events.
  • A strict verdict grammar has a price: 87 of 584 verdicts were INVALID (~15%). Watch what INVALID looks like, or you will fix formatting instead of behaviour.
  • Locking collides with builds (database is locked); the rule is to pause the lock holder during builds.
  • "Ready before verified" is the most common self-deception; for UI, nobody has verified it until someone looked at a screenshot with real data.
  • Never put credentials in a handoff contract.
  • The four fields prevent "cannot start". They do not prevent "did it wrong" — that needs the evidence package and the acceptor. Do not merge the two jobs.
  • Thresholds quoted here (256 KB packet, 200 log lines, 15 s heartbeat, 10 min idle, 40 M tokens, two slots) are one operator's values, not universal standards — recalibrate for your repo and budget.

Sources

Disclosures. The first two posts disclose AI assistance in drafting and state that the worked example was not run against a production repository, and reference a vendor's product page for the workflow model. The third is written by the author of the packages it describes. Agent Board is one operator's single-machine record (its own limitations section applies). The negotiation piece is a vendor CEO's column. Nothing here was independently reproduced by me.

A longer Chinese version with the field-level contracts, the seven-state machine and the full failure list lives at hubwiz.com/blog.

Top comments (1)

Collapse
 
sherotree profile image
Sherotree •

The stop_when field is the part I keep stealing. Giving an agent an explicit reason to halt beats another round of "please continue" when the requirement itself turned out wrong.