Skip to content
Public template

About

Delivery workflow for coding agents across Claude Code, Codex, and opencode: issues, OpenSpec changes, worktrees, and cross-provider review. Install with npx create-switchyard.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Switchyard: an agent coordination framework

A rail yard where switches sort one wagon onto each of three sidings, each ending at a signal

OpenSpec Claude Code Codex opencode Herdr GitHub CLI

Switchyard is a delivery workflow for coding agents, born out of my own personal AI development setup.

Add it to any git repo, new or existing, from the repo's root:

npm init switchyard@latest

The repo then has the workflow in place: issues, specs, isolated worktrees, and review gates. Install says what the command adds and merges. To start from the GitHub template instead, see Start a new project.

It does not depend on one AI provider. Use any agent CLI for the worker roles, and change a provider or a model by editing one file, switchyard/routing.md. The values it ships with are examples. The coordinator runs on any CLI that reads AGENTS.md and can run the skill's scripts.

It holds no application code. You add your stack on top.

One session coordinates. For each issue, a planner writes a spec change, an implementer builds it, and a reviewer checks it, in a git worktree of its own. The reviewer runs on another provider than the implementer when the project lists more than one. You answer the product questions and approve the merges.

The workflow

Each piece covers a gap the others leave:

  • Issue, then plan, then code. Every issue starts with a short problem statement and ships as one PR. A project with openspec/ uses one OpenSpec change per issue; the PR syncs any spec delta and archives the change. Without openspec/, the planner writes scenarios and tasks in .switchyard/plan.md. The implementer and reviewer use that plan, and nothing is archived.
  • Planning and building are usually separate sessions. The planner and implementer can use different effort settings, and the planner stops at a plan. A small issue plans and builds in one session, stopping after the plan only when it has questions. The implementer starts fresh and reads the change from disk. Its session does not inherit the planner's context.
  • Two reviews, two reviewers. The coordinator checks the plan before any code is written, except on a small issue, where it reads the plan with the diff. A reviewer checks the diff against the change before anything is pushed, on another provider when one is available.
  • Files carry every handoff. The brief, the answers, the change, and the review live in the worktree. Each session reads them from disk, so no stage works from another session's summary, and the coordinator holds verdicts, not diffs.
  • One worktree per issue. Each issue gets its own branch, ports, and data. Issues that own different folders run in parallel.
  • Workers never push. Each worktree's push URL is disabled, and a script lists any file changed outside the paths the issue owns. Nothing reaches the remote until the review passes, and nothing merges until you approve it.
  • Several providers share the load. Worker roles run on any agent CLI the project lists in routing.md, and you choose the models there. Implementers rotate across the listed providers, so one usage window does not stall a batch. The coordinator sizes each issue and picks each role's row by that size.

Who does what

Role Where it runs Does Never
You GitHub and the coordinator's chat Open issues, answer product questions, approve merges
Coordinator Your session on the main checkout, any CLI that reads AGENTS.md and can run the skill's scripts Splits the work, writes briefs, reviews plans, acts on review verdicts, pushes, opens PRs, merges, cleans up Implements
Planner The issue's worktree, any provider Reads the issue and the code, proposes the OpenSpec change, takes before screenshots, lists open questions Writes code
Implementer The same worktree, fresh session that fix rounds resume, any provider Applies the change, runs every check, takes after screenshots, archives the change, commits, writes the PR body Pushes
Reviewer The same worktree, fresh session, another provider when available Checks the diff against the change's scenarios and tasks, runs every check, writes a verdict file Edits code

One issue, start to finish

