Add artifact vault contents
This commit is contained in:
579
projects/dotfiles/027-pi-subagents-implementation-spec-task.md
Normal file
579
projects/dotfiles/027-pi-subagents-implementation-spec-task.md
Normal file
@@ -0,0 +1,579 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user