Skip to content

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

text
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:

AgentSavedView shape
API{ id, projectId, query: string, createdBy } — query as serialized QS
UI{ id, projectId, filters: Filter[], sort: Sort, name } — structured
TestsAssumed 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.

markdown
## 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 honor

Only 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:

text
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.json checked into main before 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?: string for 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

  1. Interface PR review — the highest-leverage review of the feature; treat it like a public API. Two reviewers on packages/types even when app PRs need one.
  2. Scope cut — share links deferred so the freeze stayed small. Parallelism amplifies every optional field.
  3. Conflict resolution — when API agent needed a field (updatedAt), human amended types in a tiny PR; subagents paused and rebased. No silent local extensions.
  4. 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.
  5. 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/types or apps/api.
  • Chat-coordinating agents via the human copying messages. Lost a field rename (isDefaultdefault). 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 IssueFilter lacked assigneeIds never 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

  1. Parallel subagents multiply drift unless interfaces and fixtures are merged first.
  2. Types/OpenAPI/examples are the coordination bus; chat between agents is not.
  3. Test agents must consume the frozen contract — mocks of imagined APIs create false greens.
  4. When the freeze is wrong, stop all lanes and amend types in one place; never let a lane extend the contract locally.
  5. Parallelism is for implementation against a contract, not for discovering the contract.
  6. Freeze reused dependency types too — drift often hides in the imports, not the new DTO.

A field manual for AI-native software engineering.