A desktop app for organizing, managing, and automating agent coding
sessions. Built in Rust with egui /
eframe from
egui-app-template; design notes live in docs/.
Requires Rust 1.95 or newer.
cargo run --locked # launch the app
cargo test --locked --workspace # unit + headless UI + tmux integration tests, every crate
cargo test --locked -p egui_term --lib # the vendored terminal widget (outside the workspace)
cargo clippy --locked --workspace --all-targets -- -D warnings # lint
cargo fmt --all # format
(cd client && swift test) # the Swift client's packages, 81 tests (Xcode; not in the hook or CI)
(cd client && scripts/sim.sh run) # the visionOS app on the simulator (xcodegen; see client/README.md)
./scripts/test-times.sh # every test timed serially, the slowest ten, the budget checked
(cd tests/browser && npm ci && npx playwright install chromium) # once, for the browser test
cargo test --locked --test remote -- --ignored the_page_works_in_a_real_browser --nocapture # the web page in headless Chromium (not in the hook)Test times. The budget is no test over 5 s and the whole workspace
under two minutes locally; scripts/test-times.sh checks the first and
exits non-zero past it. Measured on an M-series Mac with the script
(serial):
slowest ten:
2.26 s gate project_config_is_listed_but_never_run_until_approved
2.19 s gate killing_the_attached_client_keeps_the_session
1.52 s switchboard (lib) ui::session::tests::a_dropped_terminal_releases_its_descriptors
1.51 s dispatch/dispatch (lib) health::tests::a_socket_that_never_answers_times_out_and_one_that_does_is_timed
0.88 s dispatch/first_slice a_lost_unmark_is_sent_again_until_it_lands
0.74 s dispatch/first_slice keep_or_no_key_leaves_history_alone
0.70 s dispatch/first_slice a_rebase_that_changes_nothing_or_past_the_cap_is_a_question
0.66 s gate restart_reattaches_to_live_sessions
0.62 s dispatch/first_slice a_branch_pushed_by_an_agent_is_not_rewritten_after_a_send_back
0.60 s switchboard (lib) adapters::tmux::tests::shell_session_with_env_cwd_and_scrollback
each binary, serially:
40.07 s dispatch/first_slice
5.54 s dispatch/dispatch (lib)
5.44 s gate
3.48 s switchboard (lib)
1.11 s ui
0.28 s control
55.92 s in all
cargo test --locked --workspace took 37 s of wall time in parallel on
the same machine, built. The two gate tests at the top spend most of
their time holding still on purpose (half-second and two-second windows
in which nothing may happen). Measure on a quiet machine: another
checkout running its tests at the same time makes every file operation
wait behind its device flushes, and the same script then reads ten
times slower.
Requirements: macOS, tmux 3.2 or newer (brew install tmux), Ghostty
for agent sessions, cmake (brew install cmake; the embedded Prompt Box
builds whisper.cpp), and claude and/or codex on PATH. The app tells
you at the bottom of the window when tmux is missing. Prompt Box is a git
dependency pinned by revision; if cargo cannot fetch it because a git
insteadOf rule turns the URL into ssh, run with
CARGO_NET_GIT_FETCH_WITH_CLI=true (or set net.git-fetch-with-cli in
~/.cargo/config.toml) so the git CLI's credentials are used. The
bundle declares microphone use (NSMicrophoneUsageDescription): a
bundled app without it gets silence from the microphone, no prompt and
no error, so listening looks dead.
Data lives in ~/Library/Application Support/Switchboard/: one JSON file
per project under projects/ (with a .bak of the previous version;
approvals of defined commands live inside these records),
launch.log (what the last launch loaded and chose for the main
window's frame, for a launch with no terminal to log to),
operations.log and control.sock (the control port: every command
another process sent, its reply, and the socket it came in on; see
docs/dispatch.md),
identity.json (the remote port's identity: the server id, its display
name, the self-signed certificate and its key hash, and whether the
port listens on the local network and on which port; the certificate's
private key is in the Keychain) and devices.json (the devices paired
with this host: name, scope, when paired and last seen, and the SHA-256
of each bearer token, never the token; see docs/remote-client.md),
settings.json (theme, the active workspace, editor, global variables, file
side shown next to sessions and its tab, the directory each project's
file side starts at when narrowed, the screen to reopen on, the
Prompt Box switch and its trigger word, model, captions, the screen
its caption bar and preview panel appear on, whether updates are
checked for on a schedule and when the last check was answered with the
ETag and the release it described, the sessions open in
windows of their own and where each window sits, where the main
window sits, the zoom of each display, the workflow round cap and the
user's workflow definitions, and the environment sets: each set's
variables, which of them are secret, and its AWS method),
views.json (schema v7: the workspaces, and the working sets: each
one's name, workspace, which sessions and files are on it, and where
each card sits on its grid, with a .bak; a set made in the global
workspace, "Everywhere", is in no real workspace and may hold cards
from all of them; a rule set stores its rule, such as "active in the
last 24 hours", the sessions dismissed from it, whether it shows
only running sessions, and its card size instead of cards),
the
tmux.conf, claude-hooks.json (passed to Claude Code
with --settings), events.log (the hook event log; a
UserPromptSubmit line carries injected, never the prompt; a Stop
line carries tasks and crons, kinds and schedules only),
crons/ (one file per Claude Code session: a hash of each cron prompt
the last Stop listed, never the text; removed when the session starts
or ends, and swept after a day),
wake.sock, and
scrollback/ (one <host>-r<n>.vt per run of a command or service,
the last 20 runs kept), renders/ (first pages of PDFs shown on cards,
rasterized by Quick Look), and workflows/<run>/round-<n>/ (copies of
the plan, feedback, and response at the end of each review round). Sessions run
on a private tmux server (tmux -L switchboard), never on your default
one. Nothing is written into a project directory except what the Config
editor saves on your click. Secret values live in the login Keychain,
never in these files, under the accounts global/<VAR>,
project/<id>/<VAR> and set/<name>/<VAR> (an environment set's), and
remote-key:<server id> (the remote port's private key, as PEM).
A project can declare its commands and services in
<root>/.switchboard/project.json, so an agent working in the project
can set them up for you:
{
"version": 1,
"commands": [
{ "name": "rebundle", "command": "./scripts/bundle.sh" },
{ "name": "lint", "command": "cargo clippy", "cwd": "crates/app", "env": ["RUSTFLAGS"] },
{ "name": "report", "command": "make report", "output": ["out/report.pdf", "out/*.md"] }
],
"services": [
{ "name": "web", "command": "npm run dev", "autostart": true }
],
"show": ["manager", "orchard-backend"]
}show lists folders under the root that the file side lists even when
the root's .gitignore hides them, such as sub-repositories checked out
inside a workspace directory. Each is walked as its own tree: its own
.gitignore applies, the root's does not. Entries must be relative
paths without ..; others are skipped with a warning.
Names are 1 to 64 characters and unique across both lists; cwd is
relative to the project root and may not use ..; env lists the
variable names the command expects (values come from the project's
environment, never from this file); output is a glob pattern or a
list of them, relative to the command's directory, naming the files a
run produces: those written during the run are listed on the command's
card and page afterwards, with a Markdown or PDF file shown in place.
An entry with an unknown field or
a bad value is skipped with a warning that names it; the rest still
load. The file is limited to 64 KiB and must not be a symlink.
Nothing in the file runs until you approve the entry in the Run tab,
where you see the command, its directory, and the variables it asks
for. Any change to an entry drops its approval; autostart is honored
only for approved services. The board's Config button opens the file
for editing, with the options listed beside it and the parse result shown
as you type; Save is the only time Switchboard writes into
.switchboard/.
./scripts/bundle.sh # installs ~/Applications/Switchboard.app
./scripts/bump.sh minor # the next version into Cargo.toml, Cargo.lock and CHANGELOG.md, folding changelog.d/
./scripts/release.sh # publishes the committed version as a GitHub release (--dry-run: build and zip only)
./scripts/icon.sh # regenerates the icons from assets/Switchboard.svg and assets/vision/*.svgThe bundle carries the binaries side by side in Contents/MacOS:
switchboard, switchboard-hook (which the app locates next to its own
executable), switchboard-env and switchboard-ask (which Dispatch
locates next to its own) and dispatch. Raising a Ghostty
window needs Accessibility, and macOS ties that grant to the code
signature, and the Keychain ties its per-item "always allow" to the
signer's Team ID. The script signs with CODESIGN_IDENTITY, else the
first "Developer ID Application" identity (has a Team ID, so Keychain
items stay allowed across rebuilds), else a "Switchboard Dev" or "Prompt
Box Dev" certificate from Keychain Access (keeps the Accessibility grant,
but the Keychain asks for its items again after each build), else ad-hoc
(the grant is reset each rebuild). The bundle's Info.plist gives macOS
the usage text for Automation (System Events), the microphone and the
Local Network, and names the _switchboard._tcp Bonjour service; a bare
cargo run binary has no Info.plist and so carries none of that text.
scripts/icon.sh renders the SVG with AppKit into
assets/Switchboard.icns for Finder and assets/icon-256.rgba, which
src/main.rs embeds for the Dock, and the three layers of the visionOS
icon (assets/vision/*.svg, the same drawing split for the headset's
parallax and circular mask) into the client's asset catalog.
Releases are numbered by semantic versioning and
listed in CHANGELOG.md; the version in Cargo.toml is the one source,
and it reaches the binary (env!("CARGO_PKG_VERSION"), shown at the
bottom of the Settings menu), the bundle's Info.plist and the host's
/info reply (the headset shows it beside each host). A release is
three steps:
./scripts/bump.sh minor # or patch, major, or X.Y.Z: Cargo.toml, Cargo.lock, changelog.d/ into CHANGELOG.md
git commit -am "Release 0.3.0" # after a look at the diff
./scripts/release.sh # builds, signs, zips, tags v0.3.0, publishes with the changelog sectionEvery user-visible change adds a fragment, changelog.d/<branch>.md,
as it lands (format in changelog.d/README.md), so concurrent branches
never edit the same lines of CHANGELOG.md; the bump folds the
fragments into the new version's section and removes them.
scripts/release.sh builds the bundle through bundle.sh into a
staging directory, zips it with ditto (a plain zip would lose the
signature), writes the zip's SHA-256 as a file of its own, tags the
commit v<version> and attaches both to a GitHub release whose notes
are the changelog's section. It refuses a dirty tree, a version without
a changelog section, and a tag that exists.
A running Switchboard checks the repository's latest release
(core::updates, the adapters::updates::GitHubReleases source over
ureq) thirty seconds after launch and once a day, unless Settings' "Check
for updates automatically" is off; then only "Check for updates" looks. A newer version
is a notice and an "Update" item on the rail: the click downloads the
zip to $TMPDIR/switchboard-update, checks it against the published
SHA-256, unpacks it with ditto, verifies the bundle's signature with
codesign and requires its Team ID to equal the running app's (so the
Accessibility and Keychain grants keyed on the signature survive), and
then offers "Restart to update": a helper script waits for the process
to exit, moves the old bundle to .Switchboard.app.previous beside it,
copies the new one in and relaunches it, putting the old one back if the
copy fails (its output is in $TMPDIR/switchboard-update/swap.log). A
bare cargo run binary, an unsigned bundle or one outside a writable
directory refuses to install and says so. A pre-release tag or one that
does not parse is never offered.
Settings' "What's new" opens the changelog the binary was built with
(core::changelog), from the running version down. While a newer
release is on offer, its notes come first, marked not installed, read
from the release body with the install footer dropped; the sections
between the running version and that one appear after the update.
Another Mac installs a release by hand, with tmux, Ghostty and gh
installed:
gh release download v0.2.0 -R msull/switchboard -p 'Switchboard-*.zip' -D ~/Downloads
ditto -x -k ~/Downloads/Switchboard-0.2.0.zip ~/Applications
open ~/Applications/Switchboard.appgh and curl set no quarantine flag, so Gatekeeper asks nothing of
the bundle, which is signed but not notarized; a browser download
carries the flag, and xattr -dr com.apple.quarantine <app> clears
it. From then on the app updates itself. That Mac makes its own
identity and key on first launch, so it is a second host to the remote
client, never a copy of this one.
Dev aids, all environment variables:
SWITCHBOARD_DATA_DIR=<dir>: use another data directory (keep the path short; the wake socket path has a 104-byte limit). The app sets it in every pane it spawns, soswitchboard-envin a shell asks the instance that launched it.SWITCHBOARD_RECORD_TOKEN: set by the app in every pane, besideSWITCHBOARD_RECORD_ID, and new at every spawn.switchboard-env execsends it withenv.resolve; the record keeps only its SHA-256. To tryswitchboard-envby hand, run it in a shell the app launched, afterenv-setup unlock(or the settings menu's Unlock environment setup) for the setup commands.switchboard-asksends it too, withsession.ask;switchboard-ask "a question"in a shell the app launched marks that shell's card as waiting on you, andswitchboard-ask --cleartakes the mark off. A Claude Code session may add--confirm,--choice "<option>"(once per option) or--text; the owner answers on the card, and the answer comes back as the session's next prompt,Owner answered "<question>": <answer>, once its turn has ended.SWITCHBOARD_TMUX_SOCKET=<name>: use another tmux socket name.SWITCHBOARD_REMOTE_PORT=<port>: bind the remote port there instead of the port inidentity.json, so two instances can run on one machine;0picks a free one. The port answers/api/v1/infoto anyone and every other read to a paired device; pair one from the settings menu's Remote devices dialog, and read with the token it was given (curl --insecuretrusts any certificate; a real client pins the key hash the dialog shows):curl -sk -H "Authorization: Bearer <token>" https://127.0.0.1:43187/api/v1/waiting. The Swift client's command-line browser does the pinned version:client/README.md; the dialog's "Copy invite" puts the QR code's text, the page's URL with the code and key in its fragment, on the clipboard, soswitchboard-remote pair "$(pbpaste)"pairs it, and opening it in a browser loads the port's web page (web/), which pairs into a cookie. Script linesremote-listen lan <port>andpairing <code>put an instance on the network with a window open.DISPATCH_DATA_DIR=<dir>: where the Dispatch page looks fordispatch.sockand where its console opens (thedispatchbinary reads the same variable). Tickets' trees go under~/.dispatch/worktreesregardless, or whereverdispatch worktrees <path>(also on the Dispatch page) points; the setting is<dir>/settings.json.<dir>/events.jsonlis the event logdispatch eventsandwaitread, and<dir>/runner.jsonthe runner's status after its last pass, whichdispatch healthreads.<dir>/lockis the writer lock, with a line naming its holder, and<dir>/tickets/<id>/closing.lockis held by whicheverdispatchis finishing that ticket's close; both are lock files, not records.<dir>/tickets/<id>/<stage>/<n>/<context>/<name>/(mode 0700) holds a stage's evidence files. It is removedevidence_keep_daysafter the ticket closes, or at close with--drop-evidence.<dir>/tickets/<id>/usage.jsoncaches the token counts, model names and timestamps read from the ticket's sessions' transcripts, never their content, soreportand the Usage tab keep them after Claude Code deletes the transcripts.<dir>/projects/<project>/supervisor/holds a project's supervisor files:seed.md, the prompt its session starts from,handoff.md, which the session keeps, andhandoff.<stamp>.md, each earlier hand-off kept when a fresh session replaced it. The supervisor works insupervisor-<project>under the worktrees root. The project's record<dir>/projects/<project>.jsonkeeps the supervisor's sessions and, insupervision.refusals, the last 20 commands it was refused for want of amaycapability. The overview's Start runsdispatch runas the app'srunnerservice in that directory, and the runner reaches this app through its ownSWITCHBOARD_DATA_DIR, so a dev instance should pointDISPATCH_DATA_DIRat a throwaway directory before Start. Acargo runfinds a runner to start only oncecargo build -p dispatchhas putdispatchbeside the app binary; otherwise Start is refused.SWITCHBOARD_SCRIPT=<file>: run actions at startup, one per line, so the app can be put into a known state without clicking. Seesrc/script.rsfor the lines (add-project,new-shell,new-claude,new-codex,new-service,show-board,show-session,show-document,show-file,files,side-position,terminal,select-file,set-env,set-secret,dotenv,environment,config,env-setup unlock|lock,pairing <code>|close,remote-devices,remote-listen lan|loopback <port>,send,interrupt,return,kill,remove,approve,revoke,side,switchboard,working-set,new-working-set,clone-working-set,rename-working-set,delete-working-set,new-recent-set,set-hours,running-only,card-scale,dismiss-from-set,add-to-working-set,add-file-to-working-set,arrange,show-message,clone-session,discard-to,undo-discard,review-plan,show-review,show-dispatch,dispatch-runner-start,dispatch-runner-stop,dispatch-runner-restart,show-ticket,close-ticket,ticket-tab(<id>and one oftimeline,issue,plan,notes,review,changes,usage),ticket-diff,review-file,review-continue,review-object,review-finalize,show-artifact,pop-out,close-pop-out,files-root,zoom,place-pop-out,place-card,new-workspace,workspace,workspace-global,move-project,move-working-set,controller,open-terminal,update-check on|off,prompt-box,theme,sleep).SWITCHBOARD_CONTROLLER=<device>: the nunchuk's serial port (default: the/dev/cu.usbmodem*whose USB product name is Feather or MagTag, waited for if absent; no other board is ever opened, since a port that never answers blocks the process in the kernel at exit).SWITCHBOARD_TMUX=<path>: tmux binary to use.RUST_LOG=switchboard=debug: verbose logging.
Live tests that spend money or open windows are #[ignore]d:
cargo test --test live -- --ignored --nocapture # one cheap claude, codex, and Ghostty run each
cargo test --test gate -- --ignored --nocapture # the Milestone 1 gate items that need real agentssrc/main.rs launcher: wires real adapters, opens the window
src/bin/switchboard-hook.rs helper Claude Code hooks call (std only)
src/bin/switchboard-env.rs runs one child with a session's environment sets, resolved over the control port with its launch token; the sets' setup commands (std and the wire crate only)
src/bin/switchboard-ask.rs marks the session it runs in as asking the owner a one-line question, or a confirm, a choice or a line of text whose answer comes back as its next prompt, or clears it, over the control port with its launch token (std and the wire crate only)
src/lib.rs module tree and the layering rules
src/core/
model.rs durable data model (Project, SessionRecord, ResumeHandle, CardState)
action.rs AppAction, Effect, Clock, AppCore::dispatch, read model for the UI
reconcile.rs StoreLoaded / HostListed: card states, autostart services, spawn specs
sessions.rs launch, idempotent return, resume preflight, Codex serialization
grid.rs Working Set placement: default card sizes, first free spot, overlap, minimum size, the card a step away, a rule set's card size at its scale and flowed layout
controller.rs the hand controller's meaning: the selected card per working set, Z's radial menu, the session C holds open
definitions.rs .switchboard/project.json entries -> records; hash-keyed approval
env.rs environment resolution for a new session: global variables, opted-in .env files, the project's own; secrets by account name
file_refs.rs file paths in an agent's prose as links: finder, rewrite given which name a file, per-message cache
events.rs hook events -> record activity (matched by record id, ordered by time)
workflow.rs plan review runs: reviewer and planner rounds as a state machine over records
control.rs the control port's commands run quietly under an operation id; read models in the wire's shapes; `ready_for_prompt`, the one verdict `session.prompt` and a view's `prompt_refusal` share
remote.rs the remote port's state: the host's identity and paired devices as loaded, the pairing window with its code, scope and failure counts per source address (three lock an address out until the window is reopened), a device's scope and revocation; `invite_url`, the page's URL with the code, key and id in its fragment that the QR carries
updates.rs the app's version and its update: the scheduled check, daily, with jitter (off when the setting says so), a newer release offered, downloaded on a click and installed on another; semantic versions, pre-releases never offered
changelog.rs the CHANGELOG.md this binary was built with, compiled in, as sections of titled groups; a found release's notes the same way with the release footer dropped; the sections at or below the running version
tickets.rs Dispatch's tickets as the remote port says them: each ticket's summary with its standing worded as the Mac's page words it, its decisions, and alone its body, attempts and lanes (the checks log listed once); one value for the port's fingerprint and its replies; and whether an attempt ran with grants (its checks, or a session that holds sets)
revision.rs the read-model fingerprints behind the remote port's revision: every exported view hashed with its time-derived fields held still, and the `changed`/`removed` diff between two passes (and a record's runs as the `runs` collection)
diff.rs a changed file's diff as held per ticket, lane and path, when a re-read is due, which unchanged runs fold
dispatch.rs Dispatch as the app shows it: the runner's last status as views, decisions answered as calls, a ticket's events and full view read once per change and its timeline, a project's supervisor chip, the console session; the stale-runner rule (an older build is restarted once no attempt is open, else named and offered Restart)
window_fit.rs a saved window frame fitted before a window is asked for it: to its display's visible frame and to 8192 device pixels a side; the first launch's frame from the primary display
tests.rs state-transition tests for the core
src/ports/ traits: store, host, events, agent, opener, transcript, secrets, project_config, round_files, artifacts, changes (a branch's commits and files over its base, one file's diff), controller, control (the operations log), dispatch (Dispatch's port), remote (the identity and paired-device files, and the Bonjour advertisement), updates (the release source: the newest release, its bundle fetched and checked, the swap arranged)
src/adapters/
store.rs JSON store: atomic writes, .bak, flock
updates.rs GitHub Releases over `ureq`: the latest release parsed from the API by a conditional (`If-None-Match`), size-capped request that follows redirects only to two pinned hosts, the zip staged under `$TMPDIR/switchboard-update` and checked against its `.sha256` asset, unpacked with `ditto`, verified with `codesign` and pinned to the running app's Team ID; the swap helper that replaces the bundle after exit and relaunches, keeping the old one aside
tmux.rs tmux process host on the private socket
hooks.rs append-first event log + socket wake-up + hook settings JSON
control.rs the control port: operations.log (append-only, synced) and control.sock (one thread per connection, requests handed to the frame)
remote.rs the remote port: identity.json and devices.json, the certificate and key (`rcgen`), the pinned client a device uses, and the TLS listener (`rustls`, one thread per connection, pools of 4 unauthenticated (3 per peer address, counted by a drop guard that frees the slot however the connection ends) and 12 paired, absolute deadlines before pairing (5 s handshake, 5 s head, 10 s body, enforced on each socket read beneath TLS) and the 30 s idle timeout after, every reply before pairing saying `Connection: close` and closing, per-source `/pair` and `/info` rate windows over at most 256 addresses and the page's window, each 429 counted for the frame's minute summary, the peer address on each request, requests handed to the frame 8 per pass, the page's files answered on the connection thread and the connection closed after each; a browser's `Cookie`, `Origin` and `Host` kept on the request, extra reply headers such as `Set-Cookie`, and `no-store` and `nosniff` on every JSON reply); a connection the frame accepts an upgrade on becomes a WebSocket subscriber (`tungstenite`, non-blocking, fed from a queue of 256, `resync_required` then the close on overflow); a subscriber's own text frames (`watch`, `unwatch`) and its end go to the frame as `SubscriberFrame`s; the request head read under an 8 KiB bound, a subscriber's inbound frame under 16 KiB
web.rs the remote port's web page: the three files of `web/` built in by path and type, the content security policy and the headers every page file carries
bonjour.rs `_switchboard._tcp` over `mdns-sd` while the remote port is on the local network (macOS; a stub elsewhere): the display name with a short id suffix, TXT `v`, `name`, `id`
dispatch.rs the Dispatch port's client over <Dispatch data dir>/dispatch.sock, reconnecting after an error; the `dispatch` beside the app's binary; `build_on_disk`, the `dispatch` file a restart would run
displays.rs the attached displays with their backing scale (promptbox's screen list, `NSScreen` on macOS; empty elsewhere)
dock.rs Dock badge with the waiting-session count (macOS)
controller.rs the nunchuk over USB serial: a thread owns the port, reconnects, hands events over a channel
files.rs project file index: gitignore-aware scan, lazy children, fuzzy match; the existing file a path an agent typed names; `open_pinned`, the descriptor walk the remote port reads a pinned file through (no symlink followed, a FIFO refused, dotenv refused, the version from the inode, change time and size)
git.rs branches, change counts, per-path status; finds repos one or two dirs down; a branch's commits and numstat over its base, one file's diff (`GitChanges`)
diff.rs one file's `git diff` read into numbered hunks, changed words marked with `similar`
keychain.rs secrets as generic-password items in the login Keychain (tests use a temp keychain)
dotenv.rs .env parser (opt-in per project) and .env.example names
project_config.rs reads and validates .switchboard/project.json (capped, no symlinks)
round_files.rs a workflow's round files on disk: probe, snapshot into the data dir, delete
scrollback.rs read the pipe-pane stream back as plain text (cold sessions); the masked tail with its overlap and the log's version for the remote port
artifacts.rs a run's declared outputs: glob under the cwd, modified since the run began
agents.rs Claude Code / Codex launch, resume, preflight, discovery
transcript.rs Claude Code transcript (JSONL) -> Conversation turns and token totals per model, one per `message.id`, `<synthetic>` skipped; `read_masked` runs the mask over every text before the parser's caps
ghostty.rs open, reveal, Ghostty window launch and raise
fakes.rs test doubles for every port
src/app.rs SwitchboardApp: owns core + adapters; runs effects; polls host and events; answers control-port queries (`session.screen` with the project's secret values redacted); serves the remote port (the views on the frame, a screen through the content worker with the policy resolved on the frame, the watch set, the revision per content key, and the control scope's `send`, `resume` and `trust` through the control socket's `serve` so the op is the idempotency key; a browser's token as the cookie named for the server id, a cookie-only write or upgrade held to the page's `Origin`, `/pair` with `transport: "cookie"`, and `/me`)
src/cadence.rs when the frame loop ticks and polls: every call and once a second while visible, every 5 s while hidden, a hook wake reading the event log at once and listing the host at most once a second (pure, times injected)
src/content.rs the app's content caches and the port's content worker: parsed transcripts with the handle and file time each was read at (`ConversationCache`), read by the views through `DrawCtx`; `ContentWorker`, the one thread that reads content for a device (a pane's full window hashed as the source version; a Claude Code transcript parsed masked and kept on the worker by handle, file version and policy, served as a page of turns by number with the head of each tool call's text, as one turn whole, or as the last response, the three sharing one key; a run's log tail with the overlap that masks a value on the window's edge; a pinned file whole, cached by its version; a ticket's text handed over as JSON, masked in every string before it is serialized under the union of its holders' policies, cached by its input and policy) and polls the watched keys every two seconds; the policy resolved on the worker from what the frame hands it (`PolicySpec`, `resolve_policy`), cached per spec; `redact`, the content keys, the policy with its fingerprint, the page, turn, tail and file caps, the versions and the watch set's bounds (128 keys, a minute idle)
src/script.rs SWITCHBOARD_SCRIPT dev aid
src/sync.rs `lock_recover`: takes a mutex poisoned by another thread's panic back, logs the site once and clears the poison
src/ui/
mod.rs UiState, draw loop (collect actions, then dispatch), keyboard, side panel tabs
prompt_box.rs the Prompt Box editor per agent session, one voice runtime bound to one of them; the draft a device reads, its tools and its dictation
theme.rs the look: color tokens per theme, Source Serif 4, type scale, shared widgets (dot, kicker, buttons)
rail.rs left project rail (brand, All sessions, Dispatch with its pending decisions, projects with dots, Go to, Settings); a session's neighbours beside it; its list scrolls between a fixed head and Go to / Settings
switcher.rs Settings menu and the toasts (notice, host error)
updates.rs the version and the update's state in the Settings menu, its What's new button, and the rail's item while a release waits on a click
whats_new.rs the What's new dialog: a found release's notes marked not installed, then the built-in changelog from the running version down
board.rs one project's board: run bar, agent and shell cards, command and service rows, pinned documents, notes
files.rs Files tab of the side panel: lazy tree (from the project root or a directory chosen as its top), fuzzy finder, bottom preview pane, right-click hand-offs
run.rs Run tab of the side panel: commands and services, definitions, approval, last run
runs.rs a command or service as runs: the kicker (exit, duration, when), the card body with output or an artifact, the page with run history
notes.rs Notes tab of the side panel: the session's notes, edited in place
runbar.rs one button per command and service, under the board strip and the session header
document.rs read-only preview: Markdown, text, images, a PDF's first page; full view and the side pane's body
markdown.rs Markdown: prose through egui_commonmark, tables laid out here with content-sized columns
palette.rs quick-switcher (Cmd+K) over projects and sessions
popout.rs a session in a window of its own: the page and side panel in a viewport, frame saved on settle; the Dispatch window the same way
zoom.rs Cmd+= and Cmd+- per window, remembered per display; a pop-out's pass runs at its display's zoom
env.rs Environment dialog: variables, secrets, .env opt-in, masked preview
remote.rs Remote devices dialog: the host's name, id, key and address, the pairing window with its QR code (the invite URL; a Mac-only port says so), what a browser's pairing trusts, the paired devices with scope and Revoke
config.rs project config editor: .switchboard/project.json as text, options listed, parse shown
workflow.rs plan review: the Review plan dialog, the run's page (rounds, plan with diff, feedback beside response, controls), the feedback block pinned under the header
dispatch.rs the Dispatch page: tickets with what waits on you (filtered by project), each project's supervisor chip (Open, Resume, Trust, Fresh behind a confirmation), and the console; Pop out; an older-build label and Restart button on the runner row
diff.rs a changed file's diff on the ticket page: highlighted hunks beside a line-number gutter, changed rows tinted, folds, a new-side number opens the file
ticket.rs one ticket's page: header, pending decisions pinned, tabs for timeline, issue, plan with its review rounds, notes, code review, branch changes read on a thread with a file's diff under its row, and usage (tokens by model, then each attempt's sessions with their time, read from the runner once per change of the ticket); the owner's feedback on a pending finalize pinned above the plan
cards.rs the one card for every entry kind, the card grid, pinned document cards
session.rs session view: header, embedded terminal or conversation + message box
switchboard.rs every session across projects, waiting first
working_set.rs a working set: the user's grid of session and file cards from any project, or a rule set's cards with its hours field and dismiss controls
dialogs.rs add project / create session dialogs, the full-message and links-in-message dialogs
assets/fonts/ Source Serif 4 (Regular, Semibold, Italic; OFL), embedded by theme.rs
web/ the remote port's web page, no build step and no dependency: `index.html`, `app.css`, `app.js` (pair from the invite URL's fragment into a cookie, list sessions live by default and waiting first, narrowed by state, kind, project and a name search and sorted four ways, the choices kept in `localStorage`, a Claude Code session's conversation by turn with earlier turns on request, a command's or service's runs with the chosen run's output and its files by name, or any other session's screen, a reply under `control`, polling hastened by the events socket; its routes in one table the host test reads), and `app.test.js` (its pure parts under `node --test`, run by `adapters::web`'s tests: without node they fail on CI and print `SKIPPED:` elsewhere)
tests/ui.rs headless flows via egui_kittest with fakes
tests/control.rs the control port over a real socket with fakes: ops on records, logged replies, interrupted launches, a non-replayable command logged before it runs and answered `interrupted` on a retry, a pane's screen with secrets named
tests/remote.rs the remote port over a real TLS listener with fakes (the control scope: a read device refused by name, a send typed once per op and a repeat answered from the log, a resume launching once, trust answering the folder question once, the bad bodies; a conversation paged by turn, from `last` or `before` a held turn, a page carrying the head of a tool result and the turn alone carrying it whole, its last response at the conversation's revision, a shell refused, a watched conversation moving as the transcript grows; a command's runs, a run's output masked, a missing run refused, a watched run output moving as the log grows): /info to anyone, pairing with the host's key and code, a token that reads every view, revocation, scope, the allowlist, bounded strings, a redacted screen, a missing key remaking the identity, Bonjour following the listen preference, the events socket (`hello`, `changed` and `removed` from the diff, a rename and a trust question with no save, ten quiet passes emitting nothing, the event and the snapshot at one revision, overflow and revocation closing the socket), watched screens (`watch` answered `watching`, the poll emitting `changed` when a pane or its policy moves and nothing while it holds still, two reads of one text at one revision, the cap evicting the least recently read, `unwatch`, a closed socket and a revocation dropping their watches), stalled strangers filling only their address's share, a fresh paired connection beside two of them, every reply to a stranger closing its connection while a paired one stays open, a head with no newline closed early with no reply, an oversize frame ending a subscription, `/ops/{op}`, the fingerprint pass timed, the web page (its files and headers to anyone, its route table served, a browser's pairing into a cookie named per host, two hosts' cookies kept apart, the `Origin` rule on writes and upgrades, `/me` and the revoked-cookie inference, a page load freeing its slot, the page's rate window), and the client fixtures (`client/Fixtures`, rewritten with `SWITCHBOARD_WRITE_FIXTURES=1`)
tests/fixtures/ a small real Claude Code transcript for the parser tests
tests/browser/ the browser half of `tests/remote.rs`'s ignored `the_page_works_in_a_real_browser`: `page.mjs` drives the web page in headless Chromium step by step as the host answers, with Playwright pinned in `package.json` and `package-lock.json`
tests/live.rs ignored: real claude / codex / Ghostty runs
tests/gate.rs Milestone 1 gate: real store, tmux, hooks; agents ignored
tests/changelog_fold.rs scripts/fold-changelog.sh on a scratch changelog: the old insertion with no fragments, the groups' order, Unreleased lines kept, fragments removed, a bad line changing nothing
scripts/test-times.sh every test's time, serially, through libtest's `--report-time`; the slowest ten, each binary's total, the 5 s budget
scripts/ci-apt.sh CI's apt step: the two Linux packages the build links that the runner image lacks, from the image's own lists first, each call bounded by `timeout` and retried
scripts/ci-test.sh CI's test step: the workspace, then one retry of the failed tests, warning `flaky:` for each that passes on it
scripts/bump.sh the next version into Cargo.toml and Cargo.lock, and the changelog fragments and Unreleased section dated under it
scripts/fold-changelog.sh the fold bump.sh runs: `changelog.d/` fragments grouped Added, Changed, Fixed into a new version section of CHANGELOG.md, then removed; every line checked before anything is written
.github/workflows/ci.yml CI on every pull request and non-docs push to main (caches saved from main only, so branches never evict them): `check` (Linux, the hook's commands with `ci-test.sh`), `msrv` (the tests on 1.95), `deny` (`cargo deny`: advisories apart from bans, licenses and sources), `browser` (the web page in headless Chromium on Linux, the `#[ignore]`d test run with `--ignored`, advisory); actions pinned to a commit
.github/workflows/macos.yml the `macos` job: the hook's commands on a macOS runner, `--no-fail-fast`, the only job compiling the `cfg(target_os = "macos")` code and the only one with tmux; on every non-docs push to main, on pull requests touching the macOS-only files, the Cargo files or the vendored widget, and by hand; advisory
.github/dependabot.yml weekly update pull requests for the cargo lockfile (minor and patch grouped), the workflow's actions and `tests/browser`'s npm lockfile; the spikes are left out
deny.toml `cargo deny`'s configuration: the licences the lockfile may use, git sources limited to promptbox, and the ignored advisories, each an unmaintained crate with no safe upgrade and a reason
pipelines/Switchboard.toml this project's own Dispatch pipeline, the plan trial's result: haiku investigates, a sonnet facts stage settles the open questions with web search or a spike, a sonnet plan with a two-round review, then implement, code review and the pull request; a supervisor block for a Mac that runs the released app. Installed by copying it to `pipelines/Switchboard.toml` in Dispatch's data directory (a ticket runs the copy it froze at take); parsed and validated by a test in dispatch/src/pipeline.rs
pipelines/examples/Orchard.toml a multi-lane pipeline with one queued local resource, the shape the client pipeline grew into under the guide's Orchard names: three lanes, two of them served, a decisions gate, two reviewers and a fixer, a deploy check read at inspect, deploy, try and tried holding the stack, evidence, a supervisor block; the file to copy to another machine for work of that shape, with the names to change listed at its top
pipelines/examples/Orchard.pr.toml its companion for someone else's pull requests, one per lane: an investigator reads them and ends with a QUEUE line, two reviewers whose findings are a question, a deploy check, then the inspect gate as the decision to queue on the stack, which deploy, try and tried hold; nothing in it pushes
pipelines/examples/Orchard.held.toml a held build under the same names: deploy to my-dev, then an agent stage with a human gate whose session stays open while the owner works in it, asked `built` at its first stop; `proceed` checks the running note and a clean tree and records the head, then code review with its own checks, the pull request, ready and merge; its frontend's `serve` is the list form, a web process and a worker; parsed and validated by a test in dispatch/src/pipeline.rs
scripts/release.sh the signed bundle and its SHA-256 attached to a GitHub release tagged `v<version>`, with the changelog section as notes; what the app updates itself from
CHANGELOG.md every released user-visible change by version; written only by a bump
changelog.d/ one fragment per branch, `<branch>.md`, of `Added:`, `Changed:` or `Fixed:` lines, folded into CHANGELOG.md by a bump (`README.md` there has the format)
client/ the Swift client (docs/remote-client.md §7): `SwitchboardProtocol` (the wire views as Codable, qualified refs, the envelope, the event frame and resource keys), `SwitchboardTransport` (HTTPS and the events WebSocket with the SPKI-pinning delegate, Bonjour browsing), `SwitchboardClientCore` (known servers, credentials, one connection actor per host that subscribes before it snapshots and reconciles events by revision, holds reference-counted screen watches that are resent after a reconnect, the unified store), `SwitchboardUI` (SwiftUI browser and server views), the `switchboard-remote` command-line browser, tests, `Fixtures/` shared with `tests/remote.rs`, and the apps: `project.yml` (xcodegen writes the git-ignored Xcode project from it), `Apps/SwitchboardVision/` (the visionOS entry, thin over `SwitchboardUI`: the main window with list, grid and focus layouts and the filters, the card window group, the prompt window and the keys panel placed as the virtual keyboard is), `scripts/sim.sh` (build, run and screenshot on the simulator), `scripts/device.sh` (build, install and launch on a paired headset)
control/ switchboard-control: the control port's wire contract (requests, replies, views, a session's transcript path among them; `session.resume` resumes an agent without a terminal and never launches fresh; `session.prompt` types a prompt only into a Claude Code agent between turns, else fails naming why) and a blocking client; std + serde only
dispatch-control/ dispatch-control: the wire contract of Dispatch's own port (tickets as views, a ticket's events after a cursor, decide, queue, take, resume, close, worktrees, a project's supervisor and `supervisor-fresh`, a ticket's `usage`: tokens by model and each session's time), how a bring-up reads, the build a runner started with (`BuildId`) and whether it is stale against the file on disk (`stale`), and a blocking client; std + serde only
dispatch/ the `dispatch` binary (docs/dispatch.md; docs/dispatch-agent-guide.md is the command-line guide for agents that take tickets; docs/dispatch-pipeline-guide.md builds a pipeline file up step by step, every TOML block in it checked by a test; docs/dispatch-pipeline-improvements.md is the open list of pipeline changes drawn from tickets run so far): a ticket scheduler that drives Switchboard over the control port and never links the app
build.rs stamps the app's version from its `Cargo.toml` into the binary as `SWITCHBOARD_VERSION`, the version a runner records as its build
src/main.rs CLI: take, run, runner, decide, decisions, status, check, queue, park, resume, close, restart, worktrees, supervisor; for a supervising agent, brief (with each subscription's undelivered lines), subscribe, unsubscribe, subscriptions, events (with `--follow --timeout`), wait, show, report, tail and health (usage errors, and a command the ticket's state never allows, exit 64); a supervisor session's command checked against what it may run first
src/events.rs the event log `events.jsonl`: what a ticket write changed (`between`, pure), appended and synced before the record's rename, a `void` for a write that failed; a bring-up's `by` and `conflicts` as the scheduler derives them; read, followed, and waited on with each match checked against the record, and held while its write is in flight; `next_burst`, the settled burst a wait and a delivery share; `line`, `clock` and `decide_hint`, how a line prints
src/report.rs how a ticket went, pure: stage time, the plan per lane, plan and code review points counted by id, fix passes, rebases, the PR's size, tokens by model per stage and ticket from `usage`
src/usage.rs tokens by model and time in turns per session, from Claude Code's transcripts: `scan` (one message per `message.id`, subagents folded in), `sessions_of` (every session a ticket names, with its window and kind), `attribute`; the `usage.json` cache rescanned with no lock held and merged by `read_ms` under it
src/health.rs the runner's call counters and the build it started with, written as `runner.json` after every pass (also read by `dispatch runner` and `brief`), and the `health` check of that file and both sockets, repeated failures folded, with a line naming a runner older than the CLI and app that leaves `ok` alone
src/runner_cmd.rs `dispatch runner stop|start|restart`: refused while a ticket runs a deploy, asked of the app over the control port under the writer lock, then waited for in `runner.json`; a wait that times out prints `health`'s lines, the build line among them
src/serve.rs Dispatch's port on <data>/dispatch.sock while `run` is up: the records as views (a per-lane plan once per lane), the commands the CLI has, a ticket's usage from its cache brought up to date, one handler under one lock
src/check.rs `dispatch check <project|file>`: which files an argument names, the rules a take adds for a file's name (`.pr.toml` and a pull-request source go together) and its project, and the report as printed; nothing written
src/pipeline.rs the TOML pipeline file, parsed in full and validated (a served lane's env naming another lane of its stage, without a cycle, and the order a stage's services start in; `Pipeline::check`: every problem rather than the first, and `unknown_keys`, the keys serde drops, as warnings); fingerprint of the copy a ticket runs; how many commits the stage after a folding code review allows
src/ticket.rs the ticket record: source, lanes (with a conflict seen and not yet brought up, and their last bring-up, `Refreshed`: whether the branch had commits, when, the rebaser's notes, the conflict it resolved and the head it reached), attempts (with their checks' head and exit, the attempt a review carries and its send-back note, the history rewrite a review attempt made, the agent kind it launched (`Attempt.agent`) and a plan review's sessions by role (`Attempt.run_sessions`)), decisions, each session's transcripts as Switchboard named them (`Ticket.transcripts`, `TranscriptRef`), operation ledger, a close's progress (`CloseProgress`), restarts with the heads recorded at each stage entry, the tree's last bring-up, also kept at each stage entry, holds and the services a ticket runs, who took, parked, resumed or closed it and the supervisor's refused answers and refused commands; the per-project queue and closing list, the project's supervisor (`Supervision`, with its `Subscription`s and the `Delivery` in flight) and its base tree
src/scheduler.rs the runner: stage executors (agent, gate-only lanes, gate-only command stages (a deploy), review workflow), completion evidence, decisions, ledgered sends, parking, closing and restarting as sequences (a deploy lost to a restart waited for, never signalled); bringing a lane up to its moved base, recorded with the rebaser's notes; bringing the ticket's tree up to its base when no chosen lane lives on its branch; a deploy stage whose lane the ticket did not choose deploys the lane's base from the project's shared detached tree; a conflicted bring-up after the last code review gets one resolution review; who brought a lane up and how many commits conflicted; resource holds (taken before a lane is brought up; only the first stage of a `needs` run brings lanes up) and services stepped through src/services.rs; a retake's kept branches deleted or asked about at the cut; a provider lagging Dispatch's own push waited on, not asked about; the stage after a folding code review refused before its agent starts when the branch holds more commits than the pipeline allows
src/services.rs resource holds taken on entering each `needs` run (taken again past its first stage, the run's deploy and tester are asked about again) and released the pass after leaving it; who holds it and from which stage; a stage's services, a lane after the lanes its env names: the lane's before, a free port, a Switchboard service session, the readiness probe; a lane naming one served again is restarted against its new address; stopped when the ticket leaves the resource's run, parks or closes, with the `stuck` question past the stop limit; a `retry` restarts only those confirmed stopped
src/review.rs the code review stage: rounds of several reviewers over a lane's branch, a fresh implementer per round, checks at every accepted head; a rerun carries the last attempt's settled and open points; style and the plan's decisions (every lane's, for a joined review) do not hold a round open; a reviewer's line that every point left is wording is a note, not a point; folds the fix rounds into the commits they amend as it completes, or keeps them when the user answers `keep` to a failed fold; checks each folded message's backticked names against the commit and the tree, and asks `rewrite | accept | park` when one is gone, a rewrite rewording the folded commits with the tree unchanged; the one-pass resolution review (docs/dispatch.md, "The code review stage")
src/subscribe.rs a supervisor's subscriptions on the project's record: `subscribe`, `unsubscribe`, what each has not delivered; the runner's own subscription at `take` and on each pass to every Active ticket not yet followed (`subscribe_active`), from the tail or one before a waiting `decision` line; the runner's pass that types every subscribed ticket's settled burst (`events::next_burst`) into `supervisor.current`'s pane through `session.prompt` once the session view's `prompt_refusal` is empty, the delivery written first and resent under its op after a lost reply, why it waits kept on the record
src/supervisor.rs a project's supervisor session from the live `[supervisor]` table: its seed and the hash that says it is stale, its launch flags, who runs a command (`SWITCHBOARD_RECORD_ID`) and what a supervisor may run (`SUPERVISOR_VERBS`) and where the owner answers what it may not (`OWNER_ROUTES`), the capabilities a table's `may` gives (`runner` and `restart`) and the refusal kept on the project's record without one; the runner's fresh (workspace setup, seed, the old session killed and kept, the hand-off rotated), kill and resume through a ledgered request on the project's record
src/restart.rs `dispatch restart`: a ticket put at its stage or an earlier one under a fresh copy of the live pipeline, applied by the park sequence; branches reset to the heads recorded at stage entry, heads needed only for branches that moved; a stage only the live file has may be the target; later work discarded, changed setup run again; a pull-request ticket ranged only when no branch moved; a note for the next agent
src/recover.rs unanswered ledger operations resolved by class through find and op.status; nothing launched twice; and the project's queue-set request, a lost `set.new` found and a lost `set.sync` superseded by a redraw
src/view.rs the `Dispatch · <project>` working set, one card per queued ticket, redrawn through set.sync; its requests on the project's record
src/store.rs Dispatch's data directory, flock, atomic writes; versioned `read_ticket`/`read_project` through `migrate`; `read_usage`/`write_usage` for a ticket's `usage.json`, versioned apart by `USAGE_VERSION`; `write_ticket_stamped`, the one ticket write, which logs its events first, moves `updated_ms` only on a change other than poll bookkeeping and skips an equal record
src/git.rs clones (with the project's `.githooks` as `core.hooksPath`), worktrees and heads through fixed argv, running no hook itself (a branch pushed only with a lease; a worktree removed, never forced; a detached worktree; `changes`, `uncommitted` for a close's preflight, and `tree_changes` for a tree's own dirt, its nested lanes left out), a stage's checks as child processes polled by key (confined, when the pipeline says so, through `Confine`), commits replayed with `merge-tree`/`commit-tree`, the commits whose rebase conflicts and the files a merge with the base conflicts in read the same way, a branch moved by compare-and-swap, whether the remote already holds a branch's commits, and whether a port is free and answers HTTP; a kept branch read, deleted or renamed out of a retake's way; a range's commits and diff size for a report; which names a commit's diff and a tree lack (`absent`); with a fake
src/confine.rs a pipeline command's confinement: the seatbelt profile and wrapper built everywhere, used on macOS; elsewhere the command runs unconfined and says so
src/child.rs the one place a runner child's `Command` is built, with the launch token removed
src/history.rs which commits a code review's fix rounds fold into: pure plans for `fold` and `one` (whose message is the implementation's); the backticked names a folded message is checked for, and how a name is found in a diff or a tree
src/github.rs issues and pull requests through gh, with fakes; a merged pull request's merge commit and the check runs on it (the base pipeline)
src/bitbucket.rs pull requests on Bitbucket Cloud through curl, conflicts read from the pull request's diffstat, the merge commit and the base pipeline's statuses or a named step on it; credentials from the environment or <data>/env (NAME=value lines, mode 0600, never logged)
src/port.rs the Port trait over the control socket client; the connection remade after any error, a timeout included; `path()` names the socket for `health`
src/branch.rs a ticket's branch name from the pipeline's template (`{number}`, `{slug}`, `{key}`, `{type}`): rendered at take, which it refuses when a field has no value, sanitised, and checked against git's branch-name rules; the default `dispatch/<number>-<slug>` without one
src/template.rs `{a.b}` substitution for prompts; `render_checked` refuses a pipeline template naming a `{decisions.*}` the runner did not set, which fails the attempt rather than reach the agent as written
tests/first_slice.rs the acceptance table against an in-memory Switchboard (tests/support)
tests/supervisor.rs a project's supervisor made, replaced, killed, resumed and recovered against the in-memory Switchboard; through the CLI with `SWITCHBOARD_RECORD_ID` set, what a supervisor may and may not run; subscriptions delivered into its pane: held until settled, resent under the op after a lost reply, waiting on a busy or prompted pane, parks once, closes ending them, an older app, a Fresh inheriting the backlog
tests/check.rs `dispatch check` as a command: exit 1 with every problem on stdout and the data directory unchanged, 0 for an example, 64 with no argument
tests/live.rs ignored: the first stage against a real Switchboard and a haiku agent
tests/sandbox.rs ignored, macOS: a confined gate's refused write fails the attempt and is named in checks.log
vendor/egui_term/ embedded terminal widget (Harzu/egui_term @ 31bbc7ab, egui 0.37; see SWITCHBOARD-PATCHES.md)
firmware/nunchuk/ CircuitPython for the Feather that reports the nunchuk's buttons and stick
docs/research/ answers to one research question each, every claim cited to a file and line
spikes/ Spike 0 evidence, and later spikes such as 13-dispatch-service (a Dispatch service as a Switchboard service session; which bind sees a dev server's port) 15-paste-buffer (a multi-line send as one tmux paste, bracketed only for a pane that asked) 16-base-pipeline (a merged pull request's merge commit and the base's run on it) 17-prompt-origin (telling a typed prompt from a task notification or a harness reminder) 18-tmux-argv (keeping spawn's environment values off every process's argv) 19-structured-ask (a card's answer reaching the pane as its next prompt) 20-stop-pending (what a Stop lists as background work and wakeups still in flight), 21-evidence-writes (which writes reach an evidence directory without a permission prompt) 23-web-page (what a browser's page meets on the remote port: the cookie and socket questions, not run, and five parallel connections against the four-slot pool), 24-headless-browser (headless Chromium keeping the `__Host-` cookie and connecting `wss://` on a self-signed IP-literal host) and 25-hooks-path (which git commands run which hook under `core.hooksPath`, and `-c core.hooksPath` to a missing directory running none)
The flow for any feature: the UI dispatches an AppAction; the core
updates its state and returns Effects; the app runs each effect through
an adapter and dispatches the result as another action. Add a capability by
adding a port trait, a real adapter, a fake, and an Effect variant. The
core never touches egui, threads, files, or the network.
Everything enters AppCore::dispatch(action, clock) and leaves as effects,
so core tests are plain state-transition tests with an explicit clock and
no sleeping. Adapters get their own tests where they have logic. UI tests
run the real eframe::App headlessly with fake adapters, find widgets by
label, and advance frames with harness.run_steps(2): one frame to
process the click, one to render its result.
A test that waits on a real process (a tmux pane, a child process, a
thread's result) polls to a deadline through its file's helper
(poll_on in the tmux adapter, poll_to, wait_until and hold in
tests/gate.rs, frames_until in tests/ui.rs, poll_to in
Dispatch's git.rs) and never sleeps a fixed time before an assert.
A confined check's exit gets the longer poll_within deadline in
git.rs, because the sandbox wrapper's log show queries ride on it.
Dispatch's integration tests skip the device flush
(store::skip_fsync_for_tests) and move time with Env::wait rather
than stepping through it.
Tests that hit real services (a model, a paid API) go in their own
tests/*.rs file, marked #[ignore], with the command to run them
documented here.
.githooks/pre-commit checks formatting, runs Clippy, and runs the tests
before each commit, including the vendored terminal widget's tests, which
--workspace does not reach because vendor/egui_term is excluded. It
never modifies or stages files. Enable it once per clone:
git config core.hooksPath .githooks.github/workflows/ci.yml runs the hook's checks on Linux and on macOS,
plus cargo deny; the full account is in docs/design.md, "CI on
macOS". To run the dependency check locally:
cargo install --locked cargo-deny
cargo deny checkLocal secrets go in .env (ignored by git). Read them from the app at
startup; never bake them into the binary or commit them.
MIT, see LICENSE.