Case Study: Multi-Agent Feature
Background
Stackline — TypeScript monorepo (apps/web, apps/api, packages/types). Team of seven. Feature: Saved Views — users save filter+sort configs on the issues board, pin defaults per project, share read-only links. Estimated one week with agents. They decided to try Claude Code subagents in parallel: API, UI, and tests simultaneously to "go faster."
Initial vague prompt
Implement Saved Views end-to-end. Spin up subagents for the API, the
React UI, and the tests so we finish faster. Use the existing issues
board filters.Three subagents ran. Each invented a slightly different shape:
| Agent | SavedView shape |
|---|---|
| API | { id, projectId, query: string, createdBy } — query as serialized QS |
| UI | { id, projectId, filters: Filter[], sort: Sort, name } — structured |
| Tests | Assumed REST /views while API implemented /saved-views |
Integration day was a rewrite. Parallelism without a frozen interface is three agents generating incompatible fiction at 3× speed.
Why it failed
Multi-agent speedup assumes independent work against a shared contract. Filters already existed in the UI as TypeScript types, but those types were not the API contract, not versioned in packages/types, and not marked stable. Each subagent did the locally rational thing: define what it needed.
Also: the test agent wrote mocks matching its imagined API, so its suite passed while the real API diverged — a false green that delayed detection by a day.
The improved spec
They reset feature branches and spent half a day on interface freeze only.
## Saved Views — frozen interface v1
### packages/types/src/saved-views.ts
export type SavedView = {
id: string
projectId: string
name: string
filters: IssueFilter[] // reuse existing IssueFilter
sort: IssueSort | null
isDefault: boolean
createdBy: string
createdAt: string // ISO
}
### API
- GET /v1/projects/:projectId/saved-views
- POST /v1/projects/:projectId/saved-views
- PATCH /v1/saved-views/:id
- DELETE /v1/saved-views/:id
- PUT /v1/projects/:projectId/saved-views/:id/default
- Share link: out of scope for v1
### Acceptance fixtures
- fixtures/saved-views/*.json — request/response examples both agents must honorOnly after packages/types merged did subagents fan out. Test agent was instructed to hit the real API app in-process or via contract tests against fixtures — no invented mocks of unstable surfaces.
Agent workflow
Coordination rules in the session:
You are the API subagent. Read packages/types/src/saved-views.ts and
fixtures/saved-views/. Implement handlers only. If the types are wrong,
stop and report — do not change the types or invent parallel types.UI and test agents got the same "types are law" constraint. See Multi-Agent Coordination.
Practical session hygiene that mattered:
- One git worktree (or branch) per lane; no shared dirty tree.
- Shared
fixtures/saved-views/create-ok.jsonchecked intomainbefore fan-out — both API and contract-test agents asserted against the same bytes. - A sticky note in the tracking issue: "If you need a new field, open a types PR and @ the other lanes to pause." The first time someone ignored it, UI shipped
color?: stringfor view chips that the API dropped on write. Caught in E2E, not in unit tests.
Wall-clock comparison (honest): freeze-first totaled ~4 days including the failed first attempt's sunk cost. A serial single-agent approach would have been ~5 days. The win was not raw speed — it was that after the ritual existed, the next feature was actually faster. Multi-agent pays on the second use, not the first demo.
Human intervention points
- Interface PR review — the highest-leverage review of the feature; treat it like a public API. Two reviewers on
packages/typeseven when app PRs need one. - Scope cut — share links deferred so the freeze stayed small. Parallelism amplifies every optional field.
- Conflict resolution — when API agent needed a field (
updatedAt), human amended types in a tiny PR; subagents paused and rebased. No silent local extensions. - E2E — human pinned/defaulted a view on a real project; agents' unit greens were insufficient. Specifically: reload the board in a second browser profile and confirm the default applied.
- Merge order — types → API → UI, even though work was parallel on branches; integration order stayed serial. Merging UI first produced a board that called 404s for a morning.
Mistakes made
- Fan-out before freeze. Cost: ~2 days of integration thrash + skepticism about multi-agent forever after (unfair, but earned).
- Test agent with free mock invention. Cost: false confidence. Fixed by fixture-driven contract tests owned in
packages/typesorapps/api. - Chat-coordinating agents via the human copying messages. Lost a field rename (
isDefault→default). Better: types package + short ADR in-repo as the bus, not Slack. - UI agent "improving" Filter chip UX while implementing Saved Views. Scope creep in a parallel lane is hard to see until review. Bound UI agent to wire filters only — "no CSS changes outside SavedViewsPanel.tsx."
- Assuming subagents share memory. They don't. The API lane's discovery that
IssueFilterlackedassigneeIdsnever reached the UI lane until integration. The freeze must include the reused types, not only the new ones — audit imports in the interface PR.
Final outcome
After restart: interface half-day, parallel implementation 1.5 days, integration half-day. Feature shipped day 4. Second multi-agent feature (comment drafts) used the same freeze-first ritual and integrated in hours — ~1 day total, three lanes, one types PR of 80 lines. The team kept a rule: no subagent fan-out without a merged types PR, enforced by a CODEOWNERS-required check on packages/types before labeling a ticket parallel-ok.
LOC for Saved Views at merge: ~1.1k production, ~600 tests/fixtures. Roughly 85% agent-typed across lanes; the types file and fixtures were majority human-edited.
Lessons
- Parallel subagents multiply drift unless interfaces and fixtures are merged first.
- Types/OpenAPI/examples are the coordination bus; chat between agents is not.
- Test agents must consume the frozen contract — mocks of imagined APIs create false greens.
- When the freeze is wrong, stop all lanes and amend types in one place; never let a lane extend the contract locally.
- Parallelism is for implementation against a contract, not for discovering the contract.
- Freeze reused dependency types too — drift often hides in the imports, not the new DTO.
Related
- Multi-Agent Coordination — freeze and fan-out
- Multi-Agent Patterns — lane layouts
- Agent Roles — API/UI/test roles
- Playbook: Large Tasks — decomposition before parallel
- Scope and Context Failures — unbound parallel lanes