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.
- What this is
- Quick start
- Documentation
- Status & what's left
- Architecture in brief
- Repo layout
- License
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-conductorAfter 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
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> --jsonIn 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.
All hosts use bounded SDK file workers and foreground subprocesses. There is no native addon, compiler requirement, process-table scan, origin/owner admission, or Linux-only execution tier. Commands retain deadlines, bounded output and finite best-effort cancellation. Descendant cleanup is not guaranteed.
Ordinary interrupted execution history does not block resume or replacement,
and commands are never automatically replayed. Inspect partial effects before
explicitly requesting new work. Persistence ambiguity, budgets, FSM validation,
tool grants and effect authorization still apply. Historical strict policy is
accepted without restoring removed process policing. Git-backed workspaces and
file-tool delegation no longer require Linux ownership/permission admission.
The Bubblewrap backend and built-in executable controller delivery were removed, not weakened. Explicit requests (including their approval flags) fail with migration guidance; they never silently run without isolation. See execution controls, controller migration, and the approved removal scope. Upstream Pi, external tools and user-authored commands retain their own platform requirements. macOS/Windows live acceptance must come from their CI runners, not from simulated capability tests.
Bubblewrap command delegation, fixed sandbox verification and built-in executable controllers are unavailable. Remove their explicit configuration before using ordinary roles and file-only delegated children. Custom portable Host/controller integrations can still use the engine's authorization and effect contracts; the production host does not provide OS isolation.
- 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).
pi install ./ # from the checkout, dev install
pi list # verify: pi-conductor should appearRoles 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]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: 1Add 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.
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 promptsInside 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.
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, legacy file-tool compatibility, profiles, projections, artifacts, branch integration, and removed-backend migration.
- 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.
- Removed controller backend — migration and retained custom Host/effect interfaces.
- 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.
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.
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.
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)
MIT — see LICENSE.