whyline-relay
Give it a Markdown plan. It runs each task through Codex (implements) and Claude (reviews and commits), one task at a time, unattended, and stops when something needs a human.
plan.md ┌──────────────── one task ────────────────┐
┌───────────────┐ │ │
│ - [ ] T-1 ... │──next task─▶│ Codex implements ─▶ hands off for review │
│ - [ ] T-2 ... │ │ ▲ │ │
└───────────────┘ │ changes requested ▼ │
▲ │ └──────────── Claude reviews │
│ │ runs the tests │
│ ticks the box, │ commits, approves │
│ commits plan.md └──────────────────────────┬───────────────┘
└─────────────────────────────────────────────────┘
It never types into a terminal for you and never pushes. Each agent runs headlessly (codex exec, claude -p), one turn at a time. The relay decides whose turn it is by reading whyline's handoff record. It does not parse agent output to route.
Status: 0.2. The 0.2 features (
remove,--version, the progress lines and the truthfulstatus, the verified-review rule) were built by running this tool on its own plan: Codex implemented, Claude reviewed and committed. It has run on nine real tasks on one macOS machine. Treat it as early software and read the safety section before pointing it at anything valuable.
Contents
- How it works
- Requirements
- Install
- Quick start
- Writing a plan
- Running it
- When it pauses
- Command reference
- Configuration
- Permissions and safety
- What it writes
- Removing the relay
- What it costs
- FAQ
- Limitations
- Development
How it works
For each unchecked task in the plan, in file order:
- Codex implements. The relay launches
codex execwith a prompt made of the task text, whyline's current context, and instructions to run the tests, record decisions, and hand off withwhyline handoff ... --status ready-for-review. Codex must not commit. - The relay checks Codex did not commit. If the branch's HEAD moved, it pauses.
- Claude reviews. The relay launches
claude -pwith a review prompt. Claude reads the diff, runs the tests itself, and either commits with the task id in the message and hands offapproved, or hands offchanges-requestedwith concrete feedback. If it cannot run the tests it must hand offblockedrather than approve. - On
changes-requested, Codex gets the feedback and tries again, up tomax_rounds(default 3). - On
approved, the relay verifies that a commit naming the task exists, that nothing else is left uncommitted, then ticks the plan checkbox and makes one small commit of onlyplan.md.
The control plane is whyline's .whyline/active-handoff.json. A new handoff event decides the next move; a missing or unchanged one means the agent stopped without handing off, and the relay pauses. It never guesses a verdict from exit codes or text.
Work happens on its own branch (default relay/<plan-name>), created from wherever you are. You review and merge it. Nothing is pushed.
Requirements
- Python 3.11 or newer, and a git repository to run in.
whylineinstalled, andwhyline initrun once in that repository. The relay reads and writes whyline handoffs; without itstartstops with "whyline is not initialised here".- The
codexandclaudecommand-line tools, installed and logged in. Tested withcodex-cli 0.155.1and Claude Code2.1.278. - Your project's own dependencies already installed (for example
uv sync). Codex runs in a sandbox without network access, so it cannot install anything mid-run.
Tested on macOS. Linux runs the test suite in CI. Windows is untested.
Install
uv tool install whyline-relay
whyline-relay --version
Upgrade with uv tool upgrade whyline-relay, remove with uv tool uninstall whyline-relay. From a clone: uv tool install ..
Upgrading from 0.1
uv tool upgrade whyline-relay
The program upgrades, but each repository keeps the files init wrote earlier, and 0.2 ships an improved review prompt and wider permissions (the reviewer must run the tests itself and may not approve on the implementer's word; read-only helpers such as tail and grep; .venv/bin/pytest). To pick them up in a repository you set up with 0.1:
cp -r .whyline/relay /tmp/relay-backup # keep your edits
whyline-relay init --overwrite # replaces config, prompts and permissions with the 0.2 defaults
# then re-apply your own edits from the backup (timeouts, project test command, ...)
git diff .whyline/relay # review what changed, then commit it
Or apply just the two changes by hand: add Bash(tail:*), Bash(head:*), Bash(wc:*), Bash(grep:*), Bash(ls:*) (and for Python Bash(.venv/bin/pytest:*), Bash(.venv/bin/python:*)) to claude-settings.json, and copy the "How to review" paragraph from a fresh init into prompts/review.md.
Quick start
In the repository you want to work on (use a branch or a throwaway clone the first time):
cd my-project
uv sync # or whatever installs your dependencies: agents cannot, mid-run
whyline init --yes # once per repository: whyline's own hooks and instruction files
whyline-relay init # relay config, Claude's permissions, prompt templates
Write a plan (see Writing a plan), then commit everything, because start refuses a dirty working tree:
cat > plan.md <<'EOF'
- [ ] APP-1: Add a --verbose flag to the CLI
Add `--verbose` to `myapp.cli`. When set, print each step to stderr.
Add tests for both the default and the verbose output. Change nothing else.
EOF
git add -A && git commit -m "chore: configure whyline and the relay"
Preview, then run:
whyline-relay start --dry-run # prints the next task's prompt and the command; launches nothing
whyline-relay start # the real run. Leave this terminal alone.
When it finishes you have a branch to review:
git log --oneline main..relay/plan
git diff main..relay/plan
Writing a plan
The plan is a Markdown checklist. plan.md in the repository root is the default; --plan PATH or the plan config key change it.
# My project
Any prose here is ignored. So are headings.
## Phase 1: foundations
- [x] APP-1: Already done, skipped
- [ ] APP-2: Add the parser
Implement `parse(text)` in `myapp/parser.py`.
It returns a list of `Item` objects and raises `ParseError` on bad input.
Add unit tests, including the error case. Change nothing else.
- [ ] APP-3: Expose it on the command line
- Add a `parse` subcommand that reads a file and prints the items.
- Exit 1 with a clear message on `ParseError`.
## Phase 2: features
- [ ] APP-4: ...
The rules
- A task is a line
- [ ] ID: title, followed by indented lines with the details. The details end at the next unindented line or the next checkbox. - Always start a task with
ID:. If you leave it out, the first word silently becomes the id (- [ ] Add a cachegets the idAdd). Ids must be unique; duplicates are rejected. - [x]tasks are skipped. Tasks run in file order, and each builds on the commits before it.- Headings, prose and blank lines between tasks are ignored, so use them for your own structure.
- The plan is re-read after every task, so you can append tasks while a run is going. Do not edit a task that is in flight.
Writing tasks that work
Each task is handed to a fresh Codex session with no memory of earlier tasks, so it must stand alone. What worked in practice:
- Say what to build, where it goes, and the edge cases that matter.
- Say what tests to add, and what must not change ("change nothing else" prevents scope creep).
- Keep a task to about one reviewable change. Vague or huge tasks are what cost extra review rounds.
- If a behaviour is specified by exact message text or exit codes, quote them.
Phases
There is no built-in "stop after phase" gate. To get a review point between phases, use one plan file per phase:
whyline-relay start --plan phase-1.md --branch relay/work # ten tasks, then you review
whyline-relay start --plan phase-2.md --branch relay/work # continues on the same branch
Passing the same --branch keeps the work stacked. Without it, each plan gets its own branch, relay/phase-1 and relay/phase-2, and a new branch starts from whatever you have checked out. So merge phase 1, or git switch relay/phase-1, before starting phase 2, or phase 2 will not include phase 1's work.
Running it
Run whyline-relay start in a terminal of its own, not inside an agent session. The relay launches Codex and Claude itself; you do not open them.
While it runs it prints its own progress lines, then the agents' output:
[13:06:44] ==> codex: implementing APP-1 (round 1 of 3)
[13:06:46] ... codex still running (2s)
[13:06:49] <== codex finished in 5s
[13:06:49] ==> claude: reviewing APP-1 (round 1 of 3)
[13:06:55] <== claude finished in 0s
[13:06:55] ==> relay: ticked APP-1 in the plan
Plan complete: 1 task(s) approved and committed.
- The
... still runningheartbeat appears after 30 seconds of silence and repeats every 30 seconds. SetWHYLINE_RELAY_HEARTBEAT_SECONDSto change it. - These lines go to the terminal only, never into the log files.
- Claude's turn is silent until it ends. Its output arrives as one JSON line at the end, so the heartbeat is the only sign of life. This is normal.
- A finished task shows as two commits: Claude's
feat: ... (APP-1)and the relay'schore: tick APP-1 in the plan. - On completion or a pause you also get a desktop notification, where the platform supports it (macOS, or
notify-sendon Linux).
Watching from another terminal
whyline-relay status
While a run is live this prints, for example, Running: claude reviewing APP-1, round 1, since 13:06:49 (2m ago). If the run is paused it shows the reason and the log path instead. Also useful: git log --oneline --graph --all, and tail -f on a file in .whyline/relay/logs/.
Stopping
Ctrl+Cin the relay's terminal stops the running agent, saves state, and exits with code 2.whyline-relay resumecontinues.whyline-relay stopfrom another terminal lets the current agent finish and starts nothing new.
Only one relay can run in a repository at a time. start and resume refuse to begin while another live run exists. Separate repositories can run in parallel, but they share your agent quotas.
When it pauses
The relay stops at the first problem instead of pushing on. It prints Paused: <reason>, the log path, and Resume with: whyline-relay resume. Fix the cause, then resume; it re-enters at the same decision point (Codex is not re-run if it already handed off).
| The reason says | What it means, and what to do |
|---|---|
<agent> exited without handing off; it was denied permission to run: ... |
Claude tried commands the allowlist does not cover. Add them to .whyline/relay/claude-settings.json, then resume. |
<agent> exited without handing off; its last output was: "..." |
The agent stopped without a handoff. The quoted line is usually the cause. Check the log. |
<agent> reported blocked: <summary>. Question: <q> |
The agent needs a human decision. The question says what. Answer it (often: add a permission), then resume. |
codex made a commit, which the relay forbids ... Undo it with git reset <sha> |
Codex committed. Run the given command (your files stay), then resume. |
<task> was approved and committed, but files are still uncommitted (...) |
Something was left out of Claude's commit. git stash -u it (do not commit it), then resume. |
<task> was approved but no commit naming it exists |
Claude approved without committing, or the message lacks the task id. Commit by hand with the id, or run the task again. |
<task> hit the 3-round cap without an approval |
Codex and Claude could not agree. Read the last two logs, then clarify the task in plan.md or finish it by hand. |
<agent> hit a usage or rate limit |
You ran out of quota. Wait for the reset, then resume. |
<agent> handed off for '<other task>', but this run is on '<task>' |
The agent mislabelled its handoff. Check the log, then resume. |
unrecognised handoff status '...' |
An agent used a status the relay does not know. Check the log. |
... exceeded <n>s and was terminated |
An agent ran past timeout_minutes. Raise it (--timeout) or split the task. |
... is not installed or not on PATH |
The codex or claude command was not found. |
Exit codes: 0 success, 1 an error or refusal, 2 paused or interrupted.
Command reference
whyline-relay [--version] <command>
init writes the relay's setup into .whyline/relay/, after asking.
--repo REPO Use this repository root (default: current directory). --yes Skip the confirmation question. --overwrite Replace files that already exist, discarding your edits.
It detects a Python project (pyproject.toml), a Node project (package.json), or neither, and picks the permission preset to match. Declining, or having no terminal, writes nothing.
init is safe to re-run. A file that does not exist is written; a file identical to what it would write is left alone; a file that exists and differs is kept, with a line such as Kept .whyline/relay/prompts/review.md: it already exists and differs. Run with --overwrite to replace it. Pass --overwrite to replace every file with the defaults, discarding your edits.
start runs the plan from its first unchecked task.
--repo REPO Use this repository root (default: current directory).
--plan PLAN Use this plan file (default: from config).
--dry-run Print the next task's prompt and command without launching anything.
--only TASK_ID Run only the named task.
--branch BRANCH Use this work branch (default: relay/<plan-name>).
--allow-main Allow running on main or master.
--allow-dirty Skip the clean-working-tree check.
--max-rounds N Override the review-round limit from config.
--timeout MIN Override the per-agent timeout from config, in minutes.
resume continues after a pause, on the saved branch, from the saved decision point.
--repo REPO, --allow-dirty as above.
status shows whether a run is live, or where a paused one stopped. --repo REPO.
stop lets the current agent finish and starts nothing new. --repo REPO.
remove takes the relay out of a repository; see Removing the relay.
--repo REPO, --yes, and --force Remove even while a run is paused.
Configuration
init writes .whyline/relay/config.toml. Every key is optional; anything you omit uses the default shown.
plan = "plan.md" # the plan file, relative to the repository root
max_rounds = 3 # review rounds per task before it pauses
timeout_minutes = 30 # per agent turn; the agent's whole process group is killed after this
branch_prefix = "relay/" # the default work branch is branch_prefix + the plan file's name
[agents.codex]
command = ["codex", "exec", "-s", "workspace-write", "--color", "never"]
[agents.claude]
command = ["claude", "-p", "--permission-mode", "acceptEdits", "--output-format", "json",
"--settings", ".whyline/relay/claude-settings.json"]
[status_map] # the handoff status strings the relay routes on
review = "ready-for-review"
changes = "changes-requested"
approved = "approved"
blocked = "blocked"
assigned = "assigned"
- The prompt is appended to each command as its last argument.
- The two roles are named
codexandclaudein the handoff protocol. You can change the command each one runs, but the names are fixed. - Prompts:
.whyline/relay/prompts/implement.mdandreview.mdare the wrapper prompts sent each turn. They are yours to edit. Placeholders are{task_id},{task_text},{sync_packet},{round}and{review_feedback}, substituted as plain text (so braces in JSON examples are safe).--dry-runshows the assembled Codex prompt. - Where instructions reach the agents from: the task in the plan, the two prompt templates,
AGENTS.md(whyline's block, which agents read on their own), and, for Claude, the permissions file.
Permissions and safety
Read this before using it on anything valuable.
Claude's permissions live in .whyline/relay/claude-settings.json and are passed with --settings. That is deliberate: Claude Code ignores a project's .claude/settings.json permissions under claude -p until the workspace has been trusted interactively, so a fresh checkout would silently get none. init writes:
- Always:
Edit;git add,commit,diff,status,log;whyline; and read-only helperstail,head,wc,grep,ls. - Python projects add
pytest,uv run pytest,uv run,.venv/bin/pytest,.venv/bin/python. Node projects addnpm test,npm run,npx. - Always denied:
git pushandrm -rf.
Edit the file to add your project's test command so the reviewer can run tests. The allowlist matches the start of a simple command: an environment-variable prefix, a pipe into something not listed, or a ; chain is denied.
The allowlist is a convenience, not a security boundary. Broad entries such as uv run, npm and npx execute arbitrary project code. Run unattended relays on an isolated branch, in a repository (or a fresh clone) that holds no valuable secrets, and read the resulting branch before merging it.
Codex runs with -s workspace-write: it can write inside the repository, has no network, and can be told to run tests. That sandbox does not stop it from running git commit (measured on codex-cli 0.155.1). The prompt forbids it and the relay checks HEAD after every Codex turn, but that is a check, not a wall.
What the relay does to protect you
- Never runs
git push, and never passes a permission-bypass flag (--dangerously-*). Both are enforced by the test suite. - Refuses to start on
mainormasterwithout--allow-main, and refuses a dirty working tree without--allow-dirty. - Checks that Codex did not commit, that an approval is backed by a commit naming the task, and that nothing was left uncommitted afterwards.
- Reviews are fail-closed: the review prompt tells Claude not to approve on the implementer's word if it cannot run the tests, but to hand off
blockedand name the denied command. - Kills an agent's whole process group on timeout and on Ctrl+C, and refuses to run two relays in one repository.
- Stops at the first problem.
What it cannot do: it cannot tell you the reviewer was right. An automated review is lighter than a careful human one, so read the branch.
What it writes
Everything the relay adds is under .whyline/relay/:
| Path | What | In git? |
|---|---|---|
config.toml |
settings (above) | yes: commit it |
claude-settings.json |
Claude's permissions | yes: commit it, it is reviewable |
prompts/ |
the two editable prompt templates | yes |
logs/ |
one file per agent turn, <task>-<round>-<agent>.log |
no, ignored locally |
state.json |
saved progress of a paused run | no, ignored locally |
STOP |
written by whyline-relay stop |
no, ignored locally |
running.json |
marks a live run (agent, task, round, pid) | no, ignored locally |
"Ignored locally" means the relay adds those four lines to .git/info/exclude, which is never committed, so they neither dirty your tree nor end up in a task's commit.
The relay itself commits exactly one thing: plan.md, when it ticks a box (chore: tick <id> in the plan). Everything else in git history is Claude's commits, plus your own.
It never changes whyline's files, AGENTS.md, CLAUDE.md, .claude/, or .codex/.
Removing the relay
whyline-relay remove
This deletes .whyline/relay/ and the four relay lines from .git/info/exclude, after listing what will go and asking Remove these? [y/N] (default no; no terminal counts as no; --yes skips the question). It:
- refuses while a run is paused (
resumeit first, or pass--force), and refuses while a run is live, even with--force; - reports how many of the files git tracks, and reminds you the deletions need committing (it never runs
git addorgit commit); - never follows a symlink out of the repository, and never deletes outside it;
- does nothing, and says so, if the relay is not set up.
It leaves whyline's own files, your history, and your branches alone. To uninstall the program itself: uv tool uninstall whyline-relay.
What it costs
Measured on one macOS machine over the nine tasks that built 0.2 (a few dozen to a few hundred lines each):
| Typical | |
|---|---|
| Time per task | 3 to 11 minutes (Codex is most of it) |
| Codex per task | 31k to 96k tokens |
| Claude review per task | $0.15 to $0.26 |
| Review rounds | 1 (every task so far passed first time) |
At those averages a plan of 20 such tasks is roughly two hours, about 1.2 million Codex tokens and about $4 of Claude. Your quotas may pause the run before that; resume continues after the reset. These numbers are from a few runs, not a benchmark.
FAQ
Can I use it on main? Only with --allow-main. By default it works on its own branch.
What if my plan has 20 tasks in two phases? Use one file per phase; see Phases.
Can I keep working while it runs? Not in the same working tree. Use a separate clone or worktree. Agents edit the tree and the relay commits to it.
Does Codex see earlier tasks? No. Each turn is a fresh session. It sees the task text, whyline's context (recent decisions and the active handoff), and the repository as it now stands.
Where are decisions recorded? In .whyline/decisions.md, through whyline note. The prompts ask both agents to record genuine decisions, and Claude's commit includes them.
My project needs network to run its tests. Codex's sandbox has none, so those tests will fail for Codex. Install dependencies beforehand, or exclude network tests from the default command.
Can I use a different agent? You can change the command each role runs, but the roles are named codex and claude in the protocol, and the prompts and permissions assume those two tools.
It approved something wrong. It happens; the reviewer is a model. Review the branch before merging, and tighten the task text.
Limitations
- Tested on macOS with the two tool versions above; Windows is untested.
- No built-in phase gate or dependency graph between tasks: order is file order.
- Claude's turn prints nothing until it ends (JSON output).
- The relay trusts what the agents write to whyline. It verifies the commit and the working tree, not the code's correctness.
- Task ids are not validated for shape; see the plan rules.
resumere-enters a task at its saved decision point but does not preserve Codex's conversation, which is a fresh session each turn.
Development
git clone https://github.com/anishmoncivarghese/whyline-relay
cd whyline-relay
uv sync
uv run pytest
The suite never launches a real codex or claude and never pops a desktop notification; tests use small fake agents. CI runs it on Linux and macOS with Python 3.11 and 3.13. Standard library only: there are no runtime dependencies.
Design and history: the design specification and the implementation plan live in the whyline repository.
License
Apache-2.0. See LICENSE.
Release files for whyline-relay 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| whyline_relay-0.2.0.tar.gz | 51.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| whyline_relay-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 93.3 kB
Release files / whyline_relay-0.2.0.tar.gz
| Download URL | whyline_relay-0.2.0.tar.gz |
|---|---|
| Size | 51.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8422cf63795194ffde17c33864e10620af17de114d2c4fba93d8713aa4684222
|
|
BLAKE2b-256 checksum How to use checksums |
84493356b31ed3ffd3e6ea691a63a0f3017704bd4f1e245c5371f6cff82977f0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / whyline_relay-0.2.0-py3-none-any.whl
| Download URL | whyline_relay-0.2.0-py3-none-any.whl |
|---|---|
| Size | 41.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d371993021d7c7c6ddbb7a9d3040d85dfbab1e9ecb3b919b1b20ecc05c98ddc7
|
|
BLAKE2b-256 checksum How to use checksums |
a307ca4f38abaefe22cc64bf4a5c5e650f7af8df8f18f2eb8231d93f06459f60
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|