Skip to content

Latest commit

 

History

History
412 lines (356 loc) · 34.4 KB

AGENTS.md

File metadata and controls

412 lines (356 loc) · 34.4 KB

Muster

Native workspace for AI coding agents: real splits, real keybindings, agent status at a glance, local and SSH agents side by side in one window, on daemon-owned sessions that outlive the app.

This file is the project's own account of itself: what Muster is for, how it is shaped, how it is built, and the conventions a contributor works under. README.md is the front door for somebody deciding whether to install it, and carries none of this. A CLAUDE.md symlink beside it points here; it is gitignored, so make your own if your harness wants one.

Early, and specific about which parts. Built, and covered by the suite: splits and tabs, a rebindable keymap that ships Ghostty's chords where Ghostty has one, an agent list carrying a state on every row with a chord to each of the first nine and one to whichever is asking for you, states an agent reports itself where the screen cannot say them - waiting on its own build among them - an agent read as stuck once its program stops reading its input, a callsign for every pane (otter) taken wherever its id is, messages included and through a rename, renaming, which names the agent's session in its harness too and follows a session renamed there, trading two agents' places by dragging a row, dragging a pane by its handle onto another pane's edge on either machine, right-click menus on a pane, a tab and an agent row, including a split onto any attached machine, hiding a pane from every window with its agent running and bringing it back from a list, an agent's helpers made hidden and quiet into one tab of their own, listed under it in the agent list and by muster window, brought forward and put away together, and telling it when one is blocked, stuck or idle without reporting, on either machine, a pane made quiet so that nothing about it interrupts you, a focus history the mouse's back and forward buttons walk across tabs, configuration that reloads when you save it, a CLI that drives the window from inside a pane on either machine and presses keys at another agent's dialog, a notification when an agent needs you that takes you to the pane that asked, a second daemon on an SSH machine in the same window - where one tab can hold a laptop pane beside or above a devenv pane - several windows that each hold their own tabs and hand them to each other, a newer Muster taking over an older daemon's panes with their agents still running, a machine whose daemon died shown as unreachable on every pane and its daemon started again, agents posting messages to each other and being woken by them rather than polling, on one machine or across the laptop and a devenv while a window is attached to both, a group held up on a member noticed and told why once, a message for you shown in Muster's own banner with its Markdown drawn and answered from it without leaving your pane, or raising a notification that brings Muster forward while it is behind, a question's answers, offered with --option, drawn as buttons under it in the banner and in the conversation, where Tab reaches them and no digit picks one, a group's conversation drawn in place of the panes with what was for you standing out, what each member is doing and why it has not read, and a message box that posts as you and is left with Esc, the groups you are in listed in the sidebar with what is unread in each and left or deleted from a right-click there, compacting an agent's context when asked or once it is full enough, restarting an agent in its session from its notice, its menus or the CLI, upgrading Muster through Homebrew from its own menu or muster upgrade, and installing a harness's adapter through the harness's own plugin command, with muster window saying whose adapter is not reporting.

docs/origin.md is why this exists, docs/architecture.md is the shape, docs/configuration.md is every setting, and docs/cli/limits.md is the same honest account for the CLI.

