---
status: done
claimed-by: "019fba91-eabf-76ae-b086-a37ac061d6e8"
claimed-at: "2026-07-31T22:23:32-04:00"
completed-at: "2026-07-31T22:23:42-04:00"
parent: "[[002-pi-subagents-map]]"
blocked-by: []
tags:
  - ticket/task/afk
---

# Implementation ready specification

## Question

Synthesize the resolved route into an implementation-ready specification for the new neutral Pi subagent extension, including architecture, configuration schema, runtime model, context modes, status UI, lifecycle handling, trust boundaries, deployment seam, and verification plan.

## Goal

Build a new neutral Pi extension that can start context-clean subagents frequently.
The extension provides mechanism only.
Named agents, prompts, tool profiles, and policy live in Pi configuration.
The extension must not bundle opinionated subagent types.
Herdr support is out of the version-one core and may be added later as an adapter or replacement UI.

## Non-goals

Do not implement Herdr integration in version one.
Do not claim sandbox isolation.
Do not install Pi packages, npm packages, or git packages without explicit user consent.
Do not auto-write default config files.
Do not make the extension aware of Nix or Home Manager.
Do not ship opinionated named agents with the neutral extension.
Do not add persistent subagent lifetimes in version one.
Do not add summary-seeded context mode in version one.
Do not write separate result or artifact files in version one.

## Repository placement

Implement the extension under:

```text
modules/agents/pi/extensions/subagents/
├── index.ts
├── agents.ts
├── config.ts
├── runner.ts
├── supervisor.ts
├── status.ts
├── types.ts
└── ui.ts
```

`index.ts` is the Pi extension entry point.
`agents.ts` loads and validates named Markdown agent definitions.
`config.ts` loads global and trusted project config.
`runner.ts` implements the subprocess RPC child runner.
`supervisor.ts` owns child lifecycle, cancellation, timers, and status records.
`status.ts` defines child status records, durable entries, and status/result APIs.
`types.ts` holds shared types and schemas.
`ui.ts` owns the optional built-in Pi TUI presentation.

The existing `modules/agents/pi/pi.nix` recursively links `modules/agents/pi/extensions` to `~/.pi/agent/extensions`, so the extension is deployed through the existing durable dotfiles seam.

## Runtime files

The extension reads these Pi-native runtime paths:

- Global config: `~/.pi/agent/subagents.json`.
- Trusted project config: `.pi/subagents.json` through Pi's `CONFIG_DIR_NAME`.
- Global named agents: `~/.pi/agent/agents/*.md`.
- Trusted project named agents: `.pi/agents/*.md` through Pi's `CONFIG_DIR_NAME`.

Missing config files are normal.
Missing agent directories are normal.
The extension must not create them automatically.

The dotfiles module may later link plain files, render JSON from Nix attrs, or expose typed Nix options.
That module design is out of scope for the extension.

## Configuration model

### Config file ownership

Use dedicated JSON config files rather than unknown keys in Pi `settings.json`.
Global config is loaded from `~/.pi/agent/subagents.json`.
Project config is loaded from `.pi/subagents.json` only when `ctx.isProjectTrusted()` is true.
Project config overrides global config.
Do not implement tighten-only merge semantics.
Trusted project config may loosen or tighten global defaults.

### Config schema

Version one config shape:

```json
{
  "defaultContext": "independent",
  "defaultTools": "read-only",
  "maxConcurrent": 3,
  "timeouts": {
    "startMs": 30000,
    "idleMs": 0,
    "runMs": 0
  },
  "ui": {
    "enabled": true,
    "defaultExpanded": false,
    "showTranscriptMilestones": true
  },
  "toolProfiles": {
    "local-review": {
      "activeTools": ["read", "grep", "find", "ls"],
      "bash": "deny"
    }
  }
}
```

`0` timeout values mean disabled.
Unknown config keys should be ignored with a warning, not fatal.
Invalid known values should disable the affected config file and surface a diagnostic.

### Built-in defaults

In-code defaults apply when no config file exists or a field is absent.
The in-code default context is `independent`.
The in-code default tool profile is `read-only`.
The implementation may choose an initial `maxConcurrent`, but it must be configurable.

### UI config

`ui` settings gate only the bundled optional UI components.
They must not disable the status data model, status endpoints, neutral events, or durable child records.
Another extension should be able to replace the default UI by consuming the same status data.

## Named agent definitions

