How I Built a Task Spec Contract Between My Planner and Implementer Agents A developer building an autonomous coding system with Claude Code split work between a planner agent and fresh-context implementer sub-agents, but found that roughly one in three implementer runs solved the wrong problem because the natural-language handoff omitted context the planner considered obvious. The fix was a structured "task spec contract" — a schema the planner must emit and the implementer must echo back before editing code — which cut retries from 31% to 7% and also became the input for a verifier agent. My fully autonomous implementation system splits work between a planner agent and a fleet of implementer sub-agents that start with zero context. For months, roughly one in three implementer runs solved the wrong problem, even though the planner's prose instructions looked fine to me. The fix was not a better prompt. It was a task spec contract : a small, structured schema the planner must emit and the implementer must echo back before touching code. Retries dropped from 31% to 7%, and I got a bonus: the same spec became the input for my verifier agent. Here is the schema, the war story that forced it, and five lessons about agent-to-agent handoffs. Quick background. The system I have been building for about a year runs Claude Code 2.x as of September 2026 in a loop: a planner reads the repo and the backlog, breaks a goal into tasks, and hands each task to a fresh implementer sub-agent. The implementer edits, runs tests, and reports back. A separate verifier agent reviews the diff. Humans me only see the summary. The planner and the implementers do not share a context window. That is deliberate. Fresh context keeps each implementer cheap and focused, and it lets me run four or five of them in parallel. But it also means every task handoff is a cold start . Whatever the planner forgets to write down simply does not exist for the implementer. Early on, the handoff was a paragraph of natural language. Something like: Add retry logic to the webhook sender so transient 5xx errors don't drop events. Keep it simple. Reads fine, right? Here is what actually happened across a sample of 120 handoffs I logged in spring 2026: | Outcome | Count | % | |---|---|---| | ✅ Done, verifier approved on first pass | 68 | 57% | | ⚠️ Done, but scope crept or wrong file touched | 37 | 31% | | ❌ Gave up or produced nothing useful | 15 | 12% | The 31% row was the expensive one. Those tasks looked done. The implementer wrote a confident summary, tests passed, and only the verifier or worse, me, the next morning noticed that "retry logic" had been implemented as a brand new generic retry utility with its own config file, exponential backoff, jitter, a circuit breaker, and 400 lines of tests. For a webhook sender that already had a retry helper two directories over. The specific incident that made me stop and redesign the handoff: The planner emitted a task: "Fix the flaky date parsing in the export job." The repo had two export jobs. One was a legacy CSV exporter that nobody had touched in a year. The other was the active JSON exporter that had the actual flaky test. The implementer grepped for "export", found the legacy one first, "fixed" its date parsing by rewriting it to a different library, updated its tests, and reported success. The verifier approved because the diff was internally consistent. The actual flaky test kept flaking for three more days. Nobody in that chain did anything wrong given what they knew. The planner knew which exporter it meant. It just never said so, because to the planner it was obvious. That is the core failure mode of cold-start handoffs: the sender's obvious is the receiver's unknown. I stopped treating the handoff as a message and started treating it as an interface . If the planner and implementer were two services, I would never let them talk in free text. I would give them a schema. So I did. Every task the planner emits must be a single fenced block that validates against this shape: task id: T-2026-0914-03 goal: Make the JSON export job's date parsing deterministic so test export dates across dst stops flaking. why: The flaky test blocks CI ~2x/day; the root cause is naive datetime handling around DST transitions. scope: allowed paths: - src/export/json exporter.py - tests/export/test json exporter.py forbidden paths: - src/export/csv exporter.py legacy, do NOT touch - src/shared/ shared helpers need a separate task context: - "There is an existing tz helper at src/export/tz.py; use it, do not write a new one." - "The test currently fails ~30% of runs on the 2026-03-08 fixture." acceptance: - cmd: "pytest tests/export/test json exporter.py -x --count=20" expect: "all 20 runs pass" - cmd: "git diff --stat" expect: "only the two allowed paths appear" forbidden moves: - "Do not add new dependencies." - "Do not change the public signature of export json ." - "Do not create a new utility module." done signal: Reply with the exact acceptance command outputs, then the diff stat. If any acceptance check cannot pass, stop and report which one and why. Do not work around it. budget: max turns: 40 max minutes: 25 A few of these fields deserve explanation, because the obvious ones goal, acceptance are not where the value came from. scope.forbidden paths mattered more than allowed paths . Allowed paths tell the implementer where to look. Forbidden paths tell it where not to fix things, which is exactly the information that was missing in the date parsing incident. I now make the planner write at least one forbidden path for every task, even if it has to think hard to find one. The act of choosing what to fence off forces the planner to surface the ambiguity it was carrying in its head. context is for facts the implementer cannot discover cheaply. "There is already a tz helper" is a 10-second fact for the planner it just read the repo and a 15-minute discovery for a cold implementer, if it finds it at all. This field is where I put the things the planner "obviously knows." forbidden moves is for behaviors, not files. "Do not create a new utility module" kills the 400-line-retry-framework failure mode outright. My current top three forbidden moves, by how often they appear: done signal defines what the implementer's final message must contain. Before this, implementers reported success in prose. Now they must paste the acceptance command outputs verbatim. This one change made the verifier's job dramatically easier, because it could diff the claimed output against a re-run. The schema alone got me from 31% to about 18% scope failures. The second half of the fix was a mandatory echo-back : the implementer's first action, before reading a single file, is to restate the task in its own words in a fixed format. Task echo - I will change: src/export/json exporter.py, tests/export/test json exporter.py - I will NOT change: src/export/csv exporter.py, anything under src/shared/ - I am done when: 20 consecutive pytest runs pass AND diff stat shows only 2 files - Things I must not do: add deps, change export json signature, create new modules - Open questions: none If the echo does not match the spec, a tiny check script rejects it and the implementer gets one retry. If Open questions is non-empty, the task is bounced back to the planner instead of proceeding. This felt like ceremony when I added it. It turned out to be the single highest-leverage step, because roughly half the remaining scope failures were the implementer misreading a correct spec, and the echo catches those for the cost of one short turn. Here is the flow now: php flowchart LR P Planner -- |task spec YAML| V{Schema valid?} V -- no -- P V -- yes -- I Implementer