Desiderata

  • Agent states are the point. Every pane shows working / blocked / done / idle at a glance, for local and remote agents alike. Lose this and the tool has no reason to exist.
  • Attention routing. Glanceable states are the floor, not the ceiling: when an agent needs you, the notification says why, and one keystroke lands you on the pane that asked.
  • Native feel. Real GPU-rendered splits and the platform's own keybindings. No prefix key, no chord-scavenging around a host terminal, no virtual splits composed into one grid.
  • Fast is a feature. Input-to-glyph latency indistinguishable from plain Ghostty. Work is budgeted by frequency times cardinality - per byte, per event, per render, across panes and daemons - and profiled at 1 and 15 panes.
  • Local and remote in one window. Harnesses on this machine and on an SSH devenv, rendered and driven identically.
  • Sessions outlive the app, and their shape outlives the daemon. Backend daemons own the PTYs, so quitting Muster, dropping the VPN or closing the lid costs nothing: agents keep working and every pane comes back. Below that line the guarantee weakens honestly rather than silently - a daemon restart returns the pane tree and each pane's directory but not the processes, an agent whose harness reported its session coming back in that session with the flags it ran with, and a reboot is the same case with the daemon to start first. A tab holding panes on two machines is written down on both daemons, each part carrying the tab's one name and its whole tree; only the order of its parts is Muster's to keep. What can be written down is written down; a live process cannot be, and Muster does not pretend otherwise (docs/architecture.md, durability).
  • View = f(daemon state). The app owns no truth. Layout, agent state, and scrollback live in the backend; the app renders them and forwards intent.
  • Swappable organs, pragmatically. The session backend (muster-daemon, Muster's own since it replaced herdr in MIP-3) and the renderer (libghostty today) sit behind narrow seams we own: the core speaks Muster's own vocabulary, each organ lives in one adapter (the daemon's is muster-daemon-client), and the contract corpus is the executable definition of what a replacement - wholesale, or a fork - must provide. We embrace a dependency where it simplifies; we never let one own our contract - and the surface a person or an agent drives is part of that contract. Where Muster acts on the answer, the way to ask is Muster's, never the dependency's.
  • Green suite means it works. Muster is built largely by AI agents, so the suite carries the confidence an author's memory cannot. What makes that achievable here: a thin shell over a thick headless core, so no logic hides in the untestable layer; a real muster-daemon built from the same commit behind the backend seam rather than a stand-in, because a stand-in is Muster's own guess at a daemon and a wrong guess passes; oracles recorded from reality - terminal grids via libghostty-vt, intent on the wire - never pixels or internals; deterministic, and offline in the sense that nothing reaches the network.
  • Every run explains itself. Muster is several processes, often on several machines, and a symptom in one usually has its cause in another: a window that ignores the keyboard is a bridge that never started, or one that started and could not dial back. Each run leaves a single machine-readable timeline spanning all of them, so a bug report is a file an agent can read rather than a session someone has to re-stage. Terminals carry secrets, so what you typed is never in it unless you ask for it.
  • Cross-platform stays open. macOS ships first. The shell layer is thin and per-OS; nothing outside it may assume an OS, and the core is portable by construction rather than by intention - it is a different language from the shell, so an OS type cannot leak into it by accident. Inside that layer the most native answer wins: portability constrains the core, and is never a reason to make Muster feel less like the machine it is running on. libghostty already runs on Linux and Windows, and muster-daemon on Linux; Windows is not yet a target for the daemon.
  • AI-native surface. Configuration is files. Every action runs through one shared path exposed to GUI, CLI, and API alike - parity by construction, not by discipline - so an agent can drive Muster as readily as a person can, through Muster's own surface and in Muster's own vocabulary.
  • Harness-agnostic. Strive to support many harnesses.

Non-goals

Muster imposes no workflow: it provides panes, states, sessions, and a scriptable surface, and has no opinion about how you run your agents.

  • Not a terminal emulator - libghostty is.
  • Not a multiplexer. Muster runs a session daemon of its own, which exists only to serve Muster and has no interface of its own.
  • Not an agent framework - Muster runs whatever agents you already run.

Shape

native shell (macOS first)        thin, per-OS, mostly dumb - Swift + AppKit today
  ├─ renderer seam → libghostty   real splits, GPU, VT fidelity, scrollback, find
  └─ core seam     → one symbol   protobuf over a C ABI; events, never bytes
       portable core (Rust)       mirror, keymap, dispatch, attention, config
         └─ daemon seam → muster-daemon   protobuf over a Unix socket; owns the PTYs
              ├─ messages → muster-msg    agents posting to each other; needs no window
              ├─ one bridge per pane      a pane's replay and output onto a surface's PTY
              ├─ daemon on this machine   local agents
              └─ daemon on devenv (SSH)   remote agents

Configuring

docs/configuration.md is every key Muster reads, what it means, and why it is spelled the way it is. What belongs here instead is the rule that decides where the next setting goes: a setting is Muster's when Muster acts on the answer or hands it on, including the ones it only translates onward - for libghostty, and for its daemon. It is the daemon's when it is about the daemon's own interface, which Muster never shows you. And within the file, a table when a subject has several answers, a root key when it has one.

Driving Muster

Everything a chord does, muster does. muster window says what a window is showing and what every agent in it is doing, muster pane new makes a pane, and muster pane send types into one by name - all of it the same requests the keyboard sends, because there is one path through the core and this is the other door into it.

muster window
muster pane new --down --run claude
muster msg post --to otter --file brief.md
muster pane wait --pane otter --until idle,blocked
muster tab focus t1w3r07bsd
muster window --json
muster window --layout

Tabs are named too, and for a narrower reason than panes. t1w3r07bsd, minted by Muster and unique across every machine a window shows, so muster tab focus and muster tab rename reach one without saying which daemon holds it - or which window: a tab belongs to exactly one window, and a request naming another window's tab is answered by that window. What a tab does not get is a name in any pane's environment: nothing has to tell a tab which tab it is, so there is no $MUSTER_TAB, and a script that means the tab it is sitting in reads that out of muster window - where every pane says which tab holds it.

muster daemons is the one verb that is about the machine rather than about a window. muster window says which daemons this window is attached to; that says which are on this machine at all, including the ones no window is attached to - which are the ones that accumulate, and the ones somebody reaches for pkill over. Muster writes down every daemon it starts, under ~/.muster/state/daemons/, and the verb dials each socket rather than trusting the file. Nothing there ends a daemon and nothing will: a process holding somebody's live agent is the wrong thing to reap on a schedule.

muster msg is for agents talking to each other rather than to a window: muster msg post a message of any length, muster msg read it, and the daemon wakes whoever it is for, so nobody waits in a loop. It asks the daemon rather than the window, so it works from a plain terminal and with no window open - starting the daemon if none is running - and muster docs msg is the rest.

muster docs is the reference and it ships inside the binary, so it describes the version you are running. muster --help has the grammar, muster completions zsh writes a completion script.

Every pane Muster makes on this machine can drive the window it is drawn in without being set up first: the command is on its PATH from ~/.muster/bin, $MUSTER_PANE says which pane it is, and $MUSTER_SOCKET says which app to tell - every window of an install is a window of one app, which answers from the window holding that pane's tab. So muster pane new inside a pane splits that pane, and an agent told "split two panes below you and start an agent in each" can do it. Outside every pane, --window window-2 says which window a command is about.

Two paths reach muster, and it matters less than it looks which one you get. ~/.muster/bin/muster is the app's, repointed at every launch to the CLI of the app that is running, and Muster hands its daemon a PATH with that directory at the front. Homebrew's muster is a second link, into /Applications, for terminals that are not panes. Whichever one a pane ends up finding drives the window it is drawn in, because both read $MUSTER_SOCKET and $MUSTER_PANE, and those are what name the window - so the two do not compete over which window, only over which build of the CLI answers, and that is a question only while they are different versions.

Front of the PATH is what Muster asks for rather than what you necessarily get. A login shell rebuilds PATH from your profile after the daemon has handed one over, and on the machine this was measured on that leaves ~/.muster/bin in 49th place and /opt/homebrew/bin in 20th. A profile that sets PATH outright, as Debian's /etc/profile does, drops it altogether, so the daemon also has Ghostty's shell integration append the bin/ of its data directory once the profile has run, and a command a pane was made to run gets the same line. That directory holds a muster that runs ~/.muster/bin/muster, so a pane finds one either way, on an SSH machine too. If you install no Homebrew copy, add ~/.muster/bin to your own PATH and terminals outside Muster reach the running app's CLI the same way.

Muster imposes no workflow. These are primitives, and extras/skill/SKILL.md is a Claude Code skill that points an agent at them and nothing more.

Building

./dev is the only supported way to build, test, and lint. With no flags it takes the full gate, and .github/workflows/gate.yml runs that one command on every push and pull request - so whatever a contributor's green covers, the merge gate's covers too. Its linux job adds the Linux suites below, which a macOS runner has no docker to run. Flags narrow it and cluster: ./dev -t tests, ./dev -tl tests and lints, ./dev -h lists them all.

A narrowed flag still takes what it cannot run without, so ./dev -t on a checkout nothing has been built in fetches libghostty and generates the seam's types before running anything. Both are near-free once they are there - a stamp read, seven path checks, and copying the daemon's data directory aside to compare it with the one in place - and the alternative was worse than slow: the seam's Swift types are generated during a build and committed nowhere, so a suite that skipped it ran the shell against whatever was generated last and went green while the schema said something else.

./dev --bundle assembles .build/muster.app around a release build - a thing you can double-click, keep in the Dock, or hand to somebody, with muster-daemon and its data, both dylibs, the icon and the licenses inside it. Release, where the gate builds debug: until 0.9.0 every bundle shipped the unoptimized shell, core and bridge, which nobody had chosen. Out of the gate because nothing in the gate needs one, and it is also the only way to meet the descriptor ceiling launchd imposes on a GUI-launched process. The signature is ad-hoc, which is what macOS needs to run a Mach-O at all and asserts nothing about who built it. It belongs to the checkout's own install, as every build here does, so it runs a daemon of its own beside an installed Muster's unless MUSTER_INSTALL names that install; the run's last line says which.

tools/run-checkout runs this tree's debug build beside an installed Muster, for trying a change on a machine that also uses the release. A checkout's build already gets its own daemon, but in the release's home its launch would repoint ~/.muster/bin/muster at the checkout's CLI for every release pane, so the script runs it under ~/.muster-dev (MUSTER_DEV_HOME moves it) with config.toml linked from ~/.muster, and with the environment a Dock launch gets rather than the pane's. Build first with ./dev -b.

tools/install-main makes a commit of main the everyday Muster, keeping every agent. Two things decide whether a replacement app is handed the running daemon's panes, and the script sets both: the build has to be for the installed app's install, so it takes over that daemon rather than starting an empty one beside it, and its version has to be newer than the running daemon's, which a build of main is not until ./release bumps it. So it builds in a scratch worktree with a version one patch past the installed app and the running daemon (0.17.1-dev over 0.17.0; the next minor release is newer again, so a Homebrew upgrade takes the panes over in turn), signs with the keychain's Developer ID so macOS permissions survive, and asks before it quits Muster, keeps the old app beside the new one and opens it. --dry-run says all of that and changes nothing, and --bundle installs one it built earlier. A window that finds another install's daemon in its home holding panes says so, which is how a bundle installed any other way shows up.

MUSTER_SIGN_IDENTITY is what makes a bundle somebody else can run, and ./dev --notarize is the rest of it. Set the variable to a Developer ID and the same --bundle signs with it instead, under the hardened runtime and with packaging/muster-release.entitlements rather than the debug entitlement SwiftPM signs a local build with - and the run says which identity it used. --notarize then sends the result to Apple, waits, staples the ticket into the bundle and asks spctl what a stranger's machine will conclude - which is the only answer worth having, since a signature that verifies locally and a notarized one look identical until Gatekeeper is asked.

Credentials come from a notarytool keychain profile, so notarizing your own build needs no key file on disk: xcrun notarytool store-credentials muster-notary once, and MUSTER_NOTARY_PROFILE names a profile stored under another name. CI has no keychain worth storing one in, so .github/workflows/release.yml hands over the App Store Connect key itself from repository secrets, and its header comment names the whole set. It runs both flags on a v* tag, and refuses a tag that disagrees with Cargo.toml's version.

A release is Apple Silicon only, and --notarize refuses to run anywhere else. Building universal would mean cross-building libghostty under Zig for a second architecture, and the Homebrew cask in packaging/homebrew/ says depends_on arch: :arm64 so an Intel Mac is turned away by brew rather than by a crash.

./release minor cuts a release (or major, or patch), and is a script of its own because ./dev builds and checks a tree while this ships one. Write packaging/release-notes/<version>.md first: the notes take judgment, so the script refuses to start without them. It then bumps the version, moves the daemon protocol's baseline up to a protocol minor that ships, takes ./dev and ./dev --contract, commits, pushes main, tags once CI passes, waits for release.yml, checks the published zip's checksum, version and notarization, and points both casks at it - the one here and the tap's, which it clones to a scratch directory. The cask's "Still X's checksum" line marks a release under way, so running the same command after an interruption resumes it rather than bumping twice. It asks nothing before pushing: running it is the decision to publish.

./dev --contract is the exception that stays out of the gate. It launches the real app against a real muster-daemon and reads its run log to see what connected, so it needs a logged-in GUI session - which the default suite is not allowed to require. The checks are crates/muster-contract, ignored by the gate and run one at a time here, and the Swift tests that need a real surface run beside them. The daemon is the one built from this commit, like every other tier's. It assembles a bundle on the way past and launches that too, with launchd's own PATH=/usr/bin:/bin:/usr/sbin:/sbin, because a bundle is a different layout from the one SwiftPM leaves behind - the daemon moves into Contents/Library/ - and 0.3.0 shipped a cask where no pane rendered while every check here was green against the other layout.

./dev --ssh is the remote tier, and sits out of the gate for the same reason: it needs docker rather than a GUI session. It recreates this checkout's devenv container - each checkout has its own container, port and key, so two worktrees run the tier at once (devenv/README.md) - which holds no muster-daemon, so the remote tests install this build's there the way the app does and then run against it. It leaves that container running, deliberately: a devenv is a thing you keep, and recreating it per run is what up is for. The tier says so on the way out, because the thing most likely to run next is --perf, and a container running beside a benchmark is enough to move the numbers it judges - ./devenv/devenv down when you are finished with it.

The gate builds muster-daemon and the muster CLI for Linux too, for x86_64-unknown-linux-musl and aarch64-unknown-linux-musl, cross-compiled from the Mac: libghostty-vt from the same patched checkout into a prefix per target, rust-lld against the musl rustup ships with each target, and zig for mimalloc's C - so no Linux toolchain and nothing new to install. It also runs clippy over the Linux code of the daemon, detection, messaging and the CLI, which no Mac build compiles otherwise. ./dev --linux runs the daemon's, detection's and messaging's suites on Linux, which the gate cannot: cargo's runner for the musl targets puts each test binary in a Debian container with this checkout mounted at its own path. The architecture docker runs natively runs in full, the other under emulation without the three tests whose subject the emulator replaces. The container keeps Debian's own /bin/sh, dash, because a real devenv does. --ssh runs it too. CI runs the same suites after the gate, as the gate workflow's linux job: the macOS runner builds the test executables (tools/linux-run/build-tests), and a Linux runner of each architecture runs them natively in that container (tools/linux-run/ci), with no Rust, Zig or Rad on it.

./dev -d also assembles muster-daemon-data beside libghostty: Ghostty's terminfo entry and its shell integration, copied unchanged from the pin, which the daemon gives every shell it starts. It is a directory that ships beside the daemon rather than inside it, because the bash and zsh scripts are GPLv3 (packaging/muster-daemon-data/README.md).

./dev --claude-code reaches a model, where --notarize reaches only Apple and --ssh only what Docker fetches: it drives the Claude Code installed here for one turn, in a pane with extras/claude-code's hooks and one without, and checks that both the hooks and the screen rules read it working and then idle - which is what says a Claude Code update has broken neither - and that a pane's name and its session's follow each other. It compacts both sessions through the daemon. It restarts a hooked one in its pane, checking it ends when asked rather than being killed, and then restarts the daemon under it, checking each time that it comes back as claude <its flags> --resume <id> on the same conversation. It also has two sessions message each other through the daemon, and holds how Claude Code treats a message from outside the session to the newest recording in corpus/claude-code-*/. It needs ANTHROPIC_API_KEY or claude's own login, and fails saying which is missing when it has neither, since a tier that checked nothing has not passed.

./dev --codex is the same for the Codex installed here, and reaches OpenAI: a pane with extras/codex's hooks and one without read working and then idle, the hooked one reporting its context, Esc mid-turn reads idle from Codex's own report, a message posted to an idle Codex rings it in its pane and is answered, a hooked Codex holding a draft is woken through codex queue and keeps the draft, an urgent post reaches it at work, and its messaging hooks hand a sandboxed Codex what it was sent. Each harness with an adapter gets a tier of its own (MIP-5), since each reaches a different vendor with a different login.

./dev --opencode is the same for OpenCode, on a free model, opencode/big-pickle, with each pane's data and config in scratch and no credential in reach: a pane with extras/opencode's plugin and one without read working and then idle, and a message posted to an idle OpenCode is rung and answered. OpenCode's free models refuse anything before 1.18, so the tier says so on an older one, and MUSTER_OPENCODE_BINARY names a newer one than the PATH holds.

./dev --perf and ./dev --latency are the other two out-of-gate tiers: the first measures the per-unit budgets against a checked-in baseline and fails on regression, the second times input-to-glyph with crates/muster-latency: a keystroke through muster-daemon, read off the pane's stream directly and through the real bridge drawing from it, beside the bare PTY measured in the same run - idle, in a full window of fifteen and beside a flood - and prints each against MIP-3's targets. It also times pane reads and an echo beside a build it starts itself, nice-10 burners two to a core, because an agent reads its panes hardest while a gate runs. A functional green is never a performance claim, so neither tier runs by default.

--perf also refuses to run at all on a machine whose fast cores are already committed. Everywhere else a busy machine only makes a run slow; here it makes the run lie against a file in the repository, and a tier that fails for reasons unrelated to the code is one people learn to skip. The line is a one-minute load average at the machine's performance-core count, which on Apple silicon is fewer than its cores - past that many runnable threads, work starts landing on the slower ones. ./dev --perf --anyway measures regardless, for when you want the numbers and not the verdict.

./dev --doctor says what this repo's own tooling has left running on this machine: every muster-daemon with the socket it serves, any herdr daemon an older Muster left behind, every checkout's devenv container, and whatever is currently eating the CPU. It answers the question the load average at the top of every run raises and cannot itself answer, which is what is busy. What each daemon holds is muster daemons' to say, and that is what makes ending a stray one safe, since the process holding somebody's live agent looks exactly like the nineteen that hold nothing.

Two toolchains, one door: the gate builds, tests and lints the Rust core and the Swift shell together, and a suite that discovers zero tests fails in either language rather than reporting green.

Requires a Swift 6.2 toolchain, Rad, and Zig 0.16 on your PATH; a missing rad shows up as env: rad: No such file or directory. Rust installs itself - rust-toolchain.toml pins the version and rustup fetches it on the first cargo call, the same way deps/ghostty.pin decides which libghostty gets built. libclang, which the libghostty-vt bindings are generated with, comes from the Xcode command line tools.

The libghostty that gets built is the pin plus Muster's patches. deps/ghostty-patches/ holds changes Muster carries on libghostty for good, not until upstream takes them: ./dev applies them in name order on top of the pinned commit, and they reach GhosttyKit as well as libghostty-vt because both build from that one checkout. Each patch only adds C API and changes no existing behavior, and stays small, so that a re-pin means rebasing it rather than rewriting it. deps/.built records the pin and a hash of every patch, so editing a patch rebuilds exactly as a re-pin does, and CI's cache key hashes both. A patch that no longer applies stops the build and names itself. Resolve a rebase in deps/ghostty and write it back into the patch before running ./dev again: every build resets that checkout to the pin, and a resolution left there is discarded.

deps/rad.pin names the Rad the gate is written against, and is the one pin ./dev cannot act on - it is already running under Rad by the time it could look. CI installs exactly that build; your own rad is free to be any version, and one too old to parse ./dev says which line it could not read.

The daemon is built here, not fetched. muster-daemon is a crate in this workspace, so every place that needs a daemon gets the one built from the same commit: the tests spawn it, a build stages it beside the app with its data directory, and ./dev --bundle puts both inside muster.app as a helper application of its own. Deliberately not your PATH: a suite that passed did so against that daemon, and the app never meets another. MUSTER_DAEMON_BINARY=/path/to/muster-daemon overrides it - not MUSTER_DAEMON, which every pane's environment already carries, naming the daemon that pane runs on. A bundle's daemon is started through Launch Services, as the helper application Contents/Library/MusterSessions.app, so that macOS charges a pane's permission prompts to it rather than to a Muster that will quit (docs/observations/macos-26.4.1.md). Any other daemon, a SwiftPM build's included, is spawned.

A machine you attach over SSH gets this build's daemon. The bundle carries a stripped release build for Linux on x86_64 and on aarch64 under Contents/Resources/daemons/, and a remote Mac gets the app's own daemon with the libghostty-vt it links. Muster asks the machine what it is with uname -sm, and sends the matching one with the data directory as one archive over the ssh connection, into ~/.muster/daemon/<version>/, with the muster CLI built for the same machine, linked at ~/.muster/bin/muster so muster msg works there. Nothing is downloaded, so there is no pin to keep and a machine with no internet access can be installed to. The install leaves a SHA-256 of that archive beside the daemon, and a machine holding any other build gets this one in its place: two development builds share a version, and a version alone would start whichever was there first. A SwiftPM build stages the gate's debug Linux daemons beside the app, in daemons/.

Nothing about herdr is built or fetched any more. What it was recorded doing stays in corpus/herdr-0.8.0/, with a verdict per fact in MIGRATION.json that crates/muster-daemon/tests/daemon/migration.rs keeps current, and its license stays in licenses/ because muster-detect is a port of its agent detection.

The seam's types are generated from proto/muster.proto on both sides and committed on neither, so a checkout cannot hold a shell and a core that disagree. Neither generator is a thing you install: Rust compiles the schema with a Rust library, and swift-protobuf vendors protoc's own source, so ./dev builds it. ./dev --proto regenerates on demand; a normal build does it only when the schema's hash changes.

Repo conventions

  • docs/: origin.md is the founding story, frozen as history; architecture.md, configuration.md, testing.md and glossary.md are living doctrine; docs/mip/ holds MIPs, the rare large decisions; docs/observations/ records what a dependency was measured doing, one file per version, each claim citing raw transcripts in corpus/. Routine rationale lives in commit messages; open questions live in the kan board's uncommitted column. docs/cli/ is the reference muster docs ships inside the CLI binary, so a file there is prose the gate checks is reachable.
  • extras/ holds things that are Muster-adjacent rather than Muster: a Claude Code skill pointing an agent at muster docs, and each harness's wiring that reports a session's state and its context to the daemon that owns its pane - extras/claude-code, extras/codex and extras/opencode, plugins each harness installs from this directory (MIP-5).
  • packaging/ is everything that exists only so that Muster can leave this machine: the icon and its source, the entitlements a release is signed with, the Homebrew cask - which lives here rather than only in the tap because it changes when the app does, and should be reviewed beside it - and release-notes/<version>.md, what somebody updating to that version has to know, which the release workflow puts above the install lines.
  • licenses/ is what the bundle carries about software that is not ours. THIRD-PARTY.md is generated by tools/licenses.py from the dependency graph and the gate fails when it stops describing what the binaries contain; NOTICE covers what ships as a whole file rather than compiled in.
  • corpus/: what the code is judged against, in no language. conformance/ holds the cases that define the core's behavior, snapshots/ the rendered oracles too broad to be cases, and the rest raw transcripts recorded from a real dependency. The gate fails if a file here is checked in and never run.
  • crates/ is the portable core (Rust), Sources/ the macOS shell (Swift), and proto/muster.proto plus include/muster.h are the seam between them. Both languages are built, tested and linted by ./dev.
  • Work is tracked on the in-repo kan board (.kan/, kan list). Agents keep it current: move cards as work starts and finishes, and add cards for work discovered along the way. If finishing work empties the next column, refill it from the backlog by priority and natural sequencing, so there is always a scoped next thing to pick up.

Contributing

  • Keep docs/, this file, and README.md up to date with changes. Don't inflate them needlessly, be judicious.
  • Use Conventional Commits for commits.