A sequence diagram drawn as rail tracks, one track per role. You hand the coordinator an issue and answer its questions. The coordinator briefs the planner and brings you the plan's decisions. Build and review repeat until the verdict is pass. You approve the pull request and the coordinator merges.

  1. You open an issue. State the problem. Pick a milestone if the project uses them. Keep it short: acceptance criteria belong in the change, not the issue.

  2. You ask the coordinator to work on it. It reads the issue against the code and asks you about anything the issue does not settle. With several issues, it splits them by the folders each one owns and runs the independent ones in parallel.

  3. The coordinator sets up. A script creates the worktree and runs your isolation hook. The coordinator writes a brief there: the issue, the paths it owns, its ports, and the decisions so far.

  4. The planner proposes the change. It writes the proposal, optional design, and tasks under openspec/changes/<name>, plus a spec delta when the change has one. It links the change from the issue and stops with numbered questions, each with options and a recommendation. A change with skip_specs: true has no spec delta.

  5. The coordinator reviews the plan. It checks the plan's claims against the code, answers what it can, and brings you the product decisions. The answers go into a file in the worktree.

    A small issue skips steps 4 and 5: one session plans and builds, and stops after the plan only when it has questions.

  6. The implementer builds it. A new session applies the tasks (on a small issue, the same session that planned it), runs every check, syncs the spec delta into openspec/specs/ when it has one, archives the change, commits, and writes the PR body. It asks before it decides anything the plan does not cover.

  7. The reviewer checks the diff. A fresh session, on another provider when available, reads the change and the diff. It checks each scenario, or each task for a skip_specs: true change, plus scope, tests, duplication, and screenshots. It runs every check and writes a verdict to a file in the worktree.

  8. The coordinator acts on the verdict. On a pass it pushes the branch and opens the PR. Otherwise the implementer's session resumes, or a fresh one starts when the provider cannot resume. It reads the review and fixes it, and a new reviewer checks again. After two failed rounds the coordinator brings the findings to you.

  9. You approve. The coordinator squash-merges, removes the worktree and its branch, and rebases the PRs still open.

You are asked something before work starts, after the plan when there is one, and at the merge. Pre-approve the batch's merges and step 9 asks nothing more: the coordinator merges on the condition you named.

Requirements

  • macOS or Linux. CI lints and tests the workflow's scripts on ubuntu-latest, with the tests on Python 3.10 and the newest Python 3, and the maintainer develops on macOS. Windows is untested. WSL should behave like Linux, but nobody has run it.
  • git, Python 3.10 or newer, and Node.js (OpenSpec runs through npx).
  • Preflight checks GitHub once for a newer Switchyard release and skips that check when offline.
  • The GitHub CLI, signed in.
  • Any agent CLI with a worker row in routing.md. The coordinator can run on any CLI that reads AGENTS.md and can run the skill's scripts; see its note in routing.md for setup.
  • With one provider, keep only that provider's rows in routing.md. The reviewer then runs on the implementer's provider, and the review is not independent.
  • Recommended: Herdr, a terminal workspace manager for coding agents. See Runners.
  • For UI screenshots: Playwright in your project, and sips (macOS) or ImageMagick.
  • Check them with switchyard/scripts/preflight.sh <provider>..., passing the providers in routing.md.

Add it to an existing repo

Run it from the root of the repo. A new, empty repo works too:

npm init switchyard@latest

Install asks whether to use OpenSpec, default yes; use npm init switchyard@latest -- --no-openspec for plain plans. Pass npm init switchyard@latest -- --tracker linear to name Linear as the issue tracker; see Use Linear.

npx create-switchyard@latest does the same. Keep @latest: without it, npm can run an older copy from its cache. The package runs its bundled update-switchyard.py --install, so it needs Python 3.10 or newer. The command adds every workflow file the repo does not have and merges the ones it does. The merge keeps what the project owns:

  • AGENTS.md gains the Workflow and Writing sections; a repo without an AGENTS.md gets the release's whole file, Project placeholder included.
  • .gitignore gains the .switchyard/ line when it is missing.
  • When the project uses OpenSpec, openspec/config.yaml gains the Switchyard rules entries without replacing context or the project's own rules.
  • switchyard/routing.md is added when it is missing and never replaced.
  • switchyard/lessons.md is added when it is missing and never replaced.
  • switchyard/tracker.md is added when it is missing, naming github unless --tracker names another, and never replaced.

The report lists each file as added, merged, or conflict, then the totals, and writes VERSION naming the release. Conflict markers name the two sides. In AGENTS.md, .gitignore, and openspec/config.yaml, they stay inside the owned section or rule list. Any other existing workflow file that differs gets markers around the whole file. Resolve them, review the diff, and commit. The updater ends by printing the commands that undo the run.

When you choose OpenSpec and the repo has no openspec/ directory, install prints the next step, npx @fission-ai/openspec init --tools claude,codex,opencode; install writes openspec/config.yaml first, and init keeps it.

Start a new project

The GitHub template is the other way in. It copies this whole repo, docs included, into a new one. Click Use this template on GitHub, or run:

gh repo create my-project --template nicoleman0/switchyard --private --clone
cd my-project
python3 switchyard/scripts/init-project.py

