{"slug": "baton-keep-long-coding-goals-moving-across-ai-coding-agents", "title": "Baton – Keep long coding goals moving across AI coding agents", "summary": "A tool called baton keeps a single long-running coding goal alive for days by rotating it among coding agents, ending each agent's turn at USD 50 or 5 hours, whichever comes first, and dropping an agent from a goal once USD 150 has been spent in that goal. baton requires at least one of OpenAI's Codex CLI or Antigravity CLI (agy), treats Claude Code as an optional third paid by use that joins only after a Console API key is saved, and requires the Superpowers plugin for its plan, build and review rounds. A goal finishes only when its verification command exits 0 and every agent approves in consensus; an agent declaring itself done is not accepted.", "body_md": "baton keeps one long coding goal moving for days by passing it between coding agents, stopping each one before it can spend money it should not. It works with Codex and Antigravity alone; Claude is an optional third, paid by use, that only joins the queue once you save an API key for it. Its turn ends at USD 50 or 5 hours, whichever comes first, it is left out of a goal after USD 150 spent in that goal, and the monthly ceiling stays above all of that.\n\nYou give it a goal (an objective, a working directory and a verification command). It runs one agent until that agent nears its limit or its turn (5 hours) ends, hands the work to the next, and when every agent is out, sleeps until the earliest reset and carries on. It finishes only when the verification command exits 0; an agent saying \"done\" means nothing.\n\nEvery agent runs through its vendor's own CLI. baton reimplements no agent. Context moves between them through the repository, its commits and a handoff file, and each vendor's own conversation is resumed when its turn comes again.\n\nInside a goal the work follows a cycle of plan, build and review, with a strong and a working\nmodel per provider (see [Phases](#phases-plan-build-review)).\n\n```\nflowchart LR\n    subgraph queue[\"The queue: one agent runs at a time\"]\n        direction LR\n        codex[\"codex\"] --> antigravity[\"antigravity\"] --> claude[\"claude\"]\n        claude -. \"turn ends: back to the first\" .-> codex\n    end\n    queue --> cycle\n    subgraph cycle[\"Every cycle of a goal\"]\n        direction LR\n        plan[\"plan\"] --> build[\"build\"] --> review[\"review\"]\n    end\n    review --> check{\"the check passes?\"}\n    check -- \"no: back to plan\" --> plan\n    check -- yes --> consensus[\"consensus: every agent approves\"]\n    consensus -- \"an agent asks for changes\" --> build\n    consensus -- \"all approve\" --> done([\"done\"])\n```\n\nHow baton works: Codex, Antigravity and then Claude take turns; every goal runs plan, build and review, then the check, then consensus of every agent, and only then is it done.\n\nbaton does not bring its own AI or its own methods. It drives tools you install, and works with two projects it did not write. Everything it depends on or works with:\n\n| Project | What it does for baton | Required? | How to install | \n|---|---|---|---|\n| [uv](https://docs.astral.sh/uv/) and Python 3.12 or newer | installs and runs baton; baton itself has no other dependencies | required | the installer on [astral.sh/uv](https://docs.astral.sh/uv/) ; uv fetches Python 3.12 when it is missing | \n| [OpenCode](https://opencode.ai) | the chat window you type in; baton shows up in it as the model `baton/auto` | only for the chat interface: `baton run` works without it | the installer on [opencode.ai](https://opencode.ai) | \n| [Codex CLI](https://github.com/openai/codex) | runs GPT models, through your plan | at least one of Codex and Antigravity, signed in to a plan | `npm install -g @openai/codex` , then sign in | \n| [Antigravity CLI](https://antigravity.google) (`agy` ) | runs Gemini models, through your plan | at least one of Codex and Antigravity, signed in to a plan | the installer on antigravity.google, then sign in | \n| [Claude Code](https://claude.com/claude-code) | an optional third in the queue, paid by use: a turn ends at USD 50 or 5 hours, and a goal leaves it out after USD 150 | optional; needs a Console API key, not a subscription | install Claude Code, then save the key (Quick start, step 3) | \n| [Superpowers](https://github.com/obra/superpowers) | the working methods (planning, test-driven building, verification, code review) that baton puts in every round | **required** , by design: no round runs without it (a goal waits until it is installed), and`baton doctor` fails; see[Superpowers is required](#superpowers-is-required) | `git clone https://github.com/obra/superpowers ~/.config/baton/superpowers` ; it is also found automatically from the official Claude Code plugin install, and any other location needs`[superpowers] dir` | \n| [ai-memory](https://github.com/akitaonrails/ai-memory) | long-term memory across goals in the same directory | optional | download the release binary for your platform from its releases page and put it on `PATH` ; do not run`ai-memory setup` , which wires global hooks (see[Long-term memory](#long-term-memory) ) | \n| launchd | keeps `baton serve` running in the background on macOS | macOS only, recommended: `baton install-service` | built in to macOS; on Linux keep `baton serve` running in a terminal or tmux | \n\nbaton was tested with Codex CLI 0.166.0, Claude Code 2.1.318 and ai-memory 3.4.1. The stream formats and hook options it relies on are version-specific, so other versions may not behave the same.\n\n**How the pieces fit.** baton owns the rotation, the phases, the limits and the guards. Each vendor\nCLI (Codex, Antigravity, Claude Code) does the actual work, one round at a time, inside a model\nbaton chose. Superpowers gives every round its method. ai-memory gives the goals long-term memory.\nOpenCode is the chat window in front of it all.\n\n[Superpowers](https://github.com/obra/superpowers) is not optional, and there is no setting to\nturn it off. No round runs without it: a goal that cannot find it waits, still running, and looks\nagain every 5 minutes, so installing it is all that is needed. The reason is the design: Superpowers\nis the working method for every provider. Codex, Antigravity and Claude get the same planning,\ntest-driven building, verification and review discipline, in every fresh conversation, whichever\nagent happens to hold the baton. Install it as the table above says; `baton doctor` fails until it\nis found. baton finds it automatically only from the official Claude Code plugin install or from\n`~/.config/baton/superpowers`; an install anywhere else needs `[superpowers] dir` in `defaults.toml`.\n\nThis walks you through everything once, with no prior knowledge of baton. It takes about 15 minutes. The commands assume macOS; on Linux everything works except the background service, so use the note in step 4.\n\n**Before you start.** Get the tools in [What you need](#what-you-need): at least one of `codex` or\n`agy`, signed in to your plan, plus OpenCode for the chat window. Superpowers is installed in step\n1.\n\n```\ngit clone https://github.com/cahian/baton.git\ncd baton\nuv tool install .\ngit clone https://github.com/obra/superpowers ~/.config/baton/superpowers\n```\n\nThe last line is required, by design: no round runs without Superpowers (see\n[Superpowers is required](#superpowers-is-required)).\n\n```\nmkdir -p ~/.config/baton\ncp examples/defaults.toml ~/.config/baton/defaults.toml\n```\n\nClaude is optional. Open `~/.config/baton/defaults.toml` in any text editor and pick one of these:\n\n- **No Claude (the simplest):** change nothing, and skip step 3. Without a saved key, Claude never\nruns and baton uses Codex and Antigravity only. To switch it off for good, set`enabled = false` under`[agents.claude_api]` .\n- **With Claude:** set`monthly_budget_usd` to the credit you actually have, and`budget_reset_day` to the day of the month it renews. baton stops 10% before that number. Then\ndo step 3. Claude joins the queue after the other two; its turn ends at USD 50 or 5 hours,\nwhichever comes first, and a goal leaves it out after USD 150. To keep it for last, so that it\nruns only while the others are out, set`reserve = true` under`[agents.claude_api]` .\n\nChange `enabled` for `codex` or `antigravity` the same way if you only have one of them.\n\nCreate an API key in the [Claude Console](https://platform.claude.com) and copy it. Then run this,\nwhich saves what you copied into a file only you can read:\n\n```\ntouch ~/.config/baton/anthropic.key\nchmod 600 ~/.config/baton/anthropic.key\npbpaste > ~/.config/baton/anthropic.key\n```\n\nDo not paste the key into a chat or into a project.\n\n**Check your billing first.** baton cannot see or change it. If the Console has auto-reload on, or\nholds credits you bought, using the key can spend that money. Turn auto-reload off.\n\n```\nbaton install-service\nbaton doctor\n```\n\n`install-service` keeps baton running in the background, even after a restart. Every line of\n`doctor` should read `ok` (Superpowers included); with no Claude key it says `claude_api: not configured (optional)`,\nwhich is fine. `FAIL` tells you what to fix; `warn` is only a heads-up. It spends\nnothing, so run it as often as you like.\n\n**On Linux**, skip `install-service` and leave `baton serve` running in a terminal.\n\n**Optional: long-term memory.** Put the `ai-memory` binary on your `PATH` and baton does the rest;\nwithout it baton works exactly as before. See [Long-term memory](#long-term-memory).\n\n```\nbaton opencode-config\n```\n\nThis prints a block of settings. Where it goes depends on whether you already use OpenCode:\n\n- \n**No `~/.config/opencode/opencode.json` yet:** save it directly:\n\n```\nmkdir -p ~/.config/opencode\nbaton opencode-config > ~/.config/opencode/opencode.json\n```\n\n- \n**You already have that file:** do not run the command above, it would erase your settings.\nOpen the file and copy the`baton` entry into your existing`provider` section.\n\nThen tell OpenCode to read the change. If you skip this, baton will not show up:\n\n```\nopencode reload\n```\n\nUse a throwaway folder for your first try. **The agents have full access to the folder where you\nopen OpenCode**, so do not start in a project you care about:\n\n```\nmkdir -p ~/baton-test\ncd ~/baton-test\ngit init\nopencode\n```\n\n1. \nType `/models` . Searching for`baton` shows**No results found** . That is expected the first\ntime.\n2. \nPress `ctrl+a` to see all integrations, and choose**baton** .3. \nIt asks for an API key. baton does not need one, but the box cannot be empty. Type `local` and press Enter.4. \nType `/models` again and choose**Auto** . If OpenCode asks for a variant, keep**Default** .\nbaton ignores this setting; effort is set in your`defaults.toml` .\n\nYou are ready when the box at the bottom says **Auto baton**:\n\nOpenCode remembers the connection, but check that label whenever you open a new session.\n\n```\nCreate a hello.py that prints \"hi\" and run it to check.\n```\n\nThe answer streams in the chat like any other model. A small request is answered in one round: the plan finds nothing to build and the cycle ends there. A line tells you each time one agent hands over to another, and one each time the phase changes. While it works:\n\n- **To change direction,** just send another message. It reaches the agent after its current tool\ncall (never later than five minutes after), including a message you type while a reply is\nstreaming, which OpenCode would otherwise hold until the reply ends.\n- **Closing OpenCode does not stop the work.** It keeps running in the background.\n\nRun these in another terminal:\n\n```\nbaton jobs                   # everything running; names look like session-<id>\nbaton status session-<id>    # where it is and what it has spent\nbaton stop session-<id>      # stop it\nbaton doctor                 # limits and spending right now\nbaton restart                # restart the gateway between rounds (see Keeping it running)\n```\n\nBy default, the work ends when a cycle ends, which for a small request is one round. For a long goal, give baton a test to pass: create a\nfile named `baton.toml` in your project with the command that proves the job is finished.\n\n```\n[verify]\ncommand = \"./check.sh\"\ntimeout_s = 3600\n```\n\nbaton then keeps going, across agents and limit resets, until that command succeeds. An agent saying \"done\" does not count. Without a good check it may stop too early, and with an impossible one it will keep trying, so choose something you trust.\n\n| What you see | What to do | \n|---|---|\n| `baton` is not in`/models` | Run `opencode reload` , reopen OpenCode, then do step 6. | \n| It asks for an API key | Type `local` . | \n| The model still is not listed | In your project, run `echo '{\"model\":\"baton/auto\"}' > opencode.json` , then reopen OpenCode. | \n| The answer never starts | Run `baton doctor` and`baton jobs` . | \n| It says it is waiting | Every agent hit its limit. It resumes alone at the next reset. | \n| You want to go back to another model | Choose it in `/models` . Nothing else changes. | \n\nThe default order, all at medium effort (the strong tier; [Phases](#phases-plan-build-review)\nadds a working model for building):\n\n1. **GPT-6.1 Sol** , through Codex.\n2. **Gemini 4 Argon** , through Antigravity (`agy` ).\n3. **Claude Opus 5.5** , through a Console API key (never a subscription login).\n\nThat is the queue. An agent gives way to the next after **5 hours of work**, or sooner when its\nlimit runs out. Only the time its rounds ran counts: a cooldown, a wait or a stopped service does\nnot. Claude's turn also ends at **USD 50**, whichever comes first, and after **USD 150** spent in\none goal (a goal is what you pin with `baton: goal`) Claude is left out of the queue until the goal\nchanges. The others have no budget of their own. Claude is paid by use and has no reset that would\never stop it, so the ledger's monthly ceiling (see [The Claude ledger](#the-claude-ledger))\nstays above all of this.\n\n**Claude is optional.** baton works with Codex and Antigravity alone: until you save a key file\n(Quick start, step 3), Claude never runs and the rotation is the other two. It joins the queue\nonce the key is there. To turn it off for good, set `enabled = false` under `[agents.claude_api]`\nin `defaults.toml`, or leave it out of `order` in a goal file. `baton doctor` reports an\nunconfigured Claude as one `ok` line, not a problem.\n\nThe first agent in `order` starts. The current one keeps working until it is unavailable, its turn\nis over, or you type `baton: next` (or `baton next`); then the next one in `order` takes over,\nwrapping around. This happens only between two rounds: a turn that ends while a round runs lets\nthe round finish, and `baton: next now` ends the round at the next gap between tool calls. Each\nchange is one line in the chat, `[baton] rotation: codex -> antigravity (5 hours)`, with the reason\n(`asked by the person`, `5 hours`, `USD 50`), said once the next agent actually starts. When no\nother agent can run (it is out, or has no key), the agent stays, nothing is said and its turn\nstarts over. A review may go to Claude when it is the only other agent that can take it; that\nround counts in the goal's spend, not in the turn. An agent that comes back from its limit does\nnot preempt another that is working. When no agent can run, baton waits for the earliest known reset, and takes a `next` that arrives meanwhile.\n\n**Reserve.** With `reserve = true` under `[agents.claude_api]`, Claude leaves the queue and waits\nfor the others, wherever it stands in `order`: it runs only while every other agent is out. While\nit does, baton reads the others (without a model call) from their reset time onward, at most every\n30 seconds, and as soon as one is available it ends the Claude round and that agent takes over.\nIf more than one is, the first in `order` goes first. This is not a failure: Claude gets no\ncooldown. An agent whose round fails within five minutes of such a takeover stops being allowed to\nend Claude rounds until it completes one. At most six Claude rounds a day are ended this way\n(`baton status` says when the cap is active). Without `reserve` (the default) none of this\nhappens.\n\nThe turn of a reserve Claude never ends. The limits are settings (`turn_max_s` for every agent, in\nseconds, default 18000; `turn_budget_usd`, default 50, and `goal_budget_usd`, default 150, for\nClaude). They go in `defaults.toml` or in a goal file. A repository's `baton.toml` may only lower\nthem (and `round_budget_usd`), may not turn a `reserve` off, and may not enable an agent that\n`defaults.toml` disables. Claude reports a session's cost as a running total, so a resumed round counts only what it added to the last total baton saved for that session. A round that ends without a cost report (killed, timed out) counts its\nwhole reservation against the turn and the goal, as the ledger does. Change the\norder, or disable an agent, in the goal file.\n\nEach provider has a strong model for thinking and a working model for building, and baton applies the same discipline to all three:\n\n| Provider | Plan and review | Build | \n|---|---|---|\n| Codex | `gpt-6.1-sol` , medium | `gpt-6.1-terra` , high | \n| Antigravity | `gemini-4-argon-medium` | `gemini-3.8-flash-high` | \n| Claude | `claude-opus-5-5` , medium | `claude-sonnet-5-5` , high | \n\nThis is separate from the rotation: the scheduler still decides who works, and the phases decide\nwhat that agent does and which of its two models runs. State lives in `.baton/PLAN.md` and\n`.baton/REVIEW.md`, so any provider can continue a cycle.\n\n1. **Plan** (strong). The agent writes`.baton/PLAN.md` with one`- [ ]` line per task. A request\nsmall enough to do at once gets no tasks, and the cycle ends there.\n2. **Build** (working). One task per round, marked`- [x]` when done. Two completed rounds in a row\nthat close no task move on to review; so do 8 rounds in a row of any outcome that close none,\nand so does the build's round cap (3 per task open when the build began, plus 5; a review that\nsends the build back renews it).\n3. **Review** (strong). A fresh conversation, by another provider when one is available. It writes`.baton/REVIEW.md` , starting with`APPROVED` or`CHANGES` , and adds`- [ ]` tasks for changes.\nChanges go back to build at most 2 times; a third`CHANGES` ends the cycle.\n4. **End of cycle.** The verify command runs here, and a failed run goes back to plan. It also\nruns after every 3 rounds without a phase change, whatever their outcome (a timeout resumes\nrather than advances), and a pass finishes the goal as usual (with a goal set, it starts[consensus](#a-goal-ends-only-when-its-check-passes-and-every-agent-approves) instead). With no\nverify command, the end of a cycle finishes the goal.\n\nA note from a person in the middle of a cycle is delivered as before, outranks the current task,\nand changes no phase; a new message starts a new cycle at plan, and `.baton/PLAN.md` and\n`REVIEW.md` of the cycle before are moved to `.baton/archive/` (a failed verify keeps them). A\nround that does not complete leaves the phase as it was, so the next agent repeats that work. A\nClaude round cut off by its own budget cap counts as not complete for the phase and does not\nfinish a goal. In a directory that is not a git repository there is nothing to review: baton says\nso once and ends the cycle after build. `phases = false` in a goal file or `baton.toml` keeps the plain\nrelay, with the strong model for every round.\n\n**Two guards on the review, enforced by baton and not by a prompt.** baton deletes `REVIEW.md`\njust before a review round and reads the verdict only after a round that completed, so an old\n`APPROVED` can never approve anything. And if a reviewer changes the code (HEAD moves, or any\ntracked or new file outside `.baton/` changes), the review is void: it counts as `CHANGES`, a\n`phase` event says `reviewer changed the code`, and a task to review those changes is added to the\nplan. A reviewer editing a gitignored file is not detected.\n\nWhile a goal is set (`baton: goal <text>`, `baton goal`, or `oneshot`), a passed verify does not\nfinish the job, whatever the goal file says. A goal needs phases and a git repository, because\nconsensus is a phase and an approval is bound to the state of the tree. With `phases = false`, or\noutside a repository, `baton: goal` answers `[baton] a goal needs phases, and phases = false here`\n(or `a goal needs a git repository`), `baton goal` exits with that error, and `oneshot = true` with\n`phases = false` is an error in the goal file. A run that finds a goal set where it cannot do it (a\n`baton.toml` edited to `phases = false` and a restart, a `.git` that disappeared) alerts once and\nholds, as it does for a missing Superpowers: no round, nothing ends, a stop is honoured, until the\ngoal is cleared (`baton: goal off`) or the setup is fixed and the job restarted.\n\nThe check is `verify.command` when the goal file has one. Otherwise it is `.baton/VERIFY.sh`, which\nthe plan writes: it must exit 0 only when the goal is met as the person would experience it, in the\nreal setup, with no rounding, a check that measures a stand-in fails the check, and it writes only\nunder `.baton/` or outside the working directory (anything else it leaves in the tree counts as a\nchange to the work). With no check the gate fails with `no check`, and the plan stays in plan, with\ntasks or without, until `.baton/VERIFY.sh` exists. The gate before round 1 and the periodic one run\nonly with a check that exists.\n\n- **The check is locked.** At the end of each completed plan round baton saves the script's hash,\nand keeps its first version in the state directory (`verify.first.sh` ). A gate that finds another\nhash fails with`the check changed outside plan` and alerts, so a build round cannot weaken it\nsilently. A plan round may change it; consensus then shows every approver the diff against the\nfirst version, and tells them to reject a weaker check. When a goal ends or changes, the script\nmoves to`.baton/archive/` with the lock and the first version: the next goal writes its own.\n- **Consensus.** A pass starts it: every agent in`order` that can run (not left out for its goal\nbudget, and not one that can never run: disabled, no usable key, or a`round_budget_usd` below\nthe minimum) takes a fresh, strong round, one at a time, without moving the queue or its turns,\nand with no round tail and no preemption watch. Every approver is shown the check itself (the\ncommand or the script) and told to reject one that cannot fail or measures a stand-in. It may not\nchange the tree or the check, reruns the check itself, inspects real evidence, and writes`REVIEW.md` with`APPROVED` or`CHANGES` first (`APPROVED only if ...` is not an approval). An\nagent out of quota is waited for (`waiting for <agent> to approve` , said once per reason); a round\nthat did not complete is retried, and three in a row count as one failed cycle; an agent that\ncrosses its goal budget in the meantime leaves the list, and the chat says so.\n- **An approval belongs to a state.** When the gate passes baton records the tree (HEAD and every\nchanged file outside`.baton/` ) and the check's hash. After every consensus round, completed or\nnot, on a restart, and just before the goal ends, they are compared: a change rejects, consensus\nends and the work goes back to build, with a note naming the changed paths from`git status` . A\nclean round's own snapshot commit moves the record along. Before it ends, baton also looks again\nat the goal (changed or cleared ends the approval) and asks anyone who can run now and has not\napproved. Everyone approving ends the goal:`[baton] goal achieved: check passed, approved by codex, antigravity, claude_api` , and the goal is archived.\n- **The goal's terms are saved.** When a goal is first seen baton saves its terms in`state.json` (`goal_terms` : the verify command,`order` , which agents are enabled, and every agent's`round_budget_usd` ,`turn_budget_usd` and`goal_budget_usd` , which a repository's`baton.toml` may\nlower and so take Claude out of consensus). A later difference, from\na`baton.toml` an agent edited or anything else, fails the gate with`the goal's terms changed: <what>` and an alert, and voids a consensus under way, until the person sets the goal again with`baton: goal` or`baton goal` (the same words will do), which saves the terms as they are.\nConsensus shows the saved verify command.\n- **The check runs once more at the end.** Just before the goal ends baton runs the check again:\nthe tree comparison cannot see what git ignores (a`dist/` , a build). A failure rejects, back to\nbuild, and a goal that changes while any check runs does not get that run's result: the check runs\nagain for the new goal.\n- **A no goes back to build.**`CHANGES` , a missing verdict or a changed tree ends consensus: the\nreview becomes a note, a task goes to the plan, and the chat says`[baton] <agent> rejected: back to build` . A message from the person during consensus ends it the same way, and`next` is\ntaken and dropped with a`[baton]` line.\n- **No giving up.** Every 3 cycles that end with a failed gate or a rejection, baton alerts`no progress in N cycles: still working` , and carries on.\n\n**Hardening, and the trust boundary.** Every git call baton makes runs with `core.fsmonitor`,\n`core.hooksPath` and `core.pager` neutralised, so those cannot run a program inside baton. Other\nconfig still can: a repository's `filter.*` (clean and smudge) programs run inside git, and baton\ndoes not disable filters. What stops them forging an approval is the order: an approver's\n`REVIEW.md` is read before baton's next git call. The tree record\nalso covers tracked files hidden with `skip-worktree` or `assume-unchanged`. Baton sets\n`BATON_ROUND=1` in every agent's environment, and the commands that steer a goal (`baton goal`,\n`next`, `restart` and `stop`) refuse to run under it, so an agent's shell cannot accept its own\nchanged terms or clear the goal. This only raises the bar: agents run as the same user with full\naccess to the machine, so a determined agent can still write to baton's own state directory (the\ngoal file, `state.json`, `verify.first.sh`) or unset the variable. The protection is the check and\nthe consensus, which guard the work and the check, not baton from the agents it runs. Helper files\na check calls beside it are not locked either.\n\n**Subagents are allowed on the provider's two models.** A subagent may use only the strong or the\nworking model of its provider, exactly, and what enforces that differs by provider.\n\n- **Claude:** every subagent runs on the working model (Sonnet), because baton sets`CLAUDE_CODE_SUBAGENT_MODEL` with`CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1` , which makes Claude Code\ndrop the`model` parameter: a named model is replaced, not denied. A`PreToolUse` hook stays as a\nsecond check, and in practice only denies unreadable input or an empty allowed list.\n- **Codex:** a`PreToolUse` hook, given per run with`-c` (nothing in`~/.codex` is touched, and`features.hooks=true` keeps a config file from turning hooks off), denies a spawn naming a model\noutside the pair before the subagent exists, with a message naming the two allowed ones; the agent\ncan retry with an allowed one. A subagent that names no model inherits, unless a role pins one. A\nCodex spawn may not appear in the stream, since the block is in the hook.`--dangerously-bypass-hook-trust` also runs any other enabled Codex hooks without review, which\nadds no new capability, since`verify.command` already runs commands.\n- **Antigravity:** no hook. baton ends the round (a`subagent` outcome, followed by a pause; three\nin a row rest that agent) when a forbidden model is visible in the stream, and otherwise only\nlogs, since its model is rarely shown.\n\nFor Claude and Codex the stream only logs subagents as `subagent` events, shown in `baton status`;\nit never ends a round. Subagent spend is included in a Claude round's reported cost, which Claude\nchecks between steps, so `--max-budget-usd` is a soft limit: a round can pass it slightly, and the\n10% reserve covers that. Claude runs with a config directory of baton's own\n(`<state base>/.claude-config`, mode 700), so a `~/.claude.json` `env` map cannot replace the key or\nthe base URL.\n\n**Superpowers is applied in every round** (it is [required](#superpowers-is-required)). baton puts\nthe methods that fit each phase in the prompt of a fresh conversation, under `Skill: <name>`\nheadings: `writing-plans` for plan, `test-driven-development` and `verification-before-completion`\nfor build, the code-reviewer prompt for review, and `systematic-debugging` for the plan after a\nfailed verify, even in a resumed conversation. The phase's own instructions come last. It leaves\nout the skills that conflict with baton's own workflow, and says baton's files outrank the\nmethods'. To use another install, set `[superpowers] dir` in `defaults.toml` (a directory named\nthere is final: if it is incomplete, Superpowers counts as missing); a repository's `baton.toml`\ncannot set it. Files over 64 KB, or 128 KB of text in all, count as missing.\n\n`.baton/HANDOFF.md` is short-term: it dies with the goal. [ai-memory](https://github.com/akitaonrails/ai-memory)\nis the long-term layer: what past rounds did and learned, searchable, across goals in the same\ndirectory. It adds to the handoff and never replaces it. It is optional: without the binary, or\nwith memory off, nothing changes, and a memory failure can never stop a goal.\n\n**baton drives it; the agents do not.** baton calls the `ai-memory` command itself, between rounds,\nso the same memory reaches Codex, Antigravity and Claude alike. It uses no hooks and no MCP, and\nnever runs `ai-memory setup`, `install-hooks`, `install-mcp` or `run`.\n\n**What is stored, and where.**\n\n- Before every round, baton searches the project's memory with words from the last verify output\n(at most 16 of them) and the objective (the rest, up to 24 in all), and puts the best 5 hits (6000 characters at most) in the prompt, labelled as\ncontext that may be out of date, not as instructions. Words are taken from the end of the\nverify output and of the objective, where the errors and the newest message are. Each hit is\nthe page itself, not search's short snippet: baton reads it and puts in the prompt the part\naround where the search matched (the head when it cannot tell), sharing the 6000 characters\namong the hits, 1200 at most for each. Pages that hold lessons come first, and the pages of\nthis goal's own rounds are left out, since the handoff already carries them. The hits sit\nbetween `--- BEGIN BATON MEMORY ---` and`--- END BATON MEMORY ---` , one line each, starting\nwith the page's date and goal.\n- After a round that started a process, baton stores one page: the date, the agent, how it ended, how long it took, the gate result, the commits it made, and the tails of the verify output, the handoff and the round. It is written when the round ends and again, over itself, once the verify has run, so a baton killed during the verify still leaves its page. A round that fails, times out or has a failing verify is kept as a \"gotcha\"; a stop, an interrupt, a takeover by another agent or a limit is normal rotation.\n- Agents are told to keep durable findings under a `## Lessons` heading in the handoff. baton also\ntakes`## Lessons ...` and`### Lessons ...` headings and list items that start with`**Lessons` , with their indented lines (code fences are skipped). When that changes, baton\nstores it as a pinned page that outlives the goal. Each round's page is announced in`baton status` as a`memory` event`recorded` .\n- ai-memory redacts secrets before it stores anything. Memory is per directory: a project is the directory's name plus a short hash of its full path, and a search never leaves it.\n- The data is in `~/.local/state/baton/.ai-memory` (mode 700), with its own config and its own\nport,`127.0.0.1:18778` , so it never meets another ai-memory on the machine. The leading dot\nkeeps it clear of every goal's state directory, since no goal can be named that.`BATON_STATE_DIR` moves it.\nbaton starts the server when it first needs one and leaves it running.\n- ai-memory gets only `PATH` ,`HOME` ,`TMPDIR` ,`NO_COLOR` and`RUST_LOG` in its environment, so no\nprovider key can reach it, and baton configures no model for it: it uses none.\n\n**Install it** as in [What you need](#what-you-need): the release binary from the\n[ai-memory releases](https://github.com/akitaonrails/ai-memory/releases) on your `PATH`, or `bin`\nbelow set to its path. Do not run `ai-memory setup`: it wires hooks into every session on the\nmachine, which is what baton avoids. Then run `baton doctor`: the\n`memory` line shows the binary found and whether the server is up. If you run baton as a service,\nre-run `baton install-service` after installing ai-memory, so the service can find it.\n\n**Turn it off.** `memory = false` in a goal file or `baton.toml` turns it off for that project.\n`BATON_MEMORY=off` in the environment turns it off everywhere. In `defaults.toml`:\n\n```\n[memory]\nenabled = true\nbin = \"ai-memory\"      # a name on PATH, or a path\nport = 18778           # 1024-65535\nrecall_chars = 6000    # 0-20000; 0 stops recall, recording goes on\n```\n\nA `baton.toml` that comes with a repository can only turn memory off or on; it cannot set `bin`\nor `port`. A `[memory]` table that cannot be read turns memory off, with one line in `baton status`\nand a warning in `baton doctor`.\n\n**Limits.**\n\n- One memory call, starting the server included, is given 8 seconds in all. After any failure\n(server dead, garbage output) baton skips memory for 300 seconds, so a broken memory costs a\nround one short wait at most. A page that cannot be read does not stop memory: for 300 seconds\nthe hits are search's snippets instead of pages. A missing binary costs nothing and says nothing; `baton doctor` warns. Failures show in`baton status` as`memory` events, never in the chat.\n- If you change `port` while the old server is still running, baton starts no second one: it says\nso, in`baton status` and`baton doctor` , and you stop the old server (or set the old port\nback). baton never stops it for you.\n- A round that baton had to kill is still recorded, as `interrupted` .\n- Recall is keyword search, not understanding: it finds earlier pages that share words with the failing output or the objective. It can miss, and it can return something stale.\n- Lessons longer than 10000 characters are cut to their last 10000.\n- Goals in one directory share one memory; the pages are told apart by goal name.\n\nThe failure this exists to prevent: at 100% of its included usage, the Codex CLI keeps working and silently debits account credits. No error is raised. A design that waits for a limit error has already paid, so baton reads the limits and acts before they are reached.\n\n| Agent | What is read | Stop condition | \n|---|---|---|\n| Codex | `codex app-server` rate limits and credit balance. No model call, nothing spent. | 90% used in any window, or the credit balance drops | \n| Antigravity | the `/usage` and`/credits` slash commands. No model call, nothing spent. | 90% used in any Gemini row, or the credit balance drops. Both `Gemini Models` rows, Five Hour and Weekly, must be readable | \n| Claude | baton's own spend ledger (there is no API that returns the balance) | a spend ceiling of 180 USD by default, and a per-round cap | \n\nRules that apply to all of them:\n\n- Limits are read before a round and every few seconds during it (15 s for Codex, 30 s for Antigravity by default). Reaching the threshold stops the round.\n- A balance that moves between the read before the round and any read during it kills the round\nat once (`credit_guard` ). Balances are compared as exact decimals.\n- Unknown means stop. A read that fails or cannot be parsed never lets an agent run, and two failed reads in a row during a round stop it.\n- Every agent process has the vendor API-key variables removed from its environment, so a CLI\ncannot fall back to paid API access. The Claude adapter adds its own key back for its process\nonly. Before the key is added, every `ANTHROPIC_*` ,`CLAUDE_CODE_USE_*` ,`CLAUDE_CODE_OAUTH_*` ,`CLAUDE_CODE_*_FILE_DESCRIPTOR` and`AWS_BEARER_TOKEN_BEDROCK` variable is dropped from the\nClaude round, and a round whose`init` event shows any other credential (a subscription login) is\nrefused at once, before any model call.\n\nClaude is billed to the organization's promotional credit, and no API reports what is left, so the budget is baton's own:\n\n- `monthly_budget_usd` (default 200) minus`reserve_pct` (default 10) is the spend ceiling, 180\nUSD. The reserve also absorbs the error of the cost estimate.\n- Spend is summed from `total_cost_usd` in each round's result, in one ledger shared by every\ngoal (`~/.local/state/baton/claude-spend.json` ), under a lock. A period starts on`budget_reset_day` at 00:00 UTC.\n- Before a round, baton reserves `round_budget_usd` (default 5), passed to Claude as`--max-budget-usd` : a soft limit, since the cost (a subagent's included) is checked between\nsteps, so a round can pass it slightly and the reserve covers that. If the ceiling would be passed, the round shrinks to what is left, and below\n5 USD the agent is unavailable until the next period.\n- A round that ends without a result keeps its whole reservation counted. Crashes overcount and\nnever undercount. So does a round ended because another agent came back (`reserve = true` ): it has no result, and\nwhatever it spent before is counted as the whole 5 USD. The ledger drifts high, never low, and\nthe small default keeps that drift small. A corrupt ledger makes the agent unavailable; it is never recreated silently.\n- Optionally, `admin_key_file` points to an Admin API key. baton then also reads the Console cost\nreport and uses the larger of that and the ledger. It never lowers the ledger, and a failed read\nmakes the agent unavailable.\n- With no key file, Claude is unavailable with a clear reason and the other agents carry on.\n\n- **There is a poll gap.** A round can cross from the threshold to 100% between two polls. The 10%\nmargin exists for this, and it cannot be made mathematically zero. The account's own setting for\ncredits, where it exists, remains the only hard stop.\n- **Claude spend is an estimate.** It is computed locally from what the CLI reports. The hard stops\nare in the Console, not in baton.\n- **Some Console settings must be right, and baton cannot check them with the key.** Turn\nauto-reload off and hold no purchased credits, since otherwise spending continues after the\npromotional credit ends. Put the key in a dedicated workspace with a monthly spend limit at the\nceiling.`baton doctor` prints this reminder.\n- **Interactive sessions outside baton are not protected.** Codex and Antigravity share one set of\nlimits and credits between baton and anything else you run. baton cannot stop your own session,\nbut a balance drop it causes still stops baton's round.\n- **Antigravity's output when its quota actually runs out has not been observed.** The limit reads\nare expected to stop it first.\n- **Masking is by pattern.** Before it is shown, kept in the round tail or put in the log of events,\nthe agent's output has the value of`Bearer ...` and of`token` ,`api_key` ,`secret` ,`password` and`passwd` assignments (also in a longer name such as`DB_PASSWORD` ) and the well-known token\nshapes (`sk-` ,`ghp_` ,`AKIA` ,`xoxb-` ) replaced by`[masked]` . A quoted value is masked from 8\ncharacters, spaces included. An unquoted value is masked only when it holds a digit or a symbol\nother than`.` ,`_` ,`-` , so code such as`password = self.password` is left alone:**an unquoted\nvalue of letters only is not caught.** A placeholder (`$TOKEN` ,`${TOKEN}` ,`<token>` ) is never\nmasked. A secret in another shape, or split across two streamed fragments, is not caught either,\nand the raw round log keeps what the agent printed. The next agent is told what`[masked]` is.\n- **A response held open for days through OpenCode is untested** beyond seconds. The job does not\ndepend on it:`baton jobs` and`baton status session-<id>` show where it is, and a new message\nin the session reattaches the stream.\n\n```\nuv tool install .\nbaton doctor\n```\n\nPython 3.12 or newer. There are no runtime dependencies. You also need the tools in\n[What you need](#what-you-need): Superpowers (required), and the CLIs you want to use on your\n`PATH` and logged in: `codex`, `agy` and `claude`. Binary paths can be overridden per agent\nwith `bin`, in a goal file or in `defaults.toml`, since tmux and launchd may not load your shell\naliases. A leading `~` is expanded.\n\n`baton doctor [goal]` checks binaries, logins, stray API-key variables, live limits, Claude's\nspend against its ceiling and Superpowers (a blocking failure when it is missing). It spends nothing and exits non-zero on a blocking problem.\n\nSettings applied under every goal. The Claude budget (`monthly_budget_usd`, `reserve_pct`,\n`budget_reset_day`) can only be set here, because the ledger it describes is shared; a goal file\nthat sets them is refused. A full example, also in [`examples/defaults.toml`](https://github.com/cahian/baton/blob/main/examples/defaults.toml):\n\n```\n[agents.claude_api]\nenabled = true\nmodel = \"claude-opus-5-5\"\neffort = \"medium\"\nbuild_model = \"claude-sonnet-5-5\"   # the phases' working tier\nbuild_effort = \"high\"\nmonthly_budget_usd = 200   # what the organization holds\nreserve_pct = 10           # kept back; the spend ceiling is 200 - 10% = 180\nbudget_reset_day = 1       # day of the month the period starts, 00:00 UTC\nround_budget_usd = 5       # most one round may spend (a killed round counts all of it)\nreserve = false            # true: Claude waits for the others, and runs only while they are out\nturn_max_s = 18000         # a turn ends after this long (every agent has it): 5 hours\nturn_budget_usd = 50       # or after this much spent in the turn\ngoal_budget_usd = 150      # after this much in one goal, Claude is left out until the goal changes\nkey_file = \"~/.config/baton/anthropic.key\"\nadmin_key_file = \"\"        # optional Admin API key file, to cross-check spend\n\n[agents.antigravity]\nenabled = true\nmodel = \"gemini-4-argon-medium\"   # the effort is part of the model id\nbuild_model = \"gemini-3.8-flash-high\"   # must start with gemini-\nthreshold_pct = 90\npoll_s = 30\n\n[agents.codex]\nenabled = true\nmodel = \"gpt-6.1-sol\"\neffort = \"medium\"\nbuild_model = \"gpt-6.1-terra\"\nbuild_effort = \"high\"\nthreshold_pct = 90\npoll_s = 15\n\n# Only here, never in a goal file or a repository's baton.toml. Empty means: search the usual\n# places: only the official Claude Code plugin install, then ~/.config/baton/superpowers.\n# An install anywhere else needs `dir`.\n[superpowers]\ndir = \"\"\n```\n\nCreate the key in the Console, in a dedicated workspace with a monthly spend limit, and store it where only you can read it:\n\n```\nmkdir -p ~/.config/baton\n( umask 077; cat > ~/.config/baton/anthropic.key )   # paste the key, then Ctrl-D\nchmod 600 ~/.config/baton/anthropic.key\n```\n\nbaton refuses a key file that is not a regular file with mode 600. It reads the key when it starts the process and passes it only in that process's environment; it is never logged, stored in state or given to another agent.\n\nA goal is a TOML file. Only `name`, `workdir` and `objective` are required; everything else has the\ndefault shown in [`examples/goal.toml`](https://github.com/cahian/baton/blob/main/examples/goal.toml):\n\n```\nname = \"example\"\nworkdir = \"/path/to/project\"\nobjective = \"Make the build pass on all platforms.\"\nphases = true   # false keeps the plain relay, with the strong model for every round\noneshot = false # true pins the objective as the goal when the job first starts\n\n[verify]\ncommand = \"./check.sh\"\ntimeout_s = 3600\n```\n\nThe verification command runs through the shell in the working directory. Exit 0 finishes the\ngoal; anything else, including a timeout, means not done, and its output goes to the next round.\nWithout `verify.command`, the goal is finished when a cycle ends (a round, with `phases = false`).\n\nA goal the person fixes (`goal.md` in the state directory, set from OpenCode or with `baton goal`) opens\nevery prompt, fresh or resumed, with `Goal, fixed by the person. It does not change until the person changes it:` and its text. It is on while that file is not empty. A goal over 8000 characters goes to\na file under `.baton/notes/` once, and only its first 1000 characters and the file's name are pinned.\n`oneshot = true` pins the objective as the goal once, at a job's first start (state.json remembers it),\nso a restart never brings back one that was cleared. Only a goal file you pass to `baton run` may set\nit: a repository's `baton.toml` is refused, as it would pin text an agent wrote.\n\nBetween rounds each agent gets the same context: the repository, its commits, `.baton/HANDOFF.md`\n(kept current by the agents), the last gate output and the tail of the previous round. `.baton/`\nlives in the working directory and is added to `.git/info/exclude`. A handoff over 16000 characters\nis cut in the middle: its first 4000 and its last 12000 are kept, with a line saying how much was\nleft out. From OpenCode, an over-long conversation keeps the person's messages first, newest first,\neach cut to its first 8000 characters, and gives assistant messages the room that is left, each\ngap marked with `...`. With `ai-memory` installed, what earlier goals learned in the directory is\nadded too (see [Long-term memory](#long-term-memory); `memory = false` turns it off). After each round baton commits\nuncommitted work locally as `baton: snapshot round <n> (<agent>)` so a hard stop loses nothing. It\nnever pushes (`snapshot = false` turns the commits off). `.baton/`, `__pycache__/` and `*.pyc` are kept out of it through `.git/info/exclude`.\n\nWhen you use baton from OpenCode (below), a `baton.toml` in the project directory plays the role\nof the goal file: its `[verify]`, timings and `[agents.*]` settings apply. `name`, `workdir` and\n`objective` in it are ignored, since the session decides them. Without one there is no gate, and\nthe job finishes when a cycle ends (a round, with `phases = false`). An invalid `baton.toml` is an error, never a silent\nfall-back to no gate.\n\nA `baton.toml` comes with the repository, so it may not set `bin`, `key_file`, `admin_key_file`,\n`full_access` or `poll_s`, and no `[superpowers]` table: a file in a repository must not choose\nexecutables or credentials, which files become part of a prompt, or slow the guard's limit reads.\nIt may set `phases` and the `build_model` and `build_effort` of an agent: they name models, not\nexecutables. A goal file you pass yourself and the defaults file may set `poll_s`.\nEverywhere, `threshold_pct` may not go above 90, and an `antigravity.model` must start with\n`gemini-`.\n\nOpenCode sees one OpenAI-compatible provider with one model, `baton/auto`, and keeps the history\nand context on its side.\n\n1. \nPrint the provider block and merge it into your `opencode.json` :\n\n```\nbaton opencode-config\n```\n\n2. \nStart the gateway. It binds `127.0.0.1` only:\n\n```\nbaton serve            # --port 18777 by default\n```\n\n3. \nSelect `baton/auto` in OpenCode and write your request.\n\nEach OpenCode session becomes a job that runs in its own thread inside `baton serve`, independent of\nthe HTTP connection. A dropped connection never stops it, and a new message in the session becomes\na note that ends the running round once its current tool call is done (after five minutes at the\nlatest). A message typed while the reply streams, which OpenCode queues in its database and posts\nonly when the reply ends, is read from there every 2 seconds (read-only, and any failure is\nsilent), delivered as a note at once, and not delivered again when OpenCode later posts it. Every\nconversation of the job reads every note, not only the first one to take it, and a note over 8000\ncharacters goes to a file under `.baton/notes/` in the directory, with its first 1000 characters\nand the file's name in the prompt. A message gets at least one round, even if the gate\nalready passes. A request that repeats what was already accepted, as a client's retry does, attaches\nto the stream and interrupts nothing, so it costs no round. `baton jobs` lists every job, and\n`baton status`, `baton say`, `baton goal`, `baton next` and `baton stop` accept its name (`session-<id>`) in place of a goal\nfile. The stream shows the agents' text, one line per rotation, a sentence when a returning\nagent ends a reserve Claude round, waits and gate results, and when no agent can run it says why, for\neach one. Title requests are answered locally and spend nothing. The\nworking directory is read from OpenCode's own database (`~/.local/share/opencode/opencode.db`, or\nthe path in `BATON_OPENCODE_DB`); baton never guesses one.\n\nOne runner works in a directory at a time, across `baton run` and `baton serve`. A directory inside\nor around a running one counts as busy, since the two would commit each other's work.\n\nA message that starts with `baton:` (any case) is a command, not work. They never start with `/`,\nwhich belongs to OpenCode. Text that only contains these words is an ordinary message.\n\n| Message | What it does | \n|---|---|\n| `baton: goal <text>` | Fix the goal (the rest of the message, over any number of lines). With no job running it starts one with `<text>` as the objective; with one running it is also a note, so the next round reads it. Chat:`[baton] goal set` . | \n| `baton: goal off` | Clear the goal, from the next round's prompt, and leave the note `The person cleared the goal.` for the agent. Chat:`[baton] goal cleared` . | \n| `baton: next` | Ask for the next agent in the queue after this round. It is stored for the rotation to take. With no job running baton answers in one line and stores nothing. | \n| `baton: next now` | The same, and the round ends after its current tool call. | \n\nA command typed while the reply streams goes through the same parsing when baton reads it from OpenCode's queue, and neither a retry of one nor OpenCode's later post of the same words runs it twice. Old commands are left out of the conversation a new job is briefed with.\n\nJobs live inside `baton serve`, so keep it alive. If it dies, even by `kill -9`, the next\n`baton serve` first ends every agent left behind that it can prove is baton's own, then resumes\nevery job that had not finished. A job a person stopped stays stopped. A group that cannot be\nverified is left alive and its job waits, because two agents at once could spend twice.\n\nOn macOS, install it as a launchd service:\n\n```\nbaton install-service            # --port 18777 by default\n```\n\nlaunchd loads no shell profile, so this resolves everything from the shell you run it in. It finds\neach enabled agent's binary and `node`, which `codex` needs, builds `PATH` from their directories,\nwrites `~/Library/LaunchAgents/local.baton.serve.plist` with absolute paths, and loads it. Running\nit again replaces the service. The log is `serve.log` in the state directory. Run it again when an\nagent or `node` moves. Your verification commands get that same `PATH`, so a tool they need from\nelsewhere must be called by its full path. A verification command that exits 126 or 127 can never\npass, so baton says so once in the chat and in `baton status`, and the rounds go on.\n\n**Restarting the gateway.** `baton restart` writes a request that the running `baton serve` sees.\nEach job then holds at the next point between rounds (also inside a wait), says so once, and the\ngateway sends itself SIGTERM when every running job is held, so no round is cut off. A job in the\nmiddle of a round is never held, and a `baton stop` still ends a hold. The jobs are resumed after\nthe restart, exactly as after any restart. Under launchd (`baton install-service`) KeepAlive brings\nthe service back; without a supervisor the gateway simply exits and you start `baton serve` again.\nIt works whether or not jobs run. `baton restart --now` does not wait for the jobs: it is the same\nas stopping the service, and the jobs resume. A request left over when the service starts is\ndropped. To cancel a request that is still waiting, delete the file `.restart` in the state directory\n(`~/.local/state/baton/.restart` unless `BATON_STATE_DIR` moves it): the held jobs go on at once.\n\n[`examples/baton-serve.plist`](https://github.com/cahian/baton/blob/main/examples/baton-serve.plist) shows the same file with placeholders,\nfor writing it by hand.\n\n| Command | What it does | \n|---|---|\n| `baton doctor [goal]` | Check binaries, logins, stray API keys, live limits, Claude spend and Superpowers. | \n| `baton run <goal.toml>` | Run one goal in the foreground, without OpenCode. | \n| `baton serve [--port 18777]` | Start the OpenCode gateway. | \n| `baton jobs` | List every job and goal that has state: whether its runner is alive, round, current agent, last event. | \n| `baton status <goal.toml or job>` | One-shot status: rounds, cooldowns, recent events. | \n| `baton say <goal.toml or job> [--now] \"note\"` | Leave a note for the next round. `--now` also ends the current one, after its tool call. | \n| `baton goal <goal.toml or job> \"text\"` /`--off` | Fix the goal every prompt opens with, or clear it. | \n| `baton next <goal.toml or job> [--now]` | Ask for the next agent in the queue; `--now` also ends the round after its tool call. | \n| `baton stop <goal.toml or job> [--now]` | Stop after the current round, or kill the round with `--now` . | \n| `baton restart [--now]` | Restart the gateway once every running job is between rounds; `--now` does not wait. Jobs resume. | \n| `baton opencode-config [--port 18777]` | Print the provider JSON for `opencode.json` . | \n| `baton install-service [--port 18777]` | Write the launchd plist and load it (macOS). | \n\nState lives in `~/.local/state/baton/<goal name>/` (events, state, raw round logs, inbox), one\nrunner per goal. `BATON_STATE_DIR` and `BATON_CONFIG` move the goals' state and the defaults file.\n`BATON_STATE_DIR` does not move the Claude ledger: it stays at\n`~/.local/state/baton/claude-spend.json` under the real home directory, so that no goal can end up\nwith a ledger, and a budget, of its own. On macOS the runner holds `caffeinate` so the machine does not sleep mid-goal.\n\nbaton never exits because of a limit, a transient error, a crash in an agent or bad network. Only a passing gate, a stop request or a termination signal ends it.\n\n```\nuv sync\nuv run pytest -q\nuv run ruff check .\nuv run ruff format --check .\nuv run python scripts/check_private.py\n```\n\nTests never call a real vendor and never read real credential files: fake CLIs and fake limit\nreaders stand in. `scripts/check_private.py` is part of CI. It fails on any e-mail address, home\npath, private or Tailscale address, `.ts.net` hostname, UUID or credential-looking token in a\ntracked file or a commit message, because this repository must hold no personal data. Working\nnotes (`.superpowers/`, `.oneshot-state.md`) stay untracked, and a test enforces it.\n\nThe design is in [`docs/specs/2026-11-04-baton-design.md`](https://github.com/cahian/baton/blob/main/docs/specs/2026-11-04-baton-design.md).\n\nMIT. See [LICENSE](https://github.com/cahian/baton/blob/main/LICENSE).\n\nbaton uses two projects it does not bundle and did not write:\n\n- [Superpowers](https://github.com/obra/superpowers) , by Jesse Vincent (MIT): the working methods\nevery round is given. baton reads your own install of it and copies none of its files.\n- [ai-memory](https://github.com/akitaonrails/ai-memory) (MIT): the long-term memory. baton calls\nits command-line tool and bundles none of it.", "url": "https://wpnews.pro/news/baton-keep-long-coding-goals-moving-across-ai-coding-agents", "canonical_source": "https://github.com/cahian/baton", "published_at": "2026-11-06 13:24:08+00:00", "updated_at": "2026-11-06 13:50:33.166330+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-products"], "entities": ["baton", "OpenCode", "Codex CLI", "OpenAI", "Antigravity CLI", "Claude Code", "Superpowers", "uv"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/baton-keep-long-coding-goals-moving-across-ai-coding-agents", "markdown": "https://wpnews.pro/news/baton-keep-long-coding-goals-moving-across-ai-coding-agents.md", "text": "https://wpnews.pro/news/baton-keep-long-coding-goals-moving-across-ai-coding-agents.txt", "jsonld": "https://wpnews.pro/news/baton-keep-long-coding-goals-moving-across-ai-coding-agents.jsonld"}}