Named agents are Markdown files with YAML frontmatter and a Markdown body.
They are the only durable named-agent definition format in version one.
Do not define named agents inline in `subagents.json`.

Required frontmatter:

```yaml
name: review
description: Review code and report risks.
```

Optional frontmatter:

```yaml
context: independent
model: inherit
thinking: high
tools: read-only
allowedContexts:
  - independent
hidden: false
```

`name` is the canonical identity.
File names are storage only, but warn when the filename stem does not match `name`.
Names should be lowercase slugs with letters, numbers, and hyphens.
`description` is used for discovery, listing, and model-facing selection help.
The Markdown body is the base prompt or instruction text for the subagent.

### Definition precedence

Load definitions in this order:

1. Trusted project `.pi/agents/*.md`.
2. User `~/.pi/agent/agents/*.md`.

Project definitions override user definitions with the same name.
Duplicate names inside one precedence tier are configuration errors.
Do not resolve duplicates by filesystem order.

## Ad hoc runtime subagents

Version one supports ad hoc runtime subagents.
A spawn request may omit `agent` and provide only `prompt`.
This creates a one-off subagent that is not persisted as a named definition.
Ad hoc subagents inherit global and project defaults unless the spawn call overrides context, model, thinking, or tools.
Status should label ad hoc subagents with a generated short label such as `ad-hoc <child-id>`.

## Context modes

Version one supports exactly two context modes:

- `independent`.
- `fork`.

`independent` is the global default.
Parent conversation context is opt-in through `fork`.

### `independent`

`independent` receives:

- The spawn `prompt`.
- The selected named agent body if `agent` is present.
- Normal trusted project context such as `AGENTS.md`.
- Explicit attachments or snippets if the spawn request supports them later.

`independent` does not receive:

- Parent transcript.
- Generated parent summary.
- Hidden parent branch context.

### `fork`

`fork` receives the full active-branch transcript snapshot as-is.
It does not strip tool output.
It does not summarize before spawning.
It lets child Pi handle normal compaction if needed.
It appends a small wrapper that explains the delegated role, context boundary, and expected return shape.
Avoid the phrase "child agent" in user-facing text.
Prefer "subagent", "delegated agent", or "worker".

`fork` does not require special confirmation in version one.
Tool power and context inheritance are separate concerns.

### Context precedence

Resolve context mode in this order:

1. Spawn-call override.
2. Named agent frontmatter `context`.
3. Config `defaultContext`.
4. In-code default `independent`.

If a named agent has `allowedContexts`, reject a spawn that requests a context outside that list.

## Tool profiles

Agent frontmatter `tools` references a named tool profile.
Spawn calls may also override `tools` with a profile name.
Tool profiles compile to Pi active tools and optional tool-call gates.
This uses Pi-native mechanisms such as `pi.setActiveTools()` and `tool_call` blocking.
Do not build a broad permission DSL in version one.

Built-in profile names are reserved and cannot be overridden by config.
User config may add custom profiles and select the default profile.

Version-one built-in profiles:

- `none`.
- `read-only`.
- `read-only-with-safe-bash`.
- `full-tools`.

`none` has no tools.
`read-only` is local-only and includes `read`, `grep`, `find`, and `ls`.
`read-only-with-safe-bash` is local-only and adds `bash` with a read-only command allowlist.
`full-tools` means the normal full local Pi tool surface.
`full-tools` is not a sandbox permission level.
`full-tools` is not guarded by an extension confirmation prompt during the subagent run.
Version one does not restrict `full-tools` from being a global default, agent default, or spawn override.

Built-in profiles do not include web or network access except insofar as `full-tools` exposes ordinary bash.
A later `web-research` profile can be user-defined when an explicit web tool or bash-network policy exists.

Default tool profile resolution order:

1. Spawn-call override.
2. Named agent frontmatter `tools`.
3. Config `defaultTools`.
4. In-code default `read-only`.

## Model and thinking

Optional `model` can be `inherit` or a Pi model selector.
Optional `thinking` maps to Pi reasoning or effort levels such as `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`.
The selected model may clamp or ignore unsupported thinking values.

Resolution order:

1. Spawn-call override.
2. Named agent frontmatter.
3. Config default if later added.
4. Parent Pi session values.

Do not require model or thinking in named definitions.

## Spawn tools

Exact tool names are intentionally deferred to implementation, but version one must expose these surfaces:

- Single spawn.
- Batch spawn.
- List subagents.
- Get subagent status.
- Get subagent result.
- Cancel subagent.

Spawn requests use a single prose field named `prompt`.
Do not split prose into `task` and `instructions`.

Single spawn request shape:

```json
{
  "prompt": "Review the staged diff for rollback risks.",
  "agent": "review",
  "context": "independent",
  "model": "inherit",
  "thinking": "high",
  "tools": "read-only"
}
```

`prompt` is required.
`agent` is optional.
If `agent` is present, the named definition supplies the base prompt and defaults while `prompt` supplies the per-call request.
If `agent` is absent, the spawn is ad hoc.

Batch spawn request shape:

```json
{
  "subagents": [
    { "agent": "review", "prompt": "Review API risks." },
    { "prompt": "Independently sanity-check the deployment plan." }
  ]
}
```

Batch entries use the same shape as single spawn entries.

## Non-blocking behavior

All spawns are non-blocking in version one.
Single spawn and batch spawn return after spawn acceptance with child ids and initial metadata.
The extension must not block the parent tool call until the subagent completes.
The main agent decides whether and when to list, poll, retrieve, or cancel results.

Spawn acceptance result should include:

- Child id.
- Agent name or ad hoc label.
- Context mode.
- Tool profile.
- Model and thinking if resolved.
- Initial lifecycle state.
- Status/result retrieval hint.

## Result retrieval

Subagent results arrive through:

- Status data model.
- Neutral events.
- Durable milestone entries.
- Explicit result tool.

The parent receives final textual result plus compact metadata by default.
Metadata includes agent name, context mode, elapsed time, stop reason, and child status/session id.
Do not copy the child transcript into the parent context by default.
Do not copy selected evidence snippets into the parent context by default.

`subagent_list` should show active subagents plus bounded recent terminal history.
The default recent terminal history should be small, such as the last 10 terminal children in the current parent session.

Results and status persist through Pi session state and child session storage only in version one.
Do not write separate result files or artifact files.

## Runtime architecture

Use a `ChildRunner` interface so runtime can evolve without changing tools or status.
Version one implements `subprocess-rpc`.
An in-process SDK runner is an explicit later optimization.
Keep subprocess mode as permanent fallback.

`ChildRunner` responsibilities:

- Start a child Pi RPC process.
- Send a prompt command.
- Stream RPC events.
- Normalize lifecycle events.
- Abort or terminate the child.
- Return normalized terminal result to the supervisor.

Use Pi RPC mode instead of print mode.
RPC preserves prompt bytes, exposes events, supports `abort`, supports `get_state`, and is intended for embedding.

For `fork`, serialize the parent session header plus active branch entries to a temporary JSONL file.
Launch child Pi using that file as the starting session.
For `independent`, launch child Pi without parent session transcript.

## Supervisor architecture

The `Supervisor` is the only owner of child records, process handles, timers, event subscriptions, and lifecycle transitions.
Tool handlers must not spawn untracked child processes independently.

Lifecycle states:

- `queued`.
- `starting`.
- `running`.
- `settling`.
- `completed`.
- `failed`.
- `cancelled`.
- `timed_out`.
- `orphaned` if reload loses process ownership and no reattach protocol exists.

Use event-driven transitions.
Do not implement unbounded sleep-loop polling.
Use Pi RPC events, process lifecycle, abort signals, and bounded timers.
Use `agent_settled` as semantic completion.
Use process close for subprocess resource cleanup.

Timeouts:

- `startMs` guards process startup and prompt acceptance.
- `idleMs` guards no-progress hangs when nonzero.
- `runMs` guards total runtime when nonzero.

Timeout expiry marks the child as `timed_out` and runs the same cancellation path.

Cancellation is idempotent.
For subprocess RPC cancellation:

1. Send RPC `abort` when protocol is alive.
2. End stdin when appropriate.
3. Send SIGTERM to the process group on Unix.
4. Use `taskkill /T /F` on Windows.
5. Escalate to SIGKILL after a short grace period on Unix.
6. Resolve as `cancelled` unless a terminal result already exists.

On parent `session_shutdown`, cancel supervised children by default.
Detached persistence is out of version one.

## Status data model

Define a public status record that another extension can consume:

