# Architecture decision records

Decisions that shaped pi-crews, kept from the pre-release README. Each entry gives the decision and the reason. Behavior details are in [../usage.md](../usage.md).

## ADR-1: Children are ordinary Pi sessions

**Decision.** A child gets everything a Pi CLI session gets: `AGENTS.md`, `SYSTEM.md`, skills, user extensions, settings, and the CLI built-in extensions (codemode, tool_search, MCP). Built-in tools run relative to the child's working directory.

**Why.** Children should behave like a session the user opened. A custom reduced runtime would drift from Pi and ignore the user's policies.

**Consequences.**
- MCP loads only if the child's tool list has `mcp__` tools, because MCP connects at session start.
- An extension turned off by the `extensions` setting is off in children too.
- Extensions passed only with `pi -e` do not reach children. An extension's `session_start` runs once per child.

## ADR-2: Children cannot delegate

**Decision.** pi-crews is not loaded in child sessions.

**Why.** Nested delegation makes cost, concurrency and cancellation hard to reason about. One level keeps the parent in charge.

## ADR-3: Approvals and auth stay with the parent

**Decision.** Approval and policy extensions work inside children. Prompts appear on the parent screen tagged `[child name]`. A child cannot change the status line, widgets or theme. Models connect through providers in the parent's registry, and the parent handles auth per request.

**Why.** Safety policy must not be bypassed by delegating. Credentials are never copied to a child.

## ADR-4: Implementation work goes through a worktree and a patch

**Decision.** `isolation: worktree` agents start from a snapshot of the working tree (uncommitted and untracked files included, ignored files excluded). The user's `HEAD`, index and files never change. Results come back as a patch, and the parent applies it after review.

**Why.** Parallel writers must not collide with each other or the user. A patch gives the parent one reviewable, reversible step.

**Consequences.**
- Worktrees live outside the project. Ignored files (`node_modules`, `.env`) and settings above the repository root are not visible, so the worker installs what it needs.
- Worktree children cannot start in a read-only repository (`git worktree add` writes to `.git`).
- A worker's worktree is exclusive while it runs. After it ends, reviewers can share it through `target`.
- A worktree is not an OS sandbox. `bash` keeps the account's full access.

## ADR-5: Every worktree run is saved as a result commit and a patch

**Decision.** On finish, all worktree changes are kept as `…/runs/<n>/changes.patch` and as a commit at `refs/pi-crews/<parent session id>/<child name>/<n>` in the project repository.

**Why.** If the records folder or the patch file is lost, the patch can be rebuilt from the commit with `git diff --binary <baseCommit> <commit>`.

**Consequences.**
- Every patch is cumulative from the worktree's base, so the parent applies only the latest one. The parent is told so, because applying an earlier patch first makes the latest one fail.
- The refs show in `git log --all` and go out with `git push --mirror`, not with a normal `git push`.
- `/crew remove` deletes the ref together with the worktree, and only when the worktree's changes are all in the latest result. Run it after applying the patch.

## ADR-6: Records live in the Pi agent directory

**Decision.** Records go to `~/.pi/agent/crews/--<project path>--/<parent session id>/<child name>/`, using Pi's session folder naming rule. The child list and state are stored in the parent session.

**Why.** `git clean -fdx` must not delete records, and children must work in read-only projects. Reopening Pi restores the list. A run that was in progress is reported as `RUNTIME_LOST`, and recorded conversations can be continued.

**Consequences.**
- A child belongs to the session whose folder holds it. Pi's fork and clone copy the records into a session with a new ID, but not the folders. That session starts with no children, so two sessions never write one transcript or one worktree. Child operations on such a name give `CHILD_NOT_FOUND`, and using it as a target gives `WORKSPACE_UNAVAILABLE`. Both messages say the child is not in this session.
- A child that inspects a worktree its session no longer holds is not restored, because it could write into a worktree it cannot claim.
- Moving within one session with `/tree` keeps its children. They are outside resources that a branch of the conversation does not undo.

## ADR-7: Work-time budget excludes waiting for people

**Decision.** Each run has a budget (default 30 minutes, range 1–1440). Time waiting for approval prompts and for the parent's answers does not count. Work time before and after a question adds up. At 1/5 left the child is told to wrap up (1–5 minutes, at most half the budget). At zero the run ends as `timed_out` (`EXECUTION_TIMEOUT`).

**Why.** A budget should measure the child's work, not how slow the humans are. Restarting it after each answer would let a run go on without limit.

**Consequences.**
- The budget is fixed when the child is admitted. Editing the agent file does not change existing children. The only way to change it is `budgetMinutes` on a call.
- `budgetMinutes` on a run that already started has no effect.
- Timed-out children can be continued with `send_agent` and a new budget, or replaced by a new child.
- The wrap-up notice stops the step in progress and becomes the child's next prompt. Steering it in would wait for the step to end, and a command that never ends (seen in evaluation: a 24-day timer, an endless loop) kept the notice away until the budget ran out, leaving an empty result. Stopping uses Pi's abort, so it works for every tool that honors it, and keeps one clock. A long command that was healthy is stopped too; the child can run the quickest check again in its reserve.