The new repo has every file here and none of this repo's history. Init moves this guide to docs/WORKFLOW.md, fixes its paths, and deletes Switchyard's own files: the create-switchyard/ folder, CHANGELOG.md, CONTRIBUTING.md, LICENSE, the banner image, the social preview, and docs/pixelart.py. It sets the Tracker: line of switchyard/tracker.md to github. It keeps docs/workflow.svg, prints every move, deletion, and skip, and never commits. A committed copy of every moved and deleted file stays in git history: init refuses to run outside a git work tree and skips the listed paths with uncommitted changes. It ends by naming what to add next: your own README.md and LICENSE, for a start.

Then make it yours before the first issue.

Make it yours

These are your project's own files and settings. The first two rows apply only to projects made from the template, after init:

Edit What goes there
README.md, LICENSE Add your project's README and license.
CHANGELOG.md, docs/workflow.svg Start your own changelog. Init kept the workflow chart next to the guide, which now reads it from workflow.svg.
CONTRIBUTING.md Write your project's contributing guide, or leave it out.
.github/workflows/ switchyard.yml checks the workflow's own files and follows releases. Put your project's checks in a workflow of their own, such as ci.yml.
AGENTS.md, Project section Your checks, shared services, and conventions. Every agent reads it. Preflight warns while the shipped placeholder is still there.
openspec/config.yaml, context What the product is and what is out of scope.
switchyard/hooks/worktree-up.sh and worktree-down.sh Per-issue isolation: ports, a database per issue, dependency install. What worktree-up.sh prints lands in each brief.
switchyard/hooks/checks.sh The commands a branch must pass before review. Switchyard's own checks ship in it. Add your project's.
switchyard/brief-template.md, Isolation section Rules every worker must follow, such as how to start a dev server on its own port.
switchyard/scripts/shots.mjs The routes to screenshot, if the project has a UI.
switchyard/routing.md The Providers table holds each provider's commands. The Workers table holds providers, models, effort, and flags, and a row can name an issue size in its Use column. Shipped values are examples. This file belongs to your project.
switchyard/tracker.md The Tracker: line names the issue tracker. It ships as github. scripts/tracker.py reads it. This file belongs to your project.

The coordinator's CLI setup belongs in the project's Coordinator notes in routing.md.

Take a later release

Start from a clean checkout on a new branch, then run:

git switch -c update-switchyard
npm init switchyard@latest update

npx create-switchyard@latest update does the same. It updates to the newest release the package holds. Without Node, run the updater installed in your repo instead: python3 switchyard/scripts/update-switchyard.py.

The report lists files as added, updated, removed, merged, conflict, or kept. Review the diff and resolve conflict markers. The updater does not commit. It ends by printing the commands that undo the run.

npm init switchyard@latest update -- --dry-run and python3 switchyard/scripts/update-switchyard.py --dry-run print the report a real run would print and write nothing. Without the --, the npm form is also safe, but npm can fail with an enoent error on a first run.

The updater adds switchyard/routing.md when it is missing, then leaves it unchanged in later updates. Review it after it is added. The updater adds switchyard/lessons.md when it is missing, then leaves it unchanged in later updates. The project owns its content: the coordinator edits it during a batch. The updater adds switchyard/tracker.md when it is missing, naming github, then leaves it unchanged in later updates.

Preflight warns when switchyard/VERSION is behind the newest release. Run python3 switchyard/scripts/update-switchyard.py --check for the same check: it exits 0 when current, 3 when behind, and 4 when the source cannot be reached.

The updater covers paths listed in its target release. It skips OpenSpec-generated files, which npx @fission-ai/openspec update refreshes, and Switchyard's README, license, changelog, contributing guide, and banner. It prints the changelog sections for the releases it moves across. See the Switchyard releases for older notes.

Both need network access: the npm form reaches the npm registry, and the Python form clones the release source from GitHub. Run it outside the sandbox if a Codex session cannot access the network.

A repository that never had the workflow at all does not update from a release: it takes install, which needs no --from.

Use Linear

A project can keep its issues in Linear while PRs stay on GitHub:

npm init switchyard@latest -- --tracker linear
  • switchyard/tracker.md gets Tracker: linear. Edit that one line to switch; the updater adds it when missing and never replaces it.
  • Export LINEAR_API_KEY with a Linear personal API key, so tracker.py can call Linear's GraphQL API. Preflight makes one cheap call to check it.
  • Issue keys are Linear identifiers such as ENG-123, plus b for a best-of-N candidate.
  • Start every issue's branch with the lowercased key, such as eng-123-fix-login. Linear's GitHub integration links a PR whose branch name names the issue, so connect that integration.
  • gh is still required: PRs, reviews, and merges all stay on GitHub.

