pi-conductor
Multi-role LLM orchestration via a guarded, observable handoff state machine — installs as a pi extension exposing `/conduct`.
Package details
Install pi-conductor from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-conductor- Package
pi-conductor- Version
0.22.1- Published
- Sep 30, 2026
- Downloads
- 1,940/mo · 195/wk
- Author
- lynellf
- License
- MIT
- Types
- extension
- Size
- 12.2 MB
- Dependencies
- 2 dependencies · 4 peers
Pi manifest JSON
{
"extensions": [
"./extensions"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-conductor
Portable agent orchestration for long-horizon coding work. pi-conductor brings Roo/Zoo-style multi-role workflows to
piwithout tying orchestration to an editor. Run cost-controlled workflows across budget and frontier models, local or remote providers, and terminal-native environments like SSH andtmux.
Status: pre-release. pi-conductor ships as a pi extension — install it with
pi install, type/conduct <goal>, and it orchestrates a multi-role LLM workflow on top of a guarded, observable handoff state machine. The pure FSM core + SDK host driver are the engine; the extension is the UX shell around it.
Contents
- What this is
- Quick start
- Documentation
- Status & what's left
- Architecture in brief
- Repo layout
- License
What this is
pi-conductor orchestrates multi-role LLM workflows as a deterministic hub-and-spoke state machine: one orchestrator role dispatches to one or more worker roles, every transition is validated against a pinned manifest snapshot, every state change is reduced through a pure reducer, and every record is appended to a run-keyed log. Caps (per-session, per-run, per-worker visit count) are enforced as host guards that synthesize machine events through the reducer — never by mutating the checkpoint.
It ships as a pi package:
pi install ./ # from this checkout (dev)
# or, once published:
pi install npm:pi-conductor
pi install git:github.com/lynellf/pi-conductor
After install, seven slash commands are available inside any pi session:
/conduct <goal> Start a run for <goal> using .pi/conductor.yaml
/conduct:resume <run_id> Resume a previously-started run by run_id
/conduct:list List known runs in the conductor log
/conduct:abort Abort the active run
/conduct:steer <message> Guide the active role before its next model call
/conduct:followup <message> Queue guidance for the next conductor prompt boundary
/conduct:copy Copy the latest completed role response
Plus a flag:
--conduct-manifest <path> Override the default manifest path
--conduct-sandbox-approval <path> Load host-owned sandbox approval JSON
--conduct-controller-approval <path> Load host-owned controller registry JSON
A thin CLI fallback (bin/conduct) also ships, for non-pi consumers and
scripted runs:
node dist/bin/conduct.js .pi/conductor.yaml "ship the changelog"
The CLI resolves its Pi SDK from PI_PACKAGE_DIR when set, otherwise from
locally installed peers, then from the npm Pi installation behind pi on
PATH. This supports pi install npm:pi-conductor, which omits host-provided
peer dependencies. If pi is a shell wrapper or its SDK cannot be discovered,
set PI_PACKAGE_DIR to the directory containing Pi's package.json and its
importable SDK. An invalid override is reported as an error.
The CLI also provides a machine-safe mode for benchmark adapters and other noninteractive callers:
conduct \
--non-interactive \
--log-dir /tmp/pi-conductor/run-123 \
--json \
.pi/conductor.yaml \
"Implement the requested repository change."
--non-interactive makes ask_user fail immediately instead of reading
stdin. When --log-dir <path> is omitted, the CLI writes durable run logs to
<cwd>/.pi-conductor/runs (creating missing parents); --log-dir <path>
selects an explicit persistent run-log directory and creates missing parents.
The separate offline advisory report reads every *.jsonl log in a runs
directory and does not make network requests or mutate the logs:
conduct advisory-report <runs-dir>
conduct advisory-report <runs-dir> --json
In both text and --json run modes the CLI writes one immediate run_started
NDJSON event to stdout once the run handle resolves and before completion:
{"schema_version":1,"event":"run_started","run_id":"…","log_dir":"…"} with an
absolute log_dir. Under --json, stdout is therefore an NDJSON stream of two
documents: that run_started event followed by the existing versioned terminal
JSON result; prompts, warnings, and diagnostics use stderr. In text mode the
same run_started line is followed by the existing human-readable terminal
line. Normal conductor terminal outcomes (done, session_failed, and
aborted) retain exit code 0
and are distinguished by exit_reason; setup and unexpected runtime errors
remain nonzero. While a run is active, the first SIGINT or SIGTERM requests
a graceful abort so terminal state can be persisted; a second signal exits
immediately.
Resume a previously started run from its durable log:
conduct resume [options] <manifestPath> <runId>
conduct resume --log-dir /tmp/pi-conductor/run-123 .pi/conductor.yaml <run_id>
resume accepts the same --non-interactive, --log-dir, --json, and
approval options as start, resolves the log directory with the same default
and override rules, restores the original goal from the durable log, and emits
the same immediate run_started event plus the same terminal result.
The engine is the same in all three surfaces — extension, CLI, and library.
Opt-in delegated command sandbox
Delegated children remain file-only unless their subagent profile declares an
execution block. The initial command backend is Linux Bubblewrap with no
network:
subagents:
- name: implementer
models:
- model: openai-codex:gpt-5.6-luna
effort: medium
max_session_cost_usd: 2
system_prompt: .pi/subagents/implementer.md
workspace:
projection:
allowed_paths: [src, tests, package.json]
default_paths: [src, tests, package.json]
execution:
backend: bubblewrap
runtime_root: prepared-runtime
writable_paths: [src, tests]
network: none
environment:
PATH: /usr/bin:/bin
LANG: C.UTF-8
max_output_bytes: 67108864
An opted-in run also requires explicit host approval. Pass
--sandbox-approval <path> to the standalone CLI or set Pi's
--conduct-sandbox-approval <path> flag for /conduct and
/conduct:resume. The manifest and model cannot supply this approval. The
approval file must be a canonical absolute path to a current-user-owned,
single-link regular file with mode 0600.
The approved Bubblewrap executable must be an exact reviewed patched build,
and the prepared runtime must contain the complete approved regular-file
inventory, /bin/bash, and the fixed compiled capability probe. Admission
rechecks binary identity and digest, runtime contents, probe digest, and the
required namespace behavior before a command is released. The primary checkout
and its Git control files must also have protected ownership and permissions;
admission rejects group-writable or other-writable paths rather than changing
their modes. See
sandboxed delegation
for approval preparation, guarantees, limits, and recovery.
Opt-in executable controller
A manifest may replace its orchestrator model session with a fixed-argv,
Bubblewrap-sandboxed repository controller. The controller owns scheduling and
semantic gates; the host retains admission, persistence, artifacts, costs,
cleanup, and final state transitions. Controller mode requires both the sandbox
approval and a separate protected controller registry. Pass
--controller-approval <path> to the standalone CLI or set Pi's
--conduct-controller-approval <path> flag. See
sandboxed repository controllers and the
examples/controller runnable example.
For private native outputs and approved integration/delivery, see the
operator guide and
delivery example.
Opt-in trusted local providers can implement forge publication and CI observation;
see the local provider guide for
measurement, private credentials, bounded waits, and crash recovery.
Two layers, kept strictly apart
- Pure core (
src/core,src/manifest,src/seam,src/cost,src/persistence) — the deterministic FSM reducer + manifest static checks + TypeBox emission schemas + cost roll-up. Zero pi imports. Enforced by a grep-guard test that scans source as text. - SDK host driver (
src/host) — owns the orchestration loop, persists records, and enforces caps. Shared roles use the in-process SDKcreateAgentSessionpath; isolatedworktreeandcopyroles use a host-owned package-localpi --mode rpcNode process whose current working directory is the provisioned role workspace.
The extension layer (extensions/conduct.ts + src/extension/) is the UX shell
that wraps the engine. It does not become the engine: the production Host
launches every worker role through the shared SDK or isolated RPC path, never
via ctx.newSession() / ctx.fork(). A grep guard on extensions/**/*.ts
rejects those two calls — the §9.5 boundary holds. While a conduct run is
active in the TUI, press Esc and confirm to abort it; the standalone conduct
CLI does not add that Escape interrupt.
For the full architecture rationale, see
docs/archive/orchestrator-fsm-spec.md
(the authority).
Quick start
1. Install
pi install ./ # from the checkout, dev install
pi list # verify: pi-conductor should appear
2. Declare roles
Roles live in a single YAML manifest, .pi/conductor.yaml. The repo ships an
example:
version: 1
end_request_roles: [reviewer]
roles:
- name: orchestrator
is_orchestrator: true
models: [anthropic:claude-sonnet-4-5]
max_run_cost_usd: 25.0
system_prompt: .pi/roles/orchestrator.md
tools: [read, bash, handoff, end]
- name: implementer
max_visits: 3
max_session_cost_usd: 5.0
models:
- model: anthropic:claude-opus-4-5
effort: high # explicit; effort defaults to "medium" when omitted
- openai:gpt-4o # legacy shorthand → { model, effort: "medium" }
system_prompt: .pi/roles/implementer.md
tools: [read, edit, write, bash, handoff, end]
- name: reviewer
max_visits: 3
system_prompt: .pi/roles/reviewer.md
tools: [read, grep, handoff, end]
Optional shadow-only delegation advisory
An operator may opt into bounded, advisory-only TypeSafe judgments by adding
this strict block to the top level of a manifest that already has a role with a
delegation policy:
delegation_advisory:
schema_version: 1
provider: typesafe_jev
model: jev-latest
mode: shadow
max_parallel: 4
request_timeout_ms: 5000
max_attempts: 1
Add a short description (1–500 characters) to each allowed subagent profile
to make it eligible for profile-fit comparison. For example, add this field to
an existing subagents: entry:
- name: api-implementer
description: Implements one bounded API contract and its tests.
Profile fit is asked only when the parent allows at least two profiles and each
allowed profile has a description; the host never uses system_prompt as a
substitute.
The advisory records never affect admission, prompts, scheduling, child-result
normalization, or routing. Read the operator disclosure
before enabling the block; it details the outbound data boundary and rollback.
3. Write role prompts
Each role's system prompt is a plain-prose .md file at the declared
system_prompt path. The host loads it via
DefaultResourceLoader({ systemPromptOverride }) and feeds it to the role's
session. See the shipped defaults at
tests/fixtures/default-conductor/.pi/roles/. A role prompt tells the role
which tools it has, what its legal handoff target is, and whether it may request
completion. The host force-injects both handoff and end into every role;
workers return through handoff, while only the orchestrator can finalize a run.
Accepted handoffs deliver structured fields to the recipient, including returning orchestrators, and retain them across restart. The complete JSON payload is limited to 64 KiB of UTF-8; an oversized payload receives a validation error that the role can correct in the same session. Use concise public artifact locators and hashes for larger evidence. Artifact declarations still require the existing host collection and delivery checks. Older run records retain reason-only delivery where structured fields were not persisted.
A minimal starter bundle is available programmatically:
import { getDefaultBundle } from "pi-conductor";
const { yaml, prompts } = getDefaultBundle(); // default conductor.yaml + orchestrator/worker prompts
4. Run
Inside any pi session in a project with .pi/conductor.yaml:
/conduct ship the changelog for the auth refactor
You'll see the conductor's status line update as the orchestrator dispatches to
workers; while a role session is active, the footer also shows
model=<provider:id> · effort=<level> (or model=<default> · effort=medium on
the system/default model path) for the current worker. The run reaches a
terminal state and notifies with the run_id, and /conduct:list shows the same
model and effort tokens for active runs. While the run is active, Esc opens a
confirmation dialog; confirming aborts the run just like /conduct:abort.
Use /conduct:steer to redirect the addressable active role, or
/conduct:followup to carry guidance across the next handoff. /conduct:copy
copies the latest completed assistant response without tool summaries and remains
available for the most recently completed run in the current pi process.
Documentation
The reference material is split into focused pages:
RoleConfigfields — manifest fields, gated completion, and versioning.- Retained orchestrator context — conversation continuity, compaction and recovery within a run.
- Tools available to roles — machine tools, SDK tools, and
the explicit
tools:allowlist. - Worktree subagent delegation — assignment-based delegation by default, legacy compatibility, child profiles, projections, optional Bubblewrap commands, artifacts, and branch integration.
- Shadow-only delegation advisory disclosure — opt-in TypeSafe data disclosure, record limits, and the offline calibration report.
- Per-role isolated workspaces — workspace backends, artifacts, mounts, and progressive disclosure.
- Sandboxed repository controllers — fixed-argv planner setup, operator approval, protocol, recovery, and migration.
- Advanced: library use — embedding the engine in a library or application.
- Hooking into the record stream — the emitter, consumer extension, and durable-log contract.
- Architecture in brief — the full architecture overview and invariants.
- Contributing — prerequisites and verification.
Status & what's left
Full status is tracked in the authoritative specs:
docs/archive/orchestrator-fsm-spec.md— the FSM engine.src/host/record-emitter.ts— the typed in-process emitter (subscribeToRecords) and its consumer contract.
Architecture in brief
checkpoint + event + def (pinned manifest snapshot)
│
▼
reduce() ── pure, deterministic, host-agnostic (src/core)
│
▼
transition record + new checkpoint
│
▼
host persists record + snapshot, spawns next role (src/host)
│
▼
┌────────┴────────┐
▼ ▼
bin/conduct extensions/conduct.ts
(CLI) (pi extension /commands)
The full architecture rationale and invariants are in
docs/architecture.md and the authoritative
docs/archive/orchestrator-fsm-spec.md.
Repo layout
src/
core/ FSM types + reducer + lifecycle + targets + run-memory (no pi)
manifest/ manifest types + parse + validate + toMachineDefinition
seam/ TypeBox emission schemas + validateEmission
cost/ pure usage roll-up + cap predicates
persistence/ RecordLog interface + InMemoryRecordLog
host/ SDK driver — the ONLY place that imports pi (engine)
extension/ UX shell helpers — wraps src/host for the extension
(may import pi; mirrors src/host/ posture)
bin/ conduct CLI fallback (built to dist/bin/conduct.js)
index.ts public barrel
extensions/
conduct.ts pi extension entrypoint (loaded by pi via jiti)
tests/
*.test.ts unit + E2E (stub-provider-driven; no API key)
grep-guard.test.ts asserts src/core + src/manifest (+seam/cost) have zero pi imports
package-metadata.test.ts asserts pi extension manifest + peer-dependency posture
docs/
archive/orchestrator-fsm-spec.md the spec (authority)
biome.json # linter + formatter (replaces ESLint + Prettier)
lefthook.yml # git hooks: pre-push runs lint + typecheck + tests
pnpm-workspace.yaml # pnpm config + supply-chain hardening (camelCase keys)
License
MIT — see LICENSE.