pi-sidecar

Unified visible Herdr sidecars for Pi: sidecar-as-subagent with lightweight prompt launches, frozen /goal audit chain, wait-all, and selectable agent backends (Codex/Claude/Pi).

Packages

Package details

extension

Install pi-sidecar from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-sidecar
Package
pi-sidecar
Version
0.1.0
Published
Aug 29, 2026
Downloads
124/mo · 124/wk
Author
ethan_szu
License
MIT
Types
extension
Size
151.6 KB
Dependencies
0 dependencies · 2 peers
Pi manifest JSON
{
  "extensions": [
    "./extensions/pi-sidecar.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-sidecar

pi-sidecar is a Pi package: it exposes a visible Herdr sidecar as a set of Pi extension tools. A sidecar is a subagent: each sidecar runs in its own tab of the same workspace, running a full CLI TUI process (Codex / Claude Code / Pi / the 21 herdr-supported agent kinds), with its own context, tools, and quota. The main pane always runs Pi; the parent owns evidence review, admission, stage closing, and pane closing - a sidecar's idle / done is only a candidate signal, not completion.

Two launch modes:

  • Lightweight mode: sidecar_launch(prompt, agent_type?, profile_id?, auto_close?) - inline task text + optional persona (.pi/agents/<type>.md) + a chosen backend/model profile. auto_close defaults to true: the pane auto-closes after settled (done/idle); blocked is retained for takeover. Set it to false to keep the pane. The prompt can be any text, including a Codex /goal and slash commands.
  • Frozen mode: sidecar_launch(launch_spec_path, expected_launch_spec_sha256) - a SHA-256-bound launch spec (brief + /goal + dispatch); close must go through a disposition spec double-verdict (research-loop audit chain).

N parallel tasks = N sidecar_launch calls -> N separate tabs (the main tab is left untouched); sidecar_wait with no arguments is wait-all, and the settled watcher also wakes the parent one by one.

Responsibility boundaries

Object What it owns What it does NOT own
subagent Labels the sidecar agent's duty and review role, helping the parent explain the type of work delegated It is not a second, independent scheduler; it does not own pane lifecycle, evidence admission, or stage completion
visible-sidecar Launches and executes a frozen /goal; owns its own process / session / pane; registers ownership, waits for settled, and returns candidate signals to the parent It does not interpret scientific results, does not decide retain/close, and does not own the parent's session or pane
parent Freezes the launch spec, independently re-verifies evidence, writes the disposition, and triggers close only after the task is closed and evidence is admitted It must not treat idle, done, or a watcher event as completion

So "the agent inside the sidecar has finished" and "the sidecar pane should be reclaimed" are two distinct events: the former is represented by the Herdr agent state, and the latter must go through the parent's disposition. A pane left open is not because the watcher forgot to clean up, but because a legal close decision has not arrived; if the parent has finished review but has not called dispose, the extension retains it for rework or manual takeover.

Prerequisites

  • herdr CLI (herdr.dev): the entire sidecar pane/agent lifecycle is driven through the herdr socket API; when not installed, all tools report herdr_command_failed.
  • The Pi session must run inside a herdr pane (HERDR_ENV=1 + HERDR_PANE_ID injected): the sidecar opens in a separate tab of the same workspace (via herdr tab create); calls outside herdr are rejected with not_in_herdr.
  • Sidecar backend CLI: the CLI matching the profile's kind (codex / claude / pi, etc.) must be on PATH and logged in; Codex's directory-trust prompt is auto-answered by the extension (y + Enter).

Installation

A local package path can be handed straight to Pi:

pi install /home/xyh/code/pi-sidecar

This writes to the user-level Pi settings; a newly opened Pi session loads the extension. To load it temporarily without changing settings:

pi -e /home/xyh/code/pi-sidecar/extensions/pi-sidecar.ts

The Pi package manifest declares the entry in pi.extensions in package.json; @earendil-works/pi-coding-agent and typebox are Pi-provided peer dependencies, so the package does not download extra runtime deps.

Runtime model

Inputs must be frozen first

The launch tool does not accept a mutable natural-language task; it reads a pi-sidecar/launch-v1 JSON. That record binds the following files each to a SHA-256:

  1. brief: the bounded task description;
  2. goal: the full Qiaomu /goal;
  3. parent_dispatch: an existing upstream parent dispatch or equivalent control record;
  4. subagent_role: the logical role, e.g. implementer, reviewer, scout;
  5. transport_kind: currently fixed to visible-sidecar.

This means that after launch, the task brief, /goal, or parent constraints cannot be silently swapped. A helper can generate the record:

node /home/xyh/code/pi-sidecar/scripts/create-launch-spec.mjs \
  --cwd "$PWD" \
  --task-id independent-review \
  --role reviewer \
  --brief artifacts/independent-review/brief.md \
  --goal artifacts/independent-review/goal.md \
  --parent-dispatch artifacts/independent-review/dispatch.json \
  --profile pi-default \
  --placement auto \
  --out artifacts/independent-review/launch.json

Pass the command's output sha256 verbatim to sidecar_launch. The tool recomputes the digests of launch, brief, goal, and dispatch; if any one does not match, it refuses to open the pane.

parent_dispatch is a loose reference: only schema_version === "to-sidecar/dispatch-v1" and the SHA-256 binding are validated; the full fields of to-sidecar/dispatch-v1 (route / execution / outputs, etc.) are not. So a route declared in the dispatch (e.g. codex-exec / background) does not constrain the visible-sidecar transport that pi-sidecar actually executes; it only serves as a tamper-proof anchor for the upstream control record.

Tab layout

Each sidecar opens in its own tab of the parent's workspace, created with:

herdr tab create --workspace <parent-ws> --cwd <project> --label <task> --no-focus

The parent's main tab is never split or re-laid-out, so the user's current view is undisturbed. The sidecar's pane id comes from result.root_pane.pane_id and the tab id from result.tab.tab_id. N parallel tasks = N separate tabs. The launch spec's placement field is now informational only: the geometry/split logic was removed, so auto / right / down no longer affect the layout.

Sidecar ownership and task state are persisted through Pi session custom entries. After restarting or reloading the same session, the extension restores the unclosed panes created by the current parent and retakes settled watchers still in creating, running, or watcher-failed states; records from a different session or a different parent pane are not taken over.

Environment detection and safety boundary

Every tool that calls Herdr requires:

HERDR_ENV=1
HERDR_PANE_ID=wN:pN

In other words, the extension can only launch from a Herdr-managed main pane; it does not guess the main pane from a fuzzy cwd outside a pane, and it does not treat HERDR_ENV as a forgeable switch. Tab creation acts on the parent's workspace (derived from HERDR_PANE_ID), using --cwd and --no-focus, so the parent's tab is kept and the user's focus is not stolen.

Pi tools

Tool What it does What it must not do
sidecar_launch Launches a visible-sidecar, registers ownership, and starts the settled watcher. Two modes: lightweight (prompt + optional agent_type persona + profile_id backend/model + auto_close, default true); frozen (launch spec + SHA-256, executes /goal). Lightweight mode auto-closes the pane after settled (done/idle) (disposition=auto); blocked is retained for takeover; setting auto_close:false keeps the pane A sidecar cannot admit its own evidence or close a stage; only one of the two modes is allowed; frozen mode does not support auto_close:true
sidecar_status Reads the Herdr lifecycle state and returns the registered subagent/sidecar state Must not interpret done / idle as completion
sidecar_wait Bounded wait for an owned sidecar to reach done/idle/blocked (default 120s, cap 2h); a single pane_id, multiple pane_ids, or neither = wait-all (all unclosed sidecars in the current session); returns a status table The returned states are only candidate signals, not evidence admission
sidecar_insert Injects a mid-run message into a running, owned sidecar and waits for it to enter working to confirm delivery Does not modify the frozen launch spec or /goal; delivery is not task completion
sidecar_dispose Retains or closes a pane created by the current session. Lightweight mode: pane_id + disposition + note (parent decision text); frozen mode: disposition spec + SHA-256 (double verdict) Cannot close a pane that was not created by the current parent session

After sidecar_launch returns, the watcher automatically waits for Herdr's done, idle, or blocked and wakes the parent via a pi-sidecar/sidecar-settled-v1 custom message and a pi-sidecar:sidecar-settled event. This event's semantics is a task_ready_for_review candidate signal; it does not auto-close the pane. After the main Agent completes independent replay and evidence admission, it must submit a parent disposition before the controller can execute retain or close.

Goal sidecar persistent receipt (bg-task style)

On settled, the extension writes a goal-sidecar-receipt-v1 receipt: frozen mode writes it to the launch spec's directory (goal-receipt-<task_id>.json); lightweight mode writes to <cwd>/.pi-sidecar/receipts/. Receipt fields: task_id, pane_id, agent_status (done/idle/blocked normalized to settled), task_state (blocked / task_ready_for_review), launch_spec_sha256, auto_close, disposition, settled_at, evidence_paths (initially empty, filled in after the parent's independent review). A failed write goes through the existing watch_failed path to wake the parent, never silently skipped.

Sidecar profiles

Built-in profiles:

  • pi-default: inherits Pi's default provider, model, and thinking level;
  • codex-luna-max: Codex + gpt-5.6-luna + max.

Users can place a pi-sidecar/profiles-v1 in either of:

~/.pi/agent/pi-sidecar.json       # user default
<project>/.pi-sidecar.json        # current-project override

Copy from fixtures/pi-sidecar.config.example.json to declare a named Pi model/thinking level, a Codex model/reasoning config, or a Claude Code profile. A profile's args is passed as argv to herdr agent start --kind <kind> -- ..., never shell-joined; the model can only be a named profile, so arbitrary commands cannot be stuffed into a tool parameter.

For Claude Code, first run herdr agent in the target Herdr environment to confirm the currently supported kind name and native parameters, then write it into the profile. This avoids presenting unverified Herdr adapter names as available capabilities.

The main pane's runtime is always Pi; profiles only affect newly created sidecars.

Closing rules

A sidecar can be closed once its mission is complete, but closing is not the sidecar's power. The parent Agent must first independently read its artifact, replay key commands, and form a result; then write a pi-sidecar/disposition-v1:

{
  "schema_version": "pi-sidecar/disposition-v1",
  "task_id": "independent-review",
  "pane_id": "w1:p4",
  "disposition": "close",
  "parent_verdict": {
    "task_closed": true,
    "sidecar_admitted": true
  }
}

A close missing either true is rejected; when there is rework, follow-up tasks, or manual takeover, the parent Agent can only write retain. To avoid mis-killing, the extension only disposes of panes created by the current parent session whose parent pane matches. idle, done, a watcher settled, or a child session shutdown cannot alone trigger a close.

Verification

Local contract checks:

cd /home/xyh/code/pi-sidecar
node --test test/*.test.mjs
node scripts/check-extension-lifecycle.mjs  # fake Herdr + official loader: launch -> settled -> insert -> wait -> status -> disposition -> close
node scripts/check-extension-loader.mjs   # Pi official extension loader loads extensions/pi-sidecar.ts and verifies the 5 registered tools (requires globally installed pi-coding-agent)
python3 /home/xyh/.agents/skills/qiaomu-goal-meta-skill/scripts/lint_goal_command.py \
  fixtures/qiaomu-e2e.goal.zh.md fixtures/qiaomu-e2e.goal.en.md
python3 /home/xyh/.agents/skills/qiaomu-goal-meta-skill/scripts/test_lint_goal_command.py

Real Herdr + Pi + Qiaomu /goal replay can only run from inside a Herdr pane:

cd /home/xyh/code/pi-sidecar
scripts/herdr-qiaomu-e2e.sh --close-after-verify

This script creates a visible Pi sidecar, loads this extension, injects fixtures/qiaomu-e2e.goal.zh.md, waits for the settled state, independently re-runs the checks from the parent script, and only explicitly closes the pane after verification. Without --close-after-verify it retains the sidecar for manual takeover; this script path is still the raw Herdr E2E, while the extension-tool lifecycle is covered separately by node scripts/check-extension-lifecycle.mjs.

Current scope

v0.1 implements Herdr-only, source-bound launch, sidecar /goal execution, subagent role labeling, separate-tab layout, profile adapter, settled watcher, mid-run insert, bounded wait, session-persisted ownership, rollback on partial launch failure, parent-ownership retention/close, and the extension-tool lifecycle check (launch -> settled -> insert -> wait -> status -> disposition -> close, with real Herdr + fresh Pi driver E2E evidence: artifacts/e2e-lifecycle-20260826/). It does not mix tmux/Zellij into the same extension, and does not auto-close a pane without a parent disposition; those two capabilities will be added only after their own runtime ownership and E2E evidence.