```typescript
interface SubagentStatus {
  id: string;
  label: string;
  agent?: string;
  adHoc: boolean;
  context: "independent" | "fork";
  state: "queued" | "starting" | "running" | "settling" | "completed" | "failed" | "cancelled" | "timed_out" | "orphaned";
  cwd: string;
  model?: string;
  thinking?: string;
  tools: string;
  startedAt: string;
  completedAt?: string;
  elapsedMs: number;
  lastEvent?: string;
  lastEventAt?: string;
  stopReason?: string;
  resultAvailable: boolean;
  childSession?: string;
  error?: string;
}
```

Expose status through registered tools and extension-local events.
Persist durable milestone entries with `pi.appendEntry()`.
Durable status entries do not participate in LLM context.

Do not expose secrets, full environment, provider credentials, full prompts, or full child tool output in status rows.

## Optional built-in UI

The default UI is optional and replaceable.
It consumes the status data model.
It must not be required for subagent lifecycle correctness.

Use three progressive layers:

1. Collapsed live summary by default.
2. Expanded live inspector on demand or by configuration.
3. Durable transcript milestone entries for historical record.

Collapsed summary should show quick counts such as `2 running · 1 queued`.
Expanded inspector should show child id, label, context mode, lifecycle state, elapsed time, model, tool profile, trust/source, last event, and result availability.
Transcript milestones should record spawn accepted, completed, failed, cancelled, and timed out.
Do not stream every child event into the parent transcript.

UI config gates only these components.
Status tools and data remain available when UI is disabled.

## Trust and security

Project config and project agent definitions are honored only after project trust.
User/global config is in the user's local trust boundary.
The extension must not claim sandboxing.
Subagents run with the local user's permissions unless the user later routes them through an actual sandbox backend.

Do not inherit project-local extensions, project packages, or arbitrary project resources into child processes by default unless Pi's normal trusted project startup does so and the implementation explicitly documents it.
Sanitize child process environment.
Strip stale parent `PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`, and `PI_REASONING_LEVEL` unless intentionally projecting parent metadata.
Pass provider credentials only as required by child Pi.

The extension does not add runtime confirmation gates for `fork` or `full-tools` in version one.
If a request is ambiguous, the main agent may ask the user before spawning.
The extension should avoid over-prescribing agent judgment.

## Diagnostics and validation

Startup or reload diagnostics should report:

- Invalid `subagents.json` syntax.
- Unknown built-in profile override attempts.
- Unknown default tool profile.
- Duplicate agent names in one precedence tier.
- Invalid frontmatter.
- Agent `context` outside `allowedContexts`.
- Unknown tool profile references.

Invalid project config should not break user/global config.
Invalid agent definitions should be skipped with diagnostics rather than crashing the extension.

## Tests and verification

Unit tests should cover:

- Config merge order.
- Project trust gating for project config and project agents.
- Agent frontmatter parsing.
- Duplicate name detection.
- Tool profile resolution.
- Built-in profile name reservation.
- Context resolution order.
- Spawn request validation.
- Non-blocking spawn acceptance result.
- Status state transitions.
- Result retrieval before and after completion.
- Cancellation idempotence.
- Timeout handling.
- Malformed RPC event handling.
- Oversized RPC line handling.
- Process exit before settlement.
- Settlement before process exit.
- Parent shutdown cleanup.

Manual verification:

```bash
nix eval .#nixosConfigurations.neogaia.config.modules.agents.pi.enable
```

```bash
nix eval .#nixosConfigurations.neogaia.config.home-manager.users.alexion.home.file.'"/home/alexion/.pi/agent/extensions"'.source
```

```bash
nix flake check
```

After implementation, verify inside Pi:

- `/reload` loads the extension.
- A single ad hoc subagent spawn returns a child id immediately.
- A batch spawn returns multiple child ids immediately.
- `subagent_list` shows active and recent terminal entries.
- `subagent_status` shows current lifecycle state.
- `subagent_result` returns still-running before completion and final result after completion.
- `subagent_cancel` cancels a running child.
- A `fork` run receives parent active-branch context.
- An `independent` run does not receive parent transcript.
- Disabling UI does not disable status/result tools.

## Open implementation choices left to coding

Choose exact tool names in the implementation.
Choose exact JSON schema names for timeout fields and UI toggles.
Choose exact read-only bash allowlist for `read-only-with-safe-bash`, using Pi plan-mode as the reference.
Choose the default `maxConcurrent` value.
Choose final status entry custom type names.

These are implementation details, not remaining design blockers.
