Skip to content

Worked example: Batman across three repos

This walkthrough shows one feature shipped across three repos using the default Alfred fleet plus Batman. The example is “add an organization slug to every account-scoped URL” because it is a small realistic change that requires coordinated edits to backend, frontend, and mobile.

This page mirrors docs/MULTI_REPO_WORKED_EXAMPLE.md.

The repos referenced below are placeholders. Replace them with your own fleet:

  • your-org/your-specs (specs repo, planning context)
  • your-org/your-backend (Kotlin API)
  • your-org/your-frontend (React web app)
  • your-org/your-mobile (React Native app)

Fleet configuration for this example:

Terminal window
ALFRED_DRAKE_REPOS=your-backend,your-frontend,your-mobile
ALFRED_LUCIUS_REPOS=your-backend,your-frontend,your-mobile
ALFRED_RASALGHUL_REPOS=your-backend,your-frontend,your-mobile
BATMAN_SCAN_REPOS=your-backend,your-frontend,your-mobile

Step 1: File one agent:large-feature issue

Section titled “Step 1: File one agent:large-feature issue”

Open the issue in the repo that owns the first decision. For a schema-shaped feature like this, that is the backend. The issue carries two labels: agent:large-feature and agent:bundle:add-org-slug.

Title: Add `org_slug` to account-scoped URLs
Labels: agent:large-feature, agent:bundle:add-org-slug
## What
Every account-scoped URL today is keyed by numeric `account_id`. We want a
URL-safe `org_slug` (lowercase, hyphen-separated, unique per account) in
addition to the numeric id, so:
- GET /api/v1/orgs/acme/projects works alongside
GET /api/v1/accounts/4711/projects
- https://app.example.com/acme/projects works alongside
https://app.example.com/accounts/4711/projects
- The mobile deep link exampleapp://acme/projects resolves to the same place.
Numeric routes keep working. The slug is the new preferred form for new
links shared by the product.
## Acceptance criteria
- [ ] Backend: `Account` has a unique non-null `slug` column with a
lower-snake-case constraint; a new slug-resolver endpoint exists;
existing id-keyed routes still work.
- [ ] Frontend: any new internal link uses the slug; account switcher writes
the slug into the URL; loading by slug works on cold load.
- [ ] Mobile: deep links match `/<slug>/...` and `/accounts/<id>/...`.
- [ ] One end-to-end test that creates an account, reads it by slug, and
reads it by id.
## Repos this touches
- your-backend (first; schema + endpoint)
- your-frontend (after backend ships staging)
- your-mobile (after backend ships staging)
## Out of scope
- Custom domains per org.
- Renaming the `account_id` foreign key column.
- SEO-rewriting public marketing URLs.
## Rollback
Revert the migration and the slug-resolver endpoint; frontend and mobile
fall back to id-keyed routes automatically.
## Human approval checklist
- [ ] Slug uniqueness collision plan reviewed
- [ ] Migration tested against a staging copy of prod
- [ ] Marketing aware before launch

Batman fires once per hour. On the next firing it scans BATMAN_SCAN_REPOS, finds the agent:large-feature issue, resolves the bundle from the agent:bundle:add-org-slug label (one issue at this point, the trigger itself), and posts a plan summary.

[15:04:11] batman preflight ok ● green
[15:04:12] batman scanning 3 repos for agent:large-feature
[15:04:14] batman found 1 issue, 1 bundle: add-org-slug
[15:04:14] batman plan drafted ● green
[15:04:15] batman posted plan to #your-fleet-channel

The post Batman emits in Slack (rendered shape):

batman · plan drafted
Issue: your-backend#247: Add `org_slug` to account-scoped URLs
Bundle: add-org-slug
Affected: your-backend, your-frontend, your-mobile
Rollout: your-backend → your-frontend → your-mobile
Engine: hybrid
To proceed, approve the plan in the configured approval surface.
After approval, Batman files one scoped child issue per repo and labels each
issue for the normal fleet pickup path.

Batman does not bypass approval. It waits for the approval gate, then files child issues rather than opening worktrees directly. Lucius claims those issues through the same label, lock, spend, review, and merge gates as any other work.

Step 3: Batman files the child issues after approval

Section titled “Step 3: Batman files the child issues after approval”

