What you’ll leave with
A concrete ownership plan, a shared request/response contract, four executable contract checks, and an integration procedure.
Before you begin. Familiarity with Git and basic JavaScript. The contract exercise requires Node.js 22+. Worktree commands are optional examples for a repository you control; no agents are launched by this guide.
Parallel agents can create code faster than one person can inspect it. That makes coordination a design problem of its own. If one contributor names a field url and another expects href, both may finish their individual tasks with passing tests while the combined feature does not work.
Our example gives one contributor the reading-list API and another the interface. They can work independently only after they agree on the boundary. We will write that agreement, demonstrate a test that catches a mismatch, and decide what the integrator must verify after both handoffs arrive.
The example assumes contributors are already available and authorized. It does not depend on a particular agent product or team feature. Two people, two agents, or a person and an agent can use the same contract. The amount of parallelism follows the independence of the work, rather than the number of sessions a tool can open.
1. Check whether there are really two independent jobs
List the decisions that must be made before implementation: the saved-item shape, the endpoint, the duplicate rule, and the error behavior. Those are shared decisions. Asking two contributors to discover them separately creates negotiation disguised as parallel progress.
Once the boundary is stable, the API can implement validation and storage while the interface uses fixture responses. The interface does not need a live database to render a duplicate outcome. The API does not need finished typography to verify that a failed write returns no success. That is a useful split because each side can produce reviewable work without editing the other's files.
Do not create a third independent “test agent” if its first task is to guess what the other two are building. Contributors should test their own behavior, and the integrator should test the combined journey. A separate testing task becomes useful when its inputs and acceptance cases already exist. Otherwise it adds another interpretation of the same ambiguous request.
- IntegratorApprove the brief, contract v2, and file ownership.
- API + interfaceBuild separately against the same committed examples.
- IntegratorReview each handoff and combine one change at a time.
- Whole featureExercise the real route, storage, and visible recovery together.
2. Freeze behavior, not just field names
This exercise covers the POST boundary of the broader save/list workflow. The request supplies url and title. A new save returns 201 with created:true and data containing id, url, and title; a duplicate returns 200 with created:false and the existing data. Use a stable nonempty ID and a nonempty title of at most 120 characters. Invalid input and unavailable storage have separate error fixtures. Authentication, account ownership, listing, and pagination remain interfaces to define and test before building a multi-user service.
Field names are only the surface of the agreement. The API must persist before returning created. A duplicate must not write a second row or silently replace its stored title. The interface must not erase the input on storage failure. Record these semantics alongside the JSON examples because a response-shape validator cannot prove them.
Version the contract in the same repository as the implementation. Both contributors should start from the commit that contains it. A message saying “we agreed on v2” is weaker than a file they can import and a fixture they can execute. If a field must change later, update the contract first and tell both owners which behaviors need adjustment.
{
"version": 2,
"method": "POST",
"path": "/api/reading-list",
"request": {
"url": "https://example.com/guide",
"title": "Example guide"
},
"responses": [
{
"status": 201,
"body": {
"data": {
"id": "item-001",
"url": "https://example.com/guide",
"title": "Example guide"
},
"created": true
}
},
{
"status": 200,
"body": {
"data": {
"id": "item-001",
"url": "https://example.com/guide",
"title": "Example guide"
},
"created": false
}
},
{
"status": 400,
"body": {
"error": {
"code": "invalid_url",
"message": "Enter a complete HTTP(S) URL."
}
}
},
{
"status": 503,
"body": {
"error": {
"code": "storage_unavailable",
"message": "Could not save. Try again."
}
}
}
],
"identity": "Canonical HTTP(S) URL with fragment removed; preserve path and query; no embedded credentials",
"order": "Persist before returning 201; duplicate returns existing item without another write",
"scope": "This runnable contract exercise covers the POST boundary in the broader save/list workflow. Authentication, account ownership, listing and pagination are not implemented here; define and test those interfaces before using this exercise as a multi-user service."
}3. Make disagreement fail locally
A shared check can catch a renamed field before the UI and API meet. The function below verifies the response's exact keys, status/created agreement, and canonical HTTP(S) URL. Both contributors can use it against their response or fixture. It remains a teaching validator rather than a full API schema library.
The important test is the intentional mismatch. If the API returns data.href, the check rejects it even though the value looks like a perfectly good link. Another test rejects created:true paired with status 200. This is useful because a contributor may reasonably choose either convention in isolation; the check protects the convention the team actually agreed on.
export function assertSaveResponse(status, body) {
if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("body");
const keys = value => Object.keys(value).sort().join(",");
if (status === 201 || status === 200) {
if (keys(body) !== "created,data" || body.created !== (status === 201) ||
!body.data || typeof body.data !== "object" || Array.isArray(body.data) ||
keys(body.data) !== "id,title,url" ||
typeof body.data.id !== "string" || !body.data.id.trim() ||
typeof body.data.title !== "string" || !body.data.title.trim() || body.data.title.length > 120 ||
typeof body.data.url !== "string") {
throw new Error("success_contract");
}
const url = new URL(body.data.url);
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash || url.href !== body.data.url) {
throw new Error("canonical_url");
}
return;
}
const errorCode = { 400: "invalid_url", 503: "storage_unavailable" }[status];
if (!errorCode || keys(body) !== "error" || !body.error || typeof body.error !== "object" ||
keys(body.error) !== "code,message" || body.error.code !== errorCode ||
typeof body.error.message !== "string" || !body.error.message.trim()) {
throw new Error("error_contract");
}
}
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { assertSaveResponse } from "./contract-check.mjs";
const contract = JSON.parse(readFileSync(new URL("./contract.json", import.meta.url), "utf8"));
const data={id:"item-001",url:"https://example.org/article",title:"Example article"};
test("all four fixture responses and the existing workflow success shape are accepted", () => {
for(const response of contract.responses) assert.doesNotThrow(()=>assertSaveResponse(response.status,response.body));
assert.doesNotThrow(()=>assertSaveResponse(201,{data,created:true}));
});
test("renaming a field or reusing the old envelope is rejected", () => {
assert.throws(()=>assertSaveResponse(201,{data:{id:data.id,href:data.url,title:data.title},created:true}));
assert.throws(()=>assertSaveResponse(201,{item:{url:data.url},outcome:"created"}));
});
test("HTTP status and created flag must agree", () => {
assert.throws(()=>assertSaveResponse(200,{data,created:true}));
assert.throws(()=>assertSaveResponse(201,{data,created:false}));
assert.throws(()=>assertSaveResponse(500,{error:{code:"storage_unavailable",message:"Retry"}}));
});
test("unsafe or noncanonical links and missing display identity are rejected", () => {
for(const url of ["javascript:alert(1)","https://example.org/article#x","https://me:secret@example.com/","https://EXAMPLE.com/"]) {
assert.throws(()=>assertSaveResponse(201,{data:{...data,url},created:true}));
}
assert.throws(()=>assertSaveResponse(201,{data:{...data,id:""},created:true}));
assert.throws(()=>assertSaveResponse(201,{data:{...data,title:""},created:true}));
});
4. Give each contributor a bounded task pack
An effective task assignment names the deliverable, owned files, dependencies, forbidden overlaps, and completion evidence. “Build the backend” is too broad when the backend shares schemas, migrations, and package files with the interface. Assign those shared files to one owner and let contributors propose changes instead of racing to edit them.
The task pack below uses example paths. Map them to the actual repository before dispatching work. If there is no storage adapter yet, that dependency belongs in the plan; it is not an invitation for the API contributor to introduce a second database library. Similarly, an interface change that requires global styling should be surfaced to the integrator.
Ask for a handoff that distinguishes implemented behavior from remaining work. “Tests pass” is incomplete without the command, the environment, and whether the tests used fake storage. A good handoff lets the integrator reproduce the result and identify the untested boundary immediately.
# Reading-list delivery: task pack, contract v2
## Shared boundary (integrator owns)
contract.json and contract-check.mjs describe POST /api/reading-list.
Both contributors start from the commit containing those files.
Shared schemas, migrations, package manifests, and lockfiles stay with
the integrator. Propose necessary changes; do not silently edit them.
These paths are examples; map them to the actual repository first.
## API task
Own: src/server/reading-list/** and tests/server/reading-list/**.
Deliver: validate URL and title; persist before returning created:true; return created:false
without a second row; translate storage failure to the agreed error.
Dependencies: contract v2; integrator-provided storage interface.
Acceptance: all four response examples; no write on invalid input;
duplicate leaves row count unchanged; storage failure creates no success.
Do not change: UI, shared contract, migrations, lockfile.
If the storage interface is absent: report the dependency and work on
validation against a fake; do not invent a second persistence layer.
## Interface task
Own: src/ui/reading-list/** and tests/ui/reading-list/**.
Deliver: empty, saving, created, duplicate, invalid, and failed states.
Use the v2 fixture responses while API work is in progress.
Acceptance: preserve input on failure; add row only on success;
keyboard path works; long URLs wrap; duplicate feedback is visible.
Do not change: API, shared contract, global styles, lockfile.
If a shared style is required: propose the minimal integrator-owned change.
## Integrator task (after both handoffs)
Review both diffs against v2. Apply API, then interface. Run contract,
server, and UI tests on the combined tree. Replace fixture transport with
the real local route and run save, duplicate, failure, and retry journeys.
Check the implementation against the feature brief, not just the JSON shape.
Record the exact combined commit and commands with results.
## Required handoff from each contributor
- Commit or patch identity; base commit; changed files.
- Implemented behavior and contract version used.
- Commands actually run and their actual results.
- Assumptions, known failures, and untested paths.
- Any required changes outside the assigned ownership.
- Whether test servers, databases, or temporary files remain.
## Example useful handoff (illustrative, not an execution claim)
API uses contract v2. URL validation and duplicate behavior implemented.
Unit cases run against fake storage; live database integration still pending.
No migration or lockfile changes. Storage failure response is 503 as agreed.
Integrator must exercise the real adapter before declaring the feature done.
5. Isolate checkouts and shared resources separately
Separate Git worktrees give each contributor a working directory and branch while sharing repository history. That prevents ordinary file edits in one checkout from appearing inside the other. It does not isolate processes, ports, databases, credentials, or external services. Two clean worktrees can still modify the same development database.
In a repository you control, commit the shared contract first and create two new worktrees from that commit. The example commands assume the branch names and sibling directories do not already exist. Check your repository state before running them; do not remove existing directories to make a copied command succeed.
git status --short
# Start only after the shared contract is committed.
git worktree add ../reading-api -b codex/reading-api HEAD
git worktree add ../reading-ui -b codex/reading-ui HEAD
git worktree listAssign distinct server ports and disposable database schemas if both tasks need live services. Prefer fixture transport for the UI until integration, which avoids making a shared API an accidental dependency. Keep each contributor's test data identifiable and record which services remain running in the handoff.
When work is complete, preserve the commits or patches until integration is verified. Removing a worktree is cleanup, not a substitute for reviewing its changes. Avoid force options that could discard uncommitted work. If the working tree is dirty, inspect it and reconcile the handoff before cleanup.
6. Integrate the behavior, not the success messages
Review the API diff first against the brief and contract. Verify validation happens before storage, duplicates preserve one row, and a write failure cannot return created. Then apply the interface change and replace its fixture transport with the real local route. Run the shared contract checks and the contributors' relevant tests on the combined tree.
Now exercise a sequence that crosses the boundary: save a valid URL, save it again with a different fragment, force a storage failure on another URL, and retry. Check both the screen and stored row count. A status message alone cannot prove persistence, and a database row alone cannot prove the reader received useful feedback.
Suppose integration reveals that the API returns url but the UI still reads href. Do not add a quiet fallback accepting both unless the product genuinely needs a compatibility period. Identify the stale consumer, fix it to contract v2, and add a regression assertion on the visible link. Accepting multiple undocumented shapes can conceal future drift.
If the contract itself was wrong, change it openly. For example, a new multi-user requirement changes the ownership boundary and uniqueness rules. Pause dependent work, revise the contract and fixtures, and reassign the affected tasks. Treating this as a merge conflict would hide a product decision inside a mechanical code edit.
| Claim | Evidence needed |
|---|---|
| The pieces agree | Both implementations use the same contract revision |
| The feature works | Real local route and storage exercised from the UI |
| Recovery works | Failed save retains input and does not create a successful row |
| The result is reproducible | Combined commit, commands, environment, and unresolved failures recorded |
7. Learn whether parallelism actually helped
After integration, compare the time spent doing independent work with time spent reconciling assumptions. You do not need elaborate productivity metrics to notice that two contributors edited the same schema repeatedly or waited on an undecided storage interface. Those are signals to change the task boundary next time.
Keep the useful artifacts: the shared contract, the failing mismatch case, the ownership plan, and the integrated verification notes. Discard repetitive progress narration that adds no decision or evidence. The next contributor should be able to understand why the boundary exists without reading an entire chat history.
For a small change in one component, one contributor may still be the best choice. For a larger feature with a stable boundary, parallel work can shorten the critical path. The skill accompanying this article asks the agent to make that judgment first, then produce explicit assignments and evidence-based handoffs. The goal is a coherent finished feature, not a busy set of terminals.
Take it into your project
Files, a full article, and an agent skill.
Download the files you need into the same folder. Each article explains which examples run on their own and which are reference documents.
- contract.json ↓
The full v2 request/response agreement with four outcomes.
- contract-check.mjs ↓
A shared response validator for both contributors.
- contract-check.test.mjs ↓
Four tests covering agreed examples and integration mismatches.
- task-pack.md ↓
Ready-to-adapt API, interface, and integrator assignments.
- article.md ↓
The complete guide, with its inline examples and references, for offline reading.
Use the method with your agent
The article explains the decisions; the skill gives your agent reusable instructions. Review it, create a folder named coordinate-a-change in your client’s configured skills directory, and save the file there as SKILL.md. Installation and activation vary by client. In a client without skill support, paste the instructions into your task.
Use coordinate-a-change to split a reading-list feature between API, interface, and test work. Freeze the JSON contract first. Assign one owner to shared files and return an integration checklist.Inspect the complete skill before using it
---
name: coordinate-a-change
description: "Plan independent implementation tasks and integrate their results through explicit contracts and handoffs. Use when a requested change benefits from multiple contributors working in parallel."
---
# Coordinate agents without overlapping ownership
An ownership plan, reproducible handoffs, and an integrated change with verification evidence.
## Inputs
The feature brief, repository state, available contributors, and permitted working directories.
## Decide whether parallel work is worthwhile
List independent deliverables and dependencies. If work shares the same small set of files or needs a design decision first, sequence it. Use only contributors authorized and available in the current environment.
## Define a contract and ownership
Agree on field names, success/error responses, route names, test data, and version. Assign each task its files, allowed changes, dependencies, and completion criteria. One owner handles shared schemas, lockfiles, and migrations. Use separate worktrees when appropriate; shared databases still need isolated schemas.
## Require an evidence-based handoff
Each contributor returns changed files, commit or patch identity, assumptions, commands with results, unresolved failures, and dependencies. A handoff must distinguish implemented behavior from proposed work. Stop and reconcile a contract mismatch before integrating dependent work.
## Integrate and review
Integrate one change at a time in the designated integration branch. Inspect the combined diff and run the relevant tests on the combined tree. Do not force-push or discard another contributor's changes to resolve conflicts. Escalate only genuine ownership or product conflicts. Report the final integrated state, not just individual task success.
## Example request
Use coordinate-a-change to split a reading-list feature between API, interface, and test work. Freeze the JSON contract first. Assign one owner to shared files and return an integration checklist.
References & scope
The worked examples and recommendations are original instructional material. These references support the specific formats and APIs used; they do not establish a benchmark or endorse the example architecture.
- Git worktree documentation ↗
Commands for separate working directories sharing repository history.
- Node.js test runner ↗
Runner for the portable contract exercise.