## ADR-8: `completed` means only "the run ended"

**Decision.** Results never claim the goal was met or the tests passed. The parent verifies.

**Why.** A finished run and a correct outcome are different facts. Mixing them hides failures.

## ADR-9: One delivery for every piece of news

**Decision.** Waiting parents receive news in the waiting call. Idle parents get a new turn for results, questions and important messages. Working parents get one batch at a safe conversation boundary. Plain messages never wake the parent. Each piece of news is delivered exactly once. A result from a child the parent cancelled does not wake the parent. If the user steers a working parent, a pending wait ends with `steered` and children keep running.

**Why.** The parent must not be flooded, and the user must stay in control of the parent's turn.

**Consequences.** Automatic wake-up starts a parent turn even while the user types. The draft is kept.

## ADR-10: Limits are fixed and small

**Decision.** 8 tasks per call, 4 concurrent runs, 16 KiB task text, 4 KiB per message/question/answer, 8 KiB per result and 24 KiB results per response, 32 KiB per response overall, 32 `notify_parent` and 8 `ask_parent` per run, 5 s cancel wait. Long results are cut and the full text goes to `outputPath`.

**Why.** Bounded output protects the parent's context. Bounded concurrency protects the user's machine and API quota.

## ADR-11: `ask_parent` must be the only tool call in its message

**Decision.** `ask_parent` pauses the child. It is rejected if the same message has other tool calls. The parent's `send_agent` answer resumes the same conversation.

**Why.** Other calls in the same turn would run, or be left unanswered, while the child waits.

## ADR-12: Agent files use Pi's subagent format

**Decision.** One Markdown file per agent, same frontmatter as Pi's subagent example, plus pi-crews fields. Unknown fields are ignored. A file with an invalid value drops out of the list, and requesting it fails with `CONFIG_INVALID` and an explanation. Lookup: package `agents/`, then `~/.pi/agent/agents/`, then a trusted project's `.pi/agents/`. A later `.md` replaces an earlier one entirely. A `<name>.yml` (plain YAML, settings only, validated like the frontmatter, `null` clears a field) is the one exception: it changes the settings of the agent beneath it, from an earlier folder or the `.md` in its own folder, and the instructions stay with that `.md`. A `.yml` with no agent beneath it is invalid.

**Why.** Both extensions can share one folder. Ignoring unknown fields matches other Pi resources and keeps other extensions' fields safe, at the cost of silent typos. Whole-file replacement avoids hidden merging rules for instructions. Merging is limited to settings, so a user can change a bundled agent's model without freezing a copy of its instructions.

## ADR-13: An unsupported thinking level rejects the whole request

**Decision.** If a model does not support the requested level, no child starts.

**Why.** Starting only part of a batch leaves the parent with a half-done plan to untangle.

## ADR-14: `/crew` saves each choice at once and never writes an invalid file

**Decision.** There is no "Save?" step. Saving re-reads the file and edits only the changed fields, so other extensions' fields and YAML comments stay. Before writing, the agent that the changed files would make is built with the same code as loading, over every folder from the package up to the top one, from fresh file contents. If that agent would be invalid, or the `.md` it builds on is gone, nothing is written. Bundled agents are never edited. Where a change goes depends on the agent: (a) a bundled agent: settings go to the user's `<name>.yml`, instructions to the user's `<name>.md`; (b) an agent topped by a `.yml`: settings go to that `.yml`, instructions to the `.md` in the same folder; (c) a plain user or project `.md`: edited in place. A missing `.md` is created from the current text of the `.md` the instructions come from, with only the body changed. The `.yml` is never removed or rewritten by an instructions change, so `.md` and `.yml` side by side is the normal state. Deleting an agent removes the files the confirmation lists: normally its files in the folder of its top layer, so what they replaced applies again. For an agent that is broken because its `.md` is invalid, that includes the settings files in later folders attached to it, so none is left to apply to what takes over. Pi and `agents.ts` decide what is valid. The screen only shows it. Changes apply to children spawned afterwards.

**Why.** Fewer steps, no lost fields, and one place that owns validation. A settings change no longer copies the bundled instructions, so later fixes to them still reach the user.

## ADR-15: One module owns knowledge Pi does not publish

**Decision.** `src/pi.ts` is the only place that copies unpublished Pi knowledge: supported versions, session folder naming, CLI built-in extensions, thinking levels, drawing conversations as TUI components, and footer for closed sessions. `test/pi.test.ts` pins it to real Pi behavior.

**Why.** A Pi upgrade changes one file and one test file. Check them first when raising the supported version.

## ADR-16: The panel and conversation screen are view-only and yield to prompts