In the public package, approved Batman plans can file the three child issues. Each inherits agent:bundle:add-org-slug so the bundle stays trackable, and each is labelled agent:implement so Lucius can claim it. Operators who prefer a stricter manual process can keep Batman in plan-only mode and file the same children by hand.

Repo: your-org/your-backend
Title: Add `org_slug` column and resolver endpoint for accounts
Labels: agent:implement, agent:bundle:add-org-slug
## Goal
Introduce a unique `slug` column on the `accounts` table and a
slug-resolver endpoint. Numeric routes continue to work.
## Files in scope
- src/main/resources/db/migration/V20260601__add_account_slug.sql
- src/main/kotlin/com/example/account/AccountController.kt
- src/main/kotlin/com/example/account/AccountService.kt
- src/test/kotlin/com/example/account/AccountControllerTest.kt
## Acceptance criteria
- [ ] Migration adds `slug VARCHAR(64) NOT NULL UNIQUE` with a lower-snake-case
CHECK constraint, backfilled from existing account names with a
deterministic slugifier.
- [ ] `GET /api/v1/orgs/{slug}` returns the same payload as
`GET /api/v1/accounts/{id}`.
- [ ] Existing id-keyed routes return identical responses.
- [ ] Two new tests: one for slug resolution, one for collision handling.
## Out of scope
- Frontend or mobile changes.
- Custom domains.
Repo: your-org/your-frontend
Title: Use `org_slug` in account-scoped URLs
Labels: agent:implement, agent:bundle:add-org-slug
## Goal
Switch internal account-scoped link generation to slug form. Keep id-keyed
URLs working for back-compat.
## Files in scope
- src/lib/routes.ts
- src/features/account-switcher/AccountSwitcher.tsx
- src/features/account-switcher/AccountSwitcher.test.tsx
- src/pages/[slug]/projects.tsx
## Acceptance criteria
- [ ] `accountUrl(account)` returns `/<slug>/...` when slug is present,
`/accounts/<id>/...` otherwise.
- [ ] AccountSwitcher writes the slug into the URL on switch.
- [ ] Cold load on `/<slug>/projects` resolves and renders projects.
- [ ] No edits to backend response shapes.
## Out of scope
- Marketing pages and SEO routes.
- Mobile deep linking.
## Depends on
your-backend bundle:add-org-slug merged to main and deployed to staging.
Repo: your-org/your-mobile
Title: Accept `<slug>` in deep links
Labels: agent:implement, agent:bundle:add-org-slug
## Goal
Mobile deep-link handler must accept `exampleapp://<slug>/...` in addition to
`exampleapp://accounts/<id>/...`.
## Files in scope
- src/navigation/linking.ts
- src/navigation/linking.test.ts
## Acceptance criteria
- [ ] `exampleapp://acme/projects` resolves to the Projects screen for the
`acme` account.
- [ ] `exampleapp://accounts/4711/projects` continues to resolve.
- [ ] One test per deep-link form.
## Out of scope
- Push notification payloads (separate bundle).
## Depends on
your-backend bundle:add-org-slug merged to main and deployed to staging.

Step 4: Lucius picks up the backend issue first

Section titled “Step 4: Lucius picks up the backend issue first”

Lucius fires every 20 minutes. The backend issue has no depends-on blocker, so it is eligible on the first firing after labelling.

[15:24:11] lucius preflight ok ● green
[15:24:12] lucius pick_issue: oldest agent:implement
[15:24:13] lucius claimed your-backend#251 ● green
[15:24:14] lucius worktree opened
~/.alfred/worktrees/eng-lucius-your-backend-251-20260601-152414/
[15:24:15] lucius branch: agent/lucius/251-add-org-slug-column-and-resolver
[15:24:16] lucius invoking hybrid engine, max_turns=140
[15:27:42] lucius engine returned success, 38 turns, $0.41
[15:27:43] lucius pre-push: ./gradlew check (running…)
[15:30:12] lucius pre-push ok
[15:30:14] lucius pushed branch
[15:30:16] lucius gh pr create
[15:30:17] lucius PR opened: your-backend#412 ● green
[15:30:17] lucius [OK] commit 7c4a1f2
[15:30:18] lucius release_issue → agent:pr-open
[15:30:19] lucius Slack-post info

The Slack post Lucius emits:

lucius · PR opened · green
Issue: your-backend#251
PR: your-backend#412
Branch: agent/lucius/251-add-org-slug-column-and-resolver
Engine: hybrid (claude)
Turns: 38
Cost: $0.41
Pre-push: ./gradlew check (ok, 2m 28s)

