# Repo guide for AI coding agents

This repo builds **mu**: a CLI that runs AI agents in tmux or herdr
panes, coordinated through a task DAG.

## Read these first

1. [docs/guide/README.md](docs/guide/README.md): what mu does for a user.
2. [CHANGELOG.md](CHANGELOG.md): the upcoming version's entry is the
   source of truth for verbs, schema, and env vars.
3. [docs/VISION.md](docs/VISION.md): design pillars you must not violate.
4. [docs/ROADMAP.md](docs/ROADMAP.md): what's next and the promotion
   criteria. Read [Anti-feature pledges](docs/ROADMAP.md#anti-feature-pledges)
   before you add a dependency, an abstraction, or a surface.
5. [docs/VOCABULARY.md](docs/VOCABULARY.md): canonical terms for code,
   docs, and error messages. [docs/reference/](docs/reference/) holds env vars and naming.
6. [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): the module map (what
   lives where in `src/`), plus deep dives under [docs/architecture/](docs/architecture/).

## Build, test, lint

```bash
npm install
npm run build            # tsup → dist/
npm run typecheck
npm run lint
npm run test:fast        # unit tier; safe to run concurrently
npm run test             # full suite, including integration
npm run test:stress      # pre-release stability gate
```

`tsc` is TypeScript 7 (fast typecheck). The `typescript` package is
aliased to 6.x on purpose: `tsup --dts` needs the programmatic API that
TS 7.0 lacks. Revisit at TS 7.1.

## Gates

Measured 2026-10-23 on linuxpc (16 cores, warm cache, shared host);
see [the record](docs/test-health/2026-10-23-surface.md). Both gates are
over budget today.

| Gate | Command | Budget | Today |
|---|---|---|---|
| fast | `npm run typecheck && npm run lint && npm run test:fast` | 30 s | ~50 s |
| slow | `npm run build && npm run test` | 5 min | 4.5 to 8.5 min |
| targeted slow | `npm run build && npx vitest run <files>` | — | — |

The "Today" column is stale: on a shared box the RC measured fast 22–39 s
(4850 tests) and slow 307–486 s (5835 tests), over budget. Re-measure on an
idle host before the release and rewrite the column.

Workers run fast while iterating, then targeted slow for the changed area,
and fast on the final commit. The orchestrator runs fast + slow once per
merged batch (`npm run test` excludes lint and typecheck). Record gates per
[test-health](skills/mu/recipes/test-health.md#gate-contract).
`test/spawn-parallel-race.integration.test.ts` needs `dist/`, hence build
first.

## Code style

- Strict TypeScript with `noUncheckedIndexedAccess`, ESM only. Use
  `unknown` instead of `any`, and early returns instead of `!`.
- Throw typed error classes (`AgentNotFoundError`, `CycleError`,
  `MuxError` and its `TmuxError` / `HerdrError` subclasses, …) so
  `handle()` maps each to an exit code.
- Auto-fix sort and format with `npx biome check --write src test`.
  Never add `--unsafe`: it rewrites `delete process.env.X` into
  `process.env.X = undefined`, which sets the string `"undefined"`.
  Delete env vars as `const key = "FOO"; delete process.env[key];`.
- Files cap at 1500 LOC. Refactor at 800.
- Layout is flat at the `src/` root. One level of subdirectory is
  allowed for a cohesive cluster: one theme, imports only from the
  cluster to root files, and a row in the ARCHITECTURE module map.

## Tests

- Unit tests use real SQLite in a temp dir and a mocked multiplexer.
  Install mocks with `installMux()` from `test/_mux.ts`: it sets the
  backend and its executor together and returns one `restore()`.
- TUI tests render through `test/_ink-render.ts` (see `test/README.md`).
- The fast tier (`*.test.ts`) stays in-process: mocked tmux and VCS,
  per-test temp DBs, no real tmux/git/jj/sl, no sleeps over 50ms.
- Name full-only tests `*.integration.test.ts`. They may use real
  tmux, herdr, VCS fixtures, or subprocesses. Real-tmux tests run on the
  private socket and need no `$TMUX`. Set `process.env.MU_SPAWN_LIVENESS_MS = "0"` in
  `beforeEach` when the spawned process is meant to live long.
- Integration tests use unique session names
  (`mu-test-<pid>-<ts>-<rand>`), poll (50ms × 10) instead of sleeping,
  and wrap `afterEach` cleanup in `try {} catch {}`. Unit tests call
  `setSleepForTests(async () => {})`.
- **Never stop the herdr server or kill its process from a test.** It
  destroys the user's real panes. herdr isolation is only a named
  session (`MU_HERDR_SESSION`, enforced by `assertHerdrIsolated()`), so
  close only what you created.
- `openDb()` refuses the real DB under vitest. Use `MU_DB_PATH` or
  `{ path }`.
- In tests, `openDb` on a new path copies a template DB built once per
  run (`test/_db-template.ts`). A test that pins fresh-file behaviour
  (the DDL, a refusal leaving the file empty) calls
  `vi.unmock("../src/db.js")`.
- `test/_setup.ts` clears inherited `MU_*` vars except
  `MU_TMUX_SOCKET`. Opt in per test with `withEnv()` from `test/_env.ts`.
- Tests use temp DBs. `test/_global-teardown.ts` only reads the real DB
  to decide which sessions to sweep: it kills every `mu-*` tmux session
  on the default socket that has no workstream there, and skips the
  sweep when that DB is missing or unreadable.
- Workers run fast while iterating and slow for their changed area (see
  [Gates](#gates)); the orchestrator runs fast + slow once per
  merged batch. Tests may run concurrently across workers. A test that fails
  under load but passes alone is a test-infra concurrency bug (shared
  `/tmp`, sockets, leaked subprocesses). One that also fails alone is a
  real bug.
- `test/acceptance.integration.test.ts` is the end-to-end gate. Keep it
  green.

## Change rules

- **VOCABULARY first.** Add or rename a term in
  [docs/VOCABULARY.md](docs/VOCABULARY.md) before or with the code.
- **Roadmap gate.** Build only what is on the roadmap or meets all
  three promotion criteria: a real user hit the gap ≥2 times, the
  current design makes it straightforward, and it fits in <300 LOC.
  Otherwise add a ROADMAP entry and move on.
- **Commits:** one logical change each, scope prefix when useful
  (`schema: …`). The body says what changed and why, cites the VISION
  pillar or ROADMAP item, and states that the green gate passed. Add a
  [CHANGELOG.md](CHANGELOG.md) entry under the upcoming version.

## Common tasks

### Add a CLI verb

1. Write the SDK function (`src/agents.ts`, `src/tasks.ts`, …) with a
   typed result (`{ changed, previousStatus, … }`). Test with mocks.
2. Wire it in `src/cli.ts` with commander inside `handle(...)`. For
   `--workstream`, read `this.optsWithGlobals()`; the top-level option
   otherwise swallows it.
3. Document it in the matching `docs/guide/` how-to, VOCABULARY if
   it adds a term, and CHANGELOG. Remove any `mu sql` workaround it
   replaces. Touch `skills/mu/SKILL.md` only for a gotcha `--help`
   cannot state.
4. Smoke-test against real tmux, and on herdr with `MU_MUX=herdr` plus
   a private `MU_HERDR_SESSION` if it touches agents:

   ```bash
   MU_SYNC_DIR= MU_DB_PATH=/tmp/mu-smoke.db node dist/cli.js <verb> ...
   ```

   Blank `MU_SYNC_DIR` too. Otherwise the first flush writes the
   throwaway DB's `machine_id` into your real sync folder, and `mu sync`
   lists that phantom peer forever.

### Update the schema

The schema is v13 (`CURRENT_SCHEMA_VERSION` in `src/db.ts`). `openDb`
runs `upgradeSchema(db)`: the idempotent CREATE-IF-NOT-EXISTS DDL, any
missing `ADDED_COLUMNS`, the schema stamp, and the capture triggers with
their stamp, in ONE transaction. It accepts only v13 (no ladder; a v13
file missing a later addition is completed) and refuses older DBs
(`SchemaTooOldError`) and newer ones (`SchemaTooNewError`), both exit 4.

1. Bump `CURRENT_SCHEMA_VERSION` and mirror the shape in
   `CURRENT_SCHEMA`. A column an existing DB lacks also goes in
   `ADDED_COLUMNS` (plain nullable or defaulted: SQLite adds nothing else
   in place). Changing `CAPTURED_COLUMNS` or a trigger generator means
   bumping `CAPTURE_VERSION` and the pinned checksum in
   `test/capture-persistent.test.ts`. Test the upgrade on a populated
   file (`test/fixtures/v13-populated.db.gz`, `test/loop-storage.test.ts`).
2. Classify every new table in `PORTABLE_TABLES` or
   `MACHINE_LOCAL_TABLES`. `test/entities.test.ts` fails otherwise.
3. Update `test/db.test.ts` and CHANGELOG `### Changed`.

### Add a multiplexer operation

Call sites use `await activeMux()` and never name a backend.

1. Add the method to `MuxBackend` (`src/mux/types.ts`) with a comment
   saying what it means.
2. Implement it in `src/mux/tmux.ts` and `src/mux/herdr.ts`. A backend
   with no equivalent no-ops instead of throwing.
3. If only one backend can do it, make it optional and branch on the
   method's presence, not on `mux.name`.
4. Let `NoMultiplexerError` propagate (exit 5) for load-bearing calls
   (spawn, send, kill). Wrap best-effort calls in try/catch, like
   `resolveWorkerIdentity` in `src/tasks/claim.ts`.

Run tmux only through `tmux(args)` and herdr only through
`herdr(args)`. pi agents are driven through the control socket, not
the multiplexer. tmux refuses a send to a non-pi agent
(`AgentSendUnsupportedError`; it has no `sendToPane`), while herdr's
`agent prompt` is one atomic call.

## Known traps

- **Silent `node dist/cli.js --help`** with "Detected unsettled
  top-level await" is an import cycle: a file in `src/cli/tui/`
  imported `../../../cli.js`. Import from the real source module
  instead (`rg 'from "\.\./\.\./\.\./cli\.js"' src/cli/tui/`).
- **ANSI in ink `<Text>`** breaks wrap math. Size and cut styled lines
  by visual width with `fitLine` in `src/cli/tui/text.ts`, and keep
  frame lines exactly the box width (`frame.tsx`).

## Orchestrating on this repo

Follow [skills/mu/recipes/orchestrator-loop.md](skills/mu/recipes/orchestrator-loop.md). Repo specifics:

- Each worktree runs `npm install` into its own `node_modules`. Never
  symlink main's.
- A worker may fork from an old HEAD. Check `git log` in its worktree
  and cherry-pick only the new shas.
- CHANGELOG.md conflicts are routine: keep both halves in order.
  Resolve source conflicts by hand, never with `--ours` or `--theirs`.
- Fix a worker's bug in a separate commit after the cherry-pick. Do
  not amend it.
- Gate the merged tree with the fast gate and the slow gate (see
  [Gates](#gates)) plus `node dist/cli.js --help`.
- The installed `mu` may be older than the branch build. Confirm a
  worker received its brief (`mu agent read <w> -n 3`).

## Skill files are context, not documentation

`skills/mu/SKILL.md` loads into every orchestrator's context, and
the recipes in `skills/mu/recipes/` on demand. Every word competes with the
user's work. The recipes are written for an agent with no context;
[recipes/brief.md](skills/mu/recipes/brief.md) holds the rules, and
they apply to the skill files too.

- Leave verb lists, flags, and option tables to `--help`. The skill
  carries only what `--help` cannot: the gotcha, the reason, the trap.
- Say each thing once, next to where it applies.
- SKILL.md holds the trigger and the rule. Detail that only some
  branches need goes in a recipe under `skills/mu/recipes/`, with a
  row in SKILL.md's recipe index. Recipes reference only mu, never
  skills outside this repo.
- `docs/guide/` pages stay terse for humans and link to the recipe
  instead of restating it.
- Delete sentences the model obeys by default. State the target
  behaviour.

A commit that grows SKILL.md must say why. "The new verb needed
documenting" is not a reason.

## What not to do

- No config file. mu is CLI flags plus env vars.
- No daemon, watcher, or background process. Every invocation is
  short-lived.
- No abstraction without two real implementations today.
- No wrappers around wrappers, no JS-string codegen, no facade over
  SQLite (it is the canonical state; read it directly).
- No template or discovery system for agent roles.
- No render layer beyond `cli-table3` + `picocolors`, except `ink`
  inside `src/cli/tui/`. Never a second TUI stack.
- Don't bundle pi. It is an optional peer dependency.
- Write files only under `~/.local/state/mu-next/`, `~/.cache/mu/`, or the
  repo. The cache dir holds only files mu can lose at any time.

## When you're done

Run the fast gate, then the slow gate for your changed area (see
[Gates](#gates)):

```bash
npm run typecheck && npm run lint && npm run test:fast
npm run build && npx vitest run <files>
```

The orchestrator runs the fast gate and `npm run build && npm run test` on
each merged batch.

Then check that VOCABULARY, ARCHITECTURE, and CHANGELOG reflect the
change.