**Decision.** Users watch children but cannot message them. Only the parent instructs children. If another window such as an approval prompt takes the keyboard, the conversation screen and `/crew` screen close and the panel drops its selection. Keys follow Pi's key settings and plain letter keys always work. Panel rows do not reorder while the user is inside the panel.

**Why.** A prompt must never be hidden or starved of keys. Stable rows keep the user's cursor on the child they chose.

## ADR-17: Child cost is shown, not merged into `/session`

**Decision.** Cost appears in results, the panel, the conversation screen and the non-TUI `/crew` report. A forked child's tokens and cost exclude the inherited parent conversation, but its context usage includes it.

**Why.** Counting the inherited conversation twice would overstate spend. Context usage should reflect what really fills the child's window.

## ADR-18: The parent's messages reach a child at Pi's boundaries

**Decision.** `send_agent` to a queued or running child keeps the message in the crew. The child receives it word for word at Pi's next `turn_end` or `agent_before_settle` boundary, as the parent receives news (ADR-9). A message that comes after the last boundary continues the run. If the run has completed, it starts the next run. A stopped or failed run drops it.

**Why.** Pi's steer queue treats text as typed input: it expands `/` templates, rejects a message that names a command (an unhandled rejection that ends Pi), and drops a message that arrives after its last check. Owning the queue makes "delivered" mean "received".

**Consequences.** The run budget's wrap-up notice uses the same queue (ADR-7). Nothing is handed to a step that is being stopped.

## ADR-19: A parent is never held by one child

**Decision.** A foreground `spawn_agent` waits at most 10 minutes, the same limit as `wait_agent`, then returns the results so far. `pending` lists each unfinished child with the tools it is running and for how long.

**Why.** In evaluation, two finished reviewers waited 23 minutes behind a third stuck in a command, and the parent could not see why. With finished results and the running tools in hand, the parent can act on what is done and stop what looks stuck.

**Consequences.** A long foreground delegation returns before every child ends. The rest arrive as news (ADR-9).

## ADR-20: Removing a worktree needs proof that nothing is lost, and Git's worktree commands run one at a time

**Decision.** `/crew remove` deletes a worktree only if nothing that Git holds for it would be lost: its files (read exactly from disk, with every Git setting that lets Git skip files turned off), its own index, and every commit that `HEAD` or its reflog (both sides of each entry) ever pointed to must be the base, a saved result, or reachable from one. A populated tracked submodule always keeps the worktree, because its own files are outside what a tree comparison proves. The worktree is reserved for the whole removal, so no child can start in it while the removal waits in the queue. It also proves twice, before the checks and right before the delete, that the folder is a worktree of this repository inside this session's folder. A child holds its worktree until its session has fully closed. `Worktrees` runs every `git worktree add` and `remove` one at a time per repository (keyed by the real path of the common Git directory), in a queue shared by the whole process.

**Why.** Comparing files with the base was not enough. Real runs and reviews lost work through a sparse checkout, a case-only rename, a mode change hidden by `core.fileMode=false`, content staged and then overwritten on disk, a commit made and then reset away, and a child extension that wrote files while its session shut down. Git does not lock `.git/worktrees/`, so a `worktree add` could read the half-written entry of another and fail (about 1 in 250 parallel starts).

**Consequences.** Removal is slower (one exact read of every file) and refuses more often, with the reason in the message. Ignored files, repositories nested in ignored folders, `core.symlinks=false` link types and lossy clean filters are not protected; the docs say so. Two Pi processes on one repository can still race. Automatic removal of idle worktrees was considered and dropped: the user chooses with the `/crew` screen, which shows the disk each worktree uses.

## ADR-21: An answer takes a turn back at the next step boundary

**Decision.** A run holds one of the 4 turns from its start, or its answer, until it asks a question or ends. A run waiting for the parent's answer gives its turn to the next in line. Turns go to answered children first, then to children that stepped aside for them, then to children that have not started; each group in the order it joined the line. When an answered child waits and no turn is free, the next running child to finish a step that it goes on from waits at that boundary (after its tool calls, before its next model request) and gives the answered child its turn. Waiting for a turn after an answer or after stepping aside does not count as working time (ADR-7).

**Why.** Before, an answered child waited until another run ended. Runs are not interrupted, so that could take a whole budget, and the wait counted as working time: a child with a 1-minute budget ran out of time in the line without running again. Keeping the turn while waiting for the parent would leave turns idle, and a parent that waits for a queued child before answering would wait forever. Running the answered child over the limit would break the limit (ADR-10).

**Consequences.**
- No more than 4 children work at once; an answer waits at most for one step of a running child. A step that never ends keeps its turn until the run budget stops it.
- The child that stepped aside keeps its session, worktree and pending messages. Its conversation shows nothing of the wait. Its prompt cache may expire if the wait is long.
- Pi lets an extension continue a run at `turn_end` but not end it, so the wait happens inside the `turn_end` handler. Stopping such a child releases the wait after the abort.
- The `queued` state also covers a child that stepped aside; the panel calls it "Waiting its turn".