Ra’s al Ghul fires every 30 minutes. It picks the fresh agent:authored PR.

[15:48:11] rasalghul reviewing your-backend#412
[15:51:33] rasalghul posted review comment, 2 nits, 0 P0/P1

The review comment (rendered shape):

rasalghul · review
Correctness: ok (migration is idempotent, resolver handles missing slug)
Security: ok (no input echo, no SQL string concat)
Performance: ok (slug column gets unique index from the UNIQUE constraint)
Maintainability: 2 nits (P2)
Nits:
1. AccountController.kt line 88: extract the slug regex constant; it's used
twice in this file.
2. V20260601 migration: the CHECK constraint message could name the column
for easier debugging.
Ship-ready: yes

Nightwing fires every 45 minutes and only lands P0/P1 reviewer comments. P2 nits are out of scope by default. For this example, assume you also asked Nightwing to address P2 nits on this PR by labelling it nightwing:p2. On the next firing:

[16:33:11] nightwing picking review threads on your-backend#412
[16:33:14] nightwing 2 unresolved threads (P2 by label override)
[16:33:15] nightwing worktree opened
[16:35:42] nightwing engine returned success, 7 turns, $0.09
[16:35:43] nightwing pushed fix commit 9a2cdde
[16:35:44] nightwing resolved 2 threads on your-backend#412

Bane fires every 4 hours and writes only test files. It looks at the recently-changed files in your-backend and notices AccountService.kt is now the lowest-coverage actively-changed file.

[18:04:11] bane lowest-coverage actively-changed file
your-backend/src/.../AccountService.kt (62%)
[18:04:12] bane worktree opened
[18:07:38] bane engine returned success, 22 turns, $0.18
[18:07:39] bane PR opened: your-backend#414 ● green
agent:authored, tests-only

Bane’s PR is a separate agent:authored PR; it does not push to Lucius’s branch. The squash-merge utility (automerge) treats it on its own merits.

Step 8: backend merges, the bundle progresses

Section titled “Step 8: backend merges, the bundle progresses”

After Ra’s al Ghul says “Ship-ready: yes” and CI is green for 30 minutes, automerge squash-merges your-backend#412. The issue transitions to agent:done.

A separate deploy step (Alfred does not own this) rolls staging. Once backend is live on staging, you unblock the frontend and mobile child issues by removing their agent:blocked label or otherwise marking them eligible.

In parallel on the next Lucius firings, the frontend and mobile issues get claimed and worked. They run on different worktrees, in different repos, and never collide.

[19:04:11] lucius claimed your-frontend#188 ● green
[19:04:14] lucius worktree opened
~/.alfred/worktrees/eng-lucius-your-frontend-188-20260601-190414/
...
[19:24:11] lucius claimed your-mobile#92 ● green
[19:24:14] lucius worktree opened
~/.alfred/worktrees/eng-lucius-your-mobile-92-20260601-192414/

Each gets its own review pass, its own Nightwing fixes if needed, its own automerge. The labels on the original agent:large-feature issue are unchanged; the bundle is tracked by the agent:bundle:add-org-slug label that every child carries.

When the last child PR in the bundle merges, you (or a custom extension to Batman) posts a closing rollup. The OSS package does not auto-close the parent agent:large-feature issue; you do it.

Example closing rollup post:

batman · bundle shipped · add-org-slug
Parent: your-backend#247
Children:
- your-backend#251 → PR #412 (merged 15:50 → 21:30)
- your-frontend#188 → PR #207 (merged 19:48 → 20:30)
- your-mobile#92 → PR #61 (merged 19:55 → 20:30)
Bane added 2 test PRs along the way (#414 backend, #208 frontend).
Total cost: $1.84 across 7 firings.
Total wall-clock: 6h 26m.
  • You file one agent:large-feature issue, not three.
  • Batman posts a plan and waits for approval before child issues are filed.
  • The child agent:implement issues each live in the repo that owns the change. Batman can file them after approval, or you can file the same children by hand in a stricter process.
  • Lucius, Ra’s al Ghul, Nightwing, and Bane act on whatever is in their inbox without knowing they are part of a bundle. The bundle label is for tracking, not coordination. They never call each other; they only see GitHub.
  • The worktree per firing means three Lucius firings can run in three different repos at the same time without interfering.