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