To deliver a whole project, name it in the session: "deliver project Onboarding". tracker.py list <project> prints the project's open issues in delivery order: key, status, milestone, the open issues each one is blocked by, and the title. A blocked issue lists after its blocker, so the coordinator starts it only once that blocker has merged, and an issue blocked from another project is marked (outside) and waits. The project is matched by name without case or by its slug id.

Each issue moves through a Linear status per phase:

Phase Default status Set by
planning Planning start-worker.sh, planner
plan-review Plan review coordinator
building In Progress start-worker.sh, implementer
in-review In Review start-worker.sh, reviewer
needs-input Needs input coordinator
  • Add Planning, Plan review, and Needs input to each team's Started group. In Progress and In Review are in Linear's default workflow.
  • To use other names, add a table to switchyard/tracker.md. Phases without a row keep their default:
    | Phase | Status |
    | --- | --- |
    | needs-input | Blocked |
    
  • A status the team lacks stops the first worker start, with the team's status names.
  • Questions arrive as one Linear comment that starts Questions from Switchyard. Reply to it in Linear, or answer in the chat.
  • Linear's GitHub integration may also move the issue when the PR opens and merges. Done comes from it.

Use it

  1. Create an issue. Add --milestone if the project uses milestones:
    gh issue create --title "Password reset emails arrive twice" --body "..."
  2. Start a CLI that reads AGENTS.md and can run the skill's scripts from the main checkout. Follow its Coordinator note in routing.md for writable worktree paths and session setup.
  3. Tell it what to deliver:
    • "Work on issue #12."
    • "Work on #12, #14, and #15."
    • "Run the next steps of milestone M2."
    • "Put #14 on Codex."
  4. See the batch with python3 switchyard/scripts/state.py status.
  5. Answer its questions, review the PR it opens, and tell it to merge.

AGENTS.md tells any session asked to deliver issues to act as the coordinator and follow the skill, so there is nothing to invoke by name.

Not everything needs the crew:

  • An idea that is still vague: /opsx:explore.
  • One change you want to drive yourself: /opsx:propose, /opsx:apply, /opsx:archive in a single session.
  • Work through the crew that changes no observable behaviour, such as tooling, refactors, or a bug where the code diverges from the spec: use a change with skip_specs: true. A hand-made PR outside the crew needs no change.

Runners

The runner is how the coordinator starts a worker, prompts it, waits for it, and reads its report. The coordinator picks one by checking whether it runs inside Herdr.

Herdr (recommended) Headless
Needs Herdr Nothing beyond the worker CLIs
Workers Interactive agents, one pane per issue One CLI command per turn, from the Providers table
Watching Live. You can read a pane or type into it. Each turn's output is logged to .switchyard/turns/ in the worktree.
Worktrees ~/.herdr/worktrees/<repo> ../<repo>.worktrees/<branch>

The commands are in switchyard/runners/.

Layout

AGENTS.md                        rules every agent reads
CHANGELOG.md                     what changed in each release
CONTRIBUTING.md                  how to contribute to Switchyard
.github/
  workflows/switchyard.yml       checks on the workflow's own files
  ISSUE_TEMPLATE/                the short issue the workflow starts from
openspec/
  config.yaml                    project context and artifact rules
  specs/                         what the product does today
  changes/                       one folder per change in flight, plus archive/
.claude/
  agents/                        Claude role profiles
  skills/work-issues/SKILL.md    Claude Code entry point for the workflow
create-switchyard/               the npm installer; init deletes it
switchyard/
  SKILL.md                       the coordinator's playbook and routing rules
  routing.md                     project-owned providers, models, effort, flags, and commands
  lessons.md                     project-owned one-line traps from each batch
  tracker.md                     project-owned issue tracker name
  roles/                         planner, implementer, and reviewer instructions, shared by every provider
  runners/                       Herdr and headless commands
  hooks/                         your per-issue isolation
  brief-template.md              what each worker is told
  scripts/                       batch state, worktree setup and teardown, screenshots, merge helpers
  VERSION                        Switchyard release the workflow files follow
  update-paths.txt               files brought forward by the release updater
.switchyard/                     ignored briefs, answers, reviews, turns, state, sessions, PR bodies, and screenshots
.claude/skills/openspec-*, commands/ generated by OpenSpec
.agents/, .opencode/             the same OpenSpec workflows for Codex and opencode

npx @fission-ai/openspec update refreshes the generated files.

License

MIT. See LICENSE.

About

Delivery workflow for coding agents across Claude Code, Codex, and opencode: issues, OpenSpec changes, worktrees, and cross-provider review. Install with npx create-switchyard.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages