can you see pi-side-agents extension full codebase locally?


**Checking file access capabilities**

The user is asking if I can see something locally, and I need to be truthful about my capabilities. I can access the local filesystem in my current environment and check if a repository is present. It seems like I should locate it, so I'll consider using a bash command to find it. Let’s clarify that I’m about to inspect the local environment to provide the right information!

bash

pwd && ls -1 /home/sebas && find /home/sebas /mnt/rpi -maxdepth 4 \( -type d -o -type l \) -iname '*pi-side-agents*' 2>/dev/null | head -200


/home/sebas
AGENTS.md
AGENTS.md.bck
ballbox
bug-bounty
client_secret_1019422495774-0cvo8lefqh667vgmekec8ruqdnrior71.apps.googleusercontent.com.json
Desktop
Documents
downloads
Downloads
eiffel-monitor
go
health-system
inspect-ourvend.spec.ts
logs
memory-audit-report.html
Music
notas
opencode-web.log
ops-notes
Pictures
pihole-unbound-tailscale-blueprint.bck.md
pihole-unbound-tailscale-blueprint.md
PORTAL-SYSTEM-RUNBOOK.md
Public
scripts
SERVICES-ARCHITECTURE.md
telegram-bot-listener-backup-20260118
Templates
test-results
venvs
Videos
/home/sebas/.npm-global/lib/node_modules/pi-side-agents


Yes.

Local install found at:

`/home/sebas/.npm-global/lib/node_modules/pi-side-agents`

So I can read the full codebase from this machine if you want.

If you want, next I can:
1. map the repo structure
2. inspect one file/module
3. summarize how it works end to


can it spawn subagents recursively? i plan on trying it out for a workflow with just one active TUI orchestrator which spawns project-specific orchestrators which themselves spawn workers for targeted work


**Inspecting code for details**

I think I need to get some answers from my knowledge base. Maybe I should inspect the code a little bit, keeping honesty in mind. I'll look into the package and possibly use a command like grep to search for "agent-start" usage or any limitations related to it. It feels like I'm being thorough by checking the details, and I hope this approach pays off. Let's see what I can find!

grep -RIn "agent-start\|agent-check\|agent-send\|agent-wait-any\|subagent" /home/sebas/.npm-global/lib/node_modules/pi-side-agents 2>/dev/null | head -200


pi-side-agents

Side-agent orchestration for Pi — spin off parallel child agents in tmux windows and git worktrees.

pi pi-coding-agent pi-package parallel agent subagent tmux worktree

node --test --test-concurrency=1 tests/integration/side-agents.integration.test.mjs


