Introduction
git-paw (Parallel AI Worktrees) orchestrates multiple AI coding CLI sessions across git worktrees from a single terminal using tmux.
Working with AI coding assistants like Claude, Codex, or Antigravity is powerful — but what if you could run them in parallel across multiple branches at once? That’s exactly what git-paw does.
The Problem
You have a feature branch, a bugfix branch, and a refactoring branch. You want an AI assistant working on each one simultaneously. Normally you’d need to:
- Open multiple terminals
- Create git worktrees manually
- Navigate to each worktree
- Launch your AI CLI in each one
- Juggle between them
The Solution
With git-paw, you run a single command:
git paw
git-paw will:
- Detect which AI CLIs you have installed (Claude, Codex, Antigravity, Aider, etc.)
- Prompt you to pick branches and a CLI (or different CLIs per branch)
- Create git worktrees for each selected branch
- Launch a tmux session with one pane per branch, each running your chosen AI CLI
- Persist the session state so you can stop, resume, or recover after crashes
Key Features
- One command to go from zero to parallel AI sessions
- Smart start — reattaches to active sessions, recovers crashed ones, or launches fresh
- Live branch-set editing (v0.6.0) —
git paw addandgit paw removeattach or detach a single agent mid-session, no stop/purge/restart, the other agents untouched - Per-branch CLI selection — use Claude on one branch and Codex on another
- Session persistence — stop and resume without losing your place
- Custom CLI support — register any AI CLI binary, not just the built-in ones
- Presets — save branch + CLI combos in config for one-command launch
- Non-interactive mode — pass
--cliand--branchesflags for scripting - Dry run — preview what git-paw will do before it does it
How It Works
┌──────────────────────────────────────────────────────┐
│ tmux session │
│ ┌────────────────────┐ ┌────────────────────────┐ │
│ │ feat/auth → claude │ │ feat/api → claude │ │
│ │ │ │ │ │
│ │ (git worktree) │ │ (git worktree) │ │
│ │ │ │ │ │
│ ├────────────────────┤ ├────────────────────────┤ │
│ │ fix/bug → codex │ │ refactor/db → aider │ │
│ │ │ │ │ │
│ │ (git worktree) │ │ (git worktree) │ │
│ │ │ │ │ │
│ └────────────────────┘ └────────────────────────┘ │
└──────────────────────────────────────────────────────┘
Each pane runs in its own git worktree, so there are no branch conflicts. Your AI assistants work independently and in parallel.
Requirements
- Git 2.5+ (worktree support)
- tmux 1.8+
- At least one AI coding CLI installed (see Supported AI CLIs)
- macOS or Linux (Windows via WSL only)
Next Steps
- Install git-paw
- Quick Start: Same CLI Mode — get running in 2 minutes
- Quick Start: Per-Branch CLI Mode — mix different AI CLIs
Machine-Readable Docs (for Agents)
This site — https://bearicorn.github.io/git-paw/ — also publishes an agent-friendly discovery surface so an LLM can find and target the right page without crawling every chapter:
llms.txt— an llmstxt.org-style index listing every page with a one-line summary and its absolute URL.sitemap.xmlandrobots.txt— standard crawl and discovery files.- Per-page metadata — each page embeds a
<meta name="description">and a JSON block (title, canonical URL, section anchor ids) in its<head>, so an agent can identify a page and jump to a section from a single fetch.
These artifacts are generated deterministically from the documentation sources on every build, so they never drift from the published content.
Installation
Prerequisites
Before installing git-paw, ensure you have:
- Git 2.5 or later (worktree support)
- tmux 1.8 or later
git paw doctor checks both versions for you.
Installing tmux
macOS:
brew install tmux
Ubuntu / Debian:
sudo apt install tmux
Fedora:
sudo dnf install tmux
Arch Linux:
sudo pacman -S tmux
Install git-paw
From crates.io (recommended)
cargo install git-paw
Via Homebrew
brew install bearicorn/tap/git-paw
Shell installer
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/bearicorn/git-paw/releases/latest/download/git-paw-installer.sh | sh
From source
git clone https://github.com/bearicorn/git-paw.git
cd git-paw
cargo install --path .
Verify installation
git-paw --version
You should see output like:
git-paw 0.16.0
Since git-paw is named with the git- prefix, git recognizes it as a subcommand. Both of these work:
git-paw --help
git paw --help
Shell Completions
git paw completions <shell> prints a completion script to stdout, generated
from the CLI definition. Install it through your shell’s standard mechanism:
bash:
git paw completions bash > /etc/bash_completion.d/git-paw
# or, for a user-local install:
git paw completions bash > ~/.local/share/bash-completion/completions/git-paw
zsh:
git paw completions zsh > "${fpath[1]}/_git-paw"
fish:
git paw completions fish > ~/.config/fish/completions/git-paw.fish
Open a new shell (or re-source your shell config) to pick up the completions.
Platform Support
| Platform | Support |
|---|---|
| macOS (ARM / Apple Silicon) | Full support |
| macOS (x86_64 / Intel) | Full support |
| Linux (x86_64) | Full support |
| Linux (ARM64 / aarch64) | Full support |
| Windows | WSL only |
Windows (WSL)
git-paw requires tmux, which is not natively available on Windows. Use Windows Subsystem for Linux (WSL):
# Install WSL (PowerShell as admin)
wsl --install
# Then inside WSL:
sudo apt install tmux
cargo install git-paw
All git-paw features work inside WSL. Your AI CLIs must also be installed within the WSL environment.
Install an AI CLI
git-paw needs at least one AI coding CLI installed. See Supported AI CLIs for the full list. Some popular options:
# Claude Code
npm install -g @anthropic-ai/claude-code
# OpenAI Codex
npm install -g @openai/codex
# Aider
pip install aider-chat
Next Steps
Quick Start: Same CLI Mode
This walkthrough shows how to launch git-paw with the same AI CLI on all branches — the most common workflow.
Prerequisites
- git-paw installed
- tmux installed
- At least one AI CLI installed (e.g.,
claude) - A git repository with multiple branches
Step 1: Navigate to your repo
cd ~/projects/my-app
Step 2: Launch git-paw
git paw
If neither your repo nor your global config has a [supervisor] section,
git-paw first asks Start in supervisor mode? [y/N] — answer n (the default)
for this walkthrough.
Step 3: Select branches
A multi-select list of all your branches (sorted by name) appears with type-to-filter search:
Select branches (type to filter, ctrl-u to clear, space to toggle, enter to confirm)
filter:
[x] feat/api
[x] feat/auth
[ ] fix/typo
[ ] main
> [x] refactor/db
Type to narrow the list (case-insensitive substring match), use arrow keys to navigate, Space to toggle, Ctrl-U to clear the filter, and Enter to confirm. Branches you toggle stay selected even if a later filter hides them.
Step 4: Select your mode
git-paw presents a mode picker:
CLI assignment mode:
> Same CLI for all branches
Different CLI per branch
Select Same CLI for all branches and press Enter.
Step 5: Select your CLI
Pick which AI CLI to use on all selected branches. Detected CLIs are listed by display name, with the binary in parentheses:
Select AI CLI for all branches:
Antigravity (agy)
> Claude (claude)
Codex (codex)
Step 6: git-paw does the rest
git-paw now:
- Creates a git worktree for each selected branch
- Creates a tmux session named
paw-my-app - Opens one pane per branch
- Launches your chosen CLI in each pane
- Saves the session state for later recovery
The worktrees land under .git-paw/worktrees/ (for example
.git-paw/worktrees/feat-auth/) in a repo set up with git paw init, or beside
the repo (../my-app-feat-auth/) when no worktree_placement is configured —
see Worktree Placement. Nothing else is
printed on success: git-paw attaches you straight to the session. (From a
non-interactive terminal it prints Session 'paw-my-app' started in detached mode. and a tmux attach -t paw-my-app hint instead.)
You’re now inside a tmux session with three panes, each running Claude in its own worktree. Each pane’s border label shows its pane index and branch (with [broker] enabled = true, the dashboard takes pane 0 and the agents start at pane 1):
┌─── 0: feat/api ───────────────┬─── 1: feat/auth ──────────────┐
│ │ │
│ Claude is ready to help... │ Claude is ready to help... │
│ │ │
├─── 2: refactor/db ────────────┴────────────────────────────────┤
│ │
│ Claude is ready to help... │
│ │
└─────────────────────────────────────────────────────────────────┘
Mouse mode is enabled by default — click a pane to switch to it, or drag borders to resize.
Non-interactive shortcut
Skip all prompts by passing flags:
git paw start --cli claude --branches feat/auth,feat/api,refactor/db
What’s next
- Switch panes: Click with mouse, or use
Ctrl-bthen arrow keys - Detach: Press
Ctrl-b dto detach from tmux (session keeps running) - Reattach: Run
git paw attach(orgit pawagain — it detects the active session and reattaches) - Stop: Run
git paw stopto kill tmux but keep worktrees - Purge: Run
git paw purgeto remove everything
See the User Guide for the full details.
Quick Start: Per-Branch CLI Mode
This walkthrough shows how to assign different AI CLIs to different branches — useful when you want to compare AI assistants or use specialized tools for specific tasks.
Scenario
You have three branches and want to use:
- Claude for the auth feature (complex logic)
- Codex for the API work (lots of boilerplate)
- Aider for the database refactor (incremental edits)
Step 1: Launch git-paw
cd ~/projects/my-app
git paw
(If neither your repo nor your global config has a [supervisor] section,
git-paw first asks Start in supervisor mode? [y/N] — answer n.)
Step 2: Select branches
Select branches (type to filter, ctrl-u to clear, space to toggle, enter to confirm)
filter:
[x] feat/api
[x] feat/auth
[ ] main
> [x] refactor/db
Type to filter the list (case-insensitive substring match); selections persist across filter changes, and Ctrl-U clears the filter.
Step 3: Select per-branch mode
CLI assignment mode:
Same CLI for all branches
> Different CLI per branch
Step 4: Assign a CLI to each branch
git-paw prompts you for each selected branch in turn (in the order they are listed above):
Select CLI for feat/api:
Aider (aider)
Claude (claude)
> Codex (codex)
Select CLI for feat/auth:
Aider (aider)
> Claude (claude)
Codex (codex)
Select CLI for refactor/db:
> Aider (aider)
Claude (claude)
Codex (codex)
Step 5: Watch it launch
git-paw creates a worktree per branch, opens the paw-my-app tmux session with
each branch’s chosen CLI in its own pane, and attaches you to it — nothing else
is printed on success.
Each pane’s border label shows its pane index and branch (with [broker] enabled = true, the dashboard takes pane 0 and the agents start at pane 1):
┌─── 0: feat/api ───────────────┬─── 1: feat/auth ──────────────┐
│ │ │
│ Codex is ready... │ Claude is ready to help... │
│ │ │
├─── 2: refactor/db ────────────┴────────────────────────────────┤
│ │
│ Aider v0.x loaded... │
│ │
└─────────────────────────────────────────────────────────────────┘
Comparing approaches
Per-branch mode is great for:
- A/B testing AI assistants — give the same task to Claude and Codex, compare results
- Specialization — use a code-generation-focused tool for boilerplate and a reasoning-focused tool for complex logic
- Trying new tools — test a new AI CLI on one branch while using your trusted tool on others
Recovery
Session state captures the per-branch CLI assignments. If your terminal closes or tmux crashes:
git paw
git-paw detects the saved session, recreates tmux, and relaunches each branch with its assigned CLI — no re-selection needed.
Next Steps
- User Guide — session management, presets, dry-run, and more
- Configuration — save per-branch presets in config
Quick Start: Orchestrator Mode
This walkthrough shows how to launch git-paw in supervisor mode and drive it from your own agent session, the orchestrator. Several coding agents work in parallel; the orchestrator verifies them, runs tests between merges, and publishes a final summary.
What Orchestrator Mode Does
Supervisor mode with a spec flag (git paw start --supervisor --from-all-specs) launches every pending spec as its own coding agent in a tmux pane, with the dashboard above them. (Bare git paw start --supervisor, with no spec flag, opens the interactive branch picker instead.)
The orchestrator runs outside the tmux session. No orchestrator runs inside the session. The orchestrator is your own agent session (any agent CLI you like), run at the repository root and outside tmux. It loads the exported orchestrator skill and the per-session context, then drives the run through the bundled helpers (.git-paw/scripts/sweep.sh, broker.sh) or git-paw’s MCP server. It runs the configured test_command between merges, walks branches in topological order based on declared dependencies, and publishes a final summary status when the run is complete.
It does not approve permission prompts. Whether a command runs without asking is your agent CLI’s decision, and anything it prompts for is escalated to you. For a run that proceeds without stopping, pre-approve the commands in that CLI’s own allow rules, or use agent_approval = "full-auto" inside a sandbox.
Prerequisites
- The standard git-paw prerequisites: git-paw installed, tmux, an AI CLI on
PATH, and a git repo. [broker] enabled = truein your config. Supervisor mode requires the coordination broker.- A
[supervisor]section is recommended but optional. If it is missing,--supervisorruns on built-in defaults, and a plaingit paw startasksStart in supervisor mode?. No orchestrator CLI is needed to launch: the coding agents’ CLI resolves from--cli, thendefault_cli, then[supervisor].cli, and the launch fails only when none of them is set. - A specs directory configured via
[specs](default:specs/) containing at least one pending spec.
Smallest Runnable Example
1. Initialize the repository
git paw init
git paw init writes .git-paw/config.toml (asking whether to enable supervisor mode by default and, if you do, for the test command) and installs the bundled helpers under .git-paw/scripts/. It also exports two skills and adds one pointer section:
.git-paw/skills/orchestrator/SKILL.mdis the orchestrator skill, rendered with your gate commands. It is the procedure your agent follows..git-paw/skills/cli-setup/SKILL.mdholds the consent rules forgit paw setup-cli.- A section between
<!-- git-paw-orchestrator:start -->and<!-- git-paw-orchestrator:end -->is added to your rootAGENTS.md. It tells any agent reading the file that, when a git-paw session is active, it is that session’s orchestrator and should read the two files above.
Both skills follow the standard skill-directory layout (a SKILL.md with name and description frontmatter), so a skill-aware agent CLI can load them directly. Copy or symlink them into your CLI’s own skill directory if it needs them there. .git-paw/skills/ is gitignored, and git paw start re-renders both files, so they never lag your config or the binary. Re-running git paw init reports Updated only for a file whose content changed.
2. Create one OpenSpec-format spec
mkdir -p specs/add-greeting
cat > specs/add-greeting/tasks.md <<'MD'
## Add a greeting helper
- [ ] Add `pub fn greet(name: &str) -> String` to `src/lib.rs`.
- [ ] Add a unit test asserting `greet("paw") == "hello, paw"`.
- [ ] Run `cargo test` and make sure it passes.
MD
3. Configure broker + supervisor
.git-paw/config.toml:
[broker]
enabled = true
port = 9119
[specs]
dir = "specs"
type = "openspec"
[supervisor]
enabled = true
cli = "claude"
test_command = "cargo test"
agent_approval = "auto"
4. Launch
git paw start --supervisor --from-all-specs
git-paw creates a worktree for each spec and opens a tmux session. The dashboard is in pane 0 (a full-width top row). There is one coding agent per spec in the panes below it, from pane 1 onwards. The launch then prints the attach command and the two orchestrator paths:
Supervisor session 'paw-myproject' launched with 1 coding agent(s).
Orchestrator context: .git-paw/AGENTS.local.md (point your orchestrating agent session at it)
Orchestrator skill: .git-paw/skills/orchestrator/SKILL.md
Attach with: tmux attach -t paw-myproject
.git-paw/AGENTS.local.md is the short session context. It holds the session name, the broker URL, the helper paths, any [governance] documents and, under --unattended, the drive-loop directive, and it points at the skill. It is the only file that carries this session’s values. The exported skill carries none, so the same SKILL.md serves every session.
5. Load the skill in your own agent
Open your own agent session at the repository root, outside tmux. Have it load .git-paw/skills/orchestrator/SKILL.md and read .git-paw/AGENTS.local.md. An agent that reads the root AGENTS.md finds both through the pointer section. Over MCP, get_skill with the name orchestrator serves the same skill (supervisor is accepted as an alias).
From there the orchestrator:
- Follows events. It blocks on the broker’s event cursor (the MCP
wait_for_eventstool, or.git-paw/scripts/sweep.sh events), carrying the returned cursor forward. It inspects the session when an event or a stalled wave says to look, so it never polls pane captures on a timer.get_session_statusandsweep.sh statusgive the roster at any time. - Verifies. It runs the five gates against each branch that reports done. It publishes a verdict only through the credential-carrying helpers:
sweep.sh verified <agent> <message>,sweep.sh feedback-gate <agent> <gate> <message>orsweep.sh conflict-ack <agent-a> <agent-b> <file>. In the root session, the MCPpublish_verifiedandpublish_feedbacktools do the same. - Answers. It replies to a worker’s
agent.questionwithsweep.sh answer <agent> <text> [re]. - Merges. Once every expected agent is verified, it fast-forwards each branch in topological order, runs the test command after each merge, and announces each advance with
sweep.sh advanced-main <merged-branch> <summary>. In an attended run it may confirm the merge order with you first.
When every branch has merged (or been skipped), it publishes a final agent.status summarising which branches merged and which were skipped, and why.
6. Pair it with --unattended
git paw start --unattended --from-all-specs
--unattended adds an in-process drive loop: it keeps the coding-agent panes moving and escalates every live permission prompt to the broker, approving none. Your orchestrator reads those escalations through the event surface and relays each prompt to you. It runs sweep.sh approve <pane> only when you say so. In an unattended run the orchestrator merges as soon as every expected agent is verified, without asking first. It escalates only on a conflict (a non-fast-forward branch or a dependency cycle), an exhausted correction cycle, or a regression. If the loop exits on its heartbeat, re-run git paw start --unattended to re-attach a drive loop to the still-live session.
Your driving session can itself be remote-controlled from a phone through its own agent CLI. git-paw needs no extra topology for that. See Remote control.
Authoring a Change from Your Own Agent
An orchestrator connected over MCP can write the OpenSpec change it then launches, even without file tools of its own. Repository writes are off by default; you opt in per repository:
-
Enable the switch in the repository’s own
.git-paw/config.toml(the user-level config cannot enable it):[mcp] allow_repo_writes = true -
Restart the MCP server at the repository root. The switch is read once at startup, and the write tools exist only for a server in the main working tree.
-
Scaffold the change with
create_specand a kebab-case name, such asadd-greeting. -
Write each artifact with
write_spec_artifact:proposal.md, onespecs/<capability>/spec.mdper capability,tasks.md, and anything else your workflow uses. -
Check the structure with
validate_spec, and fix what it reports. -
Run your project’s own spec validator.
validate_specchecks only a subset of the rules. -
Review
git diffand commit yourself, following your project’s conventions. The tools never stage or commit.
See Spec authoring for the tools’ parameters and the confinement rules.
Skipping Supervisor for One Session
If your project sets [supervisor] enabled = true (the recommended setup for active development), every git paw start launches in supervisor mode. To skip it for a single session, pass --no-supervisor. This suits a quick debug-only run or a one-off branch flip, and you don’t need to edit the config:
# Project config says [supervisor] enabled = true, but we want a plain session
git paw start --no-supervisor --cli claude --branches feat/quick-fix
--no-supervisor is the highest-precedence step in the supervisor mode resolution chain. It wins over both [supervisor] enabled = true in config and any prompt. It is mutually exclusive with --supervisor; passing both fails at parse time.
The flag only affects the current session. Your config is untouched, so the next git paw start returns to supervisor mode as configured.
Key Config Knobs
The full reference lives in Configuration → Supervisor. The fields you will reach for most often:
| Field | What it controls |
|---|---|
[supervisor].enabled | Default to supervisor mode without passing --supervisor. |
[supervisor].cli | The orchestrator’s CLI, named in its context (falls back to default_cli); also the coding agents’ last CLI fallback. No pane runs it. |
[supervisor].test_command | Command run after each agent reports done and again after every merge. Rendered into the exported skill. |
[supervisor].agent_approval | "manual", "auto", or "full-auto". Translates into CLI permission flags for the coding agents. |
[supervisor].approval | The orchestrator’s own level, reserved for git-paw-initiated orchestrator invocations. Still parsed; it changes no pane command. |
[supervisor].manual_approvals_log | Records every prompt an --unattended run escalates, for git paw approvals to aggregate (default on). |
[supervisor.auto_approve]and[supervisor.common_dev_allowlist]are deprecated and have no effect. They still parse so old configs load, but each file declaring them warns once. Pre-approve routine commands in your agent CLI’s own allow rules instead. See Deprecated auto-approval tables.
See Configuration → Broker for [broker] settings and Configuration → Dashboard if you want the live broker-message panel turned on.
Broker Wire Format
Every coordination message the orchestrator and its agents exchange goes through the broker as a JSON envelope. The canonical example below comes from src/broker/messages.rs. The broker accepts an agent_id of supervisor or a lowercase two-part slug: letters, digits and -, with the parts joined by - or /. git-paw itself publishes the slug form, so branch feat/auth becomes feat-auth.
{"type": "agent.status", "agent_id": "feat-auth", "payload": {"status": "working", "modified_files": ["src/auth.rs"], "message": "wiring JWT verifier"}}
Nobody hand-writes these envelopes:
- The coding agents publish theirs through
.git-paw/scripts/broker.sh(status,artifact,blocked,question,intent) or the MCP publish tools. - The orchestrator publishes through
.git-paw/scripts/sweep.sh(status-publish,verified,feedback-gate,conflict-ack,learn,answer,advanced-main) orbroker.sh --agent supervisor(question,intent). - The filesystem watcher and the post-commit hook publish
agent.statusandagent.artifactautomatically.
The rest of the message set is agent.blocked, agent.intent, agent.question, agent.answer, agent.feedback, agent.verified, agent.advanced-main and agent.learning, plus the broker-emitted supervisor.verify-now. See Agent Coordination for the full reference with payload-field semantics.
Where to Look When Things Go Wrong
When a run misbehaves, these places have everything you need:
- Dashboard pane (pane 0). With
[broker] enabled = truethe dashboard runs in pane 0 of the session and updates every second. Use it to spot which agent isblocked, which isworking, and how long it has been since each agent’s last status update. See the Dashboard chapter for status-symbol meanings and controls. - Your orchestrator session. The orchestrator runs in your own agent session outside tmux and reports there as it walks agents through the merge plan. Its broker messages also appear in the dashboard’s broker log.
- The orchestrator’s final status. When its merge loop completes, the orchestrator publishes a final
agent.status(agentsupervisor). It lists which branches merged cleanly, which were skipped and why, and any regressions it hit. It shows up in the dashboard’s broker log panel. <session_name>.broker.log. This plain-text audit trail has one line per message the broker handled ([<seq>] <unix-seconds> [<agent_id>] <message summary>).- It lives in git-paw’s sessions directory, not in
.git-paw/. On Linux that is~/.local/share/git-paw/sessions/<session_name>.broker.log(or under$XDG_DATA_HOME). On macOS it is~/Library/Application Support/git-paw/sessions/<session_name>.broker.log. - The name carries the session, so two repositories each keep their own log.
- Tail it live with
tail -fto watch coordination as it happens. The file is appended every five seconds; see Agent Coordination for the message types.
- It lives in git-paw’s sessions directory, not in
To inspect an individual agent’s pane history, run .git-paw/scripts/sweep.sh capture <pane>, or tmux capture-pane -p -t <session>:0.<pane> against the session named in .git-paw/AGENTS.local.md. The agent panes are in window 0; the session’s second window, broker, hosts the broker.
Governance
If your project has existing governance docs (ADRs, DoD, security checklist, test strategy, constitution), point the orchestrator at them via the [governance] table in .git-paw/config.toml. When set, those paths are written into the session context (.git-paw/AGENTS.local.md), and the orchestrator consults each doc as a sub-step of its spec-audit step. Findings flow through the existing agent.feedback errors path, with no new wire format. See the Governance chapter for the full config, what the orchestrator checks per doc, and illustrative starting-point examples for each doc type.
What’s Next
- User Guide → Supervisor: driving the session from your own agent, in depth.
- User Guide → Remote control: driving the session from a phone.
- User Guide → MCP Server: the read and write tools the orchestrator can use.
- User Guide → Agent Coordination: broker message types and how agents talk to each other.
- User Guide → Dashboard: the dashboard pane’s status table (pane 0 in every mode with a broker), controls, and configuration.
- User Guide → Governance: pointing the orchestrator at your project’s existing governance docs.
- Configuration → Supervisor: every supervisor field with defaults.
User Guide
This guide covers the full range of git-paw features beyond the quick starts.
Starting Sessions
Interactive start
Running git paw (or git paw start) with no flags launches the interactive flow:
- Branch selection — multi-select with type-to-filter search (type to narrow, ctrl-u to clear, space to toggle, enter to confirm)
- Mode selection — same CLI for all branches, or different CLI per branch
- CLI selection — single pick (uniform) or per-branch assignment
Smart start behavior
git paw start inspects the current repo and decides what to do:
| State | Behavior |
|---|---|
| Active tmux session exists | Reattaches immediately (with no TTY — a script — it refuses with an error naming the session instead) |
| Paused session | Restarts the dashboard/broker and reattaches; the CLI panes are untouched |
Stopped session (git paw stop, or a paused session whose tmux died), or a live tmux session whose agent panes have all dropped back to a shell | Recovers: reuses worktrees, recreates tmux, relaunches CLIs (resuming conversations where the CLI supports it) |
| Stale receipt (says active, but its tmux session is gone — a crash or reboot) | Clears the receipt the way git paw purge --stale does, then launches fresh |
| No session | Full interactive launch |
You never need to think about whether to “start” or “resume” — just run git paw.
Non-interactive start
Skip prompts with flags:
# Specify both CLI and branches — no prompts at all
git paw start --cli claude --branches feat/auth,feat/api
# Specify just CLI — still prompted for branches
git paw start --cli claude
# Specify just branches — still prompted for CLI
git paw start --branches feat/auth,feat/api
Using presets
Define named presets in your config (see Configuration):
git paw start --preset backend
This uses the branches and CLI defined in the [presets.backend] section of your config.
CLI Modes
Same CLI for all branches
The default mode. Every branch gets the same AI CLI. Best for:
- Working on related features with your preferred tool
- Batch processing branches with a single assistant
Different CLI per branch
Assign a different CLI to each branch. Best for:
- Comparing AI assistants side by side
- Using specialized tools for specific tasks
- Trying a new CLI on one branch while keeping your usual tool on others
Session Management
Checking status
git paw status
Displays the current session state:
Session: paw-my-app
Status: 🟢 active
Tmux: running
feat/auth → claude (/path/to/my-app/.git-paw/worktrees/feat-auth)
feat/api → claude (/path/to/my-app/.git-paw/worktrees/feat-api)
refactor/db → aider (/path/to/my-app/.git-paw/worktrees/refactor-db)
With the broker enabled, a Broker: line shows its URL and whether it is
responding. git paw status --json emits the same information
machine-readably, including each worker’s cli_session_id.
Status indicators:
- 🟢 active — tmux session is running
- 🔵 paused —
git paw pausedetached it; CLI panes are still running - 🟡 stopped — session state saved and tmux not running, or tmux up but
every agent pane exited (
git paw startrecovers) - 🔴 stale — the receipt claims active, but the tmux session is gone;
git paw startself-heals it,git paw purge --staleclears it - No session — nothing saved for this repo
Reattaching
git paw attach
Reattaches your terminal to the session running for this repository — the same
as tmux attach -t paw-<project>, without needing the exact session name. It
errors with a pointer to git paw start when no session is running.
Stopping a session
git paw stop
This kills the tmux session but preserves:
- Git worktrees (with any uncommitted work)
- Session state file (branch/CLI assignments)
Run git paw later to recover the session with the same setup.
Purging a session
git paw purge
The nuclear option. Removes:
- Tmux session
- All git worktrees created by git-paw
- Session state file
Requires confirmation. Use --force to skip:
git paw purge --force
Dry Run
Preview what git-paw will do without executing:
git paw start --dry-run
Or with flags:
git paw start --cli claude --branches feat/auth,feat/api --dry-run
This runs the detection, selection, and planning steps, then prints the session plan and exits without creating worktrees or tmux sessions.
Tmux Navigation
Once inside a git-paw tmux session:
| Action | Keys |
|---|---|
| Switch pane | Click with mouse, or Ctrl-b + arrow key |
| Resize pane | Drag border with mouse, or Ctrl-b Ctrl-arrow |
| Detach (keep running) | Ctrl-b d |
| Scroll up | Ctrl-b [ then arrow keys, q to exit |
| Zoom pane (fullscreen toggle) | Ctrl-b z |
Mouse mode is enabled by default, so clicking and dragging just works. You can disable it in your config.
One Session Per Repo
git-paw manages one session per repository. If you run git paw in a repo that already has a session, it reattaches rather than creating a second session.
To work with multiple repos simultaneously, open separate terminals and run git paw in each repo directory.
Shell Completions
git paw completions <shell> prints a completion script to stdout for your
shell’s standard completion mechanism:
git paw completions bash > /etc/bash_completion.d/git-paw
git paw completions zsh > "${fpath[1]}/_git-paw"
git paw completions fish > ~/.config/fish/completions/git-paw.fish
See the CLI reference for the supported shells.
Session Lifecycle
This chapter covers what git paw start does to your worktrees on each
launch (or relaunch), and how to control its behaviour when the defaults
don’t fit.
Reattaching instead of forking
Before creating anything, git paw start looks up whether the current
repository already has a live session (matched by repository path, not by
project/session name) and reattaches to it instead of launching a fresh
one. This applies uniformly whether the session is a bare launch or a
supervisor-mode one ([supervisor] enabled = true in config, or
--supervisor) — a supervisor-enabled repo’s bare git paw start still
goes through the same guard before it ever reaches the supervisor
auto-start flow.
- Interactively, it attaches your terminal to the existing session —
identical to running
git paw attach. - Non-interactively (no TTY — a script, a cron job, an automated
scanner), there is nothing to attach to. Rather than silently printing a
hint and exiting successfully — which an automated caller could mistake
for “a fresh session was launched” —
startrefuses with an actionable error naming the live session and how to attach to it.
The -N suffix (paw-<project>-2, -3, …) is reserved for genuinely
distinct repositories that happen to share a project name (their
directory basename); it is never applied as a result of re-running start
against a repository that already has a session running.
Rebase on start
Every git paw start rebases each existing agent branch onto the
repository’s default branch (whatever origin/HEAD tracks — typically
main) before opening or reopening that branch’s worktree. This
keeps agents starting from current main, which matters when the
supervisor (you) advances main while agents are still working on their
branches.
Why this is the default:
- A supervisor session typically lands commits on
mainwhile one or more agents are running onfeat/*branches. Without rebasing on start, every subsequent agent commit chains from the stale baseline, forcing a per-branch rebase at merge time. - The rebase is cheap when there’s nothing to do:
git rebaseexits zero with no rewrite when the branch is already up to date. - The branch ref is updated even in the resume case —
git paw starton a session whose worktrees already exist on disk picks up the newmaincommits transparently.
Rebases that don’t happen:
- A brand-new branch (one git-paw creates via
git worktree add -bduring this launch) is not rebased. It’s already at the current default-branch tip by construction.
Resolving the default branch without origin/HEAD
The rebase target is resolved by a single resolver shared by every launch
path (start, add, and the rebase step itself), so start and a later
resume can never disagree on which branch is “the default”. It prefers
refs/remotes/origin/HEAD (whatever your remote’s default branch is —
typically main); when that isn’t set (a repository with local-only
commits and no remote, or a remote whose HEAD was never configured), it
falls back deterministically instead of aborting:
- A local
mainbranch, if one exists. - Otherwise a local
masterbranch, if one exists. - Otherwise the currently checked-out branch.
This means a fresh launch and every subsequent resume resolve to the same
default branch and neither aborts for a reason the other didn’t hit —
push the default branch to origin and run git remote set-head origin <branch> if you’d rather pin it explicitly than rely on the fallback.
Opting out: --no-rebase
Pass --no-rebase when you want the pre-v0.6 behaviour — agent branches
opened at their current SHA with no rebase attempt:
git paw start --no-rebase
git paw start --no-rebase --cli claude --branches feat/auth,feat/api
git paw start --no-rebase --supervisor # combines with other flags
Use cases:
- You are deliberately working against a stale baseline (e.g. an agent is reproducing a bug on a specific historical commit).
- An external tool has pinned a pre-rebase SHA on the agent branch and rewriting history would break it.
- You’re scripting against the pre-v0.6 launch contract and don’t want the new default behaviour.
Rebase conflicts
If the rebase hits a conflict — typically because both main and the
agent branch modified the same line of the same file — git paw start:
- Runs
git rebase --aborton the affected branch. - Leaves the branch at its pre-rebase HEAD (no half-rebased state survives).
- Exits with an error like
Error: rebase onto main failed: <git's stderr listing the conflicting files>.
When you see this error:
cdinto the affected agent’s worktree (by default at.git-paw/worktrees/<branch-slug>/;../<project>-<branch-slug>/withworktree_placement = "sibling").- Inspect the conflicting files git named in the error.
- Either reconcile manually (e.g.
git rebase mainfollowed by resolving conflicts andgit rebase --continue), or decide the change should land via a merge commit instead. - Once the branch is in a clean state, rerun
git paw start(with or without--no-rebasedepending on what you decided).
The conflict-abort path is intentional and not configurable — leaving a branch half-rebased poisons every subsequent git operation against it, which is far worse for the agent in that pane than a clean error at launch time.
What rebase does NOT do
- It does not
git fetchorgit pullformain. The rebase target is whateverorigin/HEADtracks locally; the user owns the remote-sync decision. - It does not leave a surviving worktree’s files behind its branch. The rebase runs inside the worktree that has the branch checked out, so on resume that worktree’s files move with the rebased HEAD. If git refuses the rebase there (for example, the worktree has uncommitted changes), it fails like a conflict — see above.
- It does not rebase one agent branch onto another agent branch. Only
feat/<change>onto the repository’s default branch. - It does not loop or retry on failure. One attempt, abort on conflict, surface the error.
Resume vs. fresh launch
The rebase step runs in both flows:
- Fresh launch: branch exists locally, no worktree yet. Branch is
rebased onto
main, then the worktree is created at the rebased SHA. - Resume (worktree already on disk from a prior session): branch
is rebased in the existing worktree, the existence check confirms
the worktree is registered for that branch, and
git paw startreturns success with the worktree’s branch ref now pointing at the rebased HEAD.
If you’re debugging a launch and want to know whether the rebase
modified anything, git rev-parse <branch> from the main repo before
and after git paw start will show the SHA change (or lack thereof).
The broker host
When the broker is enabled, every launch path — git paw start, the
spec-driven launch, the supervised launch, cold recovery and resume from
pause — runs the broker in its own process, git paw __broker, in a
detached tmux window named broker. The window sits outside the pane grid,
so pane numbering is unchanged; switch to it to see the host’s output.
A launch opens the session, starts the host, and waits up to 10 seconds for it to answer before it registers the agents with it, saves the session, or sends any boot prompt. If the host exits or never answers — most often because another process holds the broker port — the launch fails with an error naming the port and saves no session, so nothing half-launched is recorded. The dashboard in pane 0 is a client of the host: it shows ⚠ broker unreachable while the host is down and reconnects on its own.
git paw pause closes the broker window (the dashboard stays);
git paw stop ends the whole session, host included. Both flush the
session’s broker log on the way out.
Cold recovery resumes worker context
When git paw start recovers a stopped session — including a paused
session whose tmux server died, and a session still up in tmux whose
every agent pane has dropped back to a bare shell (crashed CLIs) — a
CLI that supports it is relaunched with its prior conversation context
restored instead of starting cold. See Per-CLI cold resume
for the mechanism (session-id assignment at launch, resume on
recovery) and which CLIs it covers today.
A session whose panes dropped back to a bare shell reports 🟡 stopped
with Tmux: running, never 🔴 stale — its tmux session is still there,
so git paw purge --stale leaves it alone and start is the remedy.
A receipt that still claims active after its tmux session vanished
(a crash, or a machine sleep that tore the tmux server down) is not
recovered: git paw status reports it as 🔴 stale, and start
clears it the way git paw purge --stale does, then launches fresh.
Adding and removing branches mid-session
Before v0.6.0 the branch set was frozen at git paw start time: changing
it meant stop/purge + re-start, which destroyed every agent’s
in-flight conversation. git paw add and git paw remove edit a running
supervisor session’s branch set in place — the other agents keep working
undisturbed.
git paw add — hot-attach an agent
git paw add feat/new-thing # attach a worktree + pane, session's default CLI
git paw add feat/api --cli codex # choose the CLI for the new pane
git paw add --from-spec add-export # derive branch + CLI from a discovered spec
What it does:
- Resolves the branch and CLI — from the positional argument, or (with
--from-spec) from a spec discovered in the configured[specs]format — OpenSpec, Markdown, Spec Kit, or Superpowers (same resolution asgit paw start --specs NAME). An unknown spec name errors with the discovered candidate list; an unknown--clierrors with the detected CLI ids — and in both cases nothing is created. - Refuses — before touching anything — a branch whose broker
agent_idis already taken by an agent in the session. The id is the slugified branch name, so two different branches can map to one id: addingfeat-xto a session already runningfeat/xfails naming both branches and the shared idfeat-x, because the two agents would share one inbox. Rename the branch so the ids differ. (Only when the broker is enabled; with it off, nothing addresses agents by id.) - Enforces the 25-agent cap before touching anything. Adding the 26th
agent fails with the same “split into multiple sessions” message
startuses. - Creates the worktree (same naming and idempotent-create behaviour as
start; re-adding an existing worktree reuses it) and attaches the agent through the same boot pipeline a start-time agent uses — the broker boot block, the sidecar spec body (.git-paw/AGENTS.local.md), and the initial prompt are byte-identical. - Re-tiles the agent grid to the layout a
startof that many agents would have produced. Existing panes keep their indices, so any in-flightsend-keystargeting (supervisor sweeps) still lands on the right pane; the new pane gets the next index. The re-tile preserves every other agent’s pane — no existing agent loses its pane — and each agent row is rebalanced to equal width, so the added grid matches a start-time grid of the same count in both pane count and pane widths. After the re-tile, git-paw reconciles the session JSON against the live panes and warns if any agent has no live pane (a JSON↔tmux desync), so it is visible and recoverable rather than silent. - Registers the branch in the session so subsequent
status,stop,purge, andpauseinclude it.
The new agent’s boot block is injected only once its CLI reaches an
interactive ready state (the launch-readiness gate), not after a blind
fixed sleep — the same protection git paw start applies.
The supervisor is not signalled directly. The new agent auto-registers with the broker (filesystem watcher + its own boot-block heartbeat) and the supervisor discovers it on its next sweep — no restart, at most one sweep-interval of latency.
Paused sessions: if the session is paused when you add, the new
pane is created but its prompt is held unsubmitted — the agent stays
paused with the rest of the session and begins on the next
git paw start.
git paw remove — detach a single agent
git paw remove feat/done-thing # close pane, remove worktree, drop from session
git paw remove feat/wip --force # remove even with uncommitted changes
git paw remove feat/keep --keep-worktree # detach pane only; leave worktree + branch on disk
What it does:
-
Locates the agent by branch (errors with the live-agent list if it isn’t in the session).
-
Uncommitted-work safety: refuses to delete a worktree with uncommitted changes, listing the changed files, unless you pass
--force. This mirrorsgit worktree remove’s own safety.--keep-worktreeskips the check entirely (nothing is deleted). -
Kills the agent’s pane and re-tiles the grid for the smaller agent count so it re-flows without leaving a hole. The target pane is resolved by mapping the removed branch’s worktree to a live pane via
pane_current_pathand killed by its tmux pane id — regardless of the process running in it (a bare shell from a failed/never-started CLI, a CLI, or anything else). Killing by resolved pane id (not by a position computed from the session JSON) guarantees only the removed agent’s pane is closed and never a different agent’s pane, even if a stale orphan pane has shifted the grid. The re-tile preserves every other agent’s pane and rebalances each row to equal width. The branch→pane mapping for the survivors is re-derived frompane_current_pathon the supervisor’s next sweep, so a mid-grid removal is safe. -
Removes the worktree (reusing
git paw purge’s per-worktree teardown) unless--keep-worktree, then drops the branch from the session. -
Branch safety: deletes the branch only when git-paw created it and its tip is an ancestor of the default branch or of the branch it was created from, printing
Deleted branch <name> (was <sha>). A branch that existed before the session is never deleted. A created branch carrying commits in neither — or one whose merge state cannot be determined — is kept after its worktree is gone, and is reported with its tip and how to pick the work back up or throw it away:Kept branch feat/wip at 9f8e7d6 (3 commit(s) not in main) resume it with: git worktree add '/path/to/worktree' feat/wip discard it with: git branch -D feat/wipThe discard command is
git branch -dwhen git would accept the safe delete,-Dwhen only the forced form would work.--forcerelaxes the uncommitted-work check only; it never deletes a branch with unmerged commits.
git paw remove supervisor is refused — to end the whole session use
git paw stop (or git paw purge). The supervisor notices a removed
agent passively: its heartbeat stops, and the supervisor drops it from
its coordination scope on the next /status poll.
Scope:
add/removeoperate on every live session — bare, spec-driven and supervisor-mode alike. Every session git-paw builds uses the agent grid, which is the layout that can be re-tiled incrementally for a new agent count, so dynamic branch attachment no longer depends on the session’s mode. One branch per invocation; bulk add/remove and configurable layouts are tracked for a later release.One exception — a session started by an earlier git-paw. Bare sessions used to be laid out with tmux’s whole-window tiled layout, which cannot be re-tiled while the session runs (re-tiling it would move every running pane under you).
addandremoverefuse such a session before touching anything, with an error naming the tiled layout. The fix is one restart:git paw stop # or detach and let the session go stale git paw start # rebuilds the window in the agent grid layoutRecovery rebuilds the window as a grid and records it, so after that restart
addandremovework on the session normally. Sessions started by this version are already grids and are never refused.
Worktree Placement
Every agent git-paw launches works in its own git worktree — an
independent checkout of a branch that shares the repository’s object store.
worktree_placement controls where git-paw creates that directory.
Child vs sibling
There are two layouts:
| Placement | Worktree path | When it applies |
|---|---|---|
child | <repo>/.git-paw/worktrees/<branch-slug>/ | New repos (git paw init writes it) |
sibling | ../<project>-<branch-slug>/ | The default when worktree_placement is absent |
Sibling is the original (v0.7.0) layout: worktrees are created in the
repository’s parent directory, beside the repo. Three agents on
my-project produce ../my-project-feat-a/, ../my-project-feat-b/, and
../my-project-fix-c/ — scattered across the parent folder.
Child keeps worktrees inside the repository, under
.git-paw/worktrees/. The same three agents produce
.git-paw/worktrees/feat-a/, .git-paw/worktrees/feat-b/, and
.git-paw/worktrees/fix-c/. This matches how tools like Claude Code keep
worktrees within the project tree.
The child-layout slug comes from the branch name alone — / becomes - and
characters outside [A-Za-z0-9._-] are stripped — so feat/auth-flow →
.git-paw/worktrees/feat-auth-flow/ and fix/issue#42 →
.git-paw/worktrees/fix-issue42/. The project name is not prepended because
the directory already lives under that project’s .git-paw/worktrees/.
Why the contained layout
The motivation for child is a project-scoped permission model. An agent
CLI (or an MCP client, or a sandbox profile) scoped to the repository can
read and write .git-paw/worktrees/ with a single grant. A sibling worktree
lives outside the repository the tool is scoped to, so it either falls
outside the grant or forces a broader, less precise one. Containing the
worktrees lets one grant for .git-paw/worktrees/ cover every agent.
Because child worktrees live inside the repo tree, they must be ignored so
git never tries to stage them. git paw init adds .git-paw/worktrees/ to
.gitignore for you. If you switch an existing repo to child by editing
the config manually, add that entry yourself. Git itself handles the nested
.git correctly — a linked worktree carries its own .git file pointing
back at the main repository.
Placement decides what an agent CLI can find
The same containment affects what an agent’s CLI can read, not just what a
grant can cover. A worktree is populated from a commit, so anything the
repository gitignores — an agent CLI’s command and skill definitions at the
repository root, .claude/ among them — is simply absent from it. A sibling
worktree lives outside the repository, so those uncommitted definitions are
invisible: an agent told to run a workflow command finds nothing and quietly
improvises one instead. A child worktree sits under the repository root, so
the CLI’s upward search through ancestor directories still reaches them.
Committing the definitions fixes it under either layout. git paw doctor
reports which applies: its spec-workflow check
warns when the command a spec-driven launch instructs
cannot be reached from where your worktrees are placed.
Existing sessions are never moved
Placement only governs new worktree creation. git-paw records the
concrete, absolute path of each worktree in the session it created, and
resume, status, and purge all operate on that recorded path — they never
re-derive it from the current worktree_placement. The consequences:
- A session created under
siblingkeeps resuming and purging from its sibling path even if you later flip the config tochild. - A session created under
childkeeps using its.git-paw/worktrees/...path. - Flipping the config mid-project leaves a mixed layout (old worktrees stay put, new ones follow the new setting). Each worktree is still torn down correctly because the recorded path is the single source of truth at teardown time. git-paw does not migrate existing worktrees.
This makes the upgrade to v0.8.0 a no-op for existing repositories: with no
worktree_placement field, the effective placement is sibling, byte-for-byte
identical to v0.7.0. Re-running git paw init backfills the field — see
below.
Setting it
Set it per-repo in .git-paw/config.toml or globally in
~/.config/git-paw/config.toml (repo wins on conflict):
worktree_placement = "child" # or "sibling"
See the Configuration reference for the full field description and merging rules.
git paw init backfills the field
Re-running git paw init in an already-initialised repo backfills every missing
top-level default key, worktree_placement = "child" among them. If the repo
already has worktrees under the sibling layout, init prints a warning naming
them, states that it did not move them, and tells you how to keep the old
layout. Nothing is moved, deleted, or re-registered — the choice is yours:
- Set
worktree_placement = "sibling"to pin the previous layout. Init never touches an explicitly-set value, so the pin holds on every later run. - Or leave the backfilled
"child": existing worktrees keep resuming from their recorded sibling paths, and only new ones land under.git-paw/worktrees/.
Runtime Isolation
git-paw isolates each agent’s source: every agent gets its own git worktree, so nobody stashes, nobody switches branches, and nobody overwrites a peer’s edits.
It does not, by default, isolate each agent’s runtime. Sibling worktrees
read the same .env and bind the same ports, so the moment two agents start a
dev server, a preview, or a debugger, the second one fails with
EADDRINUSE — or worse, quietly talks to the first one’s database.
Runtime isolation fixes the three collisions that matter: the shared env file,
the shared port, and — through a hook you write — the shared database. All are
opt-in, configured in .git-paw/config.toml:
[worktree.env]
copy = [".env", ".env.development"]
[worktree.ports]
base = 3000
stride = 10
vars = ["PORT", "VITE_PORT"]
[worktree.hooks]
on_create = "scripts/paw-db-branch.sh {worktree_id}"
on_remove = "scripts/paw-db-drop.sh {worktree_id}"
With no table present, nothing changes — no files are copied, no .env.local
is generated, no command runs, and worktrees are created exactly as they were
before. git-paw assumes no filename, no port, and no stack; you declare what
your project actually needs.
Env files are copied, not linked
Each entry in [worktree.env] copy is copied verbatim from the repository root
into the new worktree, after the worktree is created and before the
agent’s CLI process starts. The agent’s very first read already sees a
provisioned checkout.
The copy is independent — never a symlink. That is the whole point: a symlinked
.env is shared mutable state across every agent, which is exactly the silent
runtime corruption worktrees exist to prevent. One agent flipping
FEATURE_X=true to debug its branch would flip it for everyone. A copy diverges
cleanly instead, and any drift you actually care about surfaces at merge time.
A declared file that does not exist at the repository root is skipped with a warning and the worktree is still created. Per-branch divergence is normal, and a missing optional env file should not cost you a session.
Each worktree gets its own port block
Each worktree holds a runtime slot. Its port block starts at
base + slot * stride, and the Nth name in vars receives
base + slot * stride + N. With the configuration above:
| Worktree | Slot | PORT | VITE_PORT |
|---|---|---|---|
| first agent | 0 | 3000 | 3001 |
| second agent | 1 | 3010 | 3011 |
| third agent | 2 | 3020 | 3021 |
The assignments land in a delimited managed block in the worktree’s
.env.local, the override layer most stacks load last:
# >>> git-paw (managed) >>>
PORT=3010
VITE_PORT=3011
# <<< git-paw (managed) <<<
Anything you keep in .env.local outside that block survives, and regenerating
the block is idempotent. The copied .env is never touched — offset ports live
only in .env.local, so .env stays a faithful copy of the source you can diff
against.
Keep stride at least as large as the number of vars, with headroom for vars
you add later. A smaller stride would overlap adjacent worktrees’ blocks and
hand two live agents the same port; git-paw clamps the stride up and warns, but
it is better to get it right in config.
Ports are deterministic, not probed
git-paw does not scan the host for free ports. A slot’s block is computed from config and the slot index, nothing else.
The trade is deliberate. Probing would dodge a busy port once, but the block a worktree gets would change from run to run — so a bookmark, a proxy rule, or a teammate’s note about “the API is on 3010” would go stale every restart. Deterministic assignment means a worktree keeps its ports for as long as it exists.
The cost is that an assigned port can still be taken by something unrelated on
your machine. When that happens, change base or stride — you are picking a
range for the whole session, not fighting one process.
Slots survive add/remove churn
The slot is recorded in session state rather than derived from the worktree’s
position in the list. Position would be fragile: remove the middle worktree of
three and everything after it shifts down, so the next git paw add would be
handed a still-running agent’s ports.
Instead, a new agent takes the lowest slot no live agent holds:
$ git paw add feat/one # slot 0 → 3000
$ git paw add feat/two # slot 1 → 3010
$ git paw add feat/three # slot 2 → 3020
$ git paw remove feat/two # slot 1 is freed
$ git paw add feat/four # slot 1 → 3010, reused
Blocks stay bounded instead of climbing forever, and two concurrently active
worktrees are never assigned overlapping blocks. git paw purge releases every
slot along with the session.
Session state written before runtime isolation existed loads with no slot and is assigned one the next time the worktree is provisioned, so upgrading changes nothing until you opt in.
Giving each worktree its own database
Copying an env file and offsetting a port are things git-paw can do for any
project. Branching a database is not — it means a Neon API call on one
stack, createdb on another, a Docker volume on a third. So git-paw does not
ship a driver. It runs your command at the two moments that matter, and lets
that command hand its result back through .env.local.
Two scripts, one for each direction:
# scripts/paw-db-branch.sh — called with the worktree id
#!/bin/sh
set -e
createdb "paw_$1"
psql -q -d "paw_$1" -f db/schema.sql
echo "DATABASE_URL=postgres://localhost/paw_$1"
# scripts/paw-db-drop.sh — called with the same worktree id
#!/bin/sh
dropdb --if-exists "paw_$1"
Wire them up:
[worktree.hooks]
on_create = "scripts/paw-db-branch.sh {worktree_id}"
on_remove = "scripts/paw-db-drop.sh {worktree_id}"
Now git paw start --branches feat/two creates the worktree, copies the env
files, allocates the ports, then runs paw-db-branch.sh feat-two inside that
worktree. The one line the script printed to stdout joins the same managed
block:
# >>> git-paw (managed) >>>
PORT=3010
VITE_PORT=3011
DATABASE_URL=postgres://localhost/paw_feat-two
# <<< git-paw (managed) <<<
The agent’s app reads .env.local and talks to its own database. A migration
one agent runs cannot corrupt another’s fixtures.
git paw remove feat/two (or git paw purge) runs paw-db-drop.sh feat-two
in the worktree before deleting it, so the database goes away with the
branch.
Anything the hook prints that is not a KEY=value line is ignored, so your
script can log progress freely. Only the assignments become environment.
Make teardown idempotent
Note the --if-exists in the drop script. on_remove is best-effort by
design: if it fails, git-paw warns and removes the worktree anyway, because
a failed teardown must never leave you with a stranded checkout. A crash or a
forced purge can skip the hook entirely.
That means your teardown script will sometimes run against a resource that is
already gone, and sometimes not run at all. Write it so a second run is
harmless, and reconcile leftovers out of band (a periodic sweep of paw_*
databases) rather than relying on the hook alone.
By contrast, a failing on_create does stop that worktree — git-paw
reports the hook’s exit status and stderr rather than handing an agent a
half-built runtime.
Print secrets on stdout and diagnostics on stderr. git-paw logs only the names of the variables it merged, so a password inside
DATABASE_URLnever reaches a log line — but a failing hook’s stderr is shown. And since the value lands in a plain.env.local, make sure that file is gitignored.
A hook is an arbitrary command from your config file, run with your privileges — the same trust model as a custom CLI’s
command. git-paw never builds one from anything an agent or the network supplied.
Checking it worked
After starting a session, look inside any agent’s worktree (under
.git-paw/worktrees/ by default — see Worktree Placement):
$ cat .git-paw/worktrees/feat-two/.env.local
# >>> git-paw (managed) >>>
PORT=3010
VITE_PORT=3011
# <<< git-paw (managed) <<<
If .env.local is missing, [worktree.ports] is absent or its vars list is
empty and no hook printed an assignment. If a declared env file is missing from
the worktree, check the warning git paw start printed — the file probably does
not exist at the repository root. If a hook key is missing, check that the
script prints it on stdout in exact KEY=value form.
See also
- Configuration —
the full
[worktree.env]/[worktree.ports]/[worktree.hooks]key reference - Worktree Placement — where the worktrees themselves are created
Security Posture
This is the single, authoritative statement of git-paw’s trust model. Read it before trusting git-paw with autonomous execution. It is deliberately blunt about what the tool does and does not protect.
What git-paw runs
git-paw launches AI coding agents — arbitrary programs — in tmux panes on your machine, each in its own git worktree. Under supervisor/unattended mode (and whenever you point an agent CLI at a skip-permissions or full-auto flag), those agents execute arbitrary code with your user’s privileges: they run your build, your tests, shell commands, and anything else the model decides to run, often without pausing for per-command approval.
The blast radius is your user account. An agent — whether malicious, prompt- injected, or simply mistaken — can by default read and write anything your user can: your whole home directory, your SSH keys, your cloud credentials, your other repositories, and your login keychain. git-paw does not change your OS’s permission model; it automates running code within it.
git-paw decides nothing about your agent’s commands
git-paw makes no safety judgement about a command an agent asks to run, and it approves no permission prompt on its own. It has no safe-command list, no danger list, and no path-based safety rule. There is no command classifier.
Whether a command runs without a prompt is decided entirely by the agent CLI’s own permission system — the permission mode git-paw launches it with, plus whatever allow and deny rules you have configured in that CLI. Anything the CLI decides to ask about is a prompt, and every prompt is answered by a human.
What git-paw does is narrower, and it is the part it can do honestly: it
detects that a pane is sitting on a live permission prompt and gets that prompt
in front of you — on the broker, in the exit summary and in the manual-decision
log — without stalling the rest of the wave. Under
git paw start --unattended every live prompt is escalated this way. Approval
keystrokes reach a pane only when a human explicitly runs
sweep.sh approve <pane>, which answers once with option 1 and refuses an
option 1 that would grant a durable “don’t ask again” permission.
No LLM process runs at the repository root inside a session. A supervised
session holds only the dashboard and the coding agents, each confined to its own
worktree; the orchestrator runs in your own agent session, under the permission
mode you chose for it, and git-paw launches it with no flags. Every git-paw
keystroke sender — sweep.sh approve, and the drive loop’s nudges, stale-input
recovery and buffered-input submits — follows one role-based rule: keystrokes
reach only a pane resolved as a coding agent (by the session’s discovery roster
for the helper, by the recorded layout and the pane’s owning worktree for the
loop). The dashboard and any pane no agent worktree owns are refused, whatever
their index.
Earlier versions did decide. Up to and including v0.16.0, git-paw ran a command classifier that answered prompts it judged safe, often choosing the “Yes, and don’t ask again” option. Your agent CLI persisted those choices as durable permission grants in its own settings file — for Claude Code that is the repository-root
.claude/settings.local.json, which every worktree and every one of your own sessions shares. Retiring the classifier stops new grants being created; it removes none of the existing ones. Review that file (and any user-level equivalent) and delete grants you did not intend. git-paw never edits or deletes it.
What the controls do — and do not — guarantee
git-paw’s controls are the ones below. None is a complete sandbox on its own; each narrows one specific risk. Do not read any of them as “the agent is now safe.”
| Control | What it does | What it does not do |
|---|---|---|
| The agent CLI’s permission system | The [supervisor] agent_approval level maps to each coding agent’s own launch flags (manual, auto, full-auto), and the CLI then applies its own allow and deny rules to decide what runs without asking. This is the only thing deciding whether a command needs approval. | It is the CLI’s guarantee, not git-paw’s, and it is only as narrow as the rules you wrote: an allow rule for a command prefix permits everything that prefix permits. A full-auto level skips the prompts entirely, so no rule is consulted and nothing is escalated. Path-scope your rules; never grant curl * / cd *. |
| FS-scoped sandbox (optional, you configure it) | When you wrap an agent CLI in sandbox-exec/bwrap, the kernel confines the agent’s writes to its worktree — rm -rf ~, dotfile corruption, and planted git hooks are blocked at the syscall level. This is the only control here that is kernel-enforced. | It protects the machine around the work area, not the work area (the worktree, including uncommitted work, stays destroyable by design). The default profile does not restrict reads, so it protects write-integrity and not the confidentiality of host secrets unless you add read-hardening. It does not cover network, resource-exhaustion, or exposed-socket vectors unless you add rules. See Sandbox. |
| Broker Host/Origin guard | Refuses browser-shaped requests (a non-loopback or null Origin) and, on a loopback bind, requests whose Host is not loopback — closing the DNS-rebinding path into the unauthenticated coordination bus. See Browser exposure. | It is not authentication: any local process can still reach the broker, read the whole log, and publish any message. A non-loopback [broker] bind disables the Host half entirely. |
| Remote control (documented workflow) | Lets you drive a session from elsewhere by remote-controlling your own driving agent session through its CLI’s native remote control. | Exposing a session to remote control extends the blast radius to whoever reaches that channel; secure the channel yourself. |
Two things that used to appear in this table are gone, and should not be
relied on: git-paw’s auto-approve classifier and its command allowlist
([supervisor.auto_approve], [supervisor.common_dev_allowlist]) are retired —
both tables still parse so old configs load, but they have no effect and warn.
The protected-paths and .git/-write rules existed only to stop that
classifier approving such a write; with nothing approving automatically, those
prompts reach you like any other. To enforce those boundaries mechanically, use
deny rules in the agent CLI’s own settings, or the sandbox.
Writing into an agent CLI’s settings
git-paw writes a permission rule into an agent CLI’s own settings in exactly
one circumstance: you ran git paw setup-cli. Running it is
the consent. No other command, process or interface does it — not start,
add, init, session recovery, the drive loop, the dashboard, or any tool
exposed over MCP.
What it may write. For claude, one file:
<main checkout>/.claude/settings.local.json, plus one anchored line in the
repository’s shared info/exclude when the target is not already ignored.
Never a file inside a worktree, never a user-level file, never the configured
[clis.<name>].settings_path.
The grant. Twelve allow entries naming git-paw’s own helper scripts by
relative path and fixed subcommand, from a compiled table. Nothing is derived
from a command argument, your configuration or your repository content. No
entry names curl, an interpreter, an absolute path or a directory wildcard.
The keystroke-sending and verdict-publishing subcommands are deliberately
excluded, and sweep.sh approve is pinned to an ask rule so it always
reaches a human. The twelfth entry, sweep.sh events, is a pure read of the
broker’s log and status — no keystroke, no verdict. See the full table in
Per-CLI Setup.
An applied grant is never rewritten for you. When a release adds an entry
to the table, a file applied under an earlier version keeps the entries that
version wrote; git-paw changes nothing behind your back. git paw doctor
reports the applied setup as out of date, and the remedy is explicit and
yours: git paw setup-cli claude --undo, then
git paw setup-cli claude --allowlist. Until then the new subcommand prompts
like any ungranted command.
The tamper threat, and the mitigation. A granted helper runs without a
prompt, and it lives inside the agent’s own worktree. In an edit-accepting
mode the CLI would apply an agent’s edit to that script without asking, after
which the grant runs whatever the agent wrote — arbitrary code, unprompted. A
quieter variant: the helpers read .git-paw/config.toml, so an edited broker
address turns a granted publish into exfiltration.
So the allowlist item never writes the grants alone. In the same file and the
same replacement it writes Edit(**/.git-paw/scripts/**) to deny and
Edit(**/.git-paw/config.toml) to ask. Both were confirmed against the
installed CLI before the item shipped — including that the deny rule also
blocks the filesystem commands the edit-accepting mode auto-approves
(sed -i, cp, mv, rm, touch, and cd-prefixed forms), and that it
covers both the Edit and the Write tool, at any depth, from a worker’s
directory and from a root session reaching into a worker’s copy.
The gate. An item is written only when its facts have been verified
against an installed copy of that CLI. An unverified item can be planned with
--dry-run but never written. The claude allowlist item ships verified
against Claude Code 2.1.220 (2026-10-15); git paw doctor reports the
current status.
Residual risks. These remain even with the item applied:
- An agent CLI other than the one with a fact row has no row, so nothing is written for it — and nothing is pre-approved.
- A separately installed MCP server or other tool that writes files outside the CLI’s permission system is not governed by these rules.
- In
bypassPermissions/full-auto, the grant adds no privilege because nothing prompts — but the protection rules buy nothing either. Do not pair the allowlist with those modes outside the sandbox. - In the CLI’s own
automode, its classifier decides edits. broker.sh --agent <other>can publish under another agent’s id. That is pre-existing; broker-side identity is a later release. Verdict publishing is deliberately not granted.- A write by the CLI itself (a “don’t ask again” approval) landing between git-paw’s re-read and its rename would be lost. git-paw detects the change and stops rather than clobbering it, but prefer to run setup when no session is waiting on an approval.
A grant is not a safety judgement. git-paw decides nothing about your
prompts. Whether a call runs without one is decided entirely by the agent
CLI’s own permission system and the rules in its settings. git paw doctor
additionally lists the broad allow rules already in that file, so you can see
what earlier “don’t ask again” answers left behind — it reports them and never
edits them.
Self-approval residue
One shape of leftover grant deserves naming on its own, because it undoes the single most important distinction the grant table draws.
The allowlist item grants sweep.sh per subcommand, and withholds
approve, verified, feedback-gate and conflict-ack precisely because
the target settings file reaches every session in the repository — a worker’s
as much as the orchestrator’s. A durable allow rule naming one of those
subcommands, or a whole-script Bash(.git-paw/scripts/sweep.sh *) wildcard
that covers them all, hands every agent session exactly what was withheld: it
can press the approval keystroke in another pane, or publish a supervisor
verdict, without a prompt. An agent can then approve its own permission
prompts and sign off its own work.
Such a rule is not something git-paw writes. It arrives from a “don’t ask
again” answer — including ones the retired classifier accepted on your behalf
up to v0.16.0 — or from a broad rule added by hand. git paw doctor now
reports it, in the project CLI grant residue check, alongside a blanket
curl grant and the inert allowed_bash_prefixes seed earlier versions
wrote. See Doctor.
Removing it is manual by design. git-paw never rewrites or deletes a grant it did not write: it cannot prove the rule is not one you meant to add, and a tool that edits your permission rules on its own is the thing this whole section exists to rule out. Doctor names the file and the entry; the deletion is yours.
Hook bypass and tmux driving
Two things an agent session can do with ordinary commands step around git-paw’s own safeguards, and both have been seen in practice:
- Bypassing git-paw’s git hooks. git-paw’s hooks guard each agent’s branch
and report its commits. A per-invocation override such as
git -c core.hooksPath=…, orgit commit --no-verify(-n), silently skips them — the branch guard and the commit report go with it. - Driving tmux.
tmux send-keys,tmux run-shellandtmux pipe-panereach into the session’s panes from outside the agent’s CLI. Keystrokes sent that way can answer a permission prompt — the agent’s own included — and so bypass the human and the orchestrator entirely.
The bundled coordination skill tells every agent plainly not to do either. An
instruction is not a control, though, and git-paw adds no permission rule for
these commands: it pre-approves only its own helper workflow (setup-cli
writes rules naming git-paw’s helper scripts and nothing else), and every other
permission decision belongs to your agent CLI and to you. That is the same
line the v0.17.0 classifier retirement drew.
If you want these forms to prompt — or to be refused outright — add your own
ask or deny rules to your agent CLI’s settings, for example ask rules for
git -c …, … --no-verify and the tmux verbs above. git-paw never writes
them, never edits them, and never removes them. Like any rule matched on a
command’s text, they are a speed bump against the natural forms rather than a
boundary; the sandbox is the boundary.
Browser exposure — the broker’s Host and Origin guard
The coordination broker is an HTTP server on your loopback interface
(127.0.0.1:9119 by default) and it authenticates nothing: any client that
reaches the port can read GET /log — the whole session’s coordination traffic
— and POST /publish any message, including an agent.answer or
agent.feedback that coding agents read as authoritative guidance. Whatever
those agents are allowed to run without asking — under a permissive CLI
permission mode, or full-auto — a request that reaches the broker is a
prompt-injection channel into a shell.
A web page you open in a browser can reach a loopback port through
DNS rebinding: the attacker’s hostname re-resolves to 127.0.0.1, so the page
becomes “same-origin” with http://<attacker-host>:9119 and its requests go to
your broker. To block that, the broker checks two headers on every request,
before routing — so the check covers every endpoint, including the 404 and
405 fallbacks and any endpoint added in a later release:
| Check | When it applies | What it accepts |
|---|---|---|
Host | Only when [broker] bind is a loopback address (the default). | localhost (case-insensitive), any IPv4 literal in 127.0.0.0/8, or [::1]. The port is not checked, so ssh -L-style tunnels and local port-forwards keep working. A missing Host is refused. |
Origin | Whenever the header is present, on any bind. | An origin whose host passes the Host rule. The serialized opaque origin null (sandboxed iframes, file:// pages) is refused. A request with no Origin, which is what every non-browser client sends, is unaffected. |
A refused request gets 403 with {"error":"forbidden host"} or
{"error":"forbidden origin"}, is not routed, and changes no broker state.
No git-paw client is affected. The bundled broker.sh and sweep.sh
helpers, the post-commit git hook, the Rust HTTP clients used by the CLI, the
MCP server and the dashboard all address the broker by its configured
http://<bind>:<port> URL and send no Origin.
The non-loopback exception
If you set [broker] bind to 0.0.0.0 or a LAN address, git-paw cannot
enumerate the names your network uses for the machine, so the Host check is
disabled. Two things follow, and the second is easy to get wrong.
First, the broker is reachable from your network by any host, with no authentication at all — a far larger exposure than the one this guard addresses.
Second, a non-loopback bind does not keep browsers out. The Origin check
still blocks browser writes and cross-origin reads: browsers attach
Origin to every POST, and to any cross-origin GET made in CORS mode
(fetch defaults to it, as does XMLHttpRequest). A cross-origin GET made
in no-cors mode sends no Origin, but the browser hands the page an opaque
response it cannot read, so that is not a way out either.
A DNS-rebinding page, though, is same-origin with the broker once the
attacker’s hostname re-resolves to your machine. A same-origin GET carries
no Origin header at all, and its response is fully readable — so with the
Host check off, fetch('/log') from that page is served and the page reads
it. The same goes for /status and /messages/*. Only a loopback bind stops
a rebinding page from reading the coordination log.
Every command that brings the broker up prints a warning on stderr naming the
bind address: git paw start (including --from-specs), the supervisor
launch, a recovery of a stopped session, and a resume from pause. The broker
process prints it too, but into the dashboard pane, which switches to the
alternate screen moments later. Do not expose the broker this way on a network
you do not control.
This is not authentication
The guard distinguishes browsers and foreign host names from local processes. It does not authenticate anybody. Any process running on your machine can still reach the broker, publish any message, and read the whole log — including an agent that has gone off the rails, and any other software running as your user.
That includes pages: any page served from a loopback origin is accepted —
http://localhost:3000, or any other port — so a local development server you
are running, or a cross-site-scripting flaw in a page it serves, can reach the
broker from the browser on both a loopback and a non-loopback bind. The guard
excludes foreign origins, not all browser code.
If you need the broker to be unreachable by local processes, that is a containment problem; see the FS-scoped sandbox, and do not rely on this guard for it.
Migration: reverse proxies
A reverse proxy that forwards the original external Host to the broker gets
403. Configure it to rewrite Host to a loopback name (for nginx,
proxy_set_header Host 127.0.0.1:9119;), or have clients address the broker
directly. Tunnels and port-forwards need no change — they already present a
loopback Host.
Broker verdict authority
Two broker messages carry the root session’s authority: agent.verified, which
authorises a branch to merge, and agent.feedback whose from is the root
identity supervisor, which the drive loop’s correction loop treats as a gate
verdict. The broker accepts these only with the per-session authority
credential that only the root session holds, sent in the X-Paw-Authority
request header. Without it — or with a wrong value — POST /publish answers
403 {"error":"authority required"} and changes no broker state. An
agent.status or agent.artifact that claims the verified status for a worker
is treated as a verdict too, since the roster would otherwise mark the branch
verified from it. The comparison
is constant-time, and the message body (the wire envelope) is unchanged: the
credential travels only in the header.
Where the credential lives. A broker-enabled git paw start mints 32 bytes
of OS randomness (64 hex characters) per session and stores them in the
user-data session file — under the platform data directory, outside the
repository and so outside every worktree — which git-paw writes owner-only
(0600). The launcher hands the same value to the broker host through a
one-shot 0600 file next to it, which the host removes once read. It is never
on a command line, in the environment, in a pane, or in a log, and it never
appears in the per-repo discovery file, GET /status, GET /log,
git paw status --json, MCP get_session_status, or the broker log. A session
saved by an earlier git-paw gains a credential the first time recovery or a
restart from pause starts its broker host.
Who carries it. Only root-side publishers:
- the unattended drive loop, for its own branch-refresh notification;
- the
sweep.shverdict verbsverified,feedback-gateandconflict-ack, which fetch it through the hiddengit paw __authoritysubcommand — which refuses to run from a linked worktree.
A root session’s MCP server (one started in the main working tree) offers
publish_verified and publish_feedback, which carry the root credential read
from the same session file. A worktree session’s MCP server has no verdict tool.
A headless orchestrator one-shot is not
handed the credential: it publishes a verdict only through those root-side
paths — the sweep.sh verbs or a root session’s MCP verdict tools — like any
root-side helper.
The worker-side broker.sh has no credential-bearing verb.
Threat model. The rule stops a mistaken or skill-misled worker — one
following a stale example, or an LLM that decides its own work is verified —
and a rogue worker sandboxed with home-directory reads denied: the
sandbox’s Tier 2 (deny $HOME), or a Tier 1 blocklist that
covers git-paw’s data directory (~/Library/Application Support/git-paw on
macOS, $XDG_DATA_HOME/git-paw — by default ~/.local/share/git-paw — on
Linux). The default sandbox profile confines writes only, so it does not hide
the credential. A worker with home-directory read access is out of this rule’s
threat model, whether it runs unsandboxed or under the default profile: it can
read the credential, and keeping it out is the job of the sandbox’s
read-confinement tiers.
What stays ungated. Every non-verdict message is accepted without the
credential, as before — including agent.status, even one that claims the
supervisor identity, which can wind down the drive loop’s wave early.
Feedback from any other sender (the post-commit branch-guard hook, a person) is
not a verdict and stays ungated, and so does one agent publishing a non-verdict
message as another. Messages the broker’s own subsystems publish in-process
(the conflict detector, the role guard) never cross HTTP and are not gated.
The headless orchestrator one-shot
Under --unattended, the drive loop can
hand a judgment call —
an ambiguous question, a merge decision, a correction-exhausted branch — to
the orchestrator CLI, run once and non-interactively at the repository
root. Its permission level is [supervisor] approval (falling back to
agent_approval), and nobody is present to answer its own prompts.
full-autohere is an unattended agent with root-level write access for up to[supervisor.escalation] timeout_seconds(default 30 minutes). It is the power the old orchestrator pane had, without its interactive surface: bounded by the timeout (which kills the whole process group), serialized to one invocation at a time, and logged to.git-paw/logs/<session>/orchestrator-escalations.log, with each outcome published on the broker. Run it inside the sandbox.- Under
full-autotheaskrule onsweep.sh approvedoes not apply.git paw setup-cliputssweep.sh approveonaskso a human sees every approval; a skip-permissions turn consults no rule. The one-shot’s framing, which forbids runningsweep.sh approveor answering any permission prompt, is then the only guard — an instruction, not a control. Permission prompts never enter its context: the loop never hands one off, so the one-shot has no prompt in front of it to be talked into approving. - Under
autoit cannot sign off work — with the built-in mapping. Forclaude,autoresolves to--permission-mode acceptEdits; with thesetup-cligrants the verdict verbs (sweep.sh verified,feedback-gate,conflict-ack) are never granted, so a non-interactive turn is denied them and a merge decision stays on the broker for you. This does not hold for a custom CLI or wrapper that pins its own permission mode: its one-shot can do whatever that mode allows. - Prompt injection from worker-written text is the residual risk. A
question, artifact status or branch detail is text a coding agent wrote, and
the context quotes it. It is fenced between markers as data, and the
framing forbids acting on instructions inside it or verifying, merging or
publishing a verdict because it asks — but that is an instruction to a
model, not a control. Under
full-auto, or a CLI wrapper with a permissive mode, a one-shot runs at the repository root wheregit paw __authorityhands it the credential, so injected text could steer it into a self-verify and a merge. Runfull-autoinside the sandbox. - One pending question per agent. A worker publishing many distinct questions queues one one-shot at a time; the rest stay on the broker and are counted in the exit summary, so it cannot buy an LLM run per question.
- No secret reaches it. The escalation context goes on stdin, never on
argv (nothing in
ps), and the environment adds onlyGIT_PAW_BROKER_URL; the authority credential is never on its command line or in its environment. - Interrupting git-paw does not stop it. The one-shot runs in its own process group, so Ctrl-C or a closed terminal reaches git-paw but not the one-shot, and its timeout is enforced by the drive loop, which is then gone. The escalation log records each one-shot’s pid, which is also its process group id; kill that group by hand. A session torn down under a running loop kills it.
- Its background processes outlive a normal exit. The whole group is killed on timeout or teardown, but when the one-shot exits on its own, any process it left running in the background keeps running.
Set [supervisor.escalation] enabled = false to keep every judgment call on
the broker for a human.
MCP repository writes
The MCP server’s spec-authoring tools (create_spec, write_spec_artifact) are its only tools that write file content into the repository. MCP input is untrusted, because the orchestrator calling them may have been prompt-injected by content it read. These are the guarantees:
- Off by default, and only the repository can consent. The tools exist only when the repository’s own
.git-paw/config.tomlsets[mcp] allow_repo_writes = true. The user-level config, environment variables and tool arguments cannot enable them, and no tool has a parameter that grants consent. The server reads the switch once at startup and logs whether writes are enabled. - Root session only. A server in an agent’s worktree never lists or runs them, whatever that worktree’s config says.
- Confined to the change directory. Lexical checks refuse an absolute
path, a..or.gitcomponent, and achangethat is not one kebab-case segment. Canonical checks refuse a spec directory outside the repository, inside.git, at the repository root or inside.git-paw, a symlinked directory leading out of the change directory, and a symlinked target file. The spec directory comes from the repository’s own[specs]section only, never the user-level config. So writes land only inside change folders under the repository-configured spec directory, and never in.gitor.git-paw. Where that directory points is the repository’s choice: set to.githuborsrc, its “change folders” are CI or source directories. Content is written to a temporary file and renamed into place. - No process, no commit. No tool spawns a process (
validate_specchecks in-process), and nothing is staged or committed.git status/git diffand one server-log line per write record what was authored.
The limit. The switch is a file in the repository, so anything that can edit the root’s .git-paw/config.toml can turn it on. That includes an unsandboxed worker writing outside its worktree, or a merged branch that carries the key. The change shows up in the diff a human reviews and in the server’s startup log line, and the sandbox confines a worker’s writes to its worktree. Against an unsandboxed rogue worker, though, the switch is no defence, as with broker verdict authority. Likewise, a local process that can already write inside the change directory could swap a directory that was just checked for a symbolic link before the write lands. That is out of scope for the same reason: it already has write access.
The bottom line
- Treat every agent as capable of running arbitrary code as you.
- git-paw decides nothing about an agent’s commands and approves no prompt. What runs without asking is your agent CLI’s decision, from the rules you gave it.
- A CLI allow rule reduces friction; it is not containment. A
full-autolevel removes the prompts altogether. - For containment, use the optional FS-scoped sandbox — and understand precisely what it does (writes) and does not (reads, by default) protect.
- If you ran v0.16.0 or earlier, audit your agent CLI’s settings file for
durable “don’t ask again” grants the retired classifier accepted on your
behalf.
git paw doctornames the known-dangerous ones — above all asweep.shrule that lets an agent approve its own prompts — but removing them is yours to do. - Run untrusted or aggressive automation on hardware, accounts, and repositories whose compromise you could tolerate.
git paw doctor prints a pointer to this page and reports whether your OS’s
sandbox backend is available.
Remote Control
You can drive a git-paw session from a phone or another device. The orchestrator is your own agent session, so if that session can be remote-controlled, the whole run can be steered from anywhere. This chapter covers how.
The topology
phone / browser ──(the agent CLI's own remote control)──▶ your driving agent session
(local machine, repo root,
git-paw MCP server attached)
│
▼
git-paw session (tmux: dashboard
at pane 0, coding agents from pane 1)
- The driving session runs on your local machine, at the repository root and outside tmux. git-paw’s MCP server is attached to it, and it has loaded the orchestrator skill (
.git-paw/skills/orchestrator/SKILL.md) and the session context (.git-paw/AGENTS.local.md). - You remote-control that session through the agent CLI’s native remote-control feature. Your phone is only a view into the local session. The session’s files, MCP servers and tools stay on the machine.
- The driving session drives git-paw the way it does when you sit at the keyboard. It uses the bundled helpers (
.git-paw/scripts/sweep.sh,broker.sh) and the MCP tools:wait_for_eventsandget_session_statusto follow the run, and, in the root session,start_session,pause_session,stop_session,purge_session,publish_verifiedandpublish_feedback.
git-paw ships no bridge, tunnel or network transport of its own. The remote channel belongs to the agent CLI vendor, and git-paw opens no port for it. Pair the driving session with git paw start --unattended so the coding agents keep moving while you are away. Each permission prompt the drive loop escalates reaches you through the driving session, and you answer it from the phone.
Which agent CLIs can be the remote-controlled driving session
Last checked: 2026-10-29. These are third-party facts that change quickly; check the vendor’s current documentation before relying on a row.
| Agent CLI | Remote-controlled driving session | Notes |
|---|---|---|
| Claude Code | ✅ claude remote-control, or /remote-control in a running session | Continues the local session from the Claude app or the web. Local MCP servers stay available. Requires a paid plan and /login. |
| GitHub Copilot CLI | ✅ copilot --remote, or /remote on in a running session | Steered from GitHub Mobile or github.com. Whether local MCP tools are usable remotely is to verify; the shell-helper path works regardless. |
| OpenAI Codex | ✅ through the Codex desktop app, steered from the ChatGPT mobile app (preview) | The codex terminal CLI has no remote control of its own; drive git-paw from the desktop app. MCP over the remote channel is to verify. |
| Cursor | ✅ /remote-control in a desktop agent session, steered from the Cursor iOS app (public beta) | Requires a paid plan and a workspace with a Git remote. The agent loop runs in Cursor’s cloud and tool calls run on your machine. MCP over the remote channel is to verify. |
| Qwen Code | ⚠️ to verify | Earlier research (2026-07) found text-only messaging integrations, not a native remote control. |
Antigravity agy | ❌ to verify | No native remote control was found (2026-07). |
| Amp / Aider | ❌ to verify | No native remote control was found (2026-07). |
A CLI without native remote control can still drive git-paw locally. git-paw does not build a bridge to make it remote-controllable.
Caveats
- The local process must stay running. The remote control is a view into a process on your machine. If the machine sleeps, loses its network or the process exits, the remote session stops with it. An outage of roughly ten minutes ends the remote session (Claude Code’s documented timeout; other vendors behave similarly), and you have to restart it locally. Keep the machine awake for long runs.
- Secure the channel yourself. Remote control extends the blast radius to whoever can reach that channel. Treat your vendor account and its sessions as access to your repository. See Security posture.
- Remote approval is still your approval. The orchestrator relays every escalated permission prompt to you, and runs
sweep.sh approveonly when you say so, whether you are at the keyboard or on your phone.
See also
- Quick Start: Orchestrator Mode: init, start, load the skill, follow, verify, merge.
- MCP Server: the read and write tools the driving session uses.
- Supervisor → Driving the session from your own agent.
Per-CLI Setup
git paw setup-cli is the one command that writes into an agent CLI’s own
settings file. Running it is the consent.
Agents call git-paw’s bundled helper scripts constantly — to publish status,
poll for messages, capture panes. Under most agent CLIs every one of those
calls raises a permission prompt. setup-cli lets you pre-approve exactly
those calls, in the CLI’s own allow rules, once.
Setup is consent-only
Nothing else in git-paw writes an agent CLI’s settings. Not start, not
add, not init, not session recovery, not the drive loop, not the
dashboard, and no tool exposed over MCP. Earlier versions of git-paw tried to
seed permissions automatically; that was removed, and it is not coming back
under a different name.
Everything the command does is therefore gated:
- it prints its plan before anything is written;
- it writes nothing without
--yesor your confirmation on a terminal; - it records every entry, so it can take back exactly what it added;
- it refuses to run from a linked worktree, so a coding agent in a worktree cannot run it by accident.
What it writes, and where
For claude, the allowlist item targets exactly one file:
<main checkout>/.claude/settings.local.json
That one file is enough. Claude Code reads the settings file at the
repository root even when the session starts in a subdirectory or in a linked
worktree — which is what the change’s fact gate confirmed against the
installed CLI. So a single write covers your root session, every agent
worktree nested under .git-paw/worktrees/, and every session you start
later, including worktrees that do not exist yet.
git-paw never writes a settings file inside a worktree (that would put the
file inside the agent’s own reach), never writes a user-level settings file,
and never writes the path you configured as [clis.<name>].settings_path.
If the file is not already ignored by git, the plan also adds one anchored
line to the repository’s shared info/exclude, so a file git-paw created does
not show up in your git status.
The grant table
The allow entries are a fixed table. Nothing is derived from a command argument, from your configuration, or from anything in your repository — two differently configured repositories plan byte-identical entries.
| Helper | Granted | Deliberately not granted |
|---|---|---|
broker.sh | every subcommand | — |
docs-fetch.sh | find only | get |
sweep.sh | snapshot, capture, status, worktrees-status, inbox, detect-stuck, stuck-eval, status-publish, learn, events | approve, verified, feedback-gate, conflict-ack, answer, advanced-main, classify |
_paw_common.sh | — (it is sourced, never invoked) | — |
That is twelve allow entries in total.
Each entry names the helper by its relative invocation path, like
Bash(.git-paw/scripts/broker.sh *). No entry names an interpreter, an
absolute path, a directory wildcard or curl.
sweep.sh events is granted — as
Bash(.git-paw/scripts/sweep.sh events *) — because it only reads the
broker’s log and status to serve the
event long poll. It sends no
keystroke and publishes no verdict.
What is withheld, and why:
docs-fetch.sh gettakes an absolutehttp(s)://orfile://URL. It is a read-anywhere and exfiltration primitive;findonly fetches the one configured index.sweep.sh approvepresses keys in another pane. It also gets an explicit ask rule, so it prompts even if some broader rule of your own would otherwise allow it.sweep.sh verifiedandfeedback-gatepublish supervisor verdicts, andconflict-ackwrites feedback into other agents’ inboxes. The target file reaches worker sessions as well as the supervisor’s, so granting these would let a worker forge its own verification or tell a peer a conflict was cleared.sweep.sh answerandadvanced-mainpublish with supervisor authority: an answer is authoritative guidance to another agent, and a main-advance event makes the drive loop rebase worker branches. Granting either would let a worker forge one without a prompt.sweep.sh classifyis on its way out with the command classifier. It is listed as withheld so the guard that requires every subcommand to be explicitly granted or explicitly withheld keeps passing while it still exists.
The protection rules
A granted helper runs without a prompt, and it lives inside the agent’s own worktree. Without protection, an agent could edit the script and have the grant run whatever it wrote. So the allowlist item never writes the grants alone — it writes these in the same file and the same replacement:
| Rule | Where | Why |
|---|---|---|
Edit(**/.git-paw/scripts/**) | deny | refuses the CLI’s file-editing tools on any helper directory, at any depth |
Edit(**/.git-paw/config.toml) | ask | the helpers read this file (the broker address), so a change to it always prompts |
The fact gate confirmed both against the installed CLI, including that the
deny rule also blocks the filesystem commands acceptEdits auto-approves
(sed -i, cp, mv, rm, touch, and cd-prefixed forms). git paw doctor additionally warns when a granted helper no longer matches the copy
the binary bundles.
Using it
# See exactly what would be written. Writes nothing.
git paw setup-cli claude --allowlist --dry-run
# Apply it, with a confirmation on a terminal.
git paw setup-cli claude --allowlist
# Apply it without asking (scripts, CI).
git paw setup-cli claude --allowlist --yes
# Take back exactly what git-paw added.
git paw setup-cli claude --undo --yes
Run it from the repository’s main checkout. At least one of
--allowlist, --trust, --hooks or --undo is required.
--trust and --hooks exist so the flag surface will not change when later
releases add those facts. Today no CLI declares them, so they report “not
supported” and write nothing.
Re-running after an upgrade
git-paw never rewrites recorded grants itself. When a release adds an
entry to the grant table — sweep.sh events is the v0.18 example — a file you
applied under an earlier version keeps exactly the entries that version wrote.
Nothing updates it in the background.
git paw doctor notices and reports the applied setup as out of date,
naming how many entries this version would also write. It judges that against
your CLI’s settings file rather than against git-paw’s record, so an entry you
already held yourself counts as applied — see
the per-CLI setup checks. The remedy is to take back what
git-paw wrote and apply the current table:
git paw setup-cli claude --undo
git paw setup-cli claude --allowlist
Until you do, the new subcommand simply prompts the way any ungranted command does — nothing breaks, you just answer a prompt.
Undo
--undo removes an entry only when it is still present, still byte-equal to
what was recorded, and still of a shape the item produces. Anything else — an
entry you edited, an entry someone added to the record by hand — is reported
and dropped from the record, never removed from your file. The protection
rules stay as long as any allow entry still names a path under
.git-paw/scripts/, including one you added yourself.
Undo removes entries, not containers: an allow/ask/deny list that setup
created and undo emptied is left in place, empty. An empty list grants
nothing, and removing it would mean touching something git-paw cannot prove
it created.
A file git-paw created is deleted only when nothing but its own skeleton is left; if you added a key of your own, the file stays.
Upgrading from 0.17.0 or earlier
If you applied the allowlist on an earlier release and git paw doctor told
you the applied allowlist was out of date — straight after applying it, with
an --undo and re-apply remedy that never cleared the warning — that is
fixed. Setup skips a planned entry your settings file already holds and
records only what it wrote, and doctor used to compare its plan against the
record alone, so any entry you already had read as permanently missing. Doctor
now judges the plan against the settings file, so an entry you already had
counts as applied and the undo/re-apply round trip converges. Nothing about
what setup writes, what it records, or what undo removes has changed.
Doctor also reports grant residue now: durable rules left in the
project’s CLI settings by earlier versions or by earlier “don’t ask again”
answers — a blanket curl grant, a sweep.sh rule covering a subcommand the
table above withholds, or the inert allowed_bash_prefixes seed written
before v0.16.0. git-paw does not rewrite a grant it did not write, so those
must be removed by hand. See Doctor.
Verified and plan-only
Each item carries a verification status: whether its facts — the target file, the key paths, the entry forms, and the protection the entries rely on — were confirmed against an installed copy of that CLI.
- Verified: the plan can be applied.
- Plan-only:
--dry-runstill prints the plan, but writing is disabled and the command exits2.--undokeeps working, so entries written by an earlier version can still be removed.
The claude allowlist item ships verified, against Claude Code 2.1.220
(2026-10-15). git paw doctor reports the current status.
Claude Code variants (a wrapper binary registered under another name) are
covered by git paw setup-cli claude: the target settings file belongs to the
repository, not to the binary.
The record
Every entry git-paw writes is recorded at:
<git-common-dir>/git-paw/cli-setup.json
It lives in the git directory, so it can never be committed, never shows up in
any worktree’s git status, and is shared by every worktree of the
repository. It is written owner-only, and an entry is recorded before it is
written — a crash in between leaves a recorded entry that is not on disk,
which undo simply drops.
Residual risks
Read these before applying the item:
- Prefer the FS-scoped sandbox. The allowlist reduces prompts; the sandbox bounds what a process can reach at all. They are complementary, and the sandbox is the stronger control.
- Do not pair the allowlist with
full-autooutside a sandbox. In a mode where nothing prompts, the protection rules buy you nothing, because everything else is unprompted too. - Undo it when you no longer want it. The grant reaches every session in the repository, including ones you start much later.
- Undo before downgrading. Entries already written stay in your file after a downgrade, and an older git-paw does not know how to remove them.
- Run setup when no session is waiting on an approval. The CLI writes to the same file when you answer “don’t ask again”; a write that lands between git-paw’s re-read and its rename would be lost. git-paw detects the change and stops rather than clobbering it.
- A grant is not a judgement about safety. git-paw decides nothing about your prompts. What runs without one is decided entirely by the agent CLI’s own permission system and the rules in its settings.
See also: Security Posture and Doctor.
FS-Scoped Sandbox
An optional, opt-in way to confine an agent to its worktree using your OS’s
kernel sandbox — sandbox-exec (Seatbelt) on macOS, bwrap (bubblewrap) on
Linux/WSL. It bounds the blast radius of a full-auto agent: writes are allowed
inside the worktree and denied outside, enforced by the kernel.
The sandbox is your own local configuration. git-paw does not invoke or
generate it, and does not ship a maintained profile. It launches whatever
[clis.<name>].command names; you point that at a small wrapper that execs the
real CLI under the sandbox. Everything below is a worked example to copy and
adapt to your CLI, OS, and stack — not a drop-in git-paw ships.
The sandbox confines writes. By default it does not restrict reads — see Confidentiality hardening — and it does not cover network or resource-exhaustion. Read the Security Posture first.
How it hooks in
git-paw already gives a wrapper everything it needs at launch: the pane’s working
directory is the worktree, and GIT_PAW_BROKER_URL is in the environment. So a
wrapper derives its scope from $PWD with zero git-paw changes.
The wrapper must be a PATH-resolvable executable. git-paw launches a custom
CLI by its name on PATH — not by an arbitrary script path. Put your wrapper on
PATH (e.g. symlink it next to the real CLI) and set command to that name:
[clis.claude-sandboxed]
command = "claude-sandboxed" # a wrapper on PATH, NOT a path to a script
If you instead point command at a script path that isn’t resolvable by name,
the pane drops to a raw shell and the boot prompt is typed into it.
git paw doctor reports whether sandbox-exec/bwrap is installed.
macOS (sandbox-exec)
A wrapper (claude-sandboxed on your PATH):
#!/bin/sh
set -eu
WT="$PWD"
GITDIR="$(git -C "$WT" rev-parse --git-common-dir)"; GITDIR="$(cd "$GITDIR" && pwd)"
exec sandbox-exec \
-D WORKTREE="$WT" \
-D GITDIR="$GITDIR" \
-D GITHOOKS="$GITDIR/hooks" \
-D GITCONFIG="$GITDIR/config" \
-D TMPDIR="${TMPDIR%/}" \
-D CLIHOME="$HOME/.claude" \
-D CARGOHOME="${CARGO_HOME:-$HOME/.cargo}" \
-f "$HOME/.config/git-paw/sandbox.sb" \
claude "$@"
The profile (sandbox.sb), a (allow default) base that denies writes except a
scoped set:
(version 1)
(allow default) ;; reads + network stay open
(deny file-write*)
(allow file-write*
(subpath (param "WORKTREE")) ;; the worktree
(subpath (param "GITDIR")) ;; shared .git — needed to commit
(subpath (param "TMPDIR"))
(subpath (param "CLIHOME")) ;; the CLI's own config/auth/cache
(subpath (param "CARGOHOME")) ;; toolchain cache (stack-specific)
(regex #"^/private/tmp/claude-") ;; the CLI's per-session temp/cwd
(subpath "/private/tmp") ;; shell heredocs (zsh writes /private/tmp/zsh*)
(literal "/dev/null") (literal "/dev/tty") (regex #"^/dev/ttys[0-9]+$"))
;; Close the git-persistence gap (see below):
(deny file-write*
(subpath (param "GITHOOKS"))
(literal (param "GITCONFIG")))
/private/tmp must be writable, not just $TMPDIR and the CLI’s per-session
prefix: zsh writes heredoc bodies to /private/tmp/zsh* while building the
command it’s about to run, so the standard multi-line commit idiom —
git commit -m "$(cat <<'EOF' … EOF)" — hits Operation not permitted
without this grant. /private/tmp is already world-writable and holds no
secrets, so this is a write-only convenience grant; it does not change the
read-confidentiality tiers below (see
Confidentiality hardening).
What breaks under the sandbox
Some failures aren’t a missing write grant — they’re the kernel refusing to
exec at all, and no profile rule fixes them:
Setuid/setgid binaries cannot exec under sandbox-exec. /bin/ps and
/usr/bin/top are setuid root on macOS. Seatbelt refuses to exec a setuid
binary inside any profile, even the most permissive one:
$ sandbox-exec -p '(version 1)(allow default)' /bin/ps -o pid= -p $$
sandbox-exec: execvp() of '/bin/ps' failed: Operation not permitted
An agent running ps inside a wrapped CLI typically only sees a bare
operation not permitted: ps — the same wording the sandbox uses for a
denied write. Don’t chase it with a write grant: it isn’t one, and no
(allow …) rule fixes it. Use pgrep instead — it works under the same
profile because it isn’t setuid.
The bundled worker-orientation skill already tells agents this, so both
surfaces agree; an agent that hits it should adapt (use pgrep) rather than
probe or widen the profile.
Linux / WSL (bwrap)
#!/bin/sh
set -eu
command -v bwrap >/dev/null || exec claude "$@" # detect-and-degrade
WT="$PWD"
GITDIR="$(git -C "$WT" rev-parse --git-common-dir)"; GITDIR="$(cd "$GITDIR" && pwd)"
exec bwrap \
--ro-bind / / \
--bind "$WT" "$WT" \
--bind "$GITDIR" "$GITDIR" \
--ro-bind "$GITDIR/hooks" "$GITDIR/hooks" \
--ro-bind "$GITDIR/config" "$GITDIR/config" \
--bind "${TMPDIR:-/tmp}" "${TMPDIR:-/tmp}" \
--bind "$HOME/.claude" "$HOME/.claude" \
--bind "${CARGO_HOME:-$HOME/.cargo}" "${CARGO_HOME:-$HOME/.cargo}" \
--dev-bind /dev /dev \
--unshare-user \
--chdir "$WT" \
claude "$@"
bwrap needs unprivileged user namespaces; some hardened kernels disable them —
hence the command -v bwrap degrade line. sandbox-exec is deprecated but
present on every macOS; both are pragmatic, not stable contracts.
The --bind "${TMPDIR:-/tmp}" "${TMPDIR:-/tmp}" line already covers shell
heredocs on Linux — zsh writes heredoc bodies under $TMPDIR (or /tmp when
unset), and both are bound writable, so the git commit -m "$(cat <<'EOF' … EOF)" idiom works here without an extra grant. ps is not setuid on Linux
and works fine here — see What breaks under the
sandbox for why it fails on macOS instead.
bwrap itself sets no_new_privs, so even a setuid binary that did run
inside it would run without its elevated privileges.
The writable set to get right
The profile is only as good as this list. Beyond the worktree, an agent CLI legitimately needs to write:
- The shared
.git— a child worktree commits into the main repo’s.git, so it must be writable except.git/hooksand.git/config(below). - The CLI’s own config/auth/cache (
~/.claude,~/.config/…) and its per-session temp/cwd. Claude Code, for example, uses/private/tmp/claude-<uid>/and/private/tmp/claude-<hash>-cwdsymlinks (random names → a regex, not a subpath) — omit these and the agent’s shell tool dies withoperation not permitted. TMPDIRand device nodes (/dev/null,/dev/tty, the pane pty).- Toolchain caches the workers write to (
~/.cargo,~/.rustup, …) — these are stack-specific; source them from your stack, don’t assume. - Network stays open by default: agents need the loopback broker and outbound model-API egress.
The git-persistence gap
Keep .git writable for commits, but bind .git/hooks and .git/config
read-only. Otherwise a confined agent can plant a .git/hooks/post-commit
script or set core.pager/editor/fsmonitor to an arbitrary command — which
fires later, unsandboxed, with full privileges the next time you run plain
git outside the box. The agent can still commit (index/objects/refs); it just
can’t plant persistence.
Credential refresh (per-OS)
A long-lived agent refreshes its OAuth token mid-session. Where that token lives differs by OS, and it affects the profile:
- macOS: the token is in the login Keychain, and refresh writes it
back — an in-process write the
(deny file-write*)base blocks, so the agent logs in fine at first but hits “Not logged in” when the token expires. To allow refresh, add~/Library/Keychainsto the writable set. Tradeoff: that grants write to your whole login keychain (and reads were already open) — so the sandbox protects filesystem write-integrity, not the confidentiality of keychain secrets. If your CLI can be configured to use file-based credentials (a file under the already-writableCLIHOME), prefer that and deny the Keychain entirely. - Linux: credentials are file-based (headless: a file under
~/.claude*/) or written by a separate Secret Service daemon (desktop). The file lives in the writableCLIHOME, and the daemon writes on the agent’s behalf outside the sandbox — so neither has the macOS refresh problem.
Confidentiality hardening
The base profile confines writes only; the agent can still read anything your user can (SSH keys, cloud creds, other repos). If that matters, add read-denials. Two tiers:
Tier 1 — blocklist (cheap). Keep (allow default), deny reads of known
secret stores:
(deny file-read*
(subpath (param "SSH")) ;; ~/.ssh
(subpath (param "AWS")) ;; ~/.aws
(subpath (param "GNUPG"))) ;; ~/.gnupg
Tier 2 — allowlist (strong). Deny reading your home, then re-allow only what
the agent needs (everything outside $HOME — system libraries — stays readable,
so the app still runs):
(deny file-read* (subpath (param "HOME")))
(allow file-read*
(subpath (param "REPO"))
(subpath (param "CLIHOME"))
(subpath (param "CARGOHOME"))
(subpath (param "KEYCHAINS"))) ;; still needed for macOS auth (see above)
~/.ssh is safe to deny for a git-paw worker: it commits locally (no key
needed unless you sign commits with an SSH key), and pushes/fetches are done by
the unsandboxed supervisor or you — not the worker. Deny it unless your workers
sign with an SSH key or push over SSH themselves, in which case grant the
specific key plus SSH_AUTH_SOCK rather than the whole folder.
The one residual even under Tier 2 is the login Keychain (allow-listed for auth); per-item ACLs do not gate a confined agent in your unlocked session. Close it only via file-based credentials as noted above.
Spec-Driven Launch
The --from-all-specs and --specs flags let you define branches, CLI assignments, and prompts in spec files instead of using interactive selection. git-paw reads spec files from a configured directory, creates worktrees for each pending spec, and launches AI CLIs with the spec content injected into each worktree’s gitignored .git-paw/AGENTS.local.md sidecar (the tracked AGENTS.md is left untouched).
Quick Example
# Initialize repo config (creates .git-paw/config.toml)
git paw init
# Add spec files to your specs/ directory, then launch every discovered spec
git paw start --from-all-specs
# Narrow to specific specs without editing config
git paw start --specs add-auth,fix-session
# Open a multi-select picker (requires an interactive terminal)
git paw start --specs
Picking specs at launch time
You have three ways to control which specs are launched:
--from-all-specs— launches every discovered spec across the configured backend.--specs NAME[,NAME...]— comma-separated list of spec names. Mirrors the existing--branches feat/a,feat/bsyntax. Unknown names exit with the discovered-set listed as candidates so you can correct quickly.--specs(bare, no values) — opens a multi-select picker showing every discovered spec. Each row shows the unit identifier; for Spec Kit features that decompose into multiple worktrees, the row also shows a worktree-count hint (e.g.003-user-list — 3 worktrees: 2 [P] + 1 phase/). The picker is a type-to-filter multi-select (identical to the branch picker): type to narrow the rows by case-insensitive substring match against the displayed label, Ctrl-U clears the filter, Space toggles, and Enter confirms. Selecting a filtered row still expands to every underlying entry for that unit, and selections persist across filter changes.
--from-all-specs and --specs are mutually exclusive — they express opposing intents — and clap rejects any invocation that combines them.
Picker requires a TTY
The bare --specs form requires an interactive terminal. When stdin is not a TTY (CI, scripted invocation, redirected input), git-paw exits with an actionable error pointing at the explicit forms:
error: --specs without values requires an interactive terminal
Use `--specs NAME[,NAME...]` to narrow explicitly, or
`--from-all-specs` to launch every discovered spec.
Name resolution rules
--specs NAME matches against the discovered set using these strategies in order:
- Exact match on the spec id (case-sensitive). Matches OpenSpec change names, Markdown filename stems, or Spec Kit decomposed entry ids like
003-user-list-T009. - Spec Kit feature-name match — the value matches a Spec Kit feature directory prefix (e.g.
003-user-list). All decomposed entries belonging to that feature are launched together. - Spec Kit numeric prefix — a digits-only value (e.g.
003) matches a unique feature directory whose name starts with003followed by a non-digit boundary. Ambiguous prefixes (two features both starting with003) are rejected with the candidate list.
Unknown names error out before any worktrees are created. The error message includes the unresolved name AND the discovered candidate list. There is no partial start.
Spec Formats
git-paw supports four spec formats: OpenSpec (directory-based), Markdown (file-based), Spec Kit (.specify/-based, GitHub Spec Kit), and Superpowers (docs/superpowers/plans/-based, obra/superpowers).
OpenSpec Format (default)
Each pending change lives in its own subdirectory under the specs directory. The subdirectory name becomes the branch identifier.
specs/
add-auth/
tasks.md # Required — main prompt
specs/
jwt/spec.md # Optional — supplementary spec
fix-pagination/
tasks.md
Discovery is by archive status, not task completion. --from-all-specs includes every change subdirectory under specs/ except anything under specs/archive/. A change with all tasks marked - [x] in its tasks.md is still picked up — task-completion is a progress tracker, not a discovery filter. After completing and verifying a change, run:
openspec archive <change-name>
before the next git paw start --from-all-specs invocation. openspec archive moves the change directory under specs/archive/<date>-<change>/ and syncs its delta specs into the main specs at specs/<capability>/spec.md. The next session won’t spawn a worktree for it.
tasks.md contains the prompt content sent to the AI CLI. It supports optional YAML frontmatter:
---
paw_cli: claude
---
## Implement JWT Authentication
Add JWT token support to the auth module.
The paw_cli field overrides the CLI for this specific spec. If omitted, the default resolution chain applies (see CLI Resolution below).
Supplementary spec files in specs/<name>/spec.md are appended to the prompt with section headers. File ownership can be declared with “Files owned:” or “Owned files:” followed by a markdown list.
Markdown Format
Flat .md files in the specs directory. Each file uses YAML frontmatter to control status and branch mapping.
specs/
add-auth.md
fix-pagination.md
design-notes.md # ignored — no paw_status: pending
Example file (specs/add-auth.md):
---
paw_status: pending
paw_branch: add-auth
paw_cli: claude
---
## Implement JWT Authentication
Add JWT token support to the auth module.
Frontmatter Fields
| Field | Required | Description |
|---|---|---|
paw_status | Yes | Must be "pending" to be included. Other values ("done", "in-progress") are ignored. |
paw_branch | No | Branch name suffix. Falls back to filename stem if absent. |
paw_cli | No | CLI override for this spec. |
Only files with paw_status: pending are picked up.
Spec Kit Format
Spec Kit projects place each feature in its own directory under .specify/specs/<feature>/, alongside an optional .specify/memory/constitution.md for project-level rules.
.specify/
memory/
constitution.md # Optional — project rules / governance
specs/
001-room-setup/
spec.md # Feature requirements
plan.md # Implementation plan
tasks.md # Required — task list with phases
checklists/ # Optional — advisory validation criteria
security.md
002-poker-voting/
spec.md
plan.md
tasks.md
Selecting Spec Kit. git-paw does not auto-detect the spec system from the filesystem — you set it explicitly. Either configure [specs] type = "speckit" with dir = ".specify/specs" in .git-paw/config.toml (git paw init prompts you to choose the spec system and writes this section), or pass --specs-format speckit for a single launch.
Selecting a spec system for one launch with --specs-format:
# Force Spec Kit even when [specs] config says otherwise
git paw start --from-all-specs --specs-format speckit
# Force OpenSpec on a project that has a `.specify/` folder
git paw start --from-all-specs --specs-format openspec
# Force Superpowers on a project with docs/superpowers/plans/
git paw start --from-all-specs --specs-format superpowers
tasks.md decomposition. Spec Kit’s tasks.md files use ## Phase N: <Name> headings and - [ ] T<NNN> task lines. git-paw decomposes the current phase (the first phase with any incomplete task) into one worktree per kind:
- Each incomplete
[P]-marked task → its own worktree ontask/<task-id>-<slug>(one agent per task, parallel). - All remaining incomplete non-
[P]tasks → one consolidated worktree onphase/<feature>-<phase-slug>(one agent works through them sequentially, since the absence of[P]indicates shared files or context).
## Phase 2: Foundational
- [ ] T009 [P] Contract test POST /auth/otp/request → task/t009-contract-test-...
- [ ] T010 [P] Contract test POST /auth/otp/verify → task/t010-contract-test-...
- [ ] T011 Setup database schema ┐
- [ ] T012 Create auth tables ├ phase/<feature>-foundational
- [ ] T013 Seed test data ┘
In this example, one feature directory produces three worktrees and three branches. This “one Spec Kit feature → multiple branches” model is intentional — [P] tasks parallelise across worktrees; non-[P] tasks sequence within a single worktree.
Phase advancement. Phases earlier than the current one are assumed complete (all - [x]). Phases later than the current one are deferred — they only produce worktrees on a future scan, after every task in the current phase has been ticked off. Fully completed features (every task in every phase is - [x]) are skipped silently.
Boot prompts. Each Spec Kit worktree’s .git-paw/AGENTS.local.md sidecar contains:
- Feature Context — the full
spec.mdcontent. - Implementation Plan — the full
plan.mdcontent (omitted when absent). - Validation Criteria (advisory) — each
checklists/<file>.mdcontent under its own heading. Checklists are advisory. - Your Task — the single task description (
task/...worktrees) or the ordered task list plus sequential-execution +- [x]writeback +agent.artifact(withstatus: "done") instructions (phase/...worktrees).
Task writeback. Agents working in a phase/... worktree flip - [ ] to - [x] in the worktree’s tasks.md as they complete each task. The writeback may be committed alongside the task’s code change or as a separate commit. Per-line edits across worktrees are merged by git in the normal way. The conflict detector exempts writebacks to the shared tasks.md from ownership-violation detection; concurrent edits to it by two worktrees can still raise in-flight warnings.
Constitution wiring. With [specs] type = "speckit" and no explicit [governance] constitution setting, git-paw probes <specs dir>/../memory/constitution.md (normally .specify/memory/constitution.md) and, when it exists, wires it in as the governance constitution automatically — so a project that already has a Spec Kit constitution gets it configured for free. An explicit [governance] constitution value always wins; see Configuration → Spec Kit constitution auto-wiring.
Superpowers Format
obra/superpowers is a multi-CLI methodology plugin whose writing-plans skill saves implementation plans as flat Markdown files under docs/superpowers/plans/. git-paw scans those plans — one worktree per incomplete plan.
docs/superpowers/plans/
2026-07-20-add-auth.md # one plan = one worktree
2026-07-21-export-csv.md
Unlike Spec Kit, a superpowers plan is not fanned out into per-task worktrees — a plan is a sequential TDD chain meant for a single subagent-driven-development worktree, so each plan file maps to exactly one branch: plan/<plan-file-stem>.
Plan structure. Each plan carries a header (**Goal:**, **Architecture:**, **Tech Stack:**) and one or more ### Task N: <name> sections, each with a **Files:** block (exact paths) and bite-sized - [ ] checkbox steps plus Run: verification commands.
Discovery. A plan is picked up when it has at least one incomplete - [ ] step. A plan whose every step is - [x] is skipped (with a warning); a .md file with no ### Task/step lines — e.g. a design doc under docs/superpowers/specs/ — is skipped silently.
Selecting Superpowers. As with every format, git-paw does not auto-detect it — configure [specs] type = "superpowers" with dir = "docs/superpowers/plans" (git paw init prompts you to choose the spec system), or pass --specs-format superpowers for a single launch.
Boot prompt. Each worktree’s .git-paw/AGENTS.local.md sidecar contains: Plan Context (Goal / Architecture / Tech Stack), Your Tasks (the plan’s task sections with their Files: and Run: metadata), and an Execution instruction to work the steps in order, flip - [ ] → - [x] in the plan file as each lands, and commit your work when every step shows - [x]; the post-commit hook will auto-publish your completion.
Configuration
Configure spec scanning in .git-paw/config.toml (or the global config):
# Default CLI for spec-mode launches (bypasses picker when set).
default_spec_cli = "my-cli"
# Prefix for spec-derived branch names (default: "spec/").
branch_prefix = "spec/"
# Spec scanning configuration.
[specs]
dir = "specs" # Directory containing spec files (relative to repo root)
type = "openspec" # "openspec" (default), "markdown", "speckit", or "superpowers"
The interpretation of dir depends on the format:
openspec— directory of change subdirectories (<dir>/<change>/tasks.md).markdown— directory of.mdfiles (<dir>/<spec>.md).speckit— directory of feature subdirectories (<dir>/<feature>/tasks.md); typically.specify/specs.superpowers— directory of flat plan files (<dir>/<plan>.md); typicallydocs/superpowers/plans.
Branch Naming
Branch names depend on the format:
- OpenSpec:
<branch_prefix><id>where ID is the subdirectory name.specs/add-auth/with prefixspec/becomes branchspec/add-auth. - Markdown:
<branch_prefix><id>where ID ispaw_branch(if set) or the filename stem.specs/add-auth.mdwith prefixspec/becomes branchspec/add-auth. - Spec Kit: branch prefix is
task/orphase/depending on the entry kind;branch_prefixis not applied. Example:.specify/specs/003-user-list/tasks.mdwith task- [ ] T009 [P] Add login formproduces branchtask/t009-add-login-form. - Superpowers:
plan/<slugified plan-file stem>;branch_prefixis not applied. Example:docs/superpowers/plans/2026-07-20-add-auth.mdproduces branchplan/2026-07-20-add-auth.
Spec Kit and Superpowers derive their branch name from spec text — a task
description, a plan-file stem — by lowercasing it and replacing every character
outside [a-z0-9_] with -. Underscores survive into the branch name. A
task reading - [ ] T001 [P] Create ROOM_SETUP.md produces branch
task/t001-create-room_setup-md, and a plan file add_auth.md produces branch
plan/add_auth: _ is legal in a git ref, and the branch is durable state that
worktree recovery and --resume use to look up work, so it is left intact.
The agent’s broker agent_id is a separate derivation from that branch name,
and it does not keep _ — the broker’s id grammar forbids it, so the
underscore maps to - there. The branch task/t001-create-room_setup-md
publishes as agent task-t001-create-room-setup-md. See
Identifier Forms for the
full conversion.
CLI Resolution
When a spec-mode launch runs, CLIs are resolved in priority order:
--cliflag (highest) — applies to all specs, no promptpaw_cliin spec — per-spec override from frontmatterdefault_spec_cliin config — fills remaining specs without promptdefault_cliin config — pre-selects in picker for remaining- Interactive picker (lowest) — prompts for any unresolved specs
# Override all specs to use claude
git paw start --from-all-specs --cli claude
# Use per-spec paw_cli and default_spec_cli from config
git paw start --from-all-specs
# Narrow to a subset
git paw start --specs add-auth,fix-session --cli claude
# Preview without executing
git paw start --from-all-specs --dry-run
git paw start --specs add-auth --dry-run
Combining --from-all-specs with supervisor mode
--from-all-specs --supervisor (or --from-all-specs with [supervisor] enabled = true
in your config) engages the supervisor flow against the discovered specs.
git-paw scans the configured specs directory, creates one worktree per spec,
and builds one tmux session with the dashboard pane (pane 0) and the per-spec
agent panes from pane 1 — the orchestrator runs outside the session, reading
the context rendered into .git-paw/AGENTS.local.md — then returns with an
attach hint (or, with --unattended, runs the in-process drive loop instead)
— same supervisor architecture as --branches-driven sessions, just with
branches discovered from specs.
The CLI Resolution chain above applies identically here —
--supervisor never bypasses or alters it. In particular, default_spec_cli
is honoured for a spec-driven worker pane exactly as it is without
--supervisor, so a [clis.<name>] sandbox pinned via default_spec_cli
cannot be silently swapped for a different CLI just because the launch went
through the supervisor flow. git paw doctor also reports the effective
spec-worker CLI and warns if a configured default_spec_cli is never
actually reachable by any discovered spec — see Doctor.
# Spec-driven session with supervisor watching
git paw start --from-all-specs --supervisor
# Same outcome via [supervisor] enabled = true in .git-paw/config.toml
git paw start --from-all-specs
# Skip supervisor for this session even if enabled in config
git paw start --from-all-specs --no-supervisor
When [broker] enabled = true — the broker defaults to disabled and must be
opted into explicitly in config (supervisor mode does not turn it on for
you) — each spec agent pane receives a boot block via tmux send-keys
carrying its BRANCH_ID, broker URL, and the patterns for publishing through
the bundled broker.sh helper. This applies to both spec-mode-only
(--from-all-specs without supervisor) and supervisor mode; a spec-mode-only
launch additionally submits the boot block (rather than leaving it
pre-filled) and appends the task prompt naming the agent’s spec assignment,
so the agent starts work without a human completing the prompt by hand — see
Boot-block submission and task-prompt injection
below.
Boot-block submission and task-prompt injection
Not every launch path treats its agent pane’s initial prompt the same way:
- Bare
git paw start(no spec selection) pre-fills the boot block — followed by the worktree rule naming that pane’s own worktree root — into the pane’s input line and stops. NoEnteris sent. This is deliberate: the launch is human-driven, so you type your actual task after the boot instructions and submit it yourself. - A spec-driven launch (
--specs/--from-all-specs, with or without--supervisor) has no human typing the task — the task comes from the spec — so leaving the prompt pre-filled would strand the agent with nothing to act on. Both the supervisor path and the non-supervisor spec-mode path submit the agent’s prompt automatically: the text is injected, then — after a brief per-CLI settle delay — a separateEntersubmits it. That prompt is the boot block (when the broker is enabled), the task prompt’s prose naming the agent’s spec assignment, and the worktree rule, joined by blank lines. - With
[broker] enabled = false, the non-supervisor spec-mode path (bare--specs/--from-all-specs, no--supervisor) injects no boot block — it is broker-specific content — but still injects and submits the task prompt, since the agent’s assignment does not depend on the broker. The supervisor path builds its agent panes’ prompt the same way regardless of whether the broker is enabled, so a supervisor-mode agent still receives a boot block even with the broker off; only its broker-coordination instructions are then inert.
Every launch path also composes its pane’s command from the resolved
approval flags (agent_approval and
any per-CLI approval_args override) — a bare or spec-driven launch is not
exempt just because no supervisor is watching.
The workflow entry point
A spec format may declare a workflow entry point: one command line that
brings the agent into the format’s native workflow, so it walks its task list
and ticks tasks off rather than improvising a private process. Today only
OpenSpec declares one — /opsx:apply <id>. Markdown, Spec Kit, Superpowers
and the no-spec --branches path declare none.
When a format declares an entry point, a launch that submits the agent’s prompt sends two messages, not one:
- the composed prompt above (boot block + task prose + worktree rule), and
- the entry point on its own, as a separate composer line with its own
Enter.
The command used to sit inside the task prompt’s prose, at the end of a sentence — and a weaker model read that sentence and narrated the command instead of running it, going idle with the work untouched. Submitted alone, the line is what a paste-aware CLI parses as a slash-command invocation, which is the behaviour the agent needs. The text comes only from the spec format’s own bundled template; git-paw’s code names no workflow command, so a format whose apply invocation changes changes only its template.
A pane whose format declares no entry point receives exactly one message, and the launch does not wait on it at all.
The idle wait
git-paw does not type the second message the instant the first one is sent.
Typing into a CLI that is still working is unpredictable — a slash command
dropped into a busy composer may be queued as text, run later, or discarded,
and a keystroke that lands on a live permission prompt answers it. So after
the first message’s Enter, the launch waits until the pane is idle:
- no live permission prompt,
- not mid-response, and
- an input box that is empty, or whose shape the CLI’s prompt-shape profile does not recognise,
observed on two consecutive captures. A single idle-looking capture is not
enough: the instant between the Enter and the CLI starting its turn reads
as idle too. Immediately before typing, git-paw captures the pane once more
and types nothing unless it is still idle — so a pane that raised a prompt or
started responding in the meantime receives no stray keystroke. The entry
point is then submitted with the same split-send the first message used:
literal text, settle delay, separate Enter.
Every waiting pane is polled in one loop, concurrently, under a single ~120-second budget — so a session’s launch grows by its slowest first turn, not by the sum of them. The OpenSpec prose tells the agent its workflow command is coming, which keeps that first turn short.
This wait happens before the launch attaches you to the session (and
before git paw add prints its confirmation), so git-paw announces it first:
Waiting for 3 agent(s) to go idle before submitting their workflow command (up to 120s)...
If you see that line and nothing after it for a while, a pane is still
working or is sitting at a permission prompt — attach in another terminal
with tmux attach -t paw-<project> to see which.
A CLI git-paw has no profile for matches none of the markers above and therefore reads as idle. At worst its entry point follows the first message almost immediately — exactly what that CLI’s own input queue would see from a human typing two messages in a row.
If a pane never goes idle
When the budget elapses with a pane still busy or still sitting at a permission prompt, git-paw types nothing into it and prints a warning to stderr naming the pane, the agent, and the exact line to submit:
warning: pane 2 (spec-add-auth) did not go idle within 120s, so its workflow
command was not submitted; once the agent is idle, submit: /opsx:apply add-auth
The launch continues normally — the agent has its assignment and its boot block, it simply has not been dropped into its spec workflow. Attach to the session, answer whatever the pane is waiting on, and type the quoted command into that pane yourself. An unattended run leaves the same line in its output for an orchestrator to act on.
git paw add and paused sessions
git paw add to a running session follows the same rule: the new agent’s
prompt is submitted, then its entry point once its pane goes idle.
git paw add to a paused session submits nothing — the pane is created
but the prompt is held in the session record for the restart. The entry point
is held alongside it. git paw start then submits the held prompt, waits for
the pane to go idle, submits the entry point, and clears both. A session
receipt written by an earlier git-paw carries no held entry point and
restarts exactly as it did before; see Pause and Resume.
Non-interactive launches
If git paw start is invoked from a non-interactive terminal — CI, scripted
invocation, or a harness tool that pipes stdin — git-paw skips the auto-attach
step and prints an attach hint instead:
Session 'paw-myproject' started in detached mode.
Attach with: tmux attach -t paw-myproject
Supervisor mode never auto-attaches, interactive terminal or not: the
orchestrator is your own agent session outside tmux, and (without
--unattended) the launch prints Supervisor session '<name>' launched with <N> coding agent(s)., the orchestrator-context path
(.git-paw/AGENTS.local.md), and the same Attach with: hint. Attach later from a real
terminal:
tmux attach -t paw-myproject
Session Logging
git-paw can capture the full terminal output of each AI CLI pane to log files. This lets you review what happened in a session after it ends.
Enabling Logging
Add the [logging] section to your .git-paw/config.toml (or global config):
[logging]
enabled = true
When enabled, git-paw uses tmux pipe-pane to capture each coding-agent pane’s output to a log file when it launches a fresh session — a plain git paw start, a spec-driven launch, or a supervisor session. The dashboard pane is not logged, and neither are panes attached later with git paw add or the panes of a session recovered by git paw start after git paw stop.
Log Storage
Logs are stored under .git-paw/logs/ organized by session:
.git-paw/logs/
paw-my-project/
feat--add-auth.log
fix--pagination.log
Branch names are sanitized for filenames — / becomes -- (e.g., feat/add-auth becomes feat--add-auth.log).
The git paw init command automatically adds .git-paw/logs/ to .gitignore so logs are never committed.
Replaying Logs
Use git paw replay to view captured logs.
List available sessions and branches
git paw replay --list
Shows all sessions and their logged branches.
Replay a branch
git paw replay feat/add-auth
The argument must match a log exactly — as the original branch name (feat/add-auth), the sanitized stem (feat--add-auth), or the full filename (feat--add-auth.log). Partial names are not matched; when nothing matches, the error lists the available branches.
By default, ANSI escape codes are stripped for clean text output.
Replay with colors
git paw replay feat/add-auth --color
Preserves ANSI colors and pipes through less -R for scrollable colored output.
Replay from a specific session
git paw replay feat/add-auth --session paw-my-project
By default, the most recent session is used. Use --session to specify an older one.
Flags
| Flag | Description |
|---|---|
--list | List available log sessions and branches |
--color | Display with ANSI colors via less -R |
--session <name> | Session to replay from (defaults to most recent) |
AGENTS.md Injection
An agent’s working context is the combined view: the project’s tracked
AGENTS.md plus a managed git-paw section that carries the entire spec body
for the agent’s assigned change (proposal, design when present, task list, and
any spec deltas). The agent reads the spec from this combined view plus
openspec/changes/<id>/, not from the boot prompt.
git-paw writes that combined view to a gitignored sidecar,
.git-paw/AGENTS.local.md, in each worktree — not to the worktree’s
tracked AGENTS.md. The boot prompt points the agent at the sidecar. The
tracked AGENTS.md is left exactly as the project committed it, so a
legitimate hand edit to it (yours or the agent’s) shows up in git status and
commits normally, mid-session.
Why a sidecar. Through v0.7.0 git-paw injected its section into the tracked worktree
AGENTS.mdand then rangit update-index --assume-unchanged AGENTS.mdto keepgit add -Afrom committing the ephemeral injection. That bit is file-level: git stopped reporting every change toAGENTS.md, not just the injected block. An agent that needed to commit a realAGENTS.mdedit foundgit statusreporting the file unmodified and the commit silently empty for that path. Moving the injection to a gitignored sidecar removes the reasonassume-unchangedexisted: the tracked file is never touched, so it is never hidden.
Boot-Prompt-Full-Body Model
Earlier versions of git-paw embedded a condensed spec excerpt inside the
supervisor-mode boot prompt itself (“Branch + CLI + Spec content + Owned
files”). The supervisor-mode boot prompt is intentionally short:
it points the agent at the sidecar .git-paw/AGENTS.local.md and at the change
directory openspec/changes/<id>/, and tells the agent to read those files
before acting. (Because the supported CLIs auto-load only the worktree-root
AGENTS.md and not the sidecar, the prompt is what directs the agent to the
sidecar’s combined content — every backend, including OpenSpec, points there
first.)
The benefits of this split:
- The spec body lives in a markdown file the agent can re-read at any time, not in volatile boot-prompt context.
- The boot prompt becomes small enough to remain in the agent’s working context for the entire session, even after long conversations.
- Manual operators can pre-stage the boot block at the pane input line via paste, the same way the supervisor stages it programmatically.
- The same generator (
src/agents.rs::setup_worktree_agents_md) writes the same content in both supervisor mode and broker-only mode, so the agent’s experience does not depend on which mode launched it.
How It Works
- Sidecar. When a session starts, each worktree gets a
.git-paw/AGENTS.local.mdsidecar containing the combined view (the rootAGENTS.mdcontent plus the managed git-paw section). The sidecar is overwritten on everygit paw startso the spec body never goes stale. - The tracked
AGENTS.mdis untouched. git-paw does not write to it, hide it, or exclude it. A hand edit to it appears ingit statusand commits normally. - Exclusion. The sidecar path (
.git-paw/AGENTS.local.md) is added to the worktree’s ignore set so the ephemeral injection is never committed. For a linked worktree, git honorsinfo/excludeonly from the common git directory, so the entry is written there. - Self-healing upgrade. If a worktree carries a stale
assume-unchangedbit onAGENTS.mdfrom a pre-sidecar git-paw, the nextgit paw startclears it (git update-index --no-assume-unchanged AGENTS.md) so the tracked file becomes committable again — no manualgit update-indexneeded.
Markers
git-paw manages its section in the sidecar using HTML comment markers so the
root AGENTS.md content carried alongside it is preserved across launches:
<!-- git-paw:start — managed by git-paw, do not edit manually -->
(git-paw content here — full spec body)
<!-- git-paw:end -->
Content between these markers is replaced on each launch. Content outside the markers is preserved. If no markers exist, the section is appended to the end of the file.
Orchestrator pointer section
Separately from the per-session sidecar, git paw init injects one short,
permanent section into your root AGENTS.md (creating the file when it
is absent), between its own markers:
<!-- git-paw-orchestrator:start -->
(when a git-paw session is active, you are its orchestrator: read
.git-paw/skills/orchestrator/SKILL.md and .git-paw/AGENTS.local.md)
<!-- git-paw-orchestrator:end -->
It is how the agent you drive the session with discovers the orchestrator
skill: AGENTS.md is the cross-CLI file that agent already reads. The prose
comes from a bundled, project-agnostic asset and carries no broker URL, secret,
toolchain or vendor name. Re-running git paw init replaces the section in
place, leaving everything outside the markers byte-identical. Unlike the
per-session <!-- git-paw:start --> block, it is configuration, not session
state: git paw stop and git paw purge leave it alone. Delete the section by
hand if you do not want it; init reports what it wrote.
Sidecar Content
Each worktree’s .git-paw/AGENTS.local.md sidecar includes:
- The root repo’s
AGENTS.mdcontent (if any), preserved verbatim. - A git-paw section between the markers carrying:
- Branch name and assigned CLI for this worktree.
- Full spec body — proposal, design, tasks, and any spec deltas from
openspec/changes/<id>/. The exact set depends on what the change directory contains; missing files (design.md is often optional) are silently skipped. - Owned files list (from OpenSpec file-ownership declarations) when declared.
- Boot block that calls the bundled
.git-paw/scripts/broker.shhelper for the agent to self-report status, artifacts, blockers, and questions. The helper (installed bygit paw init, alongside the supervisor’ssweep.sh) discovers the broker URL and shapes the JSON, so the boot block carries no rawcurlor broker URL. Nothing is seeded into the agent CLI’s settings file, and git-paw answers no prompt on the agent’s behalf. Whether the helper runs without prompting is the CLI’s own decision; because it always lives at the stable path.git-paw/scripts/broker.sh, one permission rule you write there covers every call instead of a broadcurlrule — see Coordination.
The supervisor-mode boot prompt does not duplicate any of this content — it
points at the sidecar. If a change is updated on disk during a session (e.g.
the supervisor edits tasks.md to mark a task complete), the worktree’s
sidecar is regenerated on the next git paw start invocation rather than
live-patched mid-session.
Agent Coordination
When multiple AI agents work in parallel, they benefit from knowing what the others are doing. The coordination broker is a lightweight HTTP server that lets agents share status updates, publish artifacts, and flag blockers – all without touching git.
Enabling the Broker
Add a [broker] section to your .git-paw/config.toml:
[broker]
enabled = true
When you run git paw start, pane 0 becomes a dashboard instead of an agent pane and the agents start at pane 1. Supervisor mode keeps the same layout: the orchestrator runs outside the session (see Supervisor § Session layout). The broker runs in its own process in a detached broker window; the dashboard is a client of it and displays a live status table.
How Agents Discover the Broker
git-paw sets the GIT_PAW_BROKER_URL environment variable in every agent pane. Agents use this URL to send and receive messages. A typical value is http://127.0.0.1:9119.
With the broker enabled, each agent’s boot block calls the bundled broker helper (or, for an MCP-speaking CLI, the MCP publish tools), and the agent’s AGENTS.md view carries the bundled coordination skill with the broker URL pre-expanded, so agents know how to use the broker without any manual setup.
Requests the broker refuses
The broker is unauthenticated, so before routing any request it checks two headers to keep browsers and DNS-rebinding pages out. A refused request is not routed and changes no broker state:
| Status | Body | Cause |
|---|---|---|
403 | {"error":"forbidden host"} | The broker is bound to a loopback address and the request’s Host is missing, or its host part is not localhost, an IPv4 literal in 127.0.0.0/8, or [::1]. The port is not checked, so tunnels and port-forwards are fine. |
403 | {"error":"forbidden origin"} | The request carries an Origin header whose host is not loopback, or the serialized value null, or the header was sent more than once or with unreadable bytes. This check applies on every bind. A request with no Origin — which is what every non-browser client sends — is unaffected. |
A Host must also be followed by nothing or a well-formed :<port>, so
localhost:evil.com is refused; a duplicated or unreadable Host is refused;
and an absolute-form request target (GET http://host/path) must name a
loopback authority too.
Because the check runs before routing, it also answers for unknown paths and
wrong methods: a request with a foreign Host gets 403, not 404/405.
Every client git-paw ships — the broker.sh and sweep.sh helpers, the
post-commit hook, the CLI, the MCP server and the dashboard — addresses the
broker by its configured http://<bind>:<port> URL and sends no Origin, so
none of them is affected. See
Security Posture
for the threat model, the non-loopback-bind exception, and the reverse-proxy
migration.
A verdict message is refused on its own terms, after those checks:
| Status | Body | Cause |
|---|---|---|
403 | {"error":"authority required"} | The message is an agent.verified, or an agent.feedback whose from is supervisor, and the request’s X-Paw-Authority header is missing or does not match the session’s authority credential. Nothing is delivered or logged. |
Feedback from any other sender — the post-commit branch-guard hook, the
in-process conflict-detector, a person — is not a verdict and needs no
header. Root-side helpers (sweep.sh verified, feedback-gate,
conflict-ack) attach the credential for you; see
Security Posture.
Broker helper
git paw init installs a bundled agent-side helper at
.git-paw/scripts/broker.sh — the agent-facing analogue of the supervisor’s
sweep.sh. .git-paw/scripts/ is gitignored, so a fresh agent-worktree
checkout does not contain it. git paw start and git paw add therefore
auto-provision the helper into each agent worktree at setup: they write
broker.sh (whenever the broker is enabled) and docs-fetch.sh (whenever
docs_base_url is configured) — with the shared _paw_common.sh preamble
both scripts source — into the worktree’s .git-paw/scripts/,
executable, from the same bundled assets init installs. The scripts are
(re)written on every attach, so a worktree’s helper always matches the running
binary’s version and agents never copy it by hand from assets/. The agent
invokes the helper by its stable relative path.
The helper wraps every agent→broker curl an agent is allowed to make. It
discovers the broker URL from .git-paw/config.toml [broker] (defaulting to
http://127.0.0.1:9119) and shapes the JSON internally, so callers pass only
simple arguments. The agent id comes from --agent <id> (the boot block
passes the pre-expanded branch id) or, absent one, from slugifying the current
worktree branch. Run .git-paw/scripts/broker.sh --help for the full surface.
In the repository’s main working tree the helper needs an explicit
--agent <id>. Agents publish from linked worktrees; the main working tree is
the orchestrator’s, and deriving an id from its branch would mint a phantom
agent named after the default branch (main → agent-main) rather than
identifying anybody. Run there without --agent, the helper exits non-zero with
a diagnostic naming the remedy and publishes nothing. Pass the identity you mean
— broker.sh --agent supervisor question "which order?" — and it publishes as
before. Nothing changes for an agent in its own worktree, where derivation works
exactly as it always has.
| Subcommand | Publishes / reads |
|---|---|
status <message> | agent.status (status:"working", the message, modified_files:[]) |
artifact [--exports a,b] [--files a,b] | agent.artifact (status:"done") — the code-less DONE fallback |
blocked <needs> <from> | agent.blocked with dependency info |
question <text> | agent.question |
intent <summary> <files> [valid_for_seconds] | agent.intent (forward-coordination file claim); the TTL defaults to 900 seconds when omitted |
poll [since] | reads GET <broker>/messages/<agent-id>?since=<n> — this agent’s inbox |
valid_for_seconds is a required field of agent.intent, so the helper always
sends one: give a TTL explicitly when you want a different window, and the
default of 900 seconds (15 minutes) applies otherwise.
A publish the broker rejects is loud. Every publish subcommand checks the
broker’s HTTP response. When the broker does not accept the publish — any
non-2xx status, or no response at all because the broker is not running — the
helper prints the broker’s error to stderr and exits non-zero; it never
reports the publish as done. Treat a non-zero exit as “this event did not
reach the broker” and fix the cause (a malformed agent id, a stopped broker)
rather than moving on. The same holds for the supervisor-side sweep.sh
publish verbs.
The helpers are refreshed on every attach. git paw start and
git paw add (re)write <worktree>/.git-paw/scripts/ from the bundled assets
of the binary that launched the session, on every launch path, so a worktree’s
helper always matches that binary’s version. Re-attaching a worktree that
already has the scripts rewrites them rather than failing — which also means
local edits to a worktree’s helper copy are overwritten. Edit
assets/scripts/ (when developing git-paw itself) rather than a deployed copy.
Why a script and not a git paw publish subcommand? The helper is an
agent-internal mechanism. A user-facing subcommand would surface in --help
and produce confusing errors when run by a human (no broker, no session). A
script under .git-paw/scripts/ is unambiguously agent-internal, mirrors the
supervisor’s sweep.sh, and gives you a single stable path that one
permission rule in your agent CLI can name — see
Boot calls and permission prompts.
Boot-Prompt Injection
To ensure reliable agent self-reporting, git-paw automatically injects a standardized boot instruction block into every agent’s initial prompt. The block calls .git-paw/scripts/broker.sh (with the pre-expanded --agent <id>) for four essential operations — no raw curl and no broker URL appear in the boot block:
1. REGISTER - Immediate Status Publication
Agents automatically publish their working status with a “booting” message as their very first action:
.git-paw/scripts/broker.sh --agent feat-auth status booting
2. DONE - Task Completion Reporting
The primary completion path is git commit. The git-paw post-commit hook auto-publishes agent.artifact { status: "committed" } with modified_files derived from git diff HEAD~1 --name-only, so agents working on code changes do not publish anything manually — they commit and the hook reports on their behalf.
The boot block retains a manual agent.artifact { status: "done" } fallback for code-less tasks (docs-only updates handled outside the worktree, planning notes, exploration tasks where the artifact is information reported to the broker). The block warns agents NOT to publish manual done while their worktree has uncommitted changes — they should commit instead.
.git-paw/scripts/broker.sh --agent feat-auth artifact --exports "" --files ""
3. BLOCKED - Dependency Waiting Notification
Agents can properly declare when they’re waiting on dependencies:
.git-paw/scripts/broker.sh --agent feat-api blocked "auth token format" feat-auth
4. QUESTION - Uncertainty Escalation (Critical)
Agents are instructed to publish questions and wait for answers rather than guessing:
.git-paw/scripts/broker.sh --agent feat-auth question "Should the JWT use RS256 or HS256 signing?"
IMPORTANT: The boot block explicitly instructs agents: “DO NOT CONTINUE UNTIL YOU RECEIVE AN ANSWER!”
MCP tool form
The four events above are the broker.sh helper form. For a CLI known to
speak MCP (Model Context Protocol) — the built-in set is claude, codex,
cursor-agent, and agy — the boot block instead expresses each event as an
invocation of the matching MCP publish tool:
publish_status, publish_artifact, publish_blocked, publish_question.
The events, their meanings, and their ordering are identical between the two
forms; only the invocation mechanism differs, and each MCP publish tool
resolves the publishing agent id from the session’s own worktree branch
automatically — the boot block never needs to pass one.
An unrecognized or unknown-capability CLI always gets the broker.sh form.
This is the safe default: a boot block naming tools a CLI cannot call would
strand the agent at the very first step, so git-paw only switches to the MCP
form when it is confident the target CLI can act on it.
A [clis.<name>] mcp = true|false
entry overrides the built-in set, and is consulted first. The lookup is keyed
on the CLI’s identity — the [clis.<name>] key, or the built-in /
pass-through name — never on the command that key launches, so a custom
variant such as [clis.claude-oss] command = "claude" keeps the helper form
until you set mcp = true on it.
The MCP publish surface has no inbox or poll tool, so even the MCP form of
the boot block reads the agent’s inbox with
.git-paw/scripts/broker.sh --agent <id> poll.
Boot calls and permission prompts
Whether an agent’s first boot action stalls on a permission prompt is decided by the agent CLI’s own permission system — nothing in git-paw decides it, and git-paw grants nothing on your behalf. Two things are in play:
- the agent CLI’s resolved permission mode, set via
agent_approval(see Configuration → Supervisor). Under a mode that still prompts for shell commands, the boot publish will prompt; - your own allow rules in that CLI. The boot block always invokes the
helper at the single stable path
.git-paw/scripts/broker.sh(or thebash .git-paw/scripts/broker.shform), precisely so one permission rule scoped to that exact path covers every boot call — never a broadcurl *rule, and never per-endpointcurlprefixes that drift with URL normalisation or flag order.
git-paw writes to no CLI settings file — not the repo-root
.claude/settings.json, not a per-worktree copy, and not a configured
[clis.<name>].settings_path. It also never answers the prompt for you: if
the boot publish prompts, an --unattended run escalates it for a human
decision like any other prompt, and the rest of the wave keeps moving. The
same is true of routine dev commands (git status, grep, find, your
stack’s build/test verbs) — they run without asking only when the CLI’s own
mode or rules permit it.
Pre-approving the helper calls, with your consent
If you would rather not answer those prompts at all, you can pre-approve git-paw’s helper calls in the agent CLI’s own allow rules:
git paw setup-cli <CLI> --allowlist --dry-run # see exactly what it writes
git paw setup-cli <CLI> --allowlist # apply it
This is optional and consent-only: you run it, it prints its plan before
writing anything, and git paw setup-cli <CLI> --undo takes back exactly what
it added. No launch path, no drive loop and no MCP tool ever runs it. A call
that runs without a prompt still does so only through the agent CLI’s own
permission system and the rules in its settings — git-paw makes no safety
decision about any command. See Per-CLI Setup for the grant
table, the protection rules that come with it, and the residual risks.
Boot Block Injection Modes
- Supervisor Mode: Boot block is prepended to each agent’s task prompt, then submitted (injected, then a separate
Enterafter a settle delay) - Spec-driven, non-supervisor mode (
--specs/--from-all-specswithout--supervisor): the boot block (when the broker is enabled) is prepended to the task prompt the same way, and submitted the same way — the task comes from the spec, not a human, so the combined prompt cannot be left for someone to complete and submit by hand - Bare, human-driven mode (
git paw startwith no spec selection): Boot block is pre-filled into each agent pane’s input line and left unsubmitted (user pastes their task after the boot instructions, then submits both together)
Both submitting modes end the injected prompt with the worktree rule. When the agent’s spec format declares a workflow entry point, a second line follows as its own message once the pane goes idle — see The workflow entry point.
The worktree rule
Every prompt git-paw injects into a coding agent pane at launch ends with a
bundled worktree rule naming that agent’s absolute worktree root. It is
appended by every injecting path — the bare pre-fill (which is still left
unsubmitted, with no Enter), spec-driven and supervised start, git paw add, and a prompt held for an agent added while the session was paused — so
no path can forget it. The rule is placed last, for recency: it is the
constraint the agent should still be holding when it starts editing.
The rendered text, with the pane’s own recorded worktree path substituted:
Your worktree root is
<worktree root>. Every file you create or edit SHALL lie under that directory. Never write outside it — not by an absolute path, and not by a relative path that climbs out of it (such as../).
The ../ clause is the point. Under child
placement the repository’s main checkout is an
ancestor of every worktree, so a relative path that climbs far enough
reaches it — and an agent told only to “use relative paths” can edit the main
checkout without ever typing an absolute path. Naming the root makes the
boundary something the agent can check a path against.
Like the rest of the boot prompt, the rule is instruction, not enforcement. What actually stops an out-of-worktree write is the agent CLI’s own outside-directory permission prompt and, if you have configured one, the FS-scoped sandbox — see Memory Isolation below. The template is project-agnostic: it names no tool, CLI or project, only the path.
Paste Handling
The boot block includes instructions for proper paste handling, particularly the requirement to send an additional Enter key after paste operations to ensure full content processing.
Benefits
- Reliable Monitoring: Agents self-report immediately on boot
- Consistent Behavior: All agents follow the same coordination pattern
- One rule covers every boot call: the boot block invokes the bundled
broker.shhelper at one stable path, so a single permission rule in your agent CLI can allow it — rather than a broadcurlgrant. Whether it prompts at all is that CLI’s decision; if it does, the prompt is escalated to you, never auto-answered - Supervisor Visibility: Questions and blockers surface to the dashboard promptly
- Audit Trail: All boot operations are logged in the broker log
Worktree Environment
A fresh worktree surfaces a few conditions that look broken but are actually expected policy. The bundled coordination skill briefs agents to recognise them and adapt rather than spend budget diagnosing a non-bug:
- Gitignored install artifacts. Per-stack install artifacts (dependency
directories, build/restore caches) are absent from a fresh checkout by
design and may already be provisioned by an operator-configured worktree
on_createhook before the agent’s CLI starts. Agents are told not to reach for the repo-root copy or symlink it in, and to run their stack’s install step themselves only when the artifacts are genuinely missing. - FS-confinement is policy, not a fault. When a worktree is FS-confined
by an operator-configured sandbox, an
Operation not permittedon a path outside the worktree is the sandbox working as intended. Agents are told to adapt rather than probe it (xattr,id,ls -lO@, write-probes) and to keep working inside their worktree. - Setuid binaries under a sandbox. Some setuid-root system binaries —
psis the canonical example — cannot exec inside a sandbox. This is expected and not fixable, and does not indicate anything else is wrong.
This orientation is stack-agnostic: it never names a specific package-manager invocation, referring instead to “the stack’s install step.”
Memory Isolation
Coding agents are briefed (via the bundled coordination skill) that their worktree is their entire writable territory:
- Persistent artifacts live in the worktree. Memory files, notes, scratch state, and any configuration an agent generates for itself SHALL be created inside the agent’s own worktree — never elsewhere on the machine.
- Operator configuration directories are off-limits for writes.
Home-level CLI configuration directories (and the per-project memory
subtrees beneath them) and the host repository’s
.claude/and.git-paw/control directories belong to the operator. Writing there mutates state shared across sessions and agents. - Question instead of write. When a task appears to require writing
outside the worktree, the agent publishes
agent.questiondescribing the path and waits for an answer rather than writing.
The agent is told the same boundary twice, on purpose: once here, by the bundled coordination skill it reads from its sidecar, and once at the end of its very first prompt, by the worktree rule — which names the absolute path rather than leaving the agent to infer it.
This boundary is carried by doctrine, not by a mechanical check in git-paw: a write targeting operator territory raises the CLI’s own permission prompt, which is escalated to a human and never answered automatically (see Agent permissions). For mechanical enforcement, add deny rules in the agent CLI’s own settings or use the sandbox. The supervisor treats an observed out-of-worktree write attempt as a boundary violation with scoped feedback and repeat-escalation (see Out-of-worktree write violations). The guidance closes the remaining gap: persistence paths that never raise a prompt are governed by the briefing itself.
Message Types
Most broker messages use the same JSON envelope:
{
"type": "agent.<variant>",
"agent_id": "<slug>",
"payload": { ... }
}
<slug> is the agent’s slugified branch name (lowercase alphanumeric + -
only; slashes and underscores from a branch name like feat/auth or
feat/my_auth become hyphens — feat-auth, feat-my-auth). Eight variants
use this envelope: agent.status,
agent.artifact, agent.blocked, agent.intent, agent.question,
agent.feedback, agent.answer, and agent.verified. Three more do not:
agent.advanced-main carries its fields flat alongside type (see
Advanced Main); agent.learning, published by the
learnings aggregator,
carries its agent_id inside payload; and supervisor.verify-now is
emitted by the broker itself — carrying only the committing branch_id — to
the supervisor inbox whenever an agent.artifact { status: "committed" }
arrives and [supervisor] verify_on_commit_nudge is on (the default).
src/broker/messages.rs is the source of truth for the payload schemas.
agent.statusandagent.artifactare normally automatic. The filesystem watcher publishesagent.status(withmodified_files) whenever a worktree’sgit status --porcelainoutput changes, and the post-commit git hook publishesagent.artifactwithstatus: "committed"and the committed file list every time an agent commits. The manualcurlexamples below are escape hatches for cases where the automatic publishers do not apply (e.g. code-less tasks or heartbeat injection during read-only investigation).
Status
An agent reports what it is currently doing along with any files it has already modified in this work step.
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.status","agent_id":"feat-auth","payload":{"status":"working","modified_files":["src/auth.rs"],"message":"implementing login endpoint"}}'
Artifact
An agent shares the result of a commit (or the analogous output of a code-less
task) so peers can see exports and modified files. The modified_files array
is what the conflict detector watches for in-flight overlap.
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.artifact","agent_id":"feat-auth","payload":{"status":"committed","exports":["AuthClient"],"modified_files":["src/auth.rs","src/auth/client.rs"]}}'
Blocked
An agent declares that it is waiting on something specific from another agent
(or external resource). from names the agent that can unblock it.
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.blocked","agent_id":"feat-api","payload":{"needs":"auth token format","from":"feat-auth"}}'
Intent
An agent declares which files it plans to modify before any edit lands. The
broker conflict detector reads agent.intent to flag forward conflicts when
two agents target overlapping paths. valid_for_seconds is the TTL after which
consumers MAY treat the intent as stale.
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.intent","agent_id":"feat-auth","payload":{"files":["src/auth.rs","src/auth/client.rs"],"summary":"wire AuthClient","valid_for_seconds":900}}'
Each files entry may also be an object declaring the regions within a file
the agent intends to touch — see Declaring regions
for the region-level granularity added in v0.6.0.
Question
An agent escalates an uncertainty to the supervisor inbox. The asking agent blocks at its prompt until a typed reply arrives.
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.question","agent_id":"feat-auth","payload":{"question":"Should the JWT use RS256 or HS256 signing?"}}'
Feedback
A supervisor (or the broker’s auto-emitted [conflict-detector] voice) sends
a list of error messages to a target agent. The target agent_id field on the
envelope is the receiver; from inside the payload is the sender.
Feedback from supervisor is a verdict: the broker refuses it without the
session’s authority credential (403 {"error":"authority required"}), so the
supervisor publishes it through the helper, which attaches the credential:
.git-paw/scripts/sweep.sh feedback-gate feat-auth testing "test for HS256 path is failing"
The wire body it sends:
{"type":"agent.feedback","agent_id":"feat-auth","payload":{"from":"supervisor","errors":["[testing] test for HS256 path is failing"]}}
Answer
A supervisor replies to an asking agent’s agent.question with a non-error
answer. Like feedback, the envelope agent_id is the receiver (the agent
being answered) and from inside the payload is the sender; the message
routes to the target agent’s inbox only. answer carries the reply text and
the optional re field is a short reference to the question being answered
(omitted when absent). An answer is authoritative guidance to act on — it is
not an error report, so the receiving agent has nothing to fix and nothing to
re-publish. Reserve agent.feedback for corrective errors.
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.answer","agent_id":"feat-auth","payload":{"from":"supervisor","answer":"Use RS256; the key rotation tooling assumes it","re":"RS256 or HS256?"}}'
Verified
A supervisor confirms that an agent’s work has passed every verification gate.
The agent_id is the agent whose work was verified; verified_by names the
verifier. Every agent.verified is a verdict, refused without the session’s
authority credential, so it is published through the root-side helper:
.git-paw/scripts/sweep.sh verified feat-auth "all five gates pass"
The wire body it sends:
{"type":"agent.verified","agent_id":"feat-auth","payload":{"verified_by":"supervisor","message":"all five gates pass"}}
Advanced Main
The supervisor publishes this after every successful merge to the default
branch so dependent agents learn the base moved. The payload fields are flat
(alongside type, not nested under payload): from, merged_branch,
new_main_sha, base (the resolved default-branch name), merged_at, and an
optional summary.
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.advanced-main","from":"supervisor","merged_branch":"feat/auth","new_main_sha":"a1b2c3d4e5f6","base":"main","merged_at":"2026-06-04T13:30:00Z","summary":"landed AuthClient"}'
The broker rejects a publish missing any required field (merged_branch,
new_main_sha, base, merged_at) with a 400-class error naming the field.
Each event carries a deterministic id derived from
merged_branch + new_main_sha + base + UTC-hour-bucket, so re-publishing the
same merge within the hour dedups to one logical event.
When You Depend on a Peer’s Work
When a peer’s agent.artifact lists work an agent depends on, the bundled
coordination skill tells the agent to treat it as a signal, not an
instruction to graft the peer’s commit: publish agent.blocked naming the
peer and what it needs, then continue on unblocked work (or wait if none
remains). Agents must not cherry-pick a peer’s commit into their own
branch — the supervisor later merges that commit from the peer’s own branch,
so the grafted copy leaves the dependent branch divergent and unmergeable.
Peer work arrives through the base branch instead, via the
agent.advanced-main discipline below.
When Main Advances
An agent.advanced-main event arrives on your normal
/messages/<agent_id> poll — no special subscription is needed. When the
base it names is one your branch depends on, react deliberately, never
automatically:
git fetch origin <base>to bring the new SHA local.git log HEAD..origin/<base> --onelineto see exactly what landed.- Decide between rebase, merge, or wait based on what changed and the state of your working set.
Agents do not auto-rebase on receipt: rebasing rewrites history and can
conflict with in-flight work, so it always requires judgment. If you do rebase,
commit or deliberately stash your work first so a rebase conflict can never wipe
uncommitted edits. (The bundled coordination skill teaches this discipline to
coding agents directly.)
Separately, when the operator sets [supervisor] branch_refresh = true, the
supervisor’s unattended drive loop may itself rebase an idle, clean worker
branch onto the default branch after a merge. The worker is then told its HEAD
moved by an agent.feedback tagged [branch-refresh] — never by
agent.advanced-main. See
Supervisor → Branch refresh.
Polling for Messages
Agents poll for messages from other agents using cursor-based pagination. The since parameter is a sequence number – the broker returns only messages with a sequence greater than the given value.
# First poll -- get all messages
curl -s "$GIT_PAW_BROKER_URL/messages/feat-auth?since=0"
The response includes a last_seq field. Pass this value as since on the next poll to get only new messages:
# Subsequent poll -- only new messages since last check
curl -s "$GIT_PAW_BROKER_URL/messages/feat-auth?since=42"
This cursor-based approach is lossless – no messages are missed between polls, regardless of timing.
Agents can use the bundled helper instead of a raw curl for the same read — .git-paw/scripts/broker.sh poll [since] issues GET /messages/<agent-id>?since=<n> and prints the returned messages (see Broker helper).
Waiting for Events
/messages/<agent_id> is one agent’s inbox. GET /log is the whole session’s
message log, and it is the read surface for anything that watches the session
as a whole — a supervisor, an external orchestrator, a monitoring script.
curl -s "$GIT_PAW_BROKER_URL/log?since=0"
The response is {"entries": [...], "last_seq": N}. since is the same
cursor the inbox poll uses: the broker returns only entries whose sequence is
greater than the value you pass, and last_seq is the cursor to send next.
An empty response reports last_seq equal to the since you sent, so a
reader’s cursor never rewinds and never has to re-read the log from the start.
(Before v0.18 an empty response reported 0, which rewound every reader that
trusted it — this is the one behaviour change in the response.)
The broker epoch — reset your cursor when it changes
Sequence numbers are per-broker and restart at zero every time a broker
starts, which a git paw pause + git paw start cycle does. Because a
cursor never rewinds, a reader that kept its old cursor across a restart
would sit above everything the new broker appends: it would see nothing,
forever, and time out every long poll while the session worked normally
around it.
Every GET /log response therefore carries an epoch identifying the
running broker instance (its start time in Unix milliseconds), and so do
sweep.sh events and wait_for_events:
{ "entries": [ ... ], "last_seq": 57, "epoch": 1761500000123, "wave": { ... } }
Record the epoch alongside your cursor, and whenever it differs from the
one you recorded, send since=0 on your next request — the entries you
have already seen belong to a broker that no longer exists, so re-reading
from the start is correct and happens exactly once per restart. The first
epoch you see needs no reset; there is no earlier cursor to invalidate.
git-paw’s own in-tree readers (the drive loop’s observer, the MCP tool)
already do this.
The long poll
Add wait=<seconds> and the broker holds the request open until there is
something to return:
# Block for up to 30s, or return the moment an entry newer than 42 is appended
curl -s "$GIT_PAW_BROKER_URL/log?since=42&wait=30"
waitis whole seconds and is capped at 60. A larger value is clamped; a non-numeric value is rejected with HTTP400.- With
waitand nothing newer thansince, the request is held until an entry is appended or the wait elapses — whichever comes first. A wait that elapses with nothing new returns{"entries": [], "last_seq": <since>}. - Without
wait, the request returns immediately, exactly as before.
This is what makes an external reader event-driven rather than capture-diff polling. A reader that samples pane output on a timer silently misses anything that starts and finishes between two samples; a reader that blocks on the cursor sees every appended entry exactly once, whatever the timing.
The bundled helper verb
The supervisor-side helper wraps both reads in one call:
.git-paw/scripts/sweep.sh events <since> [wait]
wait defaults to 30 seconds. The verb calls GET /log?since=&wait= and
GET /status, and prints one JSON object to stdout:
{ "entries": [ ... ], "last_seq": 57, "epoch": 1761500000123, "wave": { ... } }
It is strictly a read: it sends no keystroke and publishes nothing. With no
reachable broker it prints an object carrying "no_session": true and exits
0, so a caller never has to distinguish a crash from a session that is not
running. It is granted read-only by
git paw setup-cli <CLI> --allowlist.
An MCP-speaking client reaches the same surface through the
wait_for_events tool.
Checking Overall Status
The /status endpoint returns a summary of all agents and their latest state:
curl -s "$GIT_PAW_BROKER_URL/status"
Wave staleness
Per-agent heartbeats answer “is this agent alive?” They do not answer “is this
wave getting anywhere?” A session can sit on an unanswered permission prompt
for days with every pane alive and every agent reporting working. So
/status also carries a wave object describing the session as a whole:
"wave": {
"last_progress_unix_secs": 1761500000,
"idle_seconds": 11520,
"stall_threshold_seconds": 1800,
"stalled": true
}
What counts as progress is deliberately narrow — exactly two things:
- A wave phase advances. An
agent.statuswhosestatusorphasediffers from that agent’s previously recorded values, or anyagent.artifact,agent.verified,agent.feedback, oragent.advanced-main. - A branch tip moves. The broker’s filesystem watcher sees an agent
worktree’s
HEADcommit change.
What does not count, however much of it arrives:
- a repeated identical
agent.status— whatever itsmodified_filesset; - uncommitted file churn;
agent.question, including the unattended drive loop’s escalation of a live permission prompt and its re-escalation of the same prompt;agent.learning,agent.blocked,agent.intent,agent.answer, and the broker’s ownsupervisor.verify-nownudge;- an
agent.statuscarrying one of the stuck-detection helper’s synthetic alert phases —stuck-on-prompt,stuck-stream-timeout,context-bloat,no-progressorblocked-on-supervisor. Each is a report about a wave that is not moving, so counting it would let the detection of a stall reset the stall clock;no-progresssays as much in its own name. A status moving an agent back out of one of those phases is an ordinary phase advance and does count.
That exclusion list is the point: those are precisely the messages that keep arriving while a wave sits on an unanswered prompt, so counting them would make the signal report healthy exactly when it matters most.
A broker that has accepted no message at all reports its own start time as the
last progress. stalled is true when the threshold is non-zero and
idle_seconds exceeds it.
The threshold is the [broker] wave_stall_minutes key — default 30
minutes, 0 to disable the flag while still reporting idle_seconds. See the
configuration reference. Thirty minutes
sits just above the unattended drive loop’s 25-minute heartbeat, so a wave
that is merely between heartbeats is not reported as stalled.
The same object is surfaced by
git paw status (human and --json),
the dashboard status line, the MCP
tools get_session_status and wait_for_events, and
sweep.sh events — so a waiter whose long poll times out with no entries
still learns that the wave has stalled.
Multi-Repo Considerations
Each git-paw session runs its own broker. If you have multiple repos running sessions simultaneously, each needs a unique port:
# In repo-a/.git-paw/config.toml
[broker]
enabled = true
port = 9119
# In repo-b/.git-paw/config.toml
[broker]
enabled = true
port = 9120
git-paw enforces this: a broker reports the repository it serves on GET /status, and a session that finds another repository’s broker on its port is refused with an error naming that repository and telling you to set a distinct [broker] port. See Multi-repo port assignment.
The default port is 9119. The broker binds to 127.0.0.1 (localhost only) by default — the [broker] bind setting — and should never be exposed to the network.
Automatic Conflict Detection (v0.5.0)
When supervisor mode is active, the broker runs an in-process conflict
detector that auto-emits agent.feedback (tagged [conflict-detector] in the
errors array) and, for unresolved in-flight conflicts, escalates to the
supervisor inbox via agent.question. Three failure shapes are detected:
- Forward conflicts — two agents publish
agent.intentwith overlappingfiles. Both receiveagent.feedbacklisting the other agent and the overlapping paths. Toggle with[supervisor.conflict] warn_on_intent_overlap. - In-flight conflicts — two agents’
agent.statusmodified_filessets overlap on the same file. Both receiveagent.feedback. The broker waits[supervisor.conflict] window_seconds(default 120) for one side to stop touching the file; if both keep modifying it, the detector escalates a true collision to the supervisor inbox viaagent.question(an overlap whose declared regions are disjoint is downgraded to an informationalagent.feedbackinstead). - Ownership violations — an agent’s
modified_filesincludes a file outside its own activeagent.intentand inside another agent’s active intent. The violator receivesagent.feedback; if[supervisor.conflict] escalate_on_violation = truethe supervisor inbox also receives a follow-upagent.question. The shared spectasks.mdevery agent ticks its own line in is exempt.
See the Conflict Detection chapter for the full
walkthrough — failure shapes, the [conflict-detector] tag, supervisor inbox
routing, and the configuration knobs.
Audit Trail
The broker appends every message to a <session_name>.broker.log file in git-paw’s session-state directory (~/Library/Application Support/git-paw/sessions/ on macOS, $XDG_DATA_HOME/git-paw/sessions/ — default ~/.local/share/git-paw/sessions/ — on Linux), outside the repository. Each line is plain text — [<seq>] <unix-seconds> [<agent_id>] <message summary> — and the file is flushed every 5 seconds, giving an audit trail of agent communication. For the full JSON of every message, query the broker’s GET /log endpoint (see Waiting for Events).
The log file is automatically cleaned up by git paw purge.
Working Heartbeat
The broker’s filesystem watcher publishes agent.status whenever a worktree’s
git status changes, which keeps the dashboard’s last_seen timestamp fresh during
active editing. The watcher cannot observe read-only tool uses (file reads,
greps, searches), permission-prompt waits, or LLM-only deliberation between tool
calls — so a long read-heavy investigation looks stuck on the dashboard even
though the agent is making progress.
To bridge that gap, the embedded coordination skill instructs agents to publish
a lightweight agent.status heartbeat every 5 tool uses while actively working,
through the bundled broker helper, which publishes
{"status":"working","message":"<text>","modified_files":[]}:
.git-paw/scripts/broker.sh --agent feat-auth status "reviewing auth tests"
The heartbeat reuses the existing agent.status shape — no new wire format is
introduced. The broker merges heartbeats with watcher-driven updates without
conflict.
Activity phrases
A status payload may also carry an optional activity field — a short phrase
naming what the agent is doing right now:
curl -s -X POST http://127.0.0.1:9119/publish \
-H "Content-Type: application/json" \
-d '{"type":"agent.status","agent_id":"feat-auth","payload":{"status":"working","activity":"running the test suite","modified_files":[]}}'
The field is optional and backward compatible: a payload that omits it deserializes and re-serializes exactly as before, so existing agents and stored messages are unaffected. When present, the dashboard’s Broker log panel renders the phrase alongside the status. Because the panel also collapses consecutive identical heartbeats, a phrase that changes as the work moves along keeps each phase on its own row.
Prefer Native Read Tools Over Shell Pipelines
The bundled coordination skill teaches agents to prefer their CLI’s own native file-read and search facilities over a shell text-extraction pipeline (a command that pipes output through line-slicing or field-extracting utilities) when inspecting source. Two costs motivate this:
- The approval stall — a shell pipeline typically requires a permission approval before it runs, so the agent waits on a prompt every time.
- The dead grant — a durable approval is keyed on the exact command string, so the next inspection (a different line range, a different search term) never matches it, and the allowlist accumulates entries that exist only to be ignored.
The same mechanic governs scratch files: when an agent captures a command’s output across repeated iterations, the skill tells it to reuse one scratch path rather than writing a fresh numbered file each time, since a durable approval keyed on a path does not carry over to a new filename.
The guidance is deliberately CLI-agnostic — it names no specific CLI, tool, or vendor — so it holds for any CLI git-paw launches.
Commit Cadence
The bundled coordination skill teaches a per-group commit cadence. When a
change has an OpenSpec-style tasks.md with numbered groups (## 1.,
## 2., …), the agent commits after every - [ ] item in a group is
- [x] — one group, one commit (by default) — before starting the next
group.
The skill bounds uncommitted work to roughly ten files at a time. If a
single group exceeds that mid-implementation, the agent splits into multiple
commits using a (part N of M) suffix:
close per-scenario coverage gaps (part 1 of 2)
close per-scenario coverage gaps (part 2 of 2)
Each commit must be a releasable unit — it builds and passes its own gates
on its own, not a checkpoint of half-finished work. When the agent needs to fix
the commit it just made (a typo, a missed file) and that commit has not yet
been verified, the skill tells it to fold the fix in with git commit --amend
rather than land a separate fix typo micro-commit. It must not --amend an
already-verified commit or an earlier group’s commit — --amend applies only to
the most-recent, not-yet-verified commit. This keeps the history as clean,
verifiable units and avoids the manual release-time squashes earlier cycles
needed (148→10 in v0.6.0, 4→1 in v0.7.0).
The bundled skill does not mandate, default to, or recommend a
commit-message format. Message format is a per-project convention, so the
skill defers entirely to the host project’s injected AGENTS.md (subject
style, any prefix or scope convention, any “no AI-assistant trailer” rule), and
any commit example it shows — like the (part N of M) split above — uses a
format-neutral subject. git-paw’s own repo uses Conventional Commits, but only
via git-paw’s own AGENTS.md, never via the skill the binary exports to other
projects.
Per-group cadence protects against agent crashes, conflict mediation, and
/clear resets losing unbounded work, and it maps cleanly to the post-commit
hook’s agent.artifact { status: "committed" } event sequence the supervisor
consumes during verification.
Terminal Action — Commit Then Publish, Never Archive
The bundled coordination skill defines the coding agent’s terminal action as:
- A commit. The post-commit git hook auto-publishes
agent.artifact { status: "committed" }with the committed file list. For code changes this is the canonical “done” signal. - A manual
agent.artifact { status: "done" }(rare). Used only for code-less tasks or to announce namedexportspeers depend on.
The skill is explicit that the coding agent SHALL NOT invoke
/opsx:verify <change-id> or /opsx:archive <change-id> — both are
off-limits for the coding agent and are the supervisor’s job:
- Verification runs the supervisor’s five-gate framework (testing → regression
→ spec audit → doc audit → security audit) against the committed branch.
Self-verification by the coding agent bypasses gates and produces a
premature
agent.verifiedthe supervisor never reviewed. - Archiving happens on the release branch during the supervisor’s cherry-pick
- merge flow, not on the agent’s feature branch. Archiving from a feature branch leaves the change directory deleted on an unmerged branch and produces confused history.
The skill frames this positively as a stand-by-after-commit protocol: once
the final commit lands, the agent publishes the terminal signal and then waits
— it does not reach for verify/archive. While standing by it waits for one of
three supervisor messages: agent.verified (work passed — pick up the next
task), agent.feedback (fix the listed errors and re-publish agent.artifact),
or a further agent.intent (new scope to pick up). This is the actionable
counterpart to the role-gating forbidden-commands rule — what to do instead of
self-verifying. On the supervisor side, that post-commit agent.artifact is the
cue for the supervisor (not the agent) to run /opsx:verify and /opsx:archive.
This is a paw-specific rule for the bundled coordination skill. Single-agent
workflows that self-verify can override the rule via the standard skill
resolution chain (a user override at
<config_dir>/git-paw/agent-skills/coordination.md wins over the bundled
default).
Identifier Forms — Branch vs agent_id
Two related forms of an agent identifier appear throughout the broker protocol:
- Branch name — the original git ref (e.g.
feat/no-supervisor-flag). Used ingit checkout,git worktree,git push, and any other git command. agent_id— the dashed slug form (e.g.feat-no-supervisor-flag). Used in every/publishenvelope’sagent_id, every/messages/<id>URL, and payload fields that name another agent, such asfrominagent.blocked.
agent_id is the slugified form of the branch name. The conversion (named
slugify_branch in the source) lowercases the input, replaces every character
outside [a-z0-9] with - — underscores included — collapses runs of -,
trims leading and trailing -, falls back to the literal agent-unnamed if the
result is empty, and prefixes a single-word result with agent-
(hotfix → agent-hotfix). Every output therefore matches the agent_id
pattern /publish enforces, and no branch can derive the reserved id
supervisor.
A branch name may keep _; its agent_id never does. Branch names git-paw
derives from spec text keep underscores — a Spec Kit task naming ROOM_SETUP.md
produces the branch task/t001-create-room_setup-md — because _ is legal in a
git ref and the branch is durable state that recovery and --resume use to look
up work. Only the broker’s id grammar forbids _, so the mapping happens at the id
boundary: that branch publishes as task-t001-create-room-setup-md.
Match the form to the context: dashed agent_id in any JSON going to or coming
from the broker; slashed branch name in any shell command involving git.
Stash Hygiene in Worktrees
When multiple worktrees run side-by-side, every worktree shares the same
underlying git stash list. A git stash pop invoked without inspection can pop
an entry created by a different worktree, conflict with your in-progress
changes, and wipe work. The embedded coordination skill teaches agents three
rules:
- List before pop —
git stash listfirst; inspect every entry’s branch label and timestamp. - Inspect before pop —
git stash show -p stash@{N}to read the patch contents of the specific entry before popping. - Pop only your own — only pop entries you authored on the current
worktree. If authorship is uncertain, leave the stash alone and escalate via
agent.question.
Blind git stash pop is a data-loss pattern in a multi-worktree session and is
not recommended.
Supervisor Acknowledgement of agent.question
When an agent publishes agent.question, it blocks at its prompt waiting for a
typed reply. A blocked agent does not poll its inbox for agent.answer
responses, so a supervisor that only publishes agent.answer to the broker
will see its answer recorded on the dashboard while the asking agent stays
blocked indefinitely.
The supervisor skill therefore instructs supervisors (both human and LLM) to
both publish agent.answer (the non-error reply shape — agent.feedback
stays reserved for corrective errors) and send the answer text to the
asking agent’s tmux pane via tmux send-keys. The MCP publish tools include
no inbox read, so the dual write applies to agents booted with the
MCP tool form too.
Spec Kit Consolidated Worktrees
When git-paw drives a Spec Kit project (.specify/specs/<feature>/), each feature’s current phase decomposes into multiple worktrees:
- One worktree per
[P]-marked task (branch prefixtask/). These are parallelisable. - One consolidated worktree per non-
[P]task group (branch prefixphase/). Non-[P]tasks share files or context, so a single agent works through them sequentially.
The embedded coordination skill picks up on the branch prefix:
task/<task-id>-<slug>branches: the agent runs the standard “before/while editing” coordination pattern for a single task.phase/<feature>-<phase-slug>branches: the agent:- Works through the listed tasks in
tasks.mdorder. - Flips
- [ ]to- [x]for each completed task in the worktree’stasks.md. The writeback can be a separate commit or bundled with the task’s code change. - Publishes
agent.intentfor the union of files across the next 1–2 tasks (with a generous TTL) rather than one publish per task. - Publishes
agent.artifactwithstatus: "done"only when every listed task shows- [x]intasks.md. Partial completion is not “done”.
- Works through the listed tasks in
When tasks.md is the merge-conflict surface between worktrees, git’s line-level merge handles per-task checkbox flips automatically. A checkbox writeback to the shared tasks.md is never reported as an ownership violation — the conflict detector exempts that file — while forward and in-flight overlap detection still apply to it.
Workflow phases
The bundled coordination skill structures an agent’s editing work into two phases that mirror the skill’s “Before you start editing” and “While you’re editing” sections.
Before you start editing
Before touching any file, the agent:
- Reads the spec or task description in full to understand the scope.
- Publishes
agent.intentlisting the specific files it plans to modify, a one-line summary, and a TTL in seconds (default900= 15 minutes). This advertises ownership to the broker conflict detector so forward conflicts are caught before any edit lands. - Polls its inbox once for warnings or overlapping peer intents — not a busy loop, a single poll.
- Decides on overlap: if a peer’s intent already covers the same
files, the agent picks among wait (peer’s TTL is short, work is
small), split (narrow the file list to avoid overlap, re-publish
agent.intentwith the reduced scope), or escalate (publishagent.questiondescribing the overlap so the supervisor or human can decide). If no overlap is reported, the agent proceeds to edit immediately — there is no explicit go-ahead to wait for.
While you’re editing
Once editing is underway, the agent keeps the intent honest and asks rather than racing:
- Re-publish intent on scope growth. If the in-progress work touches
files that were not in the original
agent.intent, the agent re-publishesagent.intentwith the expandedfileslist before touching the new files. The re-published intent replaces the previous claim for downstream consumers. - Question on peer overlap. If a peer’s
agent.intentarrives in the inbox naming a file in the same module the agent is editing, the agent sendsagent.questiondescribing the overlap and pauses edits on the contested file. Silently racing the peer to a commit is forbidden.
The agent MUST NOT:
- Perform pairwise check-ins on every change — the broker is not a chat channel and peers are not waiting for status pings.
- Wait for an explicit go-ahead from peers when no conflict signal exists — silence from the broker means “no overlap detected”, not “permission pending”.
- Block on broker silence — if
agent.intentpolling returns no overlap, the agent proceeds.
Declaring regions (v0.6.0)
By default an intent claims whole files, so the detector warns any two agents
who name the same path. When several agents collaborate on different parts of
one shared file, that whole-file warning is noise — and noisy warnings get
dismissed, taking real overlaps with them. From v0.6.0, each files entry MAY
be an object that declares the regions the agent intends to touch, and the
detector then warns only when the declared regions actually intersect.
A files array may mix bare-path strings (file-level intent, the v0.5.0
shape) and region objects freely. Four region kinds are recognised:
function—{ "kind": "function", "name": "<symbol>" }class—{ "kind": "class", "name": "<symbol>" }block—{ "kind": "block", "anchor": "<heading or landmark>" }for prose or config filesrange—{ "kind": "range", "start_line": N, "end_line": M }when no symbolic name fits
Worked example. Two agents both intend src/auth.rs, but feat-auth is
hardening validate_token while feat-session reworks refresh_session:
# feat-auth
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.intent","agent_id":"feat-auth","payload":{"files":[{"path":"src/auth.rs","regions":[{"kind":"function","name":"validate_token"}]}],"summary":"harden token checks","valid_for_seconds":900}}'
# feat-session
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.intent","agent_id":"feat-session","payload":{"files":[{"path":"src/auth.rs","regions":[{"kind":"function","name":"refresh_session"}]}],"summary":"rework session refresh","valid_for_seconds":900}}'
Because the declared functions differ, no forward-conflict warning fires —
the two agents proceed in parallel on the same file. Had both named
validate_token, the warning would fire and name the intersecting function.
Four rules govern detection:
- Both sides must declare regions for region-level matching. If either side omits regions (a bare path string), the detector conservatively falls back to a file-level warning — the v0.5.0 safety net.
- Name matching is normalized. Region names are compared after
case-folding, trimming, collapsing separator runs (space, underscore,
hyphen) to a single form, stripping a trailing
(), and stripping a leading declaration keyword (fn,def,function,class) — so spelling variants of the same symbol intersect:validate_tokenmatchesValidate Token()andfn validate_token. Genuinely distinct symbols (foo_barvsfoobar) stay distinct. - Cross-kind comparisons are conservative. A
function/class/blockregion compared against arangealways intersects (the detector can’t resolve a symbol to line numbers without parsing source). Named regions of different kinds — sayfunction foovsblock foo— intersect when their normalized names match, because they almost always describe the same construct through different lenses. Both shapes carry a hint noting the comparison was conservative. - Don’t manufacture narrow regions to dodge a warning. A region you don’t really own hides a collision that resurfaces later as a merge conflict.
Because matching is string-based, three declaration rules keep intents detector-comparable:
- Use the canonical source spelling — declare
validate_token, not a paraphrase liketoken validation logic; the detector cannot equate two paraphrases of the same symbol. - Declare all the regions the work touches, including shared constant blocks, import sections, and asset files — not only the headline function.
- Re-publish
agent.intentwhen scope grows mid-task, before touching a region that was never declared.
This guidance is taught to agents by the bundled coordination skill
(assets/agent-skills/coordination.md, the “Declaring regions” section),
which is the authoritative source.
Context budget
After the “While you’re editing” discipline, the coordination skill teaches agents to manage their own context window so they don’t hit an opaque “context length exceeded” failure mid-task and lose uncommitted work. The guidance has four parts:
- Residual-budget heuristic. After the boot block, skill prose, and governance docs load, an agent aims to keep at least ~60% of the model’s context window free for task work. This is a heuristic target judged by the agent, not a config field.
- Three named moments to compact / clear / summarise, in priority order: after each spec scenario completes (compact), when the working set grows past ~40% of the window (compact), and when switching between sub-tasks that don’t share state (clear).
- Commit before you compact. Every compact / clear / summarise operation
is preceded by a commit or an
agent.artifactpublish, so reducing context is never lossy. - Proactive context-bloat flagging. When an agent’s pane shows a
/clear to save <N>k tokenshint withNat or above[supervisor] context_bloat_threshold_k(default 250), the supervisor flags it with a syntheticagent.statuscarryingphase: "context-bloat"; the agent commits (or publishes anagent.artifact) first, then clears or compacts.
The bundled coordination skill (assets/agent-skills/coordination.md, the
“Context budget” section) is the authoritative source for the exact
heuristics, the named moments, and the per-CLI compact/clear mechanism table
(claude, claude-oss, and a generic fallback). This chapter only
summarises it.
Supervisor
This chapter mirrors the user-facing prose for the bundled orchestrator
skill (assets/agent-skills/orchestrator.md, exported to
.git-paw/skills/orchestrator/SKILL.md; supervisor remains an alias of its
name) — the doctrine the orchestrator runs against in supervisor mode. Sections
here document the same rules the skill teaches, so users reading the mdBook can
understand the orchestrator’s behaviour without opening the skill file
directly.
For the launcher-level “how do I start supervisor mode” walkthrough, see Quick Start: Orchestrator Mode. For the broker-side message contract the supervisor exchanges with coding agents, see Agent Coordination.
Which CLI the coding agents launch with
A supervised launch given neither --branches nor a spec selection runs the
same interactive selection flow as git paw start: the branch picker, the
assignment-mode prompt, and the CLI picker (uniform, or one picker per branch).
Each coding agent then launches with the CLI that selection mapped to its
branch — the uniform choice, or that branch’s own per-branch choice.
default_cli only pre-selects an entry in those pickers; it never skips them.
The session-wide fallback — --cli, else default_cli, else
[supervisor].cli — applies only to launch paths that show no CLI picker,
such as --branches without --cli; when none of the three is set the
launch fails, naming --cli and default_cli. Spec-driven branches keep
their own resolution chain (see Spec-driven launch).
Session layout: the orchestrator runs outside the session
A supervised session holds the dashboard at pane 0, as a full-width top
row, and one coding agent per pane from pane 1, row-major below it (at
most five per row). No pane runs an orchestrator CLI. The orchestrator is your
own agent session, run outside tmux at the repository root. The launch
re-renders the exported orchestrator skill
(.git-paw/skills/orchestrator/SKILL.md, with the gate commands and the
spec-path doctrine) and writes the session context — the session name, the
broker URL, the helper paths, any [governance] documents and, under
--unattended, the drive-loop directive — into the gitignored sidecar
.git-paw/AGENTS.local.md, and prints both paths with the attach hint. The
orchestrator reads them and drives the run through the bundled helpers
(.git-paw/scripts/sweep.sh, broker.sh) or the MCP server. See
Driving the session from your own agent. Its
roster row (supervisor) appears on the dashboard once it publishes its own
agent.status; the launcher never publishes on its behalf.
[supervisor].cli (else default_cli) names the orchestrator’s CLI in that
context and is the coding agents’ last CLI fallback; no orchestrator CLI is
needed to launch. It is also the CLI the --unattended drive loop runs
headlessly for a judgment call (see
Headless hand-off).
[supervisor].approval shapes no pane command — it is the approval level of
those headless invocations.
A live session started by an earlier release (orchestrator CLI at pane 0, dashboard at pane 1, agents from pane 2) keeps working until it ends: every command derives the agent panes from the session’s recorded dashboard index, and nothing types into that session’s first two panes. Recovering it rebuilds the layout above.
Driving the session from your own agent
The orchestrator is whatever agent session you already use — any agent CLI, at the repository root, outside tmux. git-paw gives it three things:
| File | Written by | What it holds |
|---|---|---|
.git-paw/skills/orchestrator/SKILL.md | git paw init, refreshed by git paw start | The orchestrator skill (the procedure: five gates, spec audit, merge orchestration, stuck detection, routing), rendered with your [supervisor] gate commands and configured spec format. It carries no session value, so one file serves every session. |
.git-paw/AGENTS.local.md | git paw start | The session context: session name, broker URL, helper paths, the discovery file, [governance] documents, the drive-loop directive under --unattended, and a pointer to SKILL.md. The only file carrying this session’s values. |
AGENTS.md pointer section | git paw init | A short section between <!-- git-paw-orchestrator:start --> and <!-- git-paw-orchestrator:end --> telling any agent that reads your root AGENTS.md that, when a git-paw session is active, it is the orchestrator and should read the two files above. git paw stop and git paw purge leave it in place. See AGENTS.md Injection. |
Both .git-paw/skills/ exports (the orchestrator skill and the cli-setup
skill beside it) are in the standard skill-directory layout, so a skill-aware
CLI can load them directly; copy or symlink them into a vendor skill directory
if your CLI needs that. Over MCP, get_skill serves the same skill under the
name orchestrator (or its alias supervisor).
The surfaces it uses.
- Follow the session by blocking on the broker’s event cursor — the MCP
wait_for_eventstool or.git-paw/scripts/sweep.sh events— carrying the returned cursor forward, and inspect when an event or astalledwave says to.get_session_status,sweep.sh statusandsweep.sh inboxread the roster and the orchestrator’s inbox. A sampled pane capture can miss a prompt that was answered and replaced between two samples, so capture (sweep.sh capture <pane>) is for inspection, not detection. - Publish verdicts only through the credential-carrying helpers:
sweep.sh verified,sweep.sh feedback-gateandsweep.sh conflict-ack(in the root session, the MCPpublish_verifiedandpublish_feedbacktools do the same). The broker refuses a verdict without the session’s authority credential. - Answer and announce with
sweep.sh answer <agent> <text> [re]and, after each merge,sweep.sh advanced-main <merged-branch> <summary>. Its own questions and main-side intents go throughbroker.sh --agent supervisor question|intent. The skill contains no raw HTTP request. - Run the session from the root session with
git pawcommands or the MCPstart_session,pause_session,stop_sessionandpurge_sessiontools.
Pairing with --unattended. git paw start --unattended adds the
in-process drive loop: it keeps the panes moving and escalates every live
permission prompt to the broker, approving none. The orchestrator reads those
escalations first each cycle, relays each prompt to you in its own session, and
runs sweep.sh approve <pane> only on your explicit instruction. It merges as
soon as every expected agent is verified and escalates only on a conflict, an
exhausted correction cycle or a regression (see
Merge orchestration). If the loop exits on its
heartbeat, re-run git paw start --unattended to re-attach a drive loop to the
live session. The driving session can itself be remote-controlled from a
phone — see Remote Control.
Unattended mode (--unattended)
git paw start --unattended runs a supervisor wave to completion with no
human in the seat. Without it, git paw start --supervisor builds the tmux
session and returns immediately with an attach hint, expecting a human (or a
throwaway pane-scraper script) to watch the panes, answer permission prompts,
and notice when the work is done. --unattended folds the driving half of
that operator loop into the tool itself.
--unattended engages supervisor mode (you do not also need --supervisor),
then — instead of returning — runs an in-process drive loop in the
foreground git paw start process. It is designed for detached operation: it
does not attach a terminal and does not replace your shell with an interactive
supervisor CLI. It is mutually exclusive with --no-supervisor.
--unattendedapproves nothing. The loop detects live permission prompts and escalates every one of them for a human decision; it never answers one. For a run that genuinely does not stop for prompts, the decision has to be made before the prompt exists — in your agent CLI’s own allow rules, or by launching the agents at[supervisor] agent_approval = "full-auto"inside the documented sandbox. See Agent permissions.
Each poll (~15 seconds) the loop:
- Sweeps every coding-agent pane — and only those. It enumerates panes and
resolves each to its agent by
pane_current_path(never by pane index, which drifts), capturing each coding pane with its owncapture-panecall. The dashboard, and any pane no agent worktree owns, is never captured, escalated or typed into. - Acts only on a live prompt — the prompt’s structural markers (numbered
option glyphs and/or the
Do you want to …question, with theEsc to cancelfooter) anchored on the modal block itself, searching upward from the end of the capture and tolerating up to 13 non-blank trailing lines of non-prompt content below it (a task list, a status strip, a progress footer), so a CLI that renders chrome under its input area doesn’t hide a live modal — and so a prompt offering a “don’t ask again” option is detected too. Beyond that true end, liveness requires the modal’s actual shape (the footer anchoring a line, with an option list nearby), not a bare mention of the footer text in narration or tool output. Prompt-like text scrolled well past that bound is ignored. - Escalates every live prompt, whatever the command is. The loop makes no
judgement about the prompted command —
ls -laandrm -rf /are handled identically. The prompt is recorded on the broker as anagent.questionreview item (fromsupervisor), written to the manual-decision log, captured as a learning, and listed in the exit summary. No keystroke is sent, to that pane or any other: the orchestrator outside the session reads the review item and relays it to the human. - Submits a directive left buffered on an idle pane. A pane showing no
live prompt and no mid-response marker, whose input box still holds
unsubmitted text, has a stranded directive: the CLI swallowed the first
Enterinto its paste buffer. The loop sends a follow-upEnterfirst; if the text survives that, it runs the same clear-and-recover sequence nudges use (see below), re-typing the text read from the whole input box. A paste-buffer placeholder is never cleared — it is escalated instead. Neither the live-prompt scan (there is no prompt) nor the ~25-minute stuck detector would otherwise surface that stall. A pane that is actively generating is never touched. - Keeps the wave moving while a prompt waits. An escalation is
non-blocking: one agent parked on a prompt never freezes the others, and the
loop does not wait for anyone to answer. Repeated escalations are
deduplicated per
(agent, command-shape)within a five-minute window, so a prompt still unanswered after that window is re-escalated as a reminder rather than every fifteen seconds. A prompt nobody ever answers does not, on its own, end the run — the wave ends on completion or the heartbeat. - Leaves judgment calls on the broker — see The loop is a pump, the supervisor is the brain — and hands a question, a merge decision or a correction-exhausted branch to a headless one-shot orchestrator when one resolves.
- Re-engages a gate-failed worker, when
[supervisor.correction] auto_loopbackis enabled — closing the verify↔fix cycle without a human hop.
The loop keeps polling until one of:
- Completion — a terminal PASS/FAIL verdict is signalled, or every agent’s tasks are checked complete.
- Stuck / bloat — the stuck-detection signal fires.
- Heartbeat — about 25 minutes pass without progress, so you are
re-engaged with a status summary rather than the loop running forever.
Progress is a coding agent’s branch tip moving (a new commit) or its phase
advancing (e.g.
working→committed→verified/done); the interval is measured from the last progress the loop saw, not from launch. Nothing the loop does itself counts — its escalations, its reminder re-escalations of an unanswered prompt, the learnings it records, its own rebases of a branch after a merge — and neither does changed pane output or a repeatedworkingstatus. So a wave that keeps committing is never cut off at 25 minutes, while a wave whose agents all sit on unanswered prompts still exits one interval after its last progress. Before exiting, the loop takes one fresh status look, so adonepublished during a long tick still ends the run ascompleted. - Session torn down — the loop’s own session instance is purged, stopped, or replaced (see Session-instance lifecycle below).
On exit it prints a summary: the outcome (completed /
escalated-for-review / stuck / heartbeat / session-torn-down), each
agent’s resolved final state and its merged/unmerged status, the deduped
list of prompts escalated for human review, a line stating that the
orchestrator runs outside the session and its CLI is not monitored by the
loop, and pointers to the broker log and captured learnings. Because there
is no human to hand-write findings, the loop self-captures qualitative
learnings during the run and once more at wind-down.
A heartbeat, stuck or interrupted exit ends the loop, not the session —
the panes, their conversations and their worktrees are all still there. Re-run
the same git paw start --unattended to drive it again; see
Re-attaching a drive loop to a live session.
Each agent’s final state is resolved from the most-progressed lifecycle phase
ever observed for it during the run — not simply the last /status row — so a
generic working heartbeat published after a verified/done artifact event
can never make a finished worker look unfinished in the summary.
The merged/unmerged roll-up is ref-derived, never inferred from a worker’s
reported status. A worker reaching verified/done says nothing about
whether its branch ever reached the default branch — reporting a wave
completed on the strength of worker status alone was the failure mode a
0.16.0 external dogfood surfaced: the summary read completed, 2/2 branches merged, zero escalations, while main was unchanged and both branches were
still ahead. At exit the loop asks git directly, once per coding branch,
whether the branch tip is an ancestor of the default branch
(git merge-base --is-ancestor); a comparison git cannot make (a deleted
branch, a bad ref) counts as unmerged — the fail-safe direction, since this
summary is the input to a destructive git paw purge. The wave-level roll-up
line (e.g. 2/2 branches merged) and each agent’s own line (<agent>: <phase> · merged / <agent>: <phase> · unmerged (N ahead of <default>, tip <sha>))
both come from this same ref check, and print for every outcome with coding
agents — a heartbeat exit gets the same truth, not only a completed one.
completed now implies every branch is merged. A wave that finishes with
any branch unmerged reads escalated-for-review instead, with one
[unmerged] escalation entry per unmerged branch, so “Escalations awaiting
human review” can no longer read 0 while work sits un-integrated — check
this list, not just the outcome word, before running purge.
CLI unavailability
A rate-limited or logged-out coding agent is an explicit non-success signal —
not a silent idle pane the loop waits out for up to 25 minutes. Each poll,
every coding-agent pane’s capture tail is also scanned for a
CLI-unavailability marker (a usage/session-limit phrase, an organisation
spend-limit phrase, or a login-loss phrase such as an expired/missing
credential). A match becomes a deduped cli-unavailable escalation that
quotes the matching line, and the loop keeps driving the rest of the wave; a
wave carrying one is never reported completed, and that worker’s branch
shows as unmerged in the exit summary regardless. Text on the dashboard —
whose broker log can quote a worker’s banner — is never scanned.
The scan tolerates the same bounded trailing region of non-prompt content the live-prompt gate does, so a banner is still found underneath chrome a CLI draws below it — current Claude Code’s persistent task list, a status strip, or similar — rather than only a fixed last few lines. A marker separated from the end of the capture by more than that bound still counts as scrolled away and is ignored, same as before.
Markers are resolved per pane, for its own CLI: a [clis.<name>] override of cli_unavailable_markers applies to
that CLI’s panes, falling back to the embedded Claude Code default (which
includes You've hit your session limit, an organisation org's monthly spend limit, an expired/missing login, and an invalid API key) when the CLI
has no override. The override resolves from that pane’s own broker-reported
CLI (the /status snapshot), so it only takes effect once that pane has
published at least one status — a pane’s first sweep of the run, before any
/status row exists for it, always falls back to the embedded default for
that one tick. git paw __prompt-profile --field cli_unavailable_markers
reads the exact same resolved field the loop does — it is a read path for the
bundled sweep.sh helper’s other detectors (stuck-shape, live-prompt) to
stay in sync with the Rust side, not a second detector: sweep.sh itself
does not consult cli_unavailable_markers or evaluate a pane for CLI
unavailability today.
A cli-unavailable escalation is deduplicated on the agent and the matched
marker — never on the banner line’s full text, which can carry a reset time
or countdown — so the same banner observed across sweeps is one escalation,
while two distinct banners on one agent (e.g. a session limit followed later
by a login failure) are two.
The orchestrator runs outside the session, so the loop has no pane to watch
for the orchestrator’s own CLI dying — every exit summary says so explicitly
(The orchestrator runs outside the session; its CLI is not monitored by the loop.) instead of implying coverage it does not have.
Nothing in an unattended session approves a prompt — the loop escalates and the human answers. The dashboard subprocess runs no approver either, attended or unattended; it renders the broker log and nothing more.
Re-attaching a drive loop to a live session
A drive loop ends while the session it was driving is still perfectly alive —
on the heartbeat, on a stuck signal, or because you interrupted it. To drive
that session again, re-run the same git paw start --unattended command in
its repository:
# Drove the wave the first time; drives it again the second time
git paw start --unattended --from-all-specs
The live session is neither relaunched nor forked. Launch-selection flags
(--branches, --specs, --from-all-specs, --cli) are never applied to a
session that already exists — attaching a new agent to a live session is
git paw add — and that is exactly what lets the
original launch command double as the re-attach command. No paw-<project>-2
is created.
What it does next depends on the session’s effective status:
| Status | What start --unattended does |
|---|---|
| Active | Binds a loop and drives the session as it stands. No pane is created, rebuilt or re-titled, no worktree is touched, and no boot block, prompt or keystroke is injected anywhere. |
| Paused | Revives the session exactly as git paw start revives it, then drives it. |
| Stopped | Recovers the session exactly as git paw start recovers it, then drives it. |
A revive or recovery that fails fails the command, without running a loop.
The terminal is never attached, even from an interactive shell — the drive
loop occupies the foreground process, exactly as it does on a fresh
--unattended launch. The command prints the session name and the
git paw attach line for watching it from another terminal.
In every other respect the re-bound loop is the same loop: the same cadence,
the same escalation, correction and heartbeat behaviour, the same exit summary,
and it approves nothing. It drives the session’s own agent roster, and
reaches the broker at the endpoint recorded for that session rather than
the one in your current configuration — so a [broker] port changed after
launch cannot point the loop at another repository’s broker, or at none.
Bare sessions are refused. A session started without supervisor mode
deliberately leaves its boot prompts unsubmitted for a human to send, and the
loop’s buffered-input recovery would submit them. start --unattended against
one exits non-zero naming the session, stating that only a session launched in
supervised mode can be driven unattended, and pointing at git paw attach for
driving it by hand. Nothing about the session is changed. A --dry-run
likewise binds nothing and is unaffected.
One drive loop per session
Two loops on one session would each nudge, submit buffered input and escalate
into the same panes, so at most one may drive a session instance at a time.
Every loop — whether from a fresh launch or a re-attach — holds a drive-loop
binding: a lease file at .git-paw/tmp/drive-loop.binding (gitignored,
beside the approval claims) naming the session instance and the loop itself. It
is refreshed every poll and released when the loop exits.
A start --unattended that finds a live loop already holding the binding
refuses before changing anything — no session, pane or worktree state is
touched, and no second loop runs. It exits non-zero with an error naming the
session, the process id holding the binding, and the binding file. Wait for
that loop to exit (or stop that process), then re-run the command.
A loop killed without releasing its binding does not wedge the next one. A
binding that stops being refreshed lapses after the staleness window — the
larger of four poll intervals and two minutes, and of the window the holder
itself recorded, so a loop polling more slowly than the caller is never
displaced while it is alive — and the next start --unattended simply takes
it. There is never a lock file to clear by hand. A loop that exits cleanly
releases its binding immediately, so a re-attach right after a heartbeat exit
binds without waiting at all.
Two start --unattended runs that reach a lapsed binding at the same instant
still produce exactly one loop: the binding is read, judged and rewritten
under an exclusive lock (.git-paw/tmp/drive-loop.lock), so the second runs
its check against the first’s result and refuses. That lock file is left in
place between runs; it is empty, and nothing needs to be done about it.
git paw stop revokes the session’s binding, and recovering a stopped session
supersedes any binding left behind for it. A loop that finds its binding
revoked or taken over exits session-torn-down on its next poll without acting
— see Session-instance lifecycle below.
Deleting
.git-paw/tmp/ends a running loop. The binding goes with it and the loop exits on its next poll. That is fail-safe rather than destructive: a loop that is not running approves nothing, so the panes go on waiting for a human exactly as they would have. Re-rungit paw start --unattendedto bind a new loop.
Upgrading mid-run. A drive loop started by a git-paw older than this one holds no binding, so a re-attach is not refused while it is still running — and you would end up with two loops driving the same panes. Stop that older
--unattendedprocess before re-attaching to its session.
Session-instance lifecycle
A loop binds to the specific session instance it began driving — not just the session name — and re-confirms on every tick, before it sweeps or sends anything, that it is still that instance’s driver. Two things must hold:
- The session receipt still describes the instance it bound to. The
receipt’s creation timestamp is the durable instance token. If the receipt is
gone, its
created_atno longer matches (a new session of the same name was started after apurge), or its status is nowstopped, the loop is done. - The drive-loop binding still names this loop.
git paw stoprevokes it, and a re-attach that recovers a stopped session supersedes it.
Either one failing exits the loop immediately — without sweeping panes or
sending any keystrokes — rather than continuing to drive whatever now occupies
that session name. This is what makes purge/stop safe to run against a
session still being driven unattended: the old loop notices on its very next
poll and gets out of the way instead of escalating prompts from a session it
no longer owns.
The binding covers the gap the receipt alone cannot see. Recovering a session
keeps its created_at, so a session stopped and recovered inside one poll
interval looks unchanged to the receipt check — and the loop bound to the
stopped instance would go on driving the recovered one. Revoking the binding on
stop is what tells it otherwise.
# Drive every discovered spec to completion, unattended
git paw start --unattended --from-all-specs
# Or a fixed branch set
git paw start --unattended --branches feat/auth,feat/api
Multiple feedback → fix → re-verify cycles per agent are normal and are not treated as a stuck signal — the loop lets agents iterate.
The loop is a pump, the supervisor is the brain
The drive loop is deliberately mechanical: it cannot decide anything. The orchestrator is the opposite — it can reason about anything — and it runs outside the session, so the loop never types into it. Every judgment call the loop meets is a broker review item, and nothing is injected into any pane:
| Judgment call | The broker record |
|---|---|
| A live permission prompt — to relay to the human, not to decide | the loop’s agent.question from supervisor |
| An ambiguous question | the worker’s own agent.question |
| A merge decision | the worker’s own committed/done agent.artifact |
| A non-converging branch | the loop’s correction slow / exhausted escalation |
The loop never waits on any of them: one call being deliberated cannot freeze
the wave, and every record stays on the broker until it is answered. The
orchestrator reads them there — through sweep.sh, the broker helper or the
MCP server — and so does a human reading the dashboard’s broker log.
A permission prompt is relayed, not decided. Deciding it from the pane text
would make the orchestrator a classifier — one reading untrusted output, and so
reachable by prompt injection. The orchestrator puts the prompt in front of the
human, naming the agent and the command, and runs sweep.sh approve <pane>
only when the human explicitly says to.
A nudge is sent reliably, not fired and forgotten. Typing text into a pane
and immediately following it with Enter is not enough: on a paste-aware CLI
the text can land in a paste buffer, and a bare follow-up Enter against a
stale input line is a proven no-op — it does not submit anything. So every
nudge — a worker re-engagement, and a stranded directive’s recovery — sends the
text, waits the CLI’s configured settle delay (the same
[clis.<name>].submit_delay_ms the boot prompt uses), then sends Enter as
a separate keystroke. Verification waits that same settle delay again, so the
pane has redrawn, before taking the check capture — and judges by the input
box’s content, never by comparing it against the sent text verbatim: a long
nudge wraps onto more than one visual row, and only its first row still
carries the prompt sigil, so an exact-equality check would misread a wrapped
line as already delivered. The text counts as still present when the input
box is non-empty and either starts with that first row or shows a
paste-buffer placeholder; it counts as submitted once the box is empty or the
pane has left its idle state. A line still present is recovered by clearing
it (Ctrl-U), re-typing the text read from the whole box, and sending
Enter again, up to a bounded number of attempts. A pane still wedged after
every attempt is escalated to the broker for human review and listed in the
exit summary’s escalations — deduplicated per pane, so a persistently stuck
line is reported once rather than on every ~15-second sweep. The whole send —
the first attempt through every recovery attempt — runs under the target
pane’s exclusive claim (see “One approver per pane, enforced” below), so a
recovery only ever clears a line the loop itself just put there, never text a
worker agent typed.
No nudge is sent once the loop is winding down. The wind-down/exit state is checked before dispatching any nudge on a tick, so a worker’s idle pane gains no further unsubmitted text on the loop’s way out.
Worker nudges skip finished workers. Any nudge aimed at a worker pane is
suppressed once that worker has finished — its broker status is done or
verified. The signal is deliberately the broker status and never a
pane-content diff: a finished worker’s pane is just as unchanging as a stuck
one’s, so a diff cannot tell “idle because done” from “idle because stuck”. A
worker that later leaves the finished state becomes nudge-eligible again — the
verdict is deferred, not dropped.
Note this is a narrower set than the one stall detection treats as terminal,
which also counts blocked and committed. That is deliberate: those two are
exactly the states a worker occupies while it awaits correction — committed
and standing by for re-verification, or blocked and not polling its inbox — and
they are precisely who the
correction loop exists to re-engage. Suppressing
them would silently disable it for its most common path. Finished and quiet are
different conditions; only the first ends a worker’s eligibility for a nudge.
What the orchestrator does with a judgment call is documented in the
bundled supervisor skill under Judgment calls handed to you: derive spawn order from
the agents’ agent.intent declarations and the conflict detector’s edges; answer
a question from the specs plus cross-agent state; sequence merges after
verifying; and declare a worker unrecoverable only per
max_cycles / on_exhausted.
The human is the exception handler. A human is re-engaged for what the orchestrator genuinely cannot decide — plus the heartbeat exit described above, and any run no orchestrator is driving, where the broker review items and the exit summary are the whole escalation.
Headless hand-off of judgment calls
Waiting for someone to pull the broker inbox makes an unattended run depend on a human. So when a headless invocation resolves, the drive loop hands each judgment call it cannot make to the orchestrator CLI once, non-interactively — a fresh one-shot turn at the repository root that reads the orchestrator skill and acts on the call:
| Judgment call | Handed to a one-shot? |
|---|---|
An ambiguous agent.question | yes |
A merge decision (a committed/done agent.artifact) | yes |
| A correction-exhausted branch | yes |
| A live permission prompt | never |
| The early correction heads-up (slow-converging branch) | never — it requires no action yet |
The broker review item is always in place first — the worker’s own message
for a question or a merge decision, the loop’s escalation otherwise — so the
hand-off adds to the record, it never replaces it. A permission prompt is
never handed off: a one-shot has no human to relay it to, and no git-paw
process answers a permission prompt. It stays on the broker, in the exit
summary and in the manual-decision log, for you or for an
orchestrator you tell to run sweep.sh approve.
Resolution. The CLI is [supervisor].cli, else default_cli. Its
headless form is [clis.<name>].headless_args, else the built-in row
(claude → -p, codex → exec), else none. The launch output reports the
result once, at loop start — for example headless hand-off: claude -p — or
says why the hand-off is unavailable (no CLI resolved, the CLI has no headless
form and which [clis.<name>].headless_args key would supply one, or
[supervisor.escalation] enabled = false). With no hand-off the loop behaves
exactly as without it: broker records and the exit summary only.
The invocation. The argv is the CLI, then the approval flags resolved from
[supervisor].approval (falling back to agent_approval), then the headless
arguments — e.g. claude --dangerously-skip-permissions -p. The escalation
context arrives on stdin, never on argv, so nothing appears in ps. It
names the agent and the call — the worker-written part fenced between markers
as quoted data the one-shot must never take instructions from — points at
.git-paw/skills/orchestrator/SKILL.md
and .git-paw/AGENTS.local.md, gives the broker URL, tells the one-shot to act
only through the bundled helpers or the MCP tools and to publish its outcome,
and forbids it to run sweep.sh approve or answer any permission prompt. For
a merge decision it carries the unattended merge rule: merge fast-forward only
once every gate is verified; escalate only on a merge conflict, an exhausted
correction cycle or a regression. “Escalating”, for a one-shot, means
publishing the reason on the broker and exiting, leaving the review item for
you. The environment adds only GIT_PAW_BROKER_URL — no credential: verdicts
go through sweep.sh, which fetches it at the root via git paw __authority.
There is no terminal; stdout and stderr are appended to
.git-paw/logs/<session>/orchestrator-escalations.log.
Bounded, serialized, deduplicated. At most one one-shot runs at a time per
session; further calls queue FIFO. Each queued call is keyed by its kind, its
agent and its discriminator (the question text, the artifact status, or
nothing for a correction-exhausted branch); a call whose key is already queued
or in flight is dropped, so a worker re-publishing the same question or
committed artifact while its one-shot is pending does not spawn a second
one. Once that one-shot has exited, a later identical message is a new event.
Each agent also gets one pending question at a time: while a question from
an agent is queued or running, its further questions stay on the broker only,
and the exit summary counts them — a looping worker cannot queue an LLM run per
question.
[supervisor.escalation] timeout_seconds (default 1800) bounds each one: on
expiry its whole process group is killed and the outcome recorded as
timeout. The loop never waits on a one-shot — the sweep continues while it
runs. At wind-down the loop stops starting queued one-shots but lets the
in-flight one finish or time out, saying on stderr that it is waiting; calls
never started are reported as still queued. A session torn down under the loop
(git paw stop, git paw purge) kills the running one-shot instead. If
git-paw itself is interrupted while a one-shot runs, the one-shot is not
killed — see the security posture.
Outcomes. Each finished or timed-out one-shot is published as an
agent.status from supervisor (status working) whose detail carries
escalation_key, exit_code (or "timeout") and duration_seconds; it is
in the broker log, and the supervisor’s /status row shows the phase
escalation until its next message. The exit summary adds the number of
invocations with a per-outcome count (completed, non-zero exit, timed out),
the number still queued and the escalation-log path — or, when no invocation
ran, why the hand-off was unavailable or disabled. A handed-off escalation is
still listed among the escalations awaiting review.
[supervisor]
cli = "claude"
approval = "full-auto" # what the one-shot may do unattended — see below
[supervisor.escalation]
enabled = true # default
timeout_seconds = 1800 # default; 0 is rejected
What a one-shot can do depends on [supervisor].approval. Nobody answers
the one-shot’s own permission prompts, so it can do exactly what its approval
level and the CLI’s own allow rules permit. Under auto with the
git paw setup-cli grants it can read state and
answer a question through the broker helper, but it cannot publish a
verdict or merge: sweep.sh verified, sweep.sh feedback-gate and
sweep.sh conflict-ack are deliberately never granted, and git merges fall
under your own rules. A merge-decision hand-off therefore needs
approval = "full-auto" — recommended only inside the
sandbox — or allow rules you write yourself. See the
security posture for what full-auto means here.
Set enabled = false when you drive the session yourself. Your own
interactive orchestrator and a one-shot would otherwise act on the same
judgment call at once. Disabling the hand-off restores broker-only behaviour
without any other change.
Unattended correction loop
When a supervisor gate fails, git-paw publishes agent.feedback to the
branch’s inbox — but a blocked worker is not polling its inbox, so the
correction stalls until a human re-engages the pane by hand. That single human
hop was the dominant cost measured across the v0.9.0–v0.11.0 dogfoods (4–8
manual verify↔fix cycles per branch).
The [supervisor.correction] table promotes that cycle into the drive loop.
Enable it with:
[supervisor.correction]
auto_loopback = true # default is false, for back-compat
max_cycles = 5
on_exhausted = "escalate" # or "abandon"
escalate_after_cycles = 3
With auto_loopback = true, each poll the loop:
- Watches for failing gate verdicts. A gate failure arrives as an
agent.feedbackfrom the supervisor whose errors carry a[<gate>]tag naming one ofgate_tags(default:[testing],[regression],[spec audit],[doc audit],[security audit],[scope]) — the shape.git-paw/scripts/sweep.sh feedback-gatepublishes. A passing gate publishesagent.verifiedinstead, so only failures start a cycle. Other traffic on the same channel — conflict-detector warnings, peer feedback — is never mistaken for a gate verdict. The tag vocabulary is configurable (seegate_tags) so a review process with different gate names can still drive the loop; the five-gate verification framework itself is unchanged. - Re-engages the worker’s pane. The branch’s pane is resolved by
pane_current_path, re-captured immediately before the send, and typed with the gate-tagged feedback plus a submittingEnteras a separate keystroke. If the fresh capture shows the pane is no longer idle (a permission prompt went up between the sweep and the send), nothing is typed and the correction is retried on the next poll — deferred, never dropped. - Counts the cycle. Each delivered re-engagement spends one of the
branch’s
max_cycles. A branch that reaches a terminal PASS leaves the cycle and its counter resets, so an unrelated later failure starts from a fresh budget. - Flags a slow converger once. When the count reaches
escalate_after_cycles— and that lands strictly beforemax_cycles— an early heads-up is surfaced while re-engagement continues. - Applies
on_exhaustedwhen the budget is spent.escalateflags the branch to the orchestrator/human as unrecoverable;abandonmarks it failed and sends no escalation. Neither re-engages that branch again, and the exit summary lists it under Correction budget exhausted.
Cycle counts are held in the drive loop’s memory and are not persisted — restarting the supervisor restarts every branch’s budget.
The feature is opt-in: with auto_loopback = false, or with no
[supervisor.correction] table at all, the loop sends no re-engagement
keystrokes and the gate feedback is published exactly as it was before this
existed.
When [supervisor] learnings = true, exhausting a branch’s budget also appends
a correction_exhausted learning carrying the branch, the worker CLI, and the
cycle count — the tiered-model tuning signal that tells you a worker’s model is
under-powered for a class of task. With learnings off, nothing is recorded.
The orchestrator’s own permission level
No pane runs an orchestrator, so no git-paw launch flag reaches your
interactive orchestrator: it runs in your own agent session, under whatever
permission mode you started that session with. [supervisor].approval changes
no pane command; it is the approval level of the drive loop’s
headless one-shot invocations, falling
back to agent_approval when unset. When it resolves to full-auto for a CLI
with no built-in flag row and no [clis.<name>].approval_args override, the
loop prints a warning naming the CLI and the override key and runs the
one-shot without flags. Coding agents resolve their flags from
agent_approval alone — setting approval never changes a worker pane.
auto resolves to a deterministic permission mode
agent_approval accepts three levels: manual, auto, and full-auto.
full-auto maps to the CLI’s native skip-permissions flag (for example
claude --dangerously-skip-permissions); manual appends no flags at all. Previously auto also appended no
flags, so an auto pane silently inherited whatever default its CLI’s config
directory happened to carry — sometimes manual approval, sometimes something
else, depending on machine state outside git-paw’s control.
auto now resolves to an explicit, deterministic flag per CLI wherever one
exists: for Claude this is --permission-mode acceptEdits — file edits are
accepted by the CLI without asking, while shell commands still raise a prompt,
which git-paw escalates for you. Codex’s auto already resolved to
--sandbox workspace-write. A CLI with no such mode keeps the empty string and
runs at whatever its own default is, unless you supply a
[clis.<name>] approval_args
override.
These resolved flags are git-paw’s only influence on what an agent may run without asking. Everything the chosen mode leaves prompting stays prompting: git-paw escalates it and waits for you. See Agent permissions.
Launch reliability: acceptance dialogs and dead-pane detection
Two failure modes could previously leave an unattended wave silently dead at boot:
- First-run acceptance dialogs. The first time a CLI launches with a
bypass-permissions flag against a config directory that has never accepted
it, some CLIs show a one-time confirmation dialog before their normal UI
appears — for example Claude Code’s bypass-permissions dialog, which
defaults its highlighted option to “No, exit”. A pane launched
unattended into that dialog exited within seconds without git-paw noticing.
The launch-readiness gate now recognises this dialog (and a “trust this
folder?” prompt) as a distinct state and answers it by selecting the
affirmative option by its text, never by a hardcoded digit or a blind
Enter— so the default-to-exit trap is never sprung. A dialog the gate cannot resolve fails the launch loudly instead of silently continuing; a CLI whose dialog the gate does not recognise falls back to the prior readiness/relaunch behaviour unchanged. - Panes that reverted to a bare shell. git-paw launches every pane as a
shell and
send-keyss the CLI command into it. When a CLI process exits — whether from an unanswered dialog, a crash, or any other reason — the pane does not close; it reverts to its underlying interactive shell.tmux has-sessionalone cannot tell a live wave from one where every agent’s CLI has quietly died, because the session (kept up by the dashboard pane) still exists.git paw statusandgit paw doctornow additionally require at least one agent pane whose foreground process is not a bare shell before reporting a session healthy, so a fully-dead wave is reportedstale/stopped rather than active. A session within its first ~30 seconds is exempted from this check — the CLI needs a moment to start.
Pane Layout and Labelling
When you attach to a supervisor session, git-paw styles the tmux panes so the boundary between the top row (the full-width dashboard) and the agent grid below is easy to see. By default each session gets:
-
Double-line pane borders (
═║) instead of tmux’s default light─│lines, so the rows visibly separate even with four or more agents. -
A per-pane label strip above every pane — a reverse-video label bar showing its index and role:
╔═ 0: dashboard ══════════════════════════════════╗ ║ (live agent table) ║ ╚═════════════════════════════════════════════════╝ ╔═ 1: feat/cold-start ════╗╔═ 2: feat/conflict-det ═╗ ║ (coding agent) ║║ (coding agent) ║ ╚═════════════════════════╝╚════════════════════════╝Pane
0is alwaysdashboardand the agent panes are labelled with their branch id (feat/<branch>). The index shown is the same one you pass totmux send-keys -t <session>:0.<N>(window 0; the session’s second window,broker, hosts the broker), so you can cross-reference the label strip with the broker’s/statusagent listing. -
An active-pane highlight — the focused pane’s border is cyan-bold while inactive panes are dimmed, so you always know which pane has keyboard focus.
These options are applied only to git-paw-managed sessions (paw-*); your
other tmux sessions keep their own styling. They apply to plain
git paw start sessions too, not just supervisor mode.
Set [layout].border_affordances = false in your config to opt out and
inherit your own tmux styling — see the
Layout configuration reference. Opting out
changes only the styling: git-paw still tags each pane with its role, which is
what status and doctor read to tell whether a session’s agents are running.
On tmux older than 3.2 the double border lines aren’t recognised; git-paw
prints a warning and continues with the remaining affordances rather than
failing.
Understanding what the supervisor is doing
You rarely need to read the orchestrator’s own session to know what it is up
to. The supervisor tags each status heartbeat it publishes with a phase — a
short label for its current lifecycle activity — and a structured detail
body. The dashboard’s supervisor row shows the current phase in its status
column, and the MCP get_session_status tool returns the same phase and
detail so external tooling can read it programmatically. (Both surfaces
degrade gracefully: a supervisor that hasn’t published a phase renders with
its plain status, exactly as in v0.5.0.)
The supervisor emits these phase-tagged statuses through the bundled
.git-paw/scripts/sweep.sh status-publish helper — --phase <phase> sets the
label and --detail '<json-object>' carries the structured body, while the
plain status-publish <message> form (no flags) publishes the v0.5.0 shape
unchanged. The helper shapes the agent.status payload internally, so the
supervisor never hand-rolls the JSON. Because the helper always lives at the
stable path .git-paw/scripts/sweep.sh, a permission rule in your agent CLI
scoped to that exact path covers every phase without a broad HTTP-client rule.
Every sweep.sh publish verb — status-publish, verified, feedback-gate,
conflict-ack, learn, answer, advanced-main — checks the broker’s HTTP response. When the broker
does not accept the publish (any non-2xx status, or no response at all), the
helper prints the broker’s error to stderr and exits non-zero instead of
printing its “published” line. A non-zero exit means the event did NOT reach
the broker: fix the cause (a malformed target agent id, a stopped broker) and
re-publish rather than treating the verb as done. The agent-side broker.sh
behaves identically.
The phases, with what each one means:
| Phase | The supervisor is… |
|---|---|
baseline | recording the regression baseline on the default branch at boot |
sweep | scanning agent panes and the message stream for new events |
audit | verifying a branch through the gates (the detail.audit_step field names which of tests, regression, spec, docs, security is running) |
merge | cherry-picking or merging a verified branch into the base |
feedback | sending feedback or a proactive route to a peer agent |
intent_watch | tracking declared file intents for overlap |
learnings | recording a structured learning record |
checkpoint | enumerating a multi-publish batch before a risky action, so it can recover from a stream timeout |
idle | waiting for the next event — nothing in flight |
The phase taxonomy is an open set: the supervisor may publish a value not in
this table, and the dashboard renders it as-is. The supervisor emits a status
on every phase transition and at most once per ~30 seconds while it stays in
the same phase, so the row stays current without flooding the
broker log. Phases are surfaced for the supervisor row
only — coding-agent rows render as they always have. (The one exception is
the supervisor-published stuck-on-prompt alert, which appears on a stalled
coding agent’s own row.)
Resolve Pane to Agent via pane_current_path
Before the supervisor tmux capture-panes or tmux send-keyss a specific
agent, it needs the pane index for that agent. The bundled supervisor skill
is explicit that pane indices are NOT alphabetical by agent_id, NOT in
the CLI-argument order from git paw start --specs A B C, and SHALL NOT be
inferred from git paw status output or the dashboard’s row order (both
of those are sorted alphabetically by the broker, which has no relationship
to the launcher’s internal scan order).
The canonical resolution command queries tmux directly:
tmux display-message -t <session>:0.<pane> -p '#{pane_current_path}'
<session> is the session name recorded in .git-paw/AGENTS.local.md;
window 0 holds the panes (the session’s second window, broker, hosts the
broker). The output is the pane’s working directory. For coding-agent panes that is
the agent’s worktree path — <repo>/.git-paw/worktrees/feat-<branch> under
the child placement git paw init configures, or <project>-feat-<branch>
beside the repo under the sibling placement. Matching it against the
worktree_path entries in the per-repo session file
(.git-paw/sessions/<session>.json) gives the owning agent’s branch_id — the
authoritative agent_id (with the slash form feat/<branch> for git
operations). A pane whose pane_current_path is
<repo>/.git-paw/worktrees/feat-auth belongs to agent feat-auth. Because
the directory name depends on the configured placement, the match is against
the recorded worktree_path, never against the directory’s basename.
The session file’s roster also records a pane_index, but that is a
launch-time value and is not used for routing — the live
pane_current_path match is.
The orchestrator reads the roster when it needs an agent’s pane rather than
building a pane map once and reusing it for the session, and in any case
re-reads it after git paw add or git paw remove — the roster changes and
tmux renumbers panes on layout changes. The roster can also be read through
the helpers or the MCP get_session_status tool.
The bundled .git-paw/scripts/sweep.sh invokes this command on every sweep
iteration. If the helper is missing for any reason, the supervisor falls
back to invoking tmux display-message directly — this is the documented
escape hatch.
The supervisor MUST NOT use git paw status output (or the dashboard’s row
order) as a mapping source — both are sorted alphabetically by the broker
and have no relationship to the launcher’s pane assignment. Always resolve
via pane_current_path first.
Pane-driving and cross-worktree git disciplines
The bundled supervisor skill hardens three behaviours so a multi-agent session runs without the human approving the supervisor’s every command:
- Use
.git-paw/scripts/sweep.shfor pane work; never inline loops. Ad-hocfor p in <panes>; do tmux send-keys -t ...:0.$p ...; doneloops trip thesimple_expansionpermission gate on each iteration, so one sweep becomes one approval prompt per pane. The helper does the explicit per-pane sends with no variable expansion. A single explicittmux send-keys -t ...:0.<index>is reserved for pushing one typed reply to one pane. - Never send-keys to the dashboard (pane 0). It is a TUI, not an agent;
keys sent there land in the dashboard, not in any agent.
sweep.shtargets coding-agent panes by design. - Cross-worktree git uses
git -C <path>, nevercd <path> && git. A barecdtrips the untrusted-hooks warning on some CLIs and leaks the working directory, so a later mutating git command can land on the wrong branch.git -Cscopes the directory to the single command.
The skill also nudges any agent whose uncommitted working set exceeds ~10 files to commit its completed section, preserving per-section verification granularity.
Spec audit governance sub-step
When a project’s .git-paw/config.toml lists governance documents under
[governance] (adr, test_strategy, security, dod, constitution),
the supervisor reads each doc as part of the
Spec Audit gate and flags drift between the diff and the doc’s checklist.
The five canonical doc-checklist examples are:
- DoD (Definition of Done) — walk each
- [ ]item against branch state. - ADRs (Architectural Decision Records) — verify new architectural decisions (new deps, new patterns) have a matching ADR.
- security.md — walk each security checklist item against the diff.
- test-strategy.md — check that test composition matches the documented strategy.
- constitution.md — check the diff against documented principles (e.g. “no panics in library code”).
Findings surface as standard agent.feedback errors tagged [doc audit]
mixed in with other doc-audit gaps. See Governance for the
config schema and how the supervisor reads each doc.
Agent permissions
git-paw does not approve permission prompts, and makes no judgement about whether an agent’s command is safe. It has no safe-command list, no danger list and no path-based safety rule. Whether a command runs without asking is decided entirely by the agent CLI’s own permission system:
- The CLI’s resolved permission mode.
[supervisor] agent_approvalmaps to each coding agent’s native launch flags — seeautoresolves to a deterministic permission mode. This is git-paw’s only influence on what runs unprompted. - The CLI’s own allow and deny rules. Whatever that CLI reads from its own settings. Pre-approve your routine dev commands there; that is the only place a standing grant can live. A grant is only as narrow as you write it.
- A sandbox, if you want a kernel-enforced boundary rather than a policy one — see Sandbox and Security Posture.
Anything the resolved mode leaves prompting stays prompting. git-paw’s job
starts there: it detects the live prompt and escalates it for a human decision
(the broker agent.question the orchestrator reads, the
manual-decision log, and the exit summary) without
blocking the rest of the wave. Then a human decides, and the approval is typed
only by an explicit sweep.sh approve <pane>.
A fully prompt-free unattended run therefore needs the decision made before
the prompt exists: either CLI allow rules covering everything the agents do, or
agent_approval = "full-auto" inside a sandbox. Without that, expect
escalations.
Deprecated:
[supervisor.auto_approve]and[supervisor.common_dev_allowlist]. Up to v0.16.0 these configured a git-paw command classifier that answered prompts it judged safe. The classifier is retired: three successive security reviews each found new command shapes it wrongly called safe, and each fix opened the next hole. Both tables still parse and round-trip, so existing configurations load unchanged — but they have no effect, and each configuration file that declares them prints one deprecation warning per process. Delete them at your leisure, and move anysafe_commands,stacksorextraentries into your agent CLI’s own allow rules. See the configuration reference.The classifier used to choose “Yes, and don’t ask again” for prompts it judged safe, and your CLI persisted those as durable grants in its own settings file. Retirement stops new ones; it removes none. Review that file and delete what you did not intend — git-paw never edits or deletes it.
Run dev commands bare — no exit-code-probe wrappers
A CLI allow rule matches a command prefix, so it generalises across
argument variants. Wrapping a command in an exit-code probe —
<cmd> && echo "EXIT $?", <cmd>; echo $?, RC=$?; echo "$RC" — defeats
that: the probe text varies per run, so the CLI’s command-string matching never
matches the next invocation and the command re-prompts every time. The bundled
supervisor and coordination skills instruct agents to run dev commands bare and
read the exit status directly; keep your own commands to the same shape so a
grant actually suppresses the prompt.
(git-paw used to normalise the two most common wrappers away before classifying. With no classifier there is nothing to normalise for, and the CLI’s own matching was never forgiving about them anyway. Run them bare.)
The explicit approve action
sweep.sh approve <pane> is the only path by which an approval keystroke
reaches a pane, and nothing in git-paw runs it for you. A human runs it, or
tells the orchestrator to run it. It answers the prompt once, with option
1, and consults nothing about the command.
Four guards sit between that instruction and the keystroke.
Option 1, never a durable grant. The helper dispatches the digit 1
followed by Enter as two separate keystrokes, reporting
approved pane <N> (option 1). It never dispatches a blind cursor-movement
sequence — Down + Enter lands on No on a 2-option prompt and takes the
permanent “don’t ask again” grant on a 3-option one. Before sending it checks
that a numbered option 1 really is on screen (and sends nothing if not), and
that option 1 does not itself carry the prompt profile’s broad-grant wording:
on some prompt shapes the first option is “Yes, and don’t ask again …”, and
pressing it would write a standing permission rule you did not ask for. In that
case the helper sends nothing and says why. git-paw never creates a durable
grant.
Re-confirm before send. Immediately before dispatching, the helper takes a
fresh capture of the target pane and confirms the prompt’s live structural
markers still anchor the modal block (the same anchored search and
bounded-trailing-region tolerance detection uses, not a fixed ~4-line scan). If
the prompt cleared between the decision and the send — the agent moved on, or
you answered it in the pane first — no keystrokes are sent, and the helper
reports cleared before send, no keys sent. This closes the stray-input race
where the option digit would otherwise land in the CLI’s chat box as literal
text, polluting context and leaving dangling unsubmitted commands.
Coding-agent panes only. The helper sends keys only to a pane the session’s
discovery roster (.git-paw/sessions/<session>.json) lists as a coding
agent’s pane_index. sweep.sh approve 0 against a broker-enabled session —
pane 0 is the dashboard — sends nothing and reports that the pane is excluded
because it is not a coding agent, whatever the capture shows; so does a pane
no roster entry claims. In a broker-less session the first coding agent sits
at pane 0, and the roster makes it approvable. With no readable roster the
helper falls back to refusing pane 0. The drive loop applies the same
role-based rule: it never types into the dashboard or an unattributed pane.
The refusal cannot be changed by a prompt profile.
One sender per pane, enforced. Keystrokes can come from sweep.sh approve
and from the drive loop’s own nudge and buffered-input recovery. If two land on
one pane, the second falls through onto whatever appears next. So before
sending, every sender takes that pane’s exclusive claim: an atomic
create-if-absent of <repo>/.git-paw/tmp/approve-pane-<N>.claim, held across
the send and released immediately after. Because the file system decides who
wins, exactly one sender can hold a pane at a time, and the exclusion works
across processes and languages — the shell helper and the binary compute the
same path and honour each other’s claims.
Taking the claim never blocks. A sender that finds a pane already claimed
skips it and moves on, retrying later; sweep.sh approve reports
pane <N> claimed by another approver, no keys sent and names the claim file.
Release is automatic on every exit path, including an error or a crash
mid-send. If a sender is hard-killed (SIGKILL) with a claim held, the next
one steals it after it has gone untouched for 10 seconds — so a crash costs one
pane one sweep, never a permanent wedge. The claim directory is ephemeral and
already gitignored; you never need to manage it by hand, though removing a
stale file is harmless.
No new broker message is involved in any of this: the trigger that a pane is
awaiting approval is the existing agent.status with
phase: "stuck-on-prompt", and an escalated prompt uses the existing
agent.question.
What detection still does
Prompt detection is the part git-paw keeps, and it is unchanged. The
live-prompt gate decides whether a modal is actually on screen: at the
capture’s true end, a textual marker (the Do you want to … question or the
Esc to cancel footer) within the last ~4 non-blank lines, or a numbered
option line anchoring that tail with a textual marker within the last ~15
non-blank lines. The search runs upward over a bounded trailing region, so a
CLI that renders a task list or status strip under its input area cannot hide a
live modal beneath that chrome; beyond the true end, liveness requires the
modal’s actual shape (a footer-shaped line plus an option list), never a bare
mention of the footer text in narration or tool output, and a match is rejected
outright if the CLI’s idle input box appears below it — a real modal replaces
the input box, so seeing one means the footer is a quoted copy.
Detection reports only that a prompt is present, and what command it is asking
about (the text between the Bash command / Bash(…) header and the
confirmation question, or the target path of a file-operation prompt). That
command text is used for the escalation message, the dedup key and the
manual-decision log — never to decide anything. A supervisor that merely
mentions rm -rf / in prose is not a prompt to run it, and a prompt asking
about ls -la gets exactly the same treatment as one asking about rm -rf /.
The window sizes, the trailing bound and the pane-0 refusal are compiled logic.
A CLI prompt profile supplies the literals
(markers, patterns) and can never widen the gate, change which option
approve presses, or lift the pane-0 refusal.
Out-of-worktree write violations
An observed out-of-worktree write attempt by a coding agent — a permission prompt asking to write into operator territory, pane narration describing one, a diff touching operator paths, a stray artifact in the repo-root control dirs — is a boundary violation, and the supervisor skill instructs a two-step response:
git-paw no longer has a mechanical check for this. The protected-path rule existed only to stop the retired classifier auto-approving such a write; with nothing approving automatically, a write prompt into operator territory reaches a human like every other prompt, and the supervisor relays it rather than approving it. The doctrine below is what carries the boundary; for mechanical enforcement use deny rules in your agent CLI’s own settings, or the sandbox.
- First attempt — scoped feedback. The supervisor sends the agent
agent.feedback(viasweep.sh feedback-gate <agent> scope …) naming the worktree boundary and the attempted path, so the agent can rescope: persist inside its worktree, or publishagent.questionbefore any outside write. - Repeat by the same agent — operator escalation. A second attempt
after the scoped feedback escalates to you via
agent.question, naming the agent, both attempted paths, and the feedback already sent — the decision (pause, re-brief, remove) is yours, not the supervisor’s.
The agent-side counterpart is the coordination skill’s memory-isolation briefing (see Agent Coordination): persistent artifacts live inside the agent’s worktree, operator configuration directories are off-limits, and a task that seems to require an outside write becomes a question instead.
Manual approvals
Every prompt the --unattended drive loop escalates is a command that needed
you. git paw approvals aggregates them, so you can see which prompts recur
and decide which deserve a standing rule in your agent CLI’s own allowlist —
closing the loop on repeat friction.
This is written by the drive loop, so it covers --unattended runs only.
An attended session (git paw start --supervisor without --unattended) runs
no loop and records nothing; git paw approvals still reads any log an
earlier unattended run left behind.
What gets recorded
Each time the drive loop escalates a live permission prompt, it appends one
JSON line to .git-paw/sessions/<session>.manual-approvals.jsonl:
{ "timestamp": "2026-05-29T12:34:56Z", "agent_id": "feat-auth",
"pattern": "make integration-test", "first_seen": true }
The loop runs outside the agent’s CLI pane, so it cannot see whether you ultimately pressed Yes or No on the prompt. What it records is the honest, observable signal: a command that required a human decision. A command escalated seven times is a strong allow-rule candidate regardless of each individual yes/no — the friction it causes is what you want to remove.
Exactly one line is written per escalation: a prompt re-seen inside the
five-minute dedup window is recorded once, and escalations that are not
permission prompts — a cli-unavailable banner, a wedged nudge, a
correction-loop signal — are not recorded here at all.
The pattern comes from the escalation’s own prompt slice — the command
between the Bash command / Bash(…) header and the confirmation question,
or a file-operation prompt’s target path. Surrounding pane text is
never scraped, so an accept-edits diff line or a previous command’s output
sitting next to the prompt cannot be recorded as if it were the command
awaiting a decision. When neither source yields something that parses as a
command, no line is written and no permission_pattern learning is emitted
— an unusable pattern is worse than a missing one, because it pollutes the
promotion report you act on.
Reporting
git paw approvals # active session, text table
git paw approvals --json # machine-readable
git paw approvals --session paw-other # a specific session
git paw approvals --limit 5 # top 5 patterns by count
The text table sorts patterns by how often each was escalated, with a
SUGGEST hint for which of your agent CLI’s own allowlists could absorb it:
PATTERN COUNT SUGGEST
make integration-test 7 user allowlist
./scripts/deploy-staging.sh 3 project allowlist
podman build -t paw-ci . 2 user allowlist
A pattern is suggested for the CLI’s project allowlist when it looks
project-specific (a ./-rooted script path, or it contains the project or
branch name); otherwise it belongs in your user allowlist, where it
benefits every project. Both are rules in the agent CLI’s own settings — the
SUGGEST column is a hint, not a rule, and git paw approvals never edits
any settings file for you.
When [supervisor] learnings = true, the first sighting of each pattern also
emits a permission_pattern learning so it surfaces in
Learnings Mode alongside the other patterns.
Opting out
Recording is on by default for --unattended runs. To disable both the log
writes and the derived learnings emission for a session:
[supervisor]
manual_approvals_log = false
The opt-out affects writes only — git paw approvals still reads any
pre-existing log. See Configuration for the
field reference.
Repo-configurable gate commands
The supervisor’s five verification gates each invoke a configurable command
substituted from [supervisor] keys at session boot. The eight keys are:
test_commandlint_commandbuild_commandfmt_check_commanddoc_build_commanddoc_tool_command— API-doc generator, separate fromdoc_build_commandspec_validate_commandsecurity_audit_command
When a key is missing or empty, the placeholder renders as (not configured)
in the supervisor skill and the supervisor gracefully skips the tooling
invocation for that gate — the gate’s manual review still applies.
doc_tool_command is the one exception: when unset it renders as the empty
string (not (not configured)) so the surrounding prose reads naturally for
projects that don’t ship an API-doc generator. See
Configuration for defaults and examples.
Polyglot bundled skills
The bundled supervisor and coordination skills ship as a single template each, but they render per session against the project’s stack and the resolved spec backend. Two placeholders make this work:
{{DOC_TOOL_COMMAND}}substitutes thedoc_tool_commandconfig value above so doc-audit prose names the actual API-doc tool for your stack.{{SPEC_PATH_DOCTRINE}}renders a per-backend path doctrine: OpenSpec sessions seeopenspec/changes/<name>/...references, Spec Kit sessions see.specify/specs/<feature>/..., Markdown sessions see the flatpaw_status: pendingshape, Superpowers sessions seedocs/superpowers/plans/*.md, and multi-backend sessions list each present backend’s conventions in one paragraph.
A CI no-leak audit asserts that the rendered supervisor skill contains
no stack-specific tokens (cargo, rustdoc, .rs:, Cargo.toml,
rustc) and no vendor product names outside the explicitly-allowed
<!-- allowlist-prose --> span, which carries a deliberate CLI-specific
enumeration (the paste-buffer marker wording).
Verification runs the whole suite (never fail-fast)
The supervisor’s testing gate runs the configured test command in a whole-suite / no-fail-fast mode. Most test runners stop at the first failing test group, so a single early failure — typically an environment-specific guard test rather than a code defect — can hide every later suite and produce a false PASS. A run that aborted early is incomplete, not a pass; a testing PASS requires the full suite to have executed.
For git-paw itself, the no-tmux-server guard test trips whenever any paw-*
tmux session exists (which it always does mid-session) and sits in an
early-alphabetical test binary, so a plain cargo test / just check stops
there. The just verify recipe runs the trustworthy gate —
GIT_PAW_ALLOW_LIVE_SESSION=1 cargo test --no-fail-fast (the suite is
socket-isolated, so the opt-out is safe) plus lint, deny, and audit — and
the dogfood config sets [supervisor].test_command = "just verify" so the
supervisor uses it automatically.
Per-commit verification
The supervisor verifies each agent’s commit as it lands, not in batches.
When an agent publishes agent.artifact { status: "committed" }, the
supervisor runs that branch’s five-gate sweep promptly — independent of
whether other agents have finished. Waiting for a slower agent so several
commits can be verified “together” delays feedback to the agent that already
committed and serialises work that is meant to run in parallel; at five-to-
eight agents that idles the whole session until the slowest one finishes.
To make the trigger an explicit event rather than something the supervisor
has to notice during a pane sweep, the broker posts a supervisor.verify-now
message naming the committing branch to the supervisor inbox on every
committed artifact. This is on by default; set
[supervisor] verify_on_commit_nudge = false to suppress the nudge and rely
on the supervisor’s sweep cadence instead.
Per-branch verifications may run concurrently — each gate sweep runs against its own branch in its own worktree, so verifying one agent never blocks starting another’s verification. The supervisor may bound how many sweeps run at once at its discretion. The one legitimate reason to defer a verification is a genuine dependency (one agent’s work needs another’s merge in place first), which the supervisor states explicitly when it defers.
Verdicts go through the helper
The verdict messages — agent.verified, and agent.feedback from supervisor
— need the session’s authority credential, so the supervisor publishes them
only through the root-side sweep.sh verbs, which fetch it with
git paw __authority and send it as X-Paw-Authority:
.git-paw/scripts/sweep.sh verified <agent-id> "<summary>"
.git-paw/scripts/sweep.sh feedback-gate <agent-id> <gate> "<message>"
.git-paw/scripts/sweep.sh conflict-ack <agent-a> <agent-b> <file> ["<note>"]
A raw curl …/publish of either message gets 403 {"error":"authority required"},
and so does the same verb run from inside a worktree (__authority refuses
there). See Security Posture.
Recovery from API stream timeouts
The supervisor’s own model stream can time out mid-sweep — most often
while it is queueing several agent.feedback or agent.verified
publishes in one iteration. Left unhandled, the natural reflex is to
fall silent and wait for the next sweep tick, which silently drops the
pending feedback: the agent that was about to hear about a regression
keeps working unaware, and you get no signal anything went wrong.
The bundled supervisor skill teaches an explicit recovery discipline so a transient timeout no longer loses work:
- Recognise the failure shape — a mid-stream cutoff, or a transport / stream error in the CLI output. The phrasing is generic so it matches across CLI variants.
- Checkpoint before risky actions — before any iteration with more
than one intended downstream publish, the supervisor publishes a
single
agent.status“checkpoint” enumerating what it is about to do, giving the recovery path a re-entry point. - Replay the missing publishes — on recovery the supervisor
re-reads its checkpoint and, for each intended target, polls
/messages/<target>since the checkpoint timestamp to see which publishes landed, then re-publishes only the missing ones (the replay is idempotent, so duplicates are safe). - Never assume a publish succeeded — a returned
publishHTTP call is not proof the record landed; the supervisor confirms by polling or re-publishes idempotently before moving on.
Each successful recovery emits a recovery_cycles agent.learning
record, so recurrent timeouts surface in the learnings output. See the
“Stream-timeout recovery” section of the bundled
assets/agent-skills/supervisor.md skill for the full doctrine, and
Learnings Mode for how the recovery_cycles records are
aggregated.
Broker-side conflict detector
Starting with v0.5.0 the broker auto-detects three failure shapes between
parallel agents and emits agent.feedback (and, where configured,
agent.question) on the supervisor’s behalf. All auto-emitted messages
begin with the [conflict-detector] token so the supervisor can distinguish
detector output from human-typed feedback. The three failure shapes are:
- Forward conflict — two agents publish overlapping
agent.intentdeclarations. - In-flight conflict — two agents’ filesystem-watched
modified_filessets overlap on the same file. - Ownership violation — an agent’s
modified_filesinclude a file inside another agent’s active intent.
See Conflict Detection for the algorithm, configuration, and escalation behaviour.
Learnings aggregator
When [supervisor] learnings = true, the supervisor session
records deterministic friction signals (stuck durations, recovery cycles,
conflict events, permission patterns) into a markdown file you can review
after the run. The supervisor also records qualitative learnings it reasons out over
the run — recurring failure shapes, doc gaps, ADR drift, scope mistakes, and
tooling friction with git-paw itself — publishing each through the bundled
.git-paw/scripts/sweep.sh learn <category> <title> <body-json> helper (never
a raw HTTP request). It captures them opportunistically during each sweep and again in
a session-end synthesis pass. See Learnings Mode for the file
format, the category set, and how to opt in.
When the user types in your session
The orchestrator runs in your own interactive agent session (outside tmux) — you can type at any time while its autonomous monitoring loop is running. The supervisor finishes the current step (spec audit, test run), responds, then resumes the loop. User input is a high-priority interrupt, not a replacement for the loop.
Each kind of user input maps to an existing mechanism — the supervisor does not invent new channels:
- Status question (“how’s feat-auth going?”, “anything blocked?”) —
answered conversationally in the orchestrator’s session using
sweep.sh status,sweep.sh inbox, andsweep.sh capture <pane>. Nothing is published to the broker — this is a conversation with the user, not a session-wide event. - Directive (“ask feat-auth to use bcrypt”, “tell feat-api to skip
the migration”) — published as
agent.feedbackto the named agent throughsweep.sh feedback-gatewith the[directive]gate, plus a conversational confirmation to the user. - Judgment-call ask (“should we merge feat-a before feat-b?”) — the
supervisor applies its normal escalation rules. If the user has already
provided enough information to decide, the supervisor answers in its
session using its reasoning.
agent.questiononly fires when the call is genuinely ambiguous beyond what the user just provided — typically when the user is asking because they don’t know either.
The mechanisms (sweep.sh status / inbox or the MCP reads,
sweep.sh capture, sweep.sh feedback-gate, tmux send-keys,
agent.question) are unchanged. The addition is when
to use which in response to user input.
Routing through the supervisor
The orchestrator is the single conversational surface: you work in its session and address individual agents through it, rather than tab-switching into each agent’s pane. Two commands you type to the orchestrator drive this.
/agents — the live inventory
Type /agents to the orchestrator, in its own session, and it responds with
the current inventory: one row per agent plus the orchestrator’s own
supervisor row once it has published a status, each coding-agent row carrying
branch_id, status, last_seen, cli, the detected mode, and
pane_index. The supervisor row has no pane index — the orchestrator has no
pane, and pane 0 is the dashboard:
branch_id status last_seen cli mode pane
feat-auth working 3s claude accept-edits 2
feat-api blocked 90s claude unknown 1
supervisor working 1s claude — —
The inventory is composed from broker GET /status (branch_id, status,
last_seen, cli) joined with the live pane_current_path mapping
(pane_index). Pane indices are path-resolved, never assumed from
ordering — a mid-session git paw add/remove that renumbers the grid is
reflected correctly after the next sweep. The mode column is best-effort
(accept-edits, interactive, or unknown); unknown is treated as the
safe case by /tell.
Known limitation — underscored branches show no pane_index. The join
matches an agent’s agent_id against its worktree directory name. A branch
containing _ (or a ., or upper case) keeps those characters in its
directory while its agent_id maps them to -, so the two no longer agree
and the pane goes unmatched: the agent still appears in the inventory with its
status and last_seen, but pane_index is empty and mode reads unknown.
/tell still reaches it — delivery falls back to the broker agent.feedback
path, which is the safe route it uses for unknown mode anyway. Only the
send-keys fast path and the pane-mode column are affected. Avoid _ in
branch names if you want the pane columns populated.
The supervisor refreshes the inventory at its sweep cadence and caches it in
memory. /agents and /tell reuse the cached snapshot while it is younger
than [supervisor.tell] inventory_max_age_seconds (default 60), re-polling
the broker only when it goes stale. The cache is not persisted — a restarted
supervisor rebuilds it on the first command.
/tell <agent> <prompt> — route a prompt
Type /tell feat-auth rebase onto main and the supervisor:
- Validates
feat-authagainst the inventory (the slugfeat-author slash formfeat/authboth work). An unknown target is not delivered — the supervisor replies with the candidate list of available agents, e.g.unknown targetfeat/ghost; available agents: feat-api, feat-auth. - Chooses a delivery mode (see below) and delivers the prompt.
- Acknowledges in its own session and, when learnings mode is on, records the routing decision (see Learnings).
/tell routes exactly the agent you name and exactly the prompt you type —
it never decides the recipient for you and never pipes your prompt into
another CLI to generate content. There is no bulk broadcast form in this
release; smart, supervisor-decided routing is a future candidate.
Delivery modes
[supervisor.tell] mode selects the default delivery channel:
feedback(the default) — the prompt is published as anagent.feedbackbroker message and the agent consumes it on its next inbox poll. Safe for mixed-mode sessions: the prompt is queued and recorded, never raced into a running prompt.send-keys— the prompt is injected directly into the target’s pane withtmux send-keys. Faster, but only safe when the target is in accept-edits mode. When you configuresend-keysbut the target’s detected mode isinteractiveorunknown,/tellfalls back tofeedbackand prints a note so you can investigate the mode-detection drift. Because of this fallback,send-keysis best reserved for sessions where your agents run in accept-edits mode.
Every /tell is auditable: when [supervisor] learnings = true, each
invocation appends a line to a ### Supervisor routing section of
.git-paw/session-learnings.md with the timestamp, target, delivery mode, and
the prompt (truncated past 200 characters). With learnings disabled, nothing
is written.
Proactive routing (opt-in, confirmed)
When a sweep finds an agent blocked on a question you have already answered
earlier in the orchestrator’s session, the orchestrator may offer to
forward that context — it asks you a yes/no question in its own session and
waits. It never routes
on its own: no proactive /tell fires without your explicit y. Reply n (or
anything else) and the offer is dropped. This keeps the pattern from feeling
surveillance-y while it earns trust through dogfooding.
opsx role gating
The bundled skills teach that /opsx:verify and /opsx:archive are
supervisor-only, but a coding agent that ignores the prose can still archive a
change — the skill is a convention, not a guard. When the session’s spec engine
is OpenSpec, git-paw closes that gap with a post-commit role-gating guard.
The guard watches the broker’s agent.artifact { status: "committed" } events.
When a commit looks like an archive operation and the committing agent is
not the supervisor, it reacts per [opsx] role_gating (see the
configuration reference):
warn(the v0.6.0 default) — publishes anagent.feedbackto the offending agent and records anagent.learningwith categorypermission_patternso the violation surfaces in learnings.block— the warn behaviour, plus anagent.feedbackto the supervisor requesting a revert. The supervisor performs the revert through its merge-orchestration skill (confirming with the user first unless[supervisor] auto_revert = true); git-paw never runsgit revertitself.off— the guard is disabled.
Detection heuristic. A commit is archive activity when either signal fires:
- its subject line matches the canonical archive shape
chore(specs): archive <name>; sync deltas to main specs, or - its diff moves files into
openspec/changes/archive/<name>/and/or adds a main spec underopenspec/specs/<capability>/spec.md.
The two-signal design is deliberately conservative — it would rather trip on
the supervisor’s own archive (which the attribution check then clears, since
agent_id == "supervisor") than miss a coding-agent archive. An unresolvable
worktree is treated as a violation.
Reading the warning. The feedback text names the short SHA, the offending agent, and the trigger so you can spot a false positive at a glance:
opsx-role-gating: detected archive activity on commit abc1234 by agent feat-x
(not the supervisor).
Reason: commit message matched the archive heuristic ("chore(specs): archive
feat-x; sync deltas to main specs").
/opsx:verify and /opsx:archive are supervisor-only ...
If a warning is a false positive, the named reason tells you which signal fired
so you can adjust (or flip role_gating = "off"). The guard is inert under
the Spec Kit, Markdown, and Superpowers engines — those have no /opsx:
commands or archive paths, so the capability and its skill sections are scoped
to OpenSpec only.
v0.6.0 behaviour change.
role_gatingdefaults towarn, so sessions where a coding agent archives a change now see guard feedback. Setrole_gating = "off"for the prior (no-guard) behaviour.
Per-CLI setup (consent-only)
Agents call git-paw’s helper scripts constantly, and under most agent CLIs every call raises a permission prompt. The supervisor does not make those prompts go away: a helper call runs without one only through the agent CLI’s own permission system and the rules in its settings.
You can pre-approve git-paw’s own helper calls in that CLI’s allow rules with
git paw setup-cli <CLI> --allowlist. It is optional and
consent-only — you run it, it prints its plan first, and --undo takes back
exactly what it added. Nothing in the supervisor, the drive loop or the MCP
surface ever runs it, and it refuses to run from a linked worktree.
The embedded cli-setup skill carries the rules an agent must follow around
it. git paw init and git paw start export it to
.git-paw/skills/cli-setup/SKILL.md beside the orchestrator skill (main
checkout only), and the MCP get_skill tool serves it under the name
cli-setup. The
supervisor skill restates the three that bind the orchestrator directly: run
the writing form only when the human asked in the current conversation, pass
--yes only after they approved the exact plan shown, and never run it
during an unattended run.
Merge orchestration
Once every spec’d agent has been verified — the orchestrator published
agent.verified for it through sweep.sh verified (agents never publish it
themselves) — or the user explicitly asks for a merge, the orchestrator runs the merge orchestration
loop. v0.5.0 removed the Rust auto-merge loop; merging is the orchestrator’s
responsibility, performed with shell, git, the configured test command and the
bundled helpers or MCP tools — never a raw HTTP request.
Trigger. Either every spec’d agent has been verified (the orchestrator
published agent.verified for it), or
the user has explicitly requested the merge.
Unattended runs merge without asking first. When the session context
carries the drive-loop directive (--unattended), the orchestrator merges as
soon as the trigger holds — fast-forward only — without asking the human
first. It holds a branch back and escalates only on:
- a conflict — a non-fast-forward or conflicting branch (skipped, with
agent.feedbackto its agent), or a dependency cycle (escalated withagent.question); - an exhausted correction cycle — a branch whose correction budget ran out
(escalated with
agent.question); - a regression — a merge that fails the test command (reverted, with
agent.feedbackto its agent).
Every held-back branch is named in the final summary. In an attended run the orchestrator may still confirm the merge order with you before merging.
Merge order. The orchestrator reads the broker’s message log
(sweep.sh inbox / sweep.sh events, or the MCP read tools) and builds a dependency graph from agent.blocked
events: each event from agent X with payload.from = Y is an edge “X
depends on Y”. The supervisor then topologically sorts the graph: agents
with no incoming edges merge first; dependents follow.
Per-branch merge. For each branch in topological order, the supervisor
checks out main and runs:
git merge --ff-only feat/<branch>
Never a merge commit — fast-forward only. If --ff-only fails (the branch
diverges from main, or there is a conflict), the supervisor SKIPS that
branch and publishes agent.feedback to the owning agent asking them to
rebase or resolve. On a successful fast-forward, the supervisor runs the
configured {{TEST_COMMAND}}; if tests fail, the supervisor reverts the
merge with git reset --hard <prev-HEAD> and publishes agent.feedback
tagged [regression]. If tests pass, it publishes agent.advanced-main
with sweep.sh advanced-main <merged-branch> <summary> — the helper resolves
the new default-branch SHA and the base branch name itself — so every dependent agent learns the base moved on its next inbox poll —
see When Main Advances — then moves on to
the next branch.
Cycle handling. If the dependency graph has a cycle, the supervisor
does NOT merge any branch in the cycle. Instead, it publishes
agent.question (through broker.sh --agent supervisor question) to the human and waits for guidance before continuing.
Final summary. When the loop completes, the supervisor publishes a
final agent.status summarising which branches merged cleanly, which were
skipped (and why), and any regressions encountered.
Branch refresh (opt-in)
Over a long wave the supervisor merges peer after peer into the default branch while every other live worker keeps committing on the baseline it started from, so each branch drifts further from what it will eventually be merged into. The only existing recourse is worker-side and advisory — see When Main Advances in Agent Coordination — which correctly tells a worker holding uncommitted work not to auto-rebase, but means refresh only happens when a worker chooses to act.
When [supervisor] branch_refresh = true is set, the supervisor’s
unattended drive loop instead performs the
refresh itself, at a moment it can prove is safe: after a successful merge
(agent.advanced-main), for a branch that is provably idle and clean. This
is a supervisor operation, not a worker judgment call — the two disciplines
do not conflict, because the supervisor acts only under exactly the
condition (no uncommitted work) that makes the worker-side “do NOT
auto-rebase” rule unnecessary.
Default off. Rewriting a live agent’s history is opt-in, permanently —
set [supervisor] branch_refresh = true to enable it. With the field
absent or false, post-merge behaviour is unchanged from every prior
version.
Every live worker branch is checked after every merge, and refreshed only when ALL of the following hold. Any single failure leaves the branch untouched — there is no partial or degraded attempt, and the skip is recorded, not retried or escalated:
- Clean. The worker’s working tree has no uncommitted changes, per
the broker watcher’s tracked
modified_files— never a fresh probe, so the supervisor and the watcher can never disagree about what “clean” means. - Idle. The worker’s pane is not mid-response.
- Claimed. The supervisor holds the pane’s exclusive approval claim (the same TOCTOU guard approvals use), narrowing the race window between the cleanliness check and the rebase.
- Conflict-free. A
git merge-treeprediction shows the rebase would not conflict — checked BEFORE the rebase starts, so the worktree never enters an in-progress rebase state a live agent could observe. A prediction that proves wrong once the rebase actually runs is recovered withgit rebase --abort, leaving the branch at its pre-rebase HEAD. - Behind. The branch actually has commits to pull in; an already current branch is left alone.
A branch that has already passed the five-gate supervisor verification and awaits merge is never refreshed — it has been verified at its current base, and rewriting it would silently invalidate that verification.
Notification. After a successful refresh the affected worker receives
agent.feedback tagged [branch-refresh] (the same bracket-tag
convention as [conflict-detector]) telling it HEAD moved, so it does not
keep reasoning about file contents or commit SHAs from before the
rewrite. This is deliberately NOT agent.advanced-main — that event means
“the base moved” and the worker-side discipline above keys off that exact
meaning; conflating it with “your own branch was rewritten” would corrupt
that discipline.
Tiered-Model Workflow
A smart orchestrator running a fleet of cheaper workers is the workflow git-paw is built for: one capable model supervises, and several lower-cost models do the implementation in parallel worktrees, with little-to-no human babysitting. This chapter is the process — how to set a run up so the cheap fleet actually succeeds. For the mechanics of how the orchestrator drives the wave, see Supervisor → the loop is a pump, the supervisor is the brain.
The two roles
git-paw already gives you both halves:
- Orchestrator — your own agent session outside the tmux session, reading the
context a
--supervisorlaunch renders into.git-paw/AGENTS.local.md([supervisor].clinames its CLI). Its job is judgment: spawn order, answering cross-agent questions, sequencing merges, and deciding when a worker is unrecoverable. Give it your most capable model. - Worker — an ordinary CLI in a worktree (
default_spec_cli/--cli). Its job is execution: implement one change against a concrete task list. This is where a cheaper model earns its keep — if the task list is concrete enough (see below).
Two things make the cheap fleet viable rather than a prompt-storm: the orchestrator handles the judgment calls the loop can’t instead of parking them in an inbox nobody reads, and the self-healing correction loop turns a gate failure into an automatic re-engagement rather than a human hop. Both ship in v0.14.0. What you bring is the two pre-steps this chapter is about: a plan, and a definition of “verified.”
Step 1 — Plan first; planning is not a git-paw command
git-paw does not author your change set — it executes one. Author the plan
before git paw start, in any supported spec format:
OpenSpec (openspec/changes/<name>/), Spec Kit, Superpowers plan documents, or
plain Markdown-with-frontmatter. git-paw starts once those artifacts exist and turns
each into a worker session.
Because planning is the one step that most rewards a capable model, do it with your
smart model — a planning pass, or an /opsx authoring session — and only then
hand the result to the cheap fleet.
Step 2 — Make the tasks task-level concrete (the rule that decides success)
This is the central rule of the whole workflow: a cheaper worker executes reliably only when the tasks artifact is concrete. Vague tasks are where cheap models diverge; concrete tasks are where they succeed — and an under-specified plan costs more in orchestrator correction cycles than a smart worker would have cost in the first place.
Concrete means, per task: the exact files to touch, the per-step verification command, and an explicit acceptance criterion.
# Too vague — a cheap worker will guess, and guess differently each run:
- [ ] Add validation for the config
# Concrete — a cheap worker can execute it deterministically:
- [ ] In src/config/supervisor.rs, reject max_cycles = 0 at load with an
actionable error (mirror the invalid on_exhausted handling); add a unit
test asserting 0 fails and 1 is accepted. Verify: cargo test --lib
config::supervisor.
The OpenSpec/Superpowers task shape (a checkbox list where each item names its file, its verification, and maps back to a spec scenario) is the exemplar. If you find the orchestrator burning correction cycles on a worker, the fix is almost always a sharper task, not a smarter worker.
Step 3 — Define “verified” in your AGENTS.md, not in git-paw
What “verified” means — the exact test/lint/build commands, the acceptance
criteria, the definition of done — is a per-project convention, so it lives in
your repo’s injected instructions (AGENTS.md / CLAUDE.md), never baked into
git-paw. This is the export-agnosticism principle: git-paw
ships only the generic five-gate scaffold — testing → regression → spec audit →
doc audit → security — and the supervisor follows your concrete gates when it
verifies a worker’s branch. Spell them out, or the orchestrator falls back to the
scaffold and guesses at your commands.
Wire the gate commands into [supervisor] (test_command, lint_command,
build_command, doc_build_command, spec_validate_command, fmt_check_command,
security_audit_command) and write the definition of done into AGENTS.md.
git-paw’s own AGENTS.md — its “Change Checklist” and “Supervisor verification is a
five-gate framework” sections — is the exemplar to model yours on.
Step 4 — Launch, and let the orchestrator drive
With the plan authored and “verified” defined:
# Attended: you watch the panes and answer the permission prompts yourself.
git paw start --specs my-change --supervisor
# Unattended: the in-process loop drives to completion, escalating every
# permission prompt to you rather than answering it.
git paw start --specs my-change --supervisor --unattended
Turn on the self-healing loop so a gate failure re-engages the worker automatically instead of waiting for you:
[supervisor.correction]
auto_loopback = true # re-engage a gate-failed worker's pane with the feedback
max_cycles = 5 # give up on a branch after this many verify-fix rounds
on_exhausted = "escalate" # escalate | abandon
From here the orchestrator answers questions, sequences merges, and re-engages failing workers on its own; see the correction loop and the pump-to-brain hand-off for exactly what it handles.
Where the human still comes in
You are the exception handler, not the operator. A human is re-engaged only for
what the orchestrator genuinely cannot decide, plus two structural cases: the
heartbeat exit after ~25 minutes without progress, and a run with no orchestrator session at all (a pure
--unattended run nobody orchestrates leaves every judgment call as a broker review
item for you, exactly as it did before). Everything the fleet can resolve — safe approvals, cross-agent
questions, verify-fix cycles, merge sequencing — resolves without you.
Dashboard
When the broker is enabled, one tmux pane runs the dashboard instead of an agent CLI, and it is pane 0 in every mode: the full-width top row above the agent grid, with the coding agents laid out row-major beneath it (no orchestrator CLI runs inside the session). That is the same layout in a supervisor-mode session, a bare one and a spec-driven one — every session git-paw builds uses the agent grid. It renders the same live status table that refreshes about once a second (and immediately on a key press), giving you an at-a-glance view of what each agent is doing. A supervised session builds the dashboard pane even with the broker off.
The dashboard is observation-only — it never sends actions back to agents, and no git-paw keystroke sender ever types into it. Beyond q to quit, its keystrokes drive the Broker log panel (toggle, filter, inspect). Human input (questions, directives, replies to agent.question events) happens in the orchestrator’s own agent session, outside tmux; the dashboard simply renders broker state. There is no inbox panel.
What the Dashboard Pane Shows
The dashboard renders a table with one row per agent. Once the orchestrator has published its own agent.status (as supervisor), its row is pinned to the top with a horizontal-line divider beneath it; coding-agent rows follow in alphabetical order:
┌────────────┬────────┬────────────┬─────────────┐
│ Agent │ CLI │ Status │ Last Update │
├────────────┼────────┼────────────┼─────────────┤
│ supervisor │ claude │ 🔵 watching│ 12s ago │
│ ────────── │ ────── │ ────────── │ ─────────── │
│ feat-api │ claude │ 🟡 blocked │ 1m ago │
│ feat-auth │ claude │ 🔵 working │ 3m ago │
│ fix-typo │ codex │ 🟢 done │ 8m ago │
└────────────┴────────┴────────────┴─────────────┘
When no orchestrator has published (e.g. broker-only mode — broker on, no --supervisor — or a supervised session before your orchestrator session has picked up its context), the divider is not rendered and coding agents fill rows alphabetically from the top.
Status Symbols
| Symbol | Meaning |
|---|---|
| 🔵 | Working – agent is actively processing |
| 🟢 | Done/verified – agent has completed its task |
| 🟣 | Committed – agent has committed work |
| 🟡 | Blocked – agent is waiting on something |
| ⚪ | Idle / unknown phase – agent has not reported a recognised status |
The Last Update column shows elapsed time since the agent’s last status update (30s ago, 3m ago, 1h 15m ago).
Supervisor row, cli field, and phase field
The supervisor row’s Status column does not show the wire-message type label that a coding-agent row shows. Instead the supervisor publishes a phase field on its agent.status messages (e.g. baseline, watching, approving, answering, merging, summary), and the dashboard prefers that label when rendering its row. This avoids the misleading status=feedback label the supervisor would otherwise show when it publishes agent.feedback to a coding agent.
The orchestrator has no pane and no worktree, so it is not a watch target and the broker cannot infer its cli from the watch-target map. To populate the CLI column for the supervisor row, the supervisor self-registration agent.status includes a cli field (e.g. "cli":"claude"). The broker upserts that value into its internal CLI map when it receives the message.
The CLI column is populated for every agent row, not just the supervisor. git-paw knows each pane’s CLI at launch and pre-fills it authoritatively — coding agents from the watch-target map (the per-repo session JSON), the supervisor from [supervisor].cli/default_cli. Agents do not self-report their CLI (they would only be guessing). A row appears once its pane publishes a status, and its CLI column shows the pre-filled value; if a CLI somehow can’t be resolved it shows a ? placeholder rather than a blank cell.
A coding-agent row can also show stuck-on-prompt in its Status column: the bundled sweep.sh helper’s stuck detector publishes this phase directly on the stalled agent’s own agent.status when its pane shows a permission-prompt marker and its heartbeat has stopped advancing (see Agent permissions). This is the one coding-agent phase outside the generic working/done/blocked/committed set in Status Symbols above.
The roster holds only real agents
The /status agent roster — and therefore the dashboard table — is built only from agents that publish agent.status. The from/target/verified_by identity fields on agent.feedback, agent.question, and agent.verified messages are routed and stored but never mint a roster row. This means a human- or supervisor-originated feedback can no longer create a phantom human agent that never heartbeats. The roster is in-memory, so any pre-existing phantom from an older broker also clears on the next git paw start.
When the supervisor row appears
The supervisor row appears after the orchestrator — your own agent session, outside tmux — publishes its first agent.status as supervisor, its first action on picking up .git-paw/AGENTS.local.md. The launcher never publishes on its behalf, and the drive loop’s escalations (published as agent.question from supervisor) never create the row either, so a session no orchestrator drives shows no phantom supervisor row.
Stalled-wave indicator
Agent rows answer “is this agent alive?” The status line at the bottom of the dashboard answers the question they cannot: “is the wave getting anywhere?” Whenever the broker reports the wave as stalled, the status line carries an indicator naming how long it has been idle:
wave stalled 1h
The reading comes straight from the broker’s
wave staleness state, so the same two things
clear it that count as progress anywhere else — a wave phase advancing or a
branch tip moving. The indicator disappears on the first refresh that reports
stalled = false; nothing is shown while the wave is progressing. Tune (or
disable) the threshold with [broker] wave_stall_minutes — see the
configuration reference.
This is the dashboard’s view of a session that looks healthy row by row and is
not moving: every pane alive, every agent working, and nothing having
actually advanced since the unanswered prompt.
Controls
| Key | Action |
|---|---|
q | Quit the dashboard (shuts down the broker; agent panes keep running but lose broker communication) |
l | Toggle the Broker log panel on/off |
a | Reset the Broker log filter to All |
1–9, 0 | Toggle the individual filter chips (status / artifact / blocked / verified / feedback / question / intent / verify-now / advanced-main / learning) |
↑/k, ↓/j | Move the highlight up/down the Broker log rows |
Enter | Open the details overlay for the highlighted row |
Esc | Close the details overlay |
Broker log panel
The Broker log panel fills the screen region freed when v0.5.0 removed the
prompt inbox. It renders a scrolling, type-filterable list of the broker
messages observed during the current dashboard session, newest at the top, so
you can watch the session’s wire-level activity at a glance instead of tailing
/messages/<id> over curl.
The log is in-memory only — closing the dashboard drops it — and bounded by
[dashboard.broker_log] max_messages (default 500). Older messages fall off the
top as new ones arrive. The panel is read-only: replying to a question or
directing an agent happens in the orchestrator’s own session.
When visible, the panel occupies a fixed 20 rows by default — materially more
than its original 12 — so more coordination traffic is visible without constant
scrolling. Tune it with [dashboard.broker_log] height_lines. The agent-status
table keeps a positive minimum height and absorbs the terminal’s slack, so on a
tall terminal the table grows while the panel stays at its configured height; on
a terminal too short to grant both, the panel yields space before the table
shrinks below its minimum.
Showing and hiding the panel
The panel is visible by default. Toggle it with l, or set the launch default
and height in .git-paw/config.toml:
[dashboard.broker_log]
max_messages = 500
default_visible = true
height_lines = 20
When the panel is hidden the dashboard layout is identical to its v0.5.0
post-inbox-removal shape (title, agent table, status line) — the agent table
expands to fill the freed space, and the status line gains a
broker log hidden — press l to show hint. See the
configuration reference for the
table’s fields.
Filter chips
A header row of chips sits above the message list, one per broker message type
plus an All reset. All is active by default (every message shows). Pressing
a digit hotkey (1–9, then 0 for the tenth chip) narrows the view to that type; pressing more digits adds
types inclusively; pressing an active chip again removes it (emptying the
selection returns to All). Press a to reset to All at any time. Filtering
is a view operation — the ring buffer always retains every message regardless of
the active chips.
| Hotkey | Chip | Matches |
|---|---|---|
1 | status | agent.status |
2 | artifact | agent.artifact |
3 | blocked | agent.blocked |
4 | verified | agent.verified |
5 | feedback | agent.feedback |
6 | question | agent.question |
7 | intent | agent.intent |
8 | verify-now | supervisor.verify-now |
9 | advanced-main | agent.advanced-main |
0 | learning | agent.learning |
agent.answer messages have no chip of their own; they show in the default
All view.
Row format
Each row is a single line: HH:MM:SS · type · agent · summary, where the
summary is a per-type one-liner derived from the message body (the status
message, the first modified file of an artifact, the blocking need, the intent
summary, and so on). Summaries that overflow the panel width are truncated with
an ellipsis (…); the full body is available in the details overlay.
┌─ Broker log (7 shown / 7 held) — l hide · a all · 1-9·0 filter · ↵ details · Esc close ─┐
│ All 1:status 2:artifact 3:blocked 4:verified 5:feedback 6:question 7:intent 8:verify-now 9:advanced-main 0:learning │
│ 14:35:09 · status · feat-auth · working: rebasing onto main │
│ 14:34:58 · intent · feat-auth · wire AuthClient │
│ 14:34:12 · blocked · feat-api · needs auth token format from feat-auth │
│ 14:33:40 · artifact · fix-typo · done: src/typos.rs │
└────────────────────────────────────────────────────────────────────────────────────────┘
Status activity phrases
An agent.status payload may carry an optional activity field — a short
phrase naming what the agent is doing right now. When present it is appended to
the status summary in parentheses, so a heartbeat conveys the work rather than
just a bare working:
│ 14:35:09 · status · feat-auth · working (running the test suite) │
The field is optional and additive. A status that omits it renders exactly as it did before the field existed, and agents are never required to set one.
Heartbeat collapsing
Agents heartbeat often, and a wall of identical working lines buries the real
state transitions between them. The panel therefore collapses a run of
consecutive status rows that are identical in every rendered field — same
agent, status, activity, and message — into one row carrying the run’s latest
timestamp and a ×N repeat count:
│ 14:35:09 · status · feat-auth · working ×12 │
│ 14:33:40 · artifact · fix-typo · done: src/typos.rs │
Anything that differs breaks the run and starts a new row — a changed status, a
changed activity phrase, a different message, another agent, or any non-status
message arriving in between. So a real transition such as working → verified
always gets its own row, and two distinct working phases separated by other
traffic are never merged.
Like filtering, the collapse is a view operation: the ring buffer still retains every individual message, the count in the panel title reflects the rows drawn, and opening the details overlay on a collapsed row shows the newest message of that run.
Details overlay
Highlight a row with the arrow keys (or j/k) and press Enter to open a
modal overlay showing that message’s full, pretty-printed JSON in a scrollable
view. Press Esc to close it and return to the panel. While the overlay is
open, q still quits the dashboard.
Resilience across broker restarts
The panel never clears its buffer when the broker is unreachable, paused or restarted. Historical messages stay visible across the outage, and new messages resume appearing at the top once the broker answers again. A restarted broker host numbers its log from 1 again; the dashboard notices from the host’s uptime going down, re-reads the new log from the start, and appends each new entry once — nothing it already shows is duplicated. A gap in the timestamp column is the only visible sign that messages briefly stopped flowing.
Legacy messages panel
The earlier [dashboard] show_message_log flag rendered a simpler, unfiltered
messages list. It is superseded by the Broker log panel and retained only for
config compatibility.
Relationship to the Broker
The dashboard is a thin client of the broker. The broker runs in its own process, git paw __broker, in a detached tmux window named broker (outside the pane grid, so pane numbering is unchanged; switch to it to see the host’s stderr). The dashboard (git paw __dashboard, pane 0) holds no broker state: it reads the broker URL from the saved session and polls GET /status and GET /log?since=<seq> on its idle cadence. Closing the dashboard pane has no effect on the broker, its watchers, conflict detection or learnings.
When the broker is unreachable
If a poll cannot reach the broker — git paw pause stopped it, the host is restarting, or a fresh launch has not finished bringing it up — the status line shows ⚠ broker unreachable, the agent table keeps the last snapshot, and the dashboard keeps polling. The indicator is independent of the stalled-wave indicator, and it clears on the first successful poll. An unreachable broker never makes the dashboard exit.
Lifecycle and exit conditions
The dashboard process is bound to its session: when the session ends, the dashboard exits on its own rather than lingering and busy-rendering to a dead terminal (a leaked __dashboard process pegging CPU). Its draw loop checks a single lifecycle gate on every iteration — including any error or degraded path, so no branch can bypass it — and exits when any of the following holds:
- Clean shutdown. tmux delivers
SIGHUPwhen it kills the session or pane, which ends the process. - Orphaned to init. On Unix, if the parent process is gone the dashboard has been reparented to init (
getppid() == 1) — the tell-tale of a session torn down withoutSIGHUP(an abrupttmux kill-server, a crash, or the machine sleeping). The loop notices the reparent and exits. On non-Unix platforms this check is inert and only theSIGHUPshutdown path applies. - Controlling terminal gone. If the pane’s terminal hangs up, reading input (
event::poll) errors, or a write to the terminal fails, the dashboard treats its controlling terminal as gone and exits. This catches the case where the pane was killed but the dashboard was reparented to a lingering shell (a live but unrelated parent), so the orphan-to-init check alone would miss it.
An unreachable broker is deliberately not on this list (see above). The broker host has its own lifecycle: it flushes the broker log and exits on SIGHUP (when pause closes its window or stop kills the session), when orphaned to init, or when its terminal hangs up, and it exits non-zero with a diagnostic naming the port if it cannot bind — it never retries in a loop.
Because the dashboard is cheap to relaunch (git paw start re-attaches to the session), exiting eagerly on any of these signals is preferred over risking a leaked, busy-looping process.
Replying to agent questions
Earlier dashboard versions included a “Questions” panel and a “Reply to” input field for human-typed answers to agent.question events. The panel was removed in v0.5.0; the dashboard has no inbox. Questions and replies go through the orchestrator — your own agent session, outside tmux — and its helpers, not through the dashboard.
agent.question messages still flow through the broker. The orchestrator reads incoming questions from the broker (through sweep.sh, the broker helper or the MCP server) and replies with sweep.sh answer (an agent.answer on the broker) plus tmux send-keys to the asking agent’s pane. See the Orchestrator mode chapter and the exported orchestrator skill (.git-paw/skills/orchestrator/SKILL.md) for the reply flow.
See the v0.5.0 changelog for the removal note.
Pause and Resume
git paw pause is the soft-stop verb for short breaks (lunch, a
meeting, end-of-day). It freezes the running session without losing
agent state, so you can resume mid-conversation an hour later by
running git paw start.
The three teardown verbs
git-paw v0.5.0 ships three verbs for taking a session down:
| Verb | What it kills | What survives | Use when |
|---|---|---|---|
pause | Tmux client attach, broker host | Tmux session, dashboard, all CLI panes (in RAM) | Short break — you’ll resume in minutes/hours |
stop | Tmux session, every CLI pane | Worktrees and branches on disk | Longer break — you want RAM back, OK with fresh CLIs later |
purge | Everything (tmux, worktrees, state) | Nothing | You’re done with the project, or you want to start clean |
What pause does, mechanically
When you run git paw pause:
- Every tmux client attached to the session is detached
(
tmux detach-client -s <session>). - When the broker is enabled, the broker host’s
brokerwindow is closed, which deliversSIGHUPto the__brokerprocess, which drops theBrokerHandle, which gracefully shuts down the broker and flushes the session’s<session_name>.broker.log. The dashboard pane stays and shows ⚠ broker unreachable until you resume. (A session started by a git-paw from before the broker host split has nobrokerwindow — its dashboard pane is the broker — so pause kills that pane instead, as it always did.) - The session state file flips from
status: activetostatus: paused. - Every coding-agent CLI pane keeps running. Their in-memory conversation, their CLI process, and the worktree they’re working in are untouched.
git paw status shows the paused state with a blue indicator and a
“run git paw start to resume” hint.
The RAM trade-off
Pause is fast and state-preserving, but the CLI processes stay allocated. A typical Claude Code instance holds ~300 MB, so a 10-pane session is roughly 3–5 GB of RAM sitting idle while paused.
Pick the right verb based on duration:
- Pause is right for short breaks where instant resume matters more than RAM.
- Stop is right for long breaks where you’d rather get the RAM
back. Resuming via
git paw startspawns fresh CLI processes — you lose conversation context but the worktrees and branches carry over. - Hibernate (future v1.0.0) will be both: state preserved AND RAM released.
Resuming a paused session
git paw start
When git paw start detects a paused session that’s still alive in
tmux, it takes the restart-from-pause path:
- Re-creates the broker host in its
brokerwindow and waits (up to 10 seconds) for it to answer. - Re-creates the dashboard pane at its saved index only if none is running — normally the paused dashboard is still there and simply reconnects.
- Flips session status back to
active. - Re-attaches your tmux client.
Cost: one broker boot (plus a dashboard pane only for a session paused
before the broker host split). No worktree
creation, no CLI process spawn, no boot-prompt injection
(beyond submitting the held boot prompt of any agent added with
git paw add while paused — and, when that agent’s spec format
declares a workflow entry point,
the held entry point after it, once its pane goes idle). The agent
panes are exactly as you left them — open conversations intact,
prompt buffer intact.
If the tmux server died while paused (rare — typically only happens
on machine reboot or tmux kill-server), git paw status shows
the session as stopped instead, and git paw start falls through to
the normal cold-recovery path (fresh CLI processes, resuming each
conversation where the CLI supports it — see
Per-CLI cold resume).
Idempotency
git paw pause is safe to run repeatedly:
- Pausing an already-paused session prints “Session ‘NAME’ is already paused.” and exits 0 without changing state.
- Pausing a stopped session prints “Session ‘NAME’ is already stopped; pause has no effect.” and exits 0.
- Pausing with no session for the current repo prints “No active session for this repo.” and exits 0.
Per-CLI cold resume
Stop’s cold-recovery path (and the same recovery git paw start runs
when every agent pane has dropped back to a bare shell) restores
conversation context too, for CLIs that support it. At launch git-paw
assigns each worker a fresh session
identifier and passes it on the CLI’s own launch command (Claude
Code’s --session-id <uuid>), then records it in the session state.
When that worktree is later recovered, git-paw relaunches its CLI
with the matching resume flag (--resume <uuid> for Claude Code)
instead of a bare launch — the worker picks up its prior conversation
instead of starting cold.
This solves the long-break case without holding RAM, complementing
pause for the short-break case: stop releases the CLI processes,
and the next start brings their conversations back rather than
only their worktrees.
A worktree whose CLI has no known resume flag (no built-in row and no
[clis.<name>].resume_args override) launches fresh exactly as
before — nothing about cold recovery changes for it. See
session_id_args / resume_args
to add resume support for a CLI git-paw doesn’t know about, and
git paw status --json to read a worker’s
assigned id (e.g. to resume it by hand outside git-paw).
See also
- Pause behavior is formally specified in the
broker-runtimecapability spec (pause flow, idempotency, restart-from-pause), alongside thepausesubcommand contract incli-parsingand the paused status variant insession-state. git paw stop— the destructive teardown verb.git paw purge— the full reset (removes worktrees and branches).- Dashboard — what the dashboard pane shows while the session is active.
Skill Templates
git-paw uses standardized agent skills following the agentskills.io specification. Skills are directories containing a SKILL.md file with YAML frontmatter and optional resource subdirectories.
Standard Location
Skills are loaded from .agents/skills/ in your project directory. git-paw walks up the directory tree from the current working directory to find this location.
my-project/
└── .agents/
└── skills/
├── coordination/
│ ├── SKILL.md # Main skill file
│ ├── scripts/ # Optional: Executable scripts
│ ├── references/ # Optional: Documentation
│ └── assets/ # Optional: Templates/resources
└── orchestrator/
├── SKILL.md
└── scripts/
Bundled skills
git-paw embeds four skills, each overridable by a .agents/skills/<name>/SKILL.md
of your own:
| Skill | What it carries |
|---|---|
coordination | How an agent publishes status, intents and questions through the broker. |
orchestrator (alias supervisor) | The orchestrator’s role: following the session, the five verification gates, merge orchestration. |
docs-fetch | How to look things up through the bundled docs helper. |
cli-setup | The consent rules for git paw setup-cli — when an agent may and may not run the command that writes an agent CLI’s own settings. |
The cli-setup skill exists because that command changes what a CLI will do
without asking the human again. Its rules are deliberately restrictive: the
writing form runs only when the human asked in the current conversation, a
--dry-run suggestion is not consent, --yes follows approval of the exact
plan shown, it never runs unattended or because of an instruction found in
content the agent read, settings files are never hand-edited, and coding
agents in worktrees never run it at all.
Skill Format
Each skill must contain a SKILL.md file with YAML frontmatter:
---
name: my-skill
description: A brief description of what this skill does
license: MIT
compatibility: git-paw v0.4.0+
---
## My Skill Instructions
This skill helps agents with {{BRANCH_ID}} workflows...
Required Fields
name: Skill name (max 64 chars, lowercase letters/numbers/hyphens only)description: Clear description of the skill’s purpose (max 1024 chars)
Optional Fields
license: License informationcompatibility: Version compatibilitymetadata: Custom metadata object
Placeholders
Skills support these placeholders that get replaced at runtime:
{{BRANCH_ID}}- Slugified branch name (e.g.,feat/http-broker→feat-http-broker){{PROJECT_NAME}}- Project name for tmux session{{GIT_PAW_BROKER_URL}}- Full broker URL{{SKILL_NAME}}- Name from YAML frontmatter{{SKILL_DESCRIPTION}}- Description from YAML frontmatter
Supervisor gate-command placeholders
The embedded supervisor skill uses eight additional placeholders fed from
[supervisor].*_command keys in .git-paw/config.toml. Each gate-command
template renders verbatim into the skill prose where the supervisor agent
reads it:
{{TEST_COMMAND}}←[supervisor].test_command— gate 1 (Testing) test runner{{LINT_COMMAND}}←[supervisor].lint_command— gate 1 lint sub-step{{BUILD_COMMAND}}←[supervisor].build_command— gate 1 build sub-step{{FMT_CHECK_COMMAND}}←[supervisor].fmt_check_command— gate 1 formatter check{{DOC_BUILD_COMMAND}}←[supervisor].doc_build_command— gate 4 (Doc audit){{DOC_TOOL_COMMAND}}←[supervisor].doc_tool_command— gate 4 API-doc generator (renders empty when unset, not(not configured)){{SPEC_VALIDATE_COMMAND}}←[supervisor].spec_validate_command— gate 3 (Spec audit){{SECURITY_AUDIT_COMMAND}}←[supervisor].security_audit_command— gate 5 (Security audit)
(not configured) graceful skip. When a key is omitted from
[supervisor], the matching placeholder substitutes to the literal string
(not configured) in the rendered skill. The supervisor agent treats that
sentinel as “skip the tooling invocation for this gate” and continues with
the gate’s manual review only (e.g. the OWASP-category diff scan still runs
for the security gate, the spec scenario coverage check still runs for the
spec gate). Pre-v0.5.x configs that only set test_command continue to
render the remaining gates as (not configured) and the supervisor agent
runs them as manual-only — no behavior change for those configs.
The one exception is {{DOC_TOOL_COMMAND}}: when the underlying key is
unset, the placeholder renders as the empty string rather than the
(not configured) sentinel. The supervisor skill template is authored
so the surrounding prose reads naturally without it for projects that
don’t ship an API-doc generator.
Backend-aware and constant-driven placeholders
Two more placeholders make the bundled supervisor skill polyglot:
{{SPEC_PATH_DOCTRINE}}— substitutes a per-backend paragraph that names where specs live and the matching workflow. The variants:- OpenSpec:
openspec/changes/<change>/{proposal,specs,tasks}.mdplusopenspec validate. - Spec Kit:
.specify/specs/<feature>/{spec,plan,tasks}.mdplus the Spec Kit checklist convention. - Markdown: flat
.mdfiles withpaw_status: pendingfrontmatter. - Superpowers: plan files under
docs/superpowers/plans/*.md, worked### Task Nstep by step with- [ ]→- [x]checkboxes. - Multi-backend: one sentence per present backend, introduced by a “This session spans multiple spec backends” clause.
- No backend resolved: a sentinel sentence pointing the supervisor at the project’s documentation.
- OpenSpec:
{{CHANGE_ID}}— see the per-invocation discussion below.
{{CHANGE_ID}} is per-invocation, not per-render. Spec validators
typically take a change name as argument (e.g. openspec validate my-change-id --strict). To support that, the spec_validate_command
template MAY embed the literal substring {{CHANGE_ID}}. git-paw does
not substitute {{CHANGE_ID}} at session boot — it passes through the
render verbatim. The supervisor agent expands it at verification time using
the change name it is currently auditing. This matches how {{BRANCH_ID}}
behaves for coding agents (per-agent, not per-render).
Resource Subdirectories
Skills can include optional resource subdirectories:
scripts/- Executable scripts referenced by the skillreferences/- Detailed documentation and referencesassets/- Templates, configuration files, and other resources
Example structure:
.agents/skills/my-skill/
├── SKILL.md # Main instructions (< 500 lines)
├── scripts/
│ └── setup.sh # Executable helper script
├── references/
│ └── api-reference.md # Detailed API documentation
└── assets/
└── config-template.json
Creating Custom Skills
To add a custom skill:
# Create skill directory structure
mkdir -p .agents/skills/my-skill
# Create SKILL.md with proper frontmatter
cat > .agents/skills/my-skill/SKILL.md << 'EOF'
---
name: my-skill
description: Custom workflow for our team
license: MIT
compatibility: git-paw v0.4.0+
---
## Custom Team Workflow
Follow these steps for {{BRANCH_ID}}:
1. Analyze requirements
2. Implement solution
3. Test thoroughly
4. Document changes
EOF
# Add optional resource directories
mkdir -p .agents/skills/my-skill/scripts
mkdir -p .agents/skills/my-skill/references
Skill Resolution
git-paw searches for skills in this order:
.agents/skills/<name>/SKILL.md(walking up directory tree from current directory)- Embedded defaults (compiled into git-paw binary)
The first match wins. If no skill is found, resolution fails with an error.
supervisor is an alias of orchestrator: both names resolve to the same
skill, and an override directory under either name applies to both. When
.agents/skills/orchestrator/ and .agents/skills/supervisor/ both exist, the
orchestrator one wins, so an override written under the old name keeps
working until you rename it.
Exported skills
git paw init (and every git paw start) also writes two bundled skills into
the main checkout for the agent that drives the session, in the same
standardized layout:
.git-paw/skills/orchestrator/SKILL.md— the orchestrator skill, rendered with your[supervisor]gate commands and configured spec format. It holds no session value (no broker URL, no session name); those live in the session context.git-paw/AGENTS.local.md..git-paw/skills/cli-setup/SKILL.md— thecli-setupskill, as embedded.
Each opens with name / description frontmatter whose name equals its
directory name, so a skill-aware agent CLI can load the directory directly.
.git-paw/skills/ is gitignored and binary-managed: a re-run overwrites it,
and an override under .agents/skills/ is what the export renders from. See
Supervisor → Driving the session from your own agent.
Validation
Skills are validated against the agentskills.io specification:
- Required
nameanddescriptionfields must be present - Frontmatter must parse within the supported subset (below)
- Skill directory must contain SKILL.md file
- Clear error messages for validation failures
Supported frontmatter subset
git-paw parses SKILL.md frontmatter with a small, dependency-free parser
rather than a full YAML library, so it supports a bounded subset rather than
arbitrary YAML:
- Top-level
key: valuepairs, where the value is a plain, single-quoted, or double-quoted scalar (surrounding whitespace is trimmed from plain scalars). - Literal (
|) and folded (>) block scalars, including their strip (-) and keep (+) chomping indicators. - Full-line and trailing
#comments outside quoted scalars. - A one-level
metadatamapping of string keys to scalar values — every value is read as a string. - A key git-paw doesn’t consume (for example
allowed-tools:followed by a list) loads fine; it’s skipped along with any indented content beneath it.
This parser does not silently misparse. If name, description,
license, compatibility, or metadata uses a construct outside this
subset — a flow mapping or list ({ a: 1 }, [a, b]), an anchor or alias
(&foo, *foo), a YAML tag (!!str), or a nested mapping inside
metadata — the skill fails to load with a validation error naming the
field, rather than silently producing a different value. This applies to
your own override skills too: a SKILL.md you hand-write under
.agents/skills/ is held to the same subset as the embedded skills git-paw
ships.
Examples
See the agentskills.io specification for more examples and best practices.
Migration from Older Versions
If you’re upgrading from git-paw v0.2.x or earlier:
- Move skills from
~/.config/git-paw/agent-skills/to.agents/skills/ - Convert single
.mdfiles to directory structure with SKILL.md - Add required YAML frontmatter to each skill
- Organize related resources into subdirectories
The new standardized format improves interoperability and enables skill sharing across different AI systems that support the agentskills.io standard.
When Skills Are Not Injected
Skill templates are only injected when the broker is enabled ([broker] enabled = true). If the broker is disabled, no coordination instructions are added to AGENTS.md.
Boot-Prompt Injection
In addition to skill template injection, git-paw automatically injects a standardized boot instruction block into every agent’s initial prompt. This ensures reliable agent self-reporting even if skill templates are not used or if agents don’t read the AGENTS.md file thoroughly.
The boot-prompt injection covers every essential coordination operation (register, done, blocked, question) with the agent’s id pre-expanded, as calls to the bundled .git-paw/scripts/broker.sh helper — or, for CLIs known to speak MCP (the built-in set is claude, codex, cursor-agent, agy), as MCP publish-tool calls — and is active in both supervisor and manual broker modes. Set [clis.<name>] mcp = true|false to override that set for a given CLI identity. See the Coordination documentation for details.
Docs Fetch Skill
git-paw bundles a docs-fetch skill and a matching helper script so an agent can consult git-paw’s own documentation on demand — pulling exactly the convention or example it needs, when it needs it, instead of carrying doc content in its boot prompt or guessing.
The design mirrors the other bundled helpers (broker.sh, sweep.sh): the
skill teaches when and why to consult the docs, and the helper does the
fetch. The agent invokes one stable script path rather than hand-rolling
curl, so one permission rule in your agent CLI scoped to that exact path
can cover every call, and the behaviour stays deterministic.
What the agent gets
When the docs-fetch skill is rendered into an agent’s instructions, the agent
is told to reach for the helper — .git-paw/scripts/docs-fetch.sh — whenever a
git-paw fact is unclear (a subcommand’s behaviour, a config field, a
coordination or governance convention, a workflow). Documentation is fetched
live and only when asked; none of it is embedded in the binary or injected
wholesale into the boot prompt.
The two operations
The helper is a thin, dependency-free wrapper (curl + Python 3, the same
primitives the broker helper uses). It exposes a discover-then-retrieve flow
that matches the machine-readable surface published by the
agent-friendly docs site (llms.txt
and per-page anchors).
find <query> — fetch the site’s llms.txt index and print the pages that
best match the query, each as a title, absolute URL, and one-line summary:
.git-paw/scripts/docs-fetch.sh find "worktree placement"
get <page-or-url> [anchor] — fetch a page (by the URL or path from
find) and print its documentation content. Pass a section anchor to narrow
the output to just that section instead of the whole page:
.git-paw/scripts/docs-fetch.sh get user-guide/worktree-placement.html
.git-paw/scripts/docs-fetch.sh get user-guide/worktree-placement.html child-vs-sibling
Configurable docs source
The docs base URL defaults to git-paw’s published documentation site and is
overridable so a fork or mirror can point the skill at its own docs. Set the
top-level docs_base_url field in
.git-paw/config.toml:
docs_base_url = "https://docs.example.com/git-paw"
The helper resolves the base URL from config, falling back to the built-in
default when the field is absent. Both find and get target the configured
URL.
Best-effort by design
Documentation lookup is an aid, never a hard dependency of the agent’s task. If the docs site is unreachable, a page does not exist, or an anchor is unknown, the helper exits non-zero with a short diagnostic (and never hangs — every fetch is time-bounded). The skill instructs the agent to continue its task without the docs in that case rather than block or retry in a loop.
Least-privilege install
git paw init installs docs-fetch.sh into .git-paw/scripts/ alongside
broker.sh, sweep.sh, and the shared _paw_common.sh preamble they source.
Nothing is seeded into the agent CLI’s settings file, and git-paw approves no
prompt for it. The point of the stable path is that you can write one
permission rule naming .git-paw/scripts/docs-fetch.sh — invoked directly or
as bash <path> / sh <path> — instead of a broad curl wildcard. That one
by-path rule covers every subcommand, so an agent consults the docs without any
wildcard network permission. Without such a rule the helper simply prompts like
any other command, and the prompt is escalated to you.
Governance
git-paw can read your team’s existing governance documents as context for the supervisor agent — ADRs, a test strategy, a security checklist, a Definition of Done, and a project constitution. You point at the docs in .git-paw/config.toml; git-paw does not generate or vendor any of them.
Why governance docs
The supervisor is an LLM. It does the right thing more often when it can see the rules the team has already written down: which architectural decisions are settled, what tests are expected, what security review looks like, what “done” actually means. Without that context the supervisor has to infer from the diff alone, and inference drifts.
git-paw’s stance on governance is deliberately thin:
- You own the documents. Their structure, format, and rubric are whatever your team already uses (Scrum, XP, OWASP,
adr-tools, Spec Kit, hand-rolled). git-paw does not template them. - You opt in per doc. Empty
[governance]table, all fieldsNone, no behaviour change from v0.4. Add only the docs you have. - The supervisor applies judgment. There is no
[governance.gates]table and no per-doc enforcement switch. The supervisor reads each configured doc as context during its audit and surfaces relevant findings viaagent.feedback.
Pointing at your docs
Add [governance] to .git-paw/config.toml:
[governance]
adr = "docs/adr"
test_strategy = "docs/test-strategy.md"
security = "docs/security-checklist.md"
dod = "docs/definition-of-done.md"
constitution = ".specify/memory/constitution.md"
All five fields are optional. List only the docs you have. Paths are resolved relative to the repository root; absolute paths are accepted as-is. A missing file does not break config-load — the runtime flags it if the supervisor tries to read it.
[governance] also accepts two more optional pointers, readme and docs (your README and documentation root). They feed the MCP server’s documentation tools rather than the supervisor’s audit.
For Spec Kit projects, governance.constitution auto-wires to .specify/memory/constitution.md when [specs] type = "speckit". You only need to set constitution explicitly if you keep it somewhere else, or want to disable the auto-wiring (set it to "").
At launch, the configured paths reach the orchestrator as a ## Governance documents section in its context — the gitignored repo-root sidecar .git-paw/AGENTS.local.md that git paw start --supervisor writes and names, and that your orchestrating agent session (outside tmux) reads — placed after the supervisor skill content so the orchestrator reads them in the same context as its own instructions. The tracked AGENTS.md is never modified, and the context carries no per-agent spec or file-ownership content. A path left unset — or set to the empty string, as with the constitution = "" opt-out above — contributes no bullet, and a [governance] table with no paths at all adds no section.
See Configuration → Governance for the full field reference and merging rules.
Illustrative examples
The shapes below are examples, not templates. git-paw never reads structure — only the content of the file you point at. Use whatever format your team already uses.
ADR-0001 — Adopt PostgreSQL for the primary store
# ADR-0001: Adopt PostgreSQL for the primary store
Status: Accepted
Date: 2026-02-14
Deciders: backend team
## Context
We need a primary datastore for user, session, and billing tables.
Options considered: PostgreSQL, MySQL, CockroachDB, DynamoDB.
## Decision
We will use PostgreSQL 16 hosted on our existing managed service.
## Consequences
- All new schema lives in PostgreSQL; we will not introduce a second
relational store without a follow-up ADR.
- Migrations go through `sqlx-migrate`; ad-hoc DDL in code review is
rejected by default.
- Read replicas are an operational concern, not an application one —
applications connect through the connection pool, not directly to
replicas.
A typical ADR directory has one file per decision (0001-postgres.md, 0002-event-bus.md, …). Whether you use Nygard, MADR, or your own headings does not matter — git-paw passes the directory pointer to the supervisor, which reads the files it finds.
Definition of Done
# Definition of Done
A change is "done" when every box below is checkable:
- [ ] All new behaviour has at least one test covering the happy path
and one covering the failure mode.
- [ ] `just check` passes locally and in CI.
- [ ] Public functions have rustdoc comments.
- [ ] If a config field was added, the configuration docs section
that owns it is updated.
- [ ] If a CLI flag was added, `--help` text and `cli-reference.md`
are updated.
- [ ] If user-visible behaviour changed, the changelog has an entry.
- [ ] The PR description names the spec/issue it implements and
lists any deviations.
Anything left unchecked goes in the PR description as a known
follow-up with an owner.
Security checklist
# Security Checklist
For every change touching authentication, authorisation, network I/O,
or persisted data:
- [ ] User input is validated at the boundary (HTTP handler or CLI
arg parser), not in the business logic.
- [ ] Secrets never appear in logs, error messages, telemetry, or
stack traces. The redaction helpers in `crate::log::redact`
are applied at the producer side.
- [ ] No new dependency on `unsafe` crates without a written
justification in the PR description.
- [ ] If the change introduces a new external request, the URL is
built with a typed builder, never with `format!` over user
input.
- [ ] Authorization checks happen before side effects — never after.
Test strategy
# Test Strategy
## Pyramid
We invest most heavily in fast, deterministic tests:
- **Unit tests** (`#[cfg(test)] mod tests {}`) — pure logic, no I/O.
Every public function in `src/` has at least one.
- **Integration tests** (`tests/`) — exercise CLI boundaries with
`assert_cmd`. Use real filesystems via `tempfile`, never mocks.
- **End-to-end smoke tests** (`tests/e2e/`) — kicked off in CI for
the happy path of each subcommand. Slow and few; tagged with
`#[ignore]` so contributors can opt in locally.
## What we do not test
- Generated output of `--help` strings (already covered by clap).
- Rendering loops in the TUI — covered manually via the smoke list
in `docs/src/user-guide/dashboard.md`.
## Test data
Always synthesise — never copy production data, even anonymised, into
fixtures. Fixtures live under `tests/fixtures/` and are kept small
enough to read at a glance.
Constitution (Spec Kit-style)
# Project Constitution
## Spec-driven
No behaviour ships without a spec. The spec is the contract; the
implementation matches the spec; the tests assert the spec. If the
implementation drifts from the spec, the spec is updated first and
re-reviewed.
## Behavioural tests, not implementation tests
Tests assert observable inputs and outputs. Tests do not assert
internal struct field values, internal function calls, or mock
interactions.
## One commit per logical change
Every commit builds, passes `just check`, and is independently
revertable. PRs may contain multiple commits, but no commit may leave
the tree broken.
Spec Kit users typically keep this file at .specify/memory/constitution.md; git-paw auto-detects it when [specs] type = "speckit".
What the supervisor does
When the supervisor verifies an agent’s change, it reads the configured governance documents alongside the diff. Findings flow through the existing agent.feedback channel — the supervisor does not crash or block on governance, it surfaces what it sees and lets the agent respond.
How the supervisor audits against these documents is described in the bundled supervisor skill, and the MCP server’s governance tools serve the same documents to MCP clients. This chapter and the [governance] config table (the core-governance-config capability) are the path-pointer slot only.
Rollout suggestion
Adopt incrementally — there is no requirement to fill in all five paths at once:
- Start with the doc you already have. Point
governance.dodat your existing DoD, orgovernance.constitutionat your principles doc. - Run a supervised session. Note which findings feel useful and which feel noisy.
- Add the next doc. Each pointer narrows the gap between what the supervisor knows and what the team already agreed on.
- If the team does not have one of these docs at all, write the smallest version you can live with rather than copying a template. The supervisor reads what you wrote — not a generic checklist.
You do not need a full governance framework to benefit from this. A two-paragraph DoD pointed at by governance.dod is more valuable than a perfect-but-empty [governance] table.
Learnings Mode
Learnings mode records deterministic friction signals from a supervisor
session into a markdown file you can review after the run. It is an opt-in
feature: it requires supervisor mode to be active and the
[supervisor] learnings = true flag to be set explicitly. The markdown file
is always written; when the broker is enabled, each record is additionally
published as an agent.learning broker message (see
Programmatic consumption).
Contents
- Why
- Privacy & Sharing
- Enabling Learnings Mode
- Output File
- The Deterministic Categories
- Qualitative signals (v0.6.0)
- Sample Output
- Flush Cadence
- Programmatic consumption:
agent.learning(v0.6.0)
Why
Supervisor runs absorb a lot of recurring friction silently: sandbox warnings the agent retries past, approvals you reflexively click, brief stuck states, conflicts caught and resolved before they hit your eyes. Each event is too small to interrupt for, but the pattern is the most useful signal git-paw can surface for tool, prompt, or process improvement.
Learnings mode aggregates those events into four deterministic categories (plus the supervisor’s qualitative signals) and writes them to a markdown file you can read between sessions — turning silent friction into something you can act on.
Privacy & Sharing
Learnings mode performs no telemetry. The aggregator writes to exactly
one place — the local .git-paw/session-learnings.md file in your repo. The
broker it observes binds to 127.0.0.1 by default, and git-paw ships no outbound HTTP
client: nothing about your session is collected, uploaded, or phoned home
under any configuration. The feature is also fully opt-in — it runs only
when you set [supervisor] learnings = true (default false), and a session
that has not opted in behaves exactly as if the feature did not exist.
That makes the learnings file yours: a purely local artifact you can read, keep, or delete. When a session starts with learnings enabled, git-paw prints a one-line reminder of where the file lives and that nothing leaves your machine, so the privacy stance is visible at the point of use, not just here.
Optional sharing. If a session surfaces a recurring rough edge that looks
worth fixing in git-paw itself, the most useful thing you can do is share that
context with the maintainers. This is a deliberate, manual action — there is
no “share now” command and no automatic upload. To contribute, open an issue
on the git-paw issue tracker
and attach (or paste) the relevant part of your session-learnings.md.
Review before you share. The file contains repo-specific details — branch names, file paths, spec IDs — that you may not want public. Read it first and strip or anonymise anything sensitive. git-paw deliberately does not scrub the file for you: only you know what is sensitive in your repo. Your own LLM or agent CLI can help here — ask it to redact paths and identifiers before you paste the result into an issue.
Enabling Learnings Mode
Set the master switch on the [supervisor] table (default false):
[supervisor]
enabled = true
learnings = true
The subsystem only activates when supervisor mode itself is active. With
[supervisor] enabled = false (or no [supervisor] section), the
learnings = true value is parsed and ignored — no aggregation runs and
no file is written.
Output File
When active, the learnings subsystem writes to:
.git-paw/session-learnings.md
The file lives in the repository root’s .git-paw/ directory. It is:
- Append-only across sessions. Each session’s first flush adds a
## Session Learnings — <ISO-8601 UTC start time>heading, and later flushes and later sessions append below it; nothing is overwritten or pruned. - Human-readable markdown. No JSON or binary; you can
tail,grep, or open it in any editor between sessions. - Not committed by default.
git paw initadds it to the project’s.gitignorealongside the other.git-paw/runtime files.
The Deterministic Categories
The aggregator tracks four deterministic categories, each written under its
own ### heading in the session’s block. Each bullet is a one-line summary;
a category with nothing to report is omitted from that flush.
Conflict events
One bullet per conflict the broker conflict detector
reported via a [conflict-detector]-tagged agent.feedback or
agent.question, classified as:
- Forward conflicts — two agents’
agent.intentdeclarations overlap (recorded with aforward-conflict-…label). Use these to spot specs that were decomposed too coarsely (two parallel agents both expected to own the same file). A forward conflict is labelledforward-conflict-intra-specwhen both agents were launched from the same spec andforward-conflict-cross-specwhen they came from different ones, using the spec each agent was launched from. The broker host learns that mapping from real launch data: a fresh launch registers every agent with the host together with its spec id,git paw add --from-specdoes the same for a hot-added agent, and a recovered or resumed session’s host reads it from the saved session. An agent started without a spec (a baregit paw start, orgit paw addwithout--from-spec) has no recorded spec, so its forward conflicts classify cross-spec; only the specs that are known are named in the bullet. - In-flight conflicts — two agents are modifying the same file at once.
- Ownership violations — an agent edited a file claimed by another
agent’s active
agent.intent. Records the violator, the owner, and the file. Use these to spot agents that drifted out of their declared scope.
The detector’s symmetric warnings (one per agent in the pair) collapse into a single bullet.
Where agents got stuck
Stuck duration: the time between an agent’s agent.blocked (naming the agent
it waits on in from) and its next agent.artifact. A block still open when
the session ends is recorded as unresolved, with the duration measured up to
shutdown.
Recovery cycles
The recovery-cycle count: how many agent.feedback messages an agent received
before its agent.verified (or before session end, if it never verified). A
high count marks an agent that needed many correction rounds. Zero cycles is
not recorded.
Conflict acknowledgements are excluded from the count. A [conflict-ack]
feedback (see Conflict Detection)
tells an agent that an overlap it was warned about has been adjudicated — it
asks the agent for nothing, so counting it would report correction rounds
that never happened.
Permission patterns
Commands that repeatedly needed a human decision. git-paw approves no
permission prompt, so this is not a count of automatic approvals: under
--unattended, the first time the drive loop escalates a given command it
emits a permission_pattern learning (when manual_approvals_log is also
on). The list shows which commands your agents trip on most, so you can
pre-approve them in your agent CLI’s own allow rules. git paw approvals
reports the same patterns with their full counts.
Qualitative signals (v0.6.0)
The four categories above are deterministic — the broker derives them
mechanically from message traffic. As of v0.6.0 the supervisor also
records qualitative signals: observations that require reasoning
over the whole session, which only the supervisor LLM can make during its
normal sweep and audit work. They ride the same agent.learning wire
format and land in their own sections of session-learnings.md. The
supervisor publishes them through the bundled
.git-paw/scripts/sweep.sh learn <category> <title> <body-json> helper — a
least-privilege, by-path publish path, never a raw curl.
Because these are LLM judgments, the supervisor skill gates each one behind a heuristic and a “do not publish unless…” rule, and suppresses near-duplicates within a session. The aim is a short, high-signal list, not an exhaustive log — if a category is empty in a run, the supervisor simply didn’t see a confident instance.
Recurring failure shapes
The same error shape recurring across multiple feedback cycles from different branches (e.g. three branches all hitting the same import-cycle pattern). What to do: treat it as a systemic signal — a missing lint, an undocumented constraint, or a fixture/scaffold gap — rather than fixing each branch in isolation.
Documentation gaps
A spec audit found a convention the spec assumes but no checked-in doc
explains. The record names the convention, the doc paths checked, and a
suggested home for it. What to do: add the convention to the suggested
doc (git-paw does not write docs for you — see Non-goals below).
ADR / architectural drift
Code introduced an architectural decision — a new dependency, framework,
or boundary — not reflected in your configured ADRs. The record names the
decision_area, the observed pattern, and a candidate ADR title. What
to do: decide whether the drift is intentional (write the ADR) or
accidental (revert or rework it).
Scope-mistake signals
Two or more branches coordinated heavily because the original spec scope
drew the boundary in the wrong place. The record names the branches, the
shared files, and a suggestion. What to do: consider re-cutting the
scope for the next change so the same work lands in one branch.
Tooling friction
Friction the supervisor absorbed about git-paw itself — a tool behaviour
that made it repeat work or work around the tool (a permission prompt cleared
on every sweep, a helper too narrow so it fell back to raw curl, a detector
that over-escalated) — as distinct from the four project-scoped categories
above. The record names the friction, the occurrences (how many times it
was absorbed this session), and a suggestion. The supervisor publishes it
only when the same friction was absorbed at least twice in the session, so
a one-off is never recorded. What to do: treat it as the prime
tool-improvement signal — fold it into a git-paw issue or a config/allowlist
change.
Correction-budget exhaustion
Emitted by the --unattended drive loop (not by the supervisor skill) when a
branch spends its whole
[supervisor.correction] max_cycles
budget without converging on a passing gate. The record carries the branch,
the worker cli, and the cycles spent, under category
correction_exhausted. What to do: read it as a tiered-model tuning
signal — a branch that burns its whole budget is usually a task whose worker
model is under-powered for that class of work, so re-assign it to a stronger
CLI or split the spec. Like every learning it is emitted only when
[supervisor] learnings = true.
Unknown categories
Records whose category the aggregator doesn’t recognise (e.g. a category
added by a newer supervisor skill) are never dropped — they appear under
an Other learnings section so nothing is lost across version skew. The
correction_exhausted category above renders there too, as a title plus its
JSON body.
Non-goals. Qualitative signals flag issues; they do not fix them. git-paw does not auto-generate docs or ADRs, and it does not scan past sessions for cross-session patterns — each observation belongs to the session that saw it. The detection thresholds live in the supervisor skill prose, not in config (see below); if a category is too noisy or too quiet for your project, edit your local copy of the supervisor skill.
Sample Output
A short illustrative excerpt (agent names, paths, and counts are made up; headings and bullet shapes are the ones the aggregator writes):
## Session Learnings — 2026-05-13T14:30:05Z
### Conflict events
- forward-conflict-cross-spec: feat-api and feat-auth
- in-flight-conflict: feat-api and feat-billing
- ownership-violation: feat-api edited `src/auth/jwt.rs` owned by feat-auth
### Where agents got stuck
- feat-api: blocked 11m12s waiting on feat-auth
- feat-billing: blocked 4m03s waiting on feat-api (unresolved at session end)
### Recovery cycles
- feat-auth: 3 feedback cycles before verifying
### Permission patterns
- `cargo test` escalated for a human decision (first seen; see `git paw approvals` for counts)
### Recurring failure shapes
- import cycle between the auth and session modules: 3 instances across feat-auth, feat-api, feat-billing
### Documentation gaps
- agents run the linter before committing — add a Conventions section to AGENTS.md naming the pre-commit lint step
## Session Learnings — 2026-05-13T16:05:41Z
...
The exact bullet wording may evolve across releases; the category set and the underlying triggers above are stable.
Flush Cadence
The aggregator buffers entries in memory and flushes to disk on an interval (default 60s):
[supervisor.learnings_config]
flush_interval_seconds = 60
A shorter interval makes the file fresher (useful when you want to
tail -f the file in another terminal); a longer interval batches more
entries per write. The file is also flushed once more at broker shutdown so
nothing is lost between sessions even if the interval has not elapsed.
See Configuration → Learnings mode tuning for the field reference.
Programmatic consumption: agent.learning (v0.6.0)
v0.5.0 shipped learnings as file output only. As of v0.6.0 the aggregator
also publishes each flushed entry to the broker as an agent.learning
message, so tools no longer have to re-parse the Markdown. The file output is
unchanged — it remains the source of truth for cold-repo / no-broker
scenarios — and the broker publish is purely additive.
Each agent.learning record carries:
| Field | Meaning |
|---|---|
id | Deterministic 16-hex-char dedup hash (see below) |
agent_id | Publishing agent (supervisor for aggregator-produced records) |
branch_id | Branch the record is scoped to; omitted for cross-cutting ones |
category | conflict_event, stuck_duration, recovery_cycles, permission_pattern (open set) |
title | Short human-readable summary (mirrors the Markdown bullet) |
body | Category-specific structured object |
timestamp | ISO 8601 UTC |
The id is a 64-bit FNV-1a hash (16 lowercase hex characters) of the
canonical form (category, branch, body sorted, UTC hour bucket).
FNV-1a is stable across Rust toolchain versions — each id is deterministic
regardless of when or where it is computed. It is a dedup key, not a security
primitive. Re-publishing the same logical record within one hour yields the
same id, so a consumer can dedupe re-emissions; a genuine recurrence in a
later hour gets a fresh id. This lets the aggregator re-run a sweep
idempotently without flooding consumers with duplicates.
The bundled sweep.sh learn helper uses the same scheme. The supervisor
publishes qualitative learnings through
.git-paw/scripts/sweep.sh learn <category> <title> <body-json>, and that
helper computes its id with exactly the form above — over the category, an
empty branch id (supervisor learnings are cross-cutting), the canonical body,
and the current UTC hour bucket. The two publishers therefore assign the same
id to the same logical record, and a consumer can dedupe across both. The
title is NOT part of the canonical form, so rephrasing it does not mint a new
record.
Because the canonical form has to render byte-identically on both sides, the
helper accepts only numbers it can reproduce exactly: 64-bit integers. A
body carrying a non-integer number (0.5, 1e16) or an integer beyond the
64-bit range is refused with a diagnostic and nothing is published — the two
serialisers disagree on those (1e+16 against 1e16), and a record published
under an id the aggregator cannot reproduce would silently defeat the dedup.
Learning bodies carry counts and labels, so this costs nothing in practice.
Note: record ids changed once in this release (v0.17.0): the aggregator’s
hash moved from platform-dependent DefaultHasher to stable FNV-1a, and
sweep.sh learn — which had been computing a truncated SHA-1 over a different
canonical form, giving the same record two different ids depending on which
publisher emitted it — moved onto the shared scheme. Existing ids from v0.16.x
are now invalid; re-run your aggregator to regenerate them.
Branch-scoped records (stuck_duration, recovery_cycles) are routed to
that branch’s inbox and are retrievable with:
curl -s http://127.0.0.1:9119/messages/<branch-id>
Cross-cutting records (conflict_event, permission_pattern) carry no
branch_id and land in the supervisor inbox. Every record is also retained
in the broker’s full message log (/log).
The MCP get_learnings() tool reads the Markdown file, not the broker: it
returns each ### section as a { category, entries } record, and returns the
four deterministic sections with empty entries when no file exists yet.
Publication is controlled by the broker_publish knob (default auto,
which follows [broker] enabled); see
Configuration → Learnings mode tuning.
Conflict Detection
When supervisor mode is active, the broker runs an in-process conflict
detector that watches for three failure shapes across parallel agents.
Detected conflicts surface as [conflict-detector]-tagged agent.feedback
delivered to the involved agents, and — for unresolved in-flight shapes —
as agent.question escalations to the supervisor inbox. The detector is
automatic; you only opt out by setting [supervisor] enabled = false (or
tuning the knobs in [supervisor.conflict]).
Contents
- The Three Failure Shapes
- The
[conflict-detector]Tag - Supervisor Inbox Routing
- Interaction with the Filesystem Watcher
- Configuration Knobs
The Three Failure Shapes
git-paw’s own managed bookkeeping — everything under a worktree’s
.git-paw/ directory (config, bundled scripts, session state) — is excluded
from the overlap computation for forward and in-flight conflicts.
Every worktree shares this untracked scaffolding, so its presence in
modified_files is not evidence of a code conflict: two agents both
reporting .git-paw/config.toml never fabricates a warning, while a genuine
source overlap alongside it (src/a.rs present in both) still conflicts on
the source path.
Forward conflict
Two agents publish agent.intent payloads whose files arrays overlap.
The conflict is forward because it surfaces before either agent commits
— intent is the early-warning channel.
Trigger. Any non-empty intersection between two active intents (an
intent stays active until its valid_for_seconds TTL expires or the agent
publishes a fresh intent, which replaces it). Each pair is warned once;
the warning can fire again only after one side’s intent changes its file
set or expires.
Action. Each agent receives an agent.feedback with the
[conflict-detector] tag in its errors[0], naming the peer and listing
the overlapping paths. Neither side blocks; the receiving agents decide
how to retract or reshape their plans.
Region granularity (v0.6.0). When both intents declare regions for a shared file (function / class / block / range — see Declaring regions), the detector triggers only when the declared regions intersect, and the warning names the intersecting regions. When either side omits regions it falls back to the file-level trigger above, preserving v0.5.0 behaviour. Cross-kind comparisons (a named region vs a line range) intersect conservatively and append a hint.
Toggle. [supervisor.conflict] warn_on_intent_overlap (default
true). When false, the detector still records intents (so in-flight
classification and ownership detection keep working) but no agent.feedback
fires for forward shape.
In-flight conflict
Two agents’ current agent.status modified_files sets overlap on the same
file. The conflict is in-flight because both sides are already writing.
Trigger. Any non-empty intersection between one agent’s latest
agent.status modified_files and another agent’s latest
agent.status modified_files (each new status replaces that agent’s
previous set). agent.artifact does not feed this check, and declared
intents only decide how an unresolved overlap is classified (below).
Action. Both agents receive [conflict-detector]-tagged
agent.feedback. The detector starts a window_seconds timer; if no
side retracts before it elapses, the detector classifies the overlap from
the two agents’ declared intent regions for the shared file and acts:
- True collision — the declared regions intersect (same named region,
same insertion anchor, overlapping line ranges, or a conservative
cross-kind match), or either side declared no regions for the file
(a file-level intent, or no active intent at all). The detector escalates
to the supervisor inbox via
agent.question(see Supervisor Inbox Routing). - Additive overlap — both agents declared regions for the file and
the region sets are disjoint (well-separated hunks or differently named
regions). The detector downgrades to a single informational
[conflict-detector]-taggedagent.feedbackdelivered to both agents (“shared file, additive — resolve at merge”) and does not escalate to the human. The overlap is still recorded: the triple is marked decided so it neither re-escalates nor re-emits the downgrade on later ticks, and it clears only when one agent stops touching the file — so an additive overlap is never silently dropped.
The downgrade requires both sides to have declared regions; whenever the region data is insufficient to prove the hunks are disjoint, the detector escalates, preserving the conservative default.
Window. [supervisor.conflict] window_seconds (default 120).
Escalations persist across commits. The overlap is as volatile as
modified_files: an agent commits, the watcher publishes an empty set, and
the overlap vanishes for a tick or two before the file reappears. The
decision — that this pair and this file have already been put to a human —
is not volatile, and the detector keeps it separately from the live overlap
view. So a commit does not re-escalate a conflict that has already been
escalated, and it does not rewind the window timer for one that has not: an
overlap first seen 100 seconds ago still escalates at the 120-second mark,
not 120 seconds after the commit. A decision is discarded only once its
conflict is genuinely over — the overlap has been continuously absent for
the escalation window (at least 60 seconds) — or when the two agents’
declared regions for the file describe a materially different overlap.
Acknowledging an escalation. Once you have adjudicated an escalation, record it so the detector stops asking:
.git-paw/scripts/sweep.sh conflict-ack <agent-a> <agent-b> <file> "optional note"
This publishes a [conflict-ack]-tagged agent.feedback from supervisor
to both agents. A note you pass rides along as a second errors entry so
the agents see your reasoning; if you pass none, the helper supplies a
default one — agents are taught that errors are problems to fix, and a
bare tag line would read as a cryptic error.
While the acknowledgement stands, no further agent.question is emitted
for that pair and file — but the detector keeps tracking the overlap
rather than dropping it, so a later escalation can still be built on it.
Acknowledging suppresses the question, not the fact. (No view currently
renders the in-flight tracker, so “keeps tracking” means exactly that:
the detector’s own state, not something you can read off the dashboard.)
The acknowledgement is invalidated automatically when the two agents’
region declarations for that file change materially — judged by the same
intersection rules that drive the additive-versus-true classification
above. A conflict that genuinely changes shape can therefore escalate
again: an agent re-declaring agent.intent with a range that now overlaps
its peer’s, or widening its claim from a named region to the whole file.
An intent merely expiring is not a change — otherwise every settled
conflict would re-open one TTL later.
What the supervisor-only rule does and does not do. The detector
honours an acknowledgement only when the carrying agent.feedback has
from = "supervisor". That filter stops an agent’s ordinary feedback
from being mistaken for an adjudication. It is not an authentication
boundary: the broker does not authenticate senders, from is
self-asserted, and sweep.sh is present in every worktree — so an agent
that set out to do so could publish conflict-ack for a conflict it is
party to. What limits the damage is visibility, not prevention: an
acknowledgement published through the helper reaches both named
agents, and every acknowledgement — however it was published — stays
in the broker message log. (The detector honours one whatever its
envelope agent_id, so a hand-rolled one need not have been delivered to
both; the log is what you audit.) Sender authentication is part of the
v0.18.0 broker-authority work.
Ownership violation
An agent’s modified_files (from its agent.status) touches a file that
another agent has claimed in its active agent.intent. Ownership is
intent-based: a file is owned by whichever agent’s non-expired intent
declares it.
Trigger. Any path in the agent’s modified_files that lies outside its
own active intent (or the agent has no active intent) and inside another
agent’s active intent.
Action. The violator receives agent.feedback naming the touched file
and the agent whose intent claims it; each violator/file pair is warned
once. When
[supervisor.conflict] escalate_on_violation = true (default), the
supervisor inbox also receives a follow-up agent.question so a human
can decide whether to override the boundary or block the work.
Exemption. The shared spec-task-tracking artifact — the tasks file
git-paw directs every agent to update with its own checkbox (a Spec Kit
specs/<feature>/tasks.md or the OpenSpec tasks.md) — is exempt from
ownership-violation detection. git-paw’s own writeback protocol has every
agent tick its own line in that single shared file, so a writeback there
is expected coordination, not a collision. The exemption only covers that
designated task-tracking artifact; ownership violations on source files
are still reported.
The [conflict-detector] Tag
Every auto-emitted agent.feedback from the detector carries a single
errors entry that starts with a fixed tag:
{
"type": "agent.feedback",
"agent_id": "feat-auth",
"payload": {
"from": "supervisor",
"errors": [
"[conflict-detector] forward conflict: agent feat-api also intends to modify: src/auth/middleware.rs"
]
}
}
The [conflict-detector] prefix distinguishes detector output from
human-typed supervisor feedback. Agents (and dashboards) that filter or
classify feedback can match on the tag without parsing payload semantics.
One further tag travels the other way. An agent.feedback from
supervisor whose errors entry reads
[conflict-ack] <agent-a> <agent-b> <file> is read by the detector as an
acknowledgement of that conflict (see
In-flight conflict); sweep.sh conflict-ack shapes
it for you. The file path runs to the end of its errors entry, so the tag
line carries nothing after the path — add any prose as a separate entry.
The detector never emits [conflict-ack] itself.
The detector publishes from the "supervisor" agent ID (the from field
in the payload) — same source as human-authored supervisor feedback —
because routing and display logic already specialise on the supervisor
identity. The tag, not the source, is the discriminator.
Supervisor Inbox Routing
When an in-flight conflict has not resolved within window_seconds and
is classified a true collision (see In-flight conflict),
the detector escalates to the supervisor by publishing an agent.question
addressed to the supervisor (not to either of the conflicting agents).
Additive overlaps are downgraded to an agent.feedback and never land in
this inbox. The orchestrator (your own agent session outside the tmux
session) sees the question in its broker inbox and can:
- Type a reply, which the supervisor skill forwards to both involved
agents via
tmux send-keys(the same dual-write pattern documented in Agent Coordination § Supervisor Acknowledgement). - Resolve the conflict directly by editing one agent’s intent or pausing the offending agent until the other side commits.
Ownership-violation escalations follow the same routing. Forward conflicts never escalate — they are advisory.
Interaction with the Filesystem Watcher
The broker’s filesystem watcher polls git status --porcelain in each
worktree every ~2 seconds and publishes agent.status (with a fresh
modified_files array) whenever the set of reported paths — modified,
staged, or untracked — changes. The conflict detector consumes these
auto-published status messages, so in-flight conflicts surface as soon as
edits land on disk — no manual agent.status curl required from the agent.
The watcher only reads git status — it never stages or commits — so the
detector sees overlaps within one poll of a file being modified, even
before git add or git commit. This is the mechanism that makes
“in-flight” meaningfully earlier than the post-commit hook’s
agent.artifact { status: "committed" }.
Configuration Knobs
All knobs live under [supervisor.conflict] in .git-paw/config.toml:
[supervisor.conflict]
window_seconds = 120
warn_on_intent_overlap = true
escalate_on_violation = true
| Field | Default | Description |
|---|---|---|
window_seconds | 120 | Seconds to wait before escalating an unresolved in-flight conflict to the supervisor inbox via agent.question. |
warn_on_intent_overlap | true | Forward-conflict feedback toggle. When false, intents are still tracked but no agent.feedback is emitted on intent overlap. |
escalate_on_violation | true | Ownership-violation escalation toggle. When false, the violator still receives agent.feedback, but no follow-up agent.question lands in the supervisor inbox. |
The [supervisor.conflict] table is fully optional. Setting
[supervisor] enabled = false (or omitting the section) disables the
detector subsystem entirely — no auto-emitted feedback fires regardless
of the values above. See Configuration → Conflict detector tuning
for the canonical field reference.
MCP Server
git paw mcp runs a Model Context Protocol
(MCP) server over stdio, so any MCP-aware client — Claude Desktop, Cursor,
ChatGPT Desktop, Windsurf, VS Code MCP extensions — can query a repository’s
state without an active git-paw session.
It exposes the same information git-paw already tracks as deterministic, read-only tools, beside a bounded write surface (publish tools, and session lifecycle and verdict tools for a root session only — see below). The read-only tools cover:
- Coordination — active agent intents and detected conflicts
- Governance — your configured ADRs, test strategy, security checklist, Definition of Done, and constitution
- Project knowledge — discovered specs and tasks (OpenSpec, Markdown, Spec Kit, Superpowers), the spec dependency graph, and rendered agent skills
- Session state — the active session’s status/summary and the session-learnings file
- Git context — branches, recent commits, and branch diffs
- Source/Files — browse the local working tree, read a file, and search code contents (gitignored paths excluded; reads confined to the repo root)
Alongside those, the server exposes a bounded write surface: the
Publish tools for every session, and — for a root session
only (a server started in the repository’s main working tree) — the
Session lifecycle tools and the two
verdict publish tools, plus — only once the repository opts in with
[mcp] allow_repo_writes — the
Spec authoring write
tools. validate_spec is read-only and every session gets it.
The server is standalone: it does not need a tmux session, broker, or supervisor for the read-only categories (the publish tools do need a reachable broker — see below). When a data source is unavailable (no broker, no session, no governance config) read tools return well-formed empty/null results rather than errors — so the client always gets an unambiguous answer.
Read/write boundary. Every tool category except Publish tools, Session lifecycle and the two Spec authoring write tools is read-only, deterministically sourced from files, git, and broker state. The write surface is a fixed set of tools: the four worker publish tools (the same four events the bundled
broker.shhelper covers), each publishing only as the calling session; and, for a root session only, the session lifecycle tools (exactly whatgit paw start/pause/stop/purgedo) and the two verdict tools (publish_verified,publish_feedback, exactly whatsweep.sh verified/feedback-gatedo). The spec-authoring write tools, for a consenting repository’s root session, write only inside one spec change directory. There is no tool that writes a file outside a spec change directory, runs a caller-supplied git or shell command, or writes configuration, and no generic “publish anything” tool. A server in an agent’s worktree never lists or runs the root-only tools, so a coding agent cannot verify its own work through MCP — see Publish tools. The server also never invokes an agent CLI (claude,codex, …) as an inference backend; every result comes from files, git, and broker state.
How it works
The MCP client spawns git paw mcp as a child process and talks to it over
the process’s stdin/stdout using newline-delimited JSON-RPC 2.0. The client
owns the lifecycle: the server starts when the client launches it and exits
cleanly when the client closes its stdin.
git paw mcp [--repo <PATH>] [--log-file <PATH>]
| Flag | Purpose |
|---|---|
--repo <PATH> | Operate against a specific repository instead of the current directory. Required for Claude Desktop (see below). |
--log-file <PATH> | Also write diagnostics to a file. stderr is always used; stdout is reserved for the JSON-RPC stream. |
Verbosity follows the standard RUST_LOG convention (default warn):
RUST_LOG=debug git paw mcp --repo /path/to/repo
Repository resolution
- If
--repo <PATH>is given, the server uses that path (it must be inside a git repository). - Otherwise the server walks up from the current directory to the nearest
enclosing git repository. Inside a
git worktree, it resolves to the worktree’s own root.
If no repository can be found, the server prints a clear error to stderr and exits non-zero — it never silently serves nothing.
Finding the session and its broker from inside a worktree. File-serving
tools keep the resolution above: inside a worktree, the repository IS that
worktree. The session lookup is deliberately wider — a worktree root never
equals the repository root, so matching it directly would leave a server
started by an agent’s CLI (its working directory is the agent’s worktree, and
it is given no --repo) with no session and no broker URL. The server
therefore resolves a worktree to the repository that owns it before looking
the session up, trying the exact path first and falling back to the owner only
on a miss. get_session_status and the broker URL in the server’s context
come from that lookup.
This widening is confined to the MCP server’s read-only view. Every git paw
command that mutates session state — stop, purge, add, remove,
pause, and start’s recover/resume dispatch — keeps the exact match, so
running one inside an agent worktree never acts on the parent session.
Server identity
In the MCP initialize handshake the server advertises serverInfo.name = "git-paw" and its real crate version. To distinguish several repositories that
each run git paw mcp, set a custom name in the repo’s .git-paw/config.toml:
[mcp]
name = "my-project" # advertised as serverInfo.name; defaults to "git-paw"
This is the server’s own advertised identity. It is distinct from the
display label some clients show, which comes from the mcpServers key in
the client config (e.g. "git-paw" in the snippet below) — that key is yours
to rename per client entry regardless of [mcp].name.
Per-client setup
Each client needs its own entry pointing at this server. The command is git
with args paw mcp … (git-paw installs as a git subcommand). If you prefer,
git-paw mcp … works too.
Claude Desktop
Claude Desktop spawns MCP servers from its own application-support directory,
not your project — so current-directory discovery cannot work. You must
pass --repo with an absolute path.
-
Open the config file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
- macOS:
-
Add a server entry:
{ "mcpServers": { "git-paw": { "command": "git", "args": ["paw", "mcp", "--repo", "/absolute/path/to/your/repo"] } } } -
Restart Claude Desktop completely (quit and reopen).
-
Verify: open a new chat, click the tools/🔌 icon, and confirm
git-pawis listed with its tools. Ask “what specs are pending in this repo?” and confirm a tool call runs.
Cursor
Cursor is workspace-aware and spawns from the workspace root, so bare
git paw mcp works (or pass --repo to be explicit).
-
Create
.cursor/mcp.jsonin your project (or edit the global~/.cursor/mcp.json):{ "mcpServers": { "git-paw": { "command": "git", "args": ["paw", "mcp"] } } } -
Reload Cursor (or toggle the server in Settings → MCP).
-
Verify: open Settings → MCP and confirm
git-pawshows a green status with its tools listed.
ChatGPT Desktop (macOS)
ChatGPT Desktop supports local MCP servers in Developer Mode. (ChatGPT Web in a browser is not supported — see Known limitations.)
- Enable Settings → Connectors → Advanced → Developer mode.
- Add a connector with command
gitand argspaw mcp --repo /absolute/path/to/your/repo(ChatGPT Desktop, like Claude Desktop, does not run from your project directory, so pass--repo). - Restart ChatGPT Desktop.
- Verify: start a new chat, open the connector/tools menu, and confirm
git-pawand its tools are available.
Windsurf
-
Open Settings → Cascade → MCP Servers → Add Server (or edit
~/.codeium/windsurf/mcp_config.json):{ "mcpServers": { "git-paw": { "command": "git", "args": ["paw", "mcp", "--repo", "/absolute/path/to/your/repo"] } } } -
Refresh the MCP server list (or restart Windsurf).
-
Verify: the MCP panel shows
git-pawconnected with its tools.
VS Code (MCP)
VS Code (1.102+) supports MCP servers. It is workspace-aware, so bare
git paw mcp works from a workspace folder.
-
Create
.vscode/mcp.jsonin your workspace:{ "servers": { "git-paw": { "type": "stdio", "command": "git", "args": ["paw", "mcp"] } } } -
Run MCP: List Servers from the Command Palette and start
git-paw(or reload the window). -
Verify: in Agent mode, open the tools picker and confirm the
git-pawtools appear.
Known limitations
- ChatGPT Web is not supported. The browser version of ChatGPT cannot spawn local processes, which stdio MCP requires. ChatGPT Desktop on macOS works (with Developer Mode). The server speaks stdio only — there is no HTTP transport.
- Per-repo configuration is required. Each repository needs its own entry
in the client config (with its own
--repopath where the client spawns from a fixed directory). For many repos this is repetitive; one server instance serves exactly one repository. - Claude Desktop (and ChatGPT Desktop) need
--repo. They spawn MCP servers from their own app-support directory, not your project, so current-directory discovery cannot find your repo. Workspace-aware clients (Cursor, VS Code MCP, Windsurf) work with a baregit paw mcp.
Tool reference
Every category below is read-only except Publish tools, Session
lifecycle and the two write tools of Spec authoring. For the
read-only categories, collection results are empty ([]) and single-record
results are null when the underlying data is unavailable. Governance tools
return a protocol error only when a configured document path exists but
cannot be read.
Coordination
| Tool | Input | Result |
|---|---|---|
get_intents | — | { intents: [{ branch_id, files, regions, summary, published_at, valid_for_seconds }] } |
get_intent | { branch_id } | { intent: <intent> | null } |
get_conflicts | — | { conflicts: [{ shape, branches, files, detected_at }] } |
wait_for_events | { since?, timeout_seconds? } | { entries: [<broker message>], last_seq, epoch, wave } |
wait_for_events is the MCP form of the broker’s
long-pollable event log. since is a
broker log sequence number (default 0) and timeout_seconds defaults to
30, capped at 60. The tool long-polls GET /log, so it returns as soon as
an entry newer than since is appended, or when the timeout elapses —
whichever comes first. The wave object reports
wave staleness, so a call that times out
with no entries still tells the caller whether the wave has stalled.
epoch identifies the broker instance that answered. Sequence numbers
restart at zero whenever a broker does — a pause + start cycle, for
instance — so a cursor is only meaningful against the epoch it came from.
Record the epoch with your cursor and pass since: 0 on the next call
whenever it changes, or you will hold a cursor above everything the new
broker appends and time out on every call. See
The broker epoch.
With no reachable session broker it returns at once with empty entries,
last_seq equal to the since it was given, and no_session: true. It
mutates nothing: like every tool outside Publish tools, it
only reads broker state.
Governance
| Tool | Input | Result |
|---|---|---|
get_adrs | — | { adrs: [{ id, title, path, status }] } |
get_adr | { query } | { adr: { id, path, content } | null } |
get_test_strategy | — | { content: <string> | null } |
get_security_checklist | — | { content: <string> | null } |
get_dod | — | { content: <string> | null } |
check_dod | { branch } | { branch, items: [{ text, complete }] | null } |
get_constitution | — | { content: <string> | null } |
Project knowledge
| Tool | Input | Result |
|---|---|---|
get_specs | — | { specs: [{ id, backend, title, status, path }] } |
get_spec | { id } | { spec: { id, backend, path, artifacts: [{ name, content }] } | null } |
get_tasks | { spec } | { tasks: [{ id, phase, parallel, description, complete }] } |
get_task | { spec, id } | { task: <task> | null } |
get_dependency_graph | — | { nodes: [{ id, backend }], edges: [{ from, to }] } |
get_skill | { name } | { skill: { name, content, source } | null, message? } |
source is one of standard (.agents/skills/), user_override, or
embedded. An unknown skill returns skill: null with a message — not an
error. The orchestrator skill is served under the name orchestrator
(supervisor is an alias returning the same content) — the same procedure
git paw init exports to .git-paw/skills/orchestrator/SKILL.md.
Session state
| Tool | Input | Result |
|---|---|---|
get_session_status | — | { session: { name, mode, status, paused, agent_count, broker_url, agents:[…], wave? } | null } |
get_session_summary | — | { summary: { name, status, agent_count, agents_by_status } | null } |
get_learnings | — | { sections: [{ category, entries:[…] }] } |
get_session_status includes the broker’s wave object — the session’s
wave-staleness reading
(last_progress_unix_secs, idle_seconds, stall_threshold_seconds,
stalled) — whenever a broker is reachable, and omits the field entirely
otherwise.
Git context
| Tool | Input | Result |
|---|---|---|
get_branches | — | { branches: [{ name, head, current, worktree }] } |
get_recent_commits | { branch, limit? } | { commits: [{ sha, author, timestamp, subject }] } (limit defaults to 20) |
get_diff | { branch, base? } | { base, branch, diff, files_changed, insertions, deletions } (base defaults to the repo’s default branch) |
Source/Files
Browse and read the repository’s local working tree — tracked files plus
untracked-but-not-ignored files. Gitignored paths (build artifacts, secrets)
are excluded throughout, and read_file is confined to the repository root
(canonicalise + starts_with guard, the same as get_doc): a path escaping
the root (../, an absolute path, a symlink target outside the root) is
refused, and a gitignored path is refused, both with null content and a
message rather than a read. Reads return the on-disk working-tree content, so
uncommitted/branch state shows. Implemented over git ls-files / git grep,
so a non-git directory or a no-match search degrades to an empty result.
| Tool | Input | Result |
|---|---|---|
list_files | { subpath? } | { files: [<path>] } (tracked + untracked-not-ignored, relative to the repo root, optionally scoped to subpath; empty when not a git repo) |
read_file | { path } | { content: <string> | null, message? } (path relative to the repo root; confined to the root and gitignore-respecting — a path escaping the root or naming a gitignored file is refused with null content and a message; null with a message when absent) |
search_code | { query, subpath? } | { matches: [{ path, line_number, line }], truncated } (matches across the working tree, binaries skipped, optionally scoped to subpath; capped with a truncated flag; empty when no matches or not a git repo) |
Documentation
Read-only access to the repository’s own documentation, driven by the
bring-your-own [governance].readme and [governance].docs config paths
(see below). Locations are configured, never
hardcoded — unset paths degrade to null/empty results.
| Tool | Input | Result |
|---|---|---|
get_readme | — | { content: <string> | null } (null when [governance].readme is unset or the file is absent) |
list_docs | — | { docs: [{ path }] } (Markdown files under [governance].docs, paths relative to that dir; empty when unset) |
get_doc | { path } | { content: <string> | null, message? } (path relative to [governance].docs; confined to that dir — a path escaping it, e.g. ../, is refused with null content and a message, not a file read outside the directory) |
The documentation tools read two optional [governance] paths in
.git-paw/config.toml:
[governance]
readme = "README.md" # path to the repository README
docs = "docs/src" # path to the documentation root directory
Both default to unset, in which case get_readme returns null, list_docs
returns an empty list, and get_doc returns null — identical to the
pre-v0.7.0 surface. list_docs walks the docs directory recursively for
*.md files; the relative paths it returns feed directly back into get_doc.
Publish tools
Four worker tools, one per agent boot event, each publishing the same message
the bundled broker.sh helper does — this is
the MCP tool form of the boot block for a CLI
that speaks MCP — plus two verdict tools for a root session only, each
publishing the same message sweep.sh verified / feedback-gate does.
| Tool | Input | Result |
|---|---|---|
publish_status | { message } | { published: true } — agent.status (status:"working", the message, modified_files:[]) |
publish_artifact | { exports?, modified_files? } | { published: true } — agent.artifact (status:"done") — the code-less DONE fallback; a task that produces a commit reports via the post-commit hook instead |
publish_blocked | { needs, from } | { published: true } — agent.blocked with dependency info |
publish_question | { question } | { published: true } — agent.question, routed to the supervisor inbox |
publish_verified (root session only) | { agent, message } | { published: true } — agent.verified for agent, verified_by:"supervisor" |
publish_feedback (root session only) | { agent, gate?, errors } | { published: true } — agent.feedback to agent from supervisor; with gate, each error is tagged [<gate>] |
There is no inbox or poll tool: an agent reads its broker inbox with
.git-paw/scripts/broker.sh poll, whichever form its boot block uses.
Three properties are structural, not incidental:
- Bounded, not generic. These are distinct tool definitions, not one
publish(type, payload)tool. A message type is never an accepted parameter, so the set of things a client can publish cannot grow by passing a new string — it can only grow by this server shipping a new tool. - Session-scoped identity. No publish tool accepts an
agent_idor publisher parameter. In an agent’s worktree, each derives the publishing id from the worktree’s branch (the same conversion thebroker.sh poll/statuspath uses), so a session can only ever publish as itself. In the repository’s main working tree — the orchestrator’s — every publish tool publishes as the root identitysupervisor, never as an agent named after the default branch. The verdict tools take the target agent asagent, as the helpers do, and set the verifier / sender tosupervisorthemselves. - Verdicts belong to the root session only.
agent.verifiedis what authorises a branch to be merged;agent.feedbackfrom the supervisor is its corrective channel. The broker accepts either only with the session’s authority credential, which lives outside the repository.publish_verifiedandpublish_feedbackare registered only for a server started in the main working tree, which attaches the credential itself (it is never a tool parameter and never appears in a result or an error). A server in an agent’s worktree does not list them, and a by-name call returns an MCP-level error naming the restriction, so a coding agent cannot self-verify through MCP.
Publish tools need a reachable broker — unlike the read-only categories,
there is no meaningful “empty” result for a publish that did not happen. When
no session is active, or the broker is unreachable, or it rejects the
message, the tool returns an MCP-level error and the server keeps running;
nothing is ever written to stdout outside the JSON-RPC protocol stream. The
verdict tools read the session’s broker and credential when they run, so a root
server that started the session itself (start_session) can publish verdicts
without restarting.
Session lifecycle (root session only)
Offered only to a server started in the repository’s main working tree (as git
reports it, so a --separate-git-dir checkout counts); a server in an agent’s
worktree does not list them, and a by-name call returns an MCP-level error.
Each does exactly what the equivalent git paw command does, in-process, and
returns structured JSON with the command’s output lines and warnings.
| Tool | Input | Result |
|---|---|---|
start_session | { cli?, branches?, specs?, from_all_specs?, supervisor?, dry_run? } | { outcome, session_name, attach_command, orchestrator_context, output, warnings } |
pause_session | {} | { no_session, session_name, outcome, output, warnings } |
stop_session | {} | { no_session, session_name, outcome, output, warnings } |
purge_session | { confirm } | { no_session, session_name, deleted, retained, output, warnings } |
start_sessionis detached and never prompts. It launches (or revives — restarts from pause, recovers a stopped session) the repository’s session without attaching a terminal, and returns once the launch’s readiness gates have run, with the session name, the command a person runs to attach, and the orchestrator-context file (.git-paw/AGENTS.local.md). It has nounattendedparameter: rungit paw start --unattendedyourself for an unattended wave. Nothing is asked interactively — passbranchesor a spec selection, andcliunless the repository setsdefault_cli; a choice the command would otherwise put to a picker is refused with the parameter to pass.supervisorabsent means the repository’s[supervisor]config decides, as a non-interactivegit paw startdoes.dry_runreports the plan and creates nothing.stop_sessiontakes no confirmation. Stopping is recoverable (the worktrees, branches and state stay;start_sessionrecovers it), as withgit paw stop.purge_sessionrequiresconfirm: trueand is annotated destructive (destructiveHint). It removes the tmux session, every worktree (including uncommitted changes in them) and the session state, as a confirmedgit paw purgedoes. Branches git-paw created are deleted only when merged; a branch with unmerged commits is kept and listed inretainedwith its tip.- With no session,
pause_session,stop_sessionandpurge_sessionreturn{ "no_session": true }rather than an error.
Authoring specs over MCP is a separate surface, gated by the repository’s own consent; see Spec authoring.
Spec authoring (repository writes, root session only)
Three tools let an orchestrator author and check an OpenSpec change without its
own file tools. create_spec and write_spec_artifact write files into the
repository working tree, so they are off by default. validate_spec only
reads, and every session gets it.
| Tool | Input | Result |
|---|---|---|
create_spec | { name } | { written } |
write_spec_artifact | { change, path, content } | { written, replaced } |
validate_spec | { change } | { valid, findings: [{ file, message }] } |
Consent. The two write tools are listed and run only when both of these hold:
- The repository’s own
.git-paw/config.tomlsets[mcp] allow_repo_writes = true. A value in the user-level config has no effect, and no environment variable or tool argument turns it on. - The server runs in the repository’s main working tree (a root session). A server in an agent’s worktree never gets them, even with the switch on.
The server reads the switch once, at startup, and logs whether repository
writes are enabled. Changing it in either direction takes effect only after
you restart the MCP server. When a condition fails, the tools are absent from
tools/list. A by-name call returns an MCP error and writes nothing. The
error names allow_repo_writes and .git-paw/config.toml when the switch is
off, or the worktree restriction in a worktree session.
The tools:
create_speccreates<[specs] dir>/<name>/.openspec.yamlwithschema: spec-drivenand today’s date ascreated: YYYY-MM-DD. That is the scaffoldopenspec new change <name>creates, so the consumer needs no OpenSpec CLI installed.namemust be lowercase letters, digits and hyphens, and an existing change is refused.write_spec_artifactwritescontent(at most 1 MiB) verbatim topathinside the existing change directory<[specs] dir>/<change>/. It refuses.openspec.yaml, whichcreate_specowns. It creates missing parent directories inside the change and replaces an existing regular file.replacedsays whether it did. A change that does not exist is an error.validate_specruns an in-process structural check on a change. It reports a missingproposal.md, nospecs/<capability>/spec.mdwith an## ADDED,## MODIFIED,## REMOVEDor## RENAMED Requirementsheader, an added or modified requirement whose first body line has neither SHALL nor MUST, and an added or modified requirement without a#### Scenario:.validistrueexactly whenfindingsis empty. It spawns no process and checks only this subset, so your project’s own spec validator stays authoritative.
{ "name": "write_spec_artifact",
"arguments": { "change": "add-export",
"path": "specs/data-export/spec.md",
"content": "## ADDED Requirements\n..." } }
{ "written": ["openspec/changes/add-export/specs/data-export/spec.md"], "replaced": false }
Returned paths are repository-relative. Each write logs one line on the server
log with the path, byte count and whether it replaced a file. The tools never
stage or commit, so git status and git diff show exactly what was authored.
Confinement. Before writing, the server refuses the call with an MCP error naming the offending input, and writes nothing, when:
pathis absolute or has a..or.gitcomponent;changeis not a single kebab-case change name (lowercase letters, digits and hyphens);- the spec directory resolves outside the repository, inside its
.git, to the repository root itself, or inside.git-paw; - a directory between the change directory and the target is a symbolic link resolving outside the change directory;
- the target itself is a symbolic link.
Content goes to a temporary file in the target directory and is renamed into place.
OpenSpec only. The spec directory and format come from the [specs]
section of the repository’s own .git-paw/config.toml; a user-level [specs]
section is ignored here, as it is for consent. Under any other [specs] type
the three tools return “not supported for format <type>”. With no [specs]
section in the repository config they return an error naming [specs].
The switch does not govern the lifecycle and verdict tools above. Those follow the root-session rule alone.
Doctor
git paw doctor answers “why won’t it launch?” in one command. It runs
read-only preflight checks over your environment, configuration, and repository
state and prints a grouped report — every check carries a ✓, ⚠, or ✗,
and every non-✓ check prints an actionable remedy.
git paw doctor
Environment
✓ git git version 2.39.5 (/usr/bin/git)
✓ tmux tmux 3.6a (/opt/homebrew/bin/tmux)
✓ git repository the working directory is inside a git repository
✓ sandbox sandbox-exec available for optional worker confinement
CLIs
✓ detected CLIs 2 available: claude, codex
Config
✓ config.toml .git-paw/config.toml parses
✓ worktree placement child — worktrees live in <repo>/.git-paw/worktrees/
Spec system
⚠ spec system no spec system configured — spec-driven launch is unavailable
↳ add a [specs] section to .git-paw/config.toml (type = "openspec" | …) or pass --specs-format
✓ spec-worker CLI no default_spec_cli configured — spec workers fall back to default_cli or the interactive picker
✓ spec workflow no spec system configured — the format declares no workflow entry point
Bundled scripts
✓ _paw_common.sh installed, executable, matches this binary
✗ sweep.sh .git-paw/scripts/sweep.sh is missing
↳ run `git paw init` to (re)install the bundled helper scripts
✓ broker.sh installed, executable, matches this binary
✓ docs-fetch.sh installed, executable, matches this binary
✓ python3 Python 3.12.4 (python3)
Broker
✓ broker disabled — the pure-manual baseline (no agent coordination, no dashboard feed)
Supervisor
✓ supervisor disabled — agents run without a supervising pane
Hygiene
✓ .gitignore every git-paw entry is ignored
✓ session state no stale session receipt
✓ worktree registrations every registered worktree exists on disk
18 ✓ · 1 ⚠ · 1 ✗
Diagnose, don’t repair
Doctor never writes. No file, config, session, or other persistent state is
created, modified, or deleted — it only reads. Apply the remedies yourself;
most of them are git paw init.
There is deliberately no --fix flag. A repair mode was considered for v0.13.0
and cut to keep the pre-v1.0.0 surface minimal; if it ever lands it will
re-run only safe, idempotent init steps.
Exit codes
The exit code is the worst check, so doctor works as a pre-launch or CI gate that blocks only on true blockers:
| Worst status | Meaning | Exit code |
|---|---|---|
| ✓ | everything checked out | 0 |
| ⚠ | something worth fixing that will not stop a launch | 0 |
| ✗ | a hard blocker | non-zero |
A ⚠ alone never fails the process. Ambiguous states (a version banner that cannot be parsed, a port probe that timed out) resolve to ⚠ rather than ✗, so doctor does not block you spuriously.
git paw doctor || echo "fix the ✗ findings before launching"
What it checks
| Group | Checks |
|---|---|
| Environment | git and tmux on PATH and at or above their minimum versions (git 2.5 for git worktree, tmux 1.8); the working directory is inside a git repository. Missing or too old is ✗. Also whether this OS’s FS-scoped sandbox backend (sandbox-exec on macOS, bwrap on Linux/WSL) is on PATH — absent is ⚠ with a pointer to the sandbox guide, never ✗, because the sandbox is optional. |
| CLIs | The AI CLIs that resolve on PATH — the known roster plus your [clis.*] entries. None resolving is ⚠, surfacing the No AI CLIs found launch failure before you hit it. Plus three read-only reports about per-CLI setup: setup state, broad grants and grant residue (below). |
| Config | .git-paw/config.toml exists and parses (unparseable is ✗, absent is ⚠); the resolved worktree_placement; any key this version does not recognise (⚠, naming the key). |
| Spec system | The explicitly configured spec format and how many specs it discovered. Unconfigured is ⚠ with the “add [specs] or pass --specs-format” guidance — there is no filesystem auto-detection. Also reports the effective spec-worker CLI: the CLI each discovered spec would actually launch with (its own paw_cli when set, else default_spec_cli). Warns (⚠) when a configured default_spec_cli is never actually reachable by any discovered spec — every one of them pins its own paw_cli instead — surfacing a silent mis-resolution (such as a spec-driven --supervisor launch quietly using an unexpected, unsandboxed CLI) rather than passing green. Informational (✓) when no specs or no default_spec_cli are configured — there’s nothing to verify against yet. Finally, whether the workflow entry point the launch submits to spec-driven agents is reachable from your worktrees (below). |
| Bundled scripts | sweep.sh, broker.sh, docs-fetch.sh, and the shared _paw_common.sh preamble they source exist under .git-paw/scripts/, are executable, and match this binary’s embedded copies. Missing or non-executable is ✗; content drift is ⚠ (“stale”). Also checks for a Python 3 interpreter, which every bundled script needs — absent is ⚠, not ✗, because core start/add/remove needs no Python. |
| Broker | When [broker] enabled = true, that the configured bind/port is free or already serving a git-paw broker for this repository. A git-paw broker serving another repository is ⚠ with the [broker] port remedy — it is the clash that would refuse your next git paw start. A non-git-paw service on the port is a separate ⚠ with its own remedy. When disabled, an informational ✓ noting the pure-manual baseline. |
| Supervisor | When [supervisor] enabled = true, that each configured gate command’s binary resolves on PATH (✗ per missing binary) and that sweep.sh is installed. When disabled, an informational ✓. |
| Hygiene | The required .gitignore entries (including .git-paw/worktrees/); session state; registered worktrees whose directory no longer exists. Each is ⚠. A receipt that claims active while its tmux session is gone gets the git paw purge --stale remedy, as does an orphaned worktree registration. A session whose tmux session is still up but whose agent panes have all dropped back to a bare shell (past a short launch grace period) is a separate finding: it names the session, says its agent panes exited, and its remedy is git paw start, which recovers it — purge --stale never purges such a session. |
The per-CLI setup checks
Three checks report on git paw setup-cli. All are strictly
read-only — they create, modify and delete nothing — and none can ever be
the reason doctor fails: they never report ✗.
Setup state, one per CLI identity that has setup facts or record entries:
- ✓ when nothing is applied, listing the available items and whether each is verified or plan-only;
- ✓ when every recorded entry is present and unchanged, and the target settings file holds every entry this binary would plan;
- ⚠ when a recorded entry is missing or has been edited, with a remedy naming
--undo; - ⚠ when the applied entries are out of date against this version, with a
remedy naming
--undofollowed by re-applying; - ⚠ when a granted helper script differs from the copy this binary bundles, naming its path. This one matters most: the allowlist lets that script run without a prompt, so drift in it is drift in what runs unprompted. Helpers are checked in the main checkout and in every session worktree, because a granted path is relative and resolves against whichever directory the session runs in.
What counts as applied is judged against the CLI’s target settings file,
not against git-paw’s record: a planned entry is applied when that file holds
it at its key path, whoever wrote it. That includes an entry you already had
before you ran git paw setup-cli. Setup skips such an entry — there is
nothing to write — and deliberately never records it, because the record
lists only what git-paw itself wrote, so --undo can take back exactly that
and nothing else.
The check reads the comparison in both directions. It also warns when the
record holds a permission rule this version of git-paw no longer plans — a
grant a previous release wrote and this one has withdrawn. The remedy is the
same either way: git paw setup-cli <CLI> --undo, then re-apply the item.
Broad grants, an advisory over the allowlist item’s target file when it
exists. It lists every permissions.allow entry whose shape grants more than
one specific command — a bare tool rule, a single-word prefix grant, a shell
or interpreter wrapper, a state-changing git or git-paw subcommand, a
far-reaching command like rm or curl (a loopback curl is exempt), a
file-tool rule anchored at the filesystem root or your home directory, or a
helper grant broader than git-paw’s own entry for that helper.
Entries are abbreviated to the tool name and the first two words of the specifier, each capped in length. That bounds the common way a secret ends up in a rule — a long URL carrying a token — but it is a mitigation, not a guarantee: a short rule that is itself a secret cannot be told apart from a short rule that is not, so treat doctor output as you would the settings file it reads. An unparseable file is reported as “not inspected”, without its content. git-paw’s own planned entries are never listed, whoever added them.
This advisory is a report over stored rules, not a decision about any prompt. git-paw does not edit these entries and never acts on them — reviewing and removing the ones you did not intend is yours to do.
Grant residue (project CLI grant residue) sits beside the advisory and
answers a narrower question: not “is this rule broad?” but “is this a
known-dangerous leftover?”. It reads the main checkout’s
.claude/settings.json and .claude/settings.local.json, and reports ⚠ when
either holds one of exactly three things:
| Residue | Example | Why it matters |
|---|---|---|
A permissions.allow entry granting curl with any arguments | Bash(curl *), Bash(curl:*), Bash(curl*) | an unrestricted network client |
A permissions.allow entry naming a helper script with a keystroke or verdict subcommand | Bash(.git-paw/scripts/sweep.sh approve *), Bash(.git-paw/scripts/sweep.sh verified *), Bash(.git-paw/scripts/sweep.sh *) | it lets an agent session approve its own permission prompts, or publish a supervisor verdict, without a prompt |
A permissions.allow entry naming the pass-through fetch subcommand | Bash(.git-paw/scripts/docs-fetch.sh get *) | it passes an arbitrary URL through, so a session can read any file it names or send repository content to any host |
A permissions.allow entry naming any other subcommand the grant table withholds | Bash(.git-paw/scripts/sweep.sh classify *) | a helper subcommand the grant table deliberately withholds from the allowlist item |
A top-level allowed_bash_prefixes key | — | the inert seed git-paw wrote before v0.16.0; it grants nothing in the current CLI and is pure leftover |
Each entry is reported with the reason true of the subcommand it actually reaches, and a rule reaching several — a whole-script wildcard reaches all of them — is reported by the gravest.
The helper rules are matched by the helper’s name wherever it appears in the
specifier, so a relative, absolute or interpreter-prefixed path is caught
alike. A wildcard is matched in or across the subcommand position: both
Bash(.git-paw/scripts/sweep.sh *) and Bash(.git-paw/scripts/sweep.sh ap*)
reach approve. Two things are deliberately not residue: a specific loopback rule
such as Bash(curl -s http://127.0.0.1:9119/status), which names one
endpoint rather than granting the client; and Bash(.git-paw/scripts/broker.sh *),
because the grant table grants that helper whole — it is one of git-paw’s own
entries.
Only Bash rules can be residue — a Read or Edit rule naming a helper
grants reading or writing that file, not running it.
The remedy is by hand. git-paw never rewrites a grant it did not write, so it names each file and each matching entry and leaves the removal to you. The check is ✓ when neither file holds a match, and ⚠ stating the file was not inspected when one of them exists but is not valid JSON. When neither file exists there is nothing to report and the check is not shown at all.
Entries are abbreviated as in the advisory above, with the same caveat — it
bounds what is printed rather than guaranteeing a secret never appears. A
helper grant gets one extra protection: it is rendered from the word naming
the helper, so an environment assignment in front of it, which is exactly
where a token would sit, is replaced by … instead of printed.
Like the other two, it is read-only, never reports ✗, and never influences how a prompt is handled.
A rule can appear in both reports — Bash(curl *) is both broad and residue.
That is intended: one is advice about breadth, the other names a specific
danger.
The spec-workflow reachability check
A spec-driven launch submits your spec tool’s
workflow command — /opsx:apply for OpenSpec — as a
second message once the
agent’s pane goes idle. That command’s definitions are installed by the spec tool into an
agent-CLI directory at the repository root (for claude:
.claude/commands/opsx/apply.md and
.claude/skills/openspec-apply-change/SKILL.md), and that directory is
frequently gitignored. git worktree add populates a new worktree from a
commit, so a sibling worktree never sees an uncommitted definition and the
agent quietly replaces the spec workflow with an ad-hoc one. The spec
workflow check surfaces that before you launch.
Each spec-driven worker CLI the discovered specs resolve to is judged on its own:
- ✓ when a definition is committed at the main checkout’s
HEAD— every new worktree checks it out, whatever the placement. Staging it is not enough: an index entry does not travel into a new worktree; - ✓ when
worktree_placementischildand a definition exists at the repository root — the worktree sits under the root, so the CLI’s ancestor search still finds it; - ⚠ when the CLI’s locations are known and no definition reaches the
worktrees — either placement is
siblingand every definition found at the root is uncommitted, or none is found at all. The finding names the command, the unreachable CLIs (and only those — a reachable CLI alongside them passes silently), and the locations checked.
Under sibling placement the remedy offers both fixes: commit the command
definitions so every new worktree checks them out, or set
worktree_placement = "child" so the worktrees sit under the repository root.
Under child placement the only cause left is that no definition exists at
all, so the remedy points at installing it with your spec tool and committing
it.
Definitions are looked for in the main checkout, so running doctor from inside an agent’s worktree — which never carries them itself — reports the same verdict as running it at the repository root.
Two states are reported rather than warned about:
- ✓ not applicable when the resolved spec format declares no workflow entry point (Markdown, Spec Kit, Superpowers, or no spec system), or when no spec-driven worker CLI resolves — there is nothing to reach;
- ✓ could not be verified for a CLI whose definition locations git-paw does
not know. Where a CLI keeps its command definitions is per-CLI knowledge;
git-paw lists only the locations verified against a real install (today,
claude) rather than guessing a path and warning about a definition that never lived there.
Like the setup checks, this one is read-only and never ✗ — an unreachable workflow degrades a launch, it does not block one.
The supervisor check is project-agnostic
The gate-command verbs doctor probes come from your [supervisor]
configuration — the resolved stack preset — never from a hard-coded toolchain.
A Node project configured with test_command = "npm test" sees npm checked; a
Go project sees go. git-paw never assumes every consumer builds with the
toolchain it happens to use itself.
Machine-readable output
--json emits the same checks as one JSON document and suppresses the human
rendering. The exit-code contract is identical, so scripts and agents can branch
on either.
git paw doctor --json
{
"status": "warn",
"checks": [
{
"group": "Environment",
"name": "git",
"status": "ok",
"detail": "git version 2.39.5 (/usr/bin/git)",
"remedy": null
},
{
"group": "Spec system",
"name": "spec system",
"status": "warn",
"detail": "no spec system configured — spec-driven launch is unavailable",
"remedy": "add a [specs] section to .git-paw/config.toml … or pass --specs-format"
}
]
}
Every entry carries group, name, status ("ok" / "warn" / "fail"),
detail, and remedy (null on a ✓). The top-level status is the worst
check.
Live smoke check
Doctor is static by default: it inspects, and never spawns a tmux session or
an AI CLI. Pass --live to add a Live smoke group that runs the full session
lifecycle (start → add → remove → stop) against an isolated throwaway
repository and a dummy CLI, and folds the verdict in as one more check:
git paw doctor --live
This is the same harness the internal git paw selftest command drives — see
Selftest for the full isolation recipe. It is slower, it needs
tmux, and it is the only arm of doctor that writes anything: strictly inside its
own .git-paw/tmp/ sandbox, which it removes again on both the success and
failure paths. A skip (no tmux) is ⚠, not ✗ — the Environment group already
reports a missing tmux as the hard failure.
Configuration
Doctor introduces no configuration fields. It reads the configuration you
already have ([broker], [supervisor], [specs], [clis.*],
worktree_placement, default_spec_cli / default_cli) and reports what it
resolved.
Selftest
git paw selftest runs an isolated, end-to-end session-lifecycle smoke
check — the shipped form of the dogfood isolation recipe. It exercises the
riskiest orchestration plumbing (start → add → remove → stop) against a
throwaway repository and a dummy CLI, then reports a single pass/fail verdict —
with no real LLM backend and no interactive terminal.
Internal diagnostic (hidden).
selftestis not shown in the main command list — it is the live smoke-test arm ofgit paw doctor, whose static preflight checks it complements. It stays available for dogfooding and CI, andgit paw doctor --liveruns this same harness and folds its verdict into doctor’s grouped report as a Live smoke check.
git paw selftest
Why
git-paw’s session-management surface (start, add, remove, stop, purge)
is its riskiest plumbing because it spans modules — worktrees (git.rs), tmux
panes and sockets (tmux/), the broker HTTP roster (broker/), and session
state (session.rs). Historically the only way to exercise the full path was to
boot a real session with a real AI CLI in an interactive terminal — something CI
cannot do and developers reproduced only through ad-hoc shell incantations.
selftest packages that recipe as a first-class subcommand, so the lifecycle is
verifiable anywhere. It is a runtime smoke check of the orchestration plumbing,
complementary to cargo test — not a replacement for the unit and
integration suite.
What it does
The harness re-invokes the running git-paw binary as child processes — the
same code path you exercise by hand — and drives the lifecycle:
- Allocate an OS-assigned ephemeral broker port.
- Create a throwaway git repository (one base commit) under
.git-paw/tmp/. - Start an isolated supervisor session with a dummy CLI, confirming it landed on the harness’s private tmux socket.
- Observe the roster through the per-repo discovery file — it holds exactly the initial agent.
- Add an agent worktree and observe the roster grow to include it.
- Remove that agent worktree and observe the roster shrink back, with the remaining entries unchanged.
- Stop the session.
Steps 4–6 make the add/remove roster transitions observable, closing the deferred live-verification that previously required a real session.
Isolation recipe
Every external resource is isolated so a run never disturbs your live work or a concurrently running selftest:
- Private tmux socket — a per-run
TMUX_TMPDIR, withTMUXandTMUX_PANEstripped from every child process so the spawned tmux server never attaches to the caller’s session. Your default tmux socket is never touched. - OS-assigned ephemeral broker port — the harness binds
127.0.0.1:0and reads back the kernel-assigned port, so concurrent runs never collide on a fixed or PID-derived port. - Isolated
HOME/XDG — the global session receipt is written under the throwaway tree, so your real sessions directory is never modified. - Throwaway repository under
.git-paw/tmp/— namespaced inside the repo’s own git-paw working tree so artifacts are discoverable and gitignorable. A stale-dir sweep removes any prior aborted run before a fresh one is created. - Dummy CLI (
cat) in place of a real agent CLI — it holds its pane open without producing output or requiring input, so the session boots deterministically in detached mode with no LLM process spawned.
Cleanup runs on both the success and failure paths: the private-socket tmux
server is killed and the .git-paw/tmp/ throwaway tree is removed.
Exit behaviour
| Outcome | stdout / stderr | Exit code |
|---|---|---|
| Lifecycle completed | selftest passed | 0 |
tmux not on PATH | selftest skipped: tmux not available | 0 |
| A lifecycle step failed | selftest failed at step '<step>': … (stderr) | non-zero |
Like the rest of the e2e suite, selftest reports a skip rather than a
failure when tmux is unavailable, and exits non-zero only on an actual lifecycle
failure. CI runners have tmux installed, so a skip never silently passes there.
When a step fails the verdict names the failing step (one of pick-port,
create-repo, start, roster-initial, add, roster-after-add, remove,
roster-after-remove, stop).
Relationship to the broker-port fix
The same OS-assigned ephemeral-port helper the harness uses for the broker port
is now the canonical scheme across the test suite. It replaced a PID-modulo port
selection (24_000 + (process::id() % 200)) that yielded only a small number of
distinct ports and collided under concurrency — the real cause of the
intermittent “address already in use” broker-bind failures. Binding
127.0.0.1:0 is collision-proof at any concurrency because the kernel guarantees
each bind returns a port not currently in use. See the
test-isolation specification for the requirement.
CLI Prompt-Shape Profiles
git-paw’s supervisor and prompt detectors recognise an agent CLI’s terminal surface — its “ready” banner, its permission-prompt wording, its mid-response footer — by matching known markers against captured pane text. A prompt-shape profile is the data that describes what one CLI’s terminal looks like. It never describes what that CLI is allowed to do — see What a profile is not below.
git-paw ships one profile, embedded in the binary: Claude Code. Every detector falls back to it, field by field, so an installation with no profile configuration behaves exactly as it always has.
Why this exists
Before profiles, the markers describing Claude Code’s terminal were
compiled into half a dozen Rust modules plus a bash mirror in the bundled
sweep.sh helper. For a non-Claude CLI this was not cosmetic — an approval
marker that never matched meant command extraction silently failed, and a
three-option prompt with different wording was read as a two-option one. A
prompt-shape profile gives each of those literals one home, so adding a new
CLI’s markers is a data change, not a code change across six files.
Resolution order
For a given CLI name, each field resolves independently:
- The field’s value in that CLI’s
[clis.<name>].prompt_profiletable, if set. - Otherwise, the embedded Claude Code default.
This is per-field, not per-profile: a [clis.<name>].prompt_profile
table that sets only approval_markers still gets the embedded default for
every other field (readiness markers, mode markers, and so on). A partial
profile can never silently disable detection for the fields it omits, and a
CLI with no prompt_profile table at all — including one git-paw has never
heard of — resolves the full embedded default rather than an empty profile.
Fields
| Field | Matches | Used by |
|---|---|---|
readiness_markers | Substrings that positively identify the CLI’s interactive ready state (as opposed to a bare shell prompt), gating boot-block injection. | tmux::readiness::classify_pane_readiness (boot-prompt injection gate) |
approval_markers | Substrings indicating the CLI is waiting for an approval decision. | supervisor::permission_prompt approval-marker scan (reports only that a prompt is present) |
live_prompt_markers | The confirmation-question lead-in ("do you want to") and the cancel footer ("esc to cancel") that anchor a LIVE (not scrolled-away) permission prompt. Detection searches upward from the end of the capture, tolerating up to 13 non-blank trailing lines of non-prompt content (a task list, a status strip) below the modal — but once the anchor has shifted past the capture’s true end, liveness there requires the modal’s actual SHAPE: the footer anchoring the START of a line (not merely appearing anywhere in it) together with a numbered option line nearby. A bare confirmation question, or the footer text only mentioned mid-sentence (narration, grep-style tool output), cannot re-confirm liveness there — an already-answered prompt can leave either behind followed by ordinary output. | supervisor::permission_prompt::is_live_prompt, the sweep.sh approve re-confirm |
mid_response_markers | Substrings identifying a pane actively producing a response (its interrupt footer showing) rather than sitting at its input box. | supervisor::drive::pane_is_mid_response (unattended drive loop) |
prompt_boilerplate_markers | Lower-cased substrings marking a captured line as prompt boilerplate (the question / choices) rather than the command awaiting a decision. | supervisor::manual_approvals pattern extraction |
command_header_markers | Lead-in forms of a command-confirmation header — a whole-line header form (e.g. "Bash command") or an inline-embedding form ending in ( (e.g. "Bash("). | supervisor::permission_prompt::extract_command_slice |
file_prompt_pattern | Regex source matching the lead-in of a filesystem-operation permission prompt, capturing the target path. | supervisor::permission_prompt::extract_path_from_file_prompt |
option_line_pattern | Regex source matching a numbered option line ("1. …", "2) …") once leading decoration is stripped. | supervisor::permission_prompt::is_option_line |
broad_grant_marker | Substring identifying a durable (“don’t ask again”) broad-grant option in a multi-option prompt. | The sweep.sh approve option-1 guard: if the prompt’s first option carries this wording, pressing it would write a permanent permission rule, so the helper sends nothing. |
input_sigils | Prefixes identifying the CLI’s input-box sigil (e.g. ">", "❯"). | supervisor::drive::input_box_text (stranded-input recovery) |
mode_accept_edits_markers | Substrings identifying an accept-edits / bypass-permissions mode footer. | coordination::inventory::detect_mode |
mode_interactive_markers | Substrings identifying a visible interactive prompt. | coordination::inventory::detect_mode |
stream_error_markers | Phrases indicating the CLI’s API call failed mid-turn (transport error, timeout, disconnect). | the bundled sweep.sh helper’s stuck-shape detector |
context_bloat_pattern | Regex source matching a context-bloat clear/compact hint, capturing the token count in thousands. | the bundled sweep.sh helper’s stuck-shape detector |
paste_buffer_pattern | Regex source matching a paste-aware CLI’s “pasted text” acknowledgement. | the bundled sweep.sh helper’s stuck-shape detector |
cli_unavailable_markers | Substrings/phrases indicating the CLI itself has stopped serving — a usage/session limit ("You've hit your session limit"), an organisation spend limit ("org's monthly spend limit"), or an expired/missing login — rather than a worker being merely stuck. | the unattended drive loop (supervisor::drive::cli_unavailable_line), resolved per pane for its own CLI: an orchestrator-pane match is an immediate cli-unavailable exit, a coding-pane match a deduped escalation |
Every marker-list field is matched as a case-insensitive substring, mirroring
the behaviour each site had before profiles existed. Pattern fields are
Rust regex crate syntax.
Authoring a profile for a new CLI
Add a prompt_profile table under that CLI’s [clis.<name>] entry,
setting only the fields that differ from Claude’s terminal surface:
[clis.my-agent]
command = "my-agent"
[clis.my-agent.prompt_profile]
readiness_markers = ["my-agent ready"]
approval_markers = ["proceed? (y/n)"]
broad_grant_marker = "remember this choice"
Every field you omit falls back to the embedded Claude default (see Resolution order) — start with just the fields you know differ, and add more once you observe a detection gap in practice.
What a profile is not
A prompt-shape profile describes what a CLI’s terminal looks like. It
has no field through which it could express what a command is
allowed to do — git-paw decides that for no command anyway. What a
profile also cannot touch is the discipline around sending a keystroke.
The following stay compiled (or mirrored in the bundled sweep.sh), are
never read from a profile, and cannot be widened by one:
- the live-prompt gate’s anchoring windows and trailing-region bound — a profile supplies the marker text, never how far from the end of a capture a prompt may sit and still count as live
- the approval send gate’s fresh-capture re-confirm
- the pane-0 refusal, which holds whatever a capture contains
- the
sweep.sh approveoption-1 rule: it always answers with option1and never another digit. A profile’sbroad_grant_markercan only make the helper refuse to send (by marking option 1 as a durable grant), never make it press something else.
This boundary exists because a profile is configuration a coding agent’s own
worktree can influence (via .git-paw/config.toml); if a profile could
alter where a keystroke lands or when one is allowed, an agent could widen
the very gate meant to constrain it. If you find yourself wanting a profile
field to affect whether or where a keystroke is sent rather than how
the prompt looks, that is a sign that a compiled rule change is needed
instead.
Do not ship a guessed profile
git-paw ships only the Claude Code profile — the one CLI its detection is tested against. A profile for a CLI nobody has verified against live output is worse than no profile at all: it produces confident misdetection instead of an obvious fallback to Claude’s markers. If you author a profile for another CLI, verify it against that CLI’s actual terminal output before relying on it, and expect to iterate.
sweep.sh reads the same profile
The bundled supervisor helper (sweep.sh) resolves its stuck-shape and
live-prompt regexes via the internal git paw __prompt-profile command
rather than carrying its own copies of the markers — the same profile every
Rust detector reads. This means an authored profile applies to both the
in-process supervisor and the shell helper without being declared twice.
Configuration
git-paw uses TOML configuration files at two levels, with repo-level settings overriding global ones.
Config File Locations
| Level | Path | Purpose |
|---|---|---|
| Global | ~/.config/git-paw/config.toml (Linux; $XDG_CONFIG_HOME/git-paw/config.toml when that is set) or ~/Library/Application Support/git-paw/config.toml (macOS) | Default CLI, custom CLIs, global presets |
| Per-repo | .git-paw/config.toml (in repo root) | Repo-specific overrides |
Both files are optional. git-paw works with sensible defaults when no config exists.
Full Config Example
# CLI pre-selected in the interactive picker when --cli is not provided
default_cli = "my-cli"
# Default CLI for spec-mode launches (--from-all-specs, --specs); bypasses picker when set
# default_spec_cli = "my-cli"
# Prefix for spec-derived branch names (default: "spec/")
# branch_prefix = "spec/"
# Documentation site the bundled docs-fetch helper consults on demand
# (default: git-paw's published docs). Set to retarget a fork or mirror.
# docs_base_url = "https://bearicorn.github.io/git-paw"
# Enable mouse mode in tmux sessions (default: true)
mouse = true
# Where agent worktrees are created: "child" (in-repo .git-paw/worktrees/)
# or "sibling" (../<project>-<branch>). Absent = "sibling"; git paw init
# writes "child" for new repos and backfills it into a config that omits it.
worktree_placement = "child"
# Pane affordances: double-line borders, per-pane labels, active-pane highlight
# (default: true; set false to inherit your own tmux styling)
# [layout]
# border_affordances = true
# Per-worktree runtime isolation (all opt-in; absent = no copying, no
# .env.local, no hook). Files are copied verbatim into each worktree; each
# worktree gets its own port block base + slot * stride (keep stride >= vars
# length); hooks run your own commands at worktree create/remove.
# [worktree.env]
# copy = [".env", ".env.development"]
# [worktree.ports]
# base = 3000
# stride = 10
# vars = ["PORT", "VITE_PORT"]
# [worktree.hooks]
# on_create = "scripts/paw-db-branch.sh {worktree_id}"
# on_remove = "scripts/paw-db-drop.sh {worktree_id}"
# Custom CLI definitions
[clis.my-agent]
command = "/usr/local/bin/my-agent"
display_name = "My Agent"
[clis.local-llm]
command = "ollama-code"
display_name = "Local LLM"
# A custom CLI is also the seam for the optional FS-scoped sandbox: point
# `command` at a PATH-resolvable wrapper that execs the real CLI under
# sandbox-exec/bwrap. git-paw launches a CLI by its name on PATH, so the wrapper
# must be on PATH (not just a script path). See the FS-Scoped Sandbox guide.
# Named presets for quick launch
[presets.backend]
branches = ["feature/api", "fix/db"]
cli = "claude"
[presets.frontend]
branches = ["feature/ui", "feature/styles"]
cli = "codex"
# Spec scanning configuration
# [specs]
# dir = "specs"
# type = "openspec" # "openspec", "markdown", "speckit", or "superpowers"
# opsx (OpenSpec) role gating — active only under the OpenSpec engine
# [opsx]
# role_gating = "warn" # "warn" (default), "block", or "off"
# Session logging
# [logging]
# enabled = false
# Agent coordination broker
# [broker]
# enabled = false
# port = 9119
# bind = "127.0.0.1"
# wave_stall_minutes = 30
# Pointers to user-maintained governance docs (all optional)
# [governance]
# adr = "docs/adr"
# test_strategy = "docs/test-strategy.md"
# security = "docs/security-checklist.md"
# dod = "docs/definition-of-done.md"
# constitution = ".specify/memory/constitution.md"
# readme = "README.md"
# docs = "docs/src"
Upgrading an Existing Config
git paw init is the upgrade path after a version bump: re-run it in an
already-initialised repo and it brings .git-paw/config.toml up to the current
defaults. The migration is strictly additive — it appends
- a
[supervisor]section when the file has none (init asks whether to enable supervisor mode in an interactive terminal, and writesenabled = falseotherwise), and - any top-level default key the file is missing, written with its
generated-default value (for example
worktree_placement, added in v0.8.0).
An existing but empty config.toml is instead seeded with the full generated
template. The other sections appear in the template only as commented examples,
so they are not appended to a file you have already written.
It never modifies or removes a section, a key, or a commented example you have
already set, so your customised settings survive verbatim. Once [supervisor]
and every top-level default key are present the config is left untouched, and
— if nothing else needed creating — init reports
Already initialized. Nothing to do. (The bundled .git-paw/scripts/ helpers,
the exported .git-paw/skills/ and the AGENTS.md pointer section are
refreshed on every run; each is reported as Updated only when its content
changed, and any such change closes with Initialized git-paw. instead.)
When something is added, init names it:
Updated .git-paw/config.toml (added missing keys: worktree_placement)
One backfilled key can change behaviour — see
worktree_placement for the warning init prints in that
case.
Settings Reference
default_cli
The AI CLI pre-selected in the interactive CLI picker when --cli is not
passed — you can still choose any other listed CLI. It does not skip the
picker: the assignment-mode prompt and the CLI picker still appear, and in
per-branch mode every branch’s picker starts on this CLI too. Unset, or naming
a CLI that is not available, the picker starts on its first entry. --cli
skips the CLI prompt entirely.
default_cli = "my-cli"
default_spec_cli
The AI CLI to use by default when launching with --from-all-specs or --specs. When set, skips the CLI picker for any specs that don’t have a paw_cli override.
default_spec_cli = "my-cli"
See Spec-Driven Launch for the full CLI resolution chain.
branch_prefix
Prefix prepended to spec-derived branch names. Defaults to "spec/".
branch_prefix = "spec/"
For example, a spec with ID add-auth produces branch spec/add-auth.
mouse
Enable or disable tmux mouse mode for git-paw sessions. When enabled, you can click panes to switch, drag borders to resize, and scroll with the mouse wheel. This is set per-session and does not affect your other tmux sessions.
mouse = true # default
worktree_placement
Controls where git-paw creates an agent’s worktree, relative to the repository:
"child"— inside the repo at.git-paw/worktrees/<branch-slug>. This is the contained layout: worktrees live under the project root, so a single permission grant for.git-paw/worktrees/covers every agent (no scattered sibling directories outside the repo).git paw initwrites this for new repos."sibling"— beside the repo at../<project>-<branch-slug>(the v0.7.0 layout). This is the default when the field is absent, so pre-existing repos and sessions created before this field behave exactly as in v0.7.0.
worktree_placement = "child"
The <branch-slug> for the child layout is derived from the branch name
alone: / becomes - and characters outside [A-Za-z0-9._-] are stripped
(the project name is not prepended, since the directory already lives under
that project’s .git-paw/worktrees/). For example branch feat/auth-flow
maps to .git-paw/worktrees/feat-auth-flow/, and fix/issue#42 maps to
.git-paw/worktrees/fix-issue42/.
Gitignore note. Child worktrees must be ignored or git would try to stage them as part of the repo.
git paw initadds.git-paw/worktrees/to.gitignoreautomatically. If you opt intoworktree_placement = "child"by editing the config manually (without re-runninggit paw init), add.git-paw/worktrees/to your.gitignoreyourself.
Placement only governs new worktree creation. Existing worktrees stay where they are: each session records the concrete worktree path it created, and resume/status/purge operate on that recorded path — so flipping the config mid-project never orphans an already-created worktree.
Backfilled by git paw init. Because worktree_placement is a top-level
default key, re-running git paw init on a
config that omits it writes worktree_placement = "child" — the generated
default, not the "sibling" the field resolves to when absent. That is
deliberate: the backfill exists to bring a config up to the current defaults.
If the repo already has worktrees under the previous sibling layout, that changes where the next worktree is created, so init prints a warning:
warning: init backfilled worktree_placement = "child" — new agent worktrees will be created inside the repo at .git-paw/worktrees/.
1 existing worktree(s) remain in their previous (sibling) location; init did not move them:
/work/my-project-feat-auth
To keep the previous layout, set worktree_placement = "sibling" in .git-paw/config.toml;
otherwise leave it as "child" and new agents will use the contained layout.
Init never moves, deletes, or re-registers a worktree — it only warns. You choose:
- Keep the old layout — set
worktree_placement = "sibling"in.git-paw/config.toml. Init leaves an explicitly-set value alone on every later run. - Adopt the contained layout — leave the backfilled
"child". Existing worktrees stay beside the repo and keep working (their recorded paths are unchanged); only newly created ones land in.git-paw/worktrees/.
A config that already sets worktree_placement (to either value) is never
touched, and the key is never duplicated.
docs_base_url
Base URL of the documentation site the bundled docs-fetch skill consults on demand. Defaults to git-paw’s published documentation site; set it so a fork or mirror can point the helper at its own docs.
docs_base_url = "https://bearicorn.github.io/git-paw"
The docs-fetch.sh helper resolves this value from .git-paw/config.toml for
both discovery (llms.txt) and page retrieval, falling back to the built-in
default when the field is absent. Documentation lookup is best-effort: an
unreachable site or missing page never blocks the agent.
Worktree runtime provisioning
git-paw isolates each agent’s source — every agent gets its own worktree —
but not its runtime. Sibling worktrees otherwise read the same .env and
collide on the same ports the moment two agents start a dev server, preview, or
debugger. The [worktree] table gives each worktree its own copy of the env
files you name, its own block of ports, and a pair of hooks for whatever else
your stack needs isolated.
Every sub-table is opt-in. With none present, worktree creation behaves
exactly as it did before this table existed: no files are copied, no
.env.local is generated, and no command runs. git-paw ships the mechanism
only — it assumes no filename, no port, and no stack, so you declare what your
project actually needs.
[worktree.env]
copy = [".env", ".env.development"]
[worktree.ports]
base = 3000
stride = 10
vars = ["PORT", "VITE_PORT"]
[worktree.hooks]
on_create = "scripts/paw-db-branch.sh {worktree_id}"
on_remove = "scripts/paw-db-drop.sh {worktree_id}"
[worktree.env]
| Key | Type | Default | Meaning |
|---|---|---|---|
copy | list of strings | [] | Repository-root-relative files copied into each new worktree |
Each entry is copied verbatim from the repository root into the new worktree, after the worktree is created and before the agent’s CLI starts — so the agent’s first read already sees a provisioned checkout.
The propagated file is an independent copy, never a symlink. That is
deliberate: a symlinked .env is shared mutable state across every agent, which
is the exact silent runtime corruption worktrees exist to prevent. A copy
diverges cleanly instead, and any drift surfaces at merge time.
A declared file that does not exist at the repository root is skipped with a
warning and the worktree is still created — per-branch divergence is normal
and a missing optional env file should not cost you a session. Entries that are
absolute paths, or that escape the repository root with .., are rejected with
a warning; provisioning only ever reads from the repository and writes into the
worktree.
[worktree.ports]
| Key | Type | Default | Meaning |
|---|---|---|---|
base | integer (u16) | 0 | First port of the first worktree’s block |
stride | integer (u16) | 0 | Block size per worktree |
vars | list of strings | [] | Env-var names that receive sequential ports within the block |
Each worktree holds a runtime slot and its block starts at
base + slot * stride. The Nth entry of vars receives
base + slot * stride + N. With the example above, the first worktree gets
PORT=3000 / VITE_PORT=3001 and the second gets PORT=3010 /
VITE_PORT=3011.
The assignments are written into a delimited managed block in the worktree’s
.env.local — the override layer most stacks load last:
# >>> git-paw (managed) >>>
PORT=3000
VITE_PORT=3001
# <<< git-paw (managed) <<<
Anything you keep in .env.local outside that block is preserved, and
regenerating the block is idempotent. A copied .env is never modified;
offset ports live only in .env.local.
An empty vars list writes no port lines at all — git-paw supplies no default
port variables.
stridemust be at leastvars.len(). A smaller stride would make adjacent worktrees’ blocks overlap and hand two live agents the same port. git-paw clamps the stride up to the number of vars and warns when it loads a config that gets this wrong, but set it correctly — leave headroom for vars you add later.
Ports are deterministic, not probed. git-paw does not scan the host for free ports, so an assigned port can still be occupied by an unrelated process. The trade is simplicity and stability: the same worktree keeps the same ports across restarts. If a block clashes with something else on your machine, change
baseorstride.
Slots across add/remove churn
The slot is recorded in session state (runtime_slot on the worktree entry)
rather than derived from the worktree’s position in the list. Position would
shift when a middle worktree is removed, handing a newly added agent a live
agent’s ports.
A new agent takes the lowest slot no live agent holds, so blocks stay
bounded instead of climbing forever, and removing an agent frees its block for
the next git paw add. Two concurrently active worktrees are never assigned
overlapping blocks. Session state written before this field existed loads with
no slot and is assigned one on next provisioning.
[worktree.hooks]
| Key | Type | Default | Meaning |
|---|---|---|---|
on_create | string | unset | Command run in a new worktree after env/port provisioning, before the agent’s CLI starts |
on_remove | string | unset | Command run in the worktree at git paw remove / git paw purge, before the directory is deleted |
The third shared runtime resource — a database — cannot be isolated by a
built-in mechanism, because branching one is entirely stack-specific: a Neon or
PlanetScale branch API, a local CREATE DATABASE, a Docker volume, a Redis
namespace. git-paw therefore ships no driver and instead runs your command at
the two lifecycle points, letting you provision and tear down whatever
per-worktree resource your stack needs.
[worktree.hooks]
on_create = "scripts/paw-db-branch.sh {worktree_id}"
on_remove = "scripts/paw-db-drop.sh {worktree_id}"
Both keys are optional and independent — declare one, both, or neither. With no
[worktree.hooks] table, no command runs and worktree lifecycle is byte-identical
to configuring [worktree.env] / [worktree.ports] alone.
Placeholders
Before the command is executed, git-paw substitutes:
| Placeholder | Expands to |
|---|---|
{worktree_id} | The worktree’s stable identifier — the branch-derived slug that also names its directory (feat/auth-flow → feat-auth-flow) |
{worktree_path} | The worktree’s absolute path |
The id is restricted to [A-Za-z0-9._-], so it is always safe to interpolate.
{worktree_path} is substituted verbatim — quote it in your command
(db.sh '{worktree_path}') if your repository path may contain spaces.
Each command runs through /bin/sh -c with the worktree as its working
directory, so relative paths in your script resolve against the checkout and
scripts/paw-db-branch.sh means the copy in that worktree. Nothing extra is
injected into its environment.
on_create stdout becomes environment
git-paw reads on_create’s standard output and merges every KEY=value line
into the same .env.local managed block the port assignments are written
to. That is how a hook hands its connection details to the agent:
$ cat scripts/paw-db-branch.sh
#!/bin/sh
createdb "paw_$1" >/dev/null
echo "DATABASE_URL=postgres://localhost/paw_$1"
$ cat .git-paw/worktrees/feat-two/.env.local
# >>> git-paw (managed) >>>
PORT=3010
VITE_PORT=3011
DATABASE_URL=postgres://localhost/paw_feat-two
# <<< git-paw (managed) <<<
A line counts as an assignment only if it is an identifier
([A-Za-z_][A-Za-z0-9_]*) followed by =. Everything else — progress text,
blank lines, your script’s chatter — is ignored, so a hook is free to be
talkative. The value is taken verbatim to end of line, so a URL keeps its own
= signs. Content outside the managed block is untouched, and re-provisioning
the same worktree is idempotent.
Because the ports are written before the hook runs, a hook that needs its
agent’s port can read .env.local and use it.
Failure policy
The two hooks fail in deliberately opposite directions:
on_createnon-zero fails that worktree’s provisioning, reporting the hook’s exit status and its stderr. Handing an agent a half-built runtime is worse than refusing to start it.on_removenon-zero only warns and the removal continues. A failed teardown must never strand a worktree on disk — so write your teardown script to be idempotent and reconcilable out of band. git-paw does not track external resource state, and a crash or a force-purge can skip the hook entirely.
Secrets and trust
Print secrets to stdout, diagnostics to stderr. git-paw logs only the names of the variables a hook merged, never their values, so a
DATABASE_URLcarrying a password never reaches a log line or an error. A failing hook’s stderr is surfaced, so anything secret there would be printed.
.env.localis a plain file — gitignore it. git-paw writes the hook’s values into the worktree’s.env.localwith ordinary permissions. If your repository does not already ignore it, add it, or an agent may commit a credential.
Hooks run arbitrary consumer commands. This is by design and is the same trust model as
[clis.*].commandand the[supervisor]command keys: the command comes from your repository’s config file and runs with your privileges. git-paw never builds a hook out of broker, network, or agent-supplied input. Treat write access to.git-paw/config.tomlas equivalent to command execution on your machine, and review changes to it in the same way you review a build script.
Custom CLIs
Register custom AI CLIs that aren’t in git-paw’s built-in detection list.
A [clis.<name>] entry has two distinct names, and they are used for
different things:
commandis what the pane launches. Every launch path — the bare start, the spec-driven start, the supervised start,git paw add, and session recovery — spawns the entry’scommand, not the key. It is emitted as exactly one shell word: single-quoted (with embedded single quotes escaped) whenever it contains any character outside the portable safe setA-Z a-z 0-9 _ - . / + , : @ %, so a path containing spaces or shell metacharacters is neither word-split nor expanded. Approval flags and session-id / resume arguments are appended after it.- The
<name>key is the CLI’s identity. Approval-flag, session-id and resume-template, prompt-profile, settle-delay, and MCP-capability lookups all resolve against the key, as does theclirecorded in each saved worktree entry. A CLI name with no matching entry (a built-in detected CLI, or an explicit pass-through name) launches under that name unchanged.
So [clis.claude-variant] command = "claude" launches claude while every
per-CLI override and the saved session still read claude-variant.
Via config file
[clis.my-agent]
command = "/usr/local/bin/my-agent" # absolute path
display_name = "My Agent" # optional, shown in prompts
[clis.local-llm]
command = "ollama-code" # binary name (resolved via PATH)
display_name = "Local LLM"
# A claude-family variant that reads a non-default config directory.
[clis.claude-variant]
command = "claude"
# Boot-prompt settle delay (ms) before the submit Enter. git-paw injects
# the boot block, waits this long for a paste-aware CLI to settle the
# paste, then sends Enter separately. Default suits most CLIs; raise it
# for a CLI whose large-paste handling needs longer before submit lands.
submit_delay_ms = 1500
# Path to this CLI's claude-format settings file. Parsed and preserved for
# compatibility only: git-paw never reads, writes, or seeds this file, and
# nothing in git-paw acts on it today. A leading `~` is expanded to the home
# directory.
settings_path = "~/.config/claude-variant/settings.json"
# Per-approval-level flag overrides, consulted BEFORE git-paw's built-in
# flag table when composing launch commands. Keys are the approval level
# names ("manual" | "auto" | "full-auto"); values are appended verbatim to
# the launch command at that level. This is how a variant CLI with no
# built-in table row gets native permission flags (e.g. full-auto for a
# claude-oss variant). Unknown level keys fail the config load.
approval_args = { "full-auto" = "--dangerously-skip-permissions" }
# Templates for assigning and resuming a worker CLI's own session identity.
# Each MUST contain the {{SESSION_ID}} placeholder; a template lacking it is
# rejected at config load, naming the key. Resolution is override → built-in
# → none, same order as approval_args. The built-in table covers `claude`
# (`--session-id {{SESSION_ID}}` / `--resume {{SESSION_ID}}`); a CLI without
# a built-in row and without these overrides launches unchanged (no id
# assigned, no resume on recovery).
session_id_args = "--session-id {{SESSION_ID}}"
resume_args = "--resume {{SESSION_ID}}"
# Arguments that make this CLI run one non-interactive turn reading its
# prompt from stdin — used when the unattended drive loop hands a judgment
# call to a headless orchestrator. Resolution is override → built-in
# (`claude` → `-p`, `codex` → `exec`) → none. An empty string fails the load.
headless_args = "-p"
# Whether this CLI speaks MCP, deciding which form of the coordination boot
# block it receives. Unset by default (and omitted from written config), in
# which case the built-in MCP-capable set decides. Only set `true` once
# git-paw's MCP server is registered for THIS CLI — see the field table.
# mcp = true
# Per-field overrides of the CLI's terminal-shape markers (readiness
# banner, approval-prompt wording, …), consulted before the embedded
# Claude Code default. See "CLI Prompt-Shape Profiles" in the user guide.
[clis.claude-variant.prompt_profile]
approval_markers = ["proceed? (y/n)"]
| Field | Required | Purpose |
|---|---|---|
command | yes | Command or path to the CLI binary — what the pane launches, emitted as one shell word. Must be non-empty; a blank value is rejected at config load, naming the entry. |
display_name | no | Human-readable name shown in prompts. |
submit_delay_ms | no | Boot-prompt settle delay (ms) before the submit Enter; per-CLI so the launcher stays CLI-agnostic. |
settings_path | no | Path to the CLI’s claude-format settings file. Parse-only: accepted and preserved across a load/save round-trip so existing configurations keep working, but git-paw never reads, writes, or acts on it — not even git paw setup-cli, whose only target is the main checkout’s project-local settings file. |
approval_args | no | Map from approval-level name ("manual", "auto", "full-auto") to the flags appended to the launch command at that level. Consulted before the built-in flag table; an unknown level key is rejected at config load with an error naming the key. |
session_id_args | no | Template (containing {{SESSION_ID}}) for the flag(s) that assign a fresh session identifier at launch. Consulted before the built-in table (claude only); a CLI resolving neither launches unchanged, with no id recorded. |
resume_args | no | Template (containing {{SESSION_ID}}) for the flag(s) that resume a previously assigned session. Appended on recovery when the worktree’s recorded id and this CLI’s resolved template are both present; a template omitting the placeholder is rejected at config load. |
headless_args | no | Arguments that make the CLI run one non-interactive turn reading its prompt from stdin, used for the drive loop’s headless orchestrator hand-off. Consulted before the built-in table (claude → -p, codex → exec); a CLI resolving neither has no headless form, and the hand-off is unavailable for it. An empty string is rejected at config load, naming the key. The hand-off spawns this entry’s command directly, not through a shell, so it must name a single program (a path or a name on PATH); the headless arguments and approval flags are split on whitespace, so quoted arguments are not supported there. |
prompt_profile | no | Per-field overrides of the CLI’s terminal-shape markers, consulted before the embedded Claude Code default. Carries no permission policy — see CLI Prompt-Shape Profiles. |
mcp | no | Boolean. Whether this CLI identity speaks MCP, selecting the MCP publish-tool form of the coordination boot block over the broker.sh helper form. Unset by default and omitted when the config is written back. |
settings_path used to feed a protected-path set that the auto-approve
classifier consulted before approving a write. That classifier is retired, so
the field now has no effect at all — it is kept so existing configurations
load and round-trip unchanged. Agent memory isolation is carried by the
bundled skills’ doctrine, and mechanically by deny rules in the agent CLI’s
own settings or by the sandbox. See
Agent permissions.
mcp and the boot-block form
Whether a CLI receives the MCP tool form of the coordination boot block is resolved in this order:
mcp = trueormcp = falseon the matching[clis.<name>]entry;- otherwise the built-in MCP-capable set — exactly
claude,codex,cursor-agent, andagy; - otherwise not MCP-capable, so the
broker.shhelper form is used.
The lookup uses the CLI identity (the [clis.<name>] key, or the
built-in / pass-through name), never the resolved command. That is why
[clis.claude-oss] command = "claude" does not inherit claude’s MCP form:
a variant typically reads a different config directory, which may not have
git-paw’s MCP server registered.
Warning: setting
mcp = truefor a CLI that does not have git-paw’s MCP server registered strands the agent at boot — the boot block names publish tools the agent cannot call, so its very first coordination step fails. Register the server for that CLI first (see MCP), then opt in.
A config without an mcp key loads and behaves exactly as it did before the
field existed.
Files git-paw writes
setup-cli adds no configuration field. It does write two files outside
.git-paw/, both only when you run it:
| Path | What it is |
|---|---|
<main checkout>/.claude/settings.local.json | The claude allowlist item’s only target. Merged into — every key and its order are preserved. |
<git-common-dir>/git-paw/cli-setup.json | The setup record: what git-paw wrote, so --undo can take back exactly that. Owner-only, inside the git directory, so it can never be committed and never shows up in any worktree’s git status. |
See Per-CLI Setup.
Via command line
# Add with absolute path
git paw add-cli my-agent /usr/local/bin/my-agent
# Add with binary name on PATH
git paw add-cli my-agent my-agent
# Add with display name
git paw add-cli my-agent my-agent --display-name "My Agent"
# Remove
git paw remove-cli my-agent
The add-cli and remove-cli commands modify the global config (see Config File Locations). add-cli stores a binary name given on the command line as its resolved absolute path.
Listing CLIs
git paw list-clis
Shows both auto-detected and custom CLIs with their source:
NAME SOURCE PATH
claude detected /usr/local/bin/claude
codex detected /usr/local/bin/codex
my-agent custom /usr/local/bin/my-agent
Presets
Presets save branch + CLI combinations for one-command launch.
Defining presets
[presets.backend]
branches = ["feature/api", "fix/db-migration"]
cli = "claude"
[presets.full-stack]
branches = ["feature/api", "feature/ui", "feature/styles"]
cli = "agy"
Using presets
git paw start --preset backend
This skips all interactive prompts and launches with the preset’s branches and CLI.
Specs
Configure spec file scanning for --from-all-specs and --specs mode.
[specs]
dir = "specs" # Directory containing spec files (relative to repo root)
type = "openspec" # "openspec" (OpenSpec changes), "markdown" (flat .md files), "speckit" (GitHub Spec Kit), or "superpowers" (obra/superpowers plans)
| Field | Default | Description |
|---|---|---|
dir | "specs" | Directory to scan for spec files |
type | "openspec" | Spec backend: "openspec" (directory-based OpenSpec changes), "markdown" (flat .md files with YAML frontmatter), "speckit" (GitHub Spec Kit .specify/specs/<feature>/), or "superpowers" (obra/superpowers docs/superpowers/plans/*.md) |
git-paw does not auto-detect the spec system from the filesystem: the [specs]
section is the source of truth. git paw init prompts you to choose the spec
system and writes this section. When [specs] is omitted, launching from specs
errors until you add it — or pass the --specs-format CLI flag, which selects
the spec system for a single launch.
See Spec-Driven Launch for format details.
Logging
Configure session output logging.
[logging]
enabled = true
| Field | Default | Description |
|---|---|---|
enabled | false | Whether to capture pane output to log files |
When enabled, logs are written to .git-paw/logs/<session>/ using tmux pipe-pane. See Session Logging for details.
Broker
Configure the HTTP broker for agent coordination. When enabled, git-paw starts a lightweight HTTP server that lets agents share status updates, artifacts, and blocked requests.
[broker]
enabled = true
port = 9119
bind = "127.0.0.1"
wave_stall_minutes = 30
| Field | Default | Description |
|---|---|---|
enabled | false | Whether to start the coordination broker |
port | 9119 | HTTP port for the broker server |
bind | "127.0.0.1" | Bind address – never bind to 0.0.0.0 on shared machines |
wave_stall_minutes | 30 | Minutes without wave progress after which the broker reports the wave as stalled on GET /status, in git paw status, and on the dashboard status line. “Progress” is a wave phase advancing or a branch tip moving, and nothing else — see Wave staleness. 0 disables the stalled flag while still reporting idle_seconds. Omitted → the documented default |
The default of 30 minutes sits just above the unattended drive loop’s 25-minute heartbeat, so a wave that is merely between heartbeats is not reported as stalled. A config written before the key existed loads unchanged and gets the default.
When the broker is enabled, git-paw injects the GIT_PAW_BROKER_URL environment variable into each agent pane, pointing to http://<bind>:<port>. Agents use this URL to communicate with the broker.
Warning — a non-loopback
bindremoves a protection and adds exposure. The broker authenticates nothing: whoever reaches the port can read the whole coordination log and publish any message, including guidance that coding agents will act on — up to whatever their CLI’s permission mode lets them run without asking. On the default loopbackbind, git-paw checks every request’sHostheader so a web page cannot reach the broker by DNS rebinding. Settingbindto"0.0.0.0"or a LAN address disables that check — git-paw cannot know your network’s names for the machine — and makes the broker reachable from the network with no authentication at all. It also stops blocking DNS rebinding: theOrigincheck still runs on every bind and still blocks browser writes and cross-origin reads, but a rebinding page is same-origin with the broker and itsGETs carry noOrigin, so it can read/log,/statusand/messages/*. Only a loopback bind prevents that. Both the broker and the command that launches the session print a warning on stderr when you set a non-loopback bind. See Security Posture.
Filesystem watcher
The broker’s filesystem watcher publishes agent.status: working whenever an agent’s git status changes. By default, an agent that publishes agent.artifact status: "committed" and then keeps editing is re-entered into the working state — a file modification observed within a TTL window after the commit re-publishes working, so the dashboard reflects the agent’s continued activity instead of sticking on committed.
[broker.watcher]
republish_working_ttl_seconds = 60
| Field | Default | Description |
|---|---|---|
republish_working_ttl_seconds | 60 | Seconds after a committed event during which a file write re-publishes working. 0 disables the auto-republish (restoring the prior “committed is terminal until the agent itself republishes” behaviour). Non-zero values below 5 are clamped to 5 with a warning. |
Past the TTL window the agent is considered settled at committed; only an explicit agent.status from the agent itself transitions it out.
Multi-repo port assignment
If you run git-paw sessions for multiple repositories at the same time, each session needs a different port. Set a unique port in each repo’s .git-paw/config.toml:
# Repo A
[broker]
enabled = true
port = 9119
# Repo B (in its own .git-paw/config.toml)
[broker]
enabled = true
port = 9120
This is required, not advisory. Before binding, git-paw probes the configured port and asks the listener which repository it serves (GET /status reports repo_root). A broker for this repository is reattached to; a broker for a different repository is refused:
port 9119 is held by a git-paw broker for another repository
(/home/you/other-repo) — set a distinct [broker] port in
.git-paw/config.toml for one of the repositories
Give one of the two repositories its own port and start again. The refusal exists because reattaching would route this repository’s agents into the other repository’s broker while this repository’s dashboard rendered an empty, unwatched state.
The check runs before anything is created — and before you are asked anything. git paw start (including --from-specs, the supervised launch, and session recovery) probes the port as soon as it has read your config, ahead of the branch and CLI pickers, so you are never asked to choose for a launch that cannot proceed and a refused launch leaves no session, no panes, and no session receipt behind. That ordering matters: git-paw injects GIT_PAW_BROKER_URL into every agent pane, so a refusal that came only when the dashboard started would already have handed your agents the other repository’s broker.
Recovering a stopped session probes the port recorded in the saved session rather than the configured one, because that is the URL the rebuilt agent panes are given — they can differ if you changed [broker] port after the session was saved. --dry-run creates nothing and never probes.
git paw status and git paw doctor use the same repository-aware probe, so neither reports a neighbouring repository’s broker as your session’s own:
Broker: http://127.0.0.1:9119 (held by another repository's git-paw broker)
⚠ broker enabled — 127.0.0.1:9119 is held by another repository's git-paw broker
→ set a distinct `[broker] port` in .git-paw/config.toml for one of the repositories
A variant of the message appears when the port holder does not report a repository at all:
port 9119 is held by a git-paw broker for another repository (it does not
report which — an older git-paw broker may be holding the port) — set a
distinct [broker] port in .git-paw/config.toml for one of the repositories
An unknown owner is never assumed to be yours. This is what you see when a session launched by an older git-paw is still running on the port — stop that session, or move this one to a different port.
A port held by something that is not git-paw at all reports the separate port ... is already in use by another process error.
Each session also keeps its own broker log, <session_name>.broker.log, so two repositories never write to — or purge — the same file. A session saved by an earlier git-paw recorded a single shared broker.log; that recorded path stays authoritative for both the broker and purge until the session is resumed or recovered, which migrates it to the per-session name.
See Agent Coordination for usage details.
Supervisor
Configure the supervisor agent for orchestrating parallel coding sessions. When enabled, the supervisor monitors agents, runs tests, verifies work, and coordinates merges.
[supervisor]
enabled = true
cli = "claude"
test_command = "just check"
lint_command = "cargo clippy -- -D warnings"
build_command = "cargo build"
fmt_check_command = "cargo fmt --check"
doc_build_command = "mdbook build docs/"
doc_tool_command = "cargo doc --no-deps"
spec_validate_command = "openspec validate {{CHANGE_ID}} --strict"
security_audit_command = "cargo audit"
agent_approval = "auto"
approval = "full-auto"
verify_on_commit_nudge = true
strict_branch_guard = true
manual_approvals_log = true
no_progress_window_seconds = 1500
context_bloat_threshold_k = 250
blocked_on_supervisor_window_seconds = 900
branch_refresh = false
| Field | Default | Description |
|---|---|---|
enabled | false | Whether to use supervisor mode by default (can also use --supervisor flag, or override with --no-supervisor for a single session) |
cli | (uses default_cli) | The orchestrator’s CLI, named in its context (.git-paw/AGENTS.local.md); no pane runs it, and no orchestrator CLI is needed to launch. Also the coding agents’ last CLI fallback (--cli > default_cli > [supervisor].cli) |
test_command | (none) | Test runner — gate 1 (e.g. "just check", "cargo test", "npm test", "pytest") |
lint_command | (none) | Lint check — gate 1 (e.g. "cargo clippy -- -D warnings", "npm run lint", "ruff check .", "golangci-lint run") |
build_command | (none) | Compile step — gate 1 when build is distinct from test (e.g. "cargo build", "npm run build", "mvn package", "go build ./...") |
fmt_check_command | (none) | Formatter check — gate 1 (e.g. "cargo fmt --check", "prettier --check .", "gofmt -l .", "black --check .") |
doc_build_command | (none) | Documentation build — gate 4 (e.g. "mdbook build docs/", "sphinx-build", "mkdocs build") |
doc_tool_command | (none) | API-doc generator — distinct from doc_build_command; gates the per-language extractor for changed public items (e.g. "cargo doc --no-deps", "sphinx-build -W docs docs/_build", "javadoc", "npx typedoc", "go doc"). Renders empty (not (not configured)) when unset so the surrounding prose reads naturally |
spec_validate_command | (none) | Spec validator — gate 3 (e.g. "openspec validate {{CHANGE_ID}} --strict" for OpenSpec). {{CHANGE_ID}} is substituted by the supervisor agent at verification time with the change name being audited; it is not expanded at config load |
security_audit_command | (none) | Security audit tooling — gate 5 (e.g. "cargo audit", "npm audit", "bandit -r .", "gosec ./...") |
agent_approval | "auto" | Permission level for coding agents: "manual", "auto", or "full-auto" |
approval | (unset) | The orchestrator’s own permission level: "manual", "auto", or "full-auto", distinct from agent_approval. No pane runs an orchestrator, so it changes no pane command; it is the approval level of the drive loop’s headless orchestrator invocations, whose flags resolve for (cli, approval) and fall back to agent_approval when unset. full-auto on a CLI with no flag row and no approval_args override warns and runs the invocation without flags. Coding agents resolve from agent_approval alone |
verify_on_commit_nudge | true | When on, the broker posts a supervisor.verify-now message to the supervisor inbox on every agent.artifact { status: "committed" }, so the supervisor verifies each commit promptly on an explicit event instead of batching. Set false to fall back to sweep-cadence verification |
strict_branch_guard | true | When true, a per-worktree pre-commit hook refuses any commit whose checked-out branch differs from the branch the worktree was created for, blocking cross-worktree contamination (linked worktrees share .git/refs, so a stray cd can otherwise advance the wrong branch). Set false to disable enforcement — the post-commit hook still publishes an agent.feedback + agent.learning record when it detects a mismatch (detection without enforcement) |
manual_approvals_log | true | When true, every live permission prompt the --unattended drive loop escalates for a human decision is appended to .git-paw/sessions/<session>.manual-approvals.jsonl and surfaced via git paw approvals. Applies to --unattended runs only — an attended session runs no drive loop and records nothing. On a pattern’s first sighting a permission_pattern learning is also emitted (when learnings = true). Set false to suppress both the log writes and the learnings emission; the opt-out affects writes only, so git paw approvals still reads any pre-existing log |
no_progress_window_seconds | 1500 (~25 min) | Read by .git-paw/scripts/sweep.sh: an agent is flagged no-progress when BOTH its completed-task-checkbox count AND its branch commit count stay unchanged for this many seconds. Set longer to tolerate long build/research steps, shorter to nudge sooner. Omitted → the documented default |
context_bloat_threshold_k | 250 (thousand tokens) | Read by .git-paw/scripts/sweep.sh: when an agent’s pane shows a /clear to save <N>k tokens hint whose N meets or exceeds this value, the agent is proactively flagged context-bloat so the supervisor can pre-empt the eventual freeze. Omitted → the documented default |
blocked_on_supervisor_window_seconds | 900 (~15 min) | Read by .git-paw/scripts/sweep.sh: an agent whose latest unanswered agent.blocked names the supervisor as the blocker is flagged blocked-on-supervisor once it has waited longer than this window, forcing the supervisor to answer. Omitted → the documented default |
branch_refresh | false | When true, the unattended drive loop rebases an idle, clean worker branch onto the default branch after a successful merge — see Branch refresh for the full precondition gate list. Default off: history is rewritten under a live agent only on explicit opt-in |
learnings | false | Master switch for learnings mode: when true (and the broker is enabled), the session’s friction signals are appended to .git-paw/session-learnings.md. Tuned by the [supervisor.learnings_config] sub-table |
auto_revert | false | Used by opsx role gating in block mode: when true, the supervisor reverts a coding agent’s archive commit without first confirming with you. Default off — a revert always asks first |
Approval flag resolution. When composing a pane’s launch command, git-paw
resolves the permission flags for a (CLI, level) pair in order: (1) the
per-CLI [clis.<name>] approval_args override, (2) the
built-in table below, (3) no flags. Coding-agent panes always resolve at
agent_approval; no pane resolves at approval — only the drive loop’s
headless orchestrator invocation does. This resolution applies on every
launch path that spawns a worker CLI — supervisor mode, a bare git paw start, a non-supervisor --specs / --from-all-specs launch, and git paw add alike — not only supervisor sessions, even though agent_approval is
declared under [supervisor]. A bare or spec-driven launch with no
[supervisor] section at all still resolves agent_approval against its
default ("auto").
| CLI | full-auto | auto |
|---|---|---|
claude | --dangerously-skip-permissions | --permission-mode acceptEdits |
codex | --dangerously-bypass-approvals-and-sandbox | --sandbox workspace-write |
agy | --dangerously-skip-permissions | (none) |
qwen | --yolo | (none) |
Any other CLI (or manual anywhere) resolves to no flags — use
approval_args to supply native flags for CLIs not in the table.
auto resolves to a deterministic flag wherever the CLI has a low-friction
“accept edits” mode distinct from manual and from full bypass, rather than
the empty string — an auto pane no longer silently inherits whatever
default its CLI’s config directory happens to carry. For Claude,
--permission-mode acceptEdits has the CLI accept file edits without asking;
shell commands still raise a prompt.
These resolved flags are git-paw’s only automatic influence on what an
agent runs without a prompt. git-paw approves no prompt itself: anything the
chosen mode leaves prompting is escalated for a human decision under
--unattended, and answered only by an explicit sweep.sh approve.
One further influence exists, and only because you ask for it:
git paw setup-cli <CLI> --allowlist writes
path-scoped allow rules for git-paw’s own helper scripts into the agent CLI’s
settings. Running that command is the consent — no launch, recovery, drive
loop, dashboard or MCP tool ever applies it, it prints its plan before
writing, and --undo removes exactly what it added. Those rules are then the
CLI’s rules, enforced by the CLI’s own permission system; git-paw still
decides nothing about any prompt. See
Agent permissions.
Gate-command templating. The eight *_command keys feed the supervisor
skill’s five verification gates (testing, regression analysis, spec audit, doc
audit, security audit). For each key set on this section, the supervisor skill
substitutes the matching {{...}} placeholder at session boot and the
supervisor agent runs the literal command during that gate. For each key
omitted, the placeholder renders as (not configured) and the supervisor
agent skips that tooling step — the gate’s manual review still applies (e.g.
the OWASP-category diff scan for the security gate, the spec scenario
coverage check for the spec gate). Pre-v0.5.x configs that did not name any
of the six new keys continue to work; they just run a less-rigorous
verification cycle until the keys are filled in. A user wanting to explicitly
opt out of a single gate’s tooling can set the field to "(not configured)"
verbatim — the supervisor agent recognises that as the same skip token.
Resolution chain — git-paw picks supervisor mode using the first matching rule:
--no-supervisor→ off (highest precedence; overrides everything below).--supervisor→ on.[supervisor] enabled = true→ on.[supervisor] enabled = false→ off.- No
[supervisor]section +--dry-run→ off. - No
[supervisor]section + interactive TTY → prompts you. - No
[supervisor]section + non-TTY → off.
--supervisor and --no-supervisor are mutually exclusive — passing both produces a parse error.
Approval levels:
| Level | Behavior |
|---|---|
manual | Agents prompt for every action (safest, slowest) |
auto | Low-friction mode — the CLI’s mapped auto flag from the table above (e.g. accept-edits for Claude); a CLI with no mapped flag keeps its default behavior |
full-auto | Skip all permission prompts (fastest, agents run unattended) |
The supervisor translates the approval level into CLI-specific flags at launch (e.g. --dangerously-skip-permissions for Claude in full-auto mode).
Deprecated auto-approval tables
[supervisor.auto_approve] and [supervisor.common_dev_allowlist] are
deprecated and have no effect. They configured a git-paw command classifier
that decided an agent’s command was safe and then answered the permission
prompt for it. That classifier is retired: three successive security reviews
each found new command shapes it wrongly called safe, and each fix opened the
next hole. git-paw now makes no safety judgement about an agent’s command and
approves no permission prompt.
Both tables, and every field in them, still parse and round-trip, so an existing configuration loads and saves unchanged — nothing breaks and no migration is forced. But:
- none of their values is consulted by anything;
- each configuration file that declares either table prints one deprecation warning per process, naming the tables it found, on stderr;
git paw initno longer writes either table into the default config.
# Deprecated — parsed, warned about, and ignored.
[supervisor.auto_approve]
enabled = true
safe_commands = ["just lint", "just test"]
stall_threshold_seconds = 30
approval_level = "safe"
approve_worktree_writes = true
[supervisor.common_dev_allowlist]
enabled = true
stacks = ["rust"]
extra = ["just check"]
Delete them whenever convenient. A configuration that git-paw v0.16.0 saved may
carry a [supervisor.common_dev_allowlist] table you never wrote by hand; it
will warn until you remove it.
What to use instead. Deciding whether a command may run is the agent CLI’s job:
- Move
safe_commands,stacksandextraentries into your agent CLI’s own allow rules — its project-level or user-level allowlist. That is the only place a standing grant can live, and it is what makes a routinecargo teststop prompting.git paw approvalsreports which prompts recur, with a hint for which of those two allowlists fits. - For a run that must not stop for prompts at all, set
[supervisor] approval = "full-auto"and run inside the documented sandbox. Note thatfull-autoskips the prompts entirely, so nothing is escalated either. approve_worktree_writesand the protected-path and.git/-write rules it interacted with are gone. Those writes now reach a human like any other prompt; for mechanical enforcement use deny rules in the CLI’s own settings, or the sandbox.
See Agent permissions and the Security Posture.
Durable grants the classifier already accepted. Up to v0.16.0 the classifier preferred the “Yes, and don’t ask again” option for prompts it judged safe, and your agent CLI persisted those as permanent permission rules in its own settings file. Retiring the classifier stops new ones being created; it removes none. Review that file and delete grants you did not intend — git-paw never edits or deletes it.
Conflict detector tuning
When supervisor mode is enabled, the broker runs an in-process conflict
detector that auto-emits agent.feedback (and optionally agent.question)
on forward, in-flight, and ownership conflicts. See
Agent Coordination § Automatic Conflict Detection for the runtime semantics; the table below
documents the configuration surface.
[supervisor.conflict]
window_seconds = 120
warn_on_intent_overlap = true
escalate_on_violation = true
| Field | Default | Description |
|---|---|---|
window_seconds | 120 | Seconds the detector waits before escalating an unresolved in-flight conflict to the supervisor inbox via agent.question. |
warn_on_intent_overlap | true | Master switch for forward-conflict feedback. When false, two agents declaring overlapping agent.intent files no longer trigger agent.feedback, but the intent tracker still records them (so in-flight and ownership detection continue to work). |
escalate_on_violation | true | Whether ownership violations escalate to the supervisor inbox. When false, the violator still receives agent.feedback, but no follow-up agent.question lands in the supervisor inbox. |
The [supervisor.conflict] table is fully optional. A v0.4 config with
[supervisor] and no [supervisor.conflict] loads cleanly with every field
at the defaults above. Setting [supervisor] enabled = false (or omitting
the section) disables the detector subsystem entirely — no auto-emitted
warnings fire regardless of the values here.
Correction loop tuning
The [supervisor.correction] table governs the self-healing correction cycle
the --unattended drive loop runs when a supervisor gate fails: instead of
publishing agent.feedback into an inbox a blocked worker never polls, the
loop injects the gate-tagged feedback straight into the worker’s pane and
re-verifies. See
Supervisor § Unattended correction loop
for the runtime walkthrough.
[supervisor.correction]
auto_loopback = true # default false — opt in to enable the loop
max_cycles = 5
on_exhausted = "escalate" # "escalate" (default) | "abandon"
escalate_after_cycles = 3
gate_tags = ["testing", "regression", "spec audit", "doc audit", "security audit", "scope"]
| Field | Default | Description |
|---|---|---|
auto_loopback | false | Whether a failing gate automatically re-engages the worker’s pane with that gate’s feedback. Off by default so an existing unattended setup behaves identically after an upgrade; the recommended config above turns it on. When false, the feedback is published exactly as before and no keystrokes are sent. |
max_cycles | 5 | Verify↔fix rounds the loop drives for one branch before giving up and applying on_exhausted. A branch that passes its gate leaves the cycle early and its counter resets. Must be a positive integer — 0 is a config error; to turn the loop off set auto_loopback = false. |
on_exhausted | "escalate" | What to do with a branch that reaches max_cycles. "escalate" flags it to the orchestrator/human as unrecoverable; "abandon" marks it failed and leaves it un-corrected without an escalation. Neither re-engages the branch again. Any other value is a config error naming the accepted set. |
escalate_after_cycles | 3 | Cycle count at which a slow-converging worker is flagged once, as an early heads-up, while re-engagement continues to max_cycles. No distinct early flag is emitted when this is greater than or equal to max_cycles. Must be a positive integer — 0 is a config error; to suppress the flag, set it to max_cycles or higher. |
gate_tags | ["testing", "regression", "spec audit", "doc audit", "security audit", "scope"] | Gate names recognised as a [<gate>] prefix on a supervisor agent.feedback error line; only a matching tag (case-insensitive) counts as a failing gate verdict that starts a correction cycle. The default reproduces git-paw’s own five-gate framework plus scope exactly. A consumer whose review process names different gates lists them here instead — the correction loop itself is unaffected, only the tag vocabulary it recognises. Must be non-empty; an empty list is a config error (it would silently disable the loop even with auto_loopback = true — set auto_loopback = false instead). |
The [supervisor.correction] table is fully optional. A v0.13.0 config with
[supervisor] and no [supervisor.correction] loads cleanly with every field
at the defaults above — and because auto_loopback defaults to false, its
observable behaviour is unchanged. Cycle counts live in the drive loop’s
in-process state and are not persisted: restarting the supervisor resets
every branch’s budget.
When [supervisor] learnings = true, a branch that exhausts its budget also
appends a correction_exhausted learning carrying the branch, the worker CLI,
and the cycle count — the signal that tells you a worker’s model tier is
under-powered for a class of task. With learnings disabled, nothing is
recorded.
Headless escalation hand-off
The [supervisor.escalation] table controls whether the --unattended drive
loop hands a judgment call — an ambiguous question, a merge decision or a
correction-exhausted branch, never a permission prompt — to a one-shot,
non-interactive run of the orchestrator CLI. See
Supervisor § Headless hand-off of judgment calls.
[supervisor.escalation]
enabled = true
timeout_seconds = 1800
| Field | Default | Description |
|---|---|---|
enabled | true | Whether to hand judgment calls to a headless one-shot when one resolves ([supervisor].cli, else default_cli, with a headless_args form). false keeps the broker-only path — set it when you drive the session from your own interactive orchestrator. |
timeout_seconds | 1800 | Upper bound on one invocation; on expiry its whole process group is killed and the outcome recorded as timeout. Must be a positive integer — 0 is a config error naming the key. |
The table is optional: a config without it loads with both defaults. The
invocation’s permission level is [supervisor] approval
(falling back to agent_approval).
Routing through the supervisor (/tell)
The [supervisor.tell] table tunes the /agents and /tell commands you
type to the orchestrator to route prompts to individual agents (see
Routing through the supervisor).
[supervisor.tell]
mode = "feedback"
inventory_max_age_seconds = 60
| Field | Default | Description |
|---|---|---|
mode | "feedback" | Default delivery channel for /tell. "feedback" queues an agent.feedback the target picks up on its next inbox poll (safe for mixed-mode sessions). "send-keys" injects the prompt straight into the target pane with tmux send-keys — used only when the target is in accept-edits mode, otherwise /tell falls back to feedback and prints a note. |
inventory_max_age_seconds | 60 | How stale the cached /agents inventory may be before /tell//agents re-poll the broker. Lower it for tighter freshness at the cost of more frequent polling. |
The [supervisor.tell] table is fully optional. A v0.5.0 config with
[supervisor] and no [supervisor.tell] loads with both defaults above and
round-trips unchanged.
Learnings mode tuning
When supervisor mode is active, the parent [supervisor] learnings = true
flag (default false) activates the learnings subsystem. Entries are
appended to .git-paw/session-learnings.md covering the four deterministic
categories (conflict events, where agents got stuck, recovery cycles,
permission patterns — see Learnings). The
[supervisor.learnings_config] sub-table tunes the flush cadence; the
master switch lives on the parent table.
[supervisor]
learnings = true
[supervisor.learnings_config]
flush_interval_seconds = 60
broker_publish = "auto"
| Field | Default | Description |
|---|---|---|
flush_interval_seconds | 60 | How often the learnings aggregator flushes accumulated entries from memory to .git-paw/session-learnings.md. The file is append-only across sessions; a longer interval batches more entries per write. |
broker_publish | "auto" | Whether flushed entries are also published to the broker as agent.learning messages. "auto" follows [broker] enabled (publish when the broker is running, file-only when it is not); "force_off" keeps file-only output even with an active broker. The Markdown file is written either way. |
There are no configuration fields for the qualitative signals
(recurring_failure_shape, doc_gap, adr_drift, scope_mistake,
tooling_friction). Their detection thresholds live in the supervisor skill
prose, not in [supervisor.learnings_config] — to tune how readily the
supervisor publishes a category, edit your local copy of the supervisor
skill rather than a config value. The supervisor publishes each qualitative
learning through the bundled .git-paw/scripts/sweep.sh learn helper, so no
broad curl allowlist grant is required.
See the Learnings Mode chapter for the
category-by-category walkthrough, the output-file format, and how to consume
learnings programmatically via the agent.learning broker variant and the
MCP get_learnings() tool.
opsx role gating
[opsx]
role_gating = "warn" # "warn" (default) | "block" | "off"
git-paw cannot add a permission check to the /opsx:verify and /opsx:archive
slash commands themselves (they live in the OpenSpec project). Instead, when the
session’s spec engine is OpenSpec, a post-commit guard watches for archive
activity committed from a coding-agent worktree and reacts per role_gating:
| Mode | Behaviour on a coding-agent archive |
|---|---|
warn (default) | publish an agent.feedback to the offending agent and record an agent.learning with category permission_pattern |
block | warn behaviour plus publish an agent.feedback to the supervisor requesting it revert the offending commit (the supervisor performs the revert via its merge-orchestration skill — git-paw never runs git revert itself) |
off | guard disabled entirely |
The guard is inert under non-OpenSpec engines (speckit, markdown, or no
spec source) regardless of the mode, and the /opsx: forbidden-command sections
are omitted from the bundled coordination/supervisor skills there too.
A commit is treated as archive activity when either its message matches the
canonical archive shape chore(specs): archive <name>; sync deltas to main specs or its diff moves files into openspec/changes/archive/<name>/
and/or adds a main spec under openspec/specs/<capability>/spec.md. The
supervisor’s own archives (agent_id == "supervisor") never count as a
violation. See the Supervisor guide
for how to read the warning text and tune block mode with
[supervisor] auto_revert.
v0.6.0 behaviour change.
role_gatingdefaults towarn. Sessions where a coding agent archives a change will now see guard feedback and a learnings record. Setrole_gating = "off"to restore the v0.5.0 (no-guard) behaviour.
Governance
Point git-paw at your project’s existing governance documents so the supervisor can read them as context. All fields are optional — list only the docs you have.
[governance]
adr = "docs/adr" # directory of ADR files
test_strategy = "docs/test-strategy.md" # single Markdown file
security = "docs/security-checklist.md" # single Markdown file
dod = "docs/definition-of-done.md" # single Markdown file
constitution = ".specify/memory/constitution.md" # single Markdown file
readme = "README.md" # repository README
docs = "docs/src" # documentation root directory
| Field | Kind | Description |
|---|---|---|
adr | directory | Architecture Decision Records. git-paw does not care which convention (Nygard, MADR, adr-tools) — point at the folder where they live. |
test_strategy | file | The team’s test-strategy document. |
security | file | Security checklist (OWASP-style, project-specific, whatever the team uses). |
dod | file | Definition of Done for completed work. |
constitution | file | Project constitution. Spec Kit users normally let this auto-wire (see below). |
readme | file | Repository README. Surfaced by the MCP get_readme tool (MCP server); unset → the tool returns null. |
docs | directory | Documentation root. Surfaced by the MCP list_docs/get_doc tools, which enumerate and read *.md files confined to this directory; unset → those tools return empty/null. |
git-paw does not dictate the structure, format, or rubric of any of these documents. The supervisor LLM reads them as context and applies judgment during its existing audit flow. There is no [governance.gates] table and no per-doc enforcement switch — gating-per-doc would require git-paw to define “failure” for each doc type, and that is a process choice your team owns.
Paths are stored verbatim and resolved against the repository root at use time. Relative paths point at files inside the repo; absolute paths are accepted as-is. A path that does not exist still loads cleanly — git-paw does not stat the filesystem at config-load. If you point at a missing file, the runtime consumer flags it.
Spec Kit constitution auto-wiring
When governance.constitution is unset AND [specs] type = "speckit", git-paw probes for <specs_dir>/../memory/constitution.md and, if present, populates governance.constitution automatically. This means a typical Spec Kit project (with .specify/specs/ and .specify/memory/constitution.md) gets the constitution wired up without any [governance] entry.
Explicit values always win. If governance.constitution is set to anything — including a path that does not exist or an empty string — auto-wiring is skipped:
[governance]
constitution = "" # disables auto-wiring without deleting the slot
Auto-wiring only runs for the Spec Kit backend. With [specs] type = "openspec", type = "markdown", or no [specs] section, governance.constitution stays whatever the TOML says (defaulting to None).
What the supervisor does with these paths
This [governance] table is the storage slot. The runtime consumer is the supervisor’s boot prompt, which lists each configured doc so the supervisor can read it and apply it to its audit. See the Governance chapter of the user guide for what that looks like end-to-end.
MCP
[mcp]
name = "my-project" # optional; default: "git-paw"
allow_repo_writes = false # optional; repository config only
| Field | Default | Purpose |
|---|---|---|
name | "git-paw" | The identity the git paw mcp server advertises as serverInfo.name in the MCP initialize handshake. Set a custom value to distinguish multiple repositories that each run an MCP server. Distinct from the client-side mcpServers key (the display label in clients like Claude Desktop), which you rename independently. See the MCP server chapter. |
allow_repo_writes | false | Whether the MCP server may write file content into this repository. When true, a root session (a server in the main working tree) gets the spec-authoring write tools create_spec and write_spec_artifact. See Spec authoring. |
The section is optional; omitting it (or name) makes the server advertise git-paw.
allow_repo_writes is the repository’s consent, so it is honoured only from the repository’s own .git-paw/config.toml. A value in the user-level config has no effect. The server reads it once at startup, so restart the MCP server after changing it. It never enables writes from a worktree session. It governs only tools that write file content into the repository: it does not gate the root-session lifecycle tools (start_session, pause_session, stop_session, purge_session) or the verdict tools (publish_verified, publish_feedback), which follow the root-session rule alone. The read-only validate_spec is available whatever its value.
Dashboard
Configure the dashboard TUI rendered in its own pane when the broker is enabled (pane 0 in every mode; in supervisor mode it is the full-width top row).
[dashboard]
show_message_log = true
[dashboard.broker_log]
max_messages = 500
default_visible = true
height_lines = 20
| Field | Default | Description |
|---|---|---|
show_message_log | false | When true, the dashboard renders the legacy scrolling broker-message panel. Superseded by the type-filterable Broker log panel below; leave false for a more compact layout. |
Broker log panel
The [dashboard.broker_log] table configures the v0.6.0 Broker log
panel — a scrolling, type-filterable view of recent broker messages that
fills the screen region freed when v0.5.0 removed the prompt inbox.
| Field | Default | Description |
|---|---|---|
max_messages | 500 | Maximum number of messages retained in the panel’s in-memory ring buffer. When the buffer is full, the oldest message drops off as new ones arrive. The log is in-memory only and is not persisted across dashboard restarts. |
default_visible | true | Whether the panel is shown when the dashboard first launches. The l hotkey toggles visibility at runtime regardless of this value. |
height_lines | 20 | Number of terminal rows the panel occupies when visible — raised from the v0.6.0 fixed 12 so more messages show without scrolling. The agent-status table keeps a positive minimum height and absorbs slack, so the panel stays at this height on tall terminals and yields space before the table shrinks on short ones. |
An absent [dashboard.broker_log] table — as in any v0.5.0 config —
loads these defaults, so existing config files parse unchanged.
See Dashboard for the panel’s hotkeys, filter chips, and details overlay.
Multi-repo configuration
Each repository can have its own dashboard settings in .git-paw/config.toml. The repo-level config overrides the global config.
Layout
Configure the visual styling git-paw applies to the tmux sessions it creates.
[layout]
border_affordances = true # default
| Field | Default | Description |
|---|---|---|
border_affordances | true | When true, git-paw applies pane affordances to the sessions it creates: double-line pane borders (═║ instead of the default ─│), a per-pane label bar showing each pane’s index and role/branch (e.g. 0: supervisor, 1: dashboard, 2: feat/foo), a dim border on inactive panes, and a cyan-bold border on the focused pane. Set to false to opt out and inherit your own tmux styling instead. |
These options are scoped to git-paw-managed sessions (paw-*) only — your other tmux sessions are never touched. They apply to both git paw start and supervisor-mode sessions.
This setting is purely visual. Independently of it, git-paw tags every pane it creates with a pane-scoped @paw_role tmux option naming that pane’s role (supervisor, dashboard, or the agent’s branch). The option draws nothing on its own — only the label bar above renders it — but git paw status and git paw doctor use it to tell a coding-agent pane from an infrastructure pane when they check whether a session’s agents are still running. Turning affordances off therefore changes how a session looks, never how accurately its state is reported.
When to disable. Turn border_affordances off if you run a tmux theme you prefer, are on tmux older than 3.2 (where the double border lines aren’t recognised — git-paw warns and continues, but you may prefer the consistent default look), or find the label strip noisy on small terminals.
See Supervisor for how the labelled layout looks in a supervisor session.
Merging Rules
When both global and repo configs exist, they merge with these rules:
| Field | Merge behavior |
|---|---|
default_cli | Repo wins |
default_spec_cli | Repo wins |
branch_prefix | Repo wins |
mouse | Repo wins |
worktree_placement | Repo wins |
docs_base_url | Repo wins |
clis | Maps merge (repo overrides per-key) |
presets | Maps merge (repo overrides per-key) |
specs | Repo wins |
logging | Repo wins |
broker | Repo wins |
supervisor | Repo wins |
dashboard | Repo wins |
governance | Per-field merge (repo wins on each set field, unset fields fall back to global) |
layout | Repo wins |
opsx | Repo wins |
mcp | Per-field merge (repo wins on each set field, unset fields fall back to global) |
worktree | Repo wins (the whole table, including its env, ports, and hooks sub-tables) |
Example: If global config defines [clis.my-agent] and repo config defines [clis.my-agent] with a different command, the repo version wins. But a [clis.other-tool] in global config still appears — maps are merged, not replaced.
Graceful Absence
If no config files exist, git-paw uses defaults:
- No default CLI (prompts for selection)
- Mouse mode enabled
- No custom CLIs
- No presets
Checking your configuration
git paw doctor reports what git-paw actually resolved from the files above —
whether .git-paw/config.toml parses, the effective worktree_placement, the
resolved spec system and its spec count, the [broker] bind/port state, and
whether each [supervisor] gate command’s binary is on PATH. It also flags
keys this version does not recognise, which is the quickest way to catch a
typo’d field that would otherwise be silently ignored.
Doctor adds no configuration fields of its own — it only reads the settings documented on this page. See the Doctor chapter.
git paw doctor # grouped report
git paw doctor --json # same checks, machine-readable
Supported AI CLIs
git-paw auto-detects these AI coding CLIs on your PATH:
| CLI | Binary | Description | Install | Cold resume |
|---|---|---|---|---|
| Claude Code | claude | Anthropic’s AI coding assistant | npm i -g @anthropic-ai/claude-code | Yes (built-in) |
| Codex | codex | OpenAI’s coding agent | npm i -g @openai/codex | No |
| Antigravity CLI | agy | Google’s agentic coding CLI (replaces the retired Gemini CLI) | See project docs | No |
| Aider | aider | AI pair programming in the terminal | pip install aider-chat | No |
| Vibe | vibe | Mistral AI’s coding CLI | See project docs | No |
| Qwen | qwen | Alibaba’s Qwen coding CLI | See project docs | No |
| Amp | amp | Sourcegraph’s AI coding agent | See project docs | No |
| opencode | opencode | Open-source terminal coding agent | See project docs | No |
| Cline | cline | Cline’s coding agent CLI | See project docs | No |
| Droid | droid | Factory’s coding agent CLI | See project docs | No |
| Pi | pi | Terminal coding agent | See project docs | No |
| Junie | junie | JetBrains’ coding agent CLI | See project docs | No |
| Cursor Agent | cursor-agent | Cursor’s agent CLI | See project docs | No |
| GitHub Copilot CLI | copilot | GitHub Copilot in the terminal | See project docs | No |
| cn | cn | Continue’s coding agent CLI | See project docs | No |
| Kilo Code | kilo | Kilo Code’s coding agent CLI | See project docs | No |
| Kimi | kimi | Moonshot AI’s coding agent CLI | See project docs | No |
“Cold resume” means git-paw can restore the CLI’s prior conversation
when recovering a stopped session or relaunching a dead pane, rather
than starting it fresh — see Per-CLI cold
resume. A “No” here reflects
the CLI having no known session-assignment/resume flags, not a defect
in it; add session_id_args / resume_args to a [clis.<name>]
override (see Custom CLIs) to
enable resume for a CLI that supports it but isn’t in git-paw’s
built-in table yet.
The Cursor row names cursor-agent, Cursor’s agent CLI. The bare cursor
binary is the Cursor editor launcher on most installations, so git-paw no
longer auto-detects it as an AI CLI. This affects auto-detection only — if
cursor is the command you want a pane to launch, any of these still
resolves it unchanged:
git paw start --cli cursor,- a
[clis.cursor]entry (see Custom CLIs), - a saved session whose recorded
cliiscursor.
Spec-driven selection and git paw add validate availability against the
detected list, so there a [clis.cursor] entry is the route to take.
default_cli is not an escape hatch: it only pre-selects a row in the
interactive picker, and the picker is built from the detected list — an
undetected cursor has no row to pre-select, so the setting has no effect.
Headless (one-shot) form
The --unattended drive loop can hand a judgment call to the orchestrator CLI
as a single non-interactive turn that reads its prompt from stdin — see
Headless hand-off of judgment calls.
git-paw knows the headless form of two CLIs:
| CLI | Headless arguments |
|---|---|
claude | -p |
codex | exec |
| Every other CLI | none — set [clis.<name>].headless_args |
Any CLI that can run one turn from stdin gains the hand-off through a
headless_args override in its [clis.<name>] entry;
the override also wins over a built-in row. Without a form, the drive loop
keeps every judgment call on the broker and says so at launch.
Per-CLI Setup Support
git paw setup-cli writes git-paw’s setup items
into a CLI’s own settings. An item is available only when git-paw’s compiled
fact table declares it for that CLI and those facts have been verified
against an installed copy of it.
| CLI | --allowlist | --trust | --hooks |
|---|---|---|---|
claude | Supported, verified against Claude Code 2.1.220 (2026-10-15) | Not supported | Not supported |
| Every other CLI | Not supported | Not supported | Not supported |
Nothing is withheld here as a judgement about those CLIs — their settings
formats simply have not been checked yet. The per-CLI audit that adds them is
planned for v0.23.0. Until then setup-cli reports the item as not supported
and writes nothing, which is the honest answer rather than a guess at a file
format.
A Claude Code variant registered under another name (a wrapper binary in
a [clis.<name>] entry) has no row of its own, but is still covered by
git paw setup-cli claude: the target settings file belongs to the
repository, not to the binary.
git paw doctor reports the current status for your installation.
How Detection Works
git-paw scans your PATH for each known binary name. If found, it records the full path and makes the CLI available for selection.
Detection runs every time you start a session, so newly installed CLIs are picked up automatically.
Adding Custom CLIs
Any AI CLI not in the list above can be registered as a custom CLI:
# Register by path
git paw add-cli my-agent /usr/local/bin/my-agent
# Register by binary name (resolved via PATH)
git paw add-cli my-agent my-agent --display-name "My Agent"
Custom CLIs appear alongside detected ones in the selection prompt. See Configuration for more details.
Deduplication
If a custom CLI has the same binary name as a detected one, the custom definition takes precedence. This lets you override the path or display name of a detected CLI.
Missing CLIs
If a custom CLI’s command cannot be found (the binary doesn’t exist at the specified path and isn’t on PATH), it is excluded from the selection list with a warning. This prevents launching sessions that would immediately fail.
Prompt-shape detection
The list above is git-paw’s binary detection — it says which CLIs it can launch. Separately, the supervisor’s prompt detectors recognise a launched CLI’s terminal surface (its ready banner, permission-prompt wording, and so on) via a prompt-shape profile. git-paw ships only the Claude Code profile, embedded in the binary; no other CLI in the table above has a shipped profile, because a guessed one would misdetect confidently rather than falling back visibly. Every CLI — listed or custom — resolves the embedded Claude default for any field it has no profile override for. See CLI Prompt-Shape Profiles for the field reference and how to author one.
CLI Reference
git-paw is invoked as git paw (or git-paw). Below is the reference for all subcommands and flags.
git paw
Running with no subcommand is equivalent to git paw start.
Parallel AI Worktrees — orchestrate multiple AI coding CLI sessions across git worktrees
Usage: git-paw [COMMAND]
Commands:
start Launch a new session or reattach to an existing one
add Attach a new worktree + agent pane to a running session
remove Detach a single agent from a running session
pause Pause the session (detaches client, stops broker, leaves CLIs running)
stop Stop the session (kills tmux, keeps worktrees and state)
purge Remove all worktrees and session state; keeps branches with unmerged commits
status Show session state for the current repo
attach Reattach the current terminal to the running session for this repo
list-clis List detected and custom AI CLIs
add-cli Register a custom AI CLI
remove-cli Unregister a custom AI CLI
setup-cli Apply git-paw's setup items to one AI CLI's own settings
init Initialize .git-paw/ directory and configuration
replay View captured session logs
approvals Report manually-approved command patterns for a session
mcp Run a Model Context Protocol (MCP) server over stdio
doctor Diagnose the environment, configuration, and repository state
completions Print a shell completion script to stdout
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
git paw init
Initializes a repository for git-paw. Creates the .git-paw/ directory, a default config.toml, the logs directory, the bundled helpers and the exported orchestrator skills, adds the orchestrator pointer to AGENTS.md, and sets up .gitignore.
Usage: git-paw init
Options:
-h, --help Print help
Running init is idempotent — it’s safe to run multiple times.
When it writes a fresh config.toml, init asks which spec system the project
uses (recorded in [specs]; git-paw never auto-detects it) and whether to
enable supervisor mode by default. A non-interactive run writes a commented
[specs] template and [supervisor] enabled = false instead. Re-running init
on an existing config is the upgrade path after a version bump — see
Upgrading an Existing Config.
What it creates:
.git-paw/config.toml— default configuration.git-paw/logs/— log directory (added to.gitignore).git-paw/tmp/— repo-local scratch for isolated verify worktrees and self-test sessions (added to.gitignore; preferred over OS temp because it is OS-independent).git-paw/scripts/sweep.sh— bundled supervisor helper.git-paw/scripts/broker.sh— bundled agent-broker helper (the agent side ofsweep.sh); the agent boot block calls it to self-report to the broker. See Coordination → Broker helper.git-paw/scripts/docs-fetch.sh— bundled agent helper that fetches documentation on demand (see Docs Fetch Skill).git-paw/scripts/_paw_common.sh— shared preamble the bundled helpers source for repository-root resolution, Python 3 detection, and broker / docs URL discovery. Binary-managed like the helpers themselves: it is overwritten on everygit paw init, and a helper cannot run without it.git-paw/skills/orchestrator/SKILL.md— the orchestrator skill, rendered with your[supervisor]gate commands and configured spec format, for the agent you drive the session with;.git-paw/skills/cli-setup/SKILL.mdbeside it.git paw startre-renders both. See Quick Start: Orchestrator Mode- an orchestrator pointer section in the root
AGENTS.md, between<!-- git-paw-orchestrator:start -->and<!-- git-paw-orchestrator:end -->(the file is created when absent; see AGENTS.md) .gitignoreentries for.git-paw/logs/,.git-paw/tmp/,.git-paw/worktrees/(where agent worktrees live under the defaultchildplacement),.git-paw/skills/,.git-paw/session-summary.md, and.git-paw/session-learnings.md
A re-run reports a binary-managed file (a helper, an exported skill, the
pointer section) as Updated only when its content changed, and closes with
Already initialized. Nothing to do. only when nothing was created or changed.
Example:
git paw init
git paw start
Smart start: reattaches if a session is active, recovers if stopped/crashed, or launches a new interactive session. Reattach is checked before anything else — including entering supervisor mode — so re-running start against a repository that already has a live session never forks a second, parallel one; see Reattaching instead of forking.
Usage: git-paw start [OPTIONS]
Options:
--cli <CLI> AI CLI to use (skips CLI picker)
--branches <BRANCHES> Comma-separated branches (skips branch picker)
--from-all-specs Launch from every discovered spec across all configured formats
--specs [<SPECS>...] Comma-separated spec names; bare flag opens picker (TTY required)
--specs-format <SPECS_FORMAT> Override spec format (openspec, markdown, speckit, superpowers) [possible values: openspec, markdown, speckit, superpowers]
--dry-run Preview the session plan without executing
--preset <PRESET> Use a named preset from config
--supervisor Enable supervisor mode for this session
--no-supervisor Disable supervisor for this session, overriding any [supervisor] enabled = true in config
--force Bypass uncommitted-spec validation warning
--no-rebase Skip rebasing existing agent branches onto the default branch before opening worktrees
--unattended Drive the supervisor wave to completion unattended: escalate every permission prompt for a human decision without blocking the wave, detect completion, exit with a summary (approves nothing). On a repo with a live supervised session, binds a new drive loop to it instead of launching
-h, --help Print help
| Flag | Accepted values | Purpose |
|---|---|---|
--cli | name of a detected or custom CLI | Skip the interactive CLI picker; assign this CLI to every agent that doesn’t otherwise pin one. |
--branches | comma-separated branches | Skip the interactive branch picker; launch one worktree per branch. |
--from-all-specs | (flag) | Launch every discovered spec across the configured backend. Mutually exclusive with --specs. |
--specs | comma-separated spec names; bare flag opens a multi-select picker (TTY required) | Narrow the session to named specs or open the picker. Mutually exclusive with --from-all-specs. |
--specs-format | openspec, markdown, speckit, superpowers | Select the spec system for this launch (overrides [specs] type in config). git-paw does not auto-detect the spec system from the filesystem. |
--dry-run | (flag) | Print the session plan; create no worktrees and run no tmux commands. |
--preset | preset name from config | Use a named [presets.<name>] entry. |
--supervisor | (flag) | Force supervisor mode on. Mutually exclusive with --no-supervisor. |
--no-supervisor | (flag) | Force supervisor mode off (highest precedence in the resolution chain). Mutually exclusive with --supervisor. |
--force | (flag) | Bypass the uncommitted-spec validation warning when launching from specs. |
--no-rebase | (flag) | Skip the default-on rebase of existing agent branches onto the repository’s default branch. |
--unattended | (flag) | Run the supervisor wave to completion with no human in the seat: engages supervisor mode and drives an in-process loop that escalates every live permission prompt for a human decision without blocking the wave, detects completion, and exits with a summary. It approves nothing. On a repository that already has a live supervised session, binds a new drive loop to it instead of launching. Mutually exclusive with --no-supervisor. |
--from-all-specs and --specs are mutually exclusive — one launches every
discovered spec, the other narrows to a subset or opens the picker.
--unattended engages supervisor mode (equivalent to also passing
--supervisor) and then drives the wave to completion in-process instead of
returning with an attach hint: it polls the panes on a ~15-second cadence,
escalates every live permission prompt it finds — on a coding pane or the
supervisor’s own — for a human decision WITHOUT blocking the rest of the wave,
and exits with a summary on completion, a stuck signal, or a heartbeat after
~25 minutes without progress (no new commit on a coding branch and no agent
phase advance). It is designed for detached operation and needs no attached
terminal. It is mutually exclusive with --no-supervisor.
It approves nothing. git-paw sends no approval keystroke on its own
initiative; whether a command runs without a prompt is decided by the agent
CLI’s own permission system. For a run that truly does not stop, pre-approve
the commands in that CLI’s own allow rules, or use
[supervisor] agent_approval = "full-auto" inside the documented sandbox. See
Supervisor → Unattended mode.
On a repository that already has a live supervised session, --unattended
binds a new drive loop to that session instead of launching or refusing: an
active session is driven as it stands (no pane rebuilt, no worktree touched,
nothing injected), a paused one is revived first, a stopped one recovered
first. The terminal is never attached. Launch-selection flags are not applied
to a live session, so re-running the command that launched a wave is what
drives it again after its loop exits. The loop drives the session’s own roster
and reaches the broker at the endpoint recorded for that session, not the
configured one. A session launched without supervisor mode is refused — it
leaves its prompts for a human to submit. At most one drive loop may drive a
session at a time; a second --unattended refuses, naming the holding process,
before changing anything. See
Re-attaching a drive loop to a live session.
No orchestrator runs inside the session — it drives from your own agent
session, pointed at .git-paw/AGENTS.local.md — so the loop sweeps and types
into coding-agent panes only and records every judgment call (a live
prompt, a worker’s question, a merge decision, a non-converging branch) as a
broker review item, injecting nothing into any pane. The loop never waits on a
reply. Nudges aimed at a worker pane stop once that worker has finished
(done/verified) — but not while it is merely quiet at
blocked/committed, where the correction loop must still reach it. See
the loop is a pump, the supervisor is the brain.
The selection flags compose with --supervisor: git paw start --supervisor --specs a,b launches a supervisor session for only the named subset (a
and b), exactly matching the non-supervisor --specs behaviour. --supervisor --from-all-specs launches every discovered spec, and --supervisor --specs
(bare) opens the multi-select picker.
Examples:
git paw start
git paw start --cli claude
git paw start --cli claude --branches feat/auth,feat/api
git paw start --dry-run
git paw start --preset backend
# Launch every discovered spec
git paw start --from-all-specs
git paw start --from-all-specs --cli claude
git paw start --from-all-specs --dry-run
# Narrow to specific specs or open the multi-select picker
git paw start --specs add-auth,fix-session
git paw start --specs # interactive picker (requires a TTY)
# Supervisor mode honours the same selection flags
git paw start --supervisor --specs add-auth # one agent, for add-auth only
git paw start --supervisor --from-all-specs # one agent per discovered spec
# Skip supervisor for this session even when `[supervisor] enabled = true` is set
git paw start --no-supervisor
git paw start --from-all-specs --no-supervisor
# Run the whole wave to completion with no human in the seat (detached)
git paw start --unattended --from-all-specs
git paw start --unattended --branches feat/auth,feat/api
# After the loop exits (heartbeat, stuck, interrupted): the same command again
# binds a new loop to the still-live session
git paw start --unattended --from-all-specs
Every launch path refuses, before creating any worktree or pane, a session whose branches do not all derive distinct broker agent_ids. The id is the slugified branch name, so two different branches can map to one: git paw start --branches feat/x,feat-x fails naming both branches and the shared id feat-x, because the two agents would otherwise share an inbox and receive each other’s feedback and verdicts. Rename one branch so the ids differ. (Only when the broker is enabled; with it off, nothing addresses agents by id and the launch proceeds as before.)
Supervisor mode resolution chain
git-paw decides whether to enter supervisor mode using this order (first match wins):
--no-supervisorflag present → supervisor disabled (no prompt, regardless of config).--supervisorflag present → supervisor enabled (no prompt).[supervisor] enabled = truein config → supervisor enabled (no prompt).[supervisor] enabled = falsein config → supervisor disabled (no prompt).- No
[supervisor]section +--dry-run→ supervisor disabled (skip prompt). - No
[supervisor]section + interactive TTY → prompt “Start in supervisor mode?”. - No
[supervisor]section + non-TTY → supervisor disabled (fallback).
--supervisor and --no-supervisor are mutually exclusive at parse time; passing both is rejected by clap before any command runs.
See Spec-Driven Launch for details on spec formats and configuration.
git paw add
Hot-attaches a worktree and agent pane to a running session — bare, spec-driven or supervisor-mode — without a stop/purge/restart. The agent grid re-tiles to the layout a start of that many agents would produce, the new branch is registered in the session, and the agent boots with the same broker boot block + initial prompt a start-time agent receives. The orchestrator discovers it passively on its next broker poll, and an in-process drive loop picks it up on its next sweep — no restart.
Provide a branch name, or use --from-spec to derive the branch (and CLI) from a spec discovered in the configured spec system — OpenSpec, Markdown, Spec Kit, or Superpowers (same resolution as start --specs NAME). Adding past the 25-agent cap is rejected with the same “split into multiple sessions” message start uses. Adding to a paused session leaves the new pane paused until git paw start.
Usage: git-paw add [OPTIONS] [BRANCH]
Arguments:
[BRANCH] Branch to attach (omit when using --from-spec)
Options:
--cli <CLI> AI CLI for the new pane (defaults to the session's default CLI)
--from-spec <FROM_SPEC> Derive branch + CLI from a spec (OpenSpec change, Markdown spec, Spec Kit feature, or Superpowers plan)
-h, --help Print help
Examples:
git paw add feat/new-thing
git paw add feat/api --cli codex
git paw add --from-spec add-export
Validation happens before any side effect: an unknown --cli errors with the detected CLI ids, and an unknown --from-spec errors with the discovered spec candidates — no worktree or pane is created in either case.
A branch whose broker agent_id is already taken by an agent in the session is refused the same way: the id is the slugified branch name, so git paw add feat-x against a session already running feat/x fails naming both branches and the shared id feat-x rather than attaching a second agent to one inbox. (Only when the broker is enabled; with it off nothing addresses agents by id.)
git paw remove
Detaches a single agent from any active session: closes its tmux pane, re-tiles the grid for the smaller agent count, removes its worktree (reusing git paw purge’s per-worktree teardown), and drops it from the session. The other agents are untouched; the orchestrator notices the departure on its next poll (the agent stops heartbeating) and a running drive loop stops driving it on its next sweep.
Safe by default: remove refuses to delete a worktree with uncommitted changes — listing the changed files — unless --force is passed. --keep-worktree detaches the pane and session entry but leaves the worktree and branch on disk (and skips the uncommitted-work check, since nothing is deleted). git paw remove supervisor is refused — use git paw stop to end the whole session.
Usage: git-paw remove [OPTIONS] <BRANCH>
Arguments:
<BRANCH> Branch of the agent to remove
Options:
--keep-worktree Leave the worktree + branch on disk; only detach the pane and session entry
--force Remove even with uncommitted changes (bypass the safety check)
-h, --help Print help
Examples:
git paw remove feat/done-thing
git paw remove feat/wip --force
git paw remove feat/keep --keep-worktree
Both
addandremoveoperate on every session git-paw builds — every one uses the agent grid, which is the layout that can be re-tiled incrementally. One branch per invocation.A live session started by an earlier git-paw with the whole-window tiled layout is the one exception: it is refused before any worktree, pane or session entry is touched, with an error naming the tiled layout. Stop the session and run
git paw startto relaunch it in the grid layout, after whichaddandremovework normally.
git paw pause
Soft-stops the session: detaches the tmux client, stops the broker, and leaves every CLI pane running in the background. Preserves agent conversation state for instant resume via git paw start. RAM stays allocated (~300 MB per Claude pane).
Use pause for short breaks (lunch, meetings, end-of-day). For longer breaks, use git paw stop to kill the CLIs and release RAM. See Pause and Resume for the full trade-off discussion.
Usage: git-paw pause
Options:
-h, --help Print help
Example:
git paw pause
Idempotent: pausing an already-paused or already-stopped session is a friendly no-op.
git paw stop
Kills the tmux session and every CLI pane process, but preserves worktrees and session state on disk. CLI conversation context is lost. Run git paw start later to recover the session with fresh CLI processes.
stop never prompts for confirmation. Stopping is recoverable: worktrees and session state survive, and git paw start recovers the session with fresh processes. Therefore, a confirmation prompt would protect nothing and is not rendered.
Usage: git-paw stop
Options:
-h, --help Print help
Example:
git paw stop
git paw purge
Nuclear option: kills the tmux session, removes all worktrees (including any uncommitted changes in them), and deletes session state. When the broker was enabled, also removes that session’s <session_name>.broker.log. Requires confirmation unless --force is used.
Remove all worktrees and session state; keeps branches with unmerged commits
Usage: git-paw purge [OPTIONS]
Options:
--force Skip the confirmation prompt; never deletes a branch with unmerged commits
--stale Purge only stale sessions (receipt claims active but tmux is gone); live sessions untouched
--all-repos With --stale, sweep every repository's stale receipts instead of just the current one
-h, --help Print help (see more with '--help')
Which branches purge deletes
Purge deletes a branch only when git-paw created it (so a branch that existed before the session is never touched) and its tip is an ancestor of the default branch — or of the branch it was created from, see below. Each deletion prints the SHA the branch was at:
Deleted branch feat/done-thing (was 1a2b3c4)
A branch carrying commits that are in neither — or one whose merge state cannot
be determined, for instance because the default branch cannot be resolved — is
kept, with or without --force. Only its worktree (including any
uncommitted changes) and the session state go away. Each retained branch is
reported with its tip and the two ways to finish the job by hand:
Kept branch feat/wip at 9f8e7d6 (3 commit(s) not in main)
resume it with: git worktree add '/path/to/worktree' feat/wip
discard it with: git branch -D feat/wip
The discard command is git branch -d when git would accept the safe delete
(the branch is already contained in whatever you have checked out) and
git branch -D otherwise, so the suggestion always runs.
Every comparison names refs explicitly (refs/heads/<branch>). A tag that
happens to share a branch’s name therefore cannot answer the merge question on
the tag’s history, which would otherwise let a merged tag clear the way for
deleting an unmerged branch.
Branches created from an integration branch
git worktree add -b takes no start point, so a branch git-paw creates starts
at whatever the main checkout has checked out. Launch a wave from an
integration branch and every agent branch inherits that branch’s commits.
git-paw records the start point on the session entry, and teardown asks whether
the branch is contained in the default branch or in that recorded base —
so work merged into the integration branch is recognised as finished, and a
branch that really is unmerged is counted against the base rather than being
reported with the whole integration backlog folded into its number.
Sessions written before this was recorded carry no base and are compared against the default branch alone, exactly as they were before.
Before the confirmation prompt, purge lists every branch it is going to keep,
so you see the outcome before agreeing to it. --force skips the prompt, not
the retention rule.
--stale purges only sessions whose tmux session no longer exists (a stale
receipt). By default the sweep is scoped to the current repository — it
never touches a stale receipt belonging to a different repository. Pass
--all-repos to broaden the sweep to every repository’s stale receipts on
the machine (meaningful only together with --stale; used alone it is
rejected with an error). When nothing is stale in scope it exits 0 with a
“No stale sessions to purge.” message. Pairing --stale with --force is a
no-op (--force is redundant since a stale entry is never prompted for).
In every case the live guarantee is fail-safe: a session that is live per the
liveness probe — or whose liveness cannot be positively confirmed (e.g. the
tmux binary is unavailable) — is never purged by --stale, in the current
repository or any other.
Examples:
git paw purge
git paw purge --force
git paw purge --stale # current repository only
git paw purge --stale --all-repos # every repository on the machine
git paw status
Displays the current session status, branches, CLIs, and worktree paths for the repository in the current directory. When the session was launched with the broker, also shows the broker URL and whether it is running (with its agent count and uptime), paused, not responding, or held by another repository’s git-paw broker (see Multi-repo port assignment). The broker is reported running only when this repository’s broker answers and the session’s broker host is alive; a process still answering on the port after git paw stop is reported as a leftover broker with no live session host, never as running.
Usage: git-paw status [OPTIONS]
Options:
--json Emit machine-readable JSON
-h, --help Print help
The status line shows one of:
| Display | Meaning |
|---|---|
| 🟢 active | Receipt says active and the tmux session is alive |
| 🔵 paused | Receipt says paused and the tmux session is alive |
| 🟡 stopped | Receipt says stopped, a paused session whose tmux server died, or a session whose tmux session is up but whose agent panes have all exited |
| 🔴 stale | Receipt claims active but the tmux session is gone (a crash or a release-boundary carry-over) |
When every agent pane has exited — the CLIs died and the panes fell back to a
bare shell — the session is 🟡 stopped, not stale: the tmux session itself is
still running. Status says so with a hint under the status line
(agent panes exited — run 'git paw start' to recover them), reports
Tmux: running, and sets "tmux_running": true in JSON. git paw start
recovers such a session; git paw purge --stale deliberately leaves it alone.
🔴 stale (and the JSON "stale" value) means only that the tmux session is
gone. When the status is 🔴 stale, run git paw start to self-heal (the stale
receipt is invalidated automatically before launching) or git paw purge --stale
to clear it. The --json output is an object with session, status,
tmux_running, and worktrees fields, plus wave when the session’s own
broker is reachable (just { "session": null } when the repository has no
session); its status field carries the matching lowercase value (active,
paused, stopped, or stale).
When the session’s broker reports the wave as stalled, status prints one
extra line under the Broker: line naming how long nothing has progressed:
↳ wave stalled — no progress for 3h12m
Nothing extra is printed when the wave is not stalled. “Progress” is a wave
phase advancing or a branch tip moving, and nothing else — see
Wave staleness for the full rule
and [broker] wave_stall_minutes for the threshold.
--json is not conditional: whenever a broker is reachable, its wave object
(last_progress_unix_secs, idle_seconds, stall_threshold_seconds,
stalled) is always included, stalled or not.
Each entry in --json’s worktrees array carries:
| Field | Type | Meaning |
|---|---|---|
branch | string | The agent’s branch name. |
cli | string | The CLI id assigned to this worktree. |
worktree_path | string | Absolute path to the worktree. |
cli_session_id | string or null | The session identifier git-paw assigned this worker’s CLI at launch, or null when the CLI has no assignment template. Resume it by hand with the CLI’s own resume flag (e.g. claude --resume <id>), or let git paw start do it automatically on recovery. |
The human-readable table (no --json) never shows cli_session_id — it is
operator/tooling data surfaced only in JSON.
Example:
git paw status
git paw status --json
git paw attach
Reattaches the current terminal to the tmux session running for the repository in the current directory, resolving the session the same way git paw status does (by repository path). Equivalent to tmux attach -t paw-<project>, without needing to know or type the exact session name.
Usage: git-paw attach
Options:
-h, --help Print help
Exits with an actionable error when no session is running for this repo — run git paw start to launch one.
Example:
git paw attach
Note the distinction from session revival: attach reattaches to an already-running session; git paw start is what recovers a stopped/crashed session or launches a new one.
git paw list-clis
Shows all AI CLIs found on PATH (auto-detected) and any custom CLIs registered in your config.
Usage: git-paw list-clis
Options:
-h, --help Print help
Example:
git paw list-clis
git paw add-cli
Adds a custom CLI to your global config (~/.config/git-paw/config.toml on Linux, ~/Library/Application Support/git-paw/config.toml on macOS — see Config File Locations). The command can be an absolute path or a binary name on PATH; a binary name is resolved to its absolute path when it is registered.
Usage: git-paw add-cli [OPTIONS] <NAME> <COMMAND>
Arguments:
<NAME> Name to register the CLI as
<COMMAND> Command or path to the CLI binary
Options:
--display-name <DISPLAY_NAME> Display name shown in prompts
-h, --help Print help
Examples:
git paw add-cli my-agent /usr/local/bin/my-agent
git paw add-cli my-agent my-agent --display-name "My Agent"
git paw remove-cli
Removes a custom CLI from your global config. Only custom CLIs can be removed — auto-detected CLIs cannot.
Usage: git-paw remove-cli <NAME>
Arguments:
<NAME> Name of the custom CLI to remove
Options:
-h, --help Print help
Example:
git paw remove-cli my-agent
git paw setup-cli
Writes git-paw’s setup items into an AI CLI’s own settings, so the CLI stops prompting for git-paw’s helper-script calls. Running this command is the consent — nothing else in git-paw ever writes those settings. See Per-CLI Setup for the grant table, the protection rules and the residual risks.
Usage: git-paw setup-cli [OPTIONS] <--allowlist|--trust|--hooks|--undo> <CLI>
Arguments:
<CLI> CLI identity to set up (a detected CLI or a [clis.<name>] key)
Options:
--allowlist Pre-approve git-paw's own helper-script calls, with the rules
that stop an agent rewriting what they run
--trust Pre-accept the repository as a trusted folder (not supported
by any CLI yet)
--hooks Install git-paw's lifecycle hooks (not supported by any CLI yet)
--dry-run Print the plan and exit without writing anything, even with --yes
--yes Apply the printed plan without asking for confirmation
--undo Remove the entries git-paw recorded for this CLI, limited to the
named items when item flags are given
-h, --help Print help
At least one of --allowlist, --trust, --hooks or --undo is required.
Run it from the repository’s main checkout; it refuses to run from a linked
worktree.
Exit codes:
| Code | Meaning |
|---|---|
0 | The plan was applied, there was nothing to do, or --dry-run printed it. |
1 | Refused: an unknown CLI, a linked worktree, an unsafe target (tracked, symlinked, malformed JSON, a wrong-typed key), or a target that changed between planning and writing. |
2 | Nothing attempted: no item flag, a requested item that is not supported, an item whose facts are unverified, a declined confirmation, or no terminal and no --yes. |
Examples:
# Print the plan and write nothing
git paw setup-cli claude --allowlist --dry-run
# Apply it, confirming on a terminal
git paw setup-cli claude --allowlist
# Apply it without a prompt
git paw setup-cli claude --allowlist --yes
# Remove exactly what git-paw added
git paw setup-cli claude --undo --yes
git paw replay
Replay captured session logs. Requires logging to be enabled.
Usage: git-paw replay [OPTIONS] [BRANCH]
Arguments:
[BRANCH] Branch to replay
Options:
--list List available log sessions and branches
--color Display with colors via less -R
--session <SESSION> Session to replay from (defaults to most recent)
-h, --help Print help
The branch argument matches either the original branch name or its log
filename (/ becomes --), so feat/add-auth and feat--add-auth select
the same log. An unmatched name errors with the list of available branches.
Examples:
# List all logged sessions and branches
git paw replay --list
# Replay a branch (stripped of ANSI codes)
git paw replay feat/add-auth
# Replay with colors
git paw replay feat/add-auth --color
# Replay from a specific session
git paw replay feat/add-auth --session paw-my-project
git paw approvals
Report the permission prompts the --unattended drive loop escalated for a
human decision during a session, sorted by how often each recurred. Attended
sessions run no drive loop and record nothing, though this command still reads
any log an earlier unattended run left behind. See
Manual approvals for the full
workflow.
Usage: git-paw approvals [OPTIONS]
Options:
--session <SESSION> Session to read from (defaults to the active session)
--limit <LIMIT> Show at most N patterns (top N by count)
--json Emit machine-readable JSON
-h, --help Print help
Each row carries a SUGGEST hint naming which of the agent CLI’s own
allowlists could absorb the pattern — project allowlist for project-specific
patterns (a ./-rooted script, or the project/branch name), user allowlist
otherwise. The hint is a starting point; the command never edits any settings
file for you.
Examples:
# Active session, text table
git paw approvals
# Machine-readable JSON
git paw approvals --json
# A specific session
git paw approvals --session paw-my-project
# Top 5 patterns by count
git paw approvals --limit 5
The --json output is a { "session", "approvals": [...] } object where each
entry carries pattern, count, suggested_target, first_seen, and
last_seen. An empty or missing log yields { "session": "...", "approvals": [] }.
git paw mcp
Starts a Model Context Protocol
(MCP) server on stdin/stdout so any MCP-aware client (Claude Desktop, Cursor,
ChatGPT Desktop, Windsurf, VS Code MCP) can query this repository’s read-only
state: agent coordination intents/conflicts, governance docs, specs and tasks,
session status and learnings, agent skills, git context, source browsing
(list_files, read_file, search_code over the local working tree), and the
repository’s own README and documentation.
The write surface is bounded: four publish tools for an agent’s boot events,
publishing as the calling session only; and, for a server started in the
repository’s main working tree only, the session lifecycle tools
(start_session, pause_session, stop_session, purge_session) and the
verdict tools (publish_verified, publish_feedback). A server in an agent’s
worktree neither lists nor runs those. See the
MCP Server guide.
The server is client-spawned and one-shot: the MCP client owns the process lifecycle and the server exits when stdin is closed. It runs standalone — no tmux session, broker, or supervisor is required. When a data source is unavailable (no broker, no session, no governance config) tools return well-formed empty/null results rather than errors.
Usage: git-paw mcp [OPTIONS]
Options:
--repo <PATH> Repository to serve (overrides current-directory discovery; required for Claude Desktop)
--log-file <PATH> Also write tracing output to this file (stderr is always used)
-h, --help Print help
Repository resolution: --repo wins; otherwise the nearest ancestor of the
current directory containing .git is used (worktrees resolve to their own
root). Clients that spawn servers from a fixed directory — notably Claude
Desktop, which launches from its app-support directory — must pass --repo
with an absolute path.
--log-file tees tracing diagnostics to a file in addition to stderr; stdout
always stays reserved for the JSON-RPC stream.
Claude Desktop config (claude_desktop_config.json):
{
"mcpServers": {
"git-paw": {
"command": "git",
"args": ["paw", "mcp", "--repo", "/absolute/path/to/your/repo"]
}
}
}
Examples:
git paw mcp
git paw mcp --repo /path/to/repo
git paw mcp --repo /path/to/repo --log-file /tmp/git-paw-mcp.log
git paw doctor
Runs read-only preflight checks over the environment, configuration, and repository state and prints a grouped report answering “why won’t it launch?”.
Usage: git-paw doctor [OPTIONS]
Options:
--json Emit machine-readable JSON
--live Also run the live session-lifecycle smoke check (slower; needs tmux)
-h, --help Print help
Checks are grouped under Environment, CLIs, Config, Spec system,
Bundled scripts, Broker, Supervisor, and Hygiene. Each reports
✓ (pass), ⚠ (warning), or ✗ (failure), and every non-✓ check prints an
actionable remedy on its own ↳ line.
Doctor diagnoses but never repairs — no file, config, session, or other
persistent state is created, modified, or deleted. There is deliberately no
--fix flag.
Exit behaviour:
- Exits non-zero when any check is
✗. - Exits
0when the worst status is⚠or better — a warning alone never blocks, so doctor works as a pre-launch or CI gate that fails only on true blockers. --jsonshares the identical exit-code contract and suppresses the human rendering; stdout is a single JSON document with a top-levelstatusand achecksarray whose entries carrygroup,name,status,detail, andremedy.
--live appends a Live smoke group carrying the verdict of the full
session lifecycle (start → add → remove → stop) run against an isolated
throwaway repository and a dummy CLI — the same harness git paw selftest
drives. It is the only arm that writes anything, and only inside its own
.git-paw/tmp/ sandbox, which it removes afterwards.
Examples:
git paw doctor
git paw doctor --json
git paw doctor --live
See the Doctor chapter for the full check catalogue.
git paw completions
Generates a shell-completion script from the clap command definition and prints it to stdout, for installation through the shell’s standard completion mechanism.
Usage: git-paw completions <SHELL>
Arguments:
<SHELL> Shell to generate completions for [possible values: bash, elvish, fish, powershell, zsh]
Options:
-h, --help Print help
An unsupported shell argument is rejected by clap with an actionable error naming the supported values.
Examples:
git paw completions bash > /etc/bash_completion.d/git-paw
git paw completions zsh > "${fpath[1]}/_git-paw"
git paw completions fish > ~/.config/fish/completions/git-paw.fish
See Installation for shell-specific setup.
git paw selftest
Internal diagnostic (hidden from the command list).
selftestis the live arm ofgit paw doctor— reachable directly for dogfooding and CI, or throughgit paw doctor --live, which folds its verdict into the grouped report.
Runs an isolated, end-to-end session-lifecycle smoke check — the shipped form of the dogfood isolation recipe — so the orchestration plumbing can be verified anywhere, including CI, without a real LLM backend or an interactive terminal.
Usage: git-paw selftest
Options:
-h, --help Print help
The harness drives start → add → remove → stop against a throwaway
repository and a dummy CLI (cat), observing that the agent roster grows on
add and shrinks on remove. It takes no arguments.
Every external resource is isolated so the run never disturbs live work:
- a private tmux socket (a per-run
TMUX_TMPDIR, withTMUX/TMUX_PANEstripped from every child) — your default tmux socket is untouched; - an OS-assigned ephemeral broker port (
bind 127.0.0.1:0), so concurrent runs never collide on the broker port; - an isolated
HOME, so your real sessions directory is never written; - a throwaway git repository under
.git-paw/tmp/, swept clean before the run and removed on both the success and failure paths.
Exit behaviour:
- Exits
0and printsselftest passedwhen the full lifecycle completes. - Skips with
selftest skipped: tmux not availableand exits0when tmux is not onPATH(CI runners have tmux, so a skip never silently passes there). - Exits non-zero and names the failing step (e.g.
selftest failed at step 'add': …) only on an actual lifecycle failure.
Example:
git paw selftest
See the Selftest chapter for the full isolation recipe and how the harness closes the deferred live-verification of add/remove roster transitions.
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Error (git, tmux, config, or other failure) |
| 2 | User cancelled (Ctrl+C or empty selection) |
Internal subcommands
git-paw also has hidden subcommands that it runs itself; they are not listed in
git paw --help and are not meant to be invoked by hand:
__broker --session <name> --log-path <path>— hosts a session’s broker in its detachedbrokertmux window.start, recovery and resume launch it;pausecloses its window andstopends it with the session.__dashboard— renders the dashboard in pane 0 as a thin client of that broker.__prompt-profile— prints one resolved prompt-shape profile field for the bundledsweep.shhelper.
Architecture
This chapter covers git-paw’s internal architecture: module structure, data flow, and key design decisions.
It describes the code as it is today. For the structure the code is moving toward — and the rules new code follows — see Target Architecture.
Module Diagram
┌───────────────────────────────────────────────────────────────────────┐
│ main.rs (entry layer: parse, resolve, prompt, attach) │
│ commands/ (library use cases: context + options → outcome) │
├──────────┬──────────────┬──────────────┬─────────────┬────────────────┤
│ cli.rs │ interactive/ │ config/ │ error.rs │ dirs.rs │
│ (clap) │ (prompts, │ (TOML) │ (PawError) │ (XDG) │
│ │ pickers) │ │ │ │
├──────────┴──────────────┴──────────────┴─────────────┴────────────────┤
│ │
│ detect.rs git.rs session.rs logging.rs agents.rs │
│ (PATH scan) (worktrees) (JSON state) (pane logs) (AGENTS.md) │
│ │
│ surface.rs │
│ (SessionLayout + SurfaceRole: the reserved-index record) │
│ │
│ skills.rs init.rs replay.rs selftest.rs doctor.rs │
│ (skill (project (log (smoke (preflight │
│ templates) bootstrap) playback) check) diagnostics) │
│ │
│ domain.rs lock.rs random.rs command_runner.rs │
│ (newtypes) (add/remove (UUID v4) (process-execution seam) │
│ lock) │
│ │
│ static_regex.rs │
│ (one audited compiler for build-time regex patterns) │
│ │
│ worktree_provision.rs dashboard.rs + dashboard/ │
│ (env copies, ports, hooks) (TUI: model, view, log panel) │
├───────────────────────────────────────────────────────────────────────┤
│ broker/ supervisor/ specs/ │
│ ├── mod.rs ├── mod.rs ├── mod.rs │
│ ├── server.rs ├── approval_gate.rs ├── openspec.rs │
│ ├── messages.rs ├── branch_refresh.rs ├── markdown.rs │
│ ├── delivery.rs ├── claim.rs ├── speckit.rs │
│ ├── conflict.rs ├── drive.rs ├── superpowers.rs │
│ ├── learnings.rs ├── keys.rs └── resolve.rs │
│ ├── watcher.rs ├── loop_binding.rs │
│ └── publish.rs ├── manual_approvals.rs opsx/ │
│ └── permission_prompt.rs ├── mod.rs │
│ └── role_guard.rs │
│ │
│ coordination/ │
│ ├── mod.rs │
│ ├── inventory.rs │
│ └── tell.rs │
├───────────────────────────────────────────────────────────────────────┤
│ terminal/ tmux/ mcp/ │
│ ├── mod.rs (port) ├── mod.rs ├── mod.rs │
│ ├── tmux.rs (adapter) ├── command.rs ├── server.rs │
│ ├── fake.rs ├── layout.rs ├── logging.rs │
│ └── fixtures/ ├── readiness.rs ├── query/ │
│ (argv goldens) └── session.rs └── tools/ │
└───────────────────────────────────────────────────────────────────────┘
Module Responsibilities
| Module | File | Purpose |
|---|---|---|
| Entry point | src/main.rs, src/lib.rs | main.rs is the entry layer only: it parses arguments, resolves the inputs (current directory, config, and the interactive prompts — the “Start in supervisor mode?” question, the purge confirmation, the setup-cli confirmation, and the branch/spec pickers handed to the commands as an injected interactive::Prompter), dispatches into git_paw::commands, renders the outcome, and performs the terminal steps a command’s LaunchOutcome asks for: attach (or print the detached hint), reattach-or-refuse an already-running session, or take the drive-loop binding and run the unattended loop. The only other functions it holds are allowed by name: cmd_prompt_profile (hidden __prompt-profile), cmd_setup_cli / confirm_setup (plan print, terminal check, confirmation) and cmd_dashboard (the thin __dashboard client). lib.rs is the library target the binary, the MCP server and the integration tests share — internal, not a stable API. |
| Commands | src/commands/ | The command use cases, in the library as git_paw::commands (internal API, not covered by semver). Each family takes a CommandContext (the repository root and its config — handed in loaded, or loaded on first use so error ordering is unchanged), an options struct and an injected Output sink, and returns a typed outcome; it never reads the current directory or stdin, prompts, prints, attaches, or runs the drive loop. output.rs (the Output port — line / warn — and CaptureOutput, which collects lines for in-process callers; the terminal adapter that writes stdout/stderr lives in the entry layer, main.rs), start.rs (fresh launch, restart-from-pause, spec-driven launch), supervisor.rs (supervisor, plus the drive-loop binding and run wiring the entry layer drives), recover.rs, add.rs, remove.rs, pause.rs, stop.rs, status.rs, attach.rs, approvals.rs, replay.rs, clis.rs (list-clis / add-cli / remove-cli), setup_cli.rs (setup-cli: prepare resolves the CLI identity and plans an apply or an undo, writing nothing; only apply_setup / apply_undo write, and main.rs calls them after printing the plan and getting the confirmation or --yes), purge.rs (purge, stale-receipt purge and invalidation, and the shared per-worktree teardown detach_worktree with its branch-retirement rules), launch.rs (attach_agent / AttachContext, prompt composition, LaunchOutcome, the per-repo discovery file), entry_points.rs (submitting a spec format’s workflow entry point once a pane is idle), spec_mode.rs (SpecMode and apply_spec_mode), plus shared helpers.rs (including start_broker_host, which spawns the host’s service surface and waits for /status). Two modules are entry-layer or host code and exempt by name: completions.rs (writes the generated completion script to stdout) and broker_host.rs (the hidden __broker host: builds the broker state, starts the broker, and blocks until SIGHUP / orphaned / terminal hang-up). |
| CLI | src/cli.rs | Argument parsing with clap v4 derive macros. Defines all subcommands, flags (--from-all-specs, --specs, --specs-format, --supervisor, --no-supervisor, --force, …), and help text. |
| Detection | src/detect.rs | Scans PATH for known AI CLI binaries (KNOWN_CLIS). Resolves custom CLIs from config. Merges and deduplicates. |
| Git | src/git.rs | Validates git repos, lists branches (local + remote, deduplicated), creates/removes worktrees, derives safe directory names. |
| Terminal | src/terminal/ | The terminal port. mod.rs defines TerminalBackend — every orchestration-path terminal operation (open/close a session from a SessionPlan, spawn/kill/label a surface, list surfaces with their owning worktree, send text and named keys as separate operations, read a screen tail and a surface’s foreground process, relaunch a surface’s CLI or answer its first-run dialog, stream output to a log, grid layout, attach/detach, and run / stop / query a detached service outside the agent grid — the broker host) in git-paw’s terms, keyed by an opaque SurfaceId. tmux.rs is the tmux adapter (TmuxBackend) over the src/tmux/ builders and the CommandRunner seam; it alone converts a layout position to a tmux target, implements a service as a detached named window (new-window -d -n <name>), and fixtures/ pins its argv to the pre-port behaviour. fake.rs (FakeBackend) records operations and answers scripted screens, owners and processes for logic tests. |
| Tmux | src/tmux/ | Tmux orchestration. command.rs holds the TmuxSessionBuilder plus the supervisor-session, add-agent, and remove-retile command builders (pure argv assembly, pane titles, the agent grid every session uses); layout.rs the equal-width agent-row rebalance; readiness.rs the launch-readiness gate; session.rs runtime operations on live sessions (liveness probes, session-name resolution, attach/detach, pane and session teardown, pane-to-worktree reconciliation). |
| Session | src/session.rs | Persists session state to JSON files under ~/.local/share/git-paw/sessions/. Atomic writes, crash recovery. Also writes the per-repo discovery file (see below). |
| Surface | src/surface.rs | The session’s pane-layout record and the role each surface plays. SessionLayout — derived from a saved session or from a launch plan, never persisted — owns every reserved-index fact: which index holds the dashboard, where coding agents start, and what each launch records. No surface runs the supervisor: the orchestrator drives the session from outside it. SurfaceRole (Dashboard / Coding(agent id) / Unknown) is produced by the single resolver SessionLayout::resolve, which matches a surface’s owning worktree path through the core owner rule resolve_agent (exact canonical path) before consulting the dashboard’s reserved index; a pre-relocation session’s old supervisor pane resolves as Unknown. Decision code matches on the role instead of comparing a pane index to a literal or an agent id to "supervisor"; the supervisor’s broker wire identity is defined here once. The supervisor-window grid geometry (SupervisorLayout, supervisor_layout, layout_for, SUPERVISOR_PANE_OFFSET) lives here too so src/tmux/ depends on core rather than on src/supervisor/. |
| Config | src/config/ | Parses TOML from global (~/.config/git-paw/config.toml) and per-repo (.git-paw/config.toml). Merges with repo-wins semantics. mod.rs holds PawConfig and loading; one file per section: broker.rs, cli.rs (custom CLIs and presets), dashboard.rs, layout.rs, prompt_profile.rs (per-CLI prompt-shape profiles), specs.rs, supervisor.rs. |
| Interactive | src/interactive/ | Terminal prompts, split by altitude behind the Prompter trait seam: resolver.rs (terminal-free selection logic, PickerState, spec grouping), prompter.rs (the dialoguer TerminalPrompter), picker.rs (the ratatui/crossterm fuzzy multi-select and its terminal lifecycle). The branch and spec multi-selects are fuzzy-filter pickers (type to filter, space to toggle); the mode and CLI single-selects use dialoguer. Skips prompts when flags are provided. |
| Error | src/error.rs | PawError enum with thiserror. Actionable error messages and distinct exit codes. |
| Dirs | src/dirs.rs | In-tree platform XDG path helper. Replaces the upstream dirs crate (removed in v0.5.0 for license reasons); see AGENTS.md § Dependencies. |
| Agents | src/agents.rs | Generates the gitignored .git-paw/AGENTS.local.md sidecar (the combined view) per worktree; manages the <!-- git-paw:start … end --> marker region; leaves the tracked AGENTS.md committable. |
| Skills | src/skills.rs | Resolves agent skills following the agentskills.io specification — .agents/skills/<name>/SKILL.md first, then a user override in the config directory, then the default embedded in the binary — and renders their placeholders. Injects coordination + supervisor instructions into the worktree sidecar. |
| Init | src/init.rs | git paw init bootstrap. Creates .git-paw/ (with logs/, tmp/, scripts/), installs the bundled helper scripts into .git-paw/scripts/, writes the default config, and adds gitignore entries. Prompts for the spec system and records [specs] (no filesystem auto-detection). |
| Replay | src/replay.rs | git paw replay. Reads pane logs from .git-paw/logs/ and either strips ANSI or pipes through less -R. |
| Per-CLI setup | src/cli_setup/ | git paw setup-cli, the only thing in git-paw that writes an agent CLI’s own settings. facts.rs is the compiled per-CLI fact table and the fixed grant table; plan.rs and undo.rs are the pure planners over file snapshots; render.rs prints the plan; record.rs is the setup record’s shape; shell.rs is the imperative shell (git queries through CommandRunner, snapshot collection, atomic writes); doctor.rs holds the two read-only doctor checks. |
| Doctor | src/doctor.rs | git paw doctor. Read-only preflight diagnostics: a probe layer gathers environment/CLI/config/spec/script/broker/supervisor/hygiene facts, and pure check functions map them to grouped ✓/⚠/✗ results with remedies. Renders human or --json; exits non-zero only when a check fails (warnings exit 0). --live folds in the selftest verdict. |
| Selftest | src/selftest.rs | git paw selftest. Isolated end-to-end lifecycle smoke check (start → add → remove → stop) against a throwaway repo and a dummy CLI (cat) — private tmux socket, ephemeral broker port, isolated HOME, no LLM backend. The shipped form of the dogfood isolation recipe. |
| Logging | src/logging.rs | Per-pane log capture via tmux pipe-pane. Files at .git-paw/logs/<session>/<branch>.log. |
| Worktree provisioning | src/worktree_provision.rs | Per-worktree runtime isolation driven by [worktree] config: copies declared env files into a new worktree, allocates its port block (base + slot * stride) into a managed .env.local block, and runs the consumer’s on_create / on_remove hooks. |
| Process seam | src/command_runner.rs | The CommandRunner trait every git / tmux invocation goes through: production wires RealCommandRunner, tests inject a fake that records argv and returns scripted output. |
| Domain | src/domain.rs | Newtypes for injection-prone constructed strings — SessionName, BranchSlug, WorktreePath — plus shell_quote for paths interpolated into shell commands. |
| Lock | src/lock.rs | Advisory <repo>/.git-paw/.add-remove.lock taken by git paw add and git paw remove so two concurrent pane splices cannot interleave. |
| Random | src/random.rs | Version-4 UUIDs built from OS randomness (no uuid dependency), used for CLI session ids. |
| Static regex | src/static_regex.rs | The single audited compiler for regex patterns fixed at build time. static_regex(pattern: &'static str) carries the only #[allow(clippy::expect_used)] in non-test code; every pattern routed through it is compiled by a unit test, so an invalid static pattern fails cargo test instead of panicking at run time. Patterns that arrive at run time are compiled by their caller with the error propagated. |
| Dashboard | src/dashboard.rs, src/dashboard/ | The git paw __dashboard TUI (status table + Broker log panel), a thin HTTP client of the broker. Detail below. |
| Broker | src/broker/ | HTTP coordination server (axum) with watcher + conflict detector + learnings subsystems. Detail below. |
| Supervisor | src/supervisor/ | Supervisor-mode subsystems (unattended drive loop and its one-loop-per-session binding, permission-prompt detection, the live-prompt gate and per-pane claim, keystroke dispatch, manual-decision log, branch refresh). Detail below. |
| Specs | src/specs/ | Spec scanning. Four backends (openspec, markdown, speckit, superpowers); mod.rs holds the dispatch entry point and resolve.rs resolves --specs names. |
| Task prompts | src/task_prompts.rs | The bundled per-backend task-prompt templates and the one exhaustive SpecBackendKind selector over them. Each format names a prose template and, optionally, a workflow entry point template — the single command line a launch submits as its own composer message once the agent’s pane goes idle. The launch’s build_task_prompt and the doctor’s spec-workflow probe both read this selector, so no workflow command is written in Rust and the two cannot drift. |
| opsx | src/opsx/ | OpenSpec role-gating guard (role_guard.rs): run by the broker on every committed agent.artifact, it flags archive activity by a non-supervisor agent per [opsx] role_gating. Inert unless the spec engine is OpenSpec. |
| MCP | src/mcp/ | git paw mcp. Model Context Protocol server over stdio. Read-only tools expose coordination, governance, docs, specs, session, learnings, skills, git, and source-browsing state to MCP-aware clients; the bounded write surface is four session-scoped publish tools (a worktree publishes as its own agent, the main working tree as supervisor) plus, for a root session only, the session lifecycle tools and the two verdict tools, and the spec-authoring write tools for a root session of a repository that set [mcp] allow_repo_writes. Runs standalone (no session/broker/supervisor required; broker-backed reads degrade to empty results). Detail below. |
| Coordination | src/coordination/ | User→agent coordination helpers (inventory + target validation) backing the /agents and /tell supervisor commands. Distinct from src/broker/ peer-to-peer (agent↔agent) coordination. Detail below. |
src/broker/ modules
| File | Purpose |
|---|---|
src/broker/mod.rs | Public surface (start/stop entry points, shared state types). |
src/broker/server.rs | axum HTTP server: /publish, /watch, /messages/:agent_id, /status, /log. Validates each incoming /publish body (message schema, agent_id pattern, unfilled-placeholder rejection) before handing it to delivery. |
src/broker/messages.rs | BrokerMessage enum + payload types + slug validation. Source of truth for the wire format used in user-facing examples. |
src/broker/client.rs | BrokerClient: the one HTTP client every process other than the broker host uses — bounded-timeout GET /status, GET /log?since=&wait=, POST /publish, POST /watch (with an optional spec id), and the repository-scoped stale-broker probe. The dashboard, the launcher, add, status, the drive loop and the MCP server all go through it. |
src/broker/publish.rs | Client-side helpers for callers outside the broker process (thin wrappers over BrokerClient): build BrokerMessage values and publish them over HTTP, register a live /watch target, fetch /log (by cursor, optionally long-polling), and fetch the wave-progress object from /status. Shared by commands::supervisor::supervisor, git paw status, and the MCP query and publish tools. |
src/broker/delivery.rs | Delivery layer: assigns each message its sequence number, updates the sender’s record, appends it to the message log, and routes it into inboxes (broadcast, supervisor inbox, targeted delivery). Also cursor-based polling, the dashboard snapshot, and the broker-log flush loop. |
src/broker/watcher.rs | Git-status polling watcher: one task per worktree runs git status --porcelain every 2 s and publishes agent.status (with modified_files) when the reported path set changes; reads git rev-parse HEAD on the same tick and marks wave progress when the branch tip moves (the first observation is a baseline); prunes a worktree that has vanished. |
src/broker/conflict.rs | Forward / in-flight / ownership conflict detection. Auto-emits [conflict-detector]-tagged agent.feedback and escalates via agent.question. |
src/broker/learnings.rs | Opt-in learnings subsystem. Aggregates the deterministic signals (stuck durations, recovery cycles, conflict events, permission patterns) plus published qualitative agent.learning records, and flushes them to .git-paw/session-learnings.md. |
src/dashboard.rs (top-level, not inside src/broker/) renders the dashboard
pane — the live status table and the optional message-log panel — by polling
the broker over HTTP (GET /status, GET /log?since=<seq>). It holds no
broker state. The dashboard pane sits at pane index 0 whenever the
session has one — in supervisor mode (see the layout diagram below) and in
non-supervisor broker mode alike.
The dashboard is structured as Model-View-Update:
| File | Purpose |
|---|---|
src/dashboard.rs | The Model (Model — the redraw state: derived agent rows, footer status line, Broker log buffer, quit flag) and the Update (Msg + a pure update(&mut Model, Msg)), plus what MVU cannot own: terminal setup/teardown, the poll_tty FFI, the getppid() == 1 orphan-exit gate, the broker poll, and the event loop. A failed poll sets the Model’s broker-unreachable flag (rendered in the status line, last snapshot kept); a lower /status uptime_seconds than last seen is a restarted host, which rewinds the log cursor. |
src/dashboard/view.rs | The View — a pure function of the Model producing the rendered frame (render, render_dashboard, draw_frame, and the status_symbol / format_age / format_agent_rows / arrange_with_supervisor_pinned / format_status_line helpers). No I/O, so frames are asserted against a TestBackend without a live pty. |
src/dashboard/broker_log.rs | The Broker log panel: ring buffer, filter chips, selection/overlay state, key handling, and its own rendering. |
The event loop drains crossterm key presses, the per-tick status snapshot, and
new broker-log entries into Msg values, routes each through update, and
renders the resulting Model via the View — rather than mutating local state
inline. The View re-exports keep crate::dashboard::{render_dashboard, format_agent_rows, …} resolving from the parent module.
src/supervisor/ modules
| File | Purpose |
|---|---|
src/supervisor/mod.rs | Module root declaring the submodules below. The supervisor launch itself (commands::supervisor::supervisor) lives in src/commands/supervisor.rs. |
src/supervisor/approval_gate.rs | The shared live-prompt gate (live_prompt_in_tail) and the escalation dedup key. |
src/supervisor/branch_refresh.rs | Pure precondition check for the opt-in post-merge branch refresh ([supervisor] branch_refresh): a branch is rebased only when every gate passes. The drive loop resolves the inputs. |
src/supervisor/claim.rs | Exclusive per-pane keystroke claim. Every sender — the drive loop’s nudge and buffered-input recovery, and the bundled sweep.sh approve in its own process — atomically creates <repo>/.git-paw/tmp/approve-pane-<N>.claim before sending a keystroke, so two can never both type into one pane. Non-blocking (a held pane is skipped, not waited on), released by RAII, and stealable once untouched past a 10s TTL so a hard-killed sender cannot wedge a pane. |
src/supervisor/drive.rs | The --unattended drive loop, run in-process by the binary’s entry layer once commands::supervisor::supervisor returns (or reports an existing session) and the drive-loop binding is taken: ~15 s sweeps of every coding-agent pane, non-blocking escalation of every live permission prompt as a broker review item (nothing is typed for it), buffered-input recovery, the optional correction loop and branch refresh, completion detection, and the exit summary. It sends no approval keystroke. |
src/supervisor/drive/decide.rs | The drive loop’s functional core. The loop runs observe → decide → apply: the shell in drive.rs observes (surfaces and their owners through the terminal port, screen captures, /status, new broker messages by cursor, merge states from git, the clock) and passes those facts to the pure decisions here — decide_prompt, decide_message, decide_nudge with decide_reengaged and decide_recovery_step, and summarize. Each returns the next state and an ordered list of DriveEffects (record a manual decision, publish an escalation, record a learning, submit buffered input, re-engage a worker, refresh branches, record an exhausted branch, note a judgment call; there is no approval effect). The shell applies each effect in order through the existing ports and appends it, with its EffectOutcome, to an in-memory effect log: drive_loop_logged returns DriveRun { summary, effect_log } and drive_loop returns only the summary. No decision reads a port, the clock, git or the broker. |
src/supervisor/keys.rs | The KeyDispatcher seam, implemented by every terminal backend (src/terminal/), used by the drive loop’s nudge and input-recovery paths. |
src/supervisor/loop_binding.rs | The drive-loop binding: a refreshed lease file (.git-paw/tmp/drive-loop.binding) that keeps at most one drive loop on a session instance, so a re-attach never runs a second loop beside a live one. |
src/supervisor/manual_approvals.rs | Records each prompt the drive loop escalates for a human decision to .git-paw/sessions/<session>.manual-approvals.jsonl, aggregates the log, and suggests which of the agent CLI’s own allowlists could absorb the pattern for git paw approvals. |
src/supervisor/permission_prompt.rs | All permission-prompt detection over a screen read through the terminal port: the approval-marker scan, the live-prompt gate and its anchoring windows, input-box and rule-line parsing, and extraction of the prompted command slice or file-operation target path. Reports what is on screen; decides nothing about the command. |
src/specs/ modules
| File | Purpose |
|---|---|
src/specs/mod.rs | Public surface (SpecBackend trait, SpecEntry) and the dispatch entry point (scan_specs / scan_specs_with_override). Picks the backend from the --specs-format CLI override or the [specs] type config — the only two sources (no filesystem auto-detection). |
src/specs/resolve.rs | Resolves --specs NAME[,NAME...] against the scanned entries: exact id match, then Spec Kit feature-name and numeric-prefix matches; an unknown or ambiguous name fails the whole resolution. |
src/specs/openspec.rs | OpenSpec backend: scans <dir>/<change>/tasks.md directories, skips <dir>/archive/. |
src/specs/markdown.rs | Markdown backend: scans flat .md files with YAML frontmatter; only paw_status: pending is picked up. |
src/specs/speckit.rs | Spec Kit backend: scans .specify/specs/<feature>/, decomposes the current phase into [P]-task worktrees plus one consolidated phase/… worktree; probes <dir>/../memory/constitution.md for the governance auto-wire. |
src/specs/superpowers.rs | Superpowers backend: scans docs/superpowers/plans/*.md plan files; each plan with incomplete steps becomes one entry (one worktree), never fanned out per task. |
src/mcp/ modules
The MCP subsystem follows a strict one-way dependency direction — query knows
nothing about MCP, tools knows about MCP and query, and server only wires
tools onto a transport — so a future HTTP transport stays additive.
| File | Purpose |
|---|---|
src/mcp/mod.rs | Entry point: cmd_mcp(), RepoContext, and repository resolution (--repo wins, else nearest .git ancestor). RepoContext also records whether the server runs in the main working tree (a root session, decided once with RepoLayout::is_linked_worktree) and, for a root session only, the session’s authority credential (redacted from Debug), plus the startup-resolved allow_repo_writes consent. |
src/mcp/server.rs | stdio transport setup, tool-registry wiring, and process lifecycle (exits when stdin closes). The lifecycle and verdict routers are merged only for a root session; call_tool answers a worktree’s by-name call to a root-only tool with an error naming the restriction. The spec-authoring write router is merged only when repo_write_access passes, and a by-name call otherwise gets the refusal (the switch, or the worktree restriction). |
src/mcp/repo_write.rs | The repository-write gate (repo_write_access, a pure decision over root session × allow_repo_writes) and write confinement: lexical checks on change/path, canonical checks that the spec directory lies inside the repository (not the root, .git or .git-paw), per-component symlink checks against the change directory, temp file + rename. |
src/mcp/logging.rs | Minimal in-tree logger (no tracing dependency) — diagnostics to stderr at a RUST_LOG-controlled level (default warn) and, with --log-file, tee’d to a file; stdout stays reserved for the JSON-RPC stream. |
src/mcp/query/* | Data-layer reads (no MCP types): conflicts.rs, docs.rs, events.rs, git.rs, governance.rs, intents.rs, learnings.rs, session.rs, source.rs, specs.rs, plus mod.rs. Built from broker HTTP state, files on disk, and git output. events.rs is the cursor read behind wait_for_events: it long-polls /log and returns the new entries, the new cursor, the broker epoch, and the wave. |
src/mcp/tools/* | MCP tool surfaces (one file per category): coordination.rs, docs.rs, git.rs, governance.rs, project.rs, publish.rs, session.rs, source.rs, spec_authoring.rs, plus mod.rs. Each read tool maps a query reader onto an MCP tool definition. publish.rs holds the publish tools: publish_status, publish_artifact, publish_blocked, and publish_question, built with the src/broker/publish.rs helpers, with the publishing id derived from the session (a worktree’s branch, or supervisor in the main working tree; no tool accepts one), plus the root-only publish_verified / publish_feedback, which attach the session credential read from the receipt at call time. session.rs adds the root-only lifecycle tools (start_session, pause_session, stop_session, purge_session), each an in-process call into git_paw::commands with a capturing output sink — never prompting, never attaching, never running the drive loop. spec_authoring.rs adds the gated create_spec / write_spec_artifact and the read-only validate_spec, thin adapters over the pure scaffold and structural check in src/specs/openspec.rs. |
src/coordination/ modules
| File | Purpose |
|---|---|
src/coordination/mod.rs | Public surface for the user→agent coordination helpers. |
src/coordination/inventory.rs | Agent inventory + target-validation helpers (unknown-target rejection) shared by the supervisor routing commands. |
src/coordination/tell.rs | Pure logic behind the /tell supervisor command — argument parsing and delivery-mode selection. The send itself (tmux send-keys or a broker publish) is driven by the supervisor skill. |
Start Flow
The start command is the primary flow. main.rs resolves supervisor mode and
dispatches it to one of three use cases in git_paw::commands: supervisor::supervisor when supervisor mode
resolves on (--supervisor or --unattended, else [supervisor] enabled, else
an interactive prompt when no [supervisor] section exists; --no-supervisor
always wins), start::start_with_specs for a spec-driven launch
(--from-all-specs / --specs) without a supervisor, and start::start
otherwise. All three run the same existing-session check first, and all three
return a LaunchOutcome instead of touching the terminal — main.rs attaches,
reattaches or refuses, or runs the unattended drive loop from it. Here’s what
start::start does step by step:
git paw start
│
▼
┌─ Validate git repo ──────────────────────────────────────┐
│ git::validate_repo() → repo root path │
└──────────────────────────────────────────────────────────┘
│
▼
┌─ Check for existing session ─────────────────────────────┐
│ │
│ Active, tmux session gone? ──yes──► Invalidate stale │
│ │ no receipt, then │
│ │ fresh start │
│ Paused + tmux alive? ──yes──► Restart broker, │
│ │ no reattach │
│ Active + tmux alive? ──yes──► Reattach │
│ │ no │
│ Stopped session? ──yes──► Recover │
│ │ no │
│ No session ──────► Fresh start │
└──────────────────────────────────────────────────────────┘
│
▼ (fresh start)
┌─ Load config ────────────────────────────────────────────┐
│ config::load_config() → merged PawConfig │
│ ([supervisor] enabled → hand off to supervisor()) │
└──────────────────────────────────────────────────────────┘
│
▼
┌─ Detect CLIs ────────────────────────────────────────────┐
│ detect::detect_clis() → Vec<CliInfo> │
│ (auto-detected + custom, deduplicated) │
└──────────────────────────────────────────────────────────┘
│
▼
┌─ Interactive selection ──────────────────────────────────┐
│ interactive::run_selection() │
│ → Vec<(branch, cli)> mappings │
│ (skipped if --cli + --branches provided) │
└──────────────────────────────────────────────────────────┘
│
▼
┌─ Create worktrees ───────────────────────────────────────┐
│ git::create_worktree() for each branch │
│ → .git-paw/worktrees/<slug>/ (child placement) │
│ or ../<project>-<slug>/ (sibling placement) │
│ worktree_provision::provision_worktree() │
│ → env-file copies, port slot, on_create hook │
└──────────────────────────────────────────────────────────┘
│
▼
┌─ Build tmux session ─────────────────────────────────────┐
│ TmuxSessionBuilder::new(project) │
│ .session_name("paw-project") │
│ .mouse_mode(true) │
│ .border_affordances(true) │
│ .add_pane(PaneSpec { branch, worktree, │
│ cli_command }) × N │
│ .build() → TmuxSession with command sequence │
└──────────────────────────────────────────────────────────┘
│
▼
┌─ Pane layout by mode ────────────────────────────────────┐
│ supervisor → Pane 0 = `git paw __dashboard` │
│ Pane 1..N = per-agent CLIs │
│ (supervisor() builds it with │
│ tmux::build_supervisor_session; the │
│ orchestrator runs outside the session) │
│ broker-only → Pane 0 = `git paw __dashboard` │
│ Pane 1..N = per-branch agent CLIs │
│ no broker → Pane 0..N = per-branch agent CLIs │
│ │
│ With the broker on, the tmux session environment │
│ carries GIT_PAW_BROKER_URL into every pane, and the │
│ dashboard pane renders the status table as a client │
│ of the broker host started next. │
└──────────────────────────────────────────────────────────┘
│
▼
┌─ Start the broker host (broker on) ──────────────────────┐
│ helpers::start_broker_host() │
│ → detached window `broker` (outside window 0's grid) │
│ running `git paw __broker --session <name> │
│ --log-path <log>` │
│ → wait ≤10s for GET /status (git_paw + repo_root); │
│ a host that exits or never answers fails the launch │
│ naming the port, before any receipt is saved │
│ helpers::register_agents_with_host() │
│ → POST /watch per agent (agent id, worktree, CLI, │
│ spec id) │
└──────────────────────────────────────────────────────────┘
│
▼
┌─ Save session state ─────────────────────────────────────┐
│ session::save_session() → atomic JSON write │
│ + per-repo discovery file (.git-paw/sessions/) │
└──────────────────────────────────────────────────────────┘
│
▼
┌─ Attach (entry layer, from LaunchOutcome::Attach) ───────┐
│ attach_or_print_hint() → user enters tmux session │
│ (prints an attach hint when stdin is not a TTY) │
└──────────────────────────────────────────────────────────┘
Per-repo session discovery file
Alongside the global receipt under ~/.local/share/git-paw/sessions/, git paw start writes a per-repo discovery file at
<repo>/.git-paw/sessions/<session>.json. This is the surface the bundled
sweep.sh supervisor helper reads to find the active session and its agent
roster from inside the repo (without reaching into the XDG state dir). git paw add / remove rewrite it with the updated roster; purge removes it.
Every path that creates or rebuilds the session’s panes (re)writes it, not
just the first launch — the bare launch, the spec-driven launch with or
without supervisor mode, the supervisor launch, recovery of a stopped
session, and restart of a paused session. The last two matter because
they change the layout: recovery rebuilds every pane from the saved receipt,
and a resume splits a fresh dashboard pane into the live session at whatever
index tmux assigns. A file left by the original launch would then record pane
indices that no longer describe the session. Recovery rewrites it from the
offsets of the layout it just rebuilt; the resume path reads each agent’s pane
index back from the running session (list-panes matched on
pane_current_path) rather than assuming the launch-time offsets.
Its shape — stable for sweep.sh and forward-compatible (consumers ignore unknown keys):
{
"session_name": "paw-myproject",
"agents": [
{
"branch_id": "feat-add-auth",
"worktree_path": "/abs/path/to/myproject/.git-paw/worktrees/feat-add-auth",
"cli": "claude",
"pane_index": 1
}
]
}
branch_id is the broker agent id (slugified branch); pane_index is the
agent’s tmux pane within the session window. When the file is absent (e.g. a
supervisor attached to a pre-existing paw-* session), sweep.sh falls back to
resolving the session name from $TMUX / tmux display-message -p '#S', so the
file never needs to be hand-authored.
The agents roster lists coding agents only. No orchestrator entry is
written: the orchestrator runs outside the session, so no pane is its own. A
file written by an earlier release may still carry one; it deserializes with
#[serde(default)], so such a file loads unchanged, and sweep.sh — which
reads agents and session_name by name — ignores it.
Broker Architecture
When [broker] enabled = true, the broker runs in its own process, git paw __broker, in a detached service surface of the session — on tmux, a window named broker, outside window 0’s pane grid, so pane indices are untouched. The launcher hands it the session name and the per-session log path on argv (a fresh launch saves its receipt only after the panes exist); when a saved receipt with that name exists (recovery, restart from pause) the host seeds its watch targets, the agent → spec mapping, and the broker port and bind from it. On a fresh launch the agents arrive through POST /watch instead, each with its spec id. The dashboard pane runs git paw __dashboard, a thin client that polls the host over HTTP; closing it never affects the broker.
Broker host process (git paw __broker, window `broker`):
├── tokio runtime (background threads)
│ ├── axum HTTP server on 127.0.0.1:9119 (default [broker] bind / port)
│ │ ├── POST /publish
│ │ ├── POST /watch (register a worktree as a live watch target,
│ │ │ optionally with the agent's spec id)
│ │ ├── GET /messages/:agent_id?since=N
│ │ ├── GET /status
│ │ └── GET /log?since=N[&wait=S] (full message log, cursor-paged and
│ │ long-pollable; read by the dashboard, the drive loop and the MCP server)
│ ├── Worktree watchers (src/broker/watcher.rs), one task per worktree
│ │ └── `git status --porcelain` + `git rev-parse HEAD` every 2s; publishes
│ │ agent.status on change; marks wave progress on a moved branch tip;
│ │ prunes a vanished worktree
│ └── Conflict detector (src/broker/conflict.rs), supervisor mode only
│ └── Forward / in-flight / ownership shapes
├── Log flush thread (std::thread, 5s interval)
│ └── Appends new messages to <session_name>.broker.log
├── Learnings flush thread (std::thread; opt-in via [supervisor] learnings = true)
│ └── Flushes the aggregator (src/broker/learnings.rs), fed on every publish,
│ to .git-paw/session-learnings.md (default every 60s, plus once at shutdown)
└── Main thread
└── waits for SIGHUP, reparent-to-init, or its terminal hanging up
Dashboard pane process (git paw __dashboard, pane 0):
└── ratatui dashboard (800ms idle tick; a keystroke wakes it immediately)
└── each tick: GET /status + GET /log?since=<cursor> through BrokerClient
Verdict authority. POST /publish refuses a verdict — any agent.verified, and any agent.feedback whose from is supervisor — unless the request’s X-Paw-Authority header matches the session’s authority credential (constant-time compare; 403 {"error":"authority required"}, no state change). The launcher mints the credential, persists it in the user-data Session (0600) with the receipt, and hands it to the host through a one-shot 0600 file in the sessions directory (never argv or env); the host reads it from there, else from the saved Session, and back-fills one only for a session saved before the credential existed. BrokerState holds it as an Option — None accepts no verdict. In-process publishers (conflict detector, role guard) are not gated; root-side HTTP publishers (the drive loop, sweep.sh verdict verbs via git paw __authority) attach the header. See Security Posture.
The host renders nothing and needs no $TMUX. It exits — dropping BrokerHandle, so the final broker-log flush and the learnings flush run — on SIGHUP (tmux kills its window on pause and every window on stop), on reparent-to-init (getppid() == 1, Unix only), or when a terminal it was given hangs up. It ignores SIGINT, sends no keystrokes to any surface, and a bind or probe failure exits non-zero with a diagnostic naming the port rather than retrying.
The dashboard’s render loop is bound to the session’s lifecycle the same way: a single exit gate checked every iteration (on all paths, including the degraded branch) terminates it on reparent-to-init or when the controlling terminal is gone (a poll error or a failed terminal write — catching reparent-to-a-lingering-shell). An unreachable broker is never an exit condition: the dashboard keeps the last snapshot, shows “broker unreachable” in its status line, and reconnects on its own. See the Dashboard chapter for the user-facing description.
Broker state
The broker state is an Arc<BrokerState> defined in src/broker/mod.rs: an
RwLock around the agent records, inboxes, and message log, plus a
lock-free AtomicU64 sequence counter. Lock guards are never held across an
.await (the clippy::await_holding_lock lint is set to deny). The state is
shared between the axum server handlers, the watchers, the conflict detector,
and the learnings aggregator, all inside the broker host. The server
validates incoming messages (src/broker/server.rs) and hands them to
src/broker/delivery.rs, which sequences, logs, and routes them; the dashboard
reads the latest snapshot over GET /status each tick.
The log flush thread periodically appends new messages to the session’s broker
log in the session-state directory
(~/.local/share/git-paw/sessions/<session_name>.broker.log, recorded as
broker_log_path in the session file — one log per session, so two
repositories never write to or purge the same file), one human-readable line
per message
([seq] <unix-seconds> [agent_id] <summary>). This runs on a plain
std::thread to avoid contention with the tokio runtime.
Environment injection
When the broker is enabled, git-paw sets GIT_PAW_BROKER_URL=http://<bind>:<port> (default http://127.0.0.1:9119) in the tmux environment for the session when it builds it. Each agent pane inherits this variable and can use it to communicate with the broker.
Supervisor Mode Layout
When --supervisor is active (or [supervisor] enabled = true), the tmux
session is laid out as the dashboard in a full-width top row plus a row-major
agent grid below. No pane runs an orchestrator CLI.
┌─────────────────────────────────────────────────────┐
│ pane 0: dashboard │
├──┬──┬──┬──┬──┬──────────────────────────────────────┤
│ 1│ 2│ 3│ 4│ 5│ agent grid (row 1) │
├──┴──┴──┴──┴──┤ │
│ 6│..│..│..│ N│ agent grid (row 2..M) │
└──┴──┴──┴──┴──┴──────────────────────────────────────┘
Pane 0 always hosts the dashboard; pane indices 1 onwards host one CLI per
agent. The orchestrator runs outside the tmux session — the operator’s own
agent session, pointed at the repo-root sidecar .git-paw/AGENTS.local.md that
the launch writes and names. It drives the run through the broker, the bundled
helpers (.git-paw/scripts/sweep.sh, broker.sh) or the MCP server; the
dashboard reads the same broker state for its status table.
A live session saved by an earlier release (orchestrator CLI at pane 0,
dashboard at pane 1, agents from 2) keeps resolving until it ends: every
consumer derives the first agent pane from the recorded dashboard_pane
(n → agents from n + 1), and neither of that session’s first two panes is
ever treated as a coding agent. Recovering such a session rebuilds the layout
above.
One layout for every session
There is a single pane layout. Bare start, spec-driven start and
supervisor-mode start — and the recovery of any of them — all build the grid
above through one routine, so the geometry never depends on how the session
was launched. With the broker disabled there is no dashboard: the agent rows
start at pane 0 and share the full window height instead of the 50% the
dashboard row leaves.
This is why git paw add and git paw remove work on every session. The grid
is laid out by explicit split-window + resize-pane calls with predictable,
stable pane indices, which is what lets it be re-tiled incrementally for a new
agent count. Bare sessions were previously laid out with tmux’s whole-window
select-layout tiled / main-horizontal layouts, which cannot be extended in
place; those layouts are no longer used for coding agents anywhere.
Each session records the layout it was built with (Session.layout), so a
live session started by an earlier binary — which recorded none, and for a
bare session therefore was tiled — is refused by add / remove rather than
re-tiled under the user. Recovery rebuilds such a session as a grid and
records it.
Row-height proportions
The top row is always half the session height — the dashboard spans its full
width — and the agent rows share the remaining 50% evenly, each taking
50 / agent_rows percent. This is a fixed rule rather
than a per-agent-count table: adding agents shrinks the agent rows, never the
top row.
| Agent rows | Top row | Each agent row |
|---|---|---|
| 1 | 50% | 50% |
| 2 | 50% | 25% |
| 3 | 50% | 16.7% |
| 4 | 50% | 12.5% |
| 5 | 50% | 10% |
Pane order
Every agent’s pane index is its row-major position: agent i is pane
first_agent + i. That holds because every split targets the session’s
last pane, and tmux places a split’s new pane right after its target in
the pane list: a sibling in the current row is split-window -h off the
previous pane, and a new row is a full-width split-window -v -f off the
previous pane, so the row spans the window below the rows above it. Splitting
a new row off the previous row’s first pane instead would put the new pane
mid-list and shift every later pane by one, so each launch command and prompt
typed by index would land in the wrong agent’s pane. git paw add uses the
same two splits, so an added agent takes the next index without renumbering
any existing pane.
Equal-width agent columns
Agent panes are spliced into a row by successive tmux split-window -h, and
each -h split halves the current pane — so a row populated by raw splits
renders unequal widths (a 3-agent row would render 50/25/25, not equal
thirds). select-layout tiled is deliberately not used for the whole
window because it would scramble the predictable pane-index ordering the rest
of the system relies on. Instead, after the panes for a row exist, git-paw
rebalances that row to equal width: tmux::rebalance_agent_rows queries the
live window width and issues tmux resize-pane -x <cols> so each pane in the
row gets an equal column share (the last pane absorbs the rounding remainder),
leaving the row equal-width within a one-column tolerance. The rebalance runs
on every path that changes the grid — git paw start (bare, spec-driven and
supervised, and the recovery of each), git paw add, and git paw remove —
so an incrementally-built grid matches a start-time grid of
the same agent count. It never touches the full-width dashboard row nor the
per-row vertical heights. No agent row exceeds five panes
(SUPERVISOR_AGENTS_PER_ROW), bounding the smallest equal-width target to
~20% of the window. src/surface.rs and the rebalance in src/tmux/layout.rs
are the source of truth.
Launch-readiness gate
Before injecting an agent’s boot block (the initial prompt, paste-handling
notes, and the task prose; the spec format’s workflow entry point follows
later, as its own message once the pane is idle) into a pane, git-paw verifies the pane’s CLI
has actually reached an interactive state rather than relying on a fixed
wall-clock sleep. tmux::gate_pane_for_injection polls the pane with
tmux capture-pane for a CLI-readiness marker; only once the marker appears is
the boot block injected. If the readiness budget elapses while the pane is
still a bare shell (the CLI never started), git-paw relaunches the CLI command
into that pane and polls again, up to a small relaunch budget, before falling
back to injection. The gate is conservative: an unrecognised CLI whose UI
matches no known marker simply falls back to injecting after the budget, so
launch behaviour is never worse than the previous fixed-sleep launch. This
prevents the multi-line boot block from being typed into a bare shell, where
it would be interpreted line-by-line as failing commands. The same gate also
recognises a first-run trust/bypass acceptance dialog and fails the launch
loudly rather than risk a blind Enter accepting it. The gate guards every
pane that receives an injected prompt: bare git paw start (with the broker
enabled), a spec-driven launch (--specs / --from-all-specs, with or
without --supervisor), and git paw add. Session recovery and a
broker-less bare launch spawn panes without ever calling the gate — harmless,
since neither path injects anything into them.
Non-Supervisor Layout
When supervisor mode is OFF and the broker is on, the dashboard occupies pane 0 and the agent CLIs occupy panes 1 onwards in a single row-major grid (no top row):
┌───────────────────────────────────────────────────────┐
│ pane 0: dashboard │
├──┬──┬──┬──┬──┬────────────────────────────────────────┤
│ 1│ 2│ 3│ 4│ 5│ agent grid (row 1) │
├──┴──┴──┴──┴──┤ │
│ 6│..│..│..│ N│ agent grid (row 2..M) │
└──┴──┴──┴──┴──┴───────────────────────────────────────┘
When the broker is disabled too, every pane (0..N) is an agent CLI and there is no dashboard pane.
Worktree Lifecycle
Git worktrees are the foundation of git-paw’s parallel workflow.
Creation
Where a worktree goes is set by worktree_placement in the config
(git::worktree_path_for). git paw init writes worktree_placement = "child",
which nests worktrees inside the repo. For a project named my-app and
branches feature/auth-flow and feat/api:
my-app/ ← main repo (current directory)
└── .git-paw/worktrees/
├── feature-auth-flow/ ← worktree (created by git-paw)
└── feat-api/ ← worktree (created by git-paw)
A child worktree is named by the branch slug: slashes become hyphens and
characters outside [A-Za-z0-9._-] are dropped.
A config with no worktree_placement key resolves to "sibling", the earlier
layout, which creates worktrees beside the main repo directory:
my-app/ ← main repo (current directory)
my-app-feature-auth-flow/ ← worktree (created by git-paw)
my-app-feat-api/ ← worktree (created by git-paw)
The sibling naming convention is <project>-<sanitized-branch>, where every
non-alphanumeric character becomes a hyphen.
Lifecycle states
create_worktree() stop start (recover)
│ │ │
▼ ▼ ▼
[exists on disk] → [still on disk] → [reused as-is]
│
purge │
▼
[removed from disk]
Key points:
- Stop preserves worktrees — uncommitted work survives
- Recover reuses existing worktrees — no data loss
- Purge removes worktrees — per worktree it runs the configured
[worktree.hooks] on_removecommand, thengit worktree remove --force, then decides the branch’s fate. There is no prune step. The branch is deleted only when git-paw created it andgit merge-base --is-ancestor refs/heads/<branch> refs/heads/<target>succeeds for the default branch or for the branch recorded as the worktree’s base; otherwise it is kept and printed with its tip SHA and a resume/discard hint. Each step is best-effort: a failure is reported as awarning:and never aborts the teardown. - Start prunes stale worktree registrations (
git worktree prune) before creating new worktrees
Session State
Session state is persisted as JSON under ~/.local/share/git-paw/sessions/
(~/Library/Application Support/git-paw/sessions/ on macOS):
{
"session_name": "paw-my-app",
"repo_path": "/home/you/projects/my-app",
"project_name": "my-app",
"created_at": "2025-01-15T10:30:00Z",
"status": "active",
"worktrees": [
{
"branch": "feat/auth",
"worktree_path": "/home/you/projects/my-app/.git-paw/worktrees/feat-auth",
"cli": "claude",
"branch_created": true
},
{
"branch": "feat/api",
"worktree_path": "/home/you/projects/my-app/.git-paw/worktrees/feat-api",
"cli": "codex",
"branch_created": false
}
],
"broker_port": 9119,
"broker_bind": "127.0.0.1",
"broker_log_path": "/home/you/.local/share/git-paw/sessions/paw-myproject.broker.log",
"mode": "bare",
"dashboard_pane": 0
}
status is active, paused, or stopped. broker_port, broker_bind,
broker_log_path, and dashboard_pane are present only when the broker is
enabled; git paw status uses the bind and port to probe the broker, and
git paw purge uses broker_log_path to clean up that session’s broker log. mode (bare
or supervisor) records the pane layout so recovery rebuilds the same shape.
branch_created records whether git-paw created the branch (rather than
reusing an existing one). A worktree entry can also carry base_branch (the
branch a git-paw-created branch started from, so teardown can tell an
already-merged branch from an unmerged one), pending_boot_prompt,
runtime_slot, cli_session_id, and spec_id (the id of the spec the agent
was launched from, absent for a bare git paw start or a git paw add
without --from-spec); each is omitted when unset.
Atomic writes
Session state is written atomically: write to a temporary file, then rename. This prevents corruption if the process is killed mid-write.
Effective status
The on-disk status may not reflect reality (e.g., tmux was killed externally). git-paw checks the actual tmux state:
| File status | tmux alive? | Effective status |
|---|---|---|
active | Yes | Active (reattach) |
active | No (session gone) | Stale — start invalidates the receipt and launches fresh |
paused | Yes | Paused (restart the broker, reattach) |
paused | No | Stopped (recover) |
stopped | N/A | Stopped (recover) |
| No file | N/A | No session |
For an active receipt, “alive” also requires at least one agent pane running
something other than a bare shell once the session is past a ~30-second launch
grace period; a session whose every agent CLI has exited is treated as Stopped
and recovered.
Tmux Builder Pattern
The tmux module uses a builder pattern that accumulates operations as data structures rather than immediately executing shell commands. This enables:
- Testability — generate commands without executing them
- Dry run — render the plan (
command_strings()) without side effects - Atomicity — validate the full plan before running anything
#![allow(unused)]
fn main() {
TmuxSessionBuilder::new("my-app")
.session_name(resolved_name) // e.g. "paw-my-app"
.mouse_mode(true)
.border_affordances(true)
.set_environment("GIT_PAW_BROKER_URL", &broker_url)
.add_pane(PaneSpec { branch, worktree, cli_command })
.add_pane(PaneSpec { ... })
.build()?
// → TmuxSession { name, commands: Vec<TmuxCommand> }
}
The built TmuxSession can be inspected (command_strings()) or executed
(execute()). Supervisor-mode sessions are assembled the same way by
tmux::build_supervisor_session; git paw add splices a pane in with
build_add_agent_commands and git paw remove retiles with
build_remove_retile_commands — all in src/tmux/command.rs.
CLI-launch robustness
Supervisor-mode sessions and panes hot-added by git paw add are created with
shell auto-update prompts suppressed
(new-session/session env set DISABLE_AUTO_UPDATE=true and
DISABLE_UPDATE_PROMPT=true) so an interactive framework prompt (e.g.
oh-my-zsh’s Would you like to update? [Y/n]) cannot fire as the pane’s
shell reads its rc and swallow the first keystroke of the CLI-launch command.
As a second layer, those builders send a C-u line-clear immediately before
each CLI-launch command, so any stray pending input cannot corrupt it. Every
headless new-session canvas is sized (480×140) to tile a supervisor session
with several agents when no client is attached; a real terminal resizes the
session on attach.
Error Strategy
All errors flow through PawError (defined with thiserror). Each variant carries an actionable message telling the user what went wrong and how to fix it. No panics in non-test code — all Result propagation.
Exit codes:
- 0 — success
- 1 — operational error
- 2 — user cancelled
Target Architecture
The Architecture chapter describes the code as it is today. This page describes the structure the code is moving toward, and the rules new code follows so that it moves in that direction rather than away from it.
The move is incremental. Modules are restructured when they are next changed, not in one large rewrite, and every step keeps git-paw’s behavior exactly the same.
Why
git-paw coordinates several outside systems: a terminal multiplexer, git, agent CLIs, an HTTP broker, and MCP clients. Today some of them leak into code that should not need to know about them — tmux pane numbers appear in session state and in the supervisor’s decision logic, and the drive loop assumes one particular CLI’s screen layout in places. That makes each of them hard to replace (for example, tmux by another terminal backend or a headless mode), hard to test without faking processes, and risky to freeze as a stable contract.
The pattern
Two well-known patterns, used together:
- Ports and adapters (also called hexagonal architecture) for the overall structure. Everything git-paw talks to sits behind a port — a Rust trait named in git-paw’s own terms — and is reached only through an adapter that implements that trait.
- Functional core, imperative shell (in Rust also called sans-IO) for the logic inside. Code that decides — whether to approve a prompt, whether to nudge an agent, whether a branch was merged — is a pure function: data in, data out. Separate code observes the world and carries out the decisions.
Two smaller patterns fill specific slots: Strategy for behavior that differs per agent CLI or per spec format, and data-transfer objects (DTOs) wherever data is written to a file or sent over the network.
Layers
entry who calls git-paw CLI (clap), MCP tools, dashboard, broker HTTP server
│ thin: parse input → call a use case → show the result
▼
app use cases start, stop, purge, add, remove, … and the drive-loop shell
│ orchestrates: observes and acts through ports, asks core what to do
▼
core the domain agents, waves, surfaces, phases, escalations + pure decisions
no I/O · no tmux · no HTTP or MCP libraries · no file or wire formats
ports traits the app needs terminal backend, version control, broker client, clock
adapters implementations tmux, the git command line, the broker over HTTP, test fakes
wire serialized formats session file, config file, broker messages, MCP payloads
These are layers, not directories that exist today.
The dependency rule
Dependencies point inward: entry → app → core. Adapters depend on the ports they implement,
and nothing depends on an adapter except the startup code that wires the program together.
core depends on nothing else in git-paw. If code in core would need tmux, an HTTP framework,
the MCP library, or a file format, it belongs in another layer.
Ports
| Port | What it means in git-paw’s terms | Adapters |
|---|---|---|
| Terminal backend | Create and remove an agent’s surface; send it text or a key; read its recent output; list the surfaces and the worktree each one is in; stream its output to a log; check it is alive; let a person attach | tmux today; a native terminal backend or a headless mode could be added later |
| Version control | Create and remove worktrees, read branches and refs, ask whether a branch has been merged | the git command line |
| Broker client | Publish a message; read agent status and the message log | HTTP to the broker |
| Process runner | Run an external command | CommandRunner (already exists) |
| Clock | The current time | the system clock; a fixed clock in tests |
Ports use git-paw’s vocabulary, never a tool’s. A terminal method named after tmux’s
pane_current_path only fits tmux. The question git-paw actually needs answered is “which agent
owns this surface?” — tmux answers it with pane_current_path, a native terminal backend knows
it because it created the surface, and a headless mode never needs to ask. Surfaces are identified
by an opaque id rather than a tmux pane number.
Adapters are compiled into the binary and chosen at startup, not loaded as runtime plugins. Each one runs its tool the way git-paw already does, so no new dependencies are needed.
Example: escalating a permission prompt
git-paw answers no permission prompt. When a coding agent’s screen shows one, the drive loop’s job is
to put it in front of a human without stalling the rest of the wave. The decision lives in
src/supervisor/drive/decide.rs; the shell is src/supervisor/drive.rs.
#![allow(unused)]
fn main() {
// core: pure — plain data in, the next state and effects out
pub enum DriveEffect { RecordManualDecision { .. }, PublishEscalation(Escalation), RecordLearning { .. },
SubmitBufferedInput { .. }, /* …no approval variant */ }
pub fn decide_prompt(state: &DedupWindow, obs: &SurfaceObservation, now: Instant)
-> (DedupWindow, Vec<DriveEffect>) { … }
// CLI-unavailable banner → one deduped PublishEscalation (cli-unavailable)
// not live, text buffered → SubmitBufferedInput
// live prompt, new dedup key → RecordManualDecision, PublishEscalation (permission-prompt), RecordLearning
// anything else, or a repeat inside the dedup window → nothing
// app: the shell — the only part that does I/O, and only through ports
let capture = terminal.capture(surface);
let (next, effects) = decide::decide_prompt(&dedup, &observation(&capture, profile_for(agent)), clock.now());
dedup = next;
for effect in effects {
shell.apply(deps, effect); // through the ports, in order; logged with its outcome
}
}
The decision is tested by passing in a captured screen and a time and checking the effects — no fake
terminal and no fake clock. No input makes it return a keystroke for a live prompt. Supporting a
different terminal means writing a new adapter; core and app do not change.
Behavior that differs per CLI or per spec format
Agent CLIs differ in how their screens look and which flags they accept; spec formats differ in where specs live and how tasks are tracked. Each is a Strategy: one implementation per CLI or format behind a single trait, with a generic implementation as the fallback for anything unrecognized. The profile for an agent’s CLI is worked out once for that agent and passed to the decisions that need it, instead of each decision assuming one particular CLI.
Use a Rust enum when the set is small and fixed and callers really do need to handle each case
differently — the compiler then checks that every case is handled. Use a trait when the set can
grow and callers should not need to know which implementation they hold; terminal backends are
the clearest example.
Serialized formats
The session file, the config file, broker messages and MCP responses are read by other processes
and by the bundled helper scripts, so they are contracts that must stay stable. The target keeps
each one as a separate wire type (a DTO) with a conversion to and from the core types, so
the internal model can change without changing the format. Each format carries a version field
and stays backward-compatible: new fields are optional, and older files keep loading unchanged.
This split applies only where a format is a contract, not to every struct in the program.
Rules for new code
- Put each decision in a pure function that takes data and returns data. Keep I/O in the shell.
- Reach tmux, git, the broker and the filesystem only through their port. Do not add direct
tmuxcalls outside the terminal adapter. - Name ports and core types in git-paw’s terms (agent, surface, wave), never a tool’s.
- Identify an agent’s terminal by its surface, not by a pane number. Do not add logic that depends on “pane 0”.
- Do not hardcode one agent CLI’s behavior where a per-CLI profile exists.
- Keep serialized formats additive and backward-compatible.
- Restructure in small, safe steps: first add the port or type and route the existing code through it with no change in behavior, then let the compiler find every call site, and only then add a second implementation or remove the old code.
Where the code is today
| Area | Status |
|---|---|
Live-prompt gate (supervisor::permission_prompt) | Already a pure decision |
| Dashboard | Already Model-View-Update — a functional core for a terminal UI |
Process execution (CommandRunner) | Already a port with a test fake; not yet used everywhere |
| MCP server | Already layered one way (query → tools → server) and talks to the broker over HTTP |
| Terminal | A port: terminal::TerminalBackend, in git-paw’s terms and keyed by an opaque SurfaceId, with tmux (terminal::tmux::TmuxBackend) as its first adapter and a recording fake (terminal::fake::FakeBackend) for logic tests. The drive loop, the launch paths’ session opening, logging and layout, add’s and remove’s surface changes, prompt and entry-point submission, the readiness gate and the agent inventory go through it; golden argv fixtures pin the adapter to the pre-port tmux behaviour, and a source audit keeps tmux spawns inside src/tmux/ and the adapter. One owner rule (surface::resolve_agent, exact canonical path) resolves surfaces to agents. The lifecycle commands (pause, stop, attach, status, doctor) and a few launch-path helpers (session-name resolution, the attach hand-off, add’s pane reconciliation) still call src/tmux/ helpers directly. Pane numbers remain the tmux adapter’s encoding in session state |
| Broker | Runs in its own host process (git paw __broker) in a detached service surface of the session; the dashboard is a thin HTTP client of it, like the MCP server and the drive loop, through the shared BrokerClient |
| Drive loop | Observe → decide → apply. The shell (supervisor/drive.rs) observes through the ports and calls pure decisions in supervisor/drive/decide.rs (decide_prompt, decide_message, decide_nudge / decide_reengaged / decide_recovery_step, summarize), which return the next state and a list of DriveEffects. The shell applies each effect in order and records it, with its outcome, in an in-memory effect log (drive_loop_logged returns DriveRun { summary, effect_log }). The heartbeat helpers heartbeat_due / tick_made_progress are pure functions the shell calls. The effect log is not persisted |
Commands (start, stop, purge, …) | A callable library surface (git_paw::commands): each family takes a CommandContext (repository root and config), an options struct and an injected Output sink, and returns a typed outcome. main.rs is the thin entry layer — it resolves the current directory and config, prompts, renders, and attaches or runs the drive loop from the outcome. A source audit keeps the command bodies free of cwd, stdin, prompts, prints and attach |
| Serialized formats | The session file, config file and broker messages use the same types as the internal model |
Further reading
- Hexagonal Architecture (ports and adapters), Alistair Cockburn.
- Functional Core, Imperative Shell, Gary Bernhardt.
Specifications
git-paw uses OpenSpec for formal, testable
specifications: each capability has a dedicated spec.md under
openspec/specs/
using RFC 2119 keywords (SHALL, MUST, SHOULD) and GIVEN/WHEN/THEN
scenarios, and every scenario maps to at least one test.
The eight foundational capabilities (the original v0.1–v0.2 surface) are reproduced in full below. The project has since grown to many more capabilities (broker, supervisor, dashboard, MCP, learnings, conflict detection, unattended operation, agent-friendly docs, …); the complete index at the end of this page links to every capability spec in the repository.
Foundational capabilities
| Capability | Description |
|---|---|
| CLI Parsing | Command-line argument parsing and subcommands |
| CLI Detection | Auto-detect AI CLIs on PATH, load custom CLIs |
| Git Operations | Validate repos, list branches, manage worktrees |
| Tmux Orchestration | Create sessions, manage panes, apply layout |
| Session State | Persist and recover session state |
| Configuration | Parse and merge TOML config files |
| Interactive Selection | User prompts for mode, branch, and CLI selection |
| Error Handling | Unified error types with exit codes |
CLI Parsing
Purpose
Define the command-line interface for git-paw using clap v4. Declares all subcommands (start, stop, purge, status, list-clis, add-cli, remove-cli), their flags, and argument validation. When no subcommand is given, defaults to start. This includes the git paw start --force flag that suppresses the uncommitted-spec validation warning (logging force usage to stderr for audit and propagating git status failures as PawError::GitError), and the guarantee that git paw start --supervisor --specs <names> passes the resolved --specs value (not just its presence) to the supervisor launch path so it launches worktrees only for the named subset — pinned by a regression matrix across every selection-flag/supervisor-flag combination and required to behave identically across all spec backends (OpenSpec, Markdown, Spec Kit).
Requirements
Requirement: Default to start when no subcommand is given
The system SHALL treat no arguments as equivalent to start with no flags.
The system SHALL also accept the hidden internal subcommands __broker, which hosts the session’s broker in its detached service surface; __dashboard, which renders the dashboard as a thin client of that broker in pane 0; and __prompt-profile, which prints one resolved CLI prompt-shape profile field for the bundled helper. None of them SHALL appear in --help output.
Scenario: No arguments yields None command
- GIVEN no arguments are passed
- WHEN the CLI is parsed
- THEN
commandSHALL beNone(handled asStartin main)
Scenario: __dashboard subcommand parses
- GIVEN
__dashboardis passed - WHEN the CLI is parsed
- THEN the command SHALL be
Command::Dashboard
Scenario: __broker subcommand parses
- GIVEN
__brokeris passed - WHEN the CLI is parsed
- THEN the command SHALL be
Command::Broker
Scenario: Hidden subcommands do not appear in help
- GIVEN
--helpis passed - WHEN the help text is rendered
- THEN the output SHALL NOT contain
__dashboard,__brokeror__prompt-profile
Requirement: Start subcommand with optional flags
The start subcommand SHALL be extended to accept a --supervisor flag (boolean, defaults to false). The flag MAY be combined with any other start flags.
When --supervisor is passed, the parsed StartArgs struct SHALL have supervisor: bool set to true.
The start subcommand SHALL also accept a --no-rebase flag (boolean, defaults to false). When --no-rebase is passed, the parsed StartArgs struct SHALL have no_rebase: bool set to true. The dispatch SHALL invoke create_worktree with rebase_onto_main = !args.no_rebase for every worktree creation in the launch. When --no-rebase is omitted (i.e. no_rebase == false), agent branches SHALL be rebased onto the repository’s default branch before their worktrees are opened. When --no-rebase is present, agent branches SHALL NOT be rebased, matching the post-worktree-resume-fix v0.5.0 behaviour.
The --no-rebase flag MAY be combined with any other start flags including --supervisor, --from-specs, --cli, and --branches.
Scenario: Start with –supervisor flag
- GIVEN
start --supervisor - WHEN the CLI is parsed
- THEN
supervisorSHALL betrue
Scenario: Start with –supervisor combined with other flags
- GIVEN
start --supervisor --cli claude --branches feat/a,feat/b - WHEN the CLI is parsed
- THEN
supervisorSHALL betrue - AND
cliSHALL beSome("claude") - AND
branchesSHALL be["feat/a", "feat/b"]
Scenario: Start without –supervisor defaults to false
- GIVEN
start --cli claude - WHEN the CLI is parsed
- THEN
supervisorSHALL befalse
Scenario: Start with –no-rebase flag
- GIVEN
start --no-rebase - WHEN the CLI is parsed
- THEN
no_rebaseSHALL betrue
Scenario: Start without –no-rebase defaults to false
- GIVEN
start --cli claude - WHEN the CLI is parsed
- THEN
no_rebaseSHALL befalse
Scenario: Start with –no-rebase combined with other flags
- GIVEN
start --no-rebase --supervisor --from-specs - WHEN the CLI is parsed
- THEN
no_rebaseSHALL betrue - AND
supervisorSHALL betrue - AND
from_specsSHALL betrue
Scenario: –no-rebase propagates to create_worktree as rebase_onto_main = false
- GIVEN
start --branches feat/a --no-rebaseis invoked - WHEN the dispatch reaches the worktree-creation loop
- THEN
create_worktree(repo_root, "feat/a", rebase_onto_main)SHALL be called withrebase_onto_main = false
Scenario: Default start propagates rebase_onto_main = true
- GIVEN
start --branches feat/ais invoked without--no-rebase - WHEN the dispatch reaches the worktree-creation loop
- THEN
create_worktree(repo_root, "feat/a", rebase_onto_main)SHALL be called withrebase_onto_main = true
Requirement: Stop subcommand
The stop subcommand SHALL take no confirmation flag and SHALL NOT prompt for confirmation. Stopping is recoverable — the worktrees, branches and session state survive, and git paw start recovers the session — so stop SHALL proceed immediately whether or not stdin is a TTY. The stop subcommand SHALL NOT accept a --force flag; passing one SHALL be rejected by argument parsing as an unexpected argument.
The long_about help text for stop SHALL name all three teardown verbs (pause, stop, purge) with a one-line summary of each, so users can choose the right verb at --help time, and SHALL NOT describe a confirmation prompt or a --force flag.
Scenario: Stop parses without flags
- GIVEN
stopis passed - WHEN the CLI is parsed
- THEN the command SHALL be
Command::Stop
Scenario: Stop rejects –force
- GIVEN
stop --forceis passed - WHEN the CLI is parsed
- THEN parsing SHALL fail with an unexpected-argument error naming
--force
Scenario: Stop help names all three teardown verbs
- WHEN
git paw stop --helpis run - THEN the output SHALL mention
pauseas the soft alternative - AND the output SHALL mention
purgeas the full reset - AND the output SHALL describe what
stopitself does (kills CLI processes, preserves worktrees) - AND the output SHALL NOT mention a confirmation prompt or a
--forceflag
Scenario: Stop from a TTY does not prompt
- GIVEN an active session and stdin attached to a TTY
- WHEN
git paw stopis run - THEN no interactive prompt SHALL be rendered
- AND the session SHALL be stopped immediately
Requirement: Purge subcommand with optional –force flag
The purge subcommand SHALL accept an optional --force flag (defaults to false).
Scenario: Purge without –force
- GIVEN
purgeis passed without flags - WHEN the CLI is parsed
- THEN
forceSHALL befalse
Test: cli::tests::purge_without_force
Scenario: Purge with –force
- GIVEN
purge --forceis passed - WHEN the CLI is parsed
- THEN
forceSHALL betrue
Test: cli::tests::purge_with_force
Requirement: Status subcommand
The status subcommand SHALL parse with no additional arguments.
Scenario: Status parses
- GIVEN
statusis passed - WHEN the CLI is parsed
- THEN the command SHALL be
Command::Status
Test: cli::tests::status_parses
Requirement: List-CLIs subcommand
The list-clis subcommand SHALL parse with no additional arguments.
Scenario: List-CLIs parses
- GIVEN
list-clisis passed - WHEN the CLI is parsed
- THEN the command SHALL be
Command::ListClis
Test: cli::tests::list_clis_parses
Requirement: Add-CLI subcommand with required and optional arguments
The add-cli subcommand SHALL require name and command positional arguments and accept an optional --display-name flag.
Scenario: Add-CLI with required arguments only
- GIVEN
add-cli my-agent /usr/local/bin/my-agent - WHEN the CLI is parsed
- THEN
nameSHALL be"my-agent",commandSHALL be the path, anddisplay_nameSHALL beNone
Test: cli::tests::add_cli_with_required_args
Scenario: Add-CLI with –display-name
- GIVEN
add-cli my-agent my-agent --display-name "My Agent" - WHEN the CLI is parsed
- THEN
display_nameSHALL beSome("My Agent")
Test: cli::tests::add_cli_with_display_name
Scenario: Add-CLI missing required arguments is rejected
- GIVEN
add-cliwith no positional arguments - WHEN the CLI is parsed
- THEN parsing SHALL fail
Test: cli::tests::add_cli_missing_required_args_is_rejected
Requirement: Remove-CLI subcommand with required argument
The remove-cli subcommand SHALL require a name positional argument.
Scenario: Remove-CLI parses
- GIVEN
remove-cli my-agent - WHEN the CLI is parsed
- THEN
nameSHALL be"my-agent"
Test: cli::tests::remove_cli_parses
Requirement: Standard flags –version and –help
The CLI SHALL accept --version and --help flags.
Scenario: –version flag is accepted
- GIVEN
--versionis passed - WHEN the CLI is parsed
- THEN clap SHALL emit a
DisplayVersionresponse
Test: cli::tests::version_flag_is_accepted
Scenario: –help flag is accepted
- GIVEN
--helpis passed - WHEN the CLI is parsed
- THEN clap SHALL emit a
DisplayHelpresponse
Test: cli::tests::help_flag_is_accepted
Requirement: Unknown subcommands are rejected
The CLI SHALL reject unrecognized subcommands with a parse error.
Scenario: Unknown subcommand fails
- GIVEN an unrecognized subcommand is passed
- WHEN the CLI is parsed
- THEN parsing SHALL fail
Test: cli::tests::unknown_subcommand_is_rejected
Requirement: Help output contains all subcommands and quick start
The start --help output SHALL list the --supervisor flag with a description.
Scenario: Start help shows –supervisor flag
- GIVEN
start --helpis passed - WHEN the binary runs
- THEN stdout SHALL contain
--supervisor
Requirement: Version output includes binary name
The --version output SHALL include the binary name.
Scenario: Version output
- GIVEN
--versionis passed - WHEN the binary runs
- THEN stdout SHALL contain “git-paw”
Test: cli_tests::version_output
Requirement: No arguments behaves like start
When no subcommand is provided, the binary SHALL behave identically to start.
Scenario: No args produces same error as start
- GIVEN the binary is run with no arguments outside a git repo
- WHEN both
git-pawandgit-paw startare run - THEN they SHALL produce identical stderr output
Test: cli_tests::no_args_behaves_like_start
Requirement: Subcommands run without error when applicable
Subcommands that don’t require a session SHALL succeed in a valid git repo.
Scenario: Stop runs without error
- GIVEN the binary is run in a git repo
- WHEN
stopis passed - THEN it SHALL succeed
Test: cli_tests::stop_runs_without_error
Scenario: Status runs without error
- GIVEN the binary is run in a git repo
- WHEN
statusis passed - THEN it SHALL succeed
Test: cli_tests::status_runs_without_error
Scenario: List-CLIs runs without error
- GIVEN the binary is run in a git repo
- WHEN
list-clisis passed - THEN it SHALL succeed
Test: cli_tests::list_clis_runs_without_error
Requirement: Binary rejects missing required arguments
Subcommands with required arguments SHALL fail when they are missing.
Scenario: Add-CLI requires arguments
- GIVEN
add-cliis passed with no arguments - WHEN the binary runs
- THEN it SHALL fail with stderr mentioning “required”
Test: cli_tests::add_cli_requires_arguments
Scenario: Remove-CLI requires argument
- GIVEN
remove-cliis passed with no arguments - WHEN the binary runs
- THEN it SHALL fail with stderr mentioning “required”
Test: cli_tests::remove_cli_requires_argument
Requirement: Not-a-repo error from binary
Commands requiring a git repo SHALL fail with an actionable error when run outside one.
Scenario: Start from non-git directory
- GIVEN the binary is run outside a git repository
- WHEN
startis passed - THEN it SHALL fail with stderr containing “Not a git repository”
Test: cli_tests::start_from_non_git_dir
Scenario: Unknown subcommand from binary
- GIVEN the binary is passed an unrecognized subcommand
- WHEN it runs
- THEN it SHALL fail with stderr containing “error”
Test: cli_tests::unknown_subcommand_fails
Requirement: Replay subcommand
The replay subcommand SHALL accept an optional <branch> positional argument, a --list flag, a --color flag, and an optional --session flag.
Scenario: Replay with branch
- WHEN
replay feat/add-authis passed - THEN
branchSHALL beSome("feat/add-auth"),listSHALL befalse,colorSHALL befalse
Scenario: Replay with –list
- WHEN
replay --listis passed - THEN
listSHALL betrueandbranchSHALL beNone
Scenario: Replay with –color
- WHEN
replay feat/add-auth --coloris passed - THEN
colorSHALL betrue
Scenario: Replay with –session
- WHEN
replay feat/add-auth --session paw-myprojectis passed - THEN
sessionSHALL beSome("paw-myproject")
Scenario: Replay with no arguments and no –list
- WHEN
replayis passed with no arguments and no--list - THEN parsing SHALL fail with an error indicating either a branch or
--listis required
Scenario: Replay help text
- WHEN
replay --helpis passed - THEN stdout SHALL contain descriptions of
--list,--color, and--sessionflags with examples
Requirement: Init subcommand
The init subcommand SHALL parse with no required arguments.
Scenario: Init parses
- WHEN
initis passed - THEN the command SHALL be
Command::Init
Scenario: Init help text
- WHEN
init --helpis passed - THEN stdout SHALL contain a description of project initialization and examples
Requirement: Spec mode + supervisor mode dispatch
The start subcommand dispatch SHALL evaluate the supervisor-mode resolution chain (per supervisor-cli) BEFORE branching on --from-specs. Specifically:
- Resolve supervisor-mode-enabled state from the
--supervisorflag,--no-supervisorflag,[supervisor]config, and the prompt fallback. - If supervisor mode is enabled, route to
cmd_supervisor. When--from-specsis also set, passbranches_flag = Nonesocmd_supervisor’s existingscan_specs(...)fallback runs to determine branches from configured specs. - Otherwise, if
--from-specsis set, route tocmd_start_from_specs. - Otherwise, route to bare
cmd_start.
This ordering ensures --from-specs --supervisor (or --from-specs with [supervisor] enabled = true in config) actually engages supervisor mode end-to-end, rather than silently degrading to spec-mode-without-supervisor.
--from-specs combined with --branches continues to follow v0.4’s existing behaviour (the spec-mode flow ignores explicit branches when from-specs is set); this change does not introduce a new mutual-exclusion error for that combination.
Scenario: –from-specs –supervisor engages supervisor mode
- GIVEN
git paw start --from-specs --supervisoris invoked - WHEN the dispatch resolves
- THEN the supervisor-mode resolution chain SHALL evaluate
supervisor = true - AND the dispatch SHALL route to
cmd_supervisor - AND
cmd_supervisorSHALL receivebranches_flag = None, triggering itsscan_specs(...)fallback
Scenario: –from-specs without supervisor uses spec mode
- GIVEN
git paw start --from-specsis invoked, no--supervisorflag, and[supervisor]config indicates supervisor mode is not enabled (either explicitly false or absent + non-interactive) - WHEN the dispatch resolves
- THEN the dispatch SHALL route to
cmd_start_from_specs
Scenario: –from-specs with [supervisor] enabled = true config engages supervisor mode
- GIVEN
git paw start --from-specsis invoked with no--supervisorflag - AND
.git-paw/config.tomlcontains[supervisor] enabled = true - WHEN the dispatch resolves
- THEN supervisor mode SHALL be active per the resolution chain
- AND the dispatch SHALL route to
cmd_supervisor(notcmd_start_from_specs)
Scenario: –no-supervisor –from-specs uses spec mode
- GIVEN
git paw start --from-specs --no-supervisoris invoked - AND
[supervisor] enabled = trueis set in config - WHEN the dispatch resolves
- THEN supervisor mode SHALL be disabled per the resolution chain
- AND the dispatch SHALL route to
cmd_start_from_specs
Scenario: Bare start (no –from-specs, no supervisor) uses cmd_start
- GIVEN
git paw startis invoked with no--from-specs, no--supervisor, and supervisor mode is not enabled in config - WHEN the dispatch resolves
- THEN the dispatch SHALL route to
cmd_start
Requirement: Non-TTY launch handling
When a git paw start invocation reaches its session-launch step (after worktrees are created, panes added, and tmux_session.execute() succeeds), the system SHALL detect whether stdin is connected to a terminal via std::io::IsTerminal::is_terminal(&std::io::stdin()).
When stdin is not a terminal:
- The system SHALL skip the
tmux::attach(...)call. - The system SHALL print an informational message to stdout naming the launched session and the manual-attach command (
tmux attach -t <session>). - The system SHALL exit with status
0. - For supervisor mode specifically, the system SHALL also skip the foreground supervisor-CLI launch (
Command::new(supervisor_cli).status()) with an additional hint that supervisor mode requires an interactive terminal.
When stdin is a terminal, the launch flow proceeds as before (call tmux::attach, run the supervisor CLI in foreground for supervisor mode).
This SHALL apply to all three start paths: cmd_start, cmd_start_from_specs, and cmd_supervisor.
Scenario: Non-TTY bare start exits cleanly with attach hint
- GIVEN
git paw start --branches feat/x,feat/yis invoked with stdin redirected from/dev/null(or otherwise non-TTY) - WHEN the launch flow completes its session-build steps
- THEN the command SHALL exit with status
0 - AND stdout SHALL contain “Session ‘
’ started in detached mode.” - AND stdout SHALL contain “Attach with: tmux attach -t
” - AND the tmux session SHALL exist and be alive after exit
Scenario: Non-TTY –from-specs exits cleanly
- GIVEN
git paw start --from-specsis invoked from a non-TTY context - WHEN the launch flow completes
- THEN the command SHALL exit with status
0 - AND the attach-hint message SHALL be printed
- AND the tmux session SHALL exist and be alive
Scenario: Non-TTY –supervisor skips supervisor CLI launch
- GIVEN
git paw start --supervisor --from-specsis invoked from a non-TTY context - WHEN the launch flow completes
- THEN the command SHALL exit with status
0 - AND the foreground supervisor-CLI launch SHALL be skipped
- AND stdout SHALL contain a hint indicating supervisor mode requires an interactive terminal
- AND stdout SHALL contain the manual-attach command for the launched session
Scenario: TTY launch attaches as before
- GIVEN
git paw start --branches feat/x,feat/yis invoked from a real TTY - WHEN the launch flow completes its session-build steps
- THEN the system SHALL call
tmux::attach(...)for the launched session - AND SHALL NOT print the “started in detached mode” hint
Requirement: –from-all-specs flag
The start subcommand SHALL accept a --from-all-specs flag (boolean, default false). When passed, the resulting StartArgs SHALL indicate the “launch every discovered spec” mode — the v0.4 behaviour previously gated by --from-specs.
The flag SHALL appear in git paw start --help output with a description naming it as the canonical name for this behaviour.
Scenario: –from-all-specs sets the launch-all mode
- GIVEN the user invokes
git paw start --from-all-specs - WHEN the CLI is parsed
- THEN the parsed
StartArgsSHALL indicate the launch-all-discovered-specs mode
Scenario: –from-all-specs combined with –supervisor
- GIVEN
start --from-all-specs --supervisor - WHEN the CLI is parsed
- THEN both
from_all_specsandsupervisorSHALL betrue
Scenario: –from-all-specs appears in help output
- WHEN
git paw start --helpis run - THEN the output contains
--from-all-specs - AND the output describes the flag as launching every discovered spec
Requirement: –from-specs is a hidden alias of –from-all-specs
The start subcommand SHALL accept --from-specs as a hidden alias of --from-all-specs. When the user passes --from-specs, the parsed StartArgs SHALL be byte-for-byte identical to the parse result for --from-all-specs. No stderr warning SHALL be emitted at runtime; the alias is silent.
The alias SHALL NOT appear in git paw start --help output. The alias SHALL be removed in v1.0.0; v0.5.0 keeps it for backward compatibility with v0.4 scripts.
Scenario: –from-specs parses identically to –from-all-specs
- GIVEN two CLI invocations:
start --from-specsandstart --from-all-specs - WHEN both are parsed
- THEN the resulting
StartArgsvalues SHALL be equal
Scenario: –from-specs does not appear in help
- WHEN
git paw start --helpis run - THEN the output SHALL NOT contain the substring
--from-specs
Scenario: –from-specs emits no stderr warning
- GIVEN the user runs a command containing
--from-specs - WHEN the CLI parses
- THEN no stderr warning SHALL be emitted regarding the flag’s deprecation
- AND the command proceeds exactly as if
--from-all-specshad been passed
Requirement: –specs flag with comma-separated values
The start subcommand SHALL accept a --specs flag whose value is a comma-separated list of spec names (mirroring the existing --branches feat/a,feat/b syntax). The flag SHALL accept zero or more values:
--specs(no values) — indicates the picker mode.--specs NAME— narrows to a single named spec.--specs NAME1,NAME2,NAME3— narrows to the listed specs.--specs NAME1,NAME2 --specs NAME3— equivalent to--specs NAME1,NAME2,NAME3if clap’s value-accumulation across repetitions is enabled (implementation choice; tests assert behaviour for the comma-separated form).
The parsed value distinguishes three states:
- Flag absent → no spec mode requested.
- Flag present with zero values → picker mode.
- Flag present with one or more values → narrow mode with the listed names.
The flag SHALL appear in git paw start --help output.
Scenario: –specs with single value parses as narrow
- GIVEN
start --specs add-auth - WHEN the CLI is parsed
- THEN
StartArgsSHALL indicate narrow mode with["add-auth"]
Scenario: –specs with comma-separated values parses as narrow with multiple names
- GIVEN
start --specs add-auth,fix-session,add-logging - WHEN the CLI is parsed
- THEN
StartArgsSHALL indicate narrow mode with["add-auth", "fix-session", "add-logging"]
Scenario: –specs with no values parses as picker
- GIVEN
start --specs - WHEN the CLI is parsed
- THEN
StartArgsSHALL indicate picker mode
Scenario: –specs absent leaves spec mode unset
- GIVEN
start --supervisor(no--specs, no--from-all-specs) - WHEN the CLI is parsed
- THEN
StartArgsSHALL indicate no spec mode (falls through to standard branch selection)
Requirement: –from-all-specs and –specs are mutually exclusive
The system SHALL reject any invocation that combines --from-all-specs (or its alias --from-specs) with --specs. clap’s parse step SHALL produce an error before the command runs. The error message SHALL clearly state that the two flags express opposing intents and SHALL list both flags.
Scenario: –from-all-specs and –specs together are rejected
- GIVEN
start --from-all-specs --specs add-auth - WHEN the CLI is parsed
- THEN parsing SHALL fail with an error mentioning both
--from-all-specsand--specs
Scenario: –from-specs alias and –specs together are also rejected
- GIVEN
start --from-specs --specs add-auth - WHEN the CLI is parsed
- THEN parsing SHALL fail with an error mentioning both flags
- AND the alias SHALL enforce the same mutual-exclusion rule as the canonical name
Requirement: –no-supervisor flag
The start subcommand SHALL accept a --no-supervisor flag (boolean, default false). When passed, the parsed StartArgs SHALL have no_supervisor: bool set to true. The flag SHALL appear in git paw start --help output with a description that names the use case (overriding [supervisor] enabled = true for a single session).
Scenario: –no-supervisor sets the flag
- GIVEN the user invokes
git paw start --no-supervisor - WHEN the CLI is parsed
- THEN the parsed
StartArgs.no_supervisorSHALL betrue - AND
StartArgs.supervisorSHALL befalse
Scenario: –no-supervisor absent leaves flag false
- GIVEN the user invokes
git paw startwith neither--supervisornor--no-supervisor - WHEN the CLI is parsed
- THEN
StartArgs.no_supervisorSHALL befalse - AND
StartArgs.supervisorSHALL befalse
Scenario: –no-supervisor appears in help output
- WHEN
git paw start --helpis run - THEN the output contains
--no-supervisor - AND the output describes the flag as disabling supervisor for the session and overriding any
[supervisor] enabled = trueconfig setting
Requirement: –supervisor and –no-supervisor are mutually exclusive
The system SHALL reject any invocation that combines --supervisor and --no-supervisor on the same start command. clap’s parse step SHALL produce an error before the command runs. The error message SHALL clearly state that the two flags express opposing intents and SHALL list both.
Scenario: Both flags together are rejected
- GIVEN
start --supervisor --no-supervisor - WHEN the CLI is parsed
- THEN parsing SHALL fail with an error mentioning both
--supervisorand--no-supervisor
Scenario: –no-supervisor combines with other flags
- GIVEN
start --no-supervisor --cli claude --branches feat/a,feat/b - WHEN the CLI is parsed
- THEN
no_supervisorSHALL betrue - AND
cliSHALL beSome("claude") - AND
branchesSHALL containfeat/aandfeat/b - AND parsing SHALL succeed
Requirement: Pause subcommand
The pause subcommand SHALL parse with no additional arguments and SHALL be visible in git paw --help output. The subcommand SHALL include an about string (“Pause the session (detaches client, stops broker, leaves CLIs running)”) and a long_about string that names the RAM trade-off and points the reader at stop and (forthcoming v1.0.0) hibernate for the destructive and RAM-free alternatives respectively.
The pause subcommand SHALL appear in the root after_help quick-start guide alongside start, stop, and purge.
Scenario: Pause parses
- GIVEN
pauseis passed - WHEN the CLI is parsed
- THEN the command SHALL be
Command::Pause
Scenario: Pause accepts no flags
- GIVEN
pause --anythingis passed (any flag) - WHEN the CLI is parsed
- THEN parsing SHALL fail with an unknown-argument error
Scenario: Pause appears in help
- WHEN
git paw --helpis run - THEN the output SHALL list a
pausesubcommand - AND the output SHALL include the
pauseline in the quick-startafter_helpblock
Scenario: Pause help text names the RAM trade-off
- WHEN
git paw pause --helpis run - THEN the output SHALL mention that CLI processes remain running
- AND the output SHALL mention the RAM-allocation trade-off (or words conveying “RAM stays held”)
- AND the output SHALL suggest
git paw stopfor the RAM-releasing alternative
Requirement: git paw purge interactive confirmation SHALL honour y+Enter under all conditions
The cmd_purge interactive confirmation prompt SHALL be reliably readable by the dialoguer Confirm widget regardless of preceding stderr output. When the unmerged-commits warning has been written to stderr immediately before the prompt, the warning writer SHALL flush stderr before the prompt’s interact() call begins, so the user’s y+Enter input is not racing the warning’s buffered bytes.
Scenario: Purge with unmerged commits and y+Enter proceeds
- GIVEN a session with at least one branch carrying commits not in
main - AND
git paw purgeis invoked from a TTY - WHEN the prompt “Purge is irreversible. Continue?” appears and the user types
yfollowed by Enter - THEN the purge SHALL proceed (kill tmux session + remove worktrees + delete session JSON)
- AND the exit code SHALL be 0
- AND stdout SHALL contain
Purged session 'paw-...'
Scenario: Purge with unmerged commits and n+Enter cancels
- GIVEN same setup as above
- WHEN the user types
nfollowed by Enter - THEN the purge SHALL NOT proceed
- AND the exit code SHALL be 0
- AND stdout SHALL contain
Purge cancelled. - AND the session worktrees SHALL still be on disk
Scenario: Purge with bare Enter (no y/n) defaults to no
- GIVEN same setup
- WHEN the user types Enter without first typing
yorn - THEN the prompt SHALL default to false (No)
- AND the purge SHALL NOT proceed
- AND stdout SHALL contain
Purge cancelled.
Requirement: git paw purge --force SHALL propagate --force to git worktree remove and emit per-worktree progress
When git paw purge is invoked with --force, the underlying git worktree remove invocations SHALL pass --force so the removal succeeds on worktrees with uncommitted changes. The command SHALL also emit per-worktree progress messages to stderr (e.g. Removing worktree <path>... before each removal and done (<elapsed>s) after) so the user can distinguish a slow-but-progressing removal from an actual hang.
Scenario: purge --force removes dirty worktrees
- GIVEN a session with one worktree containing uncommitted edits
- WHEN
git paw purge --forceis invoked - THEN the dirty worktree SHALL be removed successfully
- AND the exit code SHALL be 0
- AND the underlying
git worktree removeinvocation SHALL include the--forceflag
Scenario: purge --force emits per-worktree progress to stderr
- GIVEN a session with two or more worktrees
- WHEN
git paw purge --forceis invoked - THEN stderr SHALL contain a
Removing worktree <path>...line for each worktree being removed - AND stderr SHALL contain a
doneor completion marker after each removal - AND the order SHALL match the worktree iteration order
Scenario: purge without --force does NOT pass --force to git worktree remove
- GIVEN a session with one worktree containing uncommitted edits
- WHEN
git paw purge(no--force) is invoked and the user confirms withy - THEN the underlying
git worktree removeSHALL NOT include the--forceflag - AND if
git worktree removefails because of the dirty state, the failure SHALL be reported to stderr aswarning: failed to remove worktree '<path>': <git error>per the existing error-handling path - AND purge SHALL continue with the remaining worktrees
Requirement: git paw stop and git paw purge SHALL strip the supervisor boot-block injection from AGENTS.md
cmd_stop and cmd_purge (src/main.rs) SHALL invoke a helper that removes the supervisor-pane boot-block injection block from <repo>/AGENTS.md. The block is bounded by HTML comment markers <!-- git-paw:start --> … <!-- git-paw:end --> (or similar — the actual marker strings are owned by the injection code path and SHALL match exactly). The helper SHALL be idempotent and SHALL preserve all surrounding content byte-for-byte.
Scenario: Stop strips the boot-block injection
- GIVEN a session in which
cmd_supervisororcmd_startinjected a<!-- git-paw:start -->…<!-- git-paw:end -->block intoAGENTS.md - WHEN
git paw stop(with or without--force) is invoked - AND the teardown completes successfully
- THEN the resulting
AGENTS.mdSHALL contain no<!-- git-paw:start -->marker - AND no
<!-- git-paw:end -->marker
Scenario: Purge strips the boot-block injection
- GIVEN the same setup
- WHEN
git paw purge(with or without--force) is invoked - THEN
AGENTS.mdSHALL contain neither marker after the purge completes
Scenario: Stop/purge on AGENTS.md without markers is a no-op
- GIVEN an
AGENTS.mdwith no<!-- git-paw:start -->marker - WHEN
git paw stoporgit paw purgeruns the cleanup helper - THEN
AGENTS.mdSHALL be byte-identical to its pre-cleanup state - AND the helper SHALL return success
Requirement: git paw init SHALL be idempotent and additive on existing config files
src/init.rs::run_init SHALL parse the existing .git-paw/config.toml (if any) and compare its top-level keys/tables against the bundled-default schema. The init flow SHALL append commented stanzas ONLY for keys/tables missing from the user’s config. It SHALL NEVER:
- Modify the value of an existing key.
- Add a second occurrence of any top-level table (e.g. a second
[supervisor]) when the user already has that section commented OR uncommented. - Re-order or reformat existing keys/sections.
- Strip existing user comments or blank lines.
When every bundled-default key is already present in the user’s config, init SHALL print config.toml already has all default keys; no changes and return Ok without writing.
Init invocations SHALL be idempotent: running git paw init a second time on a config that the first run produced SHALL leave the file byte-identical.
Scenario: First init writes a complete commented default config
- GIVEN a fresh repo with no
.git-paw/config.toml - WHEN
git paw initis invoked - THEN the file SHALL be created
- AND SHALL parse as valid TOML
- AND SHALL contain commented stanzas for every bundled-default top-level key/section
Scenario: Second init on the just-written file is a no-op
- GIVEN the same repo after the first init
- WHEN
git paw initis invoked again - THEN the file SHALL be byte-identical to the first-run output
- AND the exit SHALL be 0
Scenario: Init preserves a user-authored [supervisor] block
- GIVEN a
.git-paw/config.tomlcontaining only:[supervisor] enabled = true cli = "claude-oss" - WHEN
git paw initis invoked - THEN the resulting file SHALL contain
enabled = trueandcli = "claude-oss"byte-identical to the input - AND SHALL NOT contain a second
[supervisor]section header (commented or uncommented) - AND SHALL parse as valid TOML (no
duplicate keyerrors)
Scenario: Init appends missing top-level sections
- GIVEN a
.git-paw/config.tomlcontaining onlybranch_prefix = "feat/" - WHEN
git paw initis invoked - THEN the resulting file SHALL preserve
branch_prefix = "feat/"byte-identical - AND SHALL gain commented stanzas for every bundled-default section the user is missing (
[broker],[dashboard],[supervisor], etc.) - AND SHALL parse as valid TOML
Scenario: Init never modifies existing user values
- GIVEN a
.git-paw/config.tomlwith[broker] port = 9200(non-default port) - WHEN
git paw initis invoked - THEN the resulting file SHALL still have
port = 9200 - AND SHALL NOT introduce a second
portkey or a commented# port = 9119stanza inside[broker]
Requirement: –unattended flag
The start subcommand SHALL accept an --unattended flag (boolean, default false). When passed, the parsed StartArgs SHALL have unattended: bool set to true. When omitted, unattended SHALL be false.
The flag SHALL appear in git paw start --help output with a description naming the use case: drive a supervisor wave to completion in-process (escalate every permission prompt for a human decision without blocking the rest of the wave, detect completion, exit with a summary). The help text SHALL NOT claim that the flag approves prompts, and its long help SHALL state that prompt-free unattended runs need the agent CLI’s own allow rules or the full-auto approval level inside a sandbox.
--unattended engages supervisor mode: when --unattended is passed, the supervisor-mode resolution chain SHALL resolve supervisor mode active (equivalent to --supervisor) so the dispatch routes to the supervisor launch path. --unattended MAY be combined with --supervisor, --from-all-specs (and its alias --from-specs), --specs, --cli, and --branches.
--unattended SHALL NOT be combined with --no-supervisor; the system SHALL reject any invocation that combines --unattended with --no-supervisor with a parse error naming both flags, because they express opposing intents.
Scenario: –unattended sets the flag
- GIVEN the user invokes
git paw start --unattended - WHEN the CLI is parsed
- THEN the parsed
StartArgs.unattendedSHALL betrue
Scenario: –unattended absent leaves flag false
- GIVEN the user invokes
git paw startwithout--unattended - WHEN the CLI is parsed
- THEN
StartArgs.unattendedSHALL befalse
Scenario: –unattended combines with other flags
- GIVEN
start --unattended --from-specs --cli claude - WHEN the CLI is parsed
- THEN
unattendedSHALL betrue - AND the spec-mode launch-all state SHALL be set
- AND
cliSHALL beSome("claude") - AND parsing SHALL succeed
Scenario: –unattended appears in help output
- WHEN
git paw start --helpis run - THEN the output SHALL contain
--unattended - AND the output SHALL describe the flag as driving the wave to completion and escalating permission prompts
- AND the output SHALL NOT describe the flag as auto-approving prompts
Scenario: –unattended engages supervisor mode
- GIVEN
git paw start --unattended --branches feat/a,feat/bis invoked - WHEN the dispatch resolves the supervisor-mode resolution chain
- THEN supervisor mode SHALL resolve active
- AND the dispatch SHALL route to the supervisor launch path (
cmd_supervisor)
Scenario: –unattended with –no-supervisor is rejected
- GIVEN
start --unattended --no-supervisor - WHEN the CLI is parsed
- THEN parsing SHALL fail with an error mentioning both
--unattendedand--no-supervisor
Requirement: Force flag definition
The git paw start command SHALL accept an optional --force boolean flag that defaults to false. The flag SHALL appear in git paw start --help output with descriptive text explaining that it bypasses the uncommitted-spec validation warning. The flag SHALL parse according to Clap’s boolean flag rules.
Scenario: Force flag is parsed by clap
- GIVEN the
git paw startcommand - WHEN the user invokes
git paw start --from-specs --force - THEN the parsed
Startarguments exposeforce = true - AND when invoked without
--force,force = false
Scenario: Force flag appears in help text
- WHEN the user runs
git paw start --help - THEN the output contains a description for
--forcementioning uncommitted-spec validation
Requirement: Validation bypass with force flag
When --force is provided, the command SHALL proceed with execution even if uncommitted spec changes are detected. When --force is NOT provided and uncommitted specs exist, the command SHALL display a warning that lists the IDs of specs with uncommitted changes and SHALL continue execution. Uncommitted-spec detection SHALL iterate through pending specs from scan_specs(), check each spec’s directory using git status --porcelain, and collect IDs of specs with non-empty git status output.
Scenario: No uncommitted specs
- GIVEN all pending specs are committed in git
- WHEN the user runs
git paw start --from-specs - THEN the command proceeds normally without any uncommitted-spec warning
Scenario: Uncommitted specs without force
- GIVEN at least one pending spec has uncommitted changes per
git status --porcelain - WHEN the user runs
git paw start --from-specs - THEN stderr contains a warning listing the IDs of specs with uncommitted changes
- AND the command continues execution
Scenario: Uncommitted specs with force
- GIVEN at least one pending spec has uncommitted changes
- WHEN the user runs
git paw start --from-specs --force - THEN the uncommitted-spec warning is suppressed
- AND the command logs force usage to stderr for audit purposes
- AND the command continues execution
Scenario: Force combined with dry run
- GIVEN at least one pending spec has uncommitted changes
- WHEN the user runs
git paw start --from-specs --force --dry-run - THEN dry-run output is shown without the uncommitted-spec warning
Requirement: Force flag error handling
When checking spec git status, the command SHALL propagate git status failures as PawError::GitError. Invalid spec paths SHALL be skipped without aborting the overall validation.
Scenario: Git status failure is reported as PawError
- GIVEN a spec directory where
git status --porcelainexits non-zero - WHEN uncommitted-spec validation runs
- THEN the command returns a
PawError::GitError
Requirement: –specs list honoured under –supervisor
The dispatcher SHALL pass the --specs flag’s resolved value
(not just its presence) to the supervisor launch path. When the
user invokes git paw start --supervisor --specs <names>, the
system SHALL launch worktrees ONLY for the named subset of
discovered specs, matching the non-supervisor --specs
behaviour exactly.
Scenario: –supervisor –specs subset filters correctly
- GIVEN an
openspec/changes/directory containing N discovered specs (N > 2) - WHEN the user runs
git paw start --supervisor --specs a,bwhereaandbare exactly two of the N specs - THEN the launch SHALL create exactly two worktrees
(for
aandb) and SHALL NOT create worktrees for any other spec in the directory
Scenario: –supervisor –from-all-specs still launches all
- GIVEN the same N discovered specs
- WHEN the user runs
git paw start --supervisor --from-all-specs - THEN the launch SHALL create worktrees for all N discovered specs (current behaviour preserved)
Scenario: –supervisor with no spec flag uses branch picker
- WHEN the user runs
git paw start --supervisorwith neither--specsnor--from-all-specs - THEN the launch SHALL behave identically to
git paw start(branch picker) but with supervisor mode enabled — no automatic discovery of every spec
Scenario: –supervisor –specs (picker, no values) opens interactive picker
- GIVEN an interactive TTY
- WHEN the user runs
git paw start --supervisor --specswith no values - THEN the system SHALL open the multi-select picker; selected specs alone SHALL determine the launched worktrees
Scenario: –supervisor + –specs without –from-all-specs
- GIVEN N discovered specs in
openspec/changes/ - WHEN the user runs
git paw start --supervisor --specs cold-start-ci-parity(a single named spec) - THEN exactly one worktree SHALL be created (for
cold-start-ci-parity); no worktree SHALL exist for any other spec
Requirement: Regression matrix for selection-flag combinations
The test suite SHALL include a matrix of integration tests
covering every combination of selection flag and supervisor
flag. The matrix SHALL include at minimum the 15 combinations
of {none, --from-all-specs, --specs <names>, --specs (picker), --branches} × {no supervisor, --supervisor, --no-supervisor}.
Scenario: Matrix passes for every combination
- WHEN the integration matrix runs
- THEN every cell SHALL pass — each combination produces the documented worktree set, and no combination produces worktrees for specs outside the filter
Scenario: Matrix catches a regression in any cell
- GIVEN a hypothetical regression in any cell (e.g.
someone re-introduces the v0.6.0 bug where
--supervisor --specs <list>launches all discovered specs) - WHEN the matrix runs
- THEN the failing cell’s test SHALL fail with a message identifying the offending flag combination
Requirement: Stack-agnostic and backend-agnostic
The filter behaviour SHALL be identical across all configured spec backends (OpenSpec, Markdown, Spec Kit). The bug fix SHALL NOT introduce new backend-specific code paths.
Scenario: Same filter behaviour on Markdown backend
- GIVEN a Markdown-backend repo with N specs
- WHEN the user runs
git paw start --supervisor --specs a - THEN the launch SHALL create exactly one worktree for
spec
a, matching the OpenSpec-backend behaviour
Scenario: Same filter behaviour on Spec Kit backend
- GIVEN a Spec Kit
.specify/specs/repo with N features - WHEN the user runs
git paw start --supervisor --specs 003-feature-name - THEN the launch SHALL create exactly one worktree for that feature
Requirement: Spec-driven launch chapter uses the canonical flag name
docs/src/user-guide/spec-driven-launch.md SHALL use
--from-all-specs as the canonical flag name in every example.
The hidden v0.4 alias --from-specs SHALL NOT appear in
example command lines in the chapter (per the
cross-format-spec-selection archive’s documentation policy,
the alias is intentionally undocumented to nudge
migration).
Scenario: Spec-driven launch chapter uses –from-all-specs
- WHEN
docs/src/user-guide/spec-driven-launch.mdis inspected - THEN every example command launching all discovered specs
uses
--from-all-specs - AND no example command line uses
--from-specs
Requirement: CLI reference lists every top-level subcommand
docs/src/cli-reference.md’s top-level commands list SHALL
include every public subcommand. Currently the list SHALL
include (at minimum) start, stop, purge, status,
list-clis, add-cli, remove-cli, init, and replay. The
v0.4 omission of init and replay SHALL be repaired.
Scenario: CLI reference lists init
- WHEN the top-level commands section of
docs/src/cli-reference.mdis inspected - THEN it contains the substring
init
Scenario: CLI reference lists replay
- WHEN the top-level commands section of
docs/src/cli-reference.mdis inspected - THEN it contains the substring
replay
Requirement: CLI reference start-flag table lists every flag
docs/src/cli-reference.md’s start-flag table SHALL include
every flag defined in src/cli.rs::StartArgs. Currently the
table SHALL include (at minimum) --cli, --branches,
--dry-run, --preset, --from-all-specs, --specs,
--specs-format, --supervisor, --no-supervisor, and
--force.
Scenario: CLI reference includes –specs-format
- WHEN the start-flag table in
docs/src/cli-reference.mdis inspected - THEN it contains the substring
--specs-format
Scenario: CLI reference includes –no-supervisor
- WHEN the start-flag table in
docs/src/cli-reference.mdis inspected - THEN it contains the substring
--no-supervisor
Scenario: CLI reference includes –from-all-specs and –specs
- WHEN the start-flag table in
docs/src/cli-reference.mdis inspected - THEN it contains the substring
--from-all-specs - AND it contains the substring
--specs(as a distinct flag, not as a prefix of--specs-format)
Scenario: CLI reference includes –force
- WHEN the start-flag table in
docs/src/cli-reference.mdis inspected - THEN it contains the substring
--force
Requirement: Attach subcommand
The attach subcommand SHALL reattach the current terminal to the running tmux session for the current repository, resolving the session by the repository path (as status does) and invoking the tmux attach path. When no session exists for the repository, or the session is not alive, it SHALL exit with an actionable error naming that no session is running and how to start one. This provides a first-class git paw attach in place of the raw tmux attach -t paw-<project>.
Scenario: Attach reattaches to the repository’s running session
- GIVEN a running session for the current repository
- WHEN the user runs
git paw attach - THEN the system SHALL attach the terminal to that session
Scenario: Attach with no running session errors actionably
- GIVEN no running session exists for the current repository
- WHEN the user runs
git paw attach - THEN the system SHALL exit with an error stating no session is running and how to start one
Requirement: Completions subcommand
The completions subcommand SHALL print a shell-completion script for a requested shell (bash, zsh, fish) to stdout, generated from the clap command definition, so a user can install completions through their shell’s standard mechanism. A missing or unsupported shell argument SHALL produce an actionable error.
Scenario: Completions prints a script for a supported shell
- WHEN the user runs
git paw completions bash - THEN the system SHALL print a bash completion script to stdout and exit successfully
Scenario: Completions rejects an unsupported shell
- WHEN the user runs
git paw completions <unsupported> - THEN the system SHALL exit with an actionable error naming the supported shells
Requirement: No resume subcommand; reattach is attach, revival is start
The CLI SHALL NOT provide a resume subcommand. Reattaching the current terminal to a running session SHALL be git paw attach; reviving a paused or stopped session SHALL remain git paw start. The documentation (CLI reference, README, user guide) SHALL NOT reference a git paw resume command.
Scenario: git paw resume is not a valid subcommand
- WHEN the user runs
git paw resume - THEN the system SHALL reject it as an unknown subcommand
Scenario: Docs do not reference a resume command
- WHEN the CLI reference and user guide are inspected
- THEN they SHALL NOT reference a
git paw resumecommand; reattach is documented asgit paw attachand session revival asgit paw start
Requirement: Purge –all-repos flag for a machine-wide stale sweep
git paw purge SHALL accept an --all-repos flag that broadens the --stale sweep from the current repository (the default) to every repository with a receipt on the machine. --all-repos SHALL be meaningful only in combination with --stale; used without --stale it SHALL be rejected with an actionable error (or treated as inert) per the CLI’s flag rules. The machine-wide sweep SHALL still only ever purge stale receipts and SHALL never purge a live session.
Scenario: –all-repos requires –stale
- WHEN the user runs
git paw purge --all-reposwithout--stale - THEN the CLI SHALL reject the combination with an actionable error (or treat
--all-reposas inert), per the flag rules
Scenario: –stale –all-repos broadens the sweep
- GIVEN stale receipts in multiple repositories
- WHEN the user runs
git paw purge --stale --all-repos - THEN the sweep SHALL cover every repository’s stale receipts
- AND live sessions SHALL remain intact
CLI Detection
cli-resolution Specification
Purpose
Detect available AI coding CLI binaries by scanning PATH for known names and merging with user-defined custom CLIs from configuration, providing a unified, deduplicated, sorted list, and resolve which CLI to use for each spec-driven branch using a multi-level priority chain that considers command-line flags, per-spec overrides, config defaults, and interactive selection, prompting the user at most once. It also covers reliable boot-block submission and CLI-pane launch hardening: injecting the prompt text literally and then sending Enter as a separate keystroke after a settle delay resolved per CLI from [clis.<name>].submit_delay_ms (with a single CLI-agnostic default and no hardcoded CLI names), clearing the shell input line before the launch command, suppressing known auto-update/confirmation prompts in the launched pane’s environment, and verifying the CLI started within a bounded window and retrying the launch once on failure — so a fresh supervisor session boots and all agents self-register unattended.
Requirements
Requirement: Auto-detect known AI CLIs on PATH
The system SHALL scan PATH for the known CLI binaries: claude, codex, agy, aider, vibe, qwen, amp, opencode, cline, droid, pi, junie, cursor-agent, copilot, cn, kilo, and kimi.
agy is the Antigravity CLI (display name “Antigravity”), which replaces the retired Gemini CLI (gemini) — Gemini CLI stops serving free / AI Pro / AI Ultra users on 2026-06-18. Removing gemini affects auto-detection only; an explicit default_cli = "gemini", a [clis.gemini] custom entry, or a saved-session cli string still resolves as a pass-through.
cursor-agent is Cursor’s agent CLI (display name “Cursor Agent”). The bare cursor binary is the Cursor editor launcher on most installations and SHALL NOT be auto-detected as an AI CLI. Removing cursor affects auto-detection only: an explicit git paw start --cli cursor, a [clis.cursor] custom entry, or a saved-session cli = "cursor" still resolves as a pass-through. The paths that validate a CLI against the detected roster — spec-driven selection, git paw add, and the interactive picker (which default_cli only pre-selects within) — require a [clis.cursor] entry.
Scenario: All known CLIs are present on PATH
- GIVEN all 17 known CLI binaries exist on PATH
- WHEN
detect_known_clis()is called - THEN it SHALL return a
CliInfofor each binary withsource = Detected, a non-emptydisplay_name, and a validpath
Test: detect::tests::all_known_clis_detected_when_present
Scenario: No known CLIs are present on PATH
- GIVEN PATH contains no known CLI binaries
- WHEN
detect_known_clis()is called - THEN it SHALL return an empty list
Test: detect::tests::returns_empty_when_no_known_clis_on_path
Scenario: Partial set of CLIs on PATH
- GIVEN only a subset of known CLIs exist on PATH
- WHEN
detect_known_clis()is called - THEN it SHALL return only the CLIs that are found
Test: detect::tests::detects_subset_of_known_clis
Scenario: Antigravity CLI is detected under its agy binary name
- GIVEN the
agybinary exists on PATH - WHEN
detect_known_clis()is called - THEN the result SHALL include a
CliInfowithbinary_name = "agy"andsource = Detected - AND the result SHALL NOT include a CLI with
binary_name = "gemini"unlessgeminiis supplied as a custom[clis.*]entry
Test: detect::tests::agy_detected_and_gemini_not_known
Scenario: Cursor’s agent CLI is detected under cursor-agent, not the editor launcher
- GIVEN both a
cursorand acursor-agentbinary exist on PATH - WHEN
detect_known_clis()is called - THEN the result SHALL include a
CliInfowithbinary_name = "cursor-agent",display_name = "Cursor Agent", andsource = Detected - AND the result SHALL NOT include a CLI with
binary_name = "cursor"
Test: detect::tests::cursor_agent_detected_and_editor_launcher_not_known
Scenario: An explicit cursor CLI still passes through
- GIVEN no
cursorentry is auto-detected and no[clis.cursor]entry is configured - WHEN a bare session is started with
--cli cursor - THEN the branch-to-CLI mapping SHALL carry
cursorunchanged - AND the pane SHALL be launched with
cursor
Test: interactive::resolver::tests::explicit_cursor_cli_passes_through_without_detection, config::tests::launch_command_falls_back_to_name_without_entry
Requirement: Resolve and merge custom CLIs from configuration
The system SHALL resolve custom CLI definitions by looking up commands as absolute paths or via PATH, and merge them with auto-detected CLIs.
Scenario: Custom CLIs merged with detected CLIs
- GIVEN auto-detected CLIs exist and custom CLI definitions are provided
- WHEN
detect_clis()is called - THEN the result SHALL contain both detected and custom CLIs
Test: detect::tests::custom_clis_merged_with_detected
Scenario: Custom CLI binary not found
- GIVEN a custom CLI definition references a non-existent binary
- WHEN
detect_clis()is called - THEN the missing CLI SHALL be excluded and a warning printed to stderr
Test: detect::tests::custom_cli_excluded_when_binary_missing
Scenario: Custom CLI resolved by absolute path
- GIVEN a custom CLI definition uses an absolute path to an existing binary
- WHEN
resolve_custom_clis()is called - THEN the resolved path SHALL match the absolute path provided
Test: detect::tests::custom_cli_resolved_by_absolute_path
Requirement: Custom CLIs override detected CLIs with the same name
When a custom CLI has the same binary_name as a detected CLI, the custom definition SHALL take precedence.
Scenario: Custom CLI overrides auto-detected CLI
- GIVEN a custom CLI shares a
binary_namewith an auto-detected CLI - WHEN
detect_clis()is called - THEN the result SHALL contain only the custom version with
source = Custom
Test: detect::tests::custom_cli_overrides_detected_with_same_binary_name
Requirement: Each CLI result includes all required fields
Every CliInfo SHALL have a non-empty display_name, binary_name, a valid path, and a source indicator.
Scenario: Detected CLI has all fields populated
- GIVEN a known CLI binary exists on PATH
- WHEN it is detected
- THEN all fields (
display_name,binary_name,path,source) SHALL be populated
Test: detect::tests::detected_cli_has_all_fields
Scenario: Custom CLI has all fields populated
- GIVEN a custom CLI definition is resolved
- WHEN it is included in results
- THEN all fields SHALL be populated
Test: detect::tests::custom_cli_has_all_fields
Requirement: Display name derivation
When no explicit display name is provided, the system SHALL derive one by capitalizing the first letter of the binary name.
Scenario: Custom CLI defaults to capitalized name
- GIVEN a custom CLI definition has no
display_name - WHEN it is resolved
- THEN the
display_nameSHALL be the binary name with the first letter capitalized
Test: detect::tests::custom_cli_display_name_defaults_to_capitalised_name
Requirement: Results sorted by display name
The combined CLI list SHALL be sorted alphabetically by display_name (case-insensitive).
Scenario: Results are sorted
- GIVEN multiple CLIs are detected and/or custom
- WHEN
detect_clis()is called - THEN the results SHALL be sorted by display name
Test: detect::tests::results_sorted_by_display_name
Requirement: CliSource display format
The CliSource enum SHALL display as "detected" or "custom".
Scenario: CliSource display strings
- GIVEN
CliSource::DetectedandCliSource::Custom - WHEN formatted with
Display - THEN they SHALL render as
"detected"and"custom"respectively
Test: detect::tests::cli_source_display_format
Requirement: CLI resolution chain for spec-driven launches
The system SHALL resolve which CLI to use for each spec-driven branch using a 5-level priority chain, from highest to lowest priority. This chain SHALL apply identically to non-supervisor and supervisor-mode spec-driven launches — the --supervisor flag SHALL NOT bypass or alter it. In particular, default_spec_cli SHALL be honoured for spec branches lacking a paw_cli override even when --supervisor is combined with --specs, so supervisor-mode workers are never silently launched with a different (e.g. unsandboxed) CLI than configured.
Scenario: –cli flag overrides everything
- WHEN
--cli claudeis passed and specs have variouspaw_clivalues - THEN all branches SHALL use
"claude"regardless of spec or config values
Scenario: paw_cli in spec overrides config
- WHEN no
--cliflag is passed and a spec haspaw_cli: gemini - THEN that branch SHALL use
"gemini"regardless ofdefault_spec_cliordefault_cli
Scenario: default_spec_cli fills remaining without prompt
- WHEN no
--cliflag, some specs have nopaw_cli, anddefault_spec_cli = "claude"in config - THEN specs without
paw_cliSHALL use"claude"with no interactive prompt
Scenario: default_cli pre-selects in picker
- WHEN no
--cliflag, nopaw_cli, nodefault_spec_cli, anddefault_cli = "claude"in config - THEN the CLI picker SHALL be shown with
"claude"pre-selected
Scenario: No defaults — full picker
- WHEN no
--cliflag, nopaw_cli, nodefault_spec_cli, and nodefault_cli - THEN the CLI picker SHALL be shown with no pre-selection
Scenario: default_spec_cli is honoured under –supervisor –specs
- WHEN
git paw start --supervisor --specs a,bruns with no--cli, specsaandbhaving nopaw_cli, anddefault_spec_cli = "claude-oss"in config - THEN the
aandbworktree panes SHALL launch the"claude-oss"CLI (not a fallback or default CLI), identical to the non-supervisor--specsresolution
Requirement: Mixed resolution across specs
The system SHALL handle specs where some have paw_cli and others don’t in the same launch.
Scenario: Mix of paw_cli and default_spec_cli
- WHEN 3 specs are launched, 1 has
paw_cli: gemini, anddefault_spec_cli = "claude" - THEN the gemini spec SHALL use
"gemini"and the other 2 SHALL use"claude"
Scenario: Mix of paw_cli and interactive
- WHEN 3 specs are launched, 1 has
paw_cli: gemini, nodefault_spec_cli, and user picks"claude"in the prompt - THEN the gemini spec SHALL use
"gemini"and the other 2 SHALL use"claude"
Requirement: Prompt at most once
The system SHALL prompt the user for CLI selection at most once during a --from-specs launch, applying the choice to all branches without a paw_cli or default_spec_cli.
Scenario: Single prompt for remaining branches
- WHEN 5 specs are launched, 2 have
paw_cli, and the picker fires for the remaining 3 - THEN the picker SHALL fire once and the chosen CLI SHALL be applied to all 3
Requirement: Validate resolved CLI names
The system SHALL validate that each resolved CLI name matches an available CLI.
Scenario: paw_cli references unknown CLI
- WHEN a spec has
paw_cli: nonexistentand no CLI named"nonexistent"is available - THEN the system SHALL return
PawError::CliNotFound("nonexistent")
Scenario: default_spec_cli references unknown CLI
- WHEN
default_spec_cli = "nonexistent"and no such CLI is available - THEN the system SHALL return
PawError::CliNotFound("nonexistent")
Scenario: –cli flag references unknown CLI
- GIVEN specs exist
- WHEN
--cli nonexistentis passed and “nonexistent” is not in available CLIs - THEN the system SHALL return
PawError::CliNotFound("nonexistent")
Requirement: No prompt when fully resolved
The system SHALL not show any interactive prompt when all branches are resolved via --cli, paw_cli, or default_spec_cli.
Scenario: All resolved without prompt
- WHEN
--cli claudeis passed - THEN no interactive prompt SHALL be shown
Scenario: All resolved via paw_cli and default_spec_cli
- WHEN every spec has
paw_cliordefault_spec_clicovers the rest - THEN no interactive prompt SHALL be shown
Requirement: Boot prompt submitted via split-send + settle delay
The boot-injection path SHALL inject the boot block into a pane
literally and then submit it with a SEPARATE Enter sent after
a settle delay, rather than a same-call trailing Enter. This
split is what reliably submits a large paste across CLIs
(W15-1: a same-call trailing Enter left the boot block
unsubmitted on a custom CLI). The mechanism SHALL contain no
hardcoded CLI names.
Scenario: Boot block is injected then submitted separately
- WHEN a boot block is injected into a pane
- THEN the system SHALL first send the prompt text
(literally, no
Enter), then after the settle delay sendEnteras a separate keystroke
Scenario: Mechanism is CLI-name-free
- WHEN the submit path is inspected
- THEN it SHALL NOT branch on any specific CLI name — the same split-send applies to every CLI
Requirement: Settle delay is config-driven with an agnostic default
The settle delay SHALL be resolved per CLI from
[clis.<name>].submit_delay_ms, falling back to a single
CLI-agnostic default (DEFAULT_SUBMIT_DELAY_MS) for any CLI
without an override. The resolver SHALL key on the leading
binary token of the CLI command (so a CLI string carrying
flags still matches its config entry).
Scenario: Unconfigured CLI uses the agnostic default
- GIVEN a CLI with no
[clis.<name>].submit_delay_msconfigured (or no[clis.<name>]entry at all) - WHEN the settle delay is resolved
- THEN it SHALL equal
DEFAULT_SUBMIT_DELAY_MS
Scenario: Per-CLI override is honoured
- GIVEN
[clis.mycli].submit_delay_ms = 2500 - WHEN the settle delay for
mycliis resolved - THEN it SHALL be 2500
Scenario: Resolver keys on the binary, not the flags
- GIVEN
[clis.mycli].submit_delay_ms = 2500 - WHEN the delay is resolved for the CLI command
"mycli --some-flag" - THEN it SHALL be 2500 (the leading token
myclimatched the config entry)
Scenario: No CLI name resolves to a hardcoded value
- GIVEN an empty
[clis]config - WHEN the delay is resolved for any CLI id (including names that might otherwise be special-cased)
- THEN every CLI SHALL resolve to the same
DEFAULT_SUBMIT_DELAY_MS— there is no built-in per-name table
Requirement: Profile applies to supervisor and agent panes
The split-send + resolved delay SHALL apply to every launched coding-agent pane, each resolved from that agent’s own CLI. No orchestrator pane exists in the session, so no boot block is submitted for an orchestrator CLI; the orchestrator’s CLI is resolved only for git-paw-initiated headless invocations, never for a pane.
Scenario: Coding-agent boot blocks are submitted with their own CLI’s delay
- GIVEN a supervised session with two coding agents running different CLIs
- WHEN each agent’s boot block is injected
- THEN each SHALL be submitted via the split-send using that agent’s CLI’s resolved delay, so the agent begins work without a manual
Enter
Scenario: No pane receives an orchestrator boot block
- GIVEN a supervised session with
[supervisor] cliset - WHEN the launch injects boot blocks
- THEN no split-send SHALL target a pane with the orchestrator CLI’s resolved delay
Requirement: End-to-end boot registration
The system SHALL boot a fresh supervisor session such that all
coding agents register with the broker without a human Enter,
for any CLI given an adequate settle delay (default or
configured). The agents’ boot publish goes through the bundled
broker.sh helper at its stable path. Whether that call runs
without a permission prompt is decided by the agent CLI’s own
permission system: its resolved permission mode, or an allow
rule for the helper’s exact path in that CLI’s settings.
Registration SHALL NOT depend on a git-paw command classifier.
When the CLI prompts for the helper, git-paw SHALL NOT answer
the prompt; under --unattended the drive loop escalates it
(supervisor-unattended-operation).
Scenario: All agents register unattended
- GIVEN a fresh supervisor session with N agents and
broker enabled, using a CLI whose settle delay is adequate
and whose permission system lets the
broker.shhelper run - WHEN the session launches
- THEN within a bounded window the broker
/statusSHALL list all N coding agents (plus the supervisor) with no humanEnteror permission approval required
Scenario: A prompted boot publish is escalated, not answered
- GIVEN an
--unattendedsession whose agent CLI raises a permission prompt for itsbroker.shboot publish - WHEN the drive loop sweeps that pane
- THEN the prompt SHALL be escalated
- AND no keystroke SHALL be sent to that pane
Requirement: Clean the shell input line before the CLI-launch command
The system SHALL ensure a pane’s shell input line is clean before sending
the CLI-launch command — by sending a clearing keystroke (e.g. C-u/C-c)
and/or a leading newline — so a pending shell startup prompt (auto-update
confirmation, MOTD, etc.) cannot swallow the leading character of the launch
command and strand the pane at a bare shell.
Scenario: Launch keystroke is not corrupted by a startup prompt
- GIVEN a pane whose interactive shell shows a startup prompt (e.g.
[oh-my-zsh] Would you like to update? [Y/n]) at launch time - WHEN git-paw sends the CLI-launch command
- THEN the pane SHALL clear the pending prompt first so the full launch
command (not a keystroke-truncated variant like
laude-oss) reaches the shell and the CLI starts
Requirement: Suppress shell startup prompts in the launched pane
The system SHALL suppress known shell auto-update / confirmation prompts in
the pane it launches where it controls the pane environment (e.g. exporting
DISABLE_AUTO_UPDATE=true or the equivalent), so such a prompt cannot fire
mid-launch. The system SHALL NOT modify the user’s global shell
configuration.
Scenario: Auto-update prompt suppressed for the launched pane
- WHEN git-paw launches a CLI pane
- THEN it SHALL set the pane environment so the shell’s auto-update
prompt does not fire during launch, without editing the user’s
~/.zshrcor global oh-my-zsh settings
Requirement: Verify the CLI started and retry once
The system SHALL verify, within a bounded window after the launch keystroke, that the pane’s CLI actually started (the shell prompt was replaced by the CLI), and SHALL retry the launch once if the first attempt did not take.
Scenario: Failed launch is retried
- GIVEN a pane where the first CLI-launch attempt did not start the CLI (the shell prompt is still present after the bounded window)
- THEN git-paw SHALL send the launch command once more before giving up, so a single swallowed attempt does not permanently strand the pane
Requirement: README Supported AI CLIs table matches src/detect.rs
The README’s Supported AI CLIs table SHALL list every CLI binary in src/detect.rs’s known-CLI roster, and SHALL NOT list a binary that the roster does not auto-detect. The mdBook docs/src/supported-clis.md table SHALL carry the same binary set.
Scenario: README CLI table mentions opencode, cline, and droid
- WHEN the README’s Supported AI CLIs table is inspected
- THEN it contains the substring
opencode - AND it contains the substring
cline - AND it contains the substring
droid
Test: tests/supported_clis_docs_sync.rs::readme_and_book_tables_match_known_cli_roster (opencode, cline, and droid are roster members, so the per-binary missing-cell assertion covers them)
Scenario: README and mdBook CLI tables list every roster binary and only those
- WHEN the README’s Supported AI CLIs table and
docs/src/supported-clis.md’s table are inspected - THEN each SHALL contain a
`<binary>`cell for every binary in the known-CLI roster, including`cursor-agent` - AND neither SHALL contain a
`cursor`binary cell
Test: tests/supported_clis_docs_sync.rs::readme_and_book_tables_match_known_cli_roster
Requirement: Per-CLI session-id and resume argument templates
The [clis.<name>] table SHALL accept two optional string templates, session_id_args (arguments
that assign a session identifier at launch) and resume_args (arguments that resume an existing
session), each containing the placeholder {{SESSION_ID}}. Resolution for a CLI SHALL consult, in
order: (1) the [clis.<name>] override when present; (2) the built-in table; (3) no template. The
built-in table SHALL contain, at minimum, claude → session_id_args = "--session-id {{SESSION_ID}}"
and resume_args = "--resume {{SESSION_ID}}"; every CLI without a built-in row and without an override
SHALL resolve to no template. A template that omits {{SESSION_ID}} SHALL be rejected at config load
with an error naming the key. The built-in rows SHALL be verified against the CLI’s upstream
documentation at implementation time.
Scenario: Claude resolves the built-in templates
- WHEN templates are resolved for
claudewith no config override - THEN
session_id_argsSHALL render to--session-id <id>andresume_argsto--resume <id>for the given id
Scenario: Override wins over the built-in row
- GIVEN
[clis.claude]definingresume_args = "--continue-session {{SESSION_ID}}" - WHEN the resume template is resolved for
claude - THEN it SHALL render to
--continue-session <id>
Scenario: Unknown CLI resolves no template
- WHEN templates are resolved for
my-agentwith no[clis.my-agent]templates - THEN both
session_id_argsandresume_argsSHALL resolve to none
Scenario: Template without the placeholder is rejected
- GIVEN
[clis.my-agent]definingsession_id_args = "--session"(no{{SESSION_ID}}) - WHEN the config is loaded
- THEN loading SHALL fail with an error naming
session_id_args
Requirement: Worker session id is assigned at launch
The system SHALL assign each worker a session identifier at launch: when the launch flow attaches a
worker (initial start, spec-driven start, git paw add) and the worker’s CLI resolves a
session_id_args template, it SHALL generate a fresh version-4-formatted UUID from OS-provided
randomness, append the rendered session_id_args to the CLI launch command after any approval flags,
and record the id in the worktree’s cli_session_id. When the CLI resolves no
session_id_args template, the launch command SHALL be unchanged and cli_session_id SHALL remain
None. The identifier SHALL be generated without adding a dependency.
Scenario: Claude worker is launched with an assigned id
- GIVEN a worker on
feat/xwithcli = "claude"and no override - WHEN the launch command is built
- THEN it SHALL contain
--session-id <uuid>where<uuid>matches the v4 UUID shape - AND the saved
WorktreeEntryforfeat/xSHALL havecli_session_id = Some(<uuid>)
Scenario: Each worker gets a distinct id
- GIVEN three Claude workers launched in one session
- WHEN their
cli_session_idvalues are compared - THEN all three SHALL differ
Scenario: CLI without an assignment template launches unchanged
- GIVEN a worker with
cli = "codex"and no[clis.codex].session_id_args - WHEN the launch command is built
- THEN it SHALL be identical to the pre-change command
- AND the saved entry’s
cli_session_idSHALL beNone
Scenario: Assignment follows approval flags
- GIVEN a Claude worker at
agent_approval = "full-auto" - WHEN the launch command is built
- THEN
--dangerously-skip-permissionsSHALL precede--session-id <uuid>in the command
Requirement: Panes launch the configured command of a CLI
Every launch path that spawns an AI CLI into a tmux pane — the bare start, the spec-driven start, the supervisor pane, git paw add, and session recovery (worker and supervisor panes) — SHALL build the pane’s launch command from the CLI’s resolved [clis.<name>].command, not from the [clis.<name>] key. The leading whitespace-delimited token of the CLI identity is looked up in the merged [clis] table; when it names an entry, that token SHALL be replaced by the entry’s command and any trailing text SHALL be preserved verbatim. When it names no entry (a built-in detected CLI, or an explicit pass-through name), the identity SHALL be used unchanged.
The substituted command SHALL be emitted as exactly one shell word that the pane’s shell resolves to the configured string: it SHALL be single-quoted (with embedded single quotes escaped) whenever it contains any character outside the portable safe set A-Z a-z 0-9 _ - . / + , : @ %, so a path containing spaces or shell metacharacters is neither word-split nor expanded. Approval flags and session-id / resume arguments SHALL be appended after the substituted command exactly as they are appended after the bare CLI today.
Scenario: A custom CLI whose key differs from its command launches the command
- GIVEN
[clis.claude-variant] command = "claude" - WHEN a session is started with a worker on
feat/xusing CLIclaude-variant - THEN the pane launch command SHALL begin with
claude - AND it SHALL NOT contain
claude-variant
Test: tests/cli_launch_identity_e2e.rs::worker_pane_launches_configured_command_not_key
Scenario: A CLI without a [clis] entry launches by name
- GIVEN no
[clis.codex]entry - WHEN a worker is attached with CLI
codex - THEN the pane launch command SHALL be
codexexactly as before this change
Test: config::tests::launch_command_falls_back_to_name_without_entry
Scenario: A command path containing a space is one shell word
- GIVEN
[clis.my-agent] command = "/opt/My Tools/my-agent"and an executable stub at that path - WHEN the launch command is built and run through a real POSIX shell
- THEN the launch command SHALL begin with
'/opt/My Tools/my-agent' - AND the shell SHALL execute the stub at that exact path (not
/opt/Mywith an argument)
Test: config::tests::launch_command_quotes_path_with_space_as_one_shell_word
Scenario: Trailing text after the identity is preserved
- GIVEN
[clis.claude-oss] command = "/usr/local/bin/claude" - WHEN the launch command is built for the identity
claude-oss --model opus - THEN it SHALL be
/usr/local/bin/claude --model opus
Test: config::tests::launch_command_preserves_trailing_text
Scenario: Supervisor pane launches the configured command
- GIVEN
[supervisor] cli = "claude-oss"and[clis.claude-oss] command = "claude"withapproval_argsresolving a non-empty flag for the supervisor’s approval level - WHEN the supervisor session launches
- THEN pane 0’s launch command SHALL be
claudefollowed by that flag
Test: tests/cli_launch_identity_e2e.rs::supervisor_pane_launches_configured_command
Scenario: Recovery relaunches the configured command
- GIVEN a saved session whose worktree entry has
cli = "claude-oss"andcli_session_id = Some(<id>), and[clis.claude-oss] command = "claude"with a resolvableresume_args - WHEN the session is recovered
- THEN the worker pane’s launch command SHALL begin with
claudefollowed by the rendered resume arguments
Test: tests/cli_launch_identity_e2e.rs::recovery_launches_configured_command_with_resume_args
Requirement: The CLI identity, not the launch command, keys per-CLI resolution
The [clis.<name>] key (or the built-in / pass-through CLI name) SHALL remain the CLI’s identity. Approval-flag resolution, session-id and resume template resolution, prompt-profile resolution (including the readiness gate’s choice of readiness markers), settle-delay resolution, MCP-capability resolution, and the cli recorded in each saved WorktreeEntry SHALL all be keyed by that identity, never by the substituted launch command. The readiness gate SHALL relaunch the substituted launch command when a pane is still a bare shell.
Scenario: Saved worktree entry records the key
- GIVEN
[clis.claude-oss] command = "claude" - WHEN a session is started with a worker using CLI
claude-oss - THEN the saved session’s
WorktreeEntry.clifor that worker SHALL beclaude-oss
Test: tests/cli_launch_identity_e2e.rs::saved_session_records_cli_key
Scenario: Per-CLI overrides resolve against the key
- GIVEN
[clis.claude-oss] command = "claude"withapproval_args = { auto = "--paw-oss-sentinel" } - WHEN a session is started with a worker using CLI
claude-ossatagent_approval = "auto" - THEN the pane launch command SHALL be
claude --paw-oss-sentinel
Test: tests/cli_launch_identity_e2e.rs::worker_overrides_resolve_by_key_and_launch_command
Scenario: Readiness gate uses the identity’s prompt profile
- GIVEN
[clis.claude-oss] command = "claude"with aprompt_profile.readiness_markersoverride - WHEN the readiness gate polls the pane for CLI identity
claude-oss - THEN it SHALL classify readiness using the override’s markers
- AND a bare-shell relaunch SHALL send the launch command
claude
Test: tmux::tests::gate_pane_resolves_profile_by_identity_and_relaunches_command
Requirement: Per-CLI headless invocation template
The [clis.<name>] table SHALL accept an optional string headless_args giving the arguments that make the CLI run one non-interactive turn reading its prompt from stdin. Resolution for a CLI SHALL consult, in order: (1) the [clis.<name>] override; (2) the built-in table, which SHALL contain at minimum claude → -p and codex → exec; (3) none. The built-in rows SHALL be verified against each CLI’s upstream documentation at implementation time. An empty override string SHALL be rejected at config load with an error naming the key.
Scenario: Claude and Codex resolve through the built-in table
- WHEN
headless_argsis resolved forclaudeand forcodexwith no overrides - THEN the results SHALL be
-pandexecrespectively
Scenario: Override wins over the built-in row
- GIVEN
[clis.claude]definingheadless_args = "-p --output-format text" - WHEN
headless_argsis resolved forclaude - THEN the result SHALL be
-p --output-format text
Scenario: Unknown CLI resolves none
- WHEN
headless_argsis resolved formy-agentwith no[clis.my-agent].headless_args - THEN the result SHALL be none
Scenario: Empty override is rejected
- GIVEN
[clis.my-agent] headless_args = "" - WHEN the config is loaded
- THEN loading SHALL fail with an error naming
headless_args
Git Operations
Purpose
Validate git repositories, list branches, create and remove worktrees, and derive worktree directory names. Provides the git plumbing that underpins parallel branch sessions.
Requirements
Requirement: Validate that a path is inside a git repository
The system SHALL confirm a path is inside a git repository and return the repository root.
Scenario: Path is inside a git repository
- GIVEN a path inside an initialized git repository
- WHEN
validate_repo()is called - THEN it SHALL return
Okwith the absolute path to the repository root
Test: git::tests::validate_repo_returns_root_inside_repo
Scenario: Path is not inside a git repository
- GIVEN a path that is not inside any git repository
- WHEN
validate_repo()is called - THEN it SHALL return
Err(PawError::NotAGitRepo)
Test: git::tests::validate_repo_returns_not_a_git_repo_outside
Requirement: List branches sorted and deduplicated
The system SHALL list all local and remote branches, deduplicated, sorted, with remote prefixes stripped and HEAD pointers excluded.
Scenario: Branches are returned sorted
- GIVEN a repository with multiple branches
- WHEN
list_branches()is called - THEN it SHALL return branches sorted alphabetically
Test: git::tests::list_branches_returns_sorted_branches
Scenario: Local and remote branches are deduplicated with prefix stripping
- GIVEN a repository cloned from a remote, with branches existing both locally and as remote-tracking refs
- WHEN
list_branches()is called - THEN each branch SHALL appear exactly once, with
origin/prefixes stripped
Test: git_integration::list_branches_strips_remote_prefix_and_deduplicates
Requirement: Derive project name from repository path
The system SHALL extract the project name from the final component of the repository root path, falling back to "project" for root paths.
Scenario: Normal repository path
- GIVEN a repository at
/Users/jie/code/git-paw - WHEN
project_name()is called - THEN it SHALL return
"git-paw"
Test: git::tests::project_name_from_path
Scenario: Root path fallback
- GIVEN a repository at
/ - WHEN
project_name()is called - THEN it SHALL return
"project"
Test: git::tests::project_name_fallback_for_root
Requirement: Build worktree directory names
The system SHALL generate worktree directory names as <project>-<sanitized-branch>, replacing / with - and stripping unsafe characters.
Scenario: Branch with single slash
- GIVEN project
"git-paw"and branch"feature/auth-flow" - WHEN
worktree_dir_name()is called - THEN it SHALL return
"git-paw-feature-auth-flow"
Test: git::tests::worktree_dir_name_replaces_slash_with_dash
Scenario: Branch with multiple slashes
- GIVEN project
"git-paw"and branch"feat/auth/v2" - WHEN
worktree_dir_name()is called - THEN it SHALL return
"git-paw-feat-auth-v2"
Test: git::tests::worktree_dir_name_handles_multiple_slashes
Scenario: Branch with special characters
- GIVEN project
"my-proj"and branch"fix/issue#42" - WHEN
worktree_dir_name()is called - THEN unsafe characters SHALL be stripped, returning
"my-proj-fix-issue42"
Test: git::tests::worktree_dir_name_strips_special_chars
Scenario: Simple branch name
- GIVEN project
"git-paw"and branch"main" - WHEN
worktree_dir_name()is called - THEN it SHALL return
"git-paw-main"
Test: git::tests::worktree_dir_name_simple_branch
Requirement: Create worktrees as siblings of the repository
The system SHALL create git worktrees at a path resolved from the
configured worktree_placement setting. When worktree_placement is
"child", the target path SHALL be
<repo_root>/.git-paw/worktrees/<branch-slug> and the
.git-paw/worktrees/ directory SHALL be created if absent. When
worktree_placement is "sibling" OR the setting is absent, the target
path SHALL be <repo_parent>/<project>-<branch-slug> using the derived
directory name convention, matching the v0.7.0 sibling layout. Only the
resolved target path varies with placement; all other behaviour described
below is unchanged.
The create_worktree function SHALL accept a rebase_onto_main: bool parameter. When rebase_onto_main is true AND the target branch already exists in the local repository, the function SHALL rebase the target branch onto the repository’s default branch (as returned by default_branch()) BEFORE performing the existence check for the worktree directory. The rebase SHALL be performed by invoking git rebase <default-branch> from the repository root. When the branch is already at or ahead of the default branch, git rebase exits zero with no rewrite; the function SHALL treat that as success.
If git rebase exits non-zero (rebase conflict or any other failure), the function SHALL invoke git rebase --abort in the repository root and return Err(PawError::WorktreeError("rebase onto main failed: <stderr>")). The branch SHALL be left at its pre-rebase HEAD after the abort; the function SHALL NOT proceed to the existence check or git worktree add when the rebase failed.
If rebase_onto_main is false, the function SHALL skip the rebase block entirely and behave identically to the post-worktree-resume-fix v0.5.0 contract (idempotent existence check followed by git worktree add).
If the target branch does NOT exist in the local repository at the time create_worktree is invoked, the function SHALL skip the rebase block regardless of the rebase_onto_main value and proceed to the existing git worktree add -b <branch> fallback, which creates the branch from current HEAD (already at the default branch tip by construction).
The system SHALL be idempotent in the resume case: when create_worktree() is invoked for a branch whose worktree already exists at the expected path AND is registered with git for that branch, the function SHALL return Ok(WorktreeCreation { path, branch_created: false }) without re-running git worktree add. Idempotency is verified by parsing git worktree list --porcelain output and matching both the worktree path and the refs/heads/<branch> line. When rebase_onto_main is true, the rebase block runs BEFORE this idempotency check, so a surviving worktree’s branch ref SHALL be updated to the rebased SHA before the function returns.
If the expected path exists on disk but is NOT a git worktree registered for the specified branch (e.g. an unrelated directory, or a worktree for a different branch), the function SHALL fall through to the existing git worktree add call so the user sees the actionable fatal: '<path>' already exists error from git directly.
Scenario: Worktree created at correct path
- GIVEN a repository with a branch
feature/test - WHEN
create_worktree()is called withrebase_onto_main = false - THEN a worktree SHALL be created at
../<project>-feature-testcontaining the repository files
Scenario: Worktree created under child placement
- GIVEN a repository whose effective config has
worktree_placement = "child"and a branchfeature/test - WHEN
create_worktree()is called withrebase_onto_main = false - THEN a worktree SHALL be created at
<repo_root>/.git-paw/worktrees/feature-testcontaining the repository files
Scenario: Worktree created under sibling placement
- GIVEN a repository whose effective config has
worktree_placement = "sibling"and a branchfeature/test - WHEN
create_worktree()is called withrebase_onto_main = false - THEN a worktree SHALL be created at
../<project>-feature-test
Scenario: Worktree created under absent placement defaults to sibling
- GIVEN a repository whose effective config has no
worktree_placementfield and a branchfeature/test - WHEN
create_worktree()is called withrebase_onto_main = false - THEN a worktree SHALL be created at
../<project>-feature-test
Scenario: Creating worktree for currently checked-out branch fails
- GIVEN the current branch is checked out in the main repo
- WHEN
create_worktree()is called for that branch - THEN it SHALL return
Err(PawError::WorktreeError)
Scenario: Resume of an existing worktree returns success without re-running git worktree add
- GIVEN a worktree already exists at
../<project>-feature-testfor branchfeature/testfrom a prior session - AND
rebase_onto_main = falseis passed - WHEN
create_worktree()is called forfeature/test - THEN the function SHALL return
Ok(WorktreeCreation { path: <expected>, branch_created: false }) - AND the existing worktree SHALL remain unchanged (HEAD SHA, working tree files, and uncommitted changes preserved)
- AND no second
git worktree addSHALL be executed
Scenario: Path exists but is not a git worktree
- GIVEN the expected worktree path
../<project>-feature-testexists as a regular directory (not registered with git) - WHEN
create_worktree()is called for branchfeature/test - THEN the function SHALL return
Err(PawError::WorktreeError)whose message contains the substringalready exists
Scenario: Path exists as a worktree but for a different branch
- GIVEN a worktree already exists at
../<project>-feature-testbut registered for branchfeature/other - WHEN
create_worktree()is called for branchfeature/test - THEN the function SHALL fall through to
git worktree addand returnErr(PawError::WorktreeError)(preserving the v0.4 contract for unrelated path collisions)
Scenario: Rebase-on-resume happy path advances branch onto current main
- GIVEN the default branch
mainhas advanced by N commits since branchfeat/examplewas created - AND
feat/exampleexists locally and is behindmainby exactly N commits with no diverging commits of its own - WHEN
create_worktree()is called forfeat/examplewithrebase_onto_main = true - THEN the function SHALL invoke
git rebase <main>againstfeat/examplein the repository root - AND the rebase SHALL succeed
- AND
feat/example’s HEAD SHA after the call SHALL be reachable frommain(i.e. include the N new commits) - AND the function SHALL return
Ok(WorktreeCreation { path: <expected>, branch_created: false })
Scenario: Rebase skipped when branch is already up-to-date
- GIVEN branch
feat/exampleexists locally and is at the same SHA asmain(no divergence) - WHEN
create_worktree()is called forfeat/examplewithrebase_onto_main = true - THEN
git rebase <main>SHALL be invoked and SHALL exit zero with no rewrite - AND
feat/example’s HEAD SHA SHALL be unchanged - AND the function SHALL return
Ok(WorktreeCreation { path: <expected>, branch_created: false }) - AND no error SHALL be returned
Scenario: Rebase conflict aborts cleanly and surfaces error
- GIVEN branch
feat/exampleandmainboth modify the same line of the same file with different content - WHEN
create_worktree()is called forfeat/examplewithrebase_onto_main = true - THEN
git rebase <main>SHALL be invoked and SHALL exit non-zero with conflict markers - AND the function SHALL invoke
git rebase --abort - AND
feat/example’s HEAD SHA after the call SHALL equal its pre-call HEAD SHA - AND no
.git/rebase-mergeor.git/rebase-applydirectory SHALL remain in the repository - AND the function SHALL return
Err(PawError::WorktreeError(msg))wheremsgcontains the substringrebase onto main failed - AND the worktree directory at
../<project>-feat-exampleSHALL NOT have been created (or, if it existed from a prior session, SHALL be unchanged)
Scenario: rebase_onto_main = false preserves v0.5 no-rebase behaviour
- GIVEN branch
feat/exampleexists locally and is behindmainby 3 commits - WHEN
create_worktree()is called forfeat/examplewithrebase_onto_main = false - THEN no
git rebaseinvocation SHALL occur - AND
feat/example’s HEAD SHA after the call SHALL equal its pre-call HEAD SHA - AND the function SHALL proceed to the existence check and (if applicable)
git worktree add, matching the post-worktree-resume-fixv0.5.0 contract exactly
Scenario: New branch creation skips rebase regardless of flag
- GIVEN branch
feat/newdoes NOT exist in the local repository - AND
rebase_onto_main = trueis passed - WHEN
create_worktree()is called forfeat/new - THEN no
git rebaseinvocation SHALL occur (there is nothing to rebase) - AND the function SHALL invoke
git worktree add -b feat/new <path>to create the branch from current HEAD - AND the function SHALL return
Ok(WorktreeCreation { path, branch_created: true })
Requirement: Remove worktrees and prune stale entries
The system SHALL force-remove a worktree and prune stale git worktree metadata. remove_worktree SHALL pass --force to git worktree remove so a worktree containing uncommitted modifications, untracked files, or both is still deleted; the function is only called from the destructive purge path, where leaving worktree directories on disk after the user already opted into a destructive operation is the wrong behaviour.
Scenario: Worktree fully cleaned up after removal
- GIVEN an existing worktree
- WHEN
remove_worktree()is called - THEN the directory SHALL be deleted and git SHALL no longer track it
Test: git::tests::remove_worktree_cleans_up_fully
Scenario: Dirty worktree is force-removed
- GIVEN an existing worktree containing both a modified tracked file and an untracked file
- WHEN
remove_worktree()is called - THEN the call SHALL succeed
- AND the worktree directory SHALL be deleted from disk
- AND git SHALL no longer track the worktree
Test: git_integration::remove_worktree_force_removes_dirty_worktree
Requirement: Repository validation SHALL work against real git repos
Scenario: Succeeds inside a real git repo
- GIVEN a temporary git repository with an initial commit
- WHEN
validate_repo()is called - THEN it SHALL return the canonicalized repo root
Test: git_integration::validate_repo_succeeds_inside_git_repo
Scenario: Fails outside a git repo
- GIVEN a temporary directory that is not a git repo
- WHEN
validate_repo()is called - THEN it SHALL return an error
Test: git_integration::validate_repo_fails_outside_git_repo
Requirement: Branch listing SHALL work against real git repos
Scenario: Lists created branches
- GIVEN a repo with branches
feature/authandfix/db - WHEN
list_branches()is called - THEN both branches SHALL appear in the result
Test: git_integration::list_branches_includes_created_branches
Scenario: Branches are sorted
- GIVEN branches created in non-alphabetical order
- WHEN
list_branches()is called - THEN results SHALL be alphabetically sorted
Test: git_integration::list_branches_returns_sorted
Scenario: Deduplicates local and remote
- GIVEN a repository with a default branch
- WHEN
list_branches()is called - THEN each branch SHALL appear exactly once
Test: git_integration::list_branches_deduplicates_local_and_remote
Requirement: Worktree lifecycle SHALL work against real git repos
Scenario: Create and remove worktree
- GIVEN a branch in a temporary repo
- WHEN
create_worktree()thenremove_worktree()are called - THEN the worktree SHALL exist after creation and be gone after removal
Test: git_integration::create_and_remove_worktree
Scenario: Worktree placed as sibling of repo
- GIVEN a repo at
<sandbox>/test-repo/ - WHEN
create_worktree()is called - THEN the worktree SHALL be in the same parent directory
Test: git_integration::worktree_placed_as_sibling_of_repo
Scenario: Fails for checked-out branch
- GIVEN the currently checked-out branch
- WHEN
create_worktree()is called for it - THEN it SHALL fail
Test: git_integration::create_worktree_fails_for_checked_out_branch
Requirement: Directory naming SHALL be correct in integration tests
Scenario: Project name from real repo path
- GIVEN a repo at
.../test-repo/ - WHEN
project_name()is called - THEN it SHALL return
"test-repo"
Test: git_integration::project_name_from_repo_path
Scenario: Worktree dir name replaces slashes
- WHEN
worktree_dir_name("my-project", "feature/auth-flow")is called - THEN it SHALL return
"my-project-feature-auth-flow"
Test: git_integration::worktree_dir_name_replaces_slashes
Scenario: Worktree dir name strips unsafe chars
- WHEN
worktree_dir_name("proj", "feat/special@chars!")is called - THEN
@and!SHALL be stripped
Test: git_integration::worktree_dir_name_strips_unsafe_chars
Scenario: Worktree dir name handles nested slashes
- WHEN
worktree_dir_name("proj", "feature/deep/nested/branch")is called - THEN it SHALL return
"proj-feature-deep-nested-branch"
Test: git_integration::worktree_dir_name_handles_nested_slashes
Requirement: Worktree creation produces a usable worktree path
The create_worktree function SHALL create a git worktree for the given branch and return its path. Callers MAY perform post-creation setup (such as AGENTS.md generation) using the returned path.
Scenario: Worktree created at correct path
- GIVEN a git repo and a branch name
- WHEN
create_worktree()is called - THEN it SHALL return the path to the new worktree as a sibling of the repo directory
Test: git::tests::create_worktree_at_correct_path
Scenario: Worktree creation fails for checked-out branch
- GIVEN a branch that is currently checked out
- WHEN
create_worktree()is called - THEN it SHALL return a
PawError::WorktreeError
Test: git::tests::create_worktree_errors_on_checked_out_branch
Requirement: Default-branch resolution is single-source and tolerant of a missing origin/HEAD
The system SHALL resolve the repository’s default branch through a single resolver used by every launch path (initial start, resume, add, and the rebase step), so no two paths can disagree. When refs/remotes/origin/HEAD is not set — e.g. a repository with local commits but no configured remote HEAD — the resolver SHALL fall back deterministically (a local main, else a local master, else the checked-out branch via git symbolic-ref --short HEAD) rather than aborting. A launch that succeeded initially SHALL therefore also succeed on resume in the same repository state.
Scenario: Resolution succeeds without origin/HEAD
- GIVEN a repository with local commits and no
refs/remotes/origin/HEADset - WHEN the default branch is resolved
- THEN it SHALL return a valid local default branch (via the fallback) rather than aborting with an error
Scenario: First launch and resume resolve identically
- GIVEN a repository whose first
git paw startresolved a default branch - WHEN the session is later resumed (a second
git paw start) in the same repository state - THEN the resume SHALL resolve the same default branch through the same resolver and SHALL NOT fail where the first launch succeeded
Scenario: origin/HEAD is still honoured when present
- GIVEN a repository with
refs/remotes/origin/HEADpointing atorigin/main - WHEN the default branch is resolved
- THEN it SHALL return
main(the configured remote HEAD), unchanged from prior behaviour
Requirement: Mid-session rebase of a live worker branch
The system SHALL provide a rebase entry point that rebases a named branch onto the repository’s default branch while that branch is checked out in a live worktree, separate from create_worktree’s creation-time rebase_onto_main path. The entry point SHALL run the rebase inside the worktree where the branch is currently checked out. On a non-zero git rebase exit it SHALL invoke git rebase --abort and return an error, leaving the branch at its pre-rebase HEAD. When the branch is already at or ahead of the default branch, git rebase exits zero with no rewrite and the entry point SHALL treat that as success.
The creation-time contract of create_worktree (including its rebase_onto_main parameter, its ordering relative to the existence check, and its idempotent resume behaviour) SHALL be unchanged by this addition.
Scenario: Rebase runs in the worktree holding the branch
- GIVEN a branch checked out in a live worktree
- WHEN the mid-session rebase entry point is invoked for that branch
- THEN the rebase SHALL be run inside that worktree
Scenario: Failed mid-session rebase aborts and restores
- GIVEN a mid-session rebase that exits non-zero
- WHEN the failure is detected
- THEN
git rebase --abortSHALL be invoked - AND the branch SHALL be left at its pre-rebase HEAD
- AND an error SHALL be returned
Scenario: Branch already current is a successful no-op
- GIVEN a branch already at or ahead of the default branch
- WHEN the mid-session rebase entry point is invoked for that branch
- THEN it SHALL return success
- AND the branch SHALL NOT be rewritten
Scenario: Creation-time rebase contract is unaffected
- WHEN
create_worktree()is called withrebase_onto_main = truefor an existing branch - THEN its behaviour SHALL be identical to the behaviour before the mid-session entry point was added
Tmux Orchestration
Purpose
Orchestrate tmux sessions with multiple panes, each running an AI CLI in a git worktree. Uses a builder pattern for testability and dry-run support, with configurable mouse mode and automatic tiled layout.
Requirements
Requirement: Check tmux availability with actionable error
The system SHALL verify tmux is installed on PATH and provide install instructions if missing.
Scenario: tmux is present on PATH
- GIVEN tmux is installed
- WHEN
ensure_tmux_installed()is called - THEN it SHALL return
Ok(())
Test: tmux::tests::ensure_tmux_installed_succeeds_when_present
Requirement: Create named sessions derived from project name
The system SHALL name tmux sessions as paw-<project_name>.
Scenario: Session named after project
- GIVEN project name
"my-project" - WHEN a session is built
- THEN the session name SHALL be
"paw-my-project"
Test: tmux::tests::session_is_named_after_project
Scenario: Session creation command uses correct name
- GIVEN project name
"app" - WHEN a session is built
- THEN the commands SHALL include
new-sessionwithpaw-app
Test: tmux::tests::session_creation_command_uses_session_name
Requirement: Session creation passes explicit dimensions for headless environments
Both session builders (the basic TmuxSessionBuilder and build_supervisor_session) SHALL emit tmux new-session with -x 200 -y 50 so the session has explicit window dimensions when created without an attached client. The user’s real terminal resizes the session on tmux attach.
Without explicit dimensions, tmux on Linux (apt-shipped tmux 3.4+) errors with size missing on subsequent split-window operations because the layout engine can’t resolve percentages without a known window size.
Additionally, immediately after the new-session command, both builders SHALL emit tmux set-option -g default-size 200x50. This pins the global default-size so subsequent split-window / resize-pane operations have a fallback size context even when no client is attached. macOS tmux honours per-session -x/-y for splits; Linux tmux 3.4+ requires the server-level fallback.
Scenario: Basic session passes -x/-y to new-session
- GIVEN any
TmuxSessionBuilderwith one or more panes - WHEN
command_strings()is invoked - THEN the first command in the output SHALL be a
new-sessioncontaining the substrings-x 200and-y 50
Scenario: Basic session sets global default-size after new-session
- GIVEN any
TmuxSessionBuilderwith one or more panes - WHEN
command_strings()is invoked - THEN the second command in the output SHALL be
set-option -g default-size 200x50
Scenario: Supervisor session passes -x/-y to new-session
- GIVEN a
build_supervisor_sessioninvocation with supervisor + dashboard + N agent panes - WHEN the command list is built
- THEN the first emitted command SHALL be a
new-sessioncontaining-x 200and-y 50
Scenario: Supervisor session sets global default-size after new-session
- GIVEN a
build_supervisor_sessioninvocation - WHEN the command list is built
- THEN the second emitted command SHALL be
set-option -g default-size 200x50
Scenario: Headless supervisor launch succeeds under socket isolation
- GIVEN a cold tmux server (no pre-existing client, isolated socket via
TMUX_TMPDIR) - WHEN
git paw start --supervisor --branches a,bis invoked - THEN the supervisor session SHALL launch successfully
- AND stderr SHALL NOT contain
Tmux error: size missing - AND the exit code SHALL be 0
Test: tmux::tests::built_session_can_be_executed_and_killed, tmux::tests::supervisor_top_row_split_50_50, and the cli_supervisor_no_config::supervisor_without_section_uses_default_when_default_cli_present integration test.
Scenario: Supervisor splits use -l <N>% syntax (tmux 3.1+) not deprecated -p <N>
- GIVEN a
build_supervisor_sessioninvocation with any agent count - WHEN the command list is built
- THEN every emitted
split-windowcommand SHALL use the-l <N>%length flag, NOT the deprecated-p <N>percentage flag
Rationale: Linux apt-tmux 3.4 (Ubuntu 24.04) emits cmd-split-window.c: "size missing" when -p cannot resolve the percentage against the parent pane’s laid-out size — on a detached server with no attached client the pane geometry is unresolved. -l <N>% resolves against the window’s -y dimension (set by new-session -x 200 -y 50) instead, which is well-defined in headless mode. macOS tmux 3.6a tolerates either form.
Test: tmux::tests::supervisor_splits_use_l_percent_not_p.
Requirement: Session name override via builder
The builder SHALL support overriding the default paw-<project> session name with a custom name.
Scenario: Override replaces default name
- GIVEN
session_name("custom-session-name")is set on the builder - WHEN the session is built
- THEN the session name SHALL be
"custom-session-name"and commands SHALL target it
Test: tmux::tests::session_name_override_replaces_default
Requirement: Dynamic pane count matches input
The number of panes in the session’s window 0 SHALL match the number of PaneSpec entries added via the builder. When broker is enabled, the builder SHALL receive an additional PaneSpec for the dashboard in pane 0, increasing the total pane count by one. A detached service window (the broker host’s broker window) is not part of this count.
Scenario: Two agent panes plus dashboard created
- GIVEN broker is enabled and 2 agent pane specs are added
- WHEN the session is built
- THEN exactly 3 panes SHALL exist in window 0: pane 0 (dashboard) + panes 1-2 (agents)
Scenario: Two panes without broker
- GIVEN broker is disabled and 2 pane specs are added
- WHEN the session is built
- THEN exactly 2 panes SHALL exist in window 0 (same as v0.2.0)
Requirement: Correct commands sent to each pane
Each pane SHALL receive a cd <worktree> && <cli_command> command targeting the correct pane index.
Scenario: Each pane receives cd and CLI command
- GIVEN two panes with different worktrees and CLIs
- WHEN the session is built
- THEN each
send-keyscommand SHALL containcd <worktree> && <cli>
Test: tmux::tests::each_pane_receives_cd_and_cli_command
Scenario: Commands are submitted with Enter
- GIVEN a pane spec
- WHEN the session is built
- THEN the
send-keyscommand SHALL includeEnter
Test: tmux::tests::pane_commands_are_submitted_with_enter
Scenario: Each pane targets a distinct index
- GIVEN 3 panes
- WHEN the session is built
- THEN
send-keysSHALL target:0.0,:0.1, and:0.2respectively
Test: tmux::tests::each_pane_targets_a_distinct_pane_index
Requirement: Pane titles show branch and CLI
Each pane SHALL be titled with <branch> → <cli_command> and border status configured.
Scenario: Pane titles contain branch and CLI
- GIVEN panes with branches and CLIs
- WHEN the session is built
- THEN
select-pane -Tcommands SHALL set titles like"feat/auth → claude"
Test: tmux::tests::each_pane_is_titled_with_branch_and_cli
Scenario: Pane border status configured
- GIVEN any session
- WHEN the session is built
- THEN
pane-border-statusSHALL be set totopandpane-border-formatSHALL use#{pane_title}
Test: tmux::tests::pane_border_status_is_configured
Requirement: Configurable mouse mode per session
Mouse mode SHALL be enabled by default and be disableable via the builder.
Scenario: Mouse mode enabled by default
- GIVEN no explicit mouse mode setting
- WHEN the session is built
- THEN a
mouse oncommand SHALL be emitted
Test: tmux::tests::mouse_mode_enabled_by_default
Scenario: Mouse mode can be disabled
- GIVEN
mouse_mode(false)is set on the builder - WHEN the session is built
- THEN no
mouse oncommand SHALL be emitted
Test: tmux::tests::mouse_mode_can_be_disabled
Requirement: Attach to a tmux session
The system SHALL attach the current terminal to a named tmux session, returning an error if the session does not exist.
Scenario: Attaching to a nonexistent session fails
- GIVEN no tmux session with the given name exists
- WHEN
attach()is called - THEN it SHALL return an error
Test: e2e_tests::attach_fails_for_nonexistent_session
Requirement: Session liveness check
The system SHALL check whether a tmux session is alive by name.
Scenario: Nonexistent session reports not alive
- GIVEN no tmux session with the queried name exists
- WHEN
is_session_alive()is called - THEN it SHALL return
false
Test: tmux::tests::is_session_alive_returns_false_for_nonexistent
Requirement: Session lifecycle management
The system SHALL support creating, checking, and killing tmux sessions.
Scenario: Full create-check-kill lifecycle
- GIVEN a tmux session is created
- WHEN
is_session_alive()is called, thenkill_session(), thenis_session_alive()again - THEN it SHALL be alive after creation and not alive after killing
Test: tmux::tests::session_lifecycle_create_check_kill
Scenario: Built session can be executed and killed
- GIVEN a session built via
TmuxSessionBuilder - WHEN
execute()is called - THEN the tmux session SHALL be alive, and after
kill_session()it SHALL be gone
Test: tmux::tests::built_session_can_be_executed_and_killed
Requirement: Session name collision resolution
The system SHALL resolve name collisions by appending -2, -3, etc. to the base session name.
Scenario: No collision returns base name
- GIVEN no existing session with the base name
- WHEN
resolve_session_name()is called - THEN it SHALL return
paw-<project_name>
Test: tmux::tests::resolve_session_name_returns_base_when_no_collision
Scenario: Collision appends numeric suffix
- GIVEN a session with the base name already exists
- WHEN
resolve_session_name()is called - THEN it SHALL return
paw-<project_name>-2
Test: tmux::tests::resolve_session_name_appends_suffix_on_collision
Requirement: Tmux session lifecycle SHALL work against a real tmux server
Scenario: Create and kill session lifecycle
- GIVEN a tmux session is created via the builder
- WHEN
execute(),is_session_alive(), andkill_session()are called - THEN the session SHALL be alive after creation and gone after killing
Test: e2e_tests::tmux_session_create_and_kill_lifecycle
Scenario: Five panes with different CLIs
- GIVEN 5 pane specs with different branch/CLI pairs
- WHEN the session is executed
- THEN tmux SHALL have 5 panes with correct titles
Test: e2e_tests::tmux_session_with_five_panes_and_different_clis
Scenario: Mouse mode enabled by default against live tmux
- GIVEN a session built with default settings
- WHEN
tmux show-optionis queried - THEN mouse SHALL be “on”
Test: e2e_tests::tmux_mouse_mode_enabled_by_default
Scenario: is_session_alive returns false for nonexistent (e2e)
- GIVEN no session with the queried name
- WHEN
is_session_alive()is called - THEN it SHALL return
false
Test: e2e_tests::tmux_is_session_alive_returns_false_for_nonexistent
Scenario: Attach succeeds for live session
- GIVEN a live tmux session
- WHEN
attach()is called and the client is detached programmatically - THEN the function SHALL execute without panic
Test: e2e_tests::attach_succeeds_for_live_session
Requirement: E2E commands SHALL behave correctly against real repos
Scenario: Dry run shows session plan
- GIVEN a git repo with branches and
--dry-run --cli echo --branches feat/a,feat/b - WHEN the binary runs
- THEN stdout SHALL contain “Dry run”, branch names, and the CLI name
Test: e2e_tests::dry_run_with_flags_shows_plan
Scenario: Preset not found returns error
- GIVEN a git repo with no presets configured
- WHEN
start --preset nonexistentis run - THEN it SHALL fail with stderr mentioning “not found”
Test: e2e_tests::preset_not_found_returns_error
Scenario: Stop with no session
- GIVEN a git repo with no active session
- WHEN
stopis run - THEN it SHALL succeed with stdout mentioning “No active session”
Test: e2e_tests::stop_with_no_session
Scenario: Purge with no session
- GIVEN a git repo with no active session
- WHEN
purge --forceis run - THEN it SHALL succeed with stdout mentioning “No session to purge”
Test: e2e_tests::purge_with_no_session
Scenario: Status with no session
- GIVEN a git repo with no active session
- WHEN
statusis run - THEN it SHALL succeed with stdout mentioning “No session”
Test: e2e_tests::status_with_no_session
Scenario: Stop from non-git directory fails
- GIVEN a directory that is not a git repository
- WHEN
stopis run - THEN it SHALL fail with “Not a git repository”
Test: e2e_tests::stop_from_non_git_dir_fails
Scenario: Status from non-git directory fails
- GIVEN a directory that is not a git repository
- WHEN
statusis run - THEN it SHALL fail with “Not a git repository”
Test: e2e_tests::status_from_non_git_dir_fails
Requirement: TmuxSession supports pipe-pane command
The TmuxSession builder SHALL support queuing a pipe-pane command to attach logging to a specific pane.
Scenario: pipe-pane queued in builder
- WHEN
pipe_pane()is called on aTmuxSessionwith a pane target and log path - THEN the command queue SHALL contain a
pipe-pane -o -t <pane> "cat >> <path>"entry
Scenario: pipe-pane in dry-run output
- WHEN a session with
pipe_pane()is rendered as dry-run - THEN the output SHALL include the
tmux pipe-panecommand string
Scenario: pipe-pane executed after pane creation
- WHEN the session commands are executed
- THEN the
pipe-panecommand SHALL execute after the correspondingsplit-windowandsend-keyscommands for that pane
Requirement: TmuxSession supports session-level environment variables
The TmuxSessionBuilder SHALL support setting session-level environment variables via a set_environment(key, value) method. The resulting set-environment -t <session> <key> <value> command SHALL be emitted before any send-keys commands to ensure all panes inherit the variable.
Scenario: set_environment emits correct tmux command
- GIVEN
set_environment("GIT_PAW_BROKER_URL", "http://127.0.0.1:9119")is called on the builder - WHEN the session is built
- THEN the command queue SHALL contain
set-environment -t <session> GIT_PAW_BROKER_URL http://127.0.0.1:9119
Scenario: set_environment appears before send-keys
- GIVEN a builder with environment variables and pane specs
- WHEN the session is built
- THEN all
set-environmentcommands SHALL appear before anysend-keyscommands in the command queue
Scenario: set_environment in dry-run output
- GIVEN a builder with
set_environmentcalled - WHEN the session is rendered as dry-run
- THEN the output SHALL include the
tmux set-environmentcommand string
Scenario: Multiple environment variables
- GIVEN
set_environment("A", "1")andset_environment("B", "2")are both called - WHEN the session is built
- THEN both
set-environmentcommands SHALL appear in the command queue
Requirement: Supervisor-mode pane layout
When the tmux session is built for a supervised session (per the supervisor-launch capability), the system SHALL produce a layout with these structural properties:
-
Top row: pane 0 (dashboard) spanning the full window width.
-
Agent grid below: dynamically sized by agent count, with up to 5 columns per row. The agent grid is a sequence of horizontal rows; each row holds up to 5 agent panes side-by-side.
-
Pane indices: pane 0 = dashboard; panes 1..N = coding agents in row-major order (left-to-right, top-to-bottom). No pane runs an orchestrator CLI.
-
Vertical proportions: the dashboard row SHALL occupy 50% of the session height regardless of agent count, and the agent rows SHALL share the remaining 50% evenly — each agent row occupying
50 / agent_rowspercent. Adding agents SHALL shrink the agent rows, never the dashboard row.Agent rows Dashboard row height Each agent row height 1 (1-5 agents) 50% 50% 2 (6-10 agents) 50% 25% each 3 (11-15 agents) 50% 16.7% each 4 (16-20 agents) 50% 12.5% each 5 (21-25 agents) 50% 10% each -
Equal-width agent panes within a row: every agent pane in the same agent row SHALL be rendered at equal width (within a one-column rounding tolerance). Because agents are added to a row by successive
tmux split-window -h— and each-hsplit halves the current pane — a row populated by raw splits renders unequal widths, so the system SHALL rebalance each agent row to equal width after its panes are created, by applyingtmux select-layout even-horizontalscoped to that row’s agent panes OR by issuingtmux resize-paneto set each agent pane to100 / agents_in_rowpercent of the row’s width. The rebalance SHALL NOT alter the dashboard row’s full width nor the vertical height proportions described above. -
Minimum usable pane width: the layout SHALL keep agent panes wide enough to be usable up to the agent cap; at the maximum 5 columns per row the equal-width target is 20% of the window width per pane. The system SHALL NOT produce agent rows with more than 5 panes (
SUPERVISOR_AGENTS_PER_ROW). -
Hard cap: 25 agents per session. Above 25, the system SHALL reject the launch with a clear “split into multiple sessions” error before any tmux command runs.
The layout SHALL be built using tmux split-window -h and -v with explicit percentages, then enforced via tmux resize-pane -y <pct> for the height proportions and the per-row equal-width rebalance described above. select-layout tiled (or other whole-window auto-layouts) SHALL NOT be used for the supervised layout because they do not preserve the predictable pane-index ordering this layout relies on; select-layout even-horizontal MAY be used only when scoped so it rebalances a single agent row’s widths without reordering pane indices or disturbing the dashboard row. No swap-pane SHALL be emitted.
Scenario: 5-agent supervised layout has 1 agent row
- GIVEN a supervised session with 5 agent branches
- WHEN the tmux layout is built
- THEN pane 0 SHALL be the dashboard spanning the full window width
- AND panes 1-5 SHALL be agents arranged in a single row below the dashboard
- AND the dashboard row’s height SHALL be 50% and the agent row’s height SHALL be 50%
Scenario: 10-agent supervised layout has 2 agent rows
- GIVEN a supervised session with 10 agent branches
- WHEN the tmux layout is built
- THEN total row count SHALL be 3 (1 dashboard + 2 agent rows)
- AND the dashboard row’s height SHALL be 50%
- AND each agent row’s height SHALL be 25%
- AND the first agent row SHALL contain panes 1-5, the second agent row SHALL contain panes 6-10
Scenario: 20-agent supervised layout has 4 agent rows
- GIVEN a supervised session with 20 agent branches
- WHEN the tmux layout is built
- THEN total row count SHALL be 5 (1 dashboard + 4 agent rows)
- AND the dashboard row’s height SHALL be 50%
- AND each of the 4 agent rows’ height SHALL be 12.5%
Scenario: 26-agent supervised session is rejected
- GIVEN 26 agent branches resolved
- WHEN the supervised launch flow runs
- THEN the launch SHALL be rejected with a
PawErrorstating the requested count (26), the maximum (25), and a hint suggesting--branches <subset> - AND no tmux session SHALL be created
Scenario: Pane indices match row-major order
- GIVEN a supervised session with 7 agents
- WHEN the tmux layout is built
- THEN pane 1 SHALL be the first agent (top-left of the agent grid)
- AND pane 5 SHALL be the fifth agent (top-right of the first agent row)
- AND pane 6 SHALL be the sixth agent (start of the second agent row)
Scenario: Three agents in a row render at equal width
- GIVEN a supervised session with 3 agent branches (a single agent row)
- WHEN the tmux layout is built and applied to a live tmux window
- THEN the three agent panes SHALL each occupy approximately one third of the window width (within a one-column rounding tolerance)
Scenario: Row rebalance leaves the dashboard row full width
- GIVEN a supervised session whose agent row is rebalanced to equal width
- WHEN the layout is applied to a live tmux window
- THEN pane 0 (dashboard) SHALL remain at the full window width
- AND the vertical height proportions SHALL be unchanged: the dashboard row at 50% and each agent row at
50 / agent_rowspercent
Scenario: Full agent row stays at five equal-width columns
- GIVEN a supervised session with 5 agents in a single row
- WHEN the layout is applied to a live tmux window
- THEN each of the 5 agent panes SHALL occupy approximately 20% of the window width
- AND no agent row SHALL contain more than 5 panes
Requirement: Launch-readiness gate before boot-block injection
The system SHALL verify that a pane’s CLI process has actually started and reached an interactive ready state before injecting any boot block, framing prompt, or task prompt into that pane, rather than relying solely on a fixed wall-clock sleep. The system SHALL poll the pane (via tmux capture-pane) for a CLI-readiness marker — content that distinguishes the launched CLI’s interactive UI from a bare shell prompt — up to a bounded timeout. The boot block SHALL NOT be injected while the pane is still a bare shell, because the shell would interpret the multi-line boot block line-by-line as failing commands (the v0.8.0 G1 dogfood failure).
If the readiness timeout elapses while the pane is still a bare shell, the system SHALL relaunch the CLI command into that pane (re-clearing the input line first) and poll again, up to a bounded number of relaunch attempts. The readiness gate and its timeout/attempt budget SHALL apply uniformly to EVERY launch path that injects text into an agent pane: bare start (which then pre-fills without submitting), spec-driven start, supervised start, git paw add, and the submission of a held boot prompt when a paused session is restarted. A pane the gate reports as blocked on an unanswered first-run dialog SHALL NOT receive the injected text and SHALL fail that launch path loudly (per “Launch gate handles a first-run permission/trust acceptance dialog”); the one exception is restart from pause, which SHALL keep that pane’s held prompt for the next restart, report the blocked pane on stderr, and continue restarting the session. On restart from pause a held prompt SHALL be submitted only to the pane resolved live from its agent’s worktree: when no live pane resolves, the prompt SHALL stay held and the agent SHALL be reported on stderr, and the prompt SHALL NOT be submitted at a computed pane index.
The readiness gate SHALL be conservative: a CLI that the gate cannot positively classify within the budget (e.g. an unrecognised custom CLI whose UI the marker heuristic does not match) SHALL fall back to injecting the boot block after the budget elapses rather than failing the launch, so launch behaviour is never worse than the previous fixed-sleep behaviour for an unrecognised CLI.
Scenario: Boot block withheld until the pane is CLI-ready
- GIVEN a freshly split agent pane whose CLI process has not yet reached its interactive prompt
- WHEN the launch flow reaches the boot-block injection step for that pane
- THEN the system SHALL poll the pane for a CLI-readiness marker and SHALL NOT inject the boot block while the pane still shows only a bare shell prompt
Scenario: Boot block injected once the CLI is ready
- GIVEN an agent pane whose CLI has reached its interactive ready state within the readiness timeout
- WHEN the readiness poll observes the CLI-readiness marker
- THEN the system SHALL inject the boot block into that pane exactly once
Scenario: CLI relaunched when the pane is still a bare shell
- GIVEN an agent pane whose CLI never started (the pane is still a bare shell when the readiness timeout elapses)
- WHEN the readiness gate exhausts its timeout for that pane
- THEN the system SHALL relaunch the CLI command into that pane and poll for readiness again, up to the relaunch-attempt budget, before falling back to injection
Scenario: Unrecognised CLI falls back to fixed-budget injection
- GIVEN an agent pane running a custom CLI whose interactive UI does not match any known readiness marker
- WHEN the readiness budget elapses without a positive readiness classification
- THEN the system SHALL inject the boot block after the budget rather than rejecting the launch, so behaviour is no worse than the prior fixed-sleep launch for an unrecognised CLI
Scenario: Bare start is gated
- GIVEN a bare
git paw startwith the broker enabled whose first agent pane is still a bare shell when the launch reaches injection - WHEN the launch injects the boot block pre-fill
- THEN the gate SHALL run first and the pre-fill SHALL NOT be typed into the bare shell
Scenario: Spec-driven start stuck on a first-run dialog fails loudly
- GIVEN a spec-driven
git paw start --specs awhose agent pane is blocked on an unanswered first-run acceptance dialog - WHEN the launch reaches injection for that pane
- THEN the launch SHALL fail with an error naming the pane and the attach command
- AND the boot block SHALL NOT be typed on top of the dialog
Scenario: Restart from pause gates a held prompt
- GIVEN a paused session holding a boot prompt for an agent added while paused, whose pane is blocked on a first-run dialog
- WHEN
git paw startrestarts the session - THEN the held prompt SHALL NOT be submitted to that pane
- AND it SHALL remain held for the next restart
- AND stderr SHALL name the blocked pane while the rest of the session restarts
Scenario: A held prompt whose pane cannot be found stays held
- GIVEN a paused session holding a boot prompt for an agent whose worktree no longer resolves to a live pane, with another ready CLI at that agent’s computed pane index
- WHEN
git paw startrestarts the session - THEN the held prompt SHALL NOT be submitted to any pane
- AND it SHALL remain held for the next restart
- AND stderr SHALL name the agent as “pane not found” while the rest of the session restarts
Requirement: The dry-run plan preview reflects worktree placement
The git paw start --dry-run session-plan preview SHALL render each agent’s
worktree path according to the configured [WorktreePlacement], rather than
assuming the sibling layout: .git-paw/worktrees/<branch-slug> (relative to the
repo root) for child placement, and ../<project>-<branch-slug> for sibling
placement. This applies uniformly to the plain, --supervisor, and
--from-specs dry-run previews. Computing the preview path MUST NOT create any
directory (the dry run stays read-only).
Test: git::tests::worktree_display_path_reflects_placement,
git::tests::worktree_path_for_creates_nothing
Scenario: Child placement preview shows the in-repo path
- GIVEN a repository configured with child worktree placement
- WHEN the dry-run plan renders the worktree path for branch
feat/auth-flow - THEN it SHALL show
.git-paw/worktrees/feat-auth-flow - AND it SHALL NOT show a
../-prefixed sibling path
Scenario: Sibling placement preview shows the sibling path
- GIVEN a repository configured with sibling worktree placement
- WHEN the dry-run plan renders the worktree path for branch
feat/auth-flow - THEN it SHALL show
../<project>-feat-auth-flowbeside the repository
Scenario: Rendering the preview path creates nothing
- WHEN the dry-run preview computes a child-placement worktree path
- THEN no worktree directory or
.git-paw/worktrees/entry is created on disk
Requirement: Launch gate handles a first-run permission/trust acceptance dialog
The readiness gate SHALL, when the launch-readiness poll observes a pane presenting
a first-run acceptance dialog for its CLI — a one-time bypass-permissions
confirmation (e.g. the dialog Claude Code shows the first time
--dangerously-skip-permissions runs against a config directory that never accepted
it) or a “trust this folder?” prompt — either answer the dialog to reach the
interactive ready state, or fail the launch loudly. It SHALL NOT treat the dialog as a bare shell and relaunch
(which would re-open the dialog), and SHALL NOT treat it as ready and inject the
boot block on top of it. A pane left blocked on an unanswered acceptance dialog
SHALL NOT be reported as a healthy, running agent. Dialog detection SHALL be
per-CLI and conservative: a CLI whose acceptance dialog the gate does not recognise
SHALL fall back to the existing readiness / relaunch / fixed-budget behaviour, so
launch is never worse than before.
Scenario: First-run bypass acceptance dialog is answered, not relaunched
- GIVEN an agent pane launched with a full-auto bypass flag against a config directory that has never accepted bypass, showing the first-run acceptance dialog
- WHEN the readiness gate polls the pane
- THEN the gate SHALL answer the acceptance dialog (or fail the launch loudly) rather than relaunching the CLI into the dialog
Scenario: A pane stuck on an acceptance dialog is not reported healthy
- GIVEN an agent pane blocked on an unanswered first-run acceptance dialog after the launch budget elapses
- WHEN launch completes
- THEN the pane SHALL NOT be reported as a healthy running agent (the launch fails loudly or the pane’s dead/blocked state is surfaced)
Scenario: Unrecognised CLI acceptance dialog falls back to prior behaviour
- GIVEN a custom CLI whose first-run dialog the gate does not recognise
- WHEN the readiness budget elapses
- THEN the gate SHALL fall back to the existing readiness / relaunch / fixed-budget injection behaviour
Requirement: Start reattaches to an existing live session instead of forking
git paw start SHALL reattach to the repository’s existing live session rather than creating a numerically-suffixed parallel session (paw-<project>-2) when that repository already has a live session (resolved by repository path, per session-state find-by-repository). The -N collision suffix SHALL be reserved for genuinely distinct sessions (e.g. two different repositories that share a project name), not a re-invocation of start against the same repository’s running session. In a non-interactive context where reattaching is not possible, start SHALL refuse with an actionable error (or require an explicit opt-in flag) rather than silently forking a parallel wave.
With --unattended, start SHALL instead bind a drive loop to the live session, whether or not the invocation is interactive (per supervisor-unattended-operation, A drive loop can be bound to a live supervised session). It SHALL NOT attach the terminal, SHALL NOT create a suffixed parallel session, and SHALL refuse only for the reasons that requirement and At most one drive loop drives a session instance state. Without --unattended the behaviour above is unchanged.
Scenario: Re-start of a repo with a live session reattaches
- GIVEN a repository with a live session
paw-x - WHEN the user runs
git paw startagain in that repository (interactive) - THEN the system SHALL reattach to
paw-xand SHALL NOT createpaw-x-2
Scenario: A distinct repository sharing a name still gets its own session
- GIVEN a live session
paw-xbelonging to a different repository - WHEN
git paw startruns in a second repository whose project name is alsox - THEN the system SHALL resolve a distinct session name (
paw-x-2) for the second repository
Scenario: Non-interactive re-start refuses rather than forking
- GIVEN a repository with a live session and a non-interactive (non-TTY) invocation
- WHEN
git paw startruns again in that repository without--unattended - THEN the system SHALL refuse with an actionable error (or require an explicit opt-in flag) rather than silently creating a suffixed parallel session
Test: tests/supervisor_reattach_integration.rs::supervisor_restart_refuses_rather_than_forking_a_parallel_session (unchanged).
Scenario: Non-interactive re-start with –unattended binds a drive loop
- GIVEN a repository with a live supervised session
paw-x, no drive loop running, and a non-interactive (non-TTY) invocation - WHEN
git paw start --unattendedruns again in that repository - THEN the system SHALL bind a drive loop to
paw-xand run it to an exit condition - AND SHALL NOT create
paw-x-2 - AND SHALL NOT print the non-interactive reattach refusal
Test: tests/unattended_drive_e2e.rs::unattended_rebinds_a_loop_to_a_live_session_after_heartbeat.
Requirement: Every pane-less session uses the agent grid
Every launch path — bare start, spec-driven start, and supervised start — and the recovery of a stopped session SHALL build the window with the agent-grid geometry defined by “Supervisor-mode pane layout”: the dashboard as the full-width top row when the broker is enabled, coding agents row-major below at no more than five per row, each row rebalanced to equal width, and the 25-agent cap. When the session has no dashboard (broker disabled), there SHALL be no top row: coding agents SHALL start at pane 0 and the agent rows SHALL share the full window height evenly. The whole-window select-layout tiled and main-horizontal layouts SHALL NOT be used to arrange coding agents. Every coding agent’s pane index SHALL be its row-major position — the session’s first agent index plus the agent’s position — at any agent count: each split that creates an agent pane, including the one that opens a new row and the one git paw add performs, SHALL target the session’s last pane, so the new pane takes the next index and no existing pane is renumbered.
Scenario: Bare start builds the grid
- GIVEN
[broker] enabled = trueand no supervisor mode - WHEN
git paw startlaunches 7 branches - THEN pane 0 SHALL be the dashboard spanning the full width
- AND panes 1-5 SHALL form the first agent row and panes 6-7 the second, each row at equal widths
Scenario: Spec-driven start builds the grid
- GIVEN a spec-driven
git paw start --specs a b cwithout supervisor mode and the broker enabled - WHEN the session is built
- THEN pane 0 SHALL be the dashboard and panes 1-3 SHALL be a single equal-width agent row
Scenario: Broker-less start uses the full height
- GIVEN
[broker] enabled = false - WHEN
git paw startlaunches 6 branches - THEN there SHALL be no dashboard pane
- AND panes 0-4 SHALL form the first agent row and pane 5 the second, the two rows sharing the full height evenly
Scenario: Every agent’s pane index holds that agent
- GIVEN a bare session started with 7 branches, with the broker enabled and again with it disabled
- WHEN the window is built, and again when
git paw addattaches a sixth agent to a 5-agent session - THEN pane
first_agent + iSHALL run in the worktree of the i-th agent and run that agent’s CLI, for every agent - AND no launch command or prompt SHALL be typed into a pane other than the one its agent occupies
Scenario: Recovery rebuilds a grid
- GIVEN a stopped bare session saved with the earlier tiled layout and 3 recorded worktrees with the broker enabled
- WHEN
git paw startrecovers it - THEN the rebuilt window SHALL be the grid (dashboard row + one agent row of 3)
- AND the re-saved session SHALL record the grid layout
Session State
Purpose
Persist session state to disk for recovery after crashes, reboots, or manual stops. Stores one JSON file per session under the XDG data directory, with atomic writes and tmux liveness checks. It also has git paw start write (and purge remove) a per-repo session JSON at .git-paw/sessions/paw-<project>.json — carrying the session name and each agent’s branch_id, worktree path, CLI, and pane index — as the discovery surface the bundled sweep.sh helper reads, with a live-tmux fallback when the file is absent; distinguishes stale session receipts from live ones via a single cheap tmux has-session probe (status reports 🔴 stale, start auto-invalidates stale receipts before launching, and purge --stale prunes only stale entries); and guarantees session recovery rebuilds exactly the panes a session defines (N + 2 in supervisor mode) on a headless canvas sized to tile them without overflow, preserving the persisted session JSON when a git paw start errors mid-launch. It also renders a Markdown session summary from BrokerState and PawSession — metadata, per-agent details (status, modified files, exports, blocked time), merge order, and totals — which the supervisor writes to a timestamped, filesystem-safe file under .git-paw/sessions/ so multiple runs coexist without overwriting.
Requirements
Requirement: Save session state atomically
The system SHALL serialize session data to JSON and write it atomically using a temp file and rename to prevent corruption.
The session data SHALL include optional broker fields: broker_port (Option<u16>), broker_bind (Option<String>), and broker_log_path (Option<PathBuf>). These fields SHALL be omitted from the JSON when None and SHALL default to None when absent during deserialization.
broker_log_path names one log per session — <session_state_dir>/<session_name>.broker.log — so two repositories on one machine never write to, or purge, the same file. A session saved by an earlier git-paw records the shared <session_state_dir>/broker.log instead; that value SHALL continue to load and remain authoritative for both the broker and purge until a resume or recovery re-stamps it.
Scenario: Saved session round-trips with all fields intact
- GIVEN an active session with 3 worktrees
- WHEN
save_session()is called and the session is loaded back - THEN all fields (session_name, repo_path, project_name, created_at, status, worktrees) SHALL match the original
Scenario: Saved session with broker fields round-trips
- GIVEN an active session with
broker_port = Some(9119),broker_bind = Some("127.0.0.1"),broker_log_path = Some("/path/to/paw-my-project.broker.log") - WHEN
save_session()is called and the session is loaded back - THEN all broker fields SHALL match the original
Scenario: Session without broker fields loads successfully
- GIVEN a session JSON file saved by v0.2.0 (no broker fields)
- WHEN the session is loaded
- THEN
broker_port,broker_bind, andbroker_log_pathSHALL all beNone - AND all existing fields SHALL load correctly
Scenario: A session recording the old shared log name still loads
- GIVEN a session JSON saved by an earlier git-paw with
broker_log_path = Some("<session_state_dir>/broker.log") - WHEN the session is loaded
- THEN
broker_log_pathSHALL be that recorded path - AND it SHALL be the path both the broker writes and
purgedeletes
Scenario: Saving again replaces previous state
- GIVEN a previously saved session
- WHEN
save_session()is called with updated fields - THEN the new state SHALL overwrite the old state
Requirement: Load session by name
The system SHALL load a session from disk by name, returning None if the file does not exist.
Scenario: Loading a nonexistent session returns None
- GIVEN no session file exists with the given name
- WHEN
load_session()is called - THEN it SHALL return
Ok(None)
Test: session::tests::loading_nonexistent_session_returns_none
Requirement: Find session by repository path
The system SHALL scan all session files and return the session matching a given repository path.
The default lookup SHALL match a session’s repository path EXACTLY: a path that is the root of — or lies inside — a linked worktree SHALL NOT resolve to the owning repository’s session. This exactness is a safety boundary. Every command that MUTATES session state (stop, purge, add, remove, pause, and start’s recover/resume dispatch) resolves its repository to the root of the checkout it is run from, which inside an agent worktree is that WORKTREE’s root; were the default lookup to resolve the owner, such a command run by an agent in its own worktree would act on the whole session — deleting sibling worktrees and branches, or killing the session.
The system SHALL additionally provide a SEPARATE, read-only owner-resolving lookup that returns the session of the repository that OWNS a given path, by resolving a linked worktree to that repository. It SHALL try the exact match first and resolve the owner only on a miss. It SHALL be used ONLY by the MCP server’s repository context and its read-only session view, so a git paw mcp spawned by an agent’s CLI from inside the agent’s worktree with no --repo finds the session and its broker. No mutating command SHALL use it.
Scenario: Finds correct session among multiple
- GIVEN two sessions for different repositories
- WHEN
find_session_for_repo()is called with one repo path - THEN it SHALL return the matching session
Test: session::tests::finds_correct_session_among_multiple_by_repo_path
Scenario: No matching session
- GIVEN saved sessions for other repositories
- WHEN
find_session_for_repo()is called with a different path - THEN it SHALL return
None
Test: session::tests::find_returns_none_when_no_repo_matches
Scenario: No sessions directory
- GIVEN no sessions directory exists
- WHEN
find_session_for_repo()is called - THEN it SHALL return
None
Test: session::tests::find_returns_none_when_no_sessions_exist
Scenario: A worktree root resolves to its repository’s session
- GIVEN an active session for a repository and one of that session’s agent worktrees
- WHEN
find_session_for_repo_or_owner()is called with the worktree’s root - THEN it SHALL return the repository’s session
Scenario: The owner-resolving lookup prefers an exact match
- GIVEN a session recorded for an agent worktree’s own root and another for the repository that owns it
- WHEN
find_session_for_repo_or_owner()is called with the worktree’s root - THEN it SHALL return the worktree’s own session, not the owner’s
Scenario: The default lookup does not cross the worktree boundary
- GIVEN an active session for a repository and one of that session’s agent worktrees
- WHEN
find_session_for_repo()is called with the worktree’s root - THEN it SHALL return
None
Scenario: A mutating command run inside an agent worktree does not act on the session
- GIVEN an active session and one of its agent worktrees
- WHEN
git paw stoporgit paw purge --forceis run with that worktree as the working directory - THEN the session SHALL remain alive, its branches and its sibling agents’ worktrees SHALL remain, and the command SHALL report that there is no session for this repository
Scenario: The MCP server started in a worktree finds the broker
- GIVEN an active broker-enabled session and
git paw mcpstarted with its working directory inside an agent worktree and no--repo - WHEN the server resolves its repository context
- THEN its broker URL SHALL be the session’s broker URL, not absent
Requirement: Delete session by name
The system SHALL delete a session file, succeeding even if the file does not exist (idempotent).
Scenario: Deleted session is no longer loadable
- GIVEN a saved session
- WHEN
delete_session()is called - THEN
load_session()SHALL returnNone
Test: session::tests::deleted_session_is_no_longer_loadable
Scenario: Deleting nonexistent session succeeds
- GIVEN no session file with the given name
- WHEN
delete_session()is called - THEN it SHALL return
Ok(())
Test: session::tests::deleting_nonexistent_session_succeeds
Requirement: Effective status combines file state with tmux liveness
Session::effective_status(is_tmux_alive) SHALL combine the persisted status field with the result of is_tmux_alive to produce the runtime-effective status:
| Recorded status | tmux alive? | Effective status |
|---|---|---|
Active | yes | Active |
Active | no | Stopped |
Paused | yes | Paused |
Paused | no | Stopped |
Stopped | any | Stopped |
The rule for Paused: tmux must still be alive for the Paused state to be valid — pause’s whole purpose is to keep tmux + CLI panes running while the client is detached. If tmux died despite a recorded Paused state (e.g. tmux server crash), effective_status SHALL downgrade to Stopped, and cmd_start SHALL run the cold-recovery path (fresh CLI spawn) rather than the restart-from-pause path.
Scenario: Active + alive remains Active
- GIVEN a session with
status = Active - WHEN
effective_status(|_| true)is called - THEN it SHALL return
Active
Scenario: Active + dead downgrades to Stopped
- GIVEN a session with
status = Active - WHEN
effective_status(|_| false)is called - THEN it SHALL return
Stopped
Scenario: Paused + alive remains Paused
- GIVEN a session with
status = Paused - WHEN
effective_status(|_| true)is called - THEN it SHALL return
Paused
Scenario: Paused + dead downgrades to Stopped
- GIVEN a session with
status = Paused - WHEN
effective_status(|_| false)is called - THEN it SHALL return
Stopped
Scenario: Stopped remains Stopped regardless of tmux liveness
- GIVEN a session with
status = Stopped - WHEN
effective_statusis called with either liveness result - THEN it SHALL return
Stopped
Requirement: SessionStatus display format
The SessionStatus enum SHALL display as lowercase strings.
Scenario: SessionStatus display strings
- GIVEN
SessionStatus::ActiveandSessionStatus::Stopped - WHEN formatted with
Display - THEN they SHALL render as
"active"and"stopped"
Test: session::tests::session_status_displays_as_lowercase_string
Requirement: Recovery data survives tmux crashes
After a tmux crash, the persisted session SHALL contain all data needed to reconstruct the session.
Scenario: Crashed session has all recovery data including broker fields
- GIVEN a saved session with worktrees and broker enabled
- WHEN tmux crashes and the session is loaded from disk
- THEN it SHALL have the session name, repo path, all worktree details, AND broker_port, broker_bind, broker_log_path
Scenario: Session recovery recreates the broker host and dashboard pane when broker was enabled
- GIVEN a saved session with
broker_port = Some(9119)andbroker_bind = Some("127.0.0.1") - WHEN
recover_session()is called - THEN the rebuilt tmux session SHALL have:
- A detached
brokerwindow runninggit-paw __broker - Dashboard pane in pane 0 running
git-paw __dashboard GIT_PAW_BROKER_URLenvironment variable set tohttp://127.0.0.1:9119- All original worktree panes in subsequent indices
- A detached
Scenario: Session recovery uses original broker config, not current config
- GIVEN a saved session with
broker_port = Some(9119) - AND current repo config has
broker.enabled = false - WHEN
recover_session()is called - THEN the broker host and dashboard pane SHALL still be created with the original broker URL
Scenario: Session recovery without original broker creates no dashboard
- GIVEN a saved session with
broker_port = None - WHEN
recover_session()is called - THEN no broker host and no dashboard pane SHALL be created
Requirement: Session persistence SHALL work through the public API
Scenario: Save and load round-trip
- GIVEN a session with 2 worktrees
- WHEN
save_session_in()andload_session_from()are called - THEN all fields SHALL match
Test: session_integration::save_and_load_round_trip
Scenario: Find session by repo path
- GIVEN a saved session
- WHEN
find_session_for_repo_in()is called with the matching repo path - THEN the correct session SHALL be returned
Test: session_integration::find_session_by_repo_path
Scenario: Find returns None for unknown repo
- GIVEN no matching session
- WHEN
find_session_for_repo_in()is called - THEN it SHALL return
None
Test: session_integration::find_session_returns_none_for_unknown_repo
Scenario: Find correct session among multiple
- GIVEN two sessions for different repos
- WHEN
find_session_for_repo_in()is called for one - THEN the correct session SHALL be returned
Test: session_integration::find_correct_session_among_multiple
Scenario: Delete removes session
- GIVEN a saved session
- WHEN
delete_session_in()is called - THEN
load_session_from()SHALL returnNone
Test: session_integration::delete_removes_session
Scenario: Delete nonexistent is idempotent
- GIVEN no session file
- WHEN
delete_session_in()is called - THEN it SHALL succeed
Test: session_integration::delete_nonexistent_is_idempotent
Scenario: Load nonexistent returns None
- GIVEN no session file
- WHEN
load_session_from()is called - THEN it SHALL return
None
Test: session_integration::load_nonexistent_returns_none
Scenario: Saving again replaces previous state
- GIVEN a saved session
- WHEN the status is changed and saved again
- THEN the loaded session SHALL have the new status
Test: session_integration::saving_again_replaces_previous_state
Scenario: Effective status active when tmux alive
- GIVEN a session with
Activestatus and tmux alive - WHEN
effective_status()is called - THEN it SHALL return
Active
Test: session_integration::effective_status_active_when_tmux_alive
Scenario: Effective status stopped when tmux dead
- GIVEN a session with
Activestatus and tmux dead - WHEN
effective_status()is called - THEN it SHALL return
Stopped
Test: session_integration::effective_status_stopped_when_tmux_dead
Scenario: Effective status stopped stays stopped
- GIVEN a session with
Stoppedstatus - WHEN
effective_status()is called - THEN it SHALL return
Stoppedregardless of tmux
Test: session_integration::effective_status_stopped_stays_stopped
Scenario: Saved session has all recovery fields
- GIVEN a saved and reloaded session
- WHEN recovery fields are checked
- THEN session_name, repo_path, project_name, and all worktree entries SHALL be non-empty
Test: session_integration::saved_session_has_all_recovery_fields
Requirement: Paused session status variant
The SessionStatus enum SHALL include a third variant Paused (alongside Active and Stopped). The serde representation SHALL serialize as the lowercase string "paused" and SHALL deserialize from the same string. The Display implementation SHALL render Paused as "paused".
The Paused state means: the tmux session is intended to remain alive, all coding-agent CLI panes are intended to remain running, the user’s tmux client is detached, and the broker is stopped. Session state files saved by v0.4.0 binaries (which only know Active and Stopped) SHALL continue to load successfully under v0.5+ binaries — the new variant only appears in files saved by v0.5+.
Scenario: Paused status serializes lowercase
- GIVEN a
Sessionwithstatus = SessionStatus::Paused - WHEN
save_session()is called and the JSON file is inspected - THEN the
"status"field SHALL be"paused"
Scenario: Paused status round-trips
- GIVEN a
Sessionwithstatus = SessionStatus::Pausedsaved to disk - WHEN the session is loaded back via
load_session() - THEN
statusSHALL beSessionStatus::Paused
Scenario: v0.4-saved sessions load under v0.5
- GIVEN a session JSON file saved by v0.4.0 (only
"active"or"stopped"in the status field) - WHEN the file is loaded by a v0.5+ binary
- THEN the load SHALL succeed
- AND the
statusfield SHALL match the original (ActiveorStopped)
Scenario: Paused Display renders lowercase
- WHEN
format!("{}", SessionStatus::Paused)is evaluated - THEN the result SHALL be
"paused"
Requirement: Dashboard pane index persisted in session state
The Session struct SHALL include an optional field dashboard_pane: Option<u32> that records the pane index of the dashboard pane within the tmux session. The field SHALL use #[serde(default, skip_serializing_if = "Option::is_none")] so sessions saved without it load with None. The field SHALL be populated with Some(0) by every supervised start flow (the supervised layout always places the dashboard at pane 0) and by every bare start flow that enables the broker, because the dashboard is pane 0 in both layouts; only a bare start without the broker records None.
Consumers that need the first coding-agent pane index or the dashboard index (status, stop, pause, restart from pause, remove, liveness, recovery, the drive loop) SHALL derive them from the session’s layout record, which reads the RECORDED dashboard_pane. When it is Some(n), the dashboard is pane n and the first coding agent is pane n + 1. A session saved by a pre-relocation binary with dashboard_pane = Some(1) (supervisor at 0, dashboard at 1, agents from 2) therefore keeps resolving its agent panes from index 2 while it is still alive after an upgrade. When the field is None, the layout record SHALL apply the earlier (pre-relocation) rules, so live sessions saved by older binaries keep resolving: a supervised session’s coding agents start at pane 2, a bare session’s at pane 1 when it recorded a broker and pane 0 when it did not, and the dashboard index defaults to 0 for pause and restart from pause. SessionMode SHALL be consulted only when dashboard_pane is None.
Scenario: Dashboard pane index round-trips
- GIVEN a
Sessionwithdashboard_pane = Some(0)saved to disk - WHEN the session is loaded back
- THEN
dashboard_paneSHALL beSome(0)
Scenario: Session without dashboard_pane defaults to None on load
- GIVEN a session JSON file with no
dashboard_panefield - WHEN the session is loaded
- THEN
dashboard_paneSHALL beNone
Scenario: Dashboard pane field is omitted when None
- GIVEN a
Sessionwithdashboard_pane = None - WHEN
save_session()is called and the JSON file is inspected - THEN the JSON SHALL NOT contain a
dashboard_panefield
Scenario: Supervised start records the dashboard at pane 0
- GIVEN
[broker] enabled = trueand a supervised launch - WHEN the session state is saved
- THEN
dashboard_paneSHALL beSome(0) - AND the first recorded coding agent’s
pane_indexin the per-repo discovery file SHALL be1
Scenario: A pre-relocation live session keeps its agent offset
- GIVEN a saved session with
mode = Supervisor,dashboard_pane = Some(1)and broker enabled - WHEN
git paw statusresolves the session’s agent panes - THEN it SHALL treat pane 2 as the first coding-agent pane
- AND no keystroke sender (the drive loop, or
sweep.sh approve) SHALL treat pane 0 or pane 1 of that session as a coding agent
Scenario: A session without dashboard_pane follows the earlier rules
- GIVEN a live saved session with
mode = Supervisor,dashboard_pane = Noneand no broker (a broker-less supervised session from a pre-relocation binary) - WHEN
git paw statusresolves the session’s agent panes - THEN it SHALL treat pane 2 as the first coding-agent pane
- AND a saved bare session with
dashboard_pane = NoneSHALL resolve its first coding agent at pane 1 when it recorded a broker and at pane 0 when it did not
Scenario: A broker-less supervised start still records the dashboard
- GIVEN
[broker] enabled = falseand a supervised launch - WHEN the session state is saved
- THEN
dashboard_paneSHALL beSome(0)
Requirement: start writes a per-repo session JSON
git paw start SHALL write a per-repo session JSON to
.git-paw/sessions/paw-<project>.json describing the launched
session. This is the discovery surface the bundled
sweep.sh helper reads. The file SHALL be written on launch
and removed on purge. Every path through which git paw start
creates or rebuilds the session’s panes SHALL (re)write it — the
bare launch, the spec-driven launch with or without supervisor
mode, the supervisor launch, recovery of a stopped session, and
restart of a paused session — and each agent entry’s pane_index
SHALL identify the pane that agent occupies in the resulting
session.
Scenario: start writes the per-repo JSON
- WHEN
git paw start(any flags) succeeds - THEN
.git-paw/sessions/paw-<project>.jsonSHALL exist describing the session (name + agent list)
Scenario: A spec-driven launch without supervisor mode writes the per-repo JSON
- WHEN
git paw start --specs <names>succeeds without supervisor mode - THEN
.git-paw/sessions/paw-<project>.jsonSHALL exist with oneagentsentry per launched spec branch - AND the bundled helper SHALL discover the session from it without relying on running inside the session
Scenario: Recovery rewrites the per-repo JSON
- GIVEN a stopped session whose per-repo JSON is missing
- WHEN
git paw startrecovers it - THEN
.git-paw/sessions/paw-<project>.jsonSHALL exist and list every recovered agent with the pane index it occupies in the rebuilt session
Scenario: Restart from pause rewrites the per-repo JSON
- GIVEN a paused session
- WHEN
git paw startresumes it - THEN
.git-paw/sessions/paw-<project>.jsonSHALL list every agent with the pane index it occupies in the resumed session
Scenario: purge removes the per-repo JSON
- GIVEN an active session with the per-repo JSON present
- WHEN the user runs
git paw purge --force - THEN the per-repo JSON SHALL be removed
Requirement: Per-repo JSON shape matches sweep.sh expectations
The per-repo paw-<project>.json SHALL include the session name plus an agent list, each entry carrying branch_id, worktree_path, cli, and pane_index. The shape SHALL match what the bundled assets/scripts/sweep.sh helper reads — invoked from the orchestrator’s own session at the repository root, outside the tmux session — so the helper works against the file without modification. The agent list SHALL list coding agents only.
Scenario: sweep.sh discovers agents via the per-repo JSON
- GIVEN a freshly-started session and the bundled
sweep.shhelper invoked from a shell at the repository root that is not attached to the tmux session - WHEN sweep.sh reads
.git-paw/sessions/paw-<project>.json - THEN the helper SHALL enumerate the full agent list with branch_id, worktree path, CLI, and pane index — no fields missing or renamed relative to its expectations
Scenario: Adding a field is backwards-compatible
- WHEN a future change adds a new field to the per-repo JSON
- THEN the current
sweep.shSHALL still find every documented field; unknown extra fields SHALL be ignored
Requirement: sweep.sh falls back to live tmux when the file is absent
The bundled sweep.sh SHALL fall back to discovering the
session name from the $TMUX environment variable, or
tmux display-message -p '#S', when
.git-paw/sessions/paw-<project>.json is absent (e.g. the
supervisor attached to a pre-existing paw-* session created
outside the normal git paw start flow). The helper SHALL
NOT require manual authoring of the session JSON.
Scenario: Helper discovers the session with no JSON present
- GIVEN a
paw-myprojtmux session is active but.git-paw/sessions/paw-myproj.jsondoes not exist - WHEN
sweep.shruns inside the session - THEN the helper SHALL resolve the session name via
$TMUX/tmux display-message -p '#S'and operate without error, instead of failing to discover the session
Scenario: Per-repo JSON takes precedence when present
- GIVEN both the per-repo JSON and a live tmux session exist
- WHEN
sweep.shruns - THEN the helper SHALL prefer the per-repo JSON’s agent list (richer — it carries worktree paths + pane indices) over the bare session-name fallback
Requirement: Status distinguishes stale receipts from active sessions
git paw status SHALL probe tmux has-session -t paw-<project>
when the receipt claims active. When the probe shows the tmux
session is absent, status SHALL report 🔴 stale instead of
🟢 active. The system SHALL preserve the existing 🟢 active
and 🟡 stopped displays unchanged.
Scenario: Active session reports active
- GIVEN a session whose receipt says
activeAND whose tmux session exists - WHEN the user runs
git paw status - THEN the output SHALL display
🟢 active
Scenario: Stale receipt reports stale
- GIVEN a session whose receipt says
activeAND whose tmux session does NOT exist (crash or release-boundary carry-over) - WHEN the user runs
git paw status - THEN the output SHALL display
🔴 stale(not🟢 active)
Scenario: Stopped session reports stopped
- GIVEN a session whose receipt says
stopped - WHEN the user runs
git paw status - THEN the output SHALL display
🟡 stoppedregardless of tmux liveness
Requirement: JSON status output adds stale value
The system SHALL extend the JSON output of git paw status
with a new "stale" value for the status field whenever the
liveness probe identifies a stale receipt. The system SHALL
preserve the existing "active" and "stopped" values from
v0.5.0 without semantic change.
Scenario: JSON output reports stale
- GIVEN a stale-receipt session
- WHEN the user runs
git paw status --json - THEN the response object SHALL contain
"status": "stale"
Requirement: start invalidates stale receipts automatically
git paw start SHALL run the same tmux has-session probe
before deciding whether to recover or launch fresh. When the
receipt claims active but the tmux session is absent, the
system SHALL invalidate the receipt (purging the recorded
worktrees + branches equivalent to git paw purge --force)
and SHALL emit a stderr notice naming the invalidated entry
before proceeding with the requested launch.
Scenario: Stale receipt is invalidated before launch
- GIVEN a stale receipt for
paw-myproj - WHEN the user runs
git paw start(any flags) - THEN the system SHALL purge the stale receipt’s worktrees + branches, SHALL emit a stderr notice identifying the purged entry, and SHALL proceed with the requested launch as if no prior session existed
Scenario: Active receipt is NOT invalidated
- GIVEN a live session whose receipt says
activeAND whose tmux session exists - WHEN the user runs
git paw start(no recovery flags) - THEN the system SHALL behave per its existing reattach-or-error semantics; it SHALL NOT purge anything
Scenario: Notice text identifies the purged entry
- WHEN the auto-invalidation fires
- THEN the stderr notice SHALL include the session name,
the receipt’s
last_seentimestamp (if present), and a one-line explanation that the tmux session no longer exists
Requirement: purge –stale flag
git paw purge SHALL accept a --stale flag. When passed, the system SHALL purge only sessions whose receipt is stale per the liveness probe, and by default SHALL scope the sweep to the CURRENT repository — it SHALL NOT purge or touch a session belonging to any other repository. A machine-wide sweep of stale receipts across all repositories SHALL require the explicit --all-repos flag. In every case the guarantee is fail-safe: a session that is live per the probe — or whose liveness cannot be positively confirmed — SHALL NEVER be purged by --stale, in the current repository or any other. The flag is additive to existing --force.
Scenario: –stale is scoped to the current repository by default
- GIVEN a stale session belonging to the current repository AND a stale session belonging to a different repository
- WHEN the user runs
git paw purge --stale(without--all-repos) - THEN only the current repository’s stale session’s worktrees + branches + receipt SHALL be purged
- AND the other repository’s session SHALL NOT be touched
Scenario: –stale never purges a live or liveness-indeterminate session
- GIVEN a live session (in the current or another repository), or a session whose liveness cannot be positively confirmed
- WHEN the user runs
git paw purge --stale(with or without--all-repos) - THEN that session SHALL NOT be purged
Scenario: –all-repos sweeps stale receipts across repositories
- GIVEN stale sessions belonging to multiple repositories
- WHEN the user runs
git paw purge --stale --all-repos - THEN every repository’s stale session SHALL be purged
- AND every live session SHALL remain intact
Scenario: –stale with nothing stale exits cleanly
- GIVEN no stale receipts in scope
- WHEN the user runs
git paw purge --stale - THEN the command SHALL exit 0 with a “nothing to purge” message, and SHALL NOT touch any active session
Scenario: –stale + –force is well-defined
- GIVEN a stale receipt in the current repository
- WHEN the user runs
git paw purge --stale --force - THEN the system SHALL behave equivalently to
--stalealone (the--forceflag is redundant in this combination; explicitly documented as a no-op pairing)
Requirement: Liveness probe is cheap
The staleness check SHALL be a single tmux has-session -t paw-<project> invocation. The system SHALL NOT probe the
broker, agent processes, or any other liveness signal as part
of the receipt-staleness check.
Scenario: Probe runs only one tmux call
- WHEN any of
status,start, orpurge --staleperforms the staleness check - THEN the resulting process tree SHALL contain exactly
one
tmux has-sessioninvocation per session being probed
Scenario: Probe failure modes are tolerated
- GIVEN a system where the
tmuxbinary is absent or unreachable - WHEN the staleness check runs
- THEN the system SHALL treat the probe failure as
inconclusive (preserve the receipt’s current state) and
SHALL NOT report
🔴 stalebased on a tmux-missing failure
Requirement: Recovery rebuilds exactly the session’s panes
Recovering a stopped session SHALL rebuild in window 0 exactly the panes the session defines — in a supervised session, the dashboard pane plus one pane per recorded worktree (N + 1). The recovery SHALL NOT create more panes than the session defines and SHALL NOT create an orchestrator pane, including when the stopped session was saved with an earlier layout. The detached broker service window is recreated separately (broker-runtime) and is not counted.
Scenario: Recovery of an N-worktree supervised session creates N+1 panes
- GIVEN a stopped supervised session with 3 recorded worktrees
- WHEN
git paw startrecovers it - THEN window 0 of the rebuilt tmux session SHALL have exactly 4 panes (dashboard + 3 agents)
Scenario: Recovery of a pre-relocation session rebuilds the new layout
- GIVEN a stopped supervised session saved with
dashboard_pane = Some(1)and 2 recorded worktrees - WHEN
git paw startrecovers it - THEN window 0 of the rebuilt session SHALL have exactly 3 panes with the dashboard at pane 0
- AND the re-saved session SHALL record
dashboard_pane = Some(0)
Scenario: Recovery that cannot fit fails cleanly
- GIVEN a recovery whose panes cannot be tiled in the available canvas
- WHEN the rebuild fails
- THEN it SHALL NOT leave a half-tiled session and SHALL NOT mutate the persisted session state
Requirement: A failed start preserves persisted session state
The system SHALL leave the persisted per-repo session JSON unchanged when a
git paw start invocation errors before completing its launch. The session
state file SHALL be rewritten only after a launch completes successfully, so
an aborted start cannot destroy a recoverable session (e.g. rewrite it to an
empty worktree list).
Scenario: Aborted start does not corrupt the session JSON
- GIVEN an existing saved session with N worktrees
- WHEN a subsequent
git paw starterrors mid-launch - THEN the saved session JSON SHALL still list those N worktrees
(unchanged), so a later
git paw startcan still recover it
Requirement: Headless canvas fits multi-agent supervisor layouts
The system SHALL size the detached/headless new-session canvas and the
global default-size large enough to tile a supervisor session with the
supported number of agents without tmux returning no space for new pane.
An attached client still resizes the session to the real terminal on attach.
Scenario: Headless supervisor session tiles without overflow
- GIVEN a detached (no attached client) supervisor launch with several agents
- WHEN the session is built at the headless fallback size
- THEN all panes SHALL tile successfully (no
no space for new paneerror)
Requirement: write_session_summary function
The system SHALL provide a public function pub fn write_session_summary(state: &BrokerState, session: &PawSession, merge_order: &[String], output_path: &Path) -> Result<(), PawError> in a new src/summary.rs module.
The function SHALL extract data from BrokerState (agent records and message log) and PawSession (project name, start time) to produce the summary. The merge_order parameter provides the sequence in which branches were merged.
The function SHALL write the summary to output_path as a UTF-8 Markdown file. If the file already exists, it SHALL be overwritten.
Scenario: Summary is written to the specified path
- GIVEN a valid
BrokerStatewith two agents and aPawSession - WHEN
write_session_summary(&state, &session, &merge_order, &path)is called - THEN a file SHALL exist at
path - AND the function returns
Ok(())
Scenario: Existing summary is overwritten
- GIVEN a summary file already exists at the output path
- WHEN
write_session_summaryis called again - THEN the file SHALL contain only the new summary content
Scenario: Write failure returns PawError
- GIVEN the output path is in a read-only directory
- WHEN
write_session_summaryis called - THEN the function returns
Err(PawError::...)
Requirement: Session metadata section
The summary SHALL begin with a # Session Summary heading followed by a metadata block containing:
- Project name (from
PawSession) - Session date (formatted as
YYYY-MM-DD) - Session duration (from
PawSessionstart time to now) - Number of agents
Scenario: Summary contains correct project name
- GIVEN a
PawSessionwithproject_name = "my-app" - WHEN
write_session_summaryis called - THEN the output SHALL contain
my-app
Scenario: Summary contains agent count
- GIVEN a
BrokerStatewith 3 known agents - WHEN
write_session_summaryis called - THEN the output SHALL contain the number
3adjacent to “Agents”
Requirement: Per-agent details section
The summary SHALL include an ## Agents section with one subsection (### <branch> (<cli>)) per known agent, containing:
Status:— the agent’s final status from its last messageFiles modified:— from the lastagent.artifactmessage’smodified_filesExports:— from the lastagent.artifactmessage’sexportsBlocked time:— estimated fromagent.blockedto nextagent.statustimestamps in the message log
When an agent has no agent.artifact message, the files and exports fields SHALL show (none).
Scenario: Per-agent section includes modified files
- GIVEN agent
"feat-config"publishedagent.artifactwithmodified_files = ["src/config.rs"] - WHEN
write_session_summaryis called - THEN the output SHALL contain
src/config.rsunder thefeat-configagent section
Scenario: Per-agent section handles no artifact
- GIVEN agent
"feat-config"only publishedagent.status(never completed) - WHEN
write_session_summaryis called - THEN the output SHALL still include a section for
feat-config - AND files and exports SHALL show
(none)
Requirement: Merge order section
The summary SHALL include an ## Overview section listing the merge order as a comma-separated list or bullet list.
Scenario: Merge order is included
- GIVEN
merge_order = ["feat-errors", "feat-config", "feat-detect"] - WHEN
write_session_summaryis called - THEN the output SHALL list
feat-errors,feat-config,feat-detectin that order under the merge order label
Requirement: Totals section
The summary SHALL include a ## Totals section with:
- Total agents: count of all known agents
- Total time: overall session duration
Scenario: Totals section is present
- GIVEN a session with 3 agents
- WHEN
write_session_summaryis called - THEN the output SHALL contain a “Totals” section with at least agent count and duration
Requirement: Summary module is public
The summary module SHALL be declared as pub mod summary in src/lib.rs. The write_session_summary function SHALL be callable from src/main.rs supervisor handler.
Scenario: Summary module is accessible from main
- WHEN
src/lib.rsis compiled - THEN
git_paw::summary::write_session_summarySHALL be a callable public function
Requirement: Summary output path is timestamped under .git-paw/sessions/
The supervisor SHALL call write_supervisor_summary, which writes the rendered Markdown to a timestamped file under <repo_root>/.git-paw/sessions/<UTC-timestamp>.md. The timestamp SHALL be in YYYY-MM-DDTHH-MM-SSZ format (filesystem-safe — colons replaced with hyphens) so multiple supervisor runs against the same repository produce distinct files that can coexist on disk without overwriting each other. write_supervisor_summary SHALL create .git-paw/sessions/ if it does not already exist, and SHALL return the absolute path it wrote so the caller can log it.
Scenario: Supervisor writes timestamped summary under sessions dir
- GIVEN a completed supervisor session with all agents verified and merged
- WHEN the supervisor finishes
- THEN a file
<repo_root>/.git-paw/sessions/<UTC-timestamp>.mdSHALL exist - AND the timestamp SHALL match
YYYY-MM-DDTHH-MM-SSZ - AND the filename SHALL NOT contain a
:character
Scenario: Two sequential supervisor runs produce distinct summary files
- GIVEN an empty repository with no
.git-paw/sessions/directory - WHEN
write_supervisor_summaryis called twice with at least one second between calls - THEN both calls SHALL succeed
- AND the returned paths SHALL differ
- AND both files SHALL exist on disk
Scenario: Sessions directory is created on demand
- GIVEN a repository where
.git-paw/sessions/does not exist - WHEN
write_supervisor_summaryis called - THEN
.git-paw/sessions/SHALL be created - AND the rendered summary SHALL be written inside it
Requirement: Session lookup matches on the canonical repository path
find_session_for_repo SHALL match a stored session to a repository by comparing
the two paths in canonical form, so a repository referred to by a symlinked or
non-normalized path (for example /var/… resolving to /private/var/…, or a path
containing a .. segment) still resolves to its session. When a path cannot be
canonicalized (for example the repository directory no longer exists), the system
SHALL fall back to comparing the raw paths, preserving the prior behavior for that
case.
Scenario: A session is found through a symlinked repo path
- GIVEN a session registered at a repository directory
- AND a symlink that resolves to that same directory
- WHEN the session is looked up by the symlink path
- THEN the session SHALL be found
Scenario: A non-existent path falls back to raw comparison
- GIVEN a session registered at a repository path that does not exist on disk
- WHEN it is looked up by that same (non-existent) path
- THEN the session SHALL still be found via the raw-path fallback
Requirement: Liveness reflects dead agent panes, not just session existence
The tmux-liveness probe that feeds effective_status SHALL report a session as not
alive when the tmux session object still exists but its agent panes’ CLI processes
have all exited (the is_tmux_alive input becomes false) — so a session whose panes
died shortly after launch is reported Stopped, not Active. Liveness SHALL NOT be
satisfied by the mere existence of the named tmux session; it SHALL require at
least one live agent pane (a pane whose launched CLI process is still running). This
ensures git paw status and git paw doctor never report a session healthy after
its panes have all died (the full-auto boot-exit failure mode). The probe SHALL
tolerate the brief launch window in which panes are still starting their CLIs (it
SHALL NOT flap a just-launched session to Stopped).
The probe SHALL report “agent panes exited” as a separate liveness outcome from “tmux
session absent”. 🔴 stale and the JSON "stale" value SHALL mean only that the tmux
session is absent. git paw status SHALL NOT report a session as stale when its tmux
session exists and all its agent panes have exited. For such a session it SHALL:
- display
🟡 stopped; - print a hint that the agent panes exited and that
git paw startrecovers them; - report the tmux session as running:
Tmux: runningin the human output and"tmux_running": truein JSON; - never say that the tmux session no longer exists.
start auto-invalidation and purge --stale SHALL keep deciding staleness with the single
tmux has-session probe. As a result, git paw start recovers a session whose agent panes
exited, and purge --stale never purges it.
The probe SHALL identify coding-agent panes by their pane-scoped @paw_role option rather
than by pane position, because a pane index shifts when an earlier pane is killed. It SHALL
classify each pane independently: a pane whose role is dashboard or supervisor is never a
coding-agent pane, any other role is, and a pane carrying no role falls back to the positional
rule. git-paw SHALL set @paw_role on every pane it creates, whether or not
[layout].border_affordances is enabled. Consequently a paused session — git paw pause
kills the dashboard pane, and tmux renumbers the survivors — SHALL NOT be reported as having
exited agent panes while any of its agents is still running.
Scenario: A session whose agent panes all exited reports Stopped
- GIVEN a session recorded
Activewhose tmux session object still exists but all agent panes’ CLI processes have exited - WHEN its effective status is resolved via the liveness probe
- THEN the liveness probe SHALL report the session not alive
- AND the effective status SHALL be
Stopped(notActive)
Scenario: A session with at least one live agent pane remains Active
- GIVEN a session recorded
Activewith at least one agent pane whose CLI process is still running - WHEN its effective status is resolved
- THEN the liveness probe SHALL report the session alive
- AND the effective status SHALL be
Active
Scenario: Exited agent panes are a distinct liveness outcome from an absent session
- GIVEN a tmux session that exists, created past the launch grace period, whose every agent pane runs a bare shell
- WHEN the agent-pane liveness probe runs
- THEN it SHALL return the agent-panes-exited outcome, not the outcome for an absent tmux session
- AND for a tmux session that does not exist the same probe SHALL return the absent-session (stale) outcome
Scenario: Status shows exited agent panes as stopped with tmux running
- GIVEN a session whose receipt says
active, whose tmux session exists, and whose agent panes have all exited past the launch grace period - WHEN the user runs
git paw status - THEN the output SHALL display
🟡 stopped, not🔴 stale - AND it SHALL print a hint that the agent panes exited and that
git paw startrecovers them - AND it SHALL print
Tmux: running - AND it SHALL NOT state that the tmux session no longer exists
Scenario: JSON status for exited agent panes reports stopped with tmux running
- GIVEN the same session (tmux session present, all agent panes exited)
- WHEN the user runs
git paw status --json - THEN the response object SHALL contain
"status": "stopped"and"tmux_running": true
Scenario: An absent tmux session still reports stale with tmux not running
- GIVEN a session whose receipt says
activeand whose tmux session does not exist - WHEN the user runs
git paw status --json - THEN the response object SHALL contain
"status": "stale"and"tmux_running": false
Scenario: purge –stale leaves a session whose agent panes exited
- GIVEN a session in the current repository whose receipt says
active, whose tmux session exists, and whose agent panes have all exited - WHEN the user runs
git paw purge --stale - THEN that session’s receipt SHALL NOT be purged
- AND the command SHALL report that there is nothing to purge
Scenario: A paused session whose dashboard pane was killed reports paused, not exited
- GIVEN a paused session whose dashboard pane
git paw pausekilled, whose remaining agent pane tmux has renumbered into the dashboard’s old index, and whose agent process is still running - WHEN the user runs
git paw statusandgit paw doctor - THEN status SHALL display
🔵 pausedand SHALL NOT print the agent-panes-exited hint - AND doctor’s session-state finding SHALL NOT say that its agent panes exited
Requirement: Worktree entry records the worker CLI session id
The WorktreeEntry struct SHALL include an optional field cli_session_id: Option<String> recording
the session identifier git-paw assigned to the worker’s CLI at launch. The field SHALL be annotated
#[serde(default, skip_serializing_if = "Option::is_none")] so session files written before this
change load with None and an entry without an id serializes byte-identically to before.
Scenario: Assigned id round-trips
- GIVEN a
WorktreeEntrywithcli_session_id = Some("3f2c…")saved to disk - WHEN the session is loaded back
- THEN
cli_session_idSHALL beSome("3f2c…")
Scenario: Older session files load with None
- GIVEN a session JSON file with no
cli_session_idfield on its worktree entries - WHEN the session is loaded
- THEN every entry’s
cli_session_idSHALL beNone - AND loading SHALL succeed without error
Scenario: Entry without an id serializes as before
- GIVEN a
WorktreeEntrywithcli_session_id = None - WHEN the session is saved and the JSON inspected
- THEN the JSON SHALL NOT contain a
cli_session_idkey
Requirement: Recovery relaunches a worker with its recorded session id
When git paw start recovers a stopped session, the system SHALL relaunch each worktree whose entry
carries a cli_session_id AND whose CLI resolves a resume template with that template’s arguments
appended to the CLI launch command, so the worker’s conversation context is restored. A worktree
whose entry has no id, or whose CLI resolves no resume template, SHALL be relaunched exactly as before
this change. Recovery SHALL NOT fail because a resume template is absent.
Scenario: Recovered Claude worker is resumed
- GIVEN a stopped session whose worktree entry for
feat/xhascli = "claude"andcli_session_id = Some("<uuid>"), and no[clis.claude]override - WHEN the session is recovered
- THEN the pane command for
feat/xSHALL contain--resume <uuid>
Scenario: Entry without an id launches fresh
- GIVEN a stopped session whose worktree entry has
cli_session_id = None - WHEN the session is recovered
- THEN the pane command SHALL be identical to the pre-change recovery command for that entry
Scenario: CLI without a resume template launches fresh
- GIVEN a worktree entry with
cli = "my-agent",cli_session_id = Some("<id>"), and no[clis.my-agent].resume_args - WHEN the session is recovered
- THEN the pane command SHALL contain no resume arguments
- AND recovery SHALL succeed
Requirement: Status JSON exposes the worker CLI session id
git paw status --json SHALL include each agent’s cli_session_id (a string, or null when
unassigned) so an operator or external orchestrator can resume a worker by hand. The human-readable
git paw status output SHALL be unchanged.
Scenario: JSON status carries the id
- GIVEN an active session whose
feat/xentry hascli_session_id = Some("<uuid>") - WHEN
git paw status --jsonis run - THEN the
feat/xagent object SHALL contain"cli_session_id": "<uuid>"
Scenario: JSON status shows null when unassigned
- GIVEN an agent whose entry has no session id
- WHEN
git paw status --jsonis run - THEN that agent object SHALL contain
"cli_session_id": null
Scenario: Human-readable status is unchanged
- WHEN
git paw statusis run without--json - THEN the output SHALL be byte-identical to the pre-change output for the same session
Requirement: Worktree entry records the spec it was launched from
The WorktreeEntry struct SHALL include an optional field spec_id: Option<String> recording the id of the SpecEntry an agent was launched from. Every launch path that creates an agent from a spec — the spec-driven launch with or without supervisor mode, and git paw add --from-spec — SHALL set it to that spec’s id; a launch without a spec (bare git paw start, git paw add <branch>) SHALL leave it None. The field SHALL be annotated #[serde(default, skip_serializing_if = "Option::is_none")] so session files written before this field existed load with None and an entry without a spec serializes byte-identically to before.
Scenario: A spec-driven launch records each agent’s spec id
- GIVEN a spec-driven launch of specs
003-user-listand004-error-handling - WHEN the session is saved and loaded back
- THEN each agent’s worktree entry SHALL carry the
spec_idof the spec it was launched from
Scenario: A launch without a spec records no spec id
- GIVEN a bare
git paw startwith explicit branches - WHEN the session is saved and the JSON inspected
- THEN no worktree entry SHALL contain a
spec_idkey
Scenario: Older session files load with no spec id
- GIVEN a session JSON file whose worktree entries have no
spec_idfield - WHEN the session is loaded
- THEN loading SHALL succeed without error
- AND every entry’s
spec_idSHALL beNone
Requirement: Session records its pane layout
The Session struct SHALL include an optional field recording the window’s pane layout, written as the agent grid by every start, recovery and restart that builds or rebuilds the window. The field SHALL be annotated #[serde(default, skip_serializing_if = "Option::is_none")] so session files written earlier load unchanged. A session without the field SHALL be read as the agent grid when its mode is supervised, and as the earlier tiled layout when its mode is bare; consumers that depend on the geometry (add, remove) SHALL decide from this field and the mode, never from the mode alone.
Scenario: New sessions record the grid
- GIVEN a bare
git paw startwith two branches - WHEN the session state is saved
- THEN the session file SHALL record the grid layout
Scenario: Earlier session files load unchanged
- GIVEN a session file written before this field existed
- WHEN it is loaded
- THEN loading SHALL succeed and the layout field SHALL be absent
Scenario: An earlier bare session reads as tiled
- GIVEN a loaded bare session without the layout field
- WHEN
git paw addinspects its layout - THEN it SHALL be treated as the tiled layout
Scenario: An earlier supervised session reads as the grid
- GIVEN a loaded supervised session without the layout field
- WHEN
git paw addinspects its layout - THEN it SHALL be treated as the agent grid
Requirement: Session file records the authority credential
The Session struct SHALL include an optional field broker_authority: Option<String> annotated #[serde(default, skip_serializing_if = "Option::is_none")], so session files written before this change load with None and a session without a broker serializes byte-identically to before. save_session SHALL write the session file with owner-only permissions (0600) on Unix. The per-repo discovery file (RepoSessionFile) SHALL NOT carry the credential.
Scenario: Credential round-trips
- GIVEN a
Sessionwithbroker_authority = Some("<hex>")saved to disk - WHEN the session is loaded back
- THEN
broker_authoritySHALL beSome("<hex>")
Scenario: Older session files load with None
- GIVEN a session JSON file with no
broker_authorityfield - WHEN the session is loaded
- THEN
broker_authoritySHALL beNone - AND loading SHALL succeed
Scenario: Session file is owner-only
- WHEN
save_sessionwrites a session file on Unix - THEN the file’s mode SHALL be
0600
Scenario: Discovery file never carries the credential
- GIVEN a session with a credential
- WHEN the per-repo discovery file is written
- THEN it SHALL NOT contain a
broker_authoritykey
Configuration
Purpose
Parse TOML configuration from global (~/.config/git-paw/config.toml) and per-repo (.git-paw/config.toml) files. Supports custom CLI definitions, presets, and programmatic add/remove of custom CLIs with repo config overriding global config.
Requirements
Requirement: Parse TOML config with all fields
The system SHALL parse a TOML configuration file containing default_cli, mouse, clis, presets, and optional sections [specs], [logging], [broker], and [supervisor].
Scenario: Config with all fields populated
- GIVEN a TOML file with
default_cli,mouse, custom CLIs, presets,[broker], and[supervisor]sections - WHEN the file is loaded
- THEN all fields SHALL be correctly parsed including supervisor fields
Scenario: All fields are optional
- GIVEN a TOML file with only
default_cli - WHEN the file is loaded
- THEN missing fields SHALL default to
Noneor empty collections - AND
supervisorSHALL beNone
Requirement: Merge repo config over global config
The system SHALL merge per-repo configuration on top of global configuration, with repo values taking precedence for scalar fields and map entries.
Scenario: Repo overrides global scalar fields
- GIVEN global config has
default_cli = "claude"andmouse = true, and repo hasdefault_cli = "gemini" - WHEN configs are merged
- THEN
default_cliSHALL be"gemini"andmouseSHALL betrue(preserved from global)
Test: config::tests::repo_config_overrides_global_scalars
Scenario: CLI maps are merged
- GIVEN global config has CLI
agent-aand repo config has CLIagent-b - WHEN configs are merged
- THEN both CLIs SHALL be present
Test: config::tests::repo_config_merges_cli_maps
Scenario: Repo CLI overrides global CLI with same name
- GIVEN both global and repo define a CLI named
my-agent - WHEN configs are merged
- THEN the repo definition SHALL win
Test: config::tests::repo_cli_overrides_global_cli_with_same_name
Scenario: Only global config exists
- GIVEN a global config file but no repo config
- WHEN
load_config()is called - THEN global values SHALL be used
Test: config::tests::load_config_from_reads_global_file_when_no_repo
Scenario: Only repo config exists
- GIVEN a repo config file but no global config
- WHEN
load_config()is called - THEN repo values SHALL be used
Test: config::tests::load_config_from_reads_repo_file_when_no_global
Requirement: Preset lookup by name
The system SHALL provide access to named presets that define branches and a CLI.
Scenario: Preset accessible by name
- GIVEN a config with a preset named
"backend" - WHEN
get_preset("backend")is called - THEN it SHALL return the preset with its branches and CLI
Test: config::tests::preset_accessible_by_name
Scenario: Missing preset returns None
- GIVEN a config without the requested preset
- WHEN
get_preset("nonexistent")is called - THEN it SHALL return
None
Test: config::tests::preset_returns_none_when_not_in_config
Requirement: Add custom CLIs to global config
The system SHALL add custom CLI definitions to the global config, resolving non-absolute commands via PATH.
Scenario: Add CLI with absolute path
- GIVEN an absolute path to a CLI binary
- WHEN
add_custom_cli()is called - THEN the CLI SHALL be written to the config file
Test: config::tests::add_cli_writes_to_config_file
Scenario: Adding preserves existing entries
- GIVEN an existing CLI in the config
- WHEN a second CLI is added
- THEN both CLIs SHALL be present
Test: config::tests::add_cli_preserves_existing_entries
Scenario: Adding CLI with missing command fails
- GIVEN a command that does not exist on PATH
- WHEN
add_custom_cli()is called - THEN it SHALL return an error mentioning “not found on PATH”
Test: config::tests::add_cli_errors_when_command_not_on_path
Requirement: Remove custom CLIs from global config
The system SHALL remove a custom CLI by name, returning an error if the CLI is not found.
Scenario: Remove existing CLI
- GIVEN a config with CLIs
keep-meandremove-me - WHEN
remove_custom_cli("remove-me")is called - THEN only
keep-meSHALL remain
Test: config::tests::remove_cli_deletes_entry_from_config_file
Scenario: Remove nonexistent CLI returns error
- GIVEN a config without the named CLI
- WHEN
remove_custom_cli()is called - THEN it SHALL return
PawError::CliNotFound
Test: config::tests::remove_nonexistent_cli_returns_cli_not_found_error
Scenario: Remove CLI from empty/missing config returns error
- GIVEN no config file exists
- WHEN
remove_custom_cli()is called - THEN it SHALL return
PawError::CliNotFound
Test: config::tests::remove_cli_from_empty_config_returns_error
Requirement: Config survives round-trip serialization
A PawConfig SHALL be identical after save and reload.
Scenario: Config round-trip
- GIVEN a fully populated config
- WHEN saved and loaded back
- THEN it SHALL be equal to the original
Test: config::tests::config_survives_save_and_load
Requirement: Config loading SHALL work with real files
The system SHALL provide a load_config(repo_root, user_config_path) function that loads the merged PawConfig from the per-repo .git-paw/config.toml and a user-level (global) config.toml. The second parameter user_config_path: Option<&Path> SHALL control which file is read as the user-level config:
- When
user_config_pathisNone, the loader SHALL resolve the user-level config path via the platform-default helper (global_config_path()→crate::dirs::config_dir().join("git-paw/config.toml")), preserving the v0.4 production behaviour. - When
user_config_pathisSome(p), the loader SHALL readpas the user-level config and SHALL NOT consult the platform-default helper. Ifpdoes not exist on disk, the user-level side of the merge SHALL be the defaultPawConfig, exactly as if no file existed at the platform-default path.
The merge semantics on top of the user-level config (per-repo config overrides user-level for scalar fields and map entries) are unchanged from prior requirements in this capability.
Scenario: Defaults when no files exist
- GIVEN a temp directory with no config files
- AND
load_configis called withuser_config_path = None - WHEN
load_config()is called - THEN all fields SHALL be None/empty
Test: config_integration::load_config_returns_defaults_when_no_files_exist
Scenario: Reads repo .git-paw/config.toml
- GIVEN a
.git-paw/config.tomlwith default_cli and mouse - AND
load_configis called withuser_config_path = Some(&unused_temp_path) - WHEN
load_config()is called - THEN the values SHALL be read correctly
Test: config_integration::load_config_reads_repo_config
Scenario: Repo config with custom CLIs
- GIVEN a
.git-paw/config.tomlwith two custom CLIs - AND
load_configis called withuser_config_path = Some(&unused_temp_path) - WHEN
load_config()is called - THEN both CLIs SHALL be parsed with correct fields
Test: config_integration::repo_config_with_custom_clis
Scenario: Repo config with presets
- GIVEN a
.git-paw/config.tomlwith two presets - AND
load_configis called withuser_config_path = Some(&unused_temp_path) - WHEN
load_config()is called - THEN presets SHALL be accessible with correct branches and CLI
Test: config_integration::repo_config_with_presets
Scenario: Default PawConfig has no presets
- GIVEN a default
PawConfig - WHEN
get_preset("nonexistent")is called - THEN it SHALL return
None
Test: config_integration::get_preset_returns_none_for_unknown
Scenario: Repo config overrides default fields
- GIVEN a
.git-paw/config.tomlwith specific values - AND
load_configis called withuser_config_path = Some(&unused_temp_path) - WHEN
load_config()is called - THEN the repo values SHALL take precedence
Test: config_integration::repo_config_overrides_default_fields
Scenario: Repo config path is correct
- GIVEN a temp directory
- WHEN
repo_config_path()is called - THEN it SHALL return
<dir>/.git-paw/config.toml
Test: config_integration::repo_config_path_is_in_repo_root
Scenario: Malformed TOML returns error
- GIVEN a
.git-paw/config.tomlwith invalid TOML - AND
load_configis called withuser_config_path = Some(&unused_temp_path) - WHEN
load_config()is called - THEN it SHALL return an error
Test: config_integration::malformed_toml_returns_error
Scenario: Empty config file is valid
- GIVEN an empty
.git-paw/config.toml - AND
load_configis called withuser_config_path = Some(&unused_temp_path) - WHEN
load_config()is called - THEN it SHALL return a default config
Test: config_integration::empty_config_file_is_valid
Scenario: None preserves platform-default user-config resolution
- GIVEN a repo
TempDirwith no.git-paw/config.toml - AND the platform-default user config path (
crate::dirs::config_dir().join("git-paw/config.toml")) is a readable file containing a custom CLI namedglobally-registered - WHEN
load_config(&repo, None)is called - THEN the returned
PawConfig.clisSHALL containglobally-registered - AND the loader SHALL have resolved the user-level path via
global_config_path(), exactly matching v0.4 behaviour
Test: config::tests::load_config_with_none_reads_platform_default_global
Scenario: Some(path) pins the user-level read to that path
- GIVEN a
TempDircontaining two distinct files:tmp/global-A.tomldefining custom CLIcli-Atmp/global-B.tomldefining custom CLIcli-B
- AND an unrelated CLI
cli-Cis registered at the platform-default user-config path - WHEN
load_config(&repo, Some(&tmp.join("global-A.toml")))is called - THEN the returned
PawConfig.clisSHALL containcli-A - AND it SHALL NOT contain
cli-Borcli-C
Test: config::tests::load_config_with_some_pins_global_to_override_path
Scenario: Some(nonexistent path) returns defaults for the user-level side
- GIVEN a
TempDirand a pathtmp/does-not-exist.tomlthat has never been written - AND an unrelated CLI
cli-leakis registered at the platform-default user-config path - WHEN
load_config(&repo, Some(&tmp.join("does-not-exist.toml")))is called - THEN the user-level side of the merge SHALL be the default
PawConfig - AND the returned
PawConfig.clisSHALL NOT containcli-leak - AND no error SHALL be returned (a missing user-config file is not an error)
Test: config::tests::load_config_with_some_nonexistent_returns_defaults
Scenario: Override path does not affect repo-config resolution
- GIVEN a
TempDirwith.git-paw/config.tomldefiningdefault_cli = "claude" - AND a separate path
tmp/global.tomldefiningdefault_cli = "gemini" - WHEN
load_config(&tmp, Some(&tmp.join("global.toml")))is called - THEN the repo-level
default_cli = "claude"SHALL override the user-leveldefault_cli = "gemini"per the existing repo-overrides-user merge semantics - AND the override parameter SHALL only control which user-level file is read, never the repo-level resolution
Test: config::tests::load_config_override_does_not_affect_repo_resolution
Requirement: Custom CLI management SHALL persist through file I/O
Scenario: Add CLI with absolute path
- GIVEN no config file
- WHEN
add_custom_cli_to()is called with an absolute path - THEN the CLI SHALL be persisted and reloadable
Test: config_integration::add_custom_cli_with_absolute_path
Scenario: Add CLI with display name
- GIVEN no config file
- WHEN
add_custom_cli_to()is called with a display name - THEN the display name SHALL be persisted
Test: config_integration::add_custom_cli_with_display_name
Scenario: Multiple CLIs preserved across adds
- GIVEN 4 CLIs added sequentially
- WHEN the config is loaded
- THEN all 4 SHALL be present with correct fields
Test: config_integration::add_multiple_custom_clis_preserves_all
Scenario: Adding overwrites existing entry
- GIVEN a CLI with name
my-agentalready exists - WHEN
add_custom_cli_to()is called with the same name but different values - THEN the new values SHALL replace the old
Test: config_integration::add_cli_overwrites_existing_entry
Scenario: Add CLI with nonexistent command fails
- GIVEN a non-absolute command that is not on PATH
- WHEN
add_custom_cli_to()is called - THEN it SHALL return an error
Test: config_integration::add_cli_with_nonexistent_path_command_fails
Scenario: Remove custom CLI
- GIVEN two CLIs in the config
- WHEN one is removed
- THEN only the other SHALL remain
Test: config_integration::remove_custom_cli
Scenario: Remove nonexistent CLI returns error
- GIVEN no CLIs in the config
- WHEN
remove_custom_cli_from()is called - THEN it SHALL return an error
Test: config_integration::remove_nonexistent_cli_returns_error
Scenario: Remove all CLIs leaves empty config
- GIVEN one CLI in the config
- WHEN it is removed
- THEN the CLI map SHALL be empty
Test: config_integration::remove_all_custom_clis_leaves_empty_config
Requirement: Global and repo config SHALL merge custom CLIs correctly
Scenario: Repo custom CLIs merge with global
- GIVEN global config with 2 CLIs and repo config with 2 CLIs (one overlapping)
- WHEN
load_config_from()is called - THEN the result SHALL have 3 CLIs, with repo winning on collision
Test: config_integration::repo_custom_clis_merge_with_global_custom_clis
Requirement: Config SHALL handle many custom CLIs
Scenario: Config with 10 custom CLIs
- GIVEN a config file with 10 custom CLI definitions
- WHEN
load_config()is called - THEN all 10 SHALL be parsed correctly
Test: config_integration::config_with_many_custom_clis
Requirement: The system SHALL support a default_spec_cli config field
The system SHALL support a default_spec_cli field in PawConfig that specifies the CLI to use for --from-specs branches that don’t have a paw_cli override, bypassing the interactive picker.
Scenario: default_spec_cli set
- WHEN a config has
default_spec_cli = "claude" - THEN
PawConfig.default_spec_cliSHALL beSome("claude")
Scenario: default_spec_cli absent
- WHEN a config has no
default_spec_clifield - THEN
PawConfig.default_spec_cliSHALL beNone
Scenario: Merge preserves repo override
- WHEN global config has
default_spec_cli = "claude"and repo config hasdefault_spec_cli = "gemini" - THEN the merged config SHALL have
default_spec_cli = Some("gemini")
Requirement: Repo SHALL override new v0.2.0 scalar fields
Scenario: Repo overrides new v0.2.0 scalar fields
- GIVEN global config has
default_spec_cli = "claude"and repo hasdefault_spec_cli = "gemini" - WHEN configs are merged
- THEN
default_spec_cliSHALL be"gemini"
Requirement: Specs configuration section
The system SHALL support an optional [specs] section with a dir field and a type field. Field names SHALL match the spec-scanning capability and the implementation in src/config.rs::SpecsConfig.
dir: String— path (relative to the repo root) to the directory containing spec filestype: String— backend identifier (e.g."openspec","markdown"); the field is exposed asspec_typein Rust to avoid clashing with thetypekeyword and is serialised astypein TOML/JSON via#[serde(rename = "type")]
When the [specs] section is absent, the optional specs field on PawConfig SHALL be None.
Scenario: Specs section with all fields
- GIVEN a TOML file with
[specs]containingdir = "openspec/specs"andtype = "openspec" - WHEN the file is loaded
- THEN
specs.dirSHALL be"openspec/specs" - AND
specs.spec_typeSHALL be"openspec"
Scenario: Specs section defaults
- GIVEN a TOML file without a
[specs]section - WHEN the file is loaded
- THEN
specsSHALL beNone
Scenario: Round-trip preserves rename
- GIVEN a
SpecsConfig { dir: "openspec/specs".into(), spec_type: "openspec".into() } - WHEN the value is serialised to TOML and parsed back
- THEN the resulting TOML SHALL contain
type = "openspec"(notspec_type) - AND parsing SHALL succeed and reproduce the original struct
Requirement: Logging configuration section
The system SHALL support an optional [logging] section with enabled and log_dir fields.
Scenario: Logging section with all fields
- GIVEN a TOML file with
[logging]containingenabled = trueandlog_dir = ".git-paw/logs" - WHEN the file is loaded
- THEN
logging.enabledSHALL betrueandlogging.log_dirSHALL be".git-paw/logs"
Scenario: Logging section defaults
- GIVEN a TOML file without a
[logging]section - WHEN the file is loaded
- THEN
loggingSHALL beNone
Requirement: Default config generation
The system SHALL provide a function to generate a default config.toml string with active defaults and commented-out fields including the [supervisor] section.
Scenario: Generated config contains commented supervisor examples
- WHEN the default config string is generated
- THEN it SHALL contain commented-out examples for
[supervisor]withenabled,cli,test_command, andagent_approvalfields
Scenario: Generated config contains commented examples
- WHEN the default config string is generated
- THEN it SHALL contain commented-out examples for
default_spec_cli,branch_prefix,[specs],[logging],[broker], and[supervisor]
Requirement: Config round-trip with new fields
A PawConfig with v0.2.0 fields populated SHALL be identical after save and reload.
Scenario: Config with specs and logging round-trips
- GIVEN a config with
default_spec_cli,branch_prefix,specs, andloggingpopulated - WHEN saved and loaded back
- THEN it SHALL be equal to the original
Requirement: Broker configuration section
The system SHALL support an optional [broker] section with the following fields:
enabled: bool— defaults tofalsewhen the field or section is absentport: u16— defaults to9119when absentbind: String— defaults to"127.0.0.1"when absent
The BrokerConfig struct SHALL provide a url(&self) -> String method returning http://<bind>:<port>.
Scenario: Broker section with all fields
- GIVEN a TOML file with
[broker]containingenabled = true,port = 9200,bind = "127.0.0.1" - WHEN the file is loaded
- THEN
broker.enabledSHALL betrue,broker.portSHALL be9200,broker.bindSHALL be"127.0.0.1"
Scenario: Broker section defaults
- GIVEN a TOML file without a
[broker]section - WHEN the file is loaded
- THEN
brokerSHALL haveenabled = false,port = 9119,bind = "127.0.0.1"
Scenario: Partial broker section
- GIVEN a TOML file with
[broker]containing onlyenabled = true - WHEN the file is loaded
- THEN
broker.enabledSHALL betrue,broker.portSHALL be9119,broker.bindSHALL be"127.0.0.1"
Scenario: BrokerConfig url method
- GIVEN
BrokerConfig { enabled: true, port: 9200, bind: "127.0.0.1" } - WHEN
url()is called - THEN the result SHALL be
"http://127.0.0.1:9200"
Scenario: Broker config round-trips through save and load
- GIVEN a config with
[broker]fully populated - WHEN saved and loaded back
- THEN all broker fields SHALL match the original
Requirement: Internal callers SHALL preserve v0.4 behaviour by passing None
All production call sites of load_config inside the git-paw binary SHALL pass None as the user_config_path argument, so production behaviour is byte-identical to the v0.4 single-argument load_config(repo_root) API.
The Option<&Path> argument SHALL exist only to give test code a discoverable way to isolate the user-level config read from whatever exists at the dev machine’s platform-default path. No production code path SHALL pass Some(_).
Scenario: All production call sites pass None
- GIVEN the source tree
- WHEN every call site of
config::load_configinsidesrc/is inspected - THEN every call SHALL be of the form
config::load_config(&repo_root, None)(modulo whitespace and the exact name of therepo_rootbinding) - AND no production call site SHALL pass
Some(_)
Test: covered by cargo build (compile-time) plus a focused grep-style assertion in src/main.rs::tests or equivalent — see tasks.md task 2.
Scenario: Production behaviour is byte-identical to v0.4
- GIVEN a binary built from this change
- AND the same
.git-paw/config.tomland platform-default user config that a v0.4 binary would read - WHEN any production command that calls
load_configruns (e.g.git paw start,git paw add-cli,git paw dashboard) - THEN the merged
PawConfigthe command operates on SHALL be equal to the mergedPawConfigv0.4 would have produced
Test: behaviour preserved by construction (every production call passes None); verified by the v0.4 test suite continuing to pass unchanged plus the new load_config_with_none_reads_platform_default_global unit test.
Requirement: MCP configuration section
The system SHALL parse an optional [mcp] section in .git-paw/config.toml into an McpConfig exposed as PawConfig.mcp. The struct SHALL contain an optional name: Option<String> field defaulting to None when absent, and an allow_repo_writes: bool field defaulting to false when absent and omitted from serialized output when false. A config file with no [mcp] section SHALL load with McpConfig::default() (name: None, allow_repo_writes: false), so pre-existing configs load unchanged. When the user-level and repository configs are merged, allow_repo_writes SHALL take the repository config’s value only; a value set in the user-level config SHALL have no effect.
Scenario: Config with [mcp] name parses the field
- GIVEN a config file with
[mcp]settingname = "my-project" - WHEN the config is loaded
- THEN
PawConfig.mcp.nameSHALL beSome("my-project")
Scenario: Config with [mcp] allow_repo_writes parses the field
- GIVEN a repository config file with
[mcp]settingallow_repo_writes = true - WHEN the config is loaded
- THEN
PawConfig.mcp.allow_repo_writesSHALL betrue
Scenario: Config without [mcp] section loads with defaults
- GIVEN a config file with no
[mcp]section - WHEN the config is loaded
- THEN
PawConfig.mcpSHALL equalMcpConfig::default()withnameset toNoneandallow_repo_writesset tofalse - AND loading SHALL NOT error
Scenario: The user-level config cannot enable repository writes
- GIVEN a user-level config setting
[mcp] allow_repo_writes = true - AND a repository config that does not set it
- WHEN the merged config is loaded
- THEN
PawConfig.mcp.allow_repo_writesSHALL befalse
Scenario: MCP config survives round-trip serialization
- GIVEN a
PawConfigwhosemcp.nameis set and whosemcp.allow_repo_writesistrue - WHEN it is serialized to TOML and re-parsed
- THEN the re-parsed
mcpfield SHALL equal the original
Scenario: A false switch is not serialized
- GIVEN a
PawConfigwhosemcp.allow_repo_writesisfalse - WHEN it is serialized to TOML
- THEN the output SHALL NOT contain
allow_repo_writes
Requirement: Worktree placement configuration field
The system SHALL support an optional worktree_placement field on
PawConfig accepting the string values "child" or "sibling". When the
field is absent, the effective placement SHALL be sibling, preserving
v0.7.0 behaviour. The field SHALL participate in the repo-overrides-global
merge as a scalar (repo value wins). A default-valued field SHALL be
skipped on serialization so that a config without an explicit
worktree_placement round-trips without the field appearing.
Scenario: worktree_placement set to child
- GIVEN a TOML file with
worktree_placement = "child" - WHEN the config is loaded
- THEN the effective placement SHALL be
child
Scenario: worktree_placement set to sibling
- GIVEN a TOML file with
worktree_placement = "sibling" - WHEN the config is loaded
- THEN the effective placement SHALL be
sibling
Scenario: worktree_placement absent defaults to sibling
- GIVEN a TOML file with no
worktree_placementfield - WHEN the config is loaded
- THEN the effective placement SHALL be
sibling
Scenario: Repo placement overrides global
- GIVEN global config has
worktree_placement = "sibling"and repo config hasworktree_placement = "child" - WHEN configs are merged
- THEN the merged placement SHALL be
child
Scenario: Placement config survives round-trip
- GIVEN a
PawConfigwithworktree_placement = "child" - WHEN it is serialized to TOML and re-parsed
- THEN the re-parsed placement SHALL be
child
Scenario: Pre-existing config without the field loads unchanged
- GIVEN a
.git-paw/config.tomlwritten before this field exists - WHEN the config is loaded
- THEN loading SHALL NOT error
- AND the effective placement SHALL be
sibling
Requirement: Broker log panel height configuration
The system SHALL support an optional height_lines field on the
[dashboard.broker_log] table, a positive integer giving the number of
terminal rows the Broker log panel occupies when visible. When the field
or the [dashboard.broker_log] table is absent, the effective height
SHALL default to a value strictly greater than the v0.6.0 fixed 12
(the documented default), so configs written before this field exists —
including existing configs — load unchanged. The field SHALL
participate in the repo-overrides-global merge as a scalar (repo value
wins) and SHALL survive round-trip serialization.
Scenario: height_lines explicitly configured
- GIVEN a TOML file with
[dashboard.broker_log] height_lines = 24 - WHEN the config is loaded
- THEN
dashboard.broker_log.height_linesSHALL be24
Scenario: height_lines absent uses the default
- GIVEN a TOML file with a
[dashboard.broker_log]table that omitsheight_lines - WHEN the config is loaded
- THEN
dashboard.broker_log.height_linesSHALL equal the documented default, which SHALL be strictly greater than12
Scenario: Pre-existing config without the field loads unchanged
- GIVEN a
.git-paw/config.tomlwritten before this field exists (e.g. a[dashboard]section with nobroker_logtable) - WHEN the config is loaded
- THEN loading SHALL NOT error
- AND
dashboard.broker_log.height_linesSHALL equal the documented default
Scenario: height_lines round-trips through save and load
- GIVEN a config with
[dashboard.broker_log] height_lines = 30 - WHEN the config is serialized to TOML and re-parsed
- THEN the re-parsed
dashboard.broker_log.height_linesSHALL be30
Requirement: Configuration reference documents the specs type value
docs/src/configuration/README.md’s commented [specs] example SHALL document type as accepting "openspec", "markdown", AND "speckit". The v0.4-era example listing only "openspec" and "markdown" SHALL be updated to add "speckit".
Scenario: Configuration reference lists all three specs.type values
- WHEN the
[specs]example indocs/src/configuration/README.mdis inspected - THEN it documents
"speckit"as a valid value alongside"openspec"and"markdown"
Requirement: Configuration reference has no dangling internal links
docs/src/configuration/README.md SHALL NOT contain Markdown
links pointing at anchors that do not exist in their target
files. Specifically the link previously pointing at
coordination.md#automatic-conflict-detection-v050 SHALL either
target a real anchor that exists in coordination.md post-refresh
or be removed.
Scenario: Configuration reference has no broken conflict-detection anchor
- WHEN
docs/src/configuration/README.mdis inspected - THEN any
[...]( ... )link targetingcoordination.mdpoints at an anchor that exists incoordination.md(verified by the anchor heading being present in the target file’s rendered TOC)
Requirement: Configuration reference documents the configuration tables
docs/src/configuration/README.md SHALL include subsections
documenting [supervisor.conflict], [supervisor.learnings_config],
and [governance]. Each subsection SHALL list every field, its type,
its default value (where applicable), and a one-line description.
It SHALL also document [supervisor.auto_approve] and
[supervisor.common_dev_allowlist] as deprecated: both are still
accepted, have no effect, print a deprecation warning when present,
and can be removed.
Scenario: Configuration reference documents supervisor.conflict
- WHEN
docs/src/configuration/README.mdis inspected - THEN it contains a subsection or table for
[supervisor.conflict] - AND the subsection mentions
window_seconds,warn_on_intent_overlap, andescalate_on_violation
Scenario: Configuration reference marks the auto-approval tables deprecated
- WHEN
docs/src/configuration/README.mdis inspected - THEN it names
[supervisor.auto_approve]and[supervisor.common_dev_allowlist] - AND states that both are deprecated and have no effect
- AND points to the agent CLI’s own permission rules, or the
full-autoapproval level inside a sandbox, as the replacement
Scenario: Configuration reference documents supervisor.learnings_config
- WHEN
docs/src/configuration/README.mdis inspected - THEN it contains a subsection or table for
[supervisor.learnings_config] - AND the subsection mentions
flush_interval_seconds
Scenario: Configuration reference documents governance
- WHEN
docs/src/configuration/README.mdis inspected - THEN it contains a subsection or table for
[governance] - AND the subsection lists all five fields:
adr,test_strategy,security,dod,constitution
Interactive Selection
Purpose
Interactive selection prompts for choosing branches and AI CLIs. Supports uniform (same CLI for all branches) and per-branch assignment modes, with CLI flags that skip prompts. Logic is separated from UI via the Prompter trait for testability.
Requirements
Requirement: CLI flags skip all prompts when both provided
When both --cli and --branches flags are provided, the system SHALL skip all interactive prompts and map the CLI to all specified branches.
Scenario: Both flags skip all prompts
- GIVEN
--cli alphaand--branches feature/auth,fix/apiflags - WHEN
run_selection()is called - THEN it SHALL return mappings without invoking any prompts
Test: interactive::tests::both_flags_skips_all_prompts_and_maps_cli_to_all_branches
Requirement: CLI flag skips CLI prompt but prompts for branches
When only --cli is provided, the system SHALL prompt for branch selection but skip CLI selection.
Scenario: CLI flag provided, branches prompted
- GIVEN
--cli alphaflag and no branches flag - WHEN
run_selection()is called - THEN branch selection SHALL be prompted and the flag CLI SHALL be used
Test: interactive::tests::cli_flag_skips_cli_prompt_but_prompts_for_branches
Requirement: Branches flag skips branch prompt but prompts for CLI
When only --branches is provided, the system SHALL skip branch selection but prompt for CLI assignment.
Scenario: Branches flag provided, CLI prompted in uniform mode
- GIVEN
--branchesflag and no CLI flag - WHEN user selects uniform mode
- THEN the selected CLI SHALL be mapped to all flagged branches
Test: interactive::tests::branches_flag_skips_branch_prompt_but_prompts_for_cli_uniform
Requirement: Uniform mode maps same CLI to all branches
In uniform mode, the system SHALL assign the selected CLI to every selected branch.
Scenario: Uniform mode selection
- GIVEN user selects uniform mode, picks 2 branches and 1 CLI
- WHEN
run_selection()completes - THEN both branches SHALL be mapped to the same CLI
Test: interactive::tests::uniform_mode_maps_same_cli_to_all_selected_branches
Requirement: Per-branch mode maps different CLIs to each branch
In per-branch mode, the system SHALL prompt for a CLI for each selected branch individually.
Scenario: Per-branch mode selection
- GIVEN user selects per-branch mode with 2 branches
- WHEN different CLIs are chosen for each branch
- THEN each branch SHALL be mapped to its respective CLI
Test: interactive::tests::per_branch_mode_maps_different_cli_to_each_branch
Scenario: Per-branch mode with branches flag
- GIVEN branches provided via flag and per-branch mode selected
- WHEN different CLIs are chosen
- THEN each flagged branch SHALL be mapped to its selected CLI
Test: interactive::tests::per_branch_mode_with_branches_flag
Requirement: Error when no CLIs available
The system SHALL return PawError::NoCLIsFound when the CLI list is empty.
Scenario: Empty CLI list
- GIVEN no CLIs available
- WHEN
run_selection()is called - THEN it SHALL return
Err(PawError::NoCLIsFound)
Test: interactive::tests::no_clis_available_returns_error
Requirement: Error when no branches available
The system SHALL return PawError::BranchError when the branch list is empty.
Scenario: Empty branch list
- GIVEN no branches available
- WHEN
run_selection()is called - THEN it SHALL return
Err(PawError::BranchError)
Test: interactive::tests::no_branches_available_returns_error
Requirement: User cancellation propagates as PawError::UserCancelled
The system SHALL propagate cancellation (Ctrl+C or empty selection) as PawError::UserCancelled.
Scenario: User cancels branch selection
- GIVEN user presses Ctrl+C during branch selection
- WHEN
run_selection()is called - THEN it SHALL return
Err(PawError::UserCancelled)
Test: interactive::tests::user_cancels_branch_selection_returns_cancelled
Scenario: User selects no branches
- GIVEN user confirms with zero branches selected
- WHEN
run_selection()is called - THEN it SHALL return
Err(PawError::UserCancelled)
Test: interactive::tests::user_selects_no_branches_returns_cancelled
Scenario: User cancels CLI selection
- GIVEN user presses Ctrl+C during CLI selection
- WHEN
run_selection()is called - THEN it SHALL return
Err(PawError::UserCancelled)
Test: interactive::tests::user_cancels_cli_selection_returns_cancelled
Requirement: Subset branch selection
The system SHALL support selecting a subset of available branches.
The branch picker (select_branches) SHALL present a fuzzy-filter
multi-select. The user SHALL be able to type a query that filters the visible
branch candidates by case-insensitive substring match against the branch name.
An empty query SHALL show the full list of available branches in their original
order. Toggling selection SHALL operate on the currently visible (filtered)
rows; branches selected under one query that are then hidden by a different
query SHALL remain selected and SHALL be returned on confirm. Clearing the
query (back to empty) SHALL restore the full list with all prior selections
intact.
Cancellation semantics SHALL be unchanged: pressing Ctrl+C SHALL yield
PawError::UserCancelled, and confirming with zero branches selected SHALL
yield PawError::UserCancelled.
Scenario: Selecting one of two branches
- GIVEN 2 available branches
- WHEN user selects only the second
- THEN only that branch SHALL appear in the result
Test: interactive::tests::selecting_subset_of_branches_works
Scenario: Typing a query filters the branch candidates
- GIVEN branches
feature/auth,fix/api,feature/login - WHEN the user types
featureinto the branch picker filter - THEN only
feature/authandfeature/loginSHALL be visible - AND
fix/apiSHALL NOT be visible
Scenario: Empty filter shows the full branch list
- GIVEN the branch picker with 3 candidates
- WHEN the filter query is empty
- THEN all 3 branches SHALL be visible in their original order
Scenario: Selection under an active filter is preserved when the filter changes
- GIVEN branches
feature/auth,fix/api,feature/login - WHEN the user types
feature, togglesfeature/auth, then changes the query tofixand togglesfix/api, then confirms - THEN the result SHALL contain both
feature/authandfix/api
Scenario: Clearing the filter restores the full list with selections intact
- GIVEN the branch picker where the user has typed
featureand toggledfeature/auth - WHEN the user clears the query back to empty
- THEN all branches SHALL be visible again
- AND
feature/authSHALL still be marked as selected
Scenario: User cancels the filtered branch picker via Ctrl+C
- GIVEN the branch picker is open with an active filter query
- WHEN the user presses Ctrl+C
- THEN
select_branchesSHALL returnErr(PawError::UserCancelled)
Scenario: Confirming with zero branches selected cancels
- GIVEN the branch picker is open
- WHEN the user confirms without toggling any branch
- THEN
select_branchesSHALL returnErr(PawError::UserCancelled)
Requirement: CliMode display format
The CliMode enum SHALL display as human-readable descriptions.
Scenario: CliMode display strings
- GIVEN
CliMode::UniformandCliMode::PerBranch - WHEN formatted with
Display - THEN they SHALL render as
"Same CLI for all branches"and"Different CLI per branch"
Test: interactive::tests::cli_mode_display
Requirement: CliInfo display format
CliInfo SHALL display as the binary name when it matches the display name, or as "DisplayName (binary)" when they differ.
Scenario: Same display and binary name
- GIVEN a
CliInfowheredisplay_nameequalsbinary_name - WHEN formatted with
Display - THEN it SHALL render as just the binary name
Test: interactive::tests::cli_info_display_same_names
Scenario: Different display and binary name
- GIVEN a
CliInfowheredisplay_namediffers frombinary_name - WHEN formatted with
Display - THEN it SHALL render as
"DisplayName (binary_name)"
Test: interactive::tests::cli_info_display_different_names
Requirement: CLI picker with optional pre-selection
The select_cli method on the Prompter trait SHALL accept an optional default CLI name for pre-selection in the interactive picker.
Scenario: Picker with default pre-selected
- WHEN
select_cli()is called withdefault = Some("claude")and"claude"is in the CLI list - THEN the picker SHALL display with
"claude"highlighted as the default selection
Scenario: Picker without default
- WHEN
select_cli()is called withdefault = None - THEN the picker SHALL display with the first item selected (no pre-selection)
Scenario: Default CLI not in available list
- WHEN
select_cli()is called withdefault = Some("nonexistent")and that CLI is not available - THEN the picker SHALL display with no pre-selection (graceful fallback)
Requirement: Spec multi-select picker
The Prompter trait SHALL include a select_specs(&self, specs: &[SpecEntry]) -> Result<Vec<SpecEntry>, PawError> method that presents a multi-select picker for spec entries and returns the user’s chosen subset.
The default TerminalPrompter implementation SHALL display one row per logical spec unit (a feature in Spec Kit terms; a change in OpenSpec; a file in plain Markdown), grouping multiple SpecEntry values that decompose from the same Spec Kit feature into a single row. Selecting a row SHALL cause every SpecEntry belonging to that row’s logical unit to be returned.
Each row’s display label SHALL include the unit identifier and, for Spec Kit features that decompose into multiple worktrees, a worktree-count hint summarising the breakdown (e.g. "003-user-list — 3 worktrees: 2 [P] + 1 phase/"). For OpenSpec changes and Markdown specs, the label SHALL be the unit identifier alone (one entry → one worktree, no hint needed).
The spec picker SHALL present a fuzzy-filter multi-select that behaves
identically to the branch picker (select_branches). The user SHALL be able to
type a query that filters the visible rows by case-insensitive substring match
against the row’s display label. An empty query SHALL show every grouped row in
its original order. Toggling selection SHALL operate on the currently visible
(filtered) rows; rows selected under one query that are then hidden by a
different query SHALL remain selected and SHALL be returned on confirm.
Clearing the query (back to empty) SHALL restore the full set of rows with all
prior selections intact. The logical-unit grouping and worktree-count hint
labels SHALL be unchanged from the non-filtered behavior — filtering matches
against the displayed labels, and selecting a visible row SHALL still expand to
every underlying SpecEntry for that unit.
The picker SHALL behave the same way as select_branches for cancellation:
- User pressing Ctrl+C →
PawError::UserCancelled. - User confirming with zero rows selected →
PawError::UserCancelled.
Scenario: select_specs returns the chosen subset
- GIVEN 3 OpenSpec entries
add-auth,fix-session,add-logging - WHEN the user toggles
add-authandadd-loggingand presses enter - THEN
select_specsreturns aVeccontaining those two entries
Scenario: select_specs groups Spec Kit entries by feature
- GIVEN 4 SpecEntry values from a Spec Kit project: two
[P]entries (003-user-list-T009,003-user-list-T010), one consolidated entry (003-user-list-phase-2), and one entry from a different feature (004-error-handling-phase-1) - WHEN the picker renders
- THEN it displays exactly 2 rows — one per logical feature
- AND the row for feature
003-user-listshows a worktree-count hint summarising the 3 underlying entries (2 [P] + 1 phase/)
Scenario: Selecting a Spec Kit feature row pulls in all its entries
- GIVEN a picker rendering one row for feature
003-user-list(3 underlying SpecEntry values) - WHEN the user selects only that row and confirms
- THEN
select_specsreturns all 3 underlying SpecEntry values
Scenario: User cancels spec picker via Ctrl+C
- GIVEN the spec picker is open
- WHEN the user presses Ctrl+C
- THEN
select_specsreturnsErr(PawError::UserCancelled)
Scenario: User confirms with zero rows selected
- GIVEN the spec picker is open with N rows displayed
- WHEN the user confirms without toggling any row
- THEN
select_specsreturnsErr(PawError::UserCancelled)
Scenario: Typing a query filters the spec rows
- GIVEN OpenSpec entries
add-auth,fix-session,add-logging - WHEN the user types
addinto the spec picker filter - THEN only the rows for
add-authandadd-loggingSHALL be visible - AND the row for
fix-sessionSHALL NOT be visible
Scenario: Empty filter shows every grouped spec row
- GIVEN the spec picker with 3 grouped rows
- WHEN the filter query is empty
- THEN all 3 rows SHALL be visible in their original order
Scenario: Selecting a filtered Spec Kit row still expands to all its entries
- GIVEN a Spec Kit feature
003-user-list(3 underlying SpecEntry values) and another feature004-error-handling - WHEN the user types
003, selects the visible003-user-listrow, and confirms - THEN
select_specsSHALL return all 3 underlying SpecEntry values for003-user-list
Scenario: Clearing the spec filter restores all rows with selections intact
- GIVEN the spec picker where the user has typed
addand toggled theadd-authrow - WHEN the user clears the query back to empty
- THEN all rows SHALL be visible again
- AND the
add-authrow SHALL still be marked as selected
Requirement: Spec picker requires an interactive terminal
When the start command would invoke select_specs (i.e. the user passed --specs with no values), the system SHALL detect whether stdin is connected to a terminal before invoking the picker. If stdin is NOT a terminal (CI, scripted invocation, redirected input), the system SHALL exit with an actionable error pointing at the explicit forms (--specs NAME[,NAME...] to narrow, --from-all-specs to launch every discovered spec).
The system SHALL NOT block waiting for picker input on a non-interactive stdin.
Scenario: Bare –specs in non-TTY environment exits with guidance
- GIVEN the user runs
git paw start --specswith stdin redirected (or no controlling terminal) - WHEN the start command attempts to open the picker
- THEN the command SHALL exit with a non-zero status before any picker UI is drawn
- AND the error message SHALL point the user at
--specs NAME[,NAME...]and--from-all-specs
Scenario: Bare –specs on TTY proceeds to picker
- GIVEN the user runs
git paw start --specsfrom an interactive terminal - WHEN the start command runs
- THEN the picker SHALL open
- AND no TTY-required error SHALL be emitted
Requirement: Spec name resolution for narrow mode
When --specs is passed with one or more values (narrow mode), the system SHALL resolve each value against the discovered SpecEntry set returned by scan_specs(). Resolution SHALL apply the following matching strategies in order, taking the first that succeeds:
- Exact match on
SpecEntry.id(case-sensitive). For Spec Kit, this matches a specific decomposed entry like003-user-list-T009. For OpenSpec / Markdown, it matches the change name or filename stem. - Spec Kit feature match on the feature directory prefix of the
SpecEntry.id(e.g.003-user-listmatches all entries belonging to that feature). When the value matches a Spec Kit feature unambiguously, ALL entries belonging to that feature SHALL be selected. - Spec Kit numeric prefix match (e.g.
003) matching a Spec Kit feature directory name’s leading numeric portion. The match SHALL succeed only when exactly one feature directory begins with the given prefix followed by a non-digit boundary; ambiguous prefixes SHALL be rejected (see below).
Resolution SHALL fail (and the start command SHALL exit before any worktrees are created) when:
- A value matches no
SpecEntryand no feature. - A Spec Kit numeric prefix matches more than one feature directory (ambiguous).
The resulting error SHALL list the unresolved or ambiguous names AND the discovered candidate names so the user can correct quickly.
Scenario: Exact match resolves to a single SpecEntry
- GIVEN a discovered set including OpenSpec change
add-auth - WHEN the user passes
--specs add-auth - THEN the resolved set SHALL contain exactly that one
SpecEntry
Scenario: Spec Kit feature name resolves to all decomposed entries
- GIVEN a Spec Kit feature
003-user-listdecomposing into 3 SpecEntry values (2[P]+ 1 consolidated) - WHEN the user passes
--specs 003-user-list - THEN the resolved set SHALL contain all 3 entries belonging to that feature
Scenario: Spec Kit numeric prefix resolves unambiguously
- GIVEN a Spec Kit project with a single feature directory beginning with
003-(e.g.003-user-list) - WHEN the user passes
--specs 003 - THEN the resolved set SHALL contain all entries belonging to that feature
Scenario: Ambiguous numeric prefix is rejected
- GIVEN a Spec Kit project containing both
003-user-listand003a-experiment - WHEN the user passes
--specs 003 - THEN the start command SHALL exit with an error
- AND the error message SHALL list both candidate feature names
Scenario: Unknown spec name is rejected with candidate list
- GIVEN a discovered set containing
add-auth,fix-session - WHEN the user passes
--specs no-such-spec - THEN the start command SHALL exit with an error
- AND the error message SHALL include
no-such-spec - AND the error message SHALL list
add-authandfix-sessionas candidates
Scenario: Multiple values are resolved independently
- GIVEN a discovered set including
add-auth,fix-session,add-logging - WHEN the user passes
--specs add-auth,add-logging - THEN the resolved set SHALL contain entries for
add-authandadd-logging - AND the resolved set SHALL NOT contain the entry for
fix-session
Scenario: Partial-failure batches do not partially start
- GIVEN a user passes
--specs add-auth,no-such-spec - WHEN resolution runs
- THEN the start command SHALL exit with the unknown-name error
- AND no worktrees SHALL be created
- AND the error message SHALL include
no-such-spec(the unresolved name)
Requirement: Configured default CLI is pre-selected in the interactive CLI picker
run_selection() SHALL accept an optional default CLI name and SHALL pass it to the CLI picker in both assignment modes: to select_cli() when the user chooses uniform mode, and to select_cli_for_branch() for every branch when the user chooses per-branch mode. The Prompter trait’s select_cli_for_branch() SHALL therefore accept an optional default with the same semantics as select_cli()’s. Every caller of the interactive selection flow (git paw start, and the supervisor launch’s branch selection) SHALL supply the loaded configuration’s default_cli.
default_cli SHALL only choose the picker’s starting entry. It SHALL NOT skip the picker: when --cli is not given, the mode prompt and the CLI picker (or, in per-branch mode, each per-branch picker) SHALL still appear, and the user can accept the pre-selected CLI or pick any other listed CLI. If default_cli is unset, or names a CLI that is not available, the picker SHALL start on the first entry (the existing select_cli() behaviour). --cli SHALL still skip the CLI prompt entirely.
Scenario: default_cli pre-selects in the uniform-mode picker
- GIVEN
default_cli = "beta", no--cliflag, and the user chooses uniform mode - WHEN
run_selection()is called with the default CLI"beta" - THEN
select_cli()SHALL receivedefault = Some("beta") - AND every selected branch SHALL map to the CLI the picker returns
Scenario: default_cli pre-selects in every per-branch picker
- GIVEN
default_cli = "beta", no--cliflag, two selected branches, and the user chooses per-branch mode - WHEN
run_selection()is called with the default CLI"beta" - THEN
select_cli_for_branch()SHALL receivedefault = Some("beta")for each branch - AND each branch SHALL map to the CLI its own picker returns
Scenario: The picker is still shown when default_cli is set
- GIVEN
default_cli = "beta"and no--cliflag - WHEN
run_selection()is called - THEN the mode prompt and the CLI picker SHALL both be invoked
- AND a different CLI the user picks there SHALL be the one mapped to the branches
Scenario: No default_cli starts the picker on the first entry
- GIVEN no
default_cliand no--cliflag, with the user choosing uniform mode - WHEN
run_selection()is called with no default CLI - THEN
select_cli()SHALL receivedefault = None
Scenario: Per-branch picker starts on a configured default
- GIVEN a terminal prompter and a CLI list in which
"beta"is the second entry - WHEN
select_cli_for_branch()is called withdefault = Some("beta") - THEN the picker SHALL display with
"beta"highlighted as the default selection - AND with
default = Noneor a CLI that is not listed, it SHALL start on the first entry
Scenario: –cli flag wins over default_cli
- GIVEN
--cli alphaanddefault_cli = "beta" - WHEN
run_selection()is called - THEN no mode prompt or CLI picker SHALL be invoked
- AND every selected branch SHALL map to
"alpha"
Scenario: Bare git paw start pre-selects the configured default CLI
- GIVEN a repository configuring two available CLIs and
default_clinaming the one that sorts second in the picker - WHEN the user runs
git paw start --branches <branch> --dry-runin a terminal, chooses uniform mode, and confirms the CLI picker without moving the cursor - THEN the dry-run plan SHALL map the branch to the configured
default_cli
Error Handling
Purpose
Define the central error type PawError used across all git-paw modules. Every variant carries an actionable, user-facing message and maps to a process exit code.
Requirements
Requirement: Actionable error messages for each variant
Each PawError variant SHALL produce a user-facing message that explains the problem and suggests a remedy where appropriate.
Scenario: NotAGitRepo is actionable
- GIVEN
PawError::NotAGitRepo - WHEN formatted with
Display - THEN the message SHALL mention “git repository” and name the tool
Test: error::tests::test_not_a_git_repo_is_actionable
Scenario: TmuxNotInstalled includes install instructions
- GIVEN
PawError::TmuxNotInstalled - WHEN formatted with
Display - THEN the message SHALL include both
brew installandapt installhints
Test: error::tests::test_tmux_not_installed_includes_install_instructions
Scenario: NoCLIsFound suggests add-cli
- GIVEN
PawError::NoCLIsFound - WHEN formatted with
Display - THEN the message SHALL suggest the
add-clicommand
Test: error::tests::test_no_clis_found_suggests_add_cli
Scenario: WorktreeError includes detail
- GIVEN
PawError::WorktreeError("failed to create") - WHEN formatted with
Display - THEN the message SHALL include the inner detail string
Test: error::tests::test_worktree_error_includes_detail
Scenario: SessionError includes detail
- GIVEN
PawError::SessionError("file corrupt") - WHEN formatted with
Display - THEN the message SHALL include the inner detail string
Test: error::tests::test_session_error_includes_detail
Scenario: ConfigError includes detail
- GIVEN
PawError::ConfigError("invalid toml") - WHEN formatted with
Display - THEN the message SHALL include the inner detail string
Test: error::tests::test_config_error_includes_detail
Scenario: BranchError includes detail
- GIVEN
PawError::BranchError("not found") - WHEN formatted with
Display - THEN the message SHALL include the inner detail string
Test: error::tests::test_branch_error_includes_detail
Scenario: UserCancelled has a message
- GIVEN
PawError::UserCancelled - WHEN formatted with
Display - THEN the message SHALL not be empty
Test: error::tests::test_user_cancelled_is_not_empty
Scenario: TmuxError includes detail
- GIVEN
PawError::TmuxError("session failed") - WHEN formatted with
Display - THEN the message SHALL include the inner detail string
Test: error::tests::test_tmux_error_includes_detail
Scenario: CliNotFound includes CLI name
- GIVEN
PawError::CliNotFound("my-agent") - WHEN formatted with
Display - THEN the message SHALL include the missing CLI name
Test: error::tests::test_cli_not_found_includes_cli_name
Requirement: Exit codes distinguish cancellation from errors
UserCancelled and NotAttempted SHALL exit with code 2; all other errors SHALL exit with code 1. NotAttempted reports that a command wrote nothing because a condition for proceeding was not met — a declined confirmation, a confirmation that could not be shown because standard input is not a terminal, or a requested item that is not supported or not enabled for its target. Its message SHALL state that condition and, where one exists, name the flag or command that proceeds, and SHALL NOT read only Cancelled..
Scenario: UserCancelled exit code
- GIVEN
PawError::UserCancelled - WHEN
exit_code()is called - THEN it SHALL return
2
Test: error::tests::test_user_cancelled_exit_code
Scenario: NotAttempted exit code and message
- GIVEN
PawError::NotAttemptedbuilt for a confirmation that could not be shown, naming--yes - WHEN
exit_code()is called and the error is formatted withDisplay - THEN
exit_code()SHALL return2 - AND the message SHALL name
--yes - AND the message SHALL NOT be
Cancelled.
Test: error::tests::test_not_attempted_exit_code_and_message
Scenario: General errors exit code
- GIVEN any error variant other than
UserCancelledandNotAttempted - WHEN
exit_code()is called - THEN it SHALL return
1
Test: error::tests::test_general_errors_exit_code
Requirement: Exit method prints to stderr and exits with correct code
PawError::exit() SHALL print the error message to stderr and terminate with the appropriate exit code.
Scenario: NotAGitRepo exits with code 1
- GIVEN the binary is run outside a git repository
- WHEN the error propagates to
exit() - THEN the process SHALL exit with code 1 and stderr SHALL contain the error message
Test: e2e_tests::error_exit_code_is_1_for_not_a_git_repo
Scenario: ConfigError exits with code 1
- GIVEN a nonexistent preset is requested
- WHEN the error propagates to
exit() - THEN the process SHALL exit with code 1 and stderr SHALL mention “not found”
Test: e2e_tests::error_exit_code_is_1_for_preset_not_found
Requirement: Debug representation is derivable
All PawError variants SHALL support Debug formatting.
Scenario: Debug format includes variant name
- GIVEN
PawError::NotAGitRepo - WHEN formatted with
Debug - THEN the output SHALL contain
"NotAGitRepo"
Test: error::tests::test_debug_derived
Requirement: SkillError variants with actionable messages
The system SHALL define a SkillError type with variants for skill loading failures. Each variant SHALL produce a user-facing message that explains the problem and suggests a remedy. SkillError SHALL be wrappable inside PawError as a variant.
The following variants SHALL exist:
UnknownSkill { name: String }— no embedded or user override found for the requested skill nameUserOverrideRead { path: PathBuf, source: std::io::Error }— a user override file exists but cannot be read
Scenario: UnknownSkill is actionable
- GIVEN
SkillError::UnknownSkill { name: "nonexistent" } - WHEN formatted with
Display - THEN the message SHALL mention the skill name
"nonexistent"and indicate no embedded default exists
Scenario: UserOverrideRead is actionable
- GIVEN
SkillError::UserOverrideRead { path: "/home/user/.config/git-paw/agent-skills/coordination.md", .. } - WHEN formatted with
Display - THEN the message SHALL include the file path and suggest checking permissions
Scenario: SkillError exit code
- GIVEN any
SkillErrorvariant wrapped inPawError - WHEN
exit_code()is called - THEN it SHALL return
1
Requirement: BrokerError variants with actionable messages
The system SHALL define a BrokerError type with variants for broker-specific failures. Each variant SHALL produce a user-facing message that explains the problem and suggests a remedy. BrokerError SHALL be wrappable inside PawError as a variant.
The following variants SHALL exist:
PortInUse { port: u16, source: std::io::Error }— the configured port is already occupied by a process that is not a git-paw broker;sourcecarries the underlying bind/probeio::Errorso callers can chain or log the original causePortHeldByOtherRepo { port: u16, repo_root: Option<String> }— the configured port is held by a git-paw broker serving a different repository, or one that reports no repository at all (an older git-paw). Distinct fromPortInUse, because the two have different causes and different fixes and must be distinguishable to users and to testsProbeTimeout { port: u16 }— the stale-broker probe timed outBindFailed(std::io::Error)— socket bind failed for a reason other than port-in-useRuntimeFailed(std::io::Error)— tokio runtime construction failed
PortInUse.source SHALL be marked #[source] (or equivalent thiserror attribute) so it participates in std::error::Error::source() chains. The Display output SHALL NOT include the source by default — it is reserved for explicit chaining via {:?} or programmatic .source() access — to avoid duplicated diagnostics in user-facing CLI output.
PortHeldByOtherRepo.repo_root is reported by whatever process answered the probe and is therefore untrusted; it SHALL be stripped of control characters and capped in length before it appears in the Display output.
Scenario: PortInUse is actionable
- GIVEN
BrokerError::PortInUse { port: 9119, source: io::Error::from(io::ErrorKind::AddrInUse) } - WHEN formatted with
Display - THEN the message SHALL mention port
9119and suggest changing[broker] portin config - AND the message SHALL NOT contain the underlying
io::ErrorDisplay text
Scenario: PortInUse exposes underlying cause
- GIVEN a
PortInUsevalue with anAddrInUsesource - WHEN
std::error::Error::source()is called on it - THEN the result SHALL be
Some(&dyn Error)referencing the wrappedio::Error
Scenario: PortHeldByOtherRepo is actionable
- GIVEN
BrokerError::PortHeldByOtherRepo { port: 9119, repo_root: Some("/home/you/other-repo".to_string()) } - WHEN formatted with
Display - THEN the message SHALL mention port
9119 - AND the message SHALL name the holding repository
/home/you/other-repo - AND the message SHALL tell the user to set a distinct
[broker] port
Scenario: PortHeldByOtherRepo names an unknown holder without guessing
- GIVEN
BrokerError::PortHeldByOtherRepo { port: 9119, repo_root: None } - WHEN formatted with
Display - THEN the message SHALL mention port
9119and the[broker] portfix - AND the message SHALL say an older git-paw broker may be holding the port
- AND the message SHALL NOT claim the broker belongs to the caller
Scenario: PortHeldByOtherRepo renders untrusted text safely
- GIVEN a
PortHeldByOtherRepowhoserepo_rootwas reported with ANSI escape sequences and other control characters - WHEN formatted with
Display - THEN the rendered message SHALL contain no control characters
Scenario: ProbeTimeout is actionable
- GIVEN
BrokerError::ProbeTimeout { port: 9119 } - WHEN formatted with
Display - THEN the message SHALL mention the port and suggest checking for stuck processes
Scenario: BrokerError exit code
- GIVEN any
BrokerErrorvariant wrapped inPawError - WHEN
exit_code()is called - THEN it SHALL return
1
Requirement: A wrapping error’s Display does not embed a redundant error prefix
A PawError variant that wraps a message SHALL NOT embed the word error in its
own Display, because the top-level exit path prepends error: when printing.
Doubling it produces user-facing output like error: Worktree error: …. Each
string-wrapping variant SHALL render as <Category>: <message> (e.g. Worktree: …,
Session: …), so the printed line carries exactly one error: prefix.
Scenario: A wrapping variant prints a single error prefix
- GIVEN a
PawErrorvariant that wraps a message (e.g. a worktree failure) - WHEN its
Displayis rendered and the exit path prependserror: - THEN the resulting line SHALL contain exactly one
error:prefix - AND the variant’s own
DisplaySHALL NOT contain the worderror
Scenario: Standalone messages are unchanged
- GIVEN a variant whose message stands alone (e.g. the not-a-git-repository or cancelled messages)
- WHEN its
Displayis rendered - THEN it SHALL be unchanged by this requirement
Complete capability index
Every capability spec in the repository, grouped by its <namespace>- prefix
(links go to the canonical openspec/specs/ source on GitHub). Each entry’s
blurb is condensed from that spec’s ## Purpose.
A few entries are internal process specs — they govern the test suite,
CI, or the verification workflow itself (e.g. core-ci-hygiene,
core-opsx-role-gating) rather than a user-facing feature. These intentionally
have no user-guide chapter: the spec itself is their documentation.
core-
core-configuration— parses and merges TOML config from global and per-repo files, with repo config overriding global.core-init— bootstraps a repo for git-paw (.git-paw/scaffold, default config,.gitignoreupdate), idempotently.core-error-handling— the centralPawErrortype: every variant carries an actionable message and maps to a process exit code.core-lang-agnostic— keeps bundled skills project-agnostic via config-sourced placeholder substitutions, an authored-in-assets rule for agent-facing prose, and a CI no-language-leak audit.core-ci-hygiene— isolates the test suite from the live environment and closes the local-vs-CI gap via cold-start smoke recipes and convention-enforcement tests.core-memory-isolation— keeps coding agents from writing outside their worktree into the operator’s config and memory territory, carried by the bundled skills’ doctrine and the supervisor’s violation response.core-project-conventions— repo-wide contributor conventions and their doc accuracy: the AGENTS.md dependency table in sync withCargo.toml, the commit-scope list, the mdBook architecture module list and changelog include, and the README’s enumeration of the current user-facing surface.core-governance-config—PawConfig.governance: optional root-relative pointers to a project’s governance/doc artifacts, paths-only with no gating semantics.core-opsx-role-gating— enforces the supervisor-only boundary on/opsx:verifyand/opsx:archive, with a post-commit guard detecting archive activity by coding agents.core-selftest—git paw selftestruns an isolated end-to-end session lifecycle against a throwaway repo with a dummy CLI and reports a single pass/fail verdict.core-security-posture— the consolidated trust-model disclaimer (what the agent CLI’s permission system, the sandbox and remote control do and do not guarantee, and that git-paw makes no safety decision and approves no prompt) and the documented FS-scoped sandbox workflow (sandbox-exec/bwrapvia the custom-CLI seam), plus thegit paw doctorsandbox-availability pointer.
cli-
cli-parsing— the clap v4 CLI: all subcommands, flags, and argument validation, defaulting tostartwhen no subcommand is given.cli-resolution— detects AI coding CLIs on PATH, merges user-defined custom CLIs, and resolves which CLI each branch uses via a priority chain.cli-interactive-selection— interactive prompts for choosing branches and CLIs (uniform or per-branch), with logic separated from UI via thePromptertrait.cli-prompt-profiles— per-CLI prompt-shape profiles: the readiness/approval/mid-response/mode markers and command-header, file-prompt and option-line forms describing how one CLI’s terminal surface looks — appearance only, never permission policy, and never able to widen the live-prompt gate or change which optionsweep.sh approvepresses — with the Claude Code profile embedded as the per-field default and shared as the single source of prompt-shape literals for the compiled code and the supervisor helper script.cli-setup—git paw setup-cli <CLI>, the only path by which git-paw writes into an agent CLI’s settings, run by the user as their consent: a compiled per-CLI fact table with unverified items left plan-only, a printed plan and a confirmation before any write, path-scoped helper allow rules shipped with the rules that guard those helpers against modification, a setup record, and an exact--undo.
code-
code-architecture— the enforceable structural and regression contract for behavior-preserving refactors: byte-identical observable surface (CLI/config/wire), injectable process/tmux/git seam, single newtype construction boundary, domain-module organization with preserved re-exports, frozen serde/wire/lock/SIGHUP surfaces untouched, and test-gated waves.
git-
git-operations— validates repos, lists branches, creates/removes worktrees, and derives worktree directory names — the git plumbing under parallel sessions.git-hook-injection— installs a post-commit dispatcher, pre-push block, and per-worktree branch-guard hooks: committing publishesagent.artifact, pushes are blocked, and pre-existing user content is preserved.git-add-remove-branch—git paw add/removeattach or detach a single agent worktree+pane on a live session, re-tiling the grid without a restart.
tmux-
tmux-orchestration— orchestrates tmux sessions with per-pane CLIs in worktrees via a testable builder with dry-run and the agent-grid layout.
safe-
safe-process-invocation— safe construction of the tmux/process commands built from untrusted inputs: the tmux session name is sanitized at a single boundary and shell-typed paths/commands are shell-quoted, so names with spaces or shell metacharacters cannot break out of a tmux target or a shell word.
session-
session-state— persists one JSON file per session for crash recovery, with atomic writes, tmux-liveness stale detection, and per-repo session receipts.session-logging— captures raw per-pane terminal output via tmux pipe-pane and replays it (ANSI strip/preserve, fuzzy branch match, most-recent auto-select).session-runtime-isolation— per-worktree runtime provisioning: verbatim env-file copy, index-derived port blocks written to a generated.env.local, and consumeron_create/on_removelifecycle hooks (stdout merged into.env.local).
boot-
boot-block— the standardized boot-instruction block injected into each agent: its format/content,{{VARIABLE}}substitution, and the sharedbuild_boot_blockrenderer.boot-agents-md— injects a marker-delimited git-paw section into AGENTS.md and writes per-worktree AGENTS.md combining root content with worktree assignment sections.
broker-
broker-protocol— the complete wire protocol: theBrokerMessagetagged enum,agent_idslugification, publish validation, and the delivery layer’s per-variant routing, sequencing, and cursor polling.broker-runtime— the in-process coordination server:[broker]config, the tokio/axum lifecycle, HTTP surface (/publish,/messages,/status,/log,/watch), and session-lifecycle integration.broker-watcher-and-state— keeps the/statusroster honest as worktrees change: filesystem watcher, roster hygiene, working/committed republishing, terminal-status stickiness, and the introspection surface.broker-agent-helper— bundled shell helpers (broker.shfor agents,sweep.shfor the supervisor) that wrap every broker interaction and detect stuck-agent shapes.broker-conflict-detection— a broker-internal, supervisor-mode detector that flags forward/in-flight/ownership conflicts across agent intents and modified-file sets (with sub-file regions).broker-dashboard— a ratatui TUI observing broker state: an agent-status table with a pinned supervisor row plus a scrolling, filterable broker-log panel.broker-messages— forward-compatible payload deserialization for the wire freeze: no-non-empty-contract list fields default to empty when absent (lean/third-party publishers parse), contracted fields stay required, and serialized output is unchanged.broker-server— runtime robustness of the broker HTTP server: blocking subprocess work runs off the tokio worker threads so a publish burst cannot stall other endpoints, and a poisoned state lock is recovered rather than fatal.
supervisor-
supervisor-launch— orchestrates the full supervisor session launch viagit_paw::commands::supervisor: layout, worktrees, pane structure, boot prompts, and the attach vs--unattendeddrive-loop branch.supervisor-config— defines the[supervisor]config schema: the approval level and its per-CLI flag mapping, nested sub-tables, gate-command templates, and the deprecated-but-parsed auto-approval tables.supervisor-skill-discipline— encodes the disciplines the bundled supervisor skill teaches: sweep-driven pane work, isolated verification worktrees, stream-timeout recovery, per-event verification, and the no-fail-fast testing gate.supervisor-unattended-operation— the in-process drive loop under--unattendedthat keeps a wave moving with no human: polling every pane, escalating each live permission prompt for a human decision without blocking, recovering buffered input, detecting completion and CLI unavailability, and exiting with a per-agent summary. It approves nothing.supervisor-learnings— an opt-in, broker-internal aggregator that derives deterministic and qualitative learning signals into.git-paw/session-learnings.md(and theagent.learningbroker variant), performing no telemetry.supervisor-correction-loop— the optional[supervisor.correction]self-healing loop that re-engages a gate-failed worker’s pane with the gate feedback, bounds retries by a per-branch cycle count, and applies an escalate-or-abandon policy when the budget is exhausted.supervisor-autonomous-orchestrator— the pump-to-brain layer: when a supervisor pane is present, the unattended loop injects each judgment call (risky prompt,agent.question, merge decision, non-converging branch) into that pane and nudges its orchestration sweep, instead of parking them in an unread inbox; with no supervisor pane nothing is injected.supervisor-branch-refresh— opt-in, default-off post-merge rebase of a live worker branch onto the default branch, gated on a conjunction of safety preconditions (clean per the watcher, idle, claimed, predicted conflict-free, behind), with conflict prediction, failed-rebase abort-and-restore, exclusion of verified-awaiting-merge branches, and a HEAD-moved notification that is neveragent.advanced-main.
approval-
approval-auto— the approval-send discipline that remains after the classifier’s retirement: the shared live-prompt gate, and approval keystrokes reaching a pane only through an explicitsweep.sh approvethat re-confirms on a fresh capture, refuses pane 0 and answers once.approval-pattern-surfacing— records every permission prompt the drive loop escalates to per-session JSONL and providesgit paw approvalsto aggregate recurring patterns, hinting which of the agent CLI’s own allowlists could absorb each.approval-command-safety— detects agent-CLI permission prompts non-invasively viatmux capture-paneand the resolved prompt profile, reporting only whether an approval marker is present. git-paw makes no safety decision about an agent’s command; the CLI’s own permission system or the human decides.
spec-
spec-providers— discovers pending specs and represents them as launchableSpecEntryvalues through a pluggable backend system (OpenSpec, Markdown, Spec Kit, superpowers plans).
preflight-
preflight-diagnostics—git paw doctorruns read-only environment/config/repository checks, reporting each as ✓/⚠/✗ with a remedy, a--jsonsurface, and an exit code equal to the worst check.
mcp-
mcp-server—git paw mcpruns a stdio JSON-RPC MCP server exposing read-only, deterministically-sourced tools over a resolved repo/worktree root.mcp-agent-docs— makes the docs site machine-consumable (llms.txt, sitemap, robots, per-page metadata) and bundles a path-allowlisteddocs-fetchhelper and skill.mcp-agent-publish— the agent→broker write path over MCP: a fixed set of publish tools for the four agent boot events (status, artifact, blocked, question), wire-identical to the shell helper, acting as the calling agent only (derivedagent_id, no impersonation) and excluding the supervisor authority verbs so an agent cannot self-verify.
skill-
skill-agent— defines the embedded coordination and supervisor skill templates git-paw injects, their resolution order (user override then embedded default), and the placeholder-rendering contract.skill-standardized— supports the agentskills.io standardized skill format (aSKILL.mddirectory): auto-detecting, loading, and schema-validating it with actionable errors.
Contributing
Contributions to git-paw are welcome! This guide covers the development workflow.
Prerequisites
- Rust (stable, per
rust-toolchain.toml; the MSRV is 1.96, perrust-versioninCargo.toml) - tmux
- just (task runner)
Getting Started
git clone https://github.com/bearicorn/git-paw.git
cd git-paw
just check
Development Commands
git-paw uses just as a task runner. Key recipes:
| Command | Description |
|---|---|
just check | Run fmt + clippy + tests |
just test | Run all tests (tmux tests included — tmux is a hard dependency) |
just verify | Lint + cargo deny + the full suite with --no-fail-fast + cargo audit |
just lint | Run cargo fmt --check and cargo clippy --all-targets -- -D warnings |
just deny | Run cargo deny check |
just audit | Run cargo audit |
just smoke-all | Cold-start integration tests (host, plus a container on macOS) |
just coverage | Generate HTML coverage report |
just docs | Build and open mdBook documentation |
just api-docs | Build and open Rustdoc API docs |
just build | Build release binary |
just install | Install from local source |
just clean | Clean build artifacts |
Building
# Debug build
cargo build
# Release build
cargo build --release
# Install locally
cargo install --path .
Testing
# All tests (unit + integration; tmux tests run normally, so tmux must be installed)
cargo test
# Coverage report
cargo llvm-cov --html
# Open: target/llvm-cov/html/index.html
Tests are organized as:
- Unit tests —
#[cfg(test)]modules within each source file - Integration tests —
tests/directory (CLI binary tests, worktree lifecycle, session round-trips) - Tmux-dependent tests — run normally (tmux is a hard dependency)
Code Style
- Formatting:
cargo fmt(config inrustfmt.toml) - Linting:
cargo clippy --all-targets -- -D warningswith pedantic lints enabled - No panics: No
unwrap()orexpect()in non-testsrc/code - Documentation:
//!module-level doc comments,///on all public items
The no-panics rule is enforced by the lint gate. src/lib.rs and src/main.rs carry
#![warn(clippy::unwrap_used, clippy::expect_used)], so cargo clippy --all-targets -- -D warnings
fails on a new unwrap() or expect() anywhere in library or binary code. The repo-root
clippy.toml sets allow-unwrap-in-tests = true and allow-expect-in-tests = true, keeping
#[test] functions and #[cfg(test)] modules exempt; integration-test crates under tests/ are
outside the rule.
The gate has two limits. Platform-gated code is linted only on a host where that cfg is active,
and CI lints on Linux, so a violation inside a macOS-only block will not be caught there. And an
#[allow(clippy::expect_used)] silences the lint by construction, so the suppression set is pinned
by a test (static_regex_helper_is_the_only_panic_lint_suppression) rather than by the gate.
Regex patterns fixed at build time are compiled through static_regex in src/static_regex.rs,
the single audited helper that holds the only #[allow(clippy::expect_used)] in non-test code.
Every pattern passed to it is compiled by a unit test, so an invalid static pattern fails
cargo test rather than panicking at run time. Patterns that arrive at run time (configuration or
other input) are compiled directly, with the error propagated.
The library boundary
Command bodies live in the library, in git_paw::commands, so the CLI, the MCP server and the
tests drive the same use cases. A command takes a CommandContext (repository root and config), an
options struct and an injected Output sink, and returns a typed outcome. It never reads the
current directory or stdin, prompts, prints — no print!-family macro and no
std::io::stdout() / std::io::stderr() handle, not even one passed to a helper — attaches a
terminal client, or runs the unattended drive loop. It writes through Output and returns; main.rs
does the rest. A source audit (tests/command_library_audit.rs) enforces this over
src/commands/, exempting only completions.rs and broker_host.rs by name, and a main.rs
guard keeps command bodies from creeping back into the entry layer.
Where agent-facing text lives
Instructions, rules, directives, framing text and task-prompt scaffolding that an agent reads —
in its boot prompt, sidecar, or skill content — belong in a bundled asset under assets/
(embedded at compile time via include_str!), never assembled from a Rust push_str!/format!
call. Compiled code keeps the assembly: placeholder substitution, conditional inclusion,
per-backend selection, dedupe, and region gating. Assets are what the no-language-leak audit
(tests/lang_agnostic_skill_audit.rs) can see; prose compiled into a &str literal is invisible
to it, which is exactly how a git-paw-specific assumption once leaked into every consumer’s
session. See the export-agnosticism dev skill (.agents/skills/export-agnosticism/) for the
full export-surface inventory.
Commit Format
This project uses Conventional Commits:
<type>(<scope>): <description>
[optional body]
[optional footer(s)]
Types: feat, fix, docs, style, refactor, test, ci, chore, perf
Scopes: detect, git, tmux, session, config, interactive, error, cli, docs, ci, specs, agents, logging, replay, init, broker, dashboard, skills, supervisor, merge-loop, user-guide, worktree, governance, learnings, pause
Compound scopes are written (scope1,scope2,...) when one change spans several scopes, e.g. feat(cli,config): add new flag with config wiring.
Examples:
feat(tmux): add mouse mode support
fix(session): handle missing state file gracefully
docs: update installation instructions
test(git): add worktree creation edge cases
Branch Naming
feat/<description> # New features
fix/<description> # Bug fixes
docs/<description> # Documentation
test/<description> # Test additions
ci/<description> # CI/CD changes
Pull Request Process
- Fork the repository
- Create a feature branch from
main - Make your changes
- Ensure
just checkpasses (fmt, clippy, tests) - Write or update tests as needed
- Open a PR against
main
PRs should:
- Have a clear title and description
- Pass all CI checks
- Include tests for new functionality
- Follow the commit format above
Architecture
See the Architecture chapter for an overview of the module structure and design decisions.
New code follows the Target Architecture: decisions are pure functions, and tmux, git, the broker and the filesystem are reached only through ports named in git-paw’s own terms.
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.18.0] - 2026-10-29
Features
- (supervisor,config) Hand unattended judgment calls to a headless orchestrator one-shot
- (mcp,config) Add consented spec-authoring tools confined to the change directory
- (skills,init) Export the orchestrator skill for driving a session from your own agent
- (mcp) Offer session lifecycle and verdict tools to a root session
- (broker,session) Require the session authority credential for verdicts BREAKING
- (broker,dashboard) Host the broker in its own process and make the dashboard a thin client
- (supervisor,cli) Re-attach a drive loop to a live session with start –unattended
- (session,tmux,cli) Lay out every pane-less session as the agent grid with hot-attach
- (supervisor,session) Move the orchestrator out of the session and drop the supervisor pane BREAKING
- (skills) Forbid git hook bypass, tmux driving and self-approval in the coordination skill
- (broker,mcp,cli) Add a cursor-based event read surface with wave staleness
- (cli,config) Judge applied cli setup against the settings file and report leftover grants
- (cli) Check that a spec workflow is reachable from a new worktree in git paw doctor
Bug Fixes
- (broker) Treat a connection reset during the stale-broker probe as no live broker
- (cli,session) Submit the workflow entry point as its own line and name the worktree root
- (tmux,session) Split new grid rows full width so each agent’s keystrokes reach its own pane
- (supervisor) Measure the unattended heartbeat from the last observed progress
- (broker,mcp,skills) Derive no publishing agent id in the main working tree
Refactor
- (cli) Move the command bodies into the library as git_paw::commands
- (supervisor) Split the drive loop into pure decisions and an effect-applying shell
- (tmux,supervisor,cli) Reach terminal surfaces through a terminal backend port
- (session,supervisor) Resolve every surface role through a typed layout record
Documentation
- (specs) Count window 0’s panes now that the broker has its own window
- (specs) Spec the v0.18.0 orchestrator relocation changes
Testing
- (broker) Wait for a freed test port to refuse connects before using it
CI/CD
- Drop truncated profiles before merging coverage
[0.17.0] - 2026-10-25
Features
- (cli) Add git paw setup-cli to seed cli allow rules with consent
- (supervisor) Retire the auto-approve classifier BREAKING
- (cli,config) Launch a cli’s configured command and detect cursor-agent BREAKING
- (broker) Guard the listener against browser and foreign-host requests BREAKING
- (skills) Teach read-tool and scratch-path discipline in the coordination skill
- (session,cli) Capture worker cli session ids and resume them on recovery
Bug Fixes
- (error,ci) Remove non-test panics, lint-enforce the rule and run the binary’s tests
- (supervisor,session) Wire governance docs and launch specs into the launch path
- (init,session) Provision bundled helpers on every launch path
- (cli,tmux) Report exited agent panes as stopped and identify panes by role
- (broker) Refuse a port held by another repository’s broker
- (broker) Keep conflict escalation decisions across commits
- (cli) Stop never asks for confirmation; remove its –force flag BREAKING
- (supervisor,cli) Close the cli-unavailable detection gaps
- (learnings) Derive learning ids from a stable fnv-1a hash
- (git,session) Never delete a branch with unmerged commits on purge or remove
- (skills,mcp) Correct false claims in shipped boot blocks, prompts and messages
- (mcp) Accept the superpowers spec backend in startup validation
- (supervisor) Anchor the live-prompt gate on the modal block
- (session) Submit spec-driven boot prompts and gate every launch path on readiness
- (supervisor) Verify drive-loop nudges by input-box state
- (skills) Replace serde_yaml with a dependency-free frontmatter parser
- (supervisor) Derive the drive-loop exit summary from git refs
Documentation
- (agents) Replay curated history with commit-tree in release-history-curation
- (agents) Add the target architecture guide and refresh stale docs
- (user-guide) Document that sandbox-exec refuses setuid binaries
- (specs) Spec the v0.17.0 reliability release changes
Testing
- (ci) Isolate every session-touching test from the real session directory
[0.16.0] - 2026-09-14
Features
- (config) Add per-CLI prompt-shape profiles
- (mcp) Add an agent boot-event publish tool
- (supervisor) Refresh the integration branch before verification
- (skills) Export worker-guidance skills for permissions and worktree orientation
- (cli) Add attach subcommand and shell completions
- (supervisor,session) Harden unattended boot to first tool call
Bug Fixes
- (merge-loop) Make drive-loop nudges submit reliably and stop at wind-down
- (merge-loop) Resolve the wave exit summary and bind the drive loop to its session
- (session) Scope purge –stale to the current repo
- (session) Honor default_spec_cli, resolve origin/HEAD, and reattach on start
- (supervisor) Stop seeding inert command allowlists into cli settings
- (specs) Point Spec Kit agents at their spec path in the boot prompt
- (broker) Exempt shared task-list edits from ownership-conflict detection
- (user-guide) Allow /private/tmp in the sandbox profile so heredocs work
- (agents) Stop leaking git-paw’s own conventions into exported runtime prose
- (merge-loop) Stop instructing peers to cherry-pick each other’s branches
Refactor
- (skills) Move compiled export prose into assets
Documentation
- (agents) Add the release-history-curation dev skill
- (specs) Spec v0.16.0 unattended-boot and friction-killers changes
[0.15.0] - 2026-09-02
Features
- (doctor,user-guide) FS-scoped sandbox workflow + security posture
- (worktree,cli) Per-worktree runtime isolation
Documentation
- (agents) Link the remaining dev skills in AGENTS.md
[0.14.0] - 2026-08-11
Features
- (supervisor) Enforce one approver per pane with an exclusive claim
- (cli,supervisor) Single-source the classifier via a hidden __classify seam
- (supervisor,approval) Harden the unattended loop against four dogfood stalls
- (dashboard,broker) Status activity phrase and broker-log heartbeat dedup
- (supervisor,tmux) Fix supervisor top row at half the session height
- (supervisor) Autonomous orchestrator (pump-to-brain hand-off)
- (init,config) Backfill missing top-level config keys on init
- (supervisor,config,broker) Self-healing correction loop
Bug Fixes
- (session,error,tmux) Pre-freeze code-quality closeout
Refactor
- (supervisor) Model the worker lifecycle as a WorkerPhase enum
- (dashboard) Restructure the dashboard TUI as Model-View-Update
- (git) Route git.rs through the CommandRunner seam
- (interactive) Split interactive.rs into resolver/prompter/picker submodules
- (init,broker,supervisor) Source shared _paw_common.sh preamble from bundled helpers
Documentation
- (user-guide) Add tiered-model workflow guide
[0.13.0] - 2026-08-05
Features
- (broker) Default absent no-contract list fields to empty vectors
- (cli) Give doctor a live session-lifecycle smoke arm
- (cli) Add git paw doctor read-only preflight diagnostics
- (cli) Hide selftest from the public command surface
- (skills) Add spec-organization dev skill for the Gate-3 spec audit
- (detect,config) Replace gemini cli with antigravity (agy)
- (skills) Add doc-completeness, security-review, safety-review, definition-of-done skills
- (skills) Add export-agnosticism agent skill across all export surfaces
- (skills) Consult the project’s .agents/skills standards at author and review time
- (skills) Add code-standards agent skill (patterns, NFRs, do-not-touch)
- (skills) Add test-strategy agent skill and conformance guard
Bug Fixes
- Lower the declared MSRV from 1.97 to 1.96
- (broker) Offload blocking publish work and recover a poisoned state lock
- (dashboard,cli) Shell-quote the binary path in the __dashboard command
- (tmux,logging) Shell-quote the pipe-pane log path
- (tmux,session) Sanitize the tmux session name at construction
- (logging) Attach pipe-pane on every launch path
- (specs) Reconcile the auto-approval live-prompt window contradiction
- (specs) Weave the agy-roster correction into merged cli-resolution
Refactor
- (tmux) Route the runtime path through the CommandRunner seam
- (cli) Move supervisor mode into commands::supervisor (repoint source_audit)
- (cli) Extract the recover/start/add/remove handlers into src/commands
- (cli) Split main.rs handlers into src/commands modules
- (git,tmux) Add domain newtypes as the construction seam
- (tmux) Introduce CommandRunner seam at the builder execute site
- (tmux) Split tmux.rs into a tmux/ module tree (R1, behavior-preserving)
- (config) Split config.rs into a config/ module tree (R1, behavior-preserving)
- Code-analysis-refactor R1 idiom/hygiene (behavior-preserving)
- (skills) Merge security-review + safety-review into one security-and-safety-review skill
Documentation
- (user-guide) Document git paw doctor and fold selftest under it
- (specs) Doctor checks for a python3 interpreter
- (skills) Capture the net-first consolidation doctrine in test-strategy
- (skills) Fold the namespacing doctrine into the spec-organization skill
- (specs) Spec-consolidation wave 6 — domain-grouped Specifications index
- (specs) Omit the deprecated –from-specs alias from the cli-interaction-e2e matrix
Testing
- (broker) Make the H1 non-stall assertion deterministic on constrained CI
- (broker) Assert populated artifact payloads round-trip byte-equivalently
- (cli) Cover the doctor exit-code, –json and read-only contracts
- (tmux) Reap the isolated tmux server on TmuxTestEnv drop
- (interactive) Cover init migrate-supervisor prompt gating
- (worktree) Parse list-panes with a space delimiter
- (cli) Delete source-grep cli_specs_tty_proceeds_to_picker (behaviorally replaced)
- (cli) Add bare –specs spec-picker shown-gate (PTY)
- (cli) Add spec-launch dispatch rows to the cli-interaction-e2e matrix
- (broker) Trim raw-TCP per-code dups + table-ify delivery routing
- (config) Fold absent-section backward-compat fixtures into one table
- (session) Delete redundant session_integration.rs (unit tests fully cover it)
- (config) Table-ify default/section-absent/enum-parse clusters (safe wave)
- (cli,supervisor) Table-ify cli arg-parse + classifier clusters (safe wave)
- (broker) Table-ify messages.rs getter/slugify/payload clusters (safe wave)
- Consolidate error/layout/dashboard unit clusters into tables (safe wave)
- (session) Add session-logging capture-on-launch guard (net-first)
- (cli) Add CLI-resolution-chain guard (net-first)
- (approval) Add protected-path / out-of-worktree enforcement guard (net-first)
- (specs) Add superpowers backend integration guard (net-first)
- (cli) Fix clippy lints in prompt-matrix helpers
- (interactive) Add destructive-confirmation gating rows (stop, purge –force)
- (interactive) Add start-family PTY shown picker rows
- (interactive) Add start-family bypass rows to the prompt matrix
- (interactive) Promote shared PTY harness and add the init prompt-matrix slice
- (skills) Explicitly guard the standards-consult wording in the lang-agnostic audit
CI/CD
- (smoke) Install python3 in the container smoke image
- (container) Run smoke-container as non-root + –init to mirror the CI runner
Miscellaneous
- (skills) Rustfmt tests/standards_skill_consult.rs
[0.12.0] - 2026-07-24
Features
- (specs) Select the spec system from config/CLI only; remove filesystem auto-detection
- (specs) Add superpowers as a fourth spec backend
- (supervisor) Tier approval so the drive loop and supervisor act on disjoint sets
Bug Fixes
- (broker) Monotonic poll cursor and duplicate-question suppression
- (broker) Accept any branch prefix in agent_id, not just feat (#58)
Documentation
- (agents) Enum-variant-ripple checklist (BrokerMessage + SpecBackendKind) and gate-exit-code note
- (broker) Update validate() rustdoc to the generalized agent_id regex
CI/CD
- Scope caches to main and split coverage from the test matrix
[0.11.0] - 2026-07-17
Features
- (supervisor) Isolate operator memory via config-driven protected-path denial
- (supervisor) Supervisor-native approval mode decoupled from agent approval
- (supervisor) Seed per-worktree claude settings allowlists at attach and recovery
- (supervisor) Harden the approval send gate
- (supervisor) Source auto-approve whitelist from stack presets, add worktree dev-test rules
- (broker,dashboard) Add agent.answer message variant with targeted delivery (W15-11)
- (broker) Detect same-region edits via normalized cross-kind matching (W15-22a)
Documentation
- (user-guide) Close doc-audit low-severity gaps in config, pause, and specs index
- (skills) De-opinionate exported verify/archive and spec-layout prose
Testing
- (e2e) Use cat stand-in in add-remove e2e to avoid bare-shell boot-block dirt
- (e2e) Force remove_middle_agent past the bare-shell boot-block artifact
CI/CD
- (docs) Split docs job into build-docs and deploy-docs
[0.10.0] - 2026-07-13
Features
- (cli,init) Provision bundled helpers into agent worktrees
- (skills) Bundle the on-demand docs-fetch skill and helper, gated on docs_base_url
- (docs) Generate agent-friendly discovery surface at build time
- (init) Document every config section in the generated init template
Bug Fixes
- (dashboard) Exit on tty hang-up instead of trapping the input poll
- (dashboard) Harden orphan-exit against bind-failure and shell-reparent busy-loops
- (dashboard) Exit when orphaned and stop busy-redrawing while idle
- (git,agents) Harden the remove dirty-check against porcelain parse bleed
Documentation
- (specs) Index every capability on the specifications page
- (specs) Backfill Purpose sections across archived capability specs
- Align docs with shipped behavior and add design principles
Testing
- (e2e) Make broker-port-release checks TIME_WAIT-immune
- (supervisor) Assert approval-log rows track first/last-seen
- (specs) Guard against the archived Purpose placeholder
CI/CD
- (docs) Add workflow_dispatch for on-demand pages redeploy
[0.9.0] - 2026-07-06
Features
- (supervisor) Add git paw start –unattended in-process drive loop
- (supervisor) Capture qualitative learnings via sweep.sh learn and the tooling_friction category
- (supervisor) Add a broker-mediated approval-send gate with live-prompt re-confirm
- (broker) Widen the sweep.sh helper surface for status-publish and by-path grants
- (supervisor) Detect stream-timeout, context-bloat, no-progress, and blocked stalls
- (broker) Classify in-flight overlaps as additive vs true conflicts
- (cli) Add git paw selftest subcommand with an isolated lifecycle harness
- (supervisor) Auto-approve classifier for safe permission prompts
- (interactive) Add fuzzy-filter multi-select branch and spec pickers
Bug Fixes
- (supervisor) Run the five verification gates against the re-resolved branch tip
- (skills) De-opinionate commit-message format in the bundled coordination skill
- (worktree) Ignore git-paw-managed files in the remove dirty-check
Documentation
- (governance) Add behavioral guidelines to AGENTS.md
- (readme) Refresh logo and banner assets, resize logo to 288px
[0.8.0] - 2026-06-26
Features
- (agents) Inject managed block into gitignored sidecar, not tracked AGENTS.md
- (agent) Bundle broker.sh helper; least-privilege boot allowlist
- (dashboard) Enlarge broker-log panel with configurable height
- (orchestration) Launch-readiness gate, remove-by-pane-id, equal-width rows
- (supervisor) Prefix-grant dev-allowlist + genericise DEV_ALLOWLIST_PRESET
- (worktree) Configurable worktree placement (child vs sibling)
- (skills) Add stand-by-after-commit + releasable-unit commit discipline
- (dashboard) Drop the always-blank Summary column from the agent table
Bug Fixes
- (init) Gitignore .git-paw/session-learnings.md
Documentation
- (readme) Add centered logo to the README header
- Add logo to README header (#54)
[0.7.0] - 2026-06-24
Features
- (mcp) Add source-browsing tools (list_files, read_file, search_code)
- (mcp) Add read-only documentation tools (get_readme, list_docs, get_doc)
- (mcp) Add read-only MCP server (
git paw mcp) over stdio - (learnings) Disclose no-telemetry stance and opt-in sharing
- (broker) Live watch-target registration for hot-added agents
Bug Fixes
- (mcp) Advertise git-paw server identity + configurable [mcp] name
[0.6.0] - 2026-06-17
Features
- (ci) Cold-start CI parity with containerised smoke recipes
- (config,init) CLI-agnostic boot, config-driven dev-allowlist, repo-local tmp scratch
- (session,cli) Git paw add/remove, session bugfixes, launch/recovery robustness
- (supervisor,skills) /tell routing, opsx role-gating, verification discipline, lang-agnostic skills
- (dashboard,tmux) Broker-log panel, supervisor introspection, pane affordances
- (broker) Advanced-main + learning message variants, region-level conflict detection, roster hygiene
Documentation
- (governance) Document PR-based release flow
- V0.6.0 user guide, CLI reference, and configuration updates
Testing
- Cover v0.6.0 capabilities (broker, dashboard, supervisor, session, init)
[0.5.0] - 2026-05-25
Features
- (cli,main,interactive) SpecMode dispatcher, pause subcommand, –no-supervisor, –specs picker, –from-specs –supervisor routing
- (init,skills) Bundle sweep.sh helper installed by git paw init, idempotent merge against existing configs
- (supervisor) Supervisor-as-pane, dev-allowlist seeding, default-config fallback, auto-approve, stall detection, layout helper
- (tmux,git,agents,session,dirs) Pause primitives, idempotent worktrees, AGENTS.md boot-block lifecycle, worktree base rebase
- (dashboard) Supervisor-as-pane row, prompt-inbox removal, phase-aware status, layout collapse
- (config) [governance], [common_dev_allowlist], supervisor gate-command keys, user_config_path override
- (skills) Supervisor + coordination skill v0.5.0 doctrine
- (specs) Spec Kit backend, backend-tagged SpecEntry, per-backend boot-prompt dispatch
- (broker) Agent.intent, learnings aggregator, conflict detector, status payload metadata, agent_id validation
Bug Fixes
- (tmux,test-isolation) CI failures from -p vs -l N% split syntax and test-process env leakage
- (tmux) Pass -x/-y plus set default-size for headless tmux environments
- (docs) List all crates in third-party licenses page
Documentation
- Align README, mdBook, AGENTS.md, and user-guide with v0.5.0 surface
- (specs) V0.5.0 OpenSpec changes, archive plan, and main-spec alignment
Testing
- Behavioral coverage for v0.5.0 surfaces + tmux/config-integration isolation harness
[0.4.0] - 2026-05-06
Features
- (supervisor) Auto-approve patterns
- (supervisor) Mode with merge loop, session summary, recovery, question forwarding
- (cli,config,init,git) Supervisor + force flags, supervisor config schema, branch handling
- (dashboard) Committed counter, prompt-inbox interactivity, message log panel, layout
- (broker) Hook injection, watcher, sticky terminal status, real uptime, verified/feedback messages
- (skills) Standardize agent-skill resolution to agentskills.io layout
- (detect) Expand auto-detection to cover 10 additional AI CLI tools
Documentation
- Align README, mdBook, and AGENTS.md with v0.4.0 surface
- (specs) V0.4.0 OpenSpec changes, archive plan, and main-spec alignment
Testing
- Behavioral integration and unit tests for v0.4.0
[0.3.0] - 2026-04-10
Features
- (broker) Wire broker into session lifecycle and update docs (#43)
- Add dashboard, skills, and agent coordination
- (broker) Add HTTP broker with message types, delivery, and config
CI/CD
- (deps) Switch dependabot to monthly and ignore cargo-dist actions
- (deps) Bump actions/deploy-pages from 4 to 5 (#40) (#40)
Build
- (deps) Bump toml from 0.9.12+spec-1.1.0 to 1.1.2+spec-1.1.0 (#41) (#41)
[0.2.0] - 2026-04-08
Features
- Add v0.2.0 spec-driven launch, init, logging, replay, and AGENTS.md integration (#42)
[0.1.0] - 2026-03-25
Features
- Add CLI tool for parallel AI coding sessions across git worktrees [0.18.0]: https://github.com/bearicorn/git-paw/compare/v0.17.0…v0.18.0 [0.17.0]: https://github.com/bearicorn/git-paw/compare/v0.16.0…v0.17.0 [0.16.0]: https://github.com/bearicorn/git-paw/compare/v0.15.0…v0.16.0 [0.15.0]: https://github.com/bearicorn/git-paw/compare/v0.14.0…v0.15.0 [0.14.0]: https://github.com/bearicorn/git-paw/compare/v0.13.0…v0.14.0 [0.13.0]: https://github.com/bearicorn/git-paw/compare/v0.12.0…v0.13.0 [0.12.0]: https://github.com/bearicorn/git-paw/compare/v0.11.0…v0.12.0 [0.11.0]: https://github.com/bearicorn/git-paw/compare/v0.10.0…v0.11.0 [0.10.0]: https://github.com/bearicorn/git-paw/compare/v0.9.0…v0.10.0 [0.9.0]: https://github.com/bearicorn/git-paw/compare/v0.8.0…v0.9.0 [0.8.0]: https://github.com/bearicorn/git-paw/compare/v0.7.0…v0.8.0 [0.7.0]: https://github.com/bearicorn/git-paw/compare/v0.6.0…v0.7.0 [0.6.0]: https://github.com/bearicorn/git-paw/compare/v0.5.0…v0.6.0 [0.5.0]: https://github.com/bearicorn/git-paw/compare/v0.4.0…v0.5.0 [0.4.0]: https://github.com/bearicorn/git-paw/compare/v0.3.0…v0.4.0 [0.3.0]: https://github.com/bearicorn/git-paw/compare/v0.2.0…v0.3.0 [0.2.0]: https://github.com/bearicorn/git-paw/compare/v0.1.0…v0.2.0
FAQ
General
What does “paw” stand for?
Parallel AI Worktrees.
Does git-paw work on Windows?
Only through WSL (Windows Subsystem for Linux). git-paw requires tmux, which is not natively available on Windows. See the Installation chapter for WSL setup instructions.
Do I need tmux experience to use git-paw?
No. git-paw creates and manages tmux sessions for you. Mouse mode is enabled by default, so you can click to switch panes and drag to resize. The only tmux shortcut you might need is Ctrl-b d to detach.
Can I use git-paw with AI CLIs not in the supported list?
Yes! Use git paw add-cli to register any CLI binary. See Custom CLIs.
Sessions
What happens if I close my terminal?
The tmux session keeps running in the background. Run git paw attach (or git paw again) to reattach.
What happens if tmux crashes or my machine reboots?
git-paw saves session state to disk. The next time you run git paw, it detects the saved state and automatically recovers: reuses existing worktrees, recreates the tmux session, and relaunches your AI CLIs.
Can I run multiple git-paw sessions?
One session per repository. To work with multiple repos, open separate terminals and run git paw in each repo directory.
How do I switch between branches in a session?
Click the pane you want (mouse mode is on by default), or use Ctrl-b followed by arrow keys to navigate between panes. Each pane’s border label shows its pane index and branch.
Worktrees
What are git worktrees?
Git worktrees let you check out multiple branches simultaneously in separate directories. Each worktree is a fully functional working copy of the repository sharing the same .git data. Changes in one worktree don’t affect others.
Where does git-paw create worktrees?
It depends on worktree_placement. git paw init writes "child", which keeps worktrees inside the repo; with no worktree_placement set, they are created as siblings of the repo. For a project at ~/projects/my-app with branch feat/auth:
~/projects/my-app/ ← your repo
~/projects/my-app/.git-paw/worktrees/feat-auth/ ← "child" (written by git paw init)
~/projects/my-app-feat-auth/ ← "sibling" (field absent)
See Worktree Placement.
Does stopping a session delete my worktrees?
No. git paw stop kills the tmux session but keeps worktrees and any uncommitted work intact. Only git paw purge (or git paw remove <branch> for a single agent) removes worktrees.
Can I manually work in a git-paw worktree?
Yes. Worktrees are regular git working directories. You can cd into them, edit files, commit, push — anything you’d do in a normal repo. When you restart the session, git-paw reuses the existing worktrees.
Configuration
Where are config files stored?
| Level | Path |
|---|---|
| Global | ~/.config/git-paw/config.toml |
| Per-repo | .git-paw/config.toml (in repo root) |
Both are optional. See Configuration.
How do I set a default CLI?
Add to your global or repo config:
default_cli = "my-cli"
It pre-selects that CLI in the interactive picker; the picker still appears, so
you can pick another. Pass --cli to skip the prompt entirely.
How do I disable mouse mode?
mouse = false
This only affects git-paw’s tmux sessions, not your other tmux usage.
Troubleshooting
“Not a git repository”
Run git-paw from inside a git repository. It needs to be anywhere within a repo’s working tree.
“tmux is required but not installed”
Install tmux:
- macOS:
brew install tmux - Ubuntu/Debian:
sudo apt install tmux - Fedora:
sudo dnf install tmux
“No AI CLIs found on PATH”
Install at least one AI coding CLI (see Supported AI CLIs), or register a custom one:
git paw add-cli my-tool /path/to/my-tool
“no space for new pane” in tmux
This can happen with many branches on a small terminal. Make your terminal window larger before launching, or select fewer branches. In supervisor mode, git-paw caps each agent row at five panes and rebalances every row to equal width, so panes stay as wide as the layout allows; the hard cap is 25 agents per session.
Session state seems stale
git-paw checks tmux liveness to determine effective status. git paw status shows a session whose receipt claims it is active but whose tmux session is gone as 🔴 stale, and git paw start self-heals such a receipt before launching. To clear stale receipts without touching a live session:
git paw purge --stale
Repository names with dots or spaces, and install paths with spaces
Supported. git-paw derives the tmux session name from your repository’s
directory name, and it sanitizes that name to a valid tmux target: everything
outside A-Z a-z 0-9 _ - becomes -, runs collapse, and leading or trailing
runs are dropped. So a repo in my.app gets the session paw-my-app, and
My Project gets paw-My-Project. Sanitizing matters because tmux reads .
and : as its own window and pane separators — left alone, a dotted name made
tmux parse paw-my.app as session paw-my plus pane app, and pane commands
failed with can't find pane. A name already made only of safe characters is
used unchanged, so existing sessions keep the names they have.
Paths with spaces are handled too: the per-pane log path passed to tmux’s
pipe-pane and the dashboard command typed into its pane are both
shell-quoted, so a repository under /Users/My User/code logs and launches
normally.
Third-Party Licenses
git-paw includes the following third-party software.
Summary
- 205 crates under MIT License (
MIT) - 7 crates under Apache License 2.0 (
Apache-2.0) - 1 crates under BSD 3-Clause “New” or “Revised” License (
BSD-3-Clause) - 1 crates under Unicode License v3 (
Unicode-3.0) - 1 crates under zlib License (
Zlib)
License notices
Each section below covers crates that share an identical license-text variant. License-IDs with multiple copyright holders therefore appear more than once, with the relevant crates and copyright notice grouped together.
Apache License 2.0 (Apache-2.0)
Used by:
- sdd 3.0.10
License text
Apache License
Version 2.0, April 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2024-present Changgyoo Park
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License 2.0 (Apache-2.0)
Used by:
- normalize-line-endings 0.3.0
License text
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License 2.0 (Apache-2.0)
Used by:
- scc 2.4.0
License text
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2020-2024 Changgyoo Park
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License 2.0 (Apache-2.0)
Used by:
- rmcp-macros 1.7.0
- rmcp 1.7.0
- ryu 1.0.23
- sync_wrapper 1.0.2
License text
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
BSD 3-Clause “New” or “Revised” License (BSD-3-Clause)
Used by:
- matchit 0.8.4
License text
BSD 3-Clause License
Copyright (c) 2013, Julien Schmidt
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
MIT License (MIT)
Used by:
- instability 0.3.12
License text
# MIT License
Copyright (c) 2020 Stephen M. Coakley
Copyright (c) The Ratatui Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- schemars_derive 0.8.22
License text
#![allow(clippy::all)]
// Copied from regex_syntax crate to avoid pulling in the whole crate just for a utility function
// https://github.com/rust-lang/regex/blob/431c4e4867e1eb33eb39b23ed47c9934b2672f8f/regex-syntax/src/lib.rs
//
// Copyright (c) 2014 The Rust Project Developers
//
// Permission is hereby granted, free of charge, to any
// person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the
// Software without restriction, including without
// limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software
// is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice
// shall be included in all copies or substantial portions
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
pub fn escape(text: &str) -> String {
let mut quoted = String::new();
escape_into(text, &mut quoted);
quoted
}
fn escape_into(text: &str, buf: &mut String) {
buf.reserve(text.len());
for c in text.chars() {
if is_meta_character(c) {
buf.push('\\');
}
buf.push(c);
}
}
fn is_meta_character(c: char) -> bool {
match c {
'\\' | '.' | '+' | '*' | '?' | '(' | ')' | '|' | '[' | ']' | '{' | '}' | '^' | '$'
| '#' | '&' | '-' | '~' => true,
_ => false,
}
}
MIT License (MIT)
Used by:
- schemars 1.2.1
License text
#![allow(clippy::all)]
use crate::_alloc_prelude::*;
// Copied from regex_syntax crate to avoid pulling in the whole crate just for a utility function
// https://github.com/rust-lang/regex/blob/431c4e4867e1eb33eb39b23ed47c9934b2672f8f/regex-syntax/src/lib.rs
//
// Copyright (c) 2014 The Rust Project Developers
//
// Permission is hereby granted, free of charge, to any
// person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the
// Software without restriction, including without
// limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software
// is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice
// shall be included in all copies or substantial portions
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
pub fn escape(text: &str) -> String {
let mut quoted = String::new();
escape_into(text, &mut quoted);
quoted
}
fn escape_into(text: &str, buf: &mut String) {
buf.reserve(text.len());
for c in text.chars() {
if is_meta_character(c) {
buf.push('\\');
}
buf.push(c);
}
}
fn is_meta_character(c: char) -> bool {
match c {
'\\' | '.' | '+' | '*' | '?' | '(' | ')' | '|' | '[' | ']' | '{' | '}' | '^' | '$'
| '#' | '&' | '-' | '~' => true,
_ => false,
}
}
MIT License (MIT)
Used by:
- atomic-waker 1.1.2
License text
===============================================================================
Copyright (c) 2016 Alex Crichton
Copyright (c) 2017 The Tokio Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
===============================================================================
Copyright (c) 2016 Alex Crichton
Copyright (c) 2017 The Tokio Authors
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- core-foundation-sys 0.8.7
License text
Copyright (c) 2012-2013 Mozilla Foundation
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- form_urlencoded 1.2.2
License text
Copyright (c) 2013-2016 The rust-url developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- percent-encoding 2.3.2
License text
Copyright (c) 2013-2025 The rust-url developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- cc 1.2.61
- cfg-if 1.0.4
- find-msvc-tools 0.1.9
- js-sys 0.3.97
- socket2 0.6.3
- wait-timeout 0.2.1
- wasm-bindgen-macro-support 0.2.120
- wasm-bindgen-macro 0.2.120
- wasm-bindgen-shared 0.2.120
- wasm-bindgen 0.2.120
License text
Copyright (c) 2014 Alex Crichton
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- mio 1.2.0
License text
Copyright (c) 2014 Carl Lerche and other MIO contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
MIT License (MIT)
Used by:
- errno 0.3.14
License text
Copyright (c) 2014 Chris Wong
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- mime 0.3.17
License text
Copyright (c) 2014 Sean McArthur
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
MIT License (MIT)
Used by:
- bitflags 2.11.0
- log 0.4.29
- num-traits 0.2.19
- regex-automata 0.4.14
- regex-syntax 0.8.10
- regex 1.12.3
License text
Copyright (c) 2014 The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- float-cmp 0.10.0
License text
Copyright (c) 2014-2020 Optimal Computing (NZ) Ltd
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
MIT License (MIT)
Used by:
- hyper 1.9.0
License text
Copyright (c) 2014-2026 Sean McArthur
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
MIT License (MIT)
Used by:
- either 1.15.0
- itertools 0.13.0
License text
Copyright (c) 2015
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- tempfile 3.27.0
License text
Copyright (c) 2015 Steven Allen
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- heck 0.5.0
- unicode-segmentation 1.13.2
- unicode-width 0.1.14
- unicode-width 0.2.0
License text
Copyright (c) 2015 The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- which 8.0.2
License text
Copyright (c) 2015 fangyuanziti
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
MIT License (MIT)
Used by:
- winapi 0.3.9
License text
Copyright (c) 2015-2018 The winapi-rs Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- httparse 1.10.1
License text
Copyright (c) 2015-2025 Sean McArthur
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
MIT License (MIT)
Used by:
- futures-channel 0.3.32
- futures-core 0.3.32
- futures-executor 0.3.32
- futures-io 0.3.32
- futures-macro 0.3.32
- futures-sink 0.3.32
- futures-task 0.3.32
- futures-util 0.3.32
- futures 0.3.32
License text
Copyright (c) 2016 Alex Crichton
Copyright (c) 2017 The Tokio Authors
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- hashbrown 0.15.5
- hashbrown 0.17.0
License text
Copyright (c) 2016 Amanieu d'Antras
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- serde_urlencoded 0.7.1
License text
Copyright (c) 2016 Anthony Ramine
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- utf8parse 0.2.2
License text
Copyright (c) 2016 Joe Wilm
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- httpdate 1.0.3
License text
Copyright (c) 2016 Pyfisch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
MIT License (MIT)
Used by:
- lock_api 0.4.14
- parking_lot 0.12.5
- parking_lot_core 0.9.12
License text
Copyright (c) 2016 The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- shell-words 1.1.1
License text
Copyright (c) 2016 Tomasz Miąsko
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- indexmap 2.14.0
License text
Copyright (c) 2016--2017
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- equivalent 1.0.2
License text
Copyright (c) 2016--2023
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- scopeguard 1.2.0
License text
Copyright (c) 2016-2019 Ulrik Sverdrup "bluss" and scopeguard developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- redox_syscall 0.5.18
License text
Copyright (c) 2017 Redox OS Developers
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- http 1.4.0
License text
Copyright (c) 2017 http-rs authors
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- signal-hook-mio 0.2.5
- signal-hook-registry 1.4.8
- signal-hook 0.3.18
License text
Copyright (c) 2017 tokio-jsonrpc developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- bytes 1.11.1
License text
Copyright (c) 2018 Carl Lerche
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- autocfg 1.5.0
License text
Copyright (c) 2018 Josh Stone
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- smallvec 1.15.1
License text
Copyright (c) 2018 The Servo Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- serial_test 3.4.0
- serial_test_derive 3.4.0
License text
Copyright (c) 2018 Tom Parker-Shemilt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- want 0.3.1
License text
Copyright (c) 2018-2019 Sean McArthur
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
MIT License (MIT)
Used by:
- try-lock 0.2.5
License text
Copyright (c) 2018-2023 Sean McArthur
Copyright (c) 2016 Alex Crichton
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
MIT License (MIT)
Used by:
- getrandom 0.4.2
License text
Copyright (c) 2018-2026 The rust-random Project Developers
Copyright (c) 2014 The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- unicode-truncate 1.1.0
License text
Copyright (c) 2019 Aetf <aetf at unlimitedcodeworks dot xyz>
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- slab 0.4.12
License text
Copyright (c) 2019 Carl Lerche
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- bumpalo 3.20.2
License text
Copyright (c) 2019 Nick Fitzgerald
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- tracing-attributes 0.1.31
- tracing-core 0.1.36
- tracing 0.1.44
License text
Copyright (c) 2019 Tokio Contributors
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- tower-layer 0.3.3
- tower-service 0.3.3
- tower 0.5.3
License text
Copyright (c) 2019 Tower Contributors
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- axum 0.8.8
License text
Copyright (c) 2019 axum Contributors
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- http-body 1.0.1
License text
Copyright (c) 2019-2024 Sean McArthur & Hyper Contributors
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- http-body-util 0.1.3
License text
Copyright (c) 2019-2025 Sean McArthur & Hyper Contributors
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- iana-time-zone-haiku 0.1.2
- iana-time-zone 0.1.65
License text
Copyright (c) 2020 Andrew D. Straw
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- hyper-util 0.1.20
License text
Copyright (c) 2023-2025 Sean McArthur
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
MIT License (MIT)
Used by:
- anstream 1.0.0
- anstyle-parse 1.0.0
- anstyle-query 1.1.5
- anstyle-wincon 3.0.11
- anstyle 1.0.14
- assert_cmd 2.2.0
- clap 4.6.0
- clap_builder 4.6.0
- clap_complete 4.6.9
- clap_derive 4.6.0
- clap_lex 1.1.0
- colorchoice 1.0.5
- is_terminal_polyfill 1.70.2
- once_cell_polyfill 1.70.2
- predicates-core 1.0.10
- predicates-tree 1.0.13
- predicates 3.1.4
- serde_spanned 1.1.1
- termtree 0.5.1
- toml 1.1.2+spec-1.1.0
- toml_datetime 1.1.1+spec-1.1.0
- toml_parser 1.1.2+spec-1.1.0
- toml_writer 1.1.1+spec-1.1.0
License text
Copyright (c) Individual contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- libc 0.2.184
License text
Copyright (c) The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- lru 0.12.5
License text
MIT License
Copyright (c) 2016 Jerome Froelich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- static_assertions 1.1.0
License text
MIT License
Copyright (c) 2017 Nikolai Vazquez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- darling 0.23.0
- darling_core 0.23.0
- darling_macro 0.23.0
License text
MIT License
Copyright (c) 2017 Ted Driggs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- zeroize 1.8.2
License text
MIT License
Copyright (c) 2018-2021 The RustCrypto Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- schemars 0.8.22
- schemars 1.2.1
- schemars_derive 0.8.22
- schemars_derive 1.2.1
License text
MIT License
Copyright (c) 2019 Graham Esau
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- strum 0.26.3
- strum_macros 0.26.4
License text
MIT License
Copyright (c) 2019 Peter Glotfelty
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- tokio-macros 2.7.0
License text
MIT License
Copyright (c) 2019 Yoshua Wuyts
Copyright (c) Tokio Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- axum-core 0.5.6
License text
MIT License
Copyright (c) 2019–2025 axum Contributors
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- compact_str 0.8.1
License text
MIT License
Copyright (c) 2021 Parker Timmerman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- castaway 0.2.4
License text
MIT License
Copyright (c) 2021 Stephen M. Coakley
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- matchit 0.8.4
License text
MIT License
Copyright (c) 2022 Ibraheem Ahmed
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- git-paw 0.18.0
License text
MIT License
Copyright (c) 2026 bearicorn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- chrono 0.4.44
- difflib 0.4.0
- r-efi 6.0.0
- wasip2 1.0.2+wasi-0.2.9
- wasip3 0.4.0+wasi-0.3.0-rc-2026-01-06
- winapi-i686-pc-windows-gnu 0.4.0
- winapi-x86_64-pc-windows-gnu 0.4.0
- windows-core 0.62.2
- windows-implement 0.60.2
- windows-interface 0.59.3
- windows-link 0.2.1
- windows-result 0.4.1
- windows-strings 0.5.1
- windows-sys 0.59.0
- windows-sys 0.61.2
- windows-targets 0.52.6
- windows_aarch64_gnullvm 0.52.6
- windows_aarch64_msvc 0.52.6
- windows_i686_gnu 0.52.6
- windows_i686_gnullvm 0.52.6
- windows_i686_msvc 0.52.6
- windows_x86_64_gnu 0.52.6
- windows_x86_64_gnullvm 0.52.6
- windows_x86_64_msvc 0.52.6
License text
MIT License
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- tokio-util 0.7.18
- tokio 1.51.6
License text
MIT License
Copyright (c) Tokio Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- ident_case 1.0.1
License text
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- crossterm 0.28.1
- crossterm_winapi 0.9.1
License text
MIT License
Copyright (c) 2019 Timon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- async-trait 0.1.89
- atomic-waker 1.1.2
- dyn-clone 1.0.20
- fastrand 2.4.1
- indoc 2.0.7
- itoa 1.0.18
- linux-raw-sys 0.12.1
- linux-raw-sys 0.4.15
- once_cell 1.21.4
- paste 1.0.15
- pastey 0.2.3
- pin-project-lite 0.2.17
- proc-macro2 1.0.106
- quote 1.0.45
- ref-cast-impl 1.0.25
- ref-cast 1.0.25
- rustix 0.38.44
- rustix 1.1.4
- rustversion 1.0.22
- serde 1.0.228
- serde_core 1.0.228
- serde_derive 1.0.228
- serde_derive_internals 0.29.1
- serde_json 1.0.149
- serde_path_to_error 0.1.20
- syn 2.0.117
- thiserror-impl 2.0.18
- thiserror 2.0.18
- unicode-ident 1.0.24
- wasi 0.11.1+wasi-snapshot-preview1
- wit-bindgen 0.51.0
- zmij 1.0.21
License text
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- allocator-api2 0.2.21
License text
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- winnow 1.0.1
License text
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- encode_unicode 1.0.0
License text
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
MIT License (MIT)
Used by:
- android_system_properties 0.1.5
License text
The MIT License (MIT)
Copyright (c) 2013 Nicolas Silva
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
MIT License (MIT)
Used by:
- tracing-core 0.1.36
License text
The MIT License (MIT)
Copyright (c) 2014 Mathijs van de Nes
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- base64 0.22.1
License text
The MIT License (MIT)
Copyright (c) 2015 Alice Maz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
MIT License (MIT)
Used by:
- aho-corasick 1.1.4
- memchr 2.8.0
License text
The MIT License (MIT)
Copyright (c) 2015 Andrew Gallant
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
MIT License (MIT)
Used by:
- strsim 0.11.1
License text
The MIT License (MIT)
Copyright (c) 2015 Danny Guo
Copyright (c) 2016 Titus Wormer <tituswormer@gmail.com>
Copyright (c) 2018 Akash Kurdekar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- shlex 1.3.0
License text
The MIT License (MIT)
Copyright (c) 2015 Nicholas Allegra (comex).
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
MIT License (MIT)
Used by:
- ratatui 0.29.0
License text
The MIT License (MIT)
Copyright (c) 2016-2022 Florian Dehau
Copyright (c) 2023-2024 The Ratatui Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- console 0.16.3
- dialoguer 0.12.0
License text
The MIT License (MIT)
Copyright (c) 2017 Armin Ronacher <armin.ronacher@active-4.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License (MIT)
Used by:
- bstr 1.12.1
License text
The MIT License (MIT)
Copyright (c) 2018-2019 Andrew Gallant
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
MIT License (MIT)
Used by:
- cassowary 0.3.0
License text
The MIT License (MIT)
Copyright (c) 2016 Dylan Ede
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Unicode License v3 (Unicode-3.0)
Used by:
- unicode-ident 1.0.24
License text
UNICODE LICENSE V3
COPYRIGHT AND PERMISSION NOTICE
Copyright © 1991-2023 Unicode, Inc.
NOTICE TO USER: Carefully read the following legal agreement. BY
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
Permission is hereby granted, free of charge, to any person obtaining a
copy of data files and any associated documentation (the "Data Files") or
software and any associated documentation (the "Software") to deal in the
Data Files or Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, and/or sell
copies of the Data Files or Software, and to permit persons to whom the
Data Files or Software are furnished to do so, provided that either (a)
this copyright and permission notice appear with all copies of the Data
Files or Software, or (b) this copyright and permission notice appear in
associated Documentation.
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
THIRD PARTY RIGHTS.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
FILES OR SOFTWARE.
Except as contained in this notice, the name of a copyright holder shall
not be used in advertising or otherwise to promote the sale, use or other
dealings in these Data Files or Software without prior written
authorization of the copyright holder.
zlib License (Zlib)
Used by:
- foldhash 0.1.5
License text
Copyright (c) 2024 Orson Peters
This software is provided 'as-is', without any express or implied warranty. In
no event will the authors be held liable for any damages arising from the use of
this software.
Permission is granted to anyone to use this software for any purpose, including
commercial applications, and to alter it and redistribute it freely, subject to
the following restrictions:
1. The origin of this software must not be misrepresented; you must not claim
that you wrote the original software. If you use this software in a product,
an acknowledgment in the product documentation would be appreciated but is
not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.