Structure for your coding agents.
A repository-agnostic hybrid dev loop. Point it at any repo — it routes tickets into an autonomous lane (headless agents, one ticket → one PR) or a human-in-the-loop lane (research → your approval → gated build). Runs on Claude Code or Cursor, locally or in any remote environment.
Quickstart · Modes · Switch runtime · Commands · Lanes · Configure · How it works
Modern coding agents are powerful but unstructured — you point one at a task and hope. Dyflo is the structure around the agent: it decides which work goes fully autonomous vs. which needs you in the loop, gives each path the right tools, and keeps a live map of your codebase so decisions are grounded in what the change actually touches.
It composes five proven pieces (a code-graph, a lazy-coding ruleset, an unattended watcher, a human-gated flow, and your choice of models) into one loop, and adds the two things none of them provide on their own: a router (which lane?) and a research stage (what's the blast radius, and what pattern fits?).
flowchart LR
T([📥 ticket]) --> R{ROUTER<br/>by label}
R -->|auto| A[AUTONOMOUS lane]:::auto
R -->|hitl / unlabeled| S[RESEARCH stage<br/>blast radius + pattern]:::hitl
A --> W[watcher →<br/>headless agent]:::auto
W --> PR([✅ one PR → exit]):::done
S --> ADR[draft ADR<br/>cited pattern]:::hitl
ADR --> H((👤 you<br/>approve / edit)):::gate
H --> B[plan → implement<br/>→ release]:::hitl
B --> PR2([✅ PR — gated]):::done
S -. no pattern needed .-> R
classDef auto fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef hitl fill:#fef9c3,stroke:#ca8a04,color:#713f12;
classDef gate fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,font-weight:bold;
classDef done fill:#e0f2fe,stroke:#0284c7,color:#075985;
linkStyle default stroke-width:2px;
One safety invariant: the router only ever downgrades (the dotted arrow — research can send a small change back to the auto lane). An unlabeled ticket never lands in the autonomous lane unattended; it goes through research first. Nothing ever escalates the other way.
Local (personal machine):
git clone https://github.com/Amanuel2x/dyflo.git ~/dyflo && cd ~/dyflo
./install.sh # Claude Code (default)
# or: ./install.sh --runtime cursor
cd /path/to/your/project
dyflo --bootstrap # build the graph, hooks, vendor ponytail, write config
dyflo --assign # route open tickets into lanesRemote (container / cloud VM / CI) — one command from a bare box:
curl -fsSL https://raw.githubusercontent.com/Amanuel2x/dyflo/master/remote-bootstrap.sh | bashThis goes from nothing installed to a working dyflo command with zero interactive prompts — it fetches uv + graphify, installs Dyflo for your runtime, validates auth, and self-checks. Add --runtime cursor to target Cursor instead of Claude Code.
On an SSH box (e.g. EC2) — set up once, then run the autonomous fleet under tmux:
# 1. one-time setup on the box
export CURSOR_API_KEY=... GITHUB_TOKEN=... # auth (warns, never stored)
curl -fsSL https://raw.githubusercontent.com/Amanuel2x/dyflo/master/remote-bootstrap.sh \
| bash -s -- --runtime cursor
cd /path/to/your/repo
dyflo --bootstrap # graph + hooks + config (once per repo)
# 2. day-to-day — one-shot commands run and exit, no tmux needed
dyflo --assign # route tickets (read-only)
dyflo --self # equipped session
dyflo --docs # architecture docs
# 3. the autonomous lane is a long-running loop — keep it alive under tmux,
# one model per window so maker ≠ checker across model families
tmux new -s dyflo
DYFLO_RUNTIME=cursor DYFLO_MODEL=claude-sonnet-4-6 python3 backend-watcher.py # coder
# Ctrl-b c →
DYFLO_RUNTIME=cursor DYFLO_MODEL=gpt-5 python3 quin-watcher.py # QA (Quin)
# Ctrl-b c →
DYFLO_RUNTIME=cursor DYFLO_MODEL=gemini-2.5-pro python3 tessy-watcher.py # tests (Tessy)
# Ctrl-b d to detach — the fleet keeps running after you log out.
# Reconnect any time: tmux attach -t dyfloThe one-shot commands (
--assign,--self,--docs) don't need tmux — they run and exit. Only the watcher loop does, since it polls forever; tmux is what keeps it alive across dropped SSH sessions.
That's the whole thing. Everything below is detail.
| Term | Meaning |
|---|---|
| Runtime | The coding-agent CLI that actually runs sessions: Claude Code (claude) or Cursor (cursor-agent). Dyflo drives either. |
| Lane | Where a ticket goes: autonomous (auto label, no human) or HITL (hitl/unlabeled, gated). |
| Router | Label → lane. Deterministic, downgrade-only. |
| Research stage | For HITL tickets: compute blast radius (Graphify), match an architecture pattern (vendored catalog), emit a draft ADR you approve. |
| Watcher | The autonomous lane's engine — polls tickets, launches a headless agent per ticket, one PR, exits, repeats. |
| Adapter | The ticket source (GitHub built-in; Jira/Linear/etc. are one small file each). |
| Agent roster | Generalist coders + two specialists: Tessy (writes/reviews tests) and Quin (QA, verifies end-to-end). |
Dyflo installs in layers. Pick the depth you need.
| Tool | Needed for | Auto-installed? |
|---|---|---|
python3 (3.10+) |
the engine | no (assumed present) |
git |
everything | no |
uv |
installs Graphify | yes (installer/bootstrap fetch it) |
graphify |
the code graph | yes |
gh CLI or GITHUB_TOKEN |
GitHub ticket adapter | no |
claude or cursor-agent |
launching agents | no (install per your license) |
| ponytail plugin | lazy-coding discipline | optional (a copy is bundled for the autonomous lane) |
| TRIP skills | the HITL lane | optional |
./install.sh # Claude Code, global (~/.claude), `dyflo` on PATH
./install.sh --runtime cursor # Cursor (.cursor/ rules + commands + mcp.json)
./install.sh --project /path/repo # project-scoped instead of global
./install.sh --no-link # skip the PATH symlinkWhat each install writes:
| Claude Code | Cursor | |
|---|---|---|
| Instructions | ~/.claude/skills/dyflo/ |
.cursor/rules/dyflo.mdc (always-apply) |
| Watcher | ~/.claude/skills/dyflo-watcher/ |
.cursor/rules/dyflo-watcher.mdc |
| Doc agent | ~/.claude/agents/doc-cartographer.md |
.cursor/rules/doc-cartographer.mdc |
| Commands | Claude slash commands | .cursor/commands/dyflo-research.md, dyflo-docs.md |
| Graphify MCP | claude mcp add |
.cursor/mcp.json |
| Headless agent | claude -p --dangerously-skip-permissions |
cursor-agent -p --force --sandbox disabled |
# devbox: full setup on a box you'll work on (default)
./remote-bootstrap.sh --mode devbox --runtime cursor
# ci: lean one-shot for a pipeline (no PATH symlink, no menu)
./remote-bootstrap.sh --mode ci --runtime claude --repo owner/nameIt installs uv + graphify from scratch, clones/uses the source, installs Dyflo for the runtime, validates auth env vars (warns, never blocks), runs the self-checks, and in devbox mode bootstraps the current repo.
Auth on a remote box (no interactive login) — set what you use:
| Env var | For |
|---|---|
GITHUB_TOKEN |
ticket adapter + PRs |
ANTHROPIC_API_KEY |
headless claude |
CURSOR_API_KEY |
headless cursor-agent |
Secrets are validated for presence only — never stored or printed.
Dyflo is intentionally multi-modal. Three independent axes combine into the mode you run:
Axis 1 — Runtime (which agent CLI): claude · cursor
Axis 2 — Operation (what you're doing): assign · self · docs · watcher · bootstrap · check
Axis 3 — Environment (where): local · remote devbox · CI
| Mode | Command | What it does | Human? |
|---|---|---|---|
| Bootstrap | dyflo --bootstrap |
Per-repo setup: graph, re-index hooks, vendor ponytail, write config, create labels | one-time |
| Assign (route) | dyflo --assign |
Read all open tickets, sort them into auto/hitl lanes. Read-only, launches nothing |
no |
| Assign (research) | dyflo --assign <id> |
Run the research stage on one HITL ticket → draft ADR or downgrade | reviews ADR |
| Self | dyflo --self |
Open an equipped interactive session (graph + rules + MCP loaded) and work it yourself | you drive |
| Docs | dyflo --docs [focus] |
Generate docs/ARCHITECTURE.md with Mermaid diagrams from the graph |
no |
| Watcher | python3 <name>-watcher.py |
The autonomous lane: poll auto tickets, one headless agent per ticket → PR → exit |
no |
| Check | dyflo --check |
Report the active runtime + run all engine self-checks | no |
| Environment | Entry | Notes |
|---|---|---|
| Local interactive | dyflo <cmd> |
Full experience; the dyflo command on your PATH. |
| Remote devbox | remote-bootstrap.sh --mode devbox |
Persistent cloud box; leaves you a working dyflo. |
| CI / pipeline | remote-bootstrap.sh --mode ci + dyflo.yml |
Non-interactive, ephemeral, secrets-based auth. No TTY needed — the launcher detects a non-interactive shell and never blocks on a prompt. |
# Personal, Claude, route tickets:
dyflo --assign
# Work, Cursor, research one big ticket with GPT-5 doing the thinking:
DYFLO_RUNTIME=cursor DYFLO_MODEL=gpt-5 dyflo --assign 142
# Remote dev box, Cursor, full setup then document the repo:
./remote-bootstrap.sh --mode devbox --runtime cursor && dyflo --docs
# CI, Claude, hourly triage (via the bundled workflow):
# .github/workflows/dyflo.yml — copy, add secrets, done.
# Autonomous lane at work with a QA agent on GPT and a coder on Claude:
DYFLO_RUNTIME=cursor DYFLO_MODEL=claude-sonnet-4-6 python3 backend-watcher.py &
DYFLO_RUNTIME=cursor DYFLO_MODEL=gpt-5 python3 quin-watcher.py &Dyflo's engine is plain Python/shell — it doesn't care which runtime drives it. Switching is a one-word change, at three possible levels (most specific wins):
1. Per invocation (env var) — instant, no reinstall:
DYFLO_RUNTIME=cursor dyflo --assign # this run uses Cursor
DYFLO_RUNTIME=claude dyflo --assign # this run uses Claude Code2. Per repo (config) — sticks for that project:
3. Per machine (install) — writes the runtime's native config layout:
./install.sh --runtime cursor # installs .cursor/ rules+commands+mcp
./install.sh --runtime claude # installs ~/.claude skills+agentsIf you set nothing, Dyflo auto-detects: it uses whichever CLI is on your PATH (cursor-agent or claude), defaulting to claude.
The point: the same repo can be worked with Claude at home and Cursor at work. The graph, the pattern catalog, the ADRs, the config — all shared. Only the agent that runs the session changes.
Dyflo resolves which agent runs from what's actually on the machine — you don't have to configure it:
- Runtime: explicit
DYFLO_RUNTIME(env or config) wins; else whichever ofclaude/cursor-agentis installed; if both are installed and nothing is set, it usesclaudeand prints a one-line note telling you how to switch. - Another agent CLI? (codex, grok, gemini, …)
dyflo --setupscans your PATH, shows what it found, and lets you pick "other" — you name the CLI and its flags once, and it's stored indyflo.config.jsonunderruntimes. The engine stays generic; no code changes. (Dyflo can't know a third-party CLI's flags, so you supply them — a wrong flag surfaces loudly on first run.) - Model: with
DYFLO_MODELunset, Dyflo passes no--modelflag — so each CLI uses its own configured default (whatever model you picked in Claude Code / Cursor). SetDYFLO_MODELonly to force a specific one.
dyflo --check prints the resolved runtime and model source, so you always know what will run.
Cursor exposes whatever your plan offers (cursor-agent --list-models). Set the agent's model with DYFLO_MODEL:
DYFLO_MODEL=gpt-5 dyflo --assign 142
DYFLO_MODEL=gemini-2.5-pro python3 tessy-watcher.pyThis unlocks real maker ≠ checker: run the coding agent on one model family and the review pass on another (author with Claude, review with GPT or Gemini). Different families catch each other's blind spots far better than same-model self-review — a stronger check than the single-model default.
dyflo --setup welcome wizard: pick your agent CLI (scans PATH), model, and ticket
source; checks auth; offers to bootstrap. Runs automatically the
first time you open dyflo in a repo with no config.
dyflo persistent interactive menu (assign/research/self/docs/status/adr/check);
shows runtime+model+repo, returns to the menu after each action;
type a QUESTION at the prompt instead of a number and a small fast
model answers it — and can configure Dyflo for you ("switch me to
cursor", "always use gpt-5", "rename the auto label");
no-ops cleanly with no TTY
dyflo --bootstrap one-time per-repo setup (graph, hooks, ponytail, config, labels)
dyflo --assign route all open tickets into auto/hitl lanes; then pick one (H<n> to
research a HITL ticket, A<n> to preview an auto one) — reuses the same
routing, no re-fetch
dyflo --assign <id> run the research stage on one HITL ticket → draft ADR or downgrade
dyflo --self open an equipped interactive session and work it yourself
dyflo --docs [focus] document the repo → docs/ARCHITECTURE.md with Mermaid diagrams
dyflo --status per *-watcher.py: running? label, queue depth, last logs, your open PRs,
and the tail of the event log
dyflo --adr [n [approve|reject]] list ADRs with parsed status; approve/reject flips the Status
line, and approve prints (or offers to run) the seeded TRIP plan step
dyflo --check report runtime + run engine self-checks
Every headless run tees its output to ~/.dyflo/logs/<timestamp>-<op>.log; watcher outcomes append to ~/.dyflo/events.jsonl (read by --status).
Environment variables the launcher honors:
| Var | Effect |
|---|---|
DYFLO_RUNTIME |
claude | cursor — overrides config + auto-detect |
DYFLO_MODEL |
model id passed to the runtime (e.g. gpt-5, claude-sonnet-4-6) |
DYFLO_REPO |
owner/name for the GitHub adapter (else auto-detected from gh) |
DYFLO_ADAPTER |
ticket source (default github) |
DYFLO_ASK_MODEL |
model for the menu's ask-line (default claude-haiku-4-5 on claude; the CLI's own default on cursor) |
DYFLO_ATTENDED |
1 → run a would-be-headless session interactively (no -p / no --force) so you can watch and steer it |
DYFLO_NOTIFY_CMD |
if set, each watcher event line is piped to it (wire up Slack/ntfy without any coupling) |
DYFLO_STATE_DIR |
where logs + events.jsonl live (default ~/.dyflo) |
--runtime claude|cursor which runtime to install for (default: auto-detect → claude)
--project DIR project-scoped install instead of global
--no-link skip the PATH symlink
--mode devbox|ci full setup vs lean one-shot (default: devbox)
--runtime claude|cursor runtime to install for
--repo owner/name target repo (CI)
--ref BRANCH Dyflo branch/tag to install (default: master)
--src DIR use a local checkout instead of cloning
--no-repo-bootstrap don't `dyflo --bootstrap` the current repo (devbox)
The watcher (dyflo-watcher skill) polls your ticket source for a label, and when there's work with no open PR of its own, launches a fresh headless session that does exactly one ticket, opens a PR, and exits — then the watcher relaunches it for the next. It self-heals: a failing-CI or CONFLICTING PR of its own gets fixed/rebased first.
The roster — an agent is a label + a mission brief + its own login:
| Agent | Role | Behavior |
|---|---|---|
| Generalist coders | scope-agnostic | take a ticket, fix code, open a PR (label scopes the work) |
| Tessy | test author | writes/strengthens tests, test-only PRs; never touches product code; files a bug instead of silently fixing; never weakens a test to go green |
| Quin | QA verifier | exercises the change end-to-end (runs the app / hits the route); PASS-with-evidence or a bug issue; writes no product code — a green suite is a signal, not a verdict |
Each agent runs isolated (CLAUDE_CONFIG_DIR / CURSOR_CONFIG_DIR), so different logins/models don't collide. Set up via the dyflo-watcher skill; run each as python3 <name>-watcher.py.
Dual-use warning: watchers run headless with skip-permissions on whatever account each config dir is logged into. Log in and test-run one agent before arming the rest.
For every HITL ticket, Dyflo:
- Blast radius —
graphify affected "<symbol>"/pathover what the ticket touches: how far it ripples, which hotspots (god_nodes) are in scope, whether it sits on a boundary. - Pattern match —
dyflo/patterns/lookup.pyscores the ticket against the vendored catalog (28 patterns from GoF · Fowler PoEAA · microservices.io · Hohpe EIP), each hit cited to its canonical URL; a miss falls back to live retrieval. - Security + ponytail gate — auth/secrets/input-validation tickets stay HITL regardless of size. Otherwise: does this even need a pattern? A small local change emits
NO_PATTERN, gets relabeledauto, and drops to the autonomous lane. No ceremony. - Draft ADR — otherwise writes
docs/adr/NNN-<slug>.md(adr.github.io format): context → blast radius (citedfile:line) → chosen pattern → consequences. This ADR is both the research output and the thing you approve — it seeds the plan step.
On approval, the human-gated build (TRIP if installed: plan → implement → release) picks it up, with gates at the plan and the diff. Nothing ships without your sign-off.
dyflo --bootstrap writes dyflo.config.json in the target repo:
{
"adapter": "github",
"labels": { "auto": "auto", "hitl": "hitl" },
"runtime": "claude"
}- Different labels? Rename
auto/hitlto whatever your team uses — the router honors it (semantics unchanged). - Different runtime? Set
"runtime": "cursor"(or useDYFLO_RUNTIME). - Different ticket source? Set
adapterand drop adyflo/adapters/<name>.pyexposinglist_open_tickets(label)andset_label(id, label)returning the envelope{id, title, body, labels, url}. GitHub ships built-in; Jira/Linear/flat-file are each one small file — no core changes. Seedyflo/adapters/github.pyas the template. - Different repo for the adapter?
export DYFLO_REPO=owner/name.
dyflo --docs runs the doc-cartographer agent: it reads the codebase's knowledge graph and writes docs/ARCHITECTURE.md with Mermaid diagrams generated from the real structure — a system map (subsystems), a module dependency map, and call-flow diagrams per entry point — every claim cited to file:line, nothing from memory. Diagrams are portable Mermaid (render in GitHub/Obsidian). Safe to re-run as the code evolves (the graph re-indexes on commit).
dyflo.sh launcher (symlinked to `dyflo`)
install.sh local installer (--runtime claude|cursor)
remote-bootstrap.sh bare box → ready (--mode devbox|ci)
mcp-server.json graphify MCP block for manual registration
.github/workflows/ dyflo.yml — ready CI workflow
dyflo/ the engine (runtime-agnostic Python + shell)
router.py label → lane (no-escalation invariant); --json for the picker
runtime.sh claude|cursor abstraction (rt_headless child+log / rt_exec_interactive / rt_mcp_add)
adr.py list/gate ADRs — parse status, approve/reject, seed the TRIP plan step
config.py validated read/write of dyflo.config.json (what the ask-line configures through)
status.py --status: watcher liveness, queue depth, logs, open PRs, events (via gh)
events.py ~/.dyflo/events.jsonl writer/reader + optional DYFLO_NOTIFY_CMD pipe
adapters/ ticket-source adapters (github built-in) + selfcheck
patterns/ catalog.json (28 patterns, 4 sources) + lookup.py matcher
docs/ graph_to_mermaid.py — graph.json → portable Mermaid
vendor-ponytail.sh put ponytail's ruleset into the target repo's AGENTS.md
vendor/ bundled ponytail AGENTS.md (MIT) — fallback for a bare box
agents/ doc-cartographer.md — the documentation agent
skill/ the /dyflo skill (SKILL.md + references)
watcher/ /dyflo-watcher — the autonomous lane (engine + generalist/Tessy/Quin briefs)
docs/adr/ where the research stage writes ADRs (template.md included)
docs/EXTERNAL-TOOLS.md what each composed tool does, with before/after examples
| Part | Tool | Role |
|---|---|---|
| Persistent memory + blast radius | Graphify | tree-sitter code graph; affected/path = "what breaks if I change X" |
| Pattern selection | vendored catalog | GoF · Fowler PoEAA · microservices.io · Hohpe EIP index; live fallback |
| Autonomous loop | dyflo-watcher (ships with Dyflo) |
polls the source, launches headless agents, one-ticket-one-PR; generalist + Tessy + Quin |
| Code-writing discipline | ponytail | "lazy senior dev" ruleset, vendored into the repo for the headless lane |
| Human-gated flow | TRIP | plan → implement → release, seeded by the ADR |
| Models | Claude Code / Cursor | your choice of runtime and model; enables maker ≠ checker |
Dyflo itself is the router and the research stage — the two pieces none of those provide — plus the glue that makes them one loop. Full explanations with before/after examples: docs/EXTERNAL-TOOLS.md.
Do I need all the external tools? No. Graphify is the one real dependency (the installer fetches it). Ponytail and TRIP are optional — a ponytail copy is bundled for the autonomous lane, and without TRIP the HITL lane still produces the ADR; you just drive the build yourself.
Does it merge my PRs? Never. Dyflo dispatches and researches; humans merge. That's a hard non-goal.
Can it schedule itself (cron/launchd)? No — scheduling is out of scope by design. The autonomous loop is the watcher process you run; the CI workflow is the scheduled entry point if you want one.
Will the autonomous lane run without a human? Only for tickets you explicitly labeled auto (or that research downgraded). Unlabeled work never runs unattended.
Cursor caveats? Confirm your model IDs with cursor-agent --list-models; cursor-agent -p has been reported to occasionally hang on exit (wrap CI in a timeout).
Dyflo composes several open tools — install their plugins for the full experience:
Graphify,
ponytail (MIT — a copy of its AGENTS.md is bundled in dyflo/vendor/ so the autonomous lane works on a bare box; see dyflo/vendor/ponytail-LICENSE), and
TRIP. See docs/EXTERNAL-TOOLS.md for what each does.
Fork it, bend it to your workflow. Before a PR, run the test suite — CI runs the same thing on Python 3.10–3.13:
./test.sh # compile + every module's --self-check + bash -n + shellcheck. Offline, no graphify/gh.Every engine module carries its own --self-check, and test.sh discovers them, so a new module with a self-check is tested automatically — no CI edits. See CONTRIBUTING.md for the house style (portable bash for macOS 3.2, the no-escalation invariant, the adapter seam).
Dyflo never merges PRs, never schedules cron/launchd, and never escalates a ticket into unattended execution. It dispatches; humans merge; the watcher loops.
MIT — see LICENSE.
