From cade27f792e574cfead74843d0c7d6aecc87bcfd Mon Sep 17 00:00:00 2001 From: Bakhomious Basta Date: Wed, 4 Nov 2026 23:12:08 +0100 Subject: [PATCH] docs(plan): install baton from GitHub releases with mise, not Homebrew The user dropped Homebrew on 2026-11-04 and chose, through nudge, GitHub releases that mise's github: backend installs, as nudge's releases were before. Decision 5 records that call in place of the formula in codedogapp/homebrew-tap, and the homebrew step becomes release, whose archive still carries mvdan.cc/sh's BSD-3 notice (decision 50). The new pane-path step answers what puts baton on a pane's PATH without Homebrew: today nothing does. A pane gets the PATH of the baton that ran tmux, so a baton started by its path leaves the agents' baton commands without it, as in the guard-orchestrator live check (fact 37). deliver-github is done (#45). --- docs/plan.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/plan.md b/docs/plan.md index f1c2c02..5a7ddb6 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -19,7 +19,7 @@ This document is the source of truth for scope and the work breakdown. How we wo ## 0. Status -The first milestone is a walking skeleton (`skeleton`): one feature on a throwaway repo, four panes, one full round of handovers and the final report, all with a stand-in `copilot`. Then a live run, then Jira, the guard and the pull request hosts, then Homebrew. +The first milestone is a walking skeleton (`skeleton`): one feature on a throwaway repo, four panes, one full round of handovers and the final report, all with a stand-in `copilot`. Then a live run, then Jira, the guard and the pull request hosts, then a release on GitHub, which mise installs. | Id | Step | Status | | --- | --- | --- | @@ -65,12 +65,13 @@ The first milestone is a walking skeleton (`skeleton`): one feature on a throwaw | `jira-client` | `internal/jira`'s client: a ticket and all its comments from Jira Cloud's REST API, through Atlassian's gateway with the user's email and scoped token, and the site's cloud ID from the site, which gets neither (decision 59, facts 13 and 41) | Done (#42) | | `jira` | `baton jira `: the ticket as Markdown, for the orchestrator to plan from, with Jira's site and the user's email from the config file and the read-only token from the Keychain; without the text's control characters; the guard runs it unasked in the orchestrator's pane and denies it in a role's (decisions 6 and 60, fact 42, risk 14) | Done (#43) | | `jira-login` | `baton jira`'s answer to a login that Jira refuses (401): the command that replaces the token in the Keychain, as tokens expire within a year (decision 60, fact 42) | Done (#44) | -| `deliver-github` | `baton deliver --title `, in the orchestrator's pane or the user's shell, on the user's word: pushes a ready feature's branch to `origin` and opens its pull request with the user's `gh`, into the GitHub repo's default branch, with the lead's final report as its body; the feature becomes `delivered` and keeps the pull request's link, and a later delivery, after the user hands it back, pushes again and comments the new report on that pull request; the guard asks the user before each delivery in the orchestrator's pane, and denies it in a role's (decisions 17, 54 and 61, fact 43, risk 15) | In review (#45) | +| `deliver-github` | `baton deliver <slug> --title <title>`, in the orchestrator's pane or the user's shell, on the user's word: pushes a ready feature's branch to `origin` and opens its pull request with the user's `gh`, into the GitHub repo's default branch, with the lead's final report as its body; the feature becomes `delivered` and keeps the pull request's link, and a later delivery, after the user hands it back, pushes again and comments the new report on that pull request; the guard asks the user before each delivery in the orchestrator's pane, and denies it in a role's (decisions 17, 54 and 61, fact 43, risk 15) | Done (#45) | +| `pane-path` | `baton pane` puts the folder of its own program first on the `PATH` of its `copilot`, unless that `PATH` finds this program as `baton` already, so that the agents' `baton` commands run the pane's baton, as the hooks do through `BATON_BIN`, however the user installed or started it (decisions 5, 42 and 47, fact 37) | Todo | | `close` | `baton feature close <slug>`: ends the feature's session; keeps its branch and, unless asked, its worktree | Todo | | `deliver-bitbucket` | `baton deliver` for Bitbucket Cloud: push the branch and open the pull request with the REST API, a token in the Keychain | Todo | | `cost` | AI credits per role and per feature in `baton status`, from each session's events (decision 20), found by the session ID that `baton pane` gives `copilot` with `--session-id` and baton keeps | Todo | | `resume` | A pane whose `copilot` ended starts it again with `--resume`, and a pane that picked up its work but couldn't start `copilot` starts it (decision 38); the orchestrator gets again a question that its wait picked up but that has no answer yet (decision 46); the features carry on after a restart of the Mac | Todo | -| `homebrew` | A release tag and a formula in `codedogapp/homebrew-tap`; the docs install baton with Homebrew (decision 5); the release carries the notices of the code built into baton, mvdan.cc/sh's BSD-3 license among them (decision 50) | Todo | +| `release` | A release workflow for each version tag, as nudge's was: baton built for macOS on Apple silicon, its version checked against the tag, in an archive with its license and the notices of the code built into it, mvdan.cc/sh's BSD-3 license among them (decision 50), on the tag's GitHub release with an attestation of its build provenance; the README installs it with mise (decision 5) | Todo | Ideas for later, not planned: a read-only page of the runs, like zeroshot's; approvals through nudge when the user is away (open question 1); other agent CLIs; Linux; for `baton deliver`, a base branch per repo, forks, and a delivery that takes over the open pull request of a feature that doesn't keep it (decision 61). @@ -87,7 +88,7 @@ Ideas for later, not planned: a read-only page of the runs, like zeroshot's; app - The guard against outward actions, and plan approval. - Jira tickets as input; GitHub and Bitbucket Cloud pull requests as output. - The state on disk, so a run survives a restart; AI credits per role. -- Installation with Homebrew. +- Installation from GitHub's releases, with mise. **Out of scope** @@ -237,7 +238,7 @@ Decisions marked **lead's call** are the lead's, for the user's end review. The 2. **`codedogapp/baton`, public** (lead's call): like nudge, it holds no secrets and nothing about any work project. 3. **The name `baton`** (lead's call): the roles pass the baton, and the user takes the last handover. 4. **One Go binary** (lead's call): no bash scripts and their bash 3.2 trouble (fact 15); one file to install; the flow in tested code. -5. **Homebrew, `codedogapp/tap/baton`** (lead's call): a formula that builds baton from its tag's source, as nudge's did. The tap's state is to be checked at the `homebrew` step, since nudge left it. +5. **A GitHub release per version, installed with mise** (the user's call, 2026-11-04, through nudge, in place of the lead's call for Homebrew, a formula in `codedogapp/homebrew-tap`, which the user dropped that day). A version tag starts a workflow, as nudge's releases did before nudge moved to Homebrew: it builds baton for macOS on Apple silicon, checks the binary's version against the tag, and puts it in an archive with its license and the notices of the code built into it (decision 50), on the tag's GitHub release, with an attestation of the archive's build provenance. The user installs it with `mise use -g github:codedogapp/baton`, which checks the archive's digest and attestation, and skips a release less than 24 hours old, as for every mise tool. No formula, no tap and no `brew` command. Each pane puts the folder of its own baton on its `copilot`'s `PATH` (`pane-path`), since the agents run `baton` by name and the user's `PATH` may lack it, or find another baton first. 6. **Jira's REST API with a scoped, read-only token** (lead's call): nothing else to install, and no Rovo credits. The token (`read:jira-work`) lives in the login Keychain, which the user fills. 7. **Claude Opus 5.5 for every role** (lead's call), configurable per role. A weaker engineer ignored rejections in zeroshot's spike (fact 16). 8. **Plan approval on** (lead's call), configurable. The guard makes the user approve each `baton feature start` in the orchestrator's pane, so an orchestrator can't skip it (decision 54); `"planApproval": false` in the config file turns that off (decision 49). @@ -282,7 +283,7 @@ Decisions marked **lead's call** are the lead's, for the user's end review. The 47. **A stop hook sends back a `copilot` that stops while nothing of its role waits** (lead's call, flagged; facts 4 and 5). The plugin's `agentStop` hook runs `"$BATON_BIN" hook stop`, and `baton pane` gives each pane's `copilot` `BATON_BIN`, the path of its own baton, in place of any value of the user's, so the hook runs the baton that started the pane, not one from the `PATH` (decision 42). `baton wait` and `baton ask`, and `baton pane` while it waits, mark their role as waiting: a shared `flock` on `<role>.wait` in the feature's folder, or on `orchestrator.wait` in baton's folder, files that baton never removes, as it never removes a lock (decision 40). The kernel releases the mark when its process ends, so a wait that crashed leaves none, and two waits of one role share it. A wait takes the mark only when its first look finds nothing, so a wait that refuses, or picks its work up at once, writes nothing; that look takes milliseconds, and a model's turn far longer, so the hook sees the mark of a wait that the turn started. `baton hook stop` lets the `copilot` stop while its role's mark is held, since the wait's end wakes it (fact 5). Otherwise it sends it back, with a reason that becomes its next prompt: to start `baton wait` when work can come to it, because another role holds the feature or its work waits there; to ask its question again when that waits for its answer, since baton doesn't ask it twice (decision 46); and otherwise to finish its part and hand over, or just to end its turn when a command of its own runs in the background. The orchestrator gets the first reason while a role holds a feature or its work waits in one, and may stop once it holds every feature with its work picked up, since those wait on the user, or when there is none. The hook changes nothing in baton's folder, and lets the `copilot` stop when its block has sent this turn back already (`stop_hook_active`), so a `copilot` that can't do what the reason says stops one turn later; that turn is also the cost for a role that stops to wait for a command of its own. It finds the repo from the `copilot`'s folder in its input, or from its own folder without one. A hook that fails lets the `copilot` stop (fact 4), so baton reports why on stderr and exits with 1, and copilot logs it. When two hooks look at one mark at the same moment, one can take the other's look for a wait and let its `copilot` stop, as a hook that fails does; only two `copilot`s of one role could do that. Rejected: looking for a `baton wait` in the process table, which needs `ps` and can't tell one pane's wait from another's; a PID file, which a crash leaves behind; and a mark that every wait takes at its start, which would write files for waits that refuse. 48. **Only baton writes in its folder** (lead's call, flagged; fact 29, risk 11). `baton handover` refuses a note, and `baton feature start` a brief, that lies in baton's folder, once a relative path is made absolute from the working folder, so that the check and the read look at the same file, and the symlinks of both paths are resolved; it says to move the file to the session's `files/` folder, and the refusal changes nothing. In the live run the reviewer wrote its notes there, named as baton names the next one: a stray file there misleads whoever reads the notes, and baton numbers a new note after the folder's highest one (decision 26), so a stray shifts the numbers. The prompts send each role's notes, and the orchestrator's briefs and notes, to its session's `files/` folder (`$COPILOT_HOME/session-state/<session id>/files/`), which Copilot CLI names in each session's context, never to the repo's worktrees, where they could get committed, or to baton's folder. They also tell the roles that the folder of the notes that baton hands them is baton's, and to run git and their other commands in the feature's worktree, where their pane starts, since the notes' paths run through the main worktree. So decision 19's guard lets the orchestrator `edit` and `create` only in its session's folder, no longer in baton's. On a disk that ignores case, as a Mac's does by default, a path that spells baton's folder in other case gets past this refusal, which stops only the note that a handover names; the guard's `preToolUse` hook keeps the file tools out of baton's folder, in any case (decision 52), but not `bash` (risk 11). baton doesn't refuse a note in a worktree, which only risks a commit, and doesn't remove a stray that is there already. 49. **One config file, in layers** (lead's call, flagged; decision 16, fact 31). `config.json` in baton's folder under the user's config folder holds the settings for every repo at its top, and each repo's section in `repos`, keyed by the remote's URL as its host and path: `git@github.com:codedogapp/baton.git`, `https://github.com/codedogapp/baton` and `ssh://git@github.com:22/codedogapp/baton.git` all give `github.com/codedogapp/baton`, so a repo's HTTPS and SSH clones share a section; a remote that is a local path keeps its path. baton refuses a key in another form, and says the form; a key keeps the URL's case. The repo's section lies over the top, and the top over baton's own settings, one setting at a time: a setting that a layer leaves out, or gives its zero value, keeps the one below, so a `roundCap` or a `maxAiCredits` of 0 keeps the layer's below, and the branch prefix can't be empty; `planApproval`, `true` or `false`, keeps the one below only when the layer leaves it out or gives `null`, so `false` turns plan approval off; the orchestrator's `extraTools`, a list, replaces the list below whole, and `[]` gives none. baton's own: Claude Opus 5.5 for every role (decision 7), copilot's own reasoning effort, no cap, nudge's two tools for the orchestrator (decision 45), three rounds (decision 13), `feature/` (decision 41) and plan approval on (decision 8). baton reads and checks the whole file, the other repos' sections too, so that a mistake shows at once, with the file's path, and not in a `copilot` that it stops: it refuses a member that it doesn't know (names match case: `roundCap`, not `RoundCap`), a member given twice, a role that it doesn't know, a model or a tool that starts with a dash, which would read as one of copilot's flags, a reasoning effort that copilot doesn't take (CLI 1.0.105's, so a new level needs a new baton), a cap below copilot's least, 30 (fact 10), a negative round cap, and extra tools for any role but the orchestrator (decision 10). A missing file gives baton's own settings. The step came in two: `config` reads the file, and `config-cli` gives its settings to `baton pane` and `baton feature start`. They find the repo's section by its `origin` remote's URL, as `git remote get-url origin` gives it, with the user's `insteadOf` rewrites (fact 32): a repo can have many remotes, and `origin` is the one that `git clone` makes. A repo without an `origin` gets the top's settings, even with other remotes, since baton can't tell which of them is the repo's own. `baton pane` reads the file after it writes the plugin and before it waits (decision 38), the orchestrator's pane too, so a mistake in the file stops a pane before its role picks up work; `baton feature start` reads it before it makes the feature; and `baton start` reads it before it makes the orchestrator's session, since a pane that fails takes its session, and its reason, with it, but not to show a session that runs, which has its settings already; `baton hook guard`, in the orchestrator's pane, reads it for a script that starts a feature (decision 54). A role's settings come after its plugin on copilot's command line: `--model`, then `--reasoning-effort` and `--max-ai-credits` only when the file sets them, the cap in plain digits, as `1000000.5`, never as `1e+06`. The other settings come with their steps. -50. **The guard reads bash with mvdan.cc/sh** (the user's call, 2026-11-03, for the module; lead's call, flagged, for the rest). The guard needs the commands that a bash call runs: a match on the text would miss `git -C x push`, `nohup git push` and `$(git push)`, and catch `echo 'git push'`. The standard library has no bash parser, so `internal/bash`, and only it, imports `mvdan.cc/sh/v3`, the parser of `shfmt`, pinned at exactly v3.14.1 in `go.mod`, which Renovate updates as it does the rest; its license is BSD-3, whose notice the release carries (`homebrew`). `bash.Calls` gives each command that a script runs as its words, as bash passes them: quotes and backslashes taken off, and each part that only the run decides, such as `$HOME` or `$(…)`, as a marker, `bash.Dynamic`. It finds the commands in substitutions, here-documents, functions and conditions too; for a wrapper (`builtin`, `caffeinate`, `command`, `env`, `exec`, `nice`, `nohup`, `stdbuf`, `sudo`, `time`, `timeout`, `xargs` and Homebrew's `g` names, such as `gtimeout`), the command that it runs, past its options, `NAME=value` words and `timeout`'s duration; and for `eval`, and `bash`, `dash`, `ksh`, `sh` and `zsh` with `-c`, the commands of their script. A program's name ignores case, as a Mac's file system does. A script that doesn't parse is an error, which the guard denies, and that is rare (fact 33). It can't see what only the run decides: an alias, a script file or `source`, a variable as the program (`"$X" push`), a script on standard input (`sh -s`, `| sh`, `bash <<EOF`), another language (`python -c`), `find -exec`, git's own aliases (`git -c alias.p=push p`), brace expansion, `$'…'` words, `env -S`, and a long option's value in its own word (`xargs --max-args 1 git push` reads `1` as the program). The guard came in steps: `bash`, then `guard`, `guard-http` and `guard-hook` for the roles' panes (decision 51), `guard-orchestrator` for the orchestrator's, and `belt`, a second belt behind the guard in the roles' panes (decision 55). +50. **The guard reads bash with mvdan.cc/sh** (the user's call, 2026-11-03, for the module; lead's call, flagged, for the rest). The guard needs the commands that a bash call runs: a match on the text would miss `git -C x push`, `nohup git push` and `$(git push)`, and catch `echo 'git push'`. The standard library has no bash parser, so `internal/bash`, and only it, imports `mvdan.cc/sh/v3`, the parser of `shfmt`, pinned at exactly v3.14.1 in `go.mod`, which Renovate updates as it does the rest; its license is BSD-3, whose notice the release carries (`release`). `bash.Calls` gives each command that a script runs as its words, as bash passes them: quotes and backslashes taken off, and each part that only the run decides, such as `$HOME` or `$(…)`, as a marker, `bash.Dynamic`. It finds the commands in substitutions, here-documents, functions and conditions too; for a wrapper (`builtin`, `caffeinate`, `command`, `env`, `exec`, `nice`, `nohup`, `stdbuf`, `sudo`, `time`, `timeout`, `xargs` and Homebrew's `g` names, such as `gtimeout`), the command that it runs, past its options, `NAME=value` words and `timeout`'s duration; and for `eval`, and `bash`, `dash`, `ksh`, `sh` and `zsh` with `-c`, the commands of their script. A program's name ignores case, as a Mac's file system does. A script that doesn't parse is an error, which the guard denies, and that is rare (fact 33). It can't see what only the run decides: an alias, a script file or `source`, a variable as the program (`"$X" push`), a script on standard input (`sh -s`, `| sh`, `bash <<EOF`), another language (`python -c`), `find -exec`, git's own aliases (`git -c alias.p=push p`), brace expansion, `$'…'` words, `env -S`, and a long option's value in its own word (`xargs --max-args 1 git push` reads `1` as the program). The guard came in steps: `bash`, then `guard`, `guard-http` and `guard-hook` for the roles' panes (decision 51), `guard-orchestrator` for the orchestrator's, and `belt`, a second belt behind the guard in the roles' panes (decision 55). 51. **The guard watches commands by program** (lead's call, flagged; decisions 9, 17 and 50, facts 34 and 35). `guard.Commands` gives the commands of a bash call that the guard watches, each with its kind and its name, such as `git push`, which a hook's reason can show; each pane's hook then decides what to do with each kind. It watches what the user named (kickoff: `git push`, force pushes and remote branch deletes, which are pushes too, pull requests and their merges, Jira writes) and what could do the same from a role's pane: outward, git's commands that send to another host (`push`, `send-email`, `send-pack`, `http-push`, `imap-send`), past git's options and the values of those that take one (fact 34), and `git-push` and the like, which name git's command themselves; `gh`'s commands but those that only read (`gh`'s `help`, `search`, `status` and `version`, and the `checks`, `checkout`, `clone`, `diff`, `download`, `list`, `status`, `view` and `watch` of any area, such as `gh pr view`), so an alias counts too, and `gh api` when it writes, as `curl` and `wget` count (below); and `ssh`, `scp`, `sftp`, `acli` and `jira`, whatever they do. Control: `tmux`, which can type into any pane, the user's too, and `osascript`, which drives the Mac's apps. Agent: another agent's CLI (`aider`, `claude`, `codex`, `copilot`, `cursor-agent`, `gemini` and `opencode`), which none of the role's limits bind. Baton: `baton`, with its command, as the roles may run some of baton's commands and not others. Unknown: a program that only the run decides, such as `"$GIT" push`, `eval "$X"` or `bash -c "$X"`, since it could be any of these. A git command or option, or one of `gh`'s first two words, that only the run decides counts as outward too, so the guard fails closed on what it can't see. A program's path doesn't count, only its name, in any case (decision 50), but git's and gh's commands match case, as git and gh do (fact 34). `guard-http` reads the options of `curl`, `wget` and `gh api` as they do (fact 35), and watches them only when they send data: by a method other than `GET` and `HEAD`, or, with no method, with data to send, in any of a curl's requests that `--next` chains; and `gh api graphql` always, since GraphQL posts its reads too. Where it doesn't follow a tool, it fails closed: data counts with curl's `-G`, which puts it into the URL; `gh api -X=GET` counts, though gh reads `GET`; and so does a method or a wget setting that only the run decides, and an option whose letters or name only the run decides (`curl -$x POST`). It misses a whole word of options that only the run decides (`curl $OPTS`), which it can't tell from a URL, the options in curl's and wget's own files (`~/.curlrc`, `-K`, `~/.wgetrc`, `WGETRC`), and what curl sends by protocols other than HTTP, such as FTP's commands (`-Q`), or a `telnet://` URL, which sends its standard input. The guard still misses what decision 50 lists, git's aliases and the commands that git runs itself (hooks, `submodule foreach`, `rebase --exec`, `bisect run`), `git lfs push`, and the tools that it doesn't know, such as `npm publish`, `docker push`, `rsync` to a host, `nc` or a cloud's CLI: in a role's pane, the belt (decision 55) makes the git pushes and `gh` commands among these fail, and the rest stays with risk 2. The step came in three, `guard`, `guard-http` and `guard-hook`, so that each stays near 150 lines. 52. **`baton hook guard` denies a role's tools what the guard watches** (lead's call, flagged; decisions 9, 17, 48, 51 and 56, facts 4 and 36, risks 2, 4, 11, 12 and 13). The plugin's `preToolUse` hook runs `baton hook guard` before each tool that a pane's `copilot` calls. In a role's pane, it denies a `bash` call whose script the guard can't read (decision 50), or that runs a command that the guard watches, but baton's commands for a role: `baton wait`, `baton handover`, `baton ask`, `baton status` and baton's help (`baton help`, `-h`, `-help` and `--help`); the guard names a `baton` command by its first word, or by its first two for `baton feature` and `baton hook`, such as `baton feature start`, so `baton wait -h` passes and `baton answer -h` doesn't. It denies a `create` or an `edit` of a file in baton's folder, which only baton writes (decision 48): it makes the path absolute from the input's `cwd`, resolves the symlinks of the part of the path that is there, and walks up its folders, comparing each with baton's folder as a file (`os.SameFile`), so that neither a symlink, nor a disk that ignores case, nor a new file or folder gets past; in a repo without baton's folder, no path is baton's. Each reason starts with "baton's guard stopped this call, and none of it ran." and says why: the first command that the guard watches, by its name, with its kind's rule (an outward action is the orchestrator's, with the user's approval, so the role keeps its work local and says in its handover note what should go out; a role never drives tmux or the Mac's apps, nor starts another agent; a program must be named in the script itself), the parser's error, with a plea for plain bash, or the file and baton's folder, with the session's `files/` folder for notes. The hook denies the whole call, since a bash call is one script, and the role can try again. It answers only `deny`, and otherwise says nothing, which leaves the tool to `copilot`'s own permissions; the role's other tools only read or look after its shells (fact 36). In the orchestrator's pane, it follows decision 54. When it can't decide, as for input that isn't a JSON object, arguments that don't fit the tool, a pane without `BATON_ROLE` or a folder outside a git repo, it fails, so `copilot` denies the tool (fact 4). It reads only its input and, for a file tool, git's common folder, so it answers well within the hook's timeout, past which `copilot` would allow (risk 4). The guard stops a role's mistakes, not a role that sets out to get past it. It misses `bash`'s own writes, which it doesn't follow: in baton's folder, such as `echo … > .git/baton/…` (risk 11), and to the binary that `BATON_BIN` names, which the hooks run. It misses a hard link, outside baton's folder, to a file in it, which the walk up the link's folders can't see; data that `web_fetch` sends in a URL, as it only reads; other repos' baton folders; and what decisions 50 and 51 list. It denies the file tools in the plugin's cache too, and `baton pane` checks the plugin's files there before each `copilot` starts (decision 53), but `bash` can still change them while a `copilot` starts or runs (risk 12); and in baton's config folder, whose file holds the gate, which `bash` can still change too (decision 56, risk 13). Of all these, the belt (decision 55) stops git's pushes and `gh`; the rest stays with risks 2, 11 and 12. 53. **baton checks the plugin's folder before each `copilot` starts** (lead's call, flagged; decisions 36, 38 and 52, risk 12). `plugin.Write` hashes the files of a folder that has the plugin's name, by their paths in the folder and their contents, as it hashes the binary's, and uses the folder only if the hashes match and it is a folder, not a symlink to one. A folder with other files, more, fewer or changed ones, or with anything but folders and regular files, such as a symlink, and anything else under the name, it leaves as it is, and writes the plugin under the next name, `plugin-<hash>-1`, then `-2`, up to `-7`; with all eight names taken, the pane fails and says why. baton never replaces a folder (Go's `os.Rename` refuses to), so a starting `copilot` never misses the plugin's folder, as it could while a swap ran, and goes on without the hooks and with the default agent (risk 10). Panes that start at once take the same new name, since a folder appears whole (decision 36). A role's pane checks again after its wait, which can last hours, just before it becomes `copilot`; a failure there leaves the work picked up, as a `copilot` that fails to start does (decision 38). The guard denies a role's `create` and `edit` in baton's cache folder, as in baton's folder, and fails without the user's cache folder (decision 52). `bash` can still change the plugin's files after a pane's check, while its `copilot` starts or runs, or through a hard link (risk 2), and a Finder `.DS_Store` in a folder costs one more folder. Rejected: rewriting a changed folder, which a starting `copilot` could see half written; removing it, which a running pane may still use; and making a checked folder read-only, which a role that runs as the user can undo.