Pi orchestration beyond terminal scale.
pi-fleet is local, Pi-native execution infrastructure for programs that coordinate long-lived Pi agents. It keeps one shared per-user pool reachable through a TypeScript SDK while every native Pi session remains under the user's control.
Control execution. Own the session. Build the orchestration above it.
The first split beta supports Ubuntu 22.04 or 24.04 amd64 with systemd user services. Install Pi separately first. Pi must be available as pi in the shell PATH used for setup. The installer checks Node.js, npm, Python 3, curl, OpenSSL 3, and sudo before it changes the system.
curl --proto '=https' --tlsv1.2 -fsSL \
https://raw.githubusercontent.com/elpapi42/pi-fleet/main/install.sh | bashThe installer downloads the matching tagged source bootstrap over GitHub HTTPS, then verifies the signed daemon release before package installation. It installs the matching pi-fleet-cli, enables systemd lingering if needed, and starts the per-user daemon. It never installs or changes Pi, edits shell profiles, or deletes native Pi sessions.
To inspect the script before execution:
curl --proto '=https' --tlsv1.2 -fsSL \
https://raw.githubusercontent.com/elpapi42/pi-fleet/main/install.sh \
--output pi-fleet-install.sh
less pi-fleet-install.sh
bash pi-fleet-install.shUse --version VERSION to select an exact beta. --dry-run verifies the release without changing packages, services, lingering, or CLI files. --no-setup installs the daemon package and CLI but does not enable lingering or start the service. If earlier combined pi-fleet control data exists, read the reset warning and add --accept-control-plane-reset before setup. The reset deletes pi-fleet-owned logical entries and receive history. It preserves native Pi sessions.
The old combined @elpapi42/pi-fleet package and pifleet executable use the previous beta architecture. Do not use them.
Terminal multiplexers such as tmux, cmux, and Herdr are useful for human-facing panes. Programmatic orchestration needs different primitives: stable agent addresses, immutable generation identity, ordered steering or follow-up input, process and recovery state, replayable high-level activity, and explicit uncertainty after a crash.
pi-fleet supplies those execution primitives. It does not define roles, workflows, schedules, dashboards, semantic retries, memory policy, or autonomy. Agents act only on explicit instructions unless a higher layer implements its own bounded policy.
The current architecture has three separate artifacts:
program ─────────────────────→ @elpapi42/pi-fleet-sdk → private Unix socket → pi-fleet-daemon → Pi
shell → pi-fleet-cli / pif ──→ @elpapi42/pi-fleet-sdk → private Unix socket → pi-fleet-daemon → Pi
The daemon is the sole owner of:
- agent lifecycle and immutable generation identity;
- journal-v3 SQLite state and semantic receive history;
- external Pi process trees and native-session restoration;
- crash recovery, no-replay behavior, and observation continuity;
- the fixed per-user socket at
$XDG_RUNTIME_DIR/pi-fleet/control.sock.
The SDK is a protocol-only client. It does not install, start, stop, inspect, repair, or upgrade the daemon. It does not resolve Pi, select a state root, open SQLite, or silently start a detached runtime.
The separate pi-fleet-cli package exposes only pif and uses the public SDK for all operations. It has seven agent commands and no daemon-management, private-protocol, custom-authority, timeout, or detached-runtime path.
pif list
pif create reviewer "Review the current change."
pif receive reviewer
pif send reviewer "Focus on lifecycle races."receive is a continuous semantic JSONL stream. The first record is receive.ready; subsequent records are unwrapped SDK lifecycle events. See packages/cli/README.md for the CLI contract.
Pi is a separately installed user-owned prerequisite. The daemon resolves the first executable pi from its configured service PATH immediately before Pi-process-starting work. It uses no Pi version allowlist and does not bundle, copy, substitute, or pin a setup-time Pi executable.
No allowlist does not mean every Pi version is compatible. The daemon bounds executable and version observation, checks executable identity around process start, and fails with typed errors when Pi is missing, unusable, changes during observation, or has incompatible RPC behavior during startup or operation. Passive daemon startup, status, list, receive, and safe cleanup remain available when Pi work cannot start.
The client-only package uses the package root:
import { connectPiFleet, PiFleetError } from "@elpapi42/pi-fleet-sdk";
const client = await connectPiFleet();
try {
const reviewer = await client.create({
name: "reviewer",
cwd: process.cwd(),
instructions: "Review the current change.",
});
const events = await reviewer.receive();
console.log("attached at", events.cursor);
await reviewer.send("Focus on lifecycle races.");
await reviewer.send("Then check documentation.", { delivery: "followUp" });
for await (const event of events) {
if (event.type === "assistant.message.finished") {
console.log(event.text);
}
}
} catch (error) {
if (error instanceof PiFleetError) {
console.error(error.code);
}
throw error;
} finally {
await client.close();
}connectPiFleet() performs a passive handshake with one explicitly installed and active daemon. Importing the SDK is inert. client.close() closes only that client's local requests, sockets, reconnect delays, and receive streams. It never stops the daemon, Pi, or shared agents.
See packages/sdk/README.md for the SDK contract.
client.create() and client.get() return remote Agent handles. client.list() returns summaries for discovery. A handle carries both the reusable friendly name and the immutable creation UUID, so an old handle cannot target a later same-name recreation.
agent.send() means durable acceptance and ordering. It does not mean task completion and does not correlate one send with one response.
delivery: "steer"is the default and preserves ordinary Pi prompt and steering behavior.delivery: "followUp"asks Pi to queue input behind active work.- Cancellation stops only the caller's wait. It does not cancel accepted remote work.
Exact piArgs tokens are sent unchanged and can contain secrets. The daemon retains them in private local state for restoration and deletes them transactionally after successful agent destruction. Do not place them in logs or diagnostics.
agent.receive() is a passive broadcast stream. It never wakes or restores Pi, and any number of clients may attach independently within host resource limits.
await agent.receive();
await agent.receive({ after: cursor });
await agent.receive({ fromStart: true });The stream exposes its initial opaque cursor before the first event. Each event has a stable event ID and cursor. Related lifecycle events share an activity ID. Delivery after reconnection is at least once, so consumers must checkpoint cursors and deduplicate by event ID.
The public event model contains exactly six types:
assistant.thinking.started/assistant.thinking.finishedassistant.message.started/assistant.message.finishedtool.execution.started/tool.execution.finished
There are no public text deltas, raw Pi RPC frames, turn events, retry events, or synthetic gap events. A crash can leave a started activity unmatched. pi-fleet never invents a finish.
After an unclean daemon death that can leave a Pi process alive, receive stops at its last safe cursor with observation_uncertain. Crossing to a supplied continuation cursor is an explicit caller decision. pi-fleet never bridges the gap silently.
A pi-fleet agent has a stable local address and a user-owned native Pi session. Process residency is separate from logical identity.
- Exact native session selectors and Pi passthrough tokens remain authoritative.
- pi-fleet never copies, relocates, normalizes, wraps, or deletes native session files.
- Successful destroy stops execution ownership and logically deletes pi-fleet-owned agent, operation, journal, and semantic history for that UUID.
- A minimal content-free idempotency receipt can remain after destroy.
- Deliberate concurrent native-session writers remain possible at the user's risk.
The daemon records every complete LF-terminated Pi RPC stdout record byte-for-byte before parsing it or exposing derived receive events. This private journal can contain prompts, thinking, tool input and output, paths, extension data, and secrets. It remains for the logical agent's lifetime.
Successful destroy performs logical SQLite deletion. pi-fleet does not claim immediate file shrinkage or forensic erasure from WAL files, freelist pages, backups, snapshots, or physical media.
Storage failure is fail-closed. Affected work stops, receive ends at the last durable cursor, committed history remains, and pi-fleet never crosses an unrecorded gap. Diagnostics expose content-free state such as counts, sizes, health, and UUIDs rather than retained payloads.
Treat delivery_uncertain, compaction_uncertain, runtime_interrupted, incarnation_cleanup_uncertain, observation_uncertain, and session failures conservatively. Semantic retry policy belongs to the orchestrator.
The standalone daemon exposes two management commands:
pi-fleet-daemon setup
pi-fleet-daemon deactivate
setup configures one systemd user service after validating supported platform requirements, lingering, service authority, and the explicit service PATH. deactivate proves daemon and Pi-process absence and removes supervision while preserving committed pi-fleet state and native Pi sessions.
Daemon installation, authenticated offline replacement, publication, and coordinated release instructions are not final user guidance yet. The accepted architecture is in DAEMON_PROPOSAL.md and DISTRIBUTION_PROPOSAL.md.
The first split release targets Ubuntu 22.04 LTS amd64 and Ubuntu 24.04 LTS amd64 with systemd user services. SDK and CLI consumers require Node.js ^22.19.0 || ^24.0.0.
The current source does not claim support for Debian, other Linux distributions, arm64, macOS, Windows, physical-host logout or reboot recovery, or every future Pi RPC contract. These targets require separate release evidence.
npm ci
npm run audit:production
npm run typecheck
npm run lint
npm run format:check
npm test
npm run build
npm run test:cli
npm run test:cli-packed
npm run check:cli-package
npm run check:sdk-package
npm run test:sdk-packed
npm run release:checkDo not run behavioral tests against the user's persistent daemon, state, or sessions without explicit authorization and isolation.
MIT © elpapi42