/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/docs/extensions.md:2244:| `subagent/` | Spawn sub-agents | `registerTool`, `exec` |
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/CHANGELOG.md:703:- Fixed subagent extension example to resolve user agents from the configured agent directory instead of hardcoded paths ([#1559](https://github.com/badlogic/pi-mono/pull/1559) by [@tianshuwang](https://github.com/tianshuwang))
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/CHANGELOG.md:843:- Fixed subagent example unknown-agent errors to include available agent names ([#1414](https://github.com/badlogic/pi-mono/pull/1414) by [@dnouri](https://github.com/dnouri))
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/CHANGELOG.md:2211:- Subagent example README referenced incorrect filename `subagent.ts` instead of `index.ts` ([#427](https://github.com/badlogic/pi-mono/pull/427) by [@Whamp](https://github.com/Whamp))
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/README.md:13:- Custom tools (todo lists, questions, subagents, output truncation)
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/prompts/implement-and-review.md:4:Use the subagent tool with the chain parameter to execute this workflow:
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/prompts/scout-and-plan.md:4:Use the subagent tool with the chain parameter to execute this workflow:
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/prompts/implement.md:4:Use the subagent tool with the chain parameter to execute this workflow:
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/agents/worker.md:3:description: General-purpose subagent with full capabilities, isolated context
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/index.ts:4: * Spawns a separate `pi` process for each subagent invocation,
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/index.ts:12: * Uses JSON mode to capture structured output from subagents.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/index.ts:211:	const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-subagent-"));
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/index.ts:432:		name: "subagent",
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/index.ts:435:			"Delegate tasks to specialized subagents with isolated context.",
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/index.ts:675:					theme.fg("toolTitle", theme.bold("subagent ")) +
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/index.ts:695:					theme.fg("toolTitle", theme.bold("subagent ")) +
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/index.ts:708:				theme.fg("toolTitle", theme.bold("subagent ")) +
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/README.md:3:Delegate tasks to specialized subagents with isolated context windows.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/README.md:7:- **Isolated context**: Each subagent runs in a separate `pi` process
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/README.md:12:- **Abort support**: Ctrl+C propagates to kill subagent processes
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/README.md:17:subagent/
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/README.md:38:mkdir -p ~/.pi/agent/extensions/subagent
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/README.md:39:ln -sf "$(pwd)/packages/coding-agent/examples/extensions/subagent/index.ts" ~/.pi/agent/extensions/subagent/index.ts
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/README.md:40:ln -sf "$(pwd)/packages/coding-agent/examples/extensions/subagent/agents.ts" ~/.pi/agent/extensions/subagent/agents.ts
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/README.md:44:for f in packages/coding-agent/examples/extensions/subagent/agents/*.md; do
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/subagent/README.md:50:for f in packages/coding-agent/examples/extensions/subagent/prompts/*.md; do
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/overlay-qa-tests.ts:472:			echo "This simulates subagent output with colors, hyperlinks, and long paths"
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/node_modules/@mariozechner/pi-coding-agent/examples/extensions/README.md:42:| `subagent/` | Delegate tasks to specialized subagents with isolated context windows |
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/skills/agent-setup/SKILL.md:11:Finish by backing up this agent-setup SKILL.md to .pi/side-agents/agent-start~/ for easy future upgrades.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/skills/agent-setup/SKILL.md:15:Upgrade setup to new pi-side-agents version: Diff this file with the current backup `.pi/side-agents/agent-start~/`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/skills/agent-setup/SKILL.md:32:4. **Overwrite existing files** – If `.pi/side-agent-start.sh` or similar already exist, overwrite them? *(default: no — skip existing files)*
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/skills/agent-setup/SKILL.md:52:### File 1: `$GIT_ROOT/.pi/side-agent-start.sh`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/skills/agent-setup/SKILL.md:74:  echo "[side-agent-start] Could not determine current branch in $WORKTREE."
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/skills/agent-setup/SKILL.md:78:echo "[side-agent-start] agent=$AGENT_ID branch=$BRANCH main=$MAIN_BRANCH"
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/skills/agent-setup/SKILL.md:81:  echo "[side-agent-start] ERROR: child worktree is on $MAIN_BRANCH; expected a dedicated agent branch."
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/skills/agent-setup/SKILL.md:87:echo "[side-agent-start] Worktree based on parent HEAD ($(git -C "$WORKTREE" rev-parse --short HEAD))."
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/recovery.md:31:- `agent-check`: `{ "ok": false, "error": "Unknown agent id: ..." }`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/recovery.md:32:- `agent-wait-any`: `{ "ok": false, "error": "Unknown agent id(s): ..." }` (fails fast on first poll)
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/recovery.md:33:- `agent-send`: `{ "ok": false, "message": "Unknown agent id: ..." }`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/recovery.md:72:- `agent-send` returns `tmux window is not active`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/recovery.md:79:agent-check { "id": "<id>" }
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/recovery.md:94:agent-send { "id": "<id>", "prompt": "!/quit" }
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/recovery.md:100:- spawn a replacement agent if needed (`/agent ...` or `agent-start`)
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/recovery.md:133:3. Retry the failed action (`/agents`, `agent-check` tool, etc.).
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/recovery.md:185:   - `agent-check` tool for `<id>`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/recovery.md:198:WT="../<repo>-agent-worktree-0001"   # use actual worktreePath from agent-check
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/todo.md:43:- [x] Scaffold `.pi/side-agent-start.sh`.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/todo.md:55:- [x] Implement `agent-check` payload with backlog tail.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/todo.md:62:- [x] `agent-start` tool.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/todo.md:63:- [x] `agent-check` tool.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/todo.md:64:- [x] `agent-wait-any` tool.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/todo.md:65:- [x] `agent-send` tool with `!` interrupt and `/` command forwarding.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/todo.md:68:  - `agent-start` success response now includes `ok: true` (consistent with error shape).
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/todo.md:69:  - `agent-wait-any` fails fast (`{ ok: false }`) on first-pass unknown IDs instead of looping forever.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/todo.md:70:  - `agent-send` inserts a 300 ms pause after C-c interrupt so Pi can return to prompt before text lands.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/todo.md:71:  - `agent-check`, `agent-wait-any`, `agent-send` tools now wrapped in try/catch (registry lock timeout no longer propagates as an unhandled throw).
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/architecture.md:27:- `agent-start`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/architecture.md:28:- `agent-check`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/architecture.md:29:- `agent-wait-any`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/architecture.md:30:- `agent-send`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/architecture.md:74:- `agent-start` requires an explicit `branchHint` slug.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/architecture.md:153:- `.pi/side-agent-start.sh`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/architecture.md:254:- `.pi/side-agent-start.sh` — optional bootstrap/validation hook (run by launcher if executable)
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/architecture.md:263:### 10.1 `agent-start`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/architecture.md:282:### 10.2 `agent-check`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/architecture.md:294:### 10.3 `agent-wait-any`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/architecture.md:308:### 10.4 `agent-send`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/README.md:5:Instead of waiting for one backlog item to finish before starting the next, spin tasks out into single-use child agents as soon as they occur to you. Each child runs in its own **tmux window** and **git worktree**, so you can keep shipping in parallel while maintaining isolation and control (asynchronous does not mean autonomous). Each child is a one-off and lives and dies with its short topic branch and tmux window—no "teams of long-running agents messaging each other" or "role-based subagents" complexity. The workflow is unified, simple, and deterministic.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/README.md:7:The most advanced users of AI coding agents have worked like this for a while, but the setup has been a bit daunting. This extension automates the full tmux/worktree/merge lifecycle for you and takes just a few seconds to set up. Plus, side agents can also be spawned and controlled by another agent to orchestrate its own flock of subagents.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/README.md:22:- Exposes orchestration _tools_ for parent agents: `agent-start`, `agent-check`, `agent-wait-any`, `agent-send`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/package.json:13:    "subagent",
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts:1168:START_SCRIPT=\"$WORKTREE/.pi/side-agent-start.sh\"
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts:1957:			return { ok: false, error: "agent-wait-any aborted" };
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts:2405:		name: "agent-start",
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts:2408:			"Start a background side agent in tmux/worktree. Lifecycle: child implements the change or asks for clarification -> wait-state and yield -> parent inspects (agent-check or agent-wait-any), reviews work, reacts -> eventually, parent asks child to wrap up (send 'LGTM, merge'), sends /quit when child is done. Provide a short kebab-case branchHint (max 3 words) for the agent's branch name. Returns { ok: true, id, task, tmuxWindowId, tmuxWindowIndex, worktreePath, branch, warnings[] } on success, or { ok: false, error } on failure.",
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts:2452:		name: "agent-check",
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts:2474:		name: "agent-wait-any",
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts:2477:			"Wait for an agent to finish its work. Returns the agent's status payload (same shape as agent-check) once it completes (done), yields (waiting_user), fails, or crashes.",
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts:2496:		name: "agent-send",


bash

grep -RIn "recursive\|spawned and controlled by another agent\|flock of subagents\|parent agent\|child agent" /home/sebas/.npm-global/lib/node_modules/pi-side-agents/README.md /home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/architecture.md /home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts 2>/dev/null | head -100


# pi-side-agents

**Code in sprints** (using agents *asynchronously*), **not in a marathon** (*sequential* task-by-task flow).

Instead of waiting for one backlog item to finish before starting the next, spin tasks out into single-use child agents as soon as they occur to you. Each child runs in its own **tmux window** and **git worktree**, so you can keep shipping in parallel while maintaining isolation and control (asynchronous does not mean autonomous). Each child is a one-off and lives and dies with its short topic branch and tmux window—no "teams of long-running agents messaging each other" or "role-based subagents" complexity. The workflow is unified, simple, and deterministic.

The most advanced users of AI coding agents have worked like this for a while, but the setup has been a bit daunting. This extension automates the full tmux/worktree/merge lifecycle for you and takes just a few seconds to set up. Plus, side agents can also be spawned and controlled by another agent to orchestrate its own flock of subagents.

**Warning:** You will build a lot more, which means you may run out of context windows and need to take better care of your wellbeing between sprints. Also, for the community's sake, please don't max out Claude subscriptions with Pi—use a Codex model (or APIs) by default.

---

(A main Pi agent console: the status line shows the currently operating side-agents. Once an agent entry turns blue, you can switch to the shown tmux window and unblock it. Fire off new ideas with a single /agent command. A new side-agent opens a tmux window in the background and automatically gets its short-lived topic branch, a separate worktree with properly replicated build setup, and merges back to main once you type "LGTM".)

## What it does

- New command `/agent [-model ...] <task>` to spawn a background child Pi agent.
- Shows active-agent summary with tmux window numbers in the statusline.
- New command `/agents` to inspect current agents and clean up stale state.
- New skill `agent-setup` to scaffold project-specific lifecycle scripts (flexible worktree initialization and merge process).
- Exposes orchestration _tools_ for parent agents: `agent-start`, `agent-check`, `agent-wait-any`, `agent-send`

## Install

```bash
pi install npm:pi-side-agents
```

This registers the extension and skill with Pi. Restart Pi after installing.

## Quick start

1. **Run setup once** in your project: `/skill:agent-setup`
   - If you want to change the setup later, or are upgrading this skill and want to get new setup goodies, just re-run the skill with a short prompt.
2. **Spawn asynchronous work items at any point** during your work:
   - `/agent wait, why is weirdMethod doing something-weird?`
   - `/agent -model gpt-5.3-codex add regression tests for auth`
   - Keep firing new items as they appear. As a rule of thumb, start all new work via `/agent`, but you can also use it only for ad hoc side questions.
3. **Check progress and attend** to the baby agents:
   - Check the statusline for which agents (by branch and tmux window) are waiting for you.
   - Use `/agents` to get a detailed overview of what's being done right now.
   - Steer the waiting children and work with them as normal Pi instances—just switch tmux windows.
4. If an agent is done, review its work and once happy, confirm by **LGTM, merge**.
   - Recommended: Write `commit your work when done` in your `AGENTS.md`. (You can always tell the agent to amend.)
   - Quickest way to review: ctrl+z, `git show`, `fg` to go back to the baby agent's Pi.
   - Your main worktree should be clean at this point; avoid editing in the main tree while side agents are active.
   - You can also tell your Pi to open GitHub PRs instead of merging locally, if that's what you prefer.
5. The agent will merge its work into your main repo. **Just type `/quit` and move on.**
   - Old worktrees are kept around and reused and updated by new agents.
   - Old branches are auto-pruned during reuse by a new agent.
   - You can pause your work on a topic—if you `/quit` before work is merged, the branch will stay around.

## Requirements

- `tmux`
- Git repository (worktrees enabled)
- Pi configured/authenticated

## Development

Run tests:

```bash
npm run test:unit
npm run test:integration
```

## Docs

- Architecture: `docs/architecture.md`
- Recovery/runbooks: `docs/recovery.md`

[2 more lines in file. Use offset=81 to continue.]


# pi-side-agents architecture (as implemented)

This document describes the **current** behaviour of `pi-side-agents` as implemented in:

- `extensions/side-agents.ts`

Project-specific lifecycle scripts are created by the `agent-setup` skill under `.pi/` in your repository root. These files are **local/runtime configuration** and should remain **untracked**.

## 1) Problem statement

Run multiple Pi coding tasks in parallel without blocking the main session:

- Keep momentum in a primary Pi session.
- Spawn short-lived child Pi sessions in parallel.
- Isolate changes via **git worktrees + topic branches**.
- Provide observability (statusline + `/agents` + tools).

## 2) Public surfaces (stable contract)

### Commands

- `/agent [-model <provider/id-or-pattern>] <task>` — spawn a child agent.
- `/agents` — list tracked agents, show orphan worktree locks, offer interactive cleanup.

### Tools (for orchestration)

- `agent-start`
- `agent-check`
- `agent-wait-any`
- `agent-send`

These are the contract used by the Pi side-agent harness (and by other parent agents).

## 3) State root + on-disk layout

**State root** is normally the git repo root (`git rev-parse --show-toplevel`).

It can be overridden via environment variable:

- `PI_SIDE_AGENTS_ROOT` — used mostly so child sessions always write to the *parent* repo’s registry/runtime directories.

Paths (relative to state root):

- Registry: `.pi/side-agents/registry.json`
- Registry lock: `.pi/side-agents/registry.lock`
- Runtime per agent: `.pi/side-agents/runtime/<agentId>/`
  - `kickoff.md` — kickoff prompt text
  - `backlog.log` — tmux pane output (via `tmux pipe-pane`)
  - `exit.json` — exit marker written by launcher (`{ exitCode, finishedAt }`)
  - `launch.sh` — generated launcher script (what tmux runs)
- Runtime archive (when reusing an agent id): `.pi/side-agents/runtime-archive/<agentId>/<stamp>/...`

Worktree pool (sibling directories of repo root):

- `../<repoBasename>-agent-worktree-0001`
- `../<repoBasename>-agent-worktree-0002`
- ... (dynamic; no fixed cap)

Per-worktree lock (inside each worktree):

- `<worktree>/.pi/active.lock`

Merge critical-section lock (only if your **finish script** uses it):

- `.pi/side-agents/merge.lock`

## 4) Agent identity model

### 4.1 Agent IDs

Agent IDs are **kebab-case slugs** (e.g. `fix-auth-leak`, `add-auth-tests-2`).

- `/agent` generates a slug from the task (LLM-assisted when possible, with heuristic fallback).
- `agent-start` requires an explicit `branchHint` slug.
- IDs are deduplicated (`-2`, `-3`, …) against:
  - current registry entries
  - any existing `side-agent/<id>` branches in `git worktree list --porcelain`

### 4.2 Branch names

Each agent uses a deterministic branch name:

- `side-agent/<agentId>`

## 5) Worktree pool manager

### 5.1 Slot selection

When starting an agent, the extension scans `..` (parent directory of repo root) for directories matching:

- `<repoBasename>-agent-worktree-####` (4-digit index)

It chooses the **first** usable unlocked slot:

- Skips slots with `.pi/active.lock`.
- Skips unlocked slots with local changes (`git status --porcelain` non-empty).
- Skips non-worktree directories that are non-empty.

If none are available, it creates the next slot with `git worktree add`.

### 5.2 Baseline commit

Agents are based on the **parent checkout’s current `HEAD`**, not necessarily the `main` branch:

- The parent’s `HEAD` commit hash is read once at spawn time.
- The agent worktree is hard-reset/cleaned to that commit.

This is intentional: it lets you spawn agents from whatever state you are currently on.

### 5.3 Reset/checkout policy for reused slots

For an already-registered worktree slot, the extension performs (best-effort):

- `git merge --abort` (ignore errors)
- `git reset --hard <parentHead>`
- `git clean -fd`
- `git checkout -B side-agent/<agentId> <parentHead>`

It then tries to delete the previous branch name (only if fully merged) to avoid accumulating old side-agent branches.

### 5.4 Worktree lock file (`.pi/active.lock`)

A JSON lock file is written into the chosen worktree:

- `<worktree>/.pi/active.lock`

Example (fields may evolve):

```json
{
  "agentId": "fix-auth-leak",
  "sessionId": "/path/to/parent/session.jsonl",
  "parentSessionId": "/path/to/parent/session.jsonl",
  "pid": 12345,
  "branch": "side-agent/fix-auth-leak",
  "startedAt": "2026-03-03T02:58:00.000Z",
  "tmuxWindowId": "@19",
  "tmuxWindowIndex": 3
}
```

[180 more lines in file. Use offset=141 to continue.]


/home/sebas/.npm-global/lib/node_modules/pi-side-agents/README.md:5:Instead of waiting for one backlog item to finish before starting the next, spin tasks out into single-use child agents as soon as they occur to you. Each child runs in its own **tmux window** and **git worktree**, so you can keep shipping in parallel while maintaining isolation and control (asynchronous does not mean autonomous). Each child is a one-off and lives and dies with its short topic branch and tmux window—no "teams of long-running agents messaging each other" or "role-based subagents" complexity. The workflow is unified, simple, and deterministic.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/README.md:7:The most advanced users of AI coding agents have worked like this for a while, but the setup has been a bit daunting. This extension automates the full tmux/worktree/merge lifecycle for you and takes just a few seconds to set up. Plus, side agents can also be spawned and controlled by another agent to orchestrate its own flock of subagents.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/README.md:22:- Exposes orchestration _tools_ for parent agents: `agent-start`, `agent-check`, `agent-wait-any`, `agent-send`
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/architecture.md:22:- `/agent [-model <provider/id-or-pattern>] <task>` — spawn a child agent.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/docs/architecture.md:32:These are the contract used by the Pi side-agent harness (and by other parent agents).
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts:499:			await fs.rm(runtimeDir, { recursive: true, force: true });
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts:524:	await fs.mkdir(path, { recursive: true });
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts:938:				await fs.rm(target, { recursive: true, force: true });
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts:2191:	// Inside a child agent, hide our own entry — only show siblings.
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts:2241:		description: "Spawn a background child agent in its own tmux window/worktree: /agent [-model <provider/id>] [-mode <name>] <task>",
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts:2410:			description: Type.String({ description: "Task description for child agent kickoff prompt (include all necessary context)" }),
/home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts:2499:			"Send a steering/follow-up prompt to a child agent's tmux pane. Returns { ok: boolean, message: string }.",


**Inspecting code section**

I need to take a closer look at the part around line 2191. It's tricky because I might have to check the environmental variables too. I want to ensure I don't overlook anything important. It feels like a puzzle where each piece might affect the overall picture. Let's see if there's any particular issue in that section or if the environment variables are causing trouble. I'll make sure to dig deep so I can understand it fully!


		},
		{ triggerTurn: false },
	);
}

async function renderStatusLine(pi: ExtensionAPI, ctx: ExtensionContext, options?: { emitTransitions?: boolean }): Promise<void> {
	if (!ctx.hasUI) return;

	const stateRoot = getStateRoot(ctx);
	const refreshed = await refreshAllAgents(stateRoot);
	const agents = Object.values(refreshed.agents).sort((a, b) => a.id.localeCompare(b.id));

	if (options?.emitTransitions ?? true) {
		const transitions = collectStatusTransitions(stateRoot, agents);
		if (transitions.length > 0) {
			emitStatusTransitions(pi, ctx, transitions);
		}
	} else if (!statusSnapshotsByStateRoot.has(stateRoot)) {
		collectStatusTransitions(stateRoot, agents);
	}

	// Inside a child agent, hide our own entry — only show siblings.
	const selfId = process.env[ENV_AGENT_ID];
	const visible = selfId ? agents.filter((r) => r.id !== selfId) : agents;

	if (visible.length === 0) {
		if (lastRenderedStatusLine !== undefined) {
			ctx.ui.setStatus(STATUS_KEY, undefined);
			lastRenderedStatusLine = undefined;
		}
		return;
	}

	const theme = ctx.ui.theme;
	const line = visible
		.map((record) => {
			const win = record.tmuxWindowIndex !== undefined ? `@${record.tmuxWindowIndex}` : "";
			const entry = `${record.id}:${statusShort(record.status)}${win}`;
			return theme.fg(statusColorRole(record.status), entry);
		})
		.join(" ");

	if (line === lastRenderedStatusLine) return;
	ctx.ui.setStatus(STATUS_KEY, line);
	lastRenderedStatusLine = line;
}

function ensureStatusPoller(pi: ExtensionAPI, ctx: ExtensionContext): void {
	statusPollContext = ctx;
	statusPollApi = pi;
	if (!ctx.hasUI) return;

	if (!statusPollTimer) {
		statusPollTimer = setInterval(() => {
			if (statusPollInFlight || !statusPollContext || !statusPollApi) return;
			statusPollInFlight = true;
			void renderStatusLine(statusPollApi, statusPollContext)
				.catch(() => {})
				.finally(() => {
					statusPollInFlight = false;
				});
		}, 2500);
		statusPollTimer.unref();
	}

	void renderStatusLine(pi, ctx).catch(() => {});
}

export default function sideAgentsExtension(pi: ExtensionAPI) {
	pi.registerCommand("agent", {
		description: "Spawn a background child agent in its own tmux window/worktree: /agent [-model <provider/id>] [-mode <name>] <task>",
		handler: async (args, ctx) => {
			const parsed = parseAgentCommandArgs(args);
			if (!parsed.task) {
				ctx.hasUI && ctx.ui.notify("Usage: /agent [-model <provider/id>] [-mode <name>] <task>", "error");
				return;
			}

			// Resolve -mode (pi-amplike modes) to a model spec if no explicit -model was given.

[288 more lines in file. Use offset=2250 to continue.]


Notes:

- `sessionId` is initially written as the parent session id, then updated by the **child** session to its own session id once linked.
- Orphan/stale lockfiles are never auto-deleted; `/agents` can offer a **user-confirmed** reclaim.

### 5.5 Replicating `.pi/side-agent-*` into worktrees

On allocation, the extension symlinks any entries in the parent repo’s `.pi/` whose names start with `side-agent-` into the child worktree’s `.pi/`.

This is how child worktrees discover:

- `.pi/side-agent-start.sh`
- `.pi/side-agent-finish.sh`
- `.pi/side-agent-skills/` (child-only skill directory)
- optional `.pi/side-agent-bootstrap.sh`

## 6) Tmux orchestrator + launcher

### 6.1 Preconditions

- `tmux` must be installed.
- `/agent` must be executed from within a tmux session.

### 6.2 Window lifecycle

- A new window is created in the **current tmux session**, named `agent-<agentId>`.
- The tmux pane is piped into `backlog.log`.
- The window runs the generated `launch.sh`.
- On Pi exit, the launcher writes `exit.json`, prompts:
  - `Press any key to close this tmux window…`
  and then kills the tmux window.

### 6.3 Child environment variables

The launcher exports (at minimum):

- `PI_SIDE_AGENT_ID`
- `PI_SIDE_PARENT_SESSION`
- `PI_SIDE_PARENT_REPO`
- `PI_SIDE_AGENTS_ROOT`
- `PI_SIDE_RUNTIME_DIR`

The child session uses these to link itself into the parent registry and to update the worktree lock.

## 7) Registry + status model

### 7.1 Registry file

Registry is a single JSON file:

- `.pi/side-agents/registry.json`

Writes are protected by:

- `.pi/side-agents/registry.lock`

Lock behaviour:

- Wait timeout: ~10s
- Stale lock reap: lock file older than ~30s is considered stale and removed

### 7.2 Runtime refresh rules

When the parent (or tools) refresh an agent record:

- If `exit.json` exists:
  - sets `exitCode` + `finishedAt`
  - marks status `done` for exit code `0`, else `failed`
  - removes `.pi/active.lock`
  - **auto-prunes** successful (`exitCode=0`) agents from registry
- If the tmux window is gone but no exit marker was recorded:
  - marks `crashed`
  - sets `error: "tmux window disappeared before an exit marker was recorded"` if missing
  - removes `.pi/active.lock`

### 7.3 Statuses

Statuses currently used by the core extension:

- `allocating_worktree` — parent is reserving record / picking a slot
- `spawning_tmux` — runtime dir + tmux window + launcher are being set up
- `running` — child is actively producing output
- `waiting_user` — child is idle and waiting for input (set from Pi lifecycle events in the child)
- `failed` — agent exited non-zero (exit marker recorded)
- `crashed` — tmux window disappeared without an exit marker
- `done` — internal terminal success state; record is usually immediately pruned

Statuses reserved for future / project-specific integration:

- `starting`
- `finishing`
- `waiting_merge_lock`
- `retrying_reconcile`

## 8) Statusline integration

In UI sessions, the extension polls every ~2.5s and sets a statusline segment under key `side-agents`.

Format (approx):

- `<id>:<short>@<tmuxWindowIndex>`

Example:

- `fix-auth-leak:wait@3 add-auth-tests:run@5`

## 9) Child lifecycle scripts (project-local)

The extension does **not** hardcode your bootstrap/merge policy.

Instead, `/skill:agent-setup` scaffolds these project-local (untracked) files:

- `.pi/side-agent-start.sh` — optional bootstrap/validation hook (run by launcher if executable)
  - Important: do **not** reset the worktree’s HEAD here; the extension already checked out the correct commit/branch.
- `.pi/side-agent-finish.sh` — your finish policy (default template: rebase + fast-forward)
- `.pi/side-agent-skills/finish/SKILL.md` — a child-only skill that invokes the finish script after explicit user confirmation

If your finish script uses a parent-side merge critical section, it typically implements it via `.pi/side-agents/merge.lock`.

[60 more lines in file. Use offset=261 to continue.]


**Investigating spawning recursion**

I need to determine if recursion is possible in this context. The documentation suggests it might be, but I should be cautious because shared registry root environments could cause descendants to register in the ancestor's root. I'll need to inspect the launch script or the logic related to the getStateRoot environment. It seems like child processes might utilize either their worktree git root or the inherited PI_SIDE_AGENTS_ROOT. I should examine how the environment variables are inherited during agent invocation.

grep -n "PI_SIDE_AGENTS_ROOT\|PI_SIDE_PARENT_REPO\|PI_SIDE_AGENT_ID\|PI_SIDE_PARENT_SESSION" -n /home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts | head -80


10:const ENV_STATE_ROOT = "PI_SIDE_AGENTS_ROOT";
11:const ENV_AGENT_ID = "PI_SIDE_AGENT_ID";
12:const ENV_PARENT_SESSION = "PI_SIDE_PARENT_SESSION";
13:const ENV_PARENT_REPO = "PI_SIDE_PARENT_REPO";


import { complete, type Message } from "@mariozechner/pi-ai";
import { convertToLlm, serializeConversation } from "@mariozechner/pi-coding-agent";
import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { spawnSync } from "node:child_process";
import { promises as fs } from "node:fs";
import os from "node:os";
import { basename, dirname, join, resolve } from "node:path";

const ENV_STATE_ROOT = "PI_SIDE_AGENTS_ROOT";
const ENV_AGENT_ID = "PI_SIDE_AGENT_ID";
const ENV_PARENT_SESSION = "PI_SIDE_PARENT_SESSION";
const ENV_PARENT_REPO = "PI_SIDE_PARENT_REPO";
const ENV_RUNTIME_DIR = "PI_SIDE_RUNTIME_DIR";

const STATUS_KEY = "side-agents";
const REGISTRY_VERSION = 1;
const CHILD_LINK_ENTRY_TYPE = "side-agent-link";
const STATUS_UPDATE_MESSAGE_TYPE = "side-agent-status";
const PROMPT_UPDATE_MESSAGE_TYPE = "side-agent-prompt";

const SUMMARY_SYSTEM_PROMPT = `You are writing a minimal handoff summary for a background coding agent.

Use the parent conversation only as context. Include only details that are directly relevant to the child task.

If the parent conversation is unrelated to the child task, output exactly:
NONE

Preferred content (but only when relevant):
- objective/constraints already established
- decisions already made
- key files/components to inspect
- risks/caveats`;

type AgentStatus =
	| "allocating_worktree"
	| "spawning_tmux"
	| "running"
	| "waiting_user"
	| "done"
	| "failed"
	| "crashed";

const ALL_AGENT_STATUSES: AgentStatus[] = [
	"allocating_worktree",
	"spawning_tmux",
	"running",
	"waiting_user",
	"failed",
	"crashed",
];

const DEFAULT_WAIT_STATES: AgentStatus[] = ["waiting_user", "failed", "crashed"];

type AgentRecord = {
	id: string;
	parentSessionId?: string;
	childSessionId?: string;
	tmuxSession?: string;
	tmuxWindowId?: string;
	tmuxWindowIndex?: number;
	worktreePath?: string;
	branch?: string;
	model?: string;
	task: string;
	status: AgentStatus;
	startedAt: string;
	updatedAt: string;
	finishedAt?: string;
	runtimeDir?: string;
	logPath?: string;
	promptPath?: string;
	exitFile?: string;
	exitCode?: number;
	error?: string;
	warnings?: string[];
};

type RegistryFile = {
	version: 1;
	agents: Record<string, AgentRecord>;
};

type AllocateWorktreeResult = {
	worktreePath: string;
	slotIndex: number;
	branch: string;
	warnings: string[];
};

type StartAgentParams = {
	task: string;
	branchHint?: string;
	model?: string;
	includeSummary: boolean;
};

type StartAgentResult = {
	id: string;
	tmuxWindowId: string;
	tmuxWindowIndex: number;
	worktreePath: string;
	branch: string;
	warnings: string[];
	prompt: string;
};

type PrepareRuntimeDirResult = {
	runtimeDir: string;
	archivedRuntimeDir?: string;
	warning?: string;
};

type ExitMarker = {
	exitCode?: number;
	finishedAt?: string;
};

type CommandResult = {
	ok: boolean;

[2417 more lines in file. Use offset=121 to continue.]


		if (!summary) {
			return { prompt: task + sessionSuffix };
		}

		const prompt = [
			task,
			"",
			"## Parent session",
			parentSession ? `- ${parentSession}` : "- (unknown)",
			"",
			"## Relevant parent context",
			summary,
		].join("\n");

		return { prompt };
	} catch (err) {
		return {
			prompt: task + sessionSuffix,
			warning: `Failed to generate context summary: ${stringifyError(err)}. Started child with raw task only.`,
		};
	}
}

function buildLaunchScript(params: {
	agentId: string;
	parentSessionId?: string;
	parentRepoRoot: string;
	stateRoot: string;
	worktreePath: string;
	tmuxWindowId: string;
	promptPath: string;
	exitFile: string;
	modelSpec?: string;
	runtimeDir: string;
}): string {
	return `#!/usr/bin/env bash
set -euo pipefail

AGENT_ID=${shellQuote(params.agentId)}
PARENT_SESSION=${shellQuote(params.parentSessionId ?? "")}
PARENT_REPO=${shellQuote(params.parentRepoRoot)}
STATE_ROOT=${shellQuote(params.stateRoot)}
WORKTREE=${shellQuote(params.worktreePath)}
WINDOW_ID=${shellQuote(params.tmuxWindowId)}
PROMPT_FILE=${shellQuote(params.promptPath)}
EXIT_FILE=${shellQuote(params.exitFile)}
MODEL_SPEC=${shellQuote(params.modelSpec ?? "")}
RUNTIME_DIR=${shellQuote(params.runtimeDir)}
START_SCRIPT=\"$WORKTREE/.pi/side-agent-start.sh\"
CHILD_SKILLS_DIR=\"$WORKTREE/.pi/side-agent-skills\"

export ${ENV_AGENT_ID}=\"$AGENT_ID\"
export ${ENV_PARENT_SESSION}=\"$PARENT_SESSION\"
export ${ENV_PARENT_REPO}=\"$PARENT_REPO\"
export ${ENV_STATE_ROOT}=\"$STATE_ROOT\"
export ${ENV_RUNTIME_DIR}=\"$RUNTIME_DIR\"

iso_now() {
  date -u +"%Y-%m-%dT%H:%M:%SZ"
}

write_exit() {
  local code="$1"
  printf '{"exitCode":%d,"finishedAt":"%s"}\n' "$code" "$(iso_now)" > "$EXIT_FILE"
}

cd "$WORKTREE"

if [[ -x "$START_SCRIPT" ]]; then
  set +e
  source "$START_SCRIPT" "$PARENT_REPO" "$WORKTREE" "$AGENT_ID"
  start_exit=$?
  set -e
  if [[ "$start_exit" -ne 0 ]]; then
    echo "[side-agent] start script failed with code $start_exit"
    write_exit "$start_exit"
    read -n 1 -s -r -p "[side-agent] Press any key to close this tmux window..." || true
    echo
    tmux kill-window -t "$WINDOW_ID" || true
    exit "$start_exit"
  fi
fi

PI_CMD=(pi)
if [[ -n "$MODEL_SPEC" ]]; then
  PI_CMD+=(--model "$MODEL_SPEC")
fi
if [[ -d "$CHILD_SKILLS_DIR" ]]; then
  # agent-setup writes the child-only finish skill here; load it explicitly.
  PI_CMD+=(--skill "$CHILD_SKILLS_DIR")
fi

set +e
"\${PI_CMD[@]}" "$(cat "$PROMPT_FILE")"
exit_code=$?
set -e

write_exit "$exit_code"

if [[ "$exit_code" -eq 0 ]]; then
  echo "[side-agent] Agent finished."
else
  echo "[side-agent] Agent exited with code $exit_code."
fi

read -n 1 -s -r -p "[side-agent] Press any key to close this tmux window..." || true
echo

tmux kill-window -t "$WINDOW_ID" || true
`;
}

function ensureTmuxReady(): void {
	const version = run("tmux", ["-V"]);
	if (!version.ok) {
		throw new Error("tmux is required for /agent but was not found or is not working");
	}

	const session = run("tmux", ["display-message", "-p", "#S"]);
	if (!session.ok) {

[1298 more lines in file. Use offset=1240 to continue.]


**Investigating state behaviors**

I need to consider the caveats around finding answers. I’m thinking that maybe a self-hiding search or nesting could work, but I also need to be clear about the state root behavior when starting. It seems like using getStateRoot might give me the environment or git root. I'll take a look at the relevant functions to see how they fit into this, as understanding their roles is essential for my approach.

grep -n "function getStateRoot\|const getStateRoot\|PI_SIDE_AGENTS_ROOT" /home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts | head -40


10:const ENV_STATE_ROOT = "PI_SIDE_AGENTS_ROOT";
445:function getStateRoot(ctx: ExtensionContext): string {


		// Best-effort cleanup: delete old branch if fully merged (-d, not -D).
		if (oldBranch && oldBranch !== branch) {
			run("git", ["-C", repoRoot, "branch", "-d", oldBranch]);
		}
	} else {
		if (await fileExists(chosenPath)) {
			const entries = await fs.readdir(chosenPath).catch(() => []);
			if (entries.length > 0) {
				throw new Error(`Cannot use worktree slot ${chosenPath}: directory exists and is not empty`);
			}
		}
		await ensureDir(dirname(chosenPath));
		runOrThrow("git", ["-C", repoRoot, "worktree", "add", "-B", branch, chosenPath, mainHead]);
	}

	await ensureDir(join(chosenPath, ".pi"));
	await syncParallelAgentPiFiles(repoRoot, chosenPath);
	await writeWorktreeLock(chosenPath, {
		agentId,
		sessionId: parentSessionId,
		parentSessionId,
		pid: process.pid,
		branch,
		startedAt: nowIso(),
	});

	return {
		worktreePath: chosenPath,
		slotIndex: chosen.index,
		branch,
		warnings,
	};
}

async function buildKickoffPrompt(ctx: ExtensionContext, task: string, includeSummary: boolean): Promise<{ prompt: string; warning?: string }> {
	const parentSession = ctx.sessionManager.getSessionFile();
	const sessionSuffix = parentSession ? `\n\nParent Pi session: ${parentSession}` : "";
	if (!includeSummary || !ctx.model) {
		return { prompt: task + sessionSuffix };
	}

	const branch = ctx.sessionManager.getBranch();
	const messages = branch
		.filter((entry): entry is SessionEntry & { type: "message" } => entry.type === "message")
		.map((entry) => entry.message);

	if (messages.length === 0) {
		return { prompt: task };
	}

	try {
		const llmMessages = convertToLlm(messages);
		const conversationText = serializeConversation(llmMessages);
		const userMessage: Message = {
			role: "user",
			content: [
				{
					type: "text",
					text: `## Parent conversation\n\n${conversationText}\n\n## Child task\n\n${task}`,
				},
			],
			timestamp: Date.now(),
		};

		const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
		if (!auth.ok) throw new Error(`Auth failed for summary generation: ${auth.error}`);
		const response = await complete(
			ctx.model,
			{ systemPrompt: SUMMARY_SYSTEM_PROMPT, messages: [userMessage] },
			{ apiKey: auth.apiKey, headers: auth.headers },
		);

		const summary = normalizeGeneratedSummary(
			response.content
				.filter((block): block is { type: "text"; text: string } => block.type === "text")
				.map((block) => block.text)
				.join("\n"),
		);

		if (!summary) {
			return { prompt: task + sessionSuffix };
		}

		const prompt = [
			task,
			"",
			"## Parent session",
			parentSession ? `- ${parentSession}` : "- (unknown)",
			"",

[1408 more lines in file. Use offset=1130 to continue.]


					if (record.status === "failed" || record.status === "crashed") {
						failedIds.push(record.id);
					}
					if (index < records.length - 1) {
						lines.push("");
					}
				}
			}

			if (orphanLocks.reclaimable.length > 0 || orphanLocks.blocked.length > 0) {
				if (lines.length > 0) lines.push("");
				lines.push("orphan worktree locks:");
				for (const lock of orphanLocks.reclaimable) {
					lines.push(`  reclaimable: ${summarizeOrphanLock(lock)}`);
				}
				for (const lock of orphanLocks.blocked) {
					lines.push(`  blocked: ${summarizeOrphanLock(lock)} (${lock.blockers.join("; ")})`);
				}
			}

			renderInfoMessage(pi, ctx, "side-agents", lines);

			if (failedIds.length > 0 && ctx.hasUI) {
				const confirmed = await ctx.ui.confirm(
					"Clean up failed agents?",
					`Remove ${failedIds.length} failed/crashed agent(s) from registry: ${failedIds.join(", ")}`,
				);
				if (confirmed) {
					// Collect worktree paths + agent IDs before removing records so we can release their locks.
					const worktreeEntries: { path: string; agentId: string }[] = [];
					registry = await mutateRegistry(stateRoot, async (next) => {
						for (const id of failedIds) {
							const rec = next.agents[id];
							if (rec?.worktreePath) worktreeEntries.push({ path: rec.worktreePath, agentId: id });
							delete next.agents[id];
						}
					});
					// Release worktree locks now that the agents are cleared.
					for (const entry of worktreeEntries) {
						await cleanupWorktreeLockBestEffort(entry.path, entry.agentId);
					}
					ctx.ui.notify(`Removed ${failedIds.length} agent(s): ${failedIds.join(", ")}`, "info");
				}
			}

			orphanLocks = await scanOrphanWorktreeLocks(repoRoot, registry);

			if (orphanLocks.reclaimable.length > 0 && ctx.hasUI) {
				const preview = orphanLocks.reclaimable.slice(0, 6).map((lock) => `- ${summarizeOrphanLock(lock)}`);
				if (orphanLocks.reclaimable.length > preview.length) {
					preview.push(`- ... and ${orphanLocks.reclaimable.length - preview.length} more`);
				}

				const confirmed = await ctx.ui.confirm(
					"Reclaim orphan worktree locks?",
					[
						`Remove ${orphanLocks.reclaimable.length} orphan worktree lock(s)?`,
						"Only lock files with no tracked registry agent and no live pid/tmux signal are included.",
						"",
						...preview,
					].join("\n"),
				);
				if (confirmed) {
					const reclaimed = await reclaimOrphanWorktreeLocks(orphanLocks.reclaimable);
					if (reclaimed.failed.length === 0) {
						ctx.ui.notify(`Reclaimed ${reclaimed.removed.length} orphan worktree lock(s).`, "info");
					} else {
						ctx.ui.notify(
							`Reclaimed ${reclaimed.removed.length} orphan lock(s); failed ${reclaimed.failed.length}.`,
							"warning",
						);
					}
				}
			}

			if (orphanLocks.blocked.length > 0 && ctx.hasUI) {
				ctx.ui.notify(
					`Found ${orphanLocks.blocked.length} orphan lock(s) that look live; leaving them untouched.`,
					"warning",
				);
			}
		},
	});

	pi.registerTool({
		name: "agent-start",
		label: "Agent Start",
		description:
			"Start a background side agent in tmux/worktree. Lifecycle: child implements the change or asks for clarification -> wait-state and yield -> parent inspects (agent-check or agent-wait-any), reviews work, reacts -> eventually, parent asks child to wrap up (send 'LGTM, merge'), sends /quit when child is done. Provide a short kebab-case branchHint (max 3 words) for the agent's branch name. Returns { ok: true, id, task, tmuxWindowId, tmuxWindowIndex, worktreePath, branch, warnings[] } on success, or { ok: false, error } on failure.",
		parameters: Type.Object({
			description: Type.String({ description: "Task description for child agent kickoff prompt (include all necessary context)" }),
			branchHint: Type.String({ description: "Short kebab-case branch slug, max 3 words (e.g. fix-auth-leak)" }),
			model: Type.Optional(Type.String({ description: "Model as provider/modelId (optional)" })),
		}),
		async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
			try {
				const started = await startAgent(pi, ctx, {
					task: params.description,
					branchHint: params.branchHint,
					model: params.model,
					includeSummary: false,
				});
				return {
					content: [
						{
							type: "text",
							text: JSON.stringify(
								{
									ok: true,
									id: started.id,

[108 more lines in file. Use offset=2430 to continue.]


	if (result.ok) {
		const root = result.stdout.trim();
		if (root.length > 0) return resolve(root);
	}
	return resolve(cwd);
}

function getCurrentBranch(cwd: string): string {
	const result = run("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"]);
	if (!result.ok) return "";
	const branch = result.stdout.trim();
	if (!branch || branch === "HEAD") return "";
	return branch;
}

function getStateRoot(ctx: ExtensionContext): string {
	const fromEnv = process.env[ENV_STATE_ROOT];
	if (fromEnv) return resolve(fromEnv);
	return resolveGitRoot(ctx.cwd);
}

function getMetaDir(stateRoot: string): string {
	return join(stateRoot, ".pi", "side-agents");
}

function getRegistryPath(stateRoot: string): string {
	return join(getMetaDir(stateRoot), "registry.json");
}

function getRegistryLockPath(stateRoot: string): string {

[2078 more lines in file. Use offset=460 to continue.]


**Examining state inheritance**

I’m thinking about how a child inherits the top-level state root. The nested child starts by using the state root from the environment it inherited, unless it’s overwritten by its launcher. If the child is launched with the PARENT_REPO set to the current worktree, it’s interesting to check how startAgent processes the parentRepoRoot parameter. I should take a closer look at that parameter and conduct a search for parentRepoRoot.

grep -n "parentRepoRoot\|getParentRepo\|ENV_PARENT_REPO" /home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts | head -80


13:const ENV_PARENT_REPO = "PI_SIDE_PARENT_REPO";
911:async function syncParallelAgentPiFiles(parentRepoRoot: string, worktreePath: string): Promise<void> {
912:	const parentPiDir = join(parentRepoRoot, ".pi");
1146:	parentRepoRoot: string;
1160:PARENT_REPO=${shellQuote(params.parentRepoRoot)}
1173:export ${ENV_PARENT_REPO}=\"$PARENT_REPO\"
1758:			parentRepoRoot: repoRoot,


			parentSessionId,
		});
		allocatedWorktreePath = worktree.worktreePath;
		allocatedBranch = worktree.branch;
		aggregatedWarnings.push(...worktree.warnings);

		const runtimePrep = await prepareFreshRuntimeDir(stateRoot, agentId);
		const runtimeDir = runtimePrep.runtimeDir;
		if (runtimePrep.archivedRuntimeDir) {
			aggregatedWarnings.push(`Archived existing runtime dir for ${agentId}: ${runtimePrep.archivedRuntimeDir}`);
		}
		if (runtimePrep.warning) {
			aggregatedWarnings.push(runtimePrep.warning);
		}

		const promptPath = join(runtimeDir, "kickoff.md");
		const logPath = join(runtimeDir, "backlog.log");
		const exitFile = join(runtimeDir, "exit.json");
		const launchScriptPath = join(runtimeDir, "launch.sh");
		await atomicWrite(logPath, "");

		await mutateRegistry(stateRoot, async (registry) => {
			const record = registry.agents[agentId];
			if (!record) return;
			record.worktreePath = worktree.worktreePath;
			record.branch = worktree.branch;
			record.runtimeDir = runtimeDir;
			record.promptPath = promptPath;
			record.logPath = logPath;
			record.exitFile = exitFile;
			await setRecordStatus(stateRoot, record, "spawning_tmux");
			record.warnings = [...(record.warnings ?? []), ...worktree.warnings];
		});

		const kickoff = await buildKickoffPrompt(ctx, params.task, params.includeSummary);
		if (kickoff.warning) aggregatedWarnings.push(kickoff.warning);

		await atomicWrite(promptPath, kickoff.prompt + "\n");
		try {
			await mutateRegistry(stateRoot, async (registry) => {
				const record = registry.agents[agentId];
				if (!record) return;
				await appendKickoffPromptToBacklog(stateRoot, record, kickoff.prompt);
			});
		} catch {
			// Best effort fallback when registry lock/update fails; write directly
			// to the known backlog path without requiring registry mutation.
			await appendKickoffPromptToBacklog(
				stateRoot,
				{
					id: agentId,
					task: params.task,
					status: "spawning_tmux",
					startedAt: now,
					updatedAt: nowIso(),
					runtimeDir,
					logPath,
				},
				kickoff.prompt,
			);
		}

		const resolvedModel = await resolveModelSpecForChild(ctx, params.model, pi.getThinkingLevel());
		const modelSpec = resolvedModel.modelSpec;
		if (resolvedModel.warning) aggregatedWarnings.push(resolvedModel.warning);

		const tmuxSession = getCurrentTmuxSession();
		const { windowId, windowIndex } = createTmuxWindow(tmuxSession, `agent-${agentId}`);
		spawnedWindowId = windowId;

		await updateWorktreeLock(worktree.worktreePath, {
			tmuxWindowId: windowId,
			tmuxWindowIndex: windowIndex,
		});

		const launchScript = buildLaunchScript({
			agentId,
			parentSessionId,
			parentRepoRoot: repoRoot,
			stateRoot,
			worktreePath: worktree.worktreePath,
			tmuxWindowId: windowId,
			promptPath,
			exitFile,
			modelSpec,
			runtimeDir,
		});
		await atomicWrite(launchScriptPath, launchScript);
		await fs.chmod(launchScriptPath, 0o755);

		tmuxPipePaneToFile(windowId, logPath);
		// Wait for the shell in the new tmux window to be ready before sending
		// commands — otherwise the keystrokes arrive before bash has initialised
		// and are silently lost (displayed as text but never executed).
		await tmuxWaitForShellReady(windowId);
		// Run cd in the interactive pane shell first so Ctrl+Z in child Pi drops
		// back to the child worktree prompt (not the parent worktree).
		tmuxSendLine(windowId, `cd ${shellQuote(worktree.worktreePath)}`);
		tmuxSendLine(windowId, `bash ${shellQuote(launchScriptPath)}`);

		await mutateRegistry(stateRoot, async (registry) => {
			const record = registry.agents[agentId];
			if (!record) return;
			record.tmuxSession = tmuxSession;
			record.tmuxWindowId = windowId;
			record.tmuxWindowIndex = windowIndex;
			record.worktreePath = worktree.worktreePath;
			record.branch = worktree.branch;
			record.runtimeDir = runtimeDir;
			record.promptPath = promptPath;
			record.logPath = logPath;
			record.exitFile = exitFile;
			record.model = modelSpec;
			await setRecordStatus(stateRoot, record, "running");
			record.warnings = [...(record.warnings ?? []), ...aggregatedWarnings];
		});

		const started: StartAgentResult = {
			id: agentId,
			tmuxWindowId: windowId,

[738 more lines in file. Use offset=1800 to continue.]


grep -n "agent-worktree" /home/sebas/.npm-global/lib/node_modules/pi-side-agents/extensions/side-agents.ts | head -40


796:	const prefix = `${basename(repoRoot)}-agent-worktree-`;
1025:		const name = `${basename(repoRoot)}-agent-worktree-${String(next).padStart(4, "0")}`;


**Clarifying naming conventions**

I realized that when I'm nested under the child repoRoot, the basename turns into the original slot name, which then adds the grandchild slot prefix as -agent-worktree. This could lead to some recursive path growth issues. I need to make sure the answer stays concise, though! Let's break it down clearly so I can effectively communicate it without getting lost in the details.

Yes. Full codebase local here:

`/home/sebas/.npm-global/lib/node_modules/pi-side-agents`

On recursion: looks like **yes, technically supported**.

Why:
- README says side agents can be “spawned and controlled by another agent”.
- Extension exposes tools for orchestration: `agent-start`, `agent-check`, `agent-wait-any`, `agent-send`.
- Child sessions get env wired for registry linkage and status tracking.
- Code explicitly handles child context. Example: inside child, statusline hides its own entry and shows siblings.

But for your exact pattern, important caveats:

1. **Shared registry root**
   - Child inherits `PI_SIDE_AGENTS_ROOT`.
   - So nested agents likely write into the **top parent repo’s** `.pi/side-agents/...` registry/runtime.
   - Good: one top-level orchestrator can observe whole tree.
   - Bad: hierarchy is not modeled explicitly. It’s more one shared flat registry than a true agent tree.

2. **Worktree naming gets weird on deep nesting**
   - Worktree slot prefix is based on `basename(repoRoot)`.
   - For a child orchestrator running inside `repo-agent-worktree-0001`, its spawned workers may get names like:
     - `repo-agent-worktree-0001-agent-worktree-0001`
   - So recursion works, but path names can grow ugly fast.

3. **Design intent is shallow async swarms, not deep org charts**
   - README also says it avoids “teams of long-running agents messaging each other”.
   - So this is closer to:
     - main orchestrator
     - child orchestrator
     - targeted workers
   - Not ideal for many recursive layers.

4. **Operational risk**
   - More layers = more tmux windows, more shared registry churn, more human review points.
   - You’ll want strict rules for:
     - who can merge
     - who only reports upward
     - when workers must yield vs continue

My take:
- **Your proposed shape should work**:
  - 1 active TUI orchestrator
  - project-specific orchestrators
  - targeted workers
- I would keep it to **2 levels below main max**.
- I would treat project orchestrators as coordinators, not autonomous mergers.

Best pattern:
- root TUI orchestrator spawns project orchestrators
- each project orchestrator spawns short-lived workers
- workers do scoped tasks only
- project orchestrator reviews/aggregates
- root orchestrator decides final integration

If you want, I can inspect the extension deeper and tell you:
- whether nested agents can safely merge back without stepping on each other
- what exact registry/worktree behavior you’ll get at 2-level recursion
- what changes would make this workflow robust before you try it.


do you have tools to search for pi extensions? there may be one better suited for this. else look manually for such tool which should exist, then use it


**Searching for pi extensions**

I need a direct answer to the user’s question about tools for searching pi extensions. It seems like they might want a local search. I think I should check if there's a tool in the database that allows for this. I’ll inspect available pi tooling or extensions, and see if pi has an install or search command. Using a bash search could be a way forward, so I'll look into that as well.

todo

Find Pi extension search capability

Check whether local tools can search Pi extensions/packages, and if not, manually locate such a tool/command in installed Pi code/docs, then use it to search for relevant extensions.


Created #1: Find Pi extension search capability (pending)

Find Pi extension search capability

Find Pi extension search capability


bash

command -v pi || true
pi --help 2>/dev/null | sed -n '1,220p' || true
printf '\n--- npm globals ---\n'
ls -1 /home/sebas/.npm-global/lib/node_modules | sed -n '1,200p'

grep -RIn "install npm:\|search.*extension\|pi search\|marketplace\|registry" /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent /home/sebas/.npm-global/lib/node_modules 2>/dev/null | head -250


/home/sebas/.npm-global/bin/pi

--- npm globals ---
@juicesharp
@malinamnam
@mariozechner
pi-btw
pi-session-switch
pi-side-agents
taskplane