Seven roles. One PR. Zero unreviewed diffs.
Relay runs feature work the way a disciplined team would. A PM writes the brief. An Architect plans it. Dev builds it. Review and QA gate it. Retro writes down what was learned. Each handoff is a real artifact; each gate is a deterministic check, not a model's opinion of itself.
The line
Every feature moves down eight stations, left to right. Click a station, use the arrow keys, or press play. The panel below always shows exactly what that role reads, writes, and is gated on.
Anatomy of a role
PM, Architect, Dev, Review, QA: different jobs, identical machinery. This is what .ai/agents.json and agent-runner.ts assemble for every single call.
.ai/GOVERNANCE.md and DENIED_ACTIONS.md are injected into every prompt: role boundaries, output rules, hard denials..ai/project-memory.md: pitfalls, conventions, and decisions carried over from every past feature.submit_changes, checked against a per-role JSON Schema ({files, artifacts, verdict}). Never free-form prose parsed after the fact.agent-runner.ts, regardless of what the prompt says. Only Dev writes source. Only Retro and Memory Compact touch project memory.run-pipeline.sh, typecheck, diagram presence, verdict value, decides retry, halt, or handoff. The model never decides its own control flow.The safety model
Nine independent guardrails, each enforced in code rather than requested in a prompt. None of them wait on the model to cooperate.
Dev batching
Found live on a thirteen-file feature: asking Dev for the complete content of every touched file in one call hit the model's real output ceiling. It truncated mid-JSON even at 64,000 tokens, at real cost. Toggle the two behaviors below.
Below project.devFileBatchSize impacted files (default 6), Dev still runs exactly one call: the same behavior as before batching existed. run-pipeline.sh never learns how many internal batches it took.
Configuration
Generated once by afp-setup. Full list lives in skills/afp-setup/assets/module.yaml.
Choosing a backend
Every role calls whichever backend .ai/config.json points at. Same schema validation, same retries, same guardrails either way.
openai‑compatible
the default- Needs an API key:
OPENROUTER_API_KEYor a customllm.apiKeyEnv fetch()to any OpenAI-compatible chat-completions endpoint- Works with OpenRouter, OpenAI, Azure OpenAI, Groq, Together, Fireworks, or a local Ollama
- Retries keyed off HTTP status: 429, 500, 502, 503
claude‑cli
subscription only, no key- Needs only
claude setup-token: a Claude subscription, no API key at all - Shells out to
claude -p --json-schemainstead of an HTTP call llm.maxBudgetUsdoptionally caps spend per individual call- Retries keyed off exit code and
is_error
Roadmap
Known limitations, tracked rather than fixed reactively.
Quick start
Node.js 18 or newer, an AI coding tool, and a BMad Method install.