Developer path
Build and operate workflows
This page describes the package contract from the host tool boundary through the sandboxed workflow worker and its durable run store.
Installation and trust
Use Node.js 22.19 or newer. Every package is trusted Pi host code with the same filesystem and process access as Pi. Install only packages you trust, and trust the project before running workflows or piewf doctor.
pi install npm:pi-extensible-workflows
npm install -g @piewf/cli
The core package provides workflow orchestration, the reviewLoop starter, and standalone subagent tools. Add @piewf/cli only when you need the piewf command; the optional @piewf/pi-ext-roles package owns pi-role and its role APIs. Workflow and subagents create their own SDK sessions, preserve provider loading and recovery, and transport extensionSettings themselves via the existing consumer session/start/hook seam (session_start event.settings). Add @piewf/herdr alongside core for Herdr session actions and transport integration.
For source installation and local development, clone the repository and change into its directory first:
git clone https://github.com/vekexasia/pi-extensible-workflows.git
cd pi-extensible-workflows
npm ci
npm run check
pi install "$PWD"
A source install for development uses the repository root, which loads the TypeScript sources with Pi's own packages. pi install "$PWD/packages/core" loads the built dist files, which resolve the checkout's own copy of Pi's packages (a development dependency) instead of the copy Pi provides.
Core and CLI install and operate without @piewf/pi-ext-roles. Roles are an optional extension. See role migration and release gating.
For a one-session trial without changing Pi settings:
pi --no-extensions --extension "$PWD/packages/core/src/index.ts"
Workspace manifests and package-lock.json are authoritative for Pi dependency versions. Core and Herdr declare Pi runtime peers; the standalone CLI installs its runtime dependencies. This npm-workspaces monorepo publishes pi-extensible-workflows from packages/core, @piewf/cli from packages/cli, and @piewf/herdr from packages/extensions/herdr. Standalone subagent tools and their tests live in packages/core/subagents; there is no separate subagents workspace.
Updates and bundled changelog
The extension shows no release notes itself. Pi versions with earendil-works/pi#8790 read the package-local CHANGELOG.md of an npm or git install and, on the next fresh interactive launch after a version bump, show the entries newer than the last seen version under "Extension Updates". The first launch that sees the package only records its version. A source checkout installed directly from packages/core normally has no package-local changelog because that file is generated only during packing, so Pi shows nothing for it; a git install of the repository root uses the root CHANGELOG.md.
Global and project settings
Core reads only the workflow settings files below. Shared role settings and role catalogs belong to the optional roles extension.
The optional global settings file is <agentDir>/pi-extensible-workflows/settings.json, normally ~/.pi/agent/pi-extensible-workflows/settings.json. Trusted projects may also use <cwd>/.pi/pi-extensible-workflows/settings.json. If PI_CODING_AGENT_DIR is set, it replaces ~/.pi/agent. Missing files use defaults. Both files use a strict schema: unknown keys, malformed JSON, and invalid values stop launch or resume with the source settings path in the error. The backgroundWidget and codemodeTools settings are global-only; project settings may configure the other project-overridable fields.
{
"concurrency": 8,
"backgroundWidget": true,
"codemodeTools": "all",
"modelAliases": {
"reviewer-model": "anthropic/claude-fable-5:high",
"developer-model": "openai-codex/gpt-5.6-luna:xhigh",
"cheap-model": "developer-model:low"
},
"skills": ["*", "!experimental-*"],
"extensions": ["**/*", "!**/unsafe.mjs"],
"tools": ["*", "!write"]
}
concurrency defaults to 8 and accepts integers from 1 through 16. Effective settings use defaults < global settings < trusted project settings < per-run options; a workflow call's concurrency field is highest precedence. Agents queue in the order the workflow calls agent(...): when a script issues several calls concurrently (for example inside parallel(...)), the limit admits them in call order rather than in the order their replay lookups happen to finish.
The strict top-level settings keys are concurrency, backgroundWidget, codemodeTools, modelAliases, skills, extensions, extensionSettings, tools, retention, and worktreeSetup; unknown keys are rejected. worktreeSetup is an array of shell commands that runs in each new worktree (see Worktree setup); a trusted project list replaces the global one. retention takes positive integer olderThanDays and maxTerminalRuns limits and is applied at session start to hard-terminal runs only. The direct extensions array contains selectors. extensionSettings is an arbitrary map of safe extension-owned JSON values; the core validates only its structure and built-in settings. A trusted project extensionSettings field replaces the complete global map, including with {}. Registered validators receive the complete effective map plus source context. The obsolete extensions.herdr shape and legacy disabledAgentResources field are rejected.
Extension selectors from settings are normalized before matching: ~ and ~/... expand from the home directory, file:// URLs are converted to filesystem paths, and relative paths resolve from the directory containing that settings file. Existing non-magic paths and the static prefixes of magic paths in settings are canonicalized with realpath when possible; *, **, and **/... remain cwd-independent patterns. At call level, those three forms also remain cwd-independent. Other relative selectors resolve from the agent's launch cwd: non-magic selectors are canonicalized, while magic patterns are path-resolved without realpath. Call-level ~ and file:// strings are not expanded by the runtime. Pi's built-in codemode, tool-search, and mcp extensions are matched by their names builtin:codemode, builtin:tool-search, and builtin:mcp, which are never resolved as paths; a list that starts with !* needs builtin:* or the individual names to keep them. Pi does not expose its built-in llama.cpp extension to extension sessions, so agents cannot use models it provides; declare the server as a provider in models.json instead.
Background workflow widget
backgroundWidget is a global boolean that defaults to true. Set it to false to disable the live background-run tree and the transcript receipts workflow runs leave (background subagents' receipts are written regardless). Foreground workflow rendering is unchanged. The setting is accepted only in the global workflow settings file; it is not a trusted-project override. Non-interactive and headless hosts do not draw the widget or append receipts.
The widget shows one row per run with its phase and progress, and the run's phases and agents below it, nested agents indented under their parent. When the tree does not fit, rows are kept by priority, each with the rows it hangs from, and drawn in tree order: run headers and the Subagents row first, then checkpoints waiting on a person and quiet or stalled agents, then failed agents and running work (an agent counts as running only while its run is running, pausing or awaiting input), then finished detail. What does not fit collapses into one last row, … 3 more; a run's header whose checkpoint row did not fit shows ◆ waiting. When the tree is taller than the widget, its hint reads Alt+O to scroll. The widget takes Alt+O ahead of editors, also under editors that keep it, only while that hint shows: a narrow title row cuts its counts to leave room for it. When the tree fits, there is no hint and the key passes through untouched. It is not a Pi shortcut and cannot be rebound. In scroll mode a cursor row is drawn on the selection background: ↑/↓ move it, enter opens the /agents workspace on that row when it can open, and esc, q, and Alt+O leave; k/j work as the arrows. Any other key leaves scroll mode and goes on to the editor. Background subagents show in the same widget as a "Subagents" group after the runs, one row per running background subagent with the same cursor; enter on any row (a run, a phase, an agent, the Subagents group, or a subagent) opens /agents on it, also in a session where nothing else has opened it yet. While any agents workspace (/agents, /workflow, or /subagents) or the dialog of one of its actions is up, the widget draws nothing and rescans nothing; it returns when they close. Live background subagent rows show only in this widget; the subagents extension draws no frame of its own. With the workflow extension not loaded or backgroundWidget: false, no live subagent rows show, only the receipt each background subagent leaves in the transcript when it settles.
codemodeTools is a global setting that decides which workflow and subagent tools Pi codemode scripts may call. "all", the default, allows every tool. "read-only" allows only workflow_status, workflow_catalog, and subagents_inspect. "none" allows none of them. The model still calls every tool directly. Tools are registered when Pi loads the extension, so a change applies after /reload, and trusted project settings cannot set it.
Workflow and subagent tools declare an outputSchema, so codemode scripts receive their results as objects instead of JSON text; a completed foreground workflow, workflow_retry, or workflow_resume call resolves with the full value. The child agent tools agent and get_subagent_result do the same inside workflow agents. workflow_status, workflow_catalog, and subagents_inspect carry readOnlyHint, so permission extensions that read tool annotations can skip confirming them.
Model aliases
Alias names are case-sensitive and must match [A-Za-z][A-Za-z0-9_-]*. Targets may be concrete provider/model:thinking references or other aliases. Unknown targets and cycles are rejected.
Aliases work anywhere the workflow runtime accepts a model, including role files from the optional roles extension. An exact configured alias wins over a native bare model name. A thinking suffix on an alias reference overrides the resolved target's suffix, so cheap-model:low can reuse a model while changing its thinking level. Concrete call-level models must include :thinking. Resolution never falls back: an unavailable target fails with UNKNOWN_MODEL and identifies the alias, resolved target when known, and settings path.
Each launch and resume snapshots one effective alias mapping for that execution segment. A trusted project's modelAliases replaces the global map, including {} to clear it; omitted project fields inherit global settings. Completed and replayed calls retain their results; prepared identities retain their concrete models on retry and resume. Calls not yet prepared and new identities use mappings reloaded on resume. Mapping changes produce one persisted warning in run events and the inspector.
workflow_catalog lists effective alias names, kinds, and provenance without probing availability or injecting them into guidance; concrete static and resolved targets remain available through settings, doctor, snapshots, and the inspector. /workflow can list, add, edit, and delete aliases in the active effective settings source. It atomically updates only modelAliases, preserves other settings, and affects new launches and resumes without /reload.
Workflow-agent resource selectors
Trusted project settings are appended after global selector arrays, followed by generic preparation and per-agent call options. The optional roles plugin composes role selectors during preparation; all layers remain inside the root and parent ceilings. Each selector is a Minimatch-compatible pattern evaluated in declaration order; every discovered candidate starts enabled, a positive match enables it and !pattern disables it, with the last matching selector winning. Use !* followed by a positive pattern to clear the current selection before adding a narrower set. An empty selector array contributes no matches as a selector layer. Prepend !* to a positive list when it must restrict the candidate set, or use !* alone to select none.
{
"skills": ["*", "!experimental-*"],
"extensions": ["**/*", "!**/unsafe.mjs"],
"tools": ["*", "!write"]
}
Skills match discovered names, extensions match discovered normalized paths, and tools are limited to the current root or parent candidate set. Patterns cannot create unavailable capabilities. The legacy disabledAgentResources field is rejected.
piewf doctor reports an error when active workflow settings still use disabledAgentResources, including empty values, because the runtime rejects that configuration. The diagnostic explains the selector polarity change and links to issue #205. It also warns when a positive-only tools selector does not form an allow-list; prepend !* when restriction is intended. Untrusted project settings remain inactive and are not reported.
Tool API
The extension registers these core tools and one command:
workflowruns a named inline or file-backed deterministic JavaScript workflow.workflow_statusreads a compact run summary for any session under the current project cwd.workflow_resumeresumes a budget-exhausted run with an optional budget patch and preserves its foreground/background launch mode unless overridden.workflow_retrycreates a linked child for an explicitly failed run, replays its completed journal operations, and preserves its launch mode unless overridden.workflow_respondapproves or rejects a pending checkpoint or budget decision.workflow_stopstops an active workflow run by ID.workflow_cataloglists registered functions and model aliases; it is registered only when the active registry has one of these entries./workflowlists and controls runs in the current Pi session.
workflow parameters
| Field | Type | Contract |
|---|---|---|
name | string | Required and non-empty for every launch. |
description | string | Optional human-readable metadata. |
script | string | Immutable workflow source. Exactly one of script or scriptPath is required. |
scriptPath | string | Path to a JavaScript workflow file, resolved from the launch project directory and read once at launch. Its exact contents become the immutable persisted source. Exactly one of script or scriptPath is required. |
args | JSON value | Available inside an inline or file-backed script as args. Defaults to null. |
foreground | boolean | Wait for the final value instead of returning a run ID; the model-visible result also includes the completed run ID. |
concurrency | integer 1-16 | Per-run active-agent limit. |
budget | object | Optional aggregate soft/hard limits for tokens, costUsd, durationMs, and agentLaunches. |
parentRunId | string | Optional terminal run in this project and Pi session for a new launch. Matching explicit named withWorktree scopes reuse its existing checkout; other scopes remain new worktrees. It never replays or resumes a run. |
workflow_respond parameters
{
"runId": "...",
"proposalId": "...",
"approved": true
}
Use name instead of proposalId for checkpoint decisions. A budget relaxation proposal remains pending until this tool approves or rejects its exact proposal ID.
workflow_stop parameters
{ "runId": "..." }
Stopping cancels the workflow, descendant agents, and scheduler work before persisting state: "stopped". The result reports stopped: true on success, unknown_run for an unavailable or differently owned run, and already_terminal for a completed, failed, or stopped run.
workflow_status parameters
{ "runId": "..." }
workflow_status is read-only: it searches all persisted sessions under the current project cwd without cleaning run directories. It returns RUN_NOT_FOUND for missing, corrupt, or foreign run IDs. The JSON summary includes run identity, state, failure metadata, phase, delivery mode/state, configured budget and usage, and agent shims containing only IDs, labels, paths, state, last activity, and accounting stats; it does not expose transcripts or session payloads.
workflow_resume parameters
{ "runId": "...", "expectedState": "budget_exhausted", "budget": { "tokens": { "hard": 120000 } }, "foreground": true }
workflow_retry accepts { runId: string, expectedState?: string, foreground?: boolean }. After workflow_status({ runId }), pass its returned state as expectedState; recovery rejects the call if the persisted source state changed. The source must be a persisted failed run in this project and Pi session. Recovery inherits the source snapshot's launch mode by default; set foreground to explicitly override it. Foreground recovery waits for the terminal value and run details inline; background recovery returns immediately and delivers completion or failure as a follow-up. Failure diagnostics expose that exact source ID, replayable and incomplete paths, artifact locations, and validated named worktrees for direct use with the advertised action. It returns a fresh child ID linked by parentRunId; completed agent, shell, registered-function, and checkpoint operations replay from the parent lineage, while incomplete operations execute normally. Parent usage and budget version carry forward, and concurrent retry children sharing a lineage are rejected. A budget_exhausted source must use workflow_resume({ runId, expectedState, budget? }); completed, stopped, and interrupted sources return state-specific recovery guidance. Replay does not repeat journaled side effects, but external effects performed before failure are not guaranteed exactly once.
{ "runId": "failed-run-id", "expectedState": "failed" }
Background workflow and recovery calls return a run ID immediately; completion or failure is delivered as one follow-up. Foreground workflow and recovery calls return the terminal workflow value and run details inline. Recovery control results retain their state and run identifiers alongside the inline value or descriptor. Completion values are inlined only when the persisted result is within Pi's 50 KiB tool-output limit and the current main-model context has room, including response headroom; otherwise successful completion delivers a descriptor with state, runId, resultPath, resultBytes, and inlined: false. The mode is persisted in snapshot.json, so budget resume, retry, and interrupted-run resume keep the original mode unless the recovery caller supplies an explicit override. Current snapshots without launch-mode metadata default to background recovery.
Aggregate run budgets
A launch may set run-wide soft and hard limits. Every dimension and threshold is optional; omitted values are unlimited.
{
budget: {
tokens: { soft: 100000, hard: 120000 },
costUsd: { soft: 5, hard: 6 },
durationMs: { soft: 900000, hard: 1200000 },
agentLaunches: { soft: 8, hard: 10 },
},
}
Token, duration, and launch limits are non-negative integers; cost limits are finite non-negative numbers. Zero is valid, and soft must be lower than hard when both are set. The former maxAgentLaunches setting and launch field have been removed; use budget.agentLaunches.
Usage covers all workflow-agent attempts, retries, and descendants but excludes the main orchestrator. Tokens count reported input plus output and exclude cache reads and writes. Accounting uses Pi's cumulative session statistics, so compaction cannot remove earlier token or cost usage from progress, budgets, or persisted attempt totals. Replayed calls retain historical usage without counting as new launches.
A soft crossing is recorded once per budget version. If more model work remains, active agents receive one wrap-up instruction before their next turn and later agents are annotated; a final crossing response completes without an injected message.
Hard launch limits are checked immediately before dispatch. Token, cost, and duration limits are checked between model turns. In-flight turns and tools may finish: a final overshooting response is accepted, while a response requiring tools or another turn stops with BUDGET_EXHAUSTED before that work begins. If accepted results let the workflow return without more model work, the run completes with an overrun event; otherwise it enters the resumable budget_exhausted state.
Duration includes active workflow JavaScript, agent work, and tool calls, but excludes pauses, checkpoint waits, and offline time. Historical usage remains charged after resume.
workflow_resume accepts the exact run ID and an optional budget patch. Omitted patch values stay unchanged and explicit null removes a limit. Tightening applies immediately; raising or removing a limit creates a persisted human-approval proposal. Every exhausted hard dimension must be raised above retained usage or removed. Answer the exact proposal ID with workflow_respond. This control is distinct from workflow_retry, which accepts only failed runs, and from per-agent retries.
/workflow and the inspector show limits, usage, percentages, budget versions, crossings, and overruns. Compact TUI surfaces omit budget rows entirely when a run has no effective limit, including after a patch removes every limit. Required exhaustion, adjustment, and approval context remains visible.
Workflow DSL
Workflow source is plain sandboxed JavaScript. It has no imports, filesystem, network, timer, or dynamic-code globals, and no direct access to the host process. The trusted host-mediated shell() primitive is the explicit exception for command execution:
| Primitive | Use |
|---|---|
agent(prompt, options) | Run one Pi agent and return its bare text or JSON value. |
agent.create(options) | Create a persistent agent handle with a stable explicit name and the config frozen for every turn. |
handle.send(prompt, options) | Run the next turn of a handle in the scope that created it, continuing its transcript. Options are outputSchema and timeoutMs. |
shell(command, options) | Run a platform-shell command and return its exit code, stdout, and stderr. |
tools.<name>(args) | Call a tool of the launching Pi session, as Pi codemode scripts do. |
prompt(template, values) | Safely interpolate JSON-compatible values into a prompt. |
parallel(name, tasks) | Run keyed independent tasks concurrently. |
pipeline(name, items, stages) | Apply ordered stages to keyed items. |
phase(name) | Record progress telemetry. In a sequential loop, use phase(slice.name) for semantic labels such as backend-safety. |
log(message) | Persists each bounded message with a timestamp in the run events. Attached foreground logs render in the live workflow item; background or detached logs append as bounded TUI transcript entries and are shown in Trajectory, capped at 4 KB. |
checkpoint(input) | Wait for an approval or rejection. |
evidence(content) | Persist large string or JSON values to run-scoped content-addressable storage (evidence/<sha256>.txt) and return a compact { ref, sha256, path, bytes } record to protect prompt context budgets. Read it in the script with tools.read({path: record.path}); give agents the path and a read tool. Saving identical content rejects a modified existing artifact. Resumed and retried runs get back the record first stored for that content, so a journaled read of its path replays. |
withWorktree(name, callback) | Materialize one explicitly named worktree scope before invoking the callback; pass the callback a frozen { path, branch } reference. |
shell(command, options) inherits the workflow cwd, or the active named withWorktree(name, callback) cwd. Options are timeoutMs and string-valued environment overrides merged over the host environment. A nonzero exit is a result, not a workflow failure; launch failures and timeouts use SHELL_FAILED. Output is not shell-truncated, but a result that exceeds the 10 MB RPC boundary fails with RPC_LIMIT_EXCEEDED.
Session tools in scripts and registered functions
await tools.<name>(args) in scripts and await ctx.tools.<name>(args) in registered functions share one bridge, without starting an agent. Each call takes one arguments object. The bridge calls a tool of the Pi session that launched the run through Pi's ctx.executeTool(), so argument validation, tool_call and tool_result handlers, and permission checks apply as for model calls. Names use the same identifiers as codemode. A tool with an outputSchema resolves to its structured content, also when it reports an error with one, any other tool to its text; other failed or blocked calls reject with TOOL_FAILED. Calls are identified by structural scope, tool, arguments, and occurrence, so concurrent calls replay correctly; registered-function scopes also include the invocation path, separating repeated and nested invoke() calls from one another and from script calls. Probing with "name" in tools or "name" in ctx.tools throws, because the answer would depend on the session that resumes the run. Workflow and subagent tools are not callable, and calls inside withWorktree fail because session tools resolve paths against the session cwd. A launch whose script names a tool the session cannot call fails with UNKNOWN_TOOL before any agent runs. Registered functions check tool names at call time instead: their implementations are outside the script source, and may choose names dynamically or compose nested functions. No tool declaration is required on defineWorkflowFunction; an unjournaled call to an unavailable tool rejects with UNKNOWN_TOOL. Results are journaled, so resume and retry replay them even inside an incomplete function and even if the recovering session no longer has that tool. New calls need a Pi tool call: runs launched or recovered by the workflow, workflow_resume, or workflow_retry tools have one, while runs resumed from /workflow or at session start only replay journaled calls. Calls from a background run still execute, but Pi no longer records them on the launching tool result. The live workflow item shows calls in flight. The run state records each executed tool or shell() call with its name, path, phase, timing, and status; Trajectory lists them under CALLS and on a status-coloured script Gantt lane. For safety, tool arguments and results are not recorded in call state or shown in Trajectory, including live state and HTML exports/shares; tool errors record only the code, not the message. Tool results remain in the journal for replay. Shell calls retain their inputs, results, and error messages; inputs and errors over 16 KB are kept as prefixes, and environment overrides record names but not values. Call metadata writes are best-effort and do not change tool outcomes. A journaled result proves completion even without finish timing; its duration remains unknown. Replayed calls are not recorded again, and the number of recorded calls is not capped.
return await withWorktree("fix-tests", async () => {
for (let attempt = 1; attempt <= 5; attempt += 1) {
const tests = await shell("yarn test");
if (tests.exitCode === 0) return tests;
await agent("Fix the test failures:\n\n" + (tests.stderr || tests.stdout));
}
return shell("yarn test");
});
Completed shell results are journaled only after process exit and RPC validation, so cold resume replays them without rerunning the command. A host crash after a command performs side effects but before its result is journaled can run it again; use shell() primarily for verification and bounded command gates, not exactly-once mutations.
Agent options
Use label as an optional non-empty display name. Without it, a preparation hook may supply a label; otherwise the effective model name is used. Use model as a configured model alias or provider/model:thinking. Other core options are skills, extensions, tools, excludeTools (exact tool names removed after preparation hooks, as in Pi's excludeTools; patterns and workflow_result, which the agent needs to return, are rejected, the names also remove custom tools of those names, including ones setup hooks add, for the agent and its descendants; names matching no root or custom tool only warn, and setup hooks or children cannot restore an excluded tool; the names are frozen with the agent's configuration, so a later handle turn, retry or resume keeps them even if the re-evaluated option changes), contextFiles, outputSchema, retries, and timeoutMs, with withWorktree(name, callback) for worktree scopes. Unknown JSON options are forwarded to loaded extensions and otherwise ignored. For role, explicitly enable the optional roles plugin.
const report = await agent("Review the implementation", {
label: "Implementation review",
model: "openai-codex/gpt-5.6-sol:high",
tools: ["!*", "read", "grep"],
});
Results are bare values. Every agent submits its final result through workflow_result; unstructured agents submit { result: "..." } and return the contained string, while outputSchema agents submit a schema-valid value. Prose is not parsed as a result. The user message of every agent(...) call and handle.send(...) turn is exactly the text you supply: the host adds no workflow, agent, phase, or parent header and no task wrapper. The once-per-send workflow_result contract lives in that tool's description, rebuilt for each turn with that turn's schema, and the child-ownership contract lives in the agent tool description, which only agents allowed to spawn children receive. Transient retry state and soft-budget instructions are not part of the task text or the system prompt, which stays stable for prompt caching. They are delivered as separate control text right after the task: a steered message on local sessions, or appended to the pane launch prompt by a transport that sets turnControlDelivered on its session (Herdr). The serialized result and every worker RPC value must fit within 10 MB.
Composition
const reports = await parallel("review", {
api: () => agent("Inspect the API"),
ui: () => agent("Inspect the UI"),
});
return agent(prompt("Combine these reports:\n{reports}", { reports }));
Calls from one source location must not race outside parallel or pipeline. Their structural keys make replay deterministic. Ordinary branch failures wait for sibling work to settle; cancellation is immediate.
Extensions
Trusted Pi extensions can register reusable workflow functions and per-agent setup hooks. The complete field-level authoring contract, validation rules, replay semantics, and copy-pasteable examples now live on the dedicated Extensions page.
Use the Roles page for the optional roles integration. Role-file format and authoring live in the @piewf/pi-ext-roles guide.
Prioritized agent setup hooks
Security: setup hooks are trusted extension code. They run after generic preparation and concrete model, tool, context, cwd, and capability validation, but before the selected AgentTransport creates a session. Hooks may replace or wrap the transport, but the immutable prepared launch remains the capability ceiling; never register untrusted code as a setup hook.
TypeScript: hook callbacks are contextually typed. agent.options is the workflow-owned AgentOptions object, including its typed core fields and JSON-compatible extension options. agent.sessionInput is the workflow-owned pre-session input with typed cwd, model, tools, and system-prompt fields; its customTools and extensionFactories fields use Pi's ToolDefinition and InlineExtension types. agent.transport receives an immutable PreparedAgentSession and an AgentTransportContext.
Register named hooks with an optional finite priority. Hooks run in ascending priority order (default 10); equal priorities use the stable hook name as the tie-breaker. Each retry starts from a fresh baseline and runs every hook again. Hooks receive the workflow cancellation signal, and their asynchronous runtime is not charged against timeoutMs. A thrown hook error fails the agent immediately, prevents later hooks and session creation, and is not retried as a native-session failure.
registerWorkflowExtension({
version: "1.0.0",
headline: "Advisor setup",
agentSetupHooks: {
advisor: {
priority: 10,
async setup(agent, context) {
if (agent.options.advisor !== true) return;
agent.sessionInput.extensionFactories ??= [];
agent.sessionInput.extensionFactories.push(scopedAdvisorFactory);
if (context.signal.aborted) return;
},
},
},
});
Unknown JSON-compatible agent options are preserved for setup hooks and the selected transport, but are not inherited by child agents. Persisted attempts use transport-neutral references in agentSessions; they do not require a transcript path. The workflow session contract requires state, synchronous stats, progress subscriptions, prompt/steer/abort, and awaited idempotent disposal. Cold recovery replays the journal and never reconnects a persisted session reference.
Extensions can also register agentAttemptActions for /workflow and, when both visibleStandalone and runStandalone are supplied, for standalone /subagents runs. Workflow actions receive workflow snapshots; standalone actions receive a standalone agent snapshot and the real attempt/session/setup/handoff data without a fabricated workflow record. Action errors are reported and the relevant navigator is refreshed after completion. In the TUI agents workspace they run detached and their dialogs are refused (see the agents workspace).
Lifecycle, checkpoints, and worktrees
Run states are queued, running, pausing, paused, awaiting_input, budget_exhausted, completed, failed, stopped, and interrupted. budget_exhausted is terminal for the current execution segment but can be resumed with an acceptable budget. A run stays in pausing until its active operations finish; resuming it there (/workflow "Cancel pause", Trajectory, or resume) cancels the pending pause and keeps it running. Agent states include queued, running, waiting_for_child, paused, retrying, completed, failed, and cancelled.
Direct consumers of createLocalPiSession() receive an extension-bound session. Always await session.dispose(); disposal emits session_shutdown before releasing the native Pi session and is idempotent. Handoff code uses the same lifecycle path with a resume shutdown and a matching session_start carrying the previous session file.
Extension load failures are CLI warnings unless the workflow runtime itself cannot start, or a workflow registration cannot be proven to come from a loaded extension (see below); then the CLI stops before running anything. A workflow that calls a function of a failed extension fails with its own error. Agent options that no preparation hook of a loaded extension declares in its optionsSchema, with that extension's entry as registration source, then fail with INVALID_METADATA instead of being ignored, because a failed extension may have owned them (see the extensions guide).
The headless CLI keeps the parent session and its inter-extension event bus alive for the whole operation. On success or failure, it awaits session_shutdown and disposes the session before clearing the bus, so shutdown handlers can still communicate through pi.events.
Use a checkpoint when an agent must wait for a human or parent decision:
const decision = await checkpoint({
name: "ship",
prompt: "Ship this build?",
context: { commit: args.commit },
});
if (decision !== "approved") return { approved: false };
Checkpoint prompts are limited to 1 KB and serialized context to 4 KB. Sandboxed workflow scripts receive "approved" or "rejected" from direct checkpoint(); registered functions receive a boolean from WorkflowOrchestrationContext.checkpoint. Responses are journaled and replay after cold recovery.
Use withWorktree(name, callback) for canonical worktree scopes: put collaborating top-level agents in one named callback, or put each independent parallel branch in its own named callback. The runtime creates and resolves each deterministic owned branch and worktree before invoking the callback; the callback receives a frozen public reference containing only path and branch. The caller branch is unchanged, children and retries reuse the enclosing scope, and recovery preserves the existing ownership and cleanup behavior. A later run can pass the previous terminal run's ID as parentRunId to reuse matching named scopes without copying, merging, or creating another branch. Borrowed bindings are validated on cold resume and remain invalid if their originating run is deleted.
Worktree setup. A new worktree starts from the clean launch HEAD only. To bring in machine-local context that Git ignores (for example .env files or private skills), set worktreeSetup in the global or trusted project workflow settings. Its commands run in order through the shell, with the new worktree root as working directory, after checkout and before the worktree is recorded. They receive PIEWF_WORKTREE_PATH, PIEWF_SOURCE_ROOT (the launch repository root), and PIEWF_WORKTREE_BRANCH. A command that exits non-zero or runs longer than 10 minutes fails the operation with WORKTREE_FAILED, naming the command and the tail of its output, and the worktree and branch are removed. Setup runs only when a worktree is physically created: existing, borrowed, and inherited worktrees never run it again. Workflow runs use the value captured at launch; standalone subagents read the current settings. With worktrunk, { "worktreeSetup": ["wt step copy-ignored --require-include"] } copies the Git-ignored paths listed in the repository's .worktreeinclude.
const results = await parallel("issues", {
issue81: () =>
withWorktree("issue-81", async ({ path, branch }) => {
const devResult = await developUntilApproved({
task: "Resolve issue #81",
});
const worktree = { path, branch };
return { devResult, worktree };
}),
});
return agent(prompt("Merge these verified worktrees: {results}", { results }));
Agent grouping and drilldown
The live workflow card and /workflow group agents by their structural workflow path and include registered-function context as a breadcrumb. Repeated calls in one structural scope keep the first function name unsuffixed and label later invocations #2, #3, and so on; nested calls compose each occurrence-aware segment. Groups remain stable as later agents start, creation order is preserved inside each group, and actual parent-child agents remain nested. Use phase(slice.name) in sequential loops when semantic phase labels are more useful than function occurrence labels.
In Pi's TUI, /workflow shows each run's phase-first tree under its header row in the /agents workspace, with that workspace's keys and action menu; outside the TUI, the picker opens a run's dashboard and its Agents... entry lists the run's agents for their actions. The run script, agent prompts, effective system prompts, and completed top-level agent results can be opened in the configured external editor from the TUI; prompts are available while agents are running or after they settle. Each opens a temporary copy with .js, .json, .md, or (for a worktree diff) .diff content types; edits to the copy are not kept. Phase and operation nodes have no artifact action of their own; their menu offers the run's actions. Agent attempt actions registered by extensions are evaluated for the latest attempt only and run with a narrow action context; built-in actions copy the agent ID and, when it owns a worktree, that branch or worktree path and, in the TUI, open its diff. Worktree ownership IDs and per-worktree copy actions are not exposed in the top-level run actions.
Run storage and executed source
Runs are stored under ~/.pi/workflows/projects/<cwd-slug>-<cwd-hash>/sessions/<session-id>/runs/<run-id>/. Identity checks use the resolved launch cwd and Pi session ID.
Every new run contains workflow.js with the exact UTF-8 JavaScript source that was preflighted and executed. It is an immutable inspection artifact written during atomic run creation and is opened in the external editor on a temporary copy whose edits are not kept. Completed workflow results are stored as readable pretty UTF-8 JSON in result.json; its exact byte size is reported when a completion descriptor is delivered. snapshot.json remains authoritative for execution and resume; runs from previous identity contracts cannot be resumed.
Launch snapshots require identity version 6 and persist generic agentConfigurations by logical operation or handle identity before dispatch. Older snapshots fail with RESUME_INCOMPATIBLE; relaunch instead of migrating them. Preparation is frozen for each logical agent, including model, plugin instructions, selectors, own and inherited excludeTools, context scopes, and settings. A stored configuration without excludeTools was prepared before exclusions were frozen, so its exclusions are unknown and it fails resume and retry with RESUME_INCOMPATIBLE; relaunch instead. Resume and retry revalidate trust and capabilities without rerunning preparation. New identities resolve current configuration. Extension code, skill/AGENTS/SYSTEM/APPEND bytes, and external state are not frozen and are reread under current trust and ceilings. Launch-mode changes use the same atomic mutation lane as prepared configurations. This is not an exactly-once guarantee for external effects.
Each run's private system-prompts.json records the full effective system prompt at every native Pi agent_start, after before_agent_start extensions have run. Entries include the native session ID, attempt, turn, SHA-256 digest, and prompt. New artifacts use append-only records and content-addressed prompt bodies under .system-prompts; version-1 artifacts remain readable. Native Pi JSONL transcripts intentionally omit system prompts.
Run directories use mode 0700, prompt artifacts use 0600, and prompt storage directories use 0700. Treat them as sensitive because they can contain role bodies, project instructions, tool guidance, and extension modifications. Confirmed run deletion removes them; native Pi transcripts remain in Pi session storage.
Workflow lifecycle events
The package exports WORKFLOW_RUN_STARTED_EVENT, WORKFLOW_RUN_RESUMED_EVENT, WORKFLOW_RUN_STATE_CHANGED_EVENT, WORKFLOW_RUN_COMPLETED_EVENT, WORKFLOW_RUN_FAILED_EVENT, WORKFLOW_AGENT_STATE_CHANGED_EVENT, WORKFLOW_PHASE_CHANGED_EVENT, WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENT, WORKFLOW_BUDGET_EVENT, and WORKFLOW_WORKTREE_CREATED_EVENT, plus matching TypeScript payload types such as WorkflowEventBase and WorkflowRunCompletedEvent.
Subscribe through pi.events. Events are in-process, best-effort, emitted after the corresponding run artifact or state persistence, and ordered within each run. Payloads contain correlation and safe inspection metadata only: prompts, responses, results, tool data, environment values, and workflow source are never included. Manual stop, interruption, pause, awaiting input, and budget exhaustion are state changes, not run completion or failure.
| Event | Fires when |
|---|---|
WORKFLOW_RUN_STARTED_EVENT | After a new run is created and execution begins. |
WORKFLOW_RUN_RESUMED_EVENT | After a run transitions from paused, interrupted, or budget_exhausted to running. |
WORKFLOW_RUN_STATE_CHANGED_EVENT | After every persisted run-state transition. |
WORKFLOW_RUN_COMPLETED_EVENT | After the result is saved and the run enters completed. |
WORKFLOW_RUN_FAILED_EVENT | After a run enters failed; it does not fire for stopped, interrupted, or budget_exhausted. |
WORKFLOW_AGENT_STATE_CHANGED_EVENT | After persisted agent state or attempt-count data changes. |
WORKFLOW_PHASE_CHANGED_EVENT | After the persisted phase changes to a different value. |
WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENT | After a checkpoint is persisted as awaiting, approved, or rejected. |
WORKFLOW_BUDGET_EVENT | After each newly persisted budget event is recorded. |
WORKFLOW_WORKTREE_CREATED_EVENT | Once after a new worktree is persisted for an owner. |
pi.events.on(WORKFLOW_RUN_COMPLETED_EVENT, (event: WorkflowRunCompletedEvent) =>
console.log(event.runId, event.resultPath),
);
Operations and debugging
Doctor and inspector
piewf inspect [session-id] --json includes an optional result for completed runs: { status: "available", value: ... } when result.json is valid and at most 64 KB including formatting, or { status: "truncated", preview: "...", previewBytes: 4096, totalBytes: ... } for larger files. The UTF-8-safe preview is at most 4096 bytes, is not validated, and may be incomplete JSON; read artifacts.resultPath for the full value. Missing files omit the field; unreadable, non-regular, symlinked, or malformed files report status: "unavailable" without hiding the run. Results are not redacted. --summary adds only result=available, result=truncated, or result=unavailable. Business outcomes such as { pass: false } remain returned data, not execution failures: the run stays completed, and --failed still selects only execution failures.
npx -p @piewf/cli piewf --version
npx -p @piewf/cli piewf --help
npx -p @piewf/cli piewf doctor [--agent-options <json>] [--prompt <text>] [--json]
npx -p @piewf/cli piewf doctor cleanup [--older-than-days <days>] [--yes]
npx -p @piewf/cli piewf inspect [session-id] [--json|--summary] [--failed]
npx -p @piewf/cli piewf transcript <session-file>
npx -p @piewf/cli piewf run <workflow-name> [workflow arguments]
npx -p @piewf/cli piewf run --script <workflow.js> [--name <workflow-name>] [--input <json>]
npx -p @piewf/cli piewf export <workflow-name> [--name <command>] [--output <path>] [--force]
npx -p @piewf/cli piewf bundle <workflow-name> [--name <command>] [--output <directory>] [--force]
npx -p @piewf/cli piewf share <run-id>
npm run inspect -- [session-id] [--json|--summary] [--failed]
piewf --help prints structured command help, and every command accepts --help or -h. piewf doctor reports native resources, settings, model aliases, and registered functions. Use --agent-options with a JSON object and optional --prompt to inspect a generically prepared agent without a provider call. --json returns the report; warnings do not fail the command. Role-specific flags and positional role/file targets are removed. piewf doctor cleanup remains explicit and dry-run-first. piewf inspect and transcript inspect persisted artifacts.
Doctor JSON includes cwd, agentDir, settingsPath, settings, settingsSources, trust, activeTools, piExtensions, piSkills, functions, modelAliases, resourcePolicy, optional agentInspection, and diagnostics. Inspection includes the resolved model, tools, native resources and prompt, and setup diagnostics. Registered extension validators check settings produced by preparation, just as during execution. There is no roles catalog or roleInspection field.
Consumers should use diagnostic code and the named fields, treat optional fields as absent, and ignore unknown fields so additions remain compatible. There is no schema-version field. settingsSources identifies the source path for represented effective settings. resourcePolicy.globalSettingsPath and projectSettingsPath identify selector files, and top-level resourcePolicy.selectorSources carries only the global and project layers; prepared agent selectors appear under agentInspection.resources.selectorSources.
Portable piewf bundle artifacts use manifest version 2 and bundle the defining extension module plus its import graph into payload/extension.mjs; they no longer reconstruct a workflow from Function.prototype.toString(). Set source: import.meta.url on the registered extension and list third-party package names in dependencies. esbuild is loaded only while creating the bundle, declared packages are inlined, and undeclared packages or unsupported dynamic imports fail before an artifact is written. Node builtins and pi-extensible-workflows remain external; Pi packages (@earendil-works/*) are neither inlined nor external, so importing one fails the bundle: use the pi-extensible-workflows API instead. The manifest records the source module/export, esbuild version, and declared dependencies. The launcher still accepts version 1 bundles. Bundles do not copy Pi, credentials, or the workflow engine. Run <command> setup to verify npm-installed Pi, approve the documented engine install if needed, validate aliases, tools, commands, and environment variables, and persist a local bundle state file before launching.
Workflow consumers use Pi's project trust decision. Pi's automatic trust gate recognizes native .pi resources, not custom role directories alone; defaultProjectTrust: "never" is not a blanket denial when Pi considers a project to have no trust-requiring resources. Use --no-approve to explicitly exclude project resources/settings, or --approve to authorize them. The independent role launcher resolves its own authorization without that native no-resources shortcut.
run accepts flat workflow arguments plus --approve or --no-approve trust overrides. The generated export launcher forwards later arguments to run. A file-backed launch uses --script <workflow.js>; its workflow name defaults to the filename without the final extension and can be overridden with --name. Pass its JSON launch value with --input. Before --, these are launcher options; -- ends launcher option parsing and passes later tokens to workflow input. The headless run command and generated export launchers cannot execute workflows containing checkpoints; use the Pi workflow tool/UI path for checkpointed workflows. The final workflow value remains on stdout and the run ID is reported on stderr as soon as the run is created.
/workflow controls
/workflow
/workflow is the sole workflow slash-command. In Pi's TUI, when the current Pi session has runs, it opens the /agents workspace on its runs only (no Subagents group), with the same keys and action menu: a run's header, phases, operation scopes, and shells offer the run's contextual pause, resume, foreground/background, stop, delete, budget, checkpoint, copy, and script actions, then Model aliases and the bulk deletes; an agent offers only that agent's actions. Leaving the workspace ends the command. When there is no run, or once the last run went away through an action in the workspace, it opens the picker with Model aliases and Close. Outside the TUI it opens the picker; select a run to open its dashboard for the same run actions and Agents... for an agent's actions. /workflow without UI is read-only; use the workflow tools for mutations. Arguments such as /workflow resume are rejected and direct users back to /workflow. Manual deletion protects dependencies of surviving runs: individual deletion is refused with the dependent run named, while bulk deletion skips protected runs and reports them.
The navigator discovers only runs from the exact current cwd and Pi session. It exposes Model aliases even when no runs exist, groups agents by structural scope, and shows budget status and adjustment controls only when relevant. The workspace and the run picker keep unresolved runs in attention order and order resolved runs newest first by terminal resolution time. The workspace presents each run's header row above its phase tree; the selected-run dashboard outside the TUI shows the run's status and agent counts above its phase tree. Pause is cooperative, stop is immediate and irreversible, and delete is available only for terminal runs after confirmation.
Invocation options override settings defaults, and the session-wide active-agent ceiling is 16. Host-facing failures are WorkflowError values with stable codes such as CONFIG_ERROR, INVALID_SYNTAX, UNKNOWN_MODEL, UNKNOWN_TOOL, TOOL_FAILED, AGENT_FAILED, AGENT_RESULT_COLLECTED, AGENT_TIMEOUT, RESULT_INVALID, CANCELLED, WORKTREE_FAILED, BUDGET_EXHAUSTED, and RESUME_INCOMPATIBLE. Fix the reported trust, model, tool, role, budget, settings, or script issue before retrying.
/agents workspace
/agents
In Pi's TUI, /agents opens a full-screen workspace over Pi with the agents of the current Pi session; it opens whenever the session lists anything, including a run that has no agents yet (shell or checkpoint work only); only when nothing is listed does it say "No agents in this session." and stay closed, and it closes with the same notice when an action leaves nothing. One workspace is open at a time: a second /agents, /workflow, or /subagents while one is opening, open, or waiting for an action says "The agents workspace is already opening or open." Outside the TUI /agents says "/agents needs the TUI."; when the session's lease cannot be taken it says "Cannot open the agents workspace: ..." with the reason, and when the workspace itself cannot open it says "agents workspace failed: ..." once, as an error. /agents takes no arguments: with any it shows "/agents takes no arguments. Use /agents to open the agents of this session." and opens nothing. The Subagents group appears only when the subagents extension is loaded. Below a title row the sidebar shows, for each workflow run, a header row (the workflow name and the first 8 characters of the run ID, the run's state glyph, its finished and total agents as done/total, their cost when there is one, and the run's runtime, counted for a running run from its first second; the header is kept short to fit the sidebar, and a run waiting on a checkpoint adds ◆ waiting with what it waits for) and below it, one level in, the run's workflow tree (its phases, operation scopes such as a parallel and its branches, shells, and agents, in the same row format); then a "Subagents" header with the count of each state and the total cost, and one row per standalone subagent. A rule above each run and above the Subagents group, except on the sidebar's first row, shows where the one before ends; rules are not nodes, and the keys and clicks pass over them; a hint row runs along the bottom and shows only the keys that apply: a menu and d details only when available. The sidebar takes 38% of the width, 24 to 42 columns, and starts hidden below 80 columns, except on a portrait terminal (more rows than half the columns), where the tree sits above the transcript instead. /agents and /subagents open on the first agent (on the first node when none is listed), with its ancestors expanded; /workflow opens on the header of the first run in attention order, so its run actions are one key away. A finished run (completed, failed, or stopped) starts collapsed, and so does the Subagents group while none of its subagents runs; the other nodes start expanded. A refresh, every second while an agent runs or the shown transcript is still being read and every 5 seconds otherwise, and, while nothing runs and the shown transcript is read to its end, at once when a run or subagent of this Pi starts or changes (/agents and /workflow only), keeps the expansion and the selection: a node keeps the expansion it first appeared with, so a run that finishes while the workspace is open stays expanded.
A selected agent fills the rest with its full transcript, drawn with Pi's own message and tool components so it reads like the main chat (it follows Pi's display settings: hidden thinking blocks, images and their width, output padding, and code-block indentation; a skill invocation shows as in Pi's chat), and the title shows it as #position/count name state among the agents with its model, tokens, and cost. Any other node shows its details there as /workflow does (state, agent counts, tokens, cost, and elapsed time; for the Subagents group, the count of each state and the total cost) and the title shows its label and state; such a node opens no session file. d on an agent shows its details instead of its transcript (an agent's /workflow details, a subagent's /subagents detail rows) and again its transcript, which goes on reading meanwhile; a transcript keeps its scroll place across an action that steps aside and across a refresh; the choice holds for every agent selected until d again, an agent without details shows its transcript, and one whose details a refresh empties shows its transcript until they are back.
An agent counts as running only while its run is running, pausing or awaiting input, so an agent a crash left active (running, waiting for a child, retrying or paused) in a run that is not running, pausing or awaiting input is not followed, not marked, not counted as running in the title, its row shows a muted · in place of the spinner, its title shows no state, its details say State: interrupted (recorded running); the counts of its run, phase and operation leave it out of running and add interrupted=N only when N is above 0; and state:interrupted finds it while state:running never does (a leftover recorded paused still matches state:paused); a phase or operation row that would say running in such a run shows the run's state instead of the spinner. n and N select the next and previous node that needs a person: a failed agent, a running agent that has been quiet or stalled (its row ends with ⚠ quiet 4m or ⚠ stalled 12m), and a run that is awaiting input (its header shows ◆ waiting) or has exhausted its budget. A mark shows only whole: a row too wide for it is cut with … before the mark, and a mark that would leave no character of the row's label is left out. The hint shows n next only while one exists, f follow while an agent runs, and / filter while the sidebar shows and has the focus and no filter is set. The title carries the session totals at its right end when they fit beside the title: running and total agents, tokens, and cost; parts without data are left out. f toggles follow mode (with no agent running it stays off): the selection moves to the running agent with the newest activity and its transcript stays at the tail, and the title says "following"; it stays on that agent while the agent runs, and moves on to the running agent with the newest activity only once it settles or is gone. f again stops it, as does moving the selection or scrolling the transcript away from the tail. With the sidebar focused, / opens a one-line filter under the title: text matches a node's label, a run's name, or part of its ID (the run ID on a run, the agent ID on an agent) case-insensitively, and state:<state> matches a state (the Subagents group's own summary state is not matched; an agent a crash left active matches state:interrupted, as above). The sidebar then shows the matches with their ancestors and the title counts them; enter keeps the filter, esc clears it, and the selection stays when its node still shows. The ancestors that show only for context are dimmed.
In the TUI a workflow agent that owns a worktree offers Open diff in editor, an in-place action that opens the full git diff against the base the worktree was created from in the external editor on a temporary copy whose changes are not written back, or says "No changes since the worktree was created." when there is none (with untracked files it says "No tracked changes since the worktree was created; untracked files are not in the diff."). The read neither takes the worktree's index lock nor refreshes its index, and it ignores a configured external diff and color.
A run's, phase's, and the Subagents group's details end with a timeline: one row per agent on a shared time scale, grouped under the phase names for a run, with a bar from its start to its end (to now while it runs; an agent a crash left active in a run that is not running, pausing or awaiting input is drawn interrupted, up to its last recorded event, not growing). Each row can also show the agent's speed between the bar and the duration: output tokens per second, mean time to first token (TTFT) and "N tok in X s", dropping parts to keep the bars, and nothing for a run recorded without counters. The figures count each assistant message that finished normally, from its first streamed content (the first non-empty text, thinking or tool-call delta) to its end, summed per attempt and across attempts; errored, aborted, pending and deferred responses, tool time, and turns handed to Herdr are not counted, and tok/s needs at least 250 ms of generation. TTFT runs from Pi's turn_start event, which precedes every request, to the first non-empty delta, so it includes Pi's work and any provider queueing before the stream starts, and it differs from tools that measure from the user message. A selected agent's title shows its persisted average as N tok/s avg; there is no live meter.
The transcript is the only scrolling view: page up and down, half pages, ctrl+home and ctrl+end, search, and the mouse wheel always act on it. ↑ and ↓ move over the tree's visible nodes while the sidebar has the focus and scroll the transcript by a line while the transcript has it. With the sidebar focused, ← and → fold the tree: ← collapses an expanded node or else moves to its parent, → expands a collapsed node or else moves to its first child, and space collapses or expands the selected node; in the sidebar k, j, h, and l move as ↑, ↓, ←, and → do. tab moves the focus, a click on a sidebar row selects that node, a click on its ▾ or ▸ also collapses or expands it, and [ and ] select the previous or next agent with either focus, skipping the other nodes (under a filter, those it hides) and expanding the agent's ancestors. s hides or shows the sidebar, Pi's tool expansion key (ctrl+o) expands or collapses tool output. With Pi's fullscreen copy-on-select setting on (the default), text selected with the mouse is copied. esc closes the search box when it is open, clears a filter that is set, and otherwise leaves, and ctrl+c always leaves. When Pi's session shuts down while the workspace is open (a SIGTERM or SIGHUP, or an extension calling shutdown()), the workspace closes from session_shutdown: it leaves the alternate screen, turns mouse tracking off, and its command ends at once; Pi's TUI is not started again, as Pi is quitting. Pi's crash path (an uncaught exception) emits no session_shutdown: Pi's uncaughtException handler prints the crash and calls process.exit(1). While it is open, the workspace holds an uncaughtExceptionMonitor listener, which Node runs before that handler, so a crash leaves the alternate screen and turns mouse tracking off before the crash is printed, and the crash text stays on the normal screen; an exit listener does the same for a process.exit() without an exception.
a with either focus, and enter with the sidebar focused on a node that is not an agent, open the selected node's action menu. A run's header, phases, operation scopes, and shells offer the run's actions as /workflow offers them, then Model aliases, then Delete all completed and Delete all failed when the session has such runs; a workflow agent offers its /workflow agent actions, a subagent its /subagents actions, and the Subagents group the bulk deletes. In the menu ↑/↓ or k/j move, enter runs the selected action, and esc, ←, or h close it. The copies (Copy run path, Copy run ID, Copy agent ID, Copy branch, Copy worktree path) and the editors (Open script in editor, Open prompt in editor, Open system prompt in editor, Open result in editor, Open diff in editor) run in place: the external editor takes the terminal and gives it back to the same frame, and their notice shows in the status row until the next key. Every other action steps aside: the workspace closes, the action runs with Pi's own dialogs and notices exactly as in /workflow and /subagents, and the workspace opens again on the same node (the node now at its row when it went away, the new run after a subagent Retry) with the same expansion, focus, sidebar, tool output, and d, the action's notices in the status row. An extension's agent action (such as Herdr's Open live session in Herdr pane) runs detached: the workspace opens again at once while it goes on, its working message shows in the status row until it settles (a message it set while the workspace was closed is cleared from Pi's own working message when the workspace opens, and an action that settles after /reload or /new shows nothing), a later failure is reported as the notice Agent attempt action failed: <error> in that row, and each dialog it asks for (confirm, select, input) is refused with a notice and answered as cancelled; the pickers outside the TUI wait for it and show its dialogs. A Foreground resume keeps Pi's screen until the run settles; an action no longer offered when it is chosen does not run, and the status row says so. The menu of /workflow offers the run and workflow-agent actions, that of /subagents the subagent actions, and that of /agents both (the subagent actions when the subagents extension is loaded).
A workflow agent's transcript is read only through the run store of this Pi session; a subagent's only when its inspected record names this Pi session, has a safe ID, and, when it records a cwd, names this project. Any other agent shows "transcript unavailable" and its session file is never opened. A session path that is not a regular file, or whose header names another session, is reported as unavailable. The whole session file is read when the transcript opens, as Pi reads it; complete records appended to it show on the next refresh, and the message an agent running in this Pi process is generating streams in live; a new attempt shows a fresh transcript.
Contributor verification
Start with the repository's code map and completion rules. Commands below run from the repository root; their implementations live in the workspace manifests, not in a separate CI test runner.
npm ci
npm run check
npm run test:packages
Pull requests and main-branch pushes run these same commands on Node.js 24. check builds and type-checks all packages, lints, runs the workspace and template tests, and validates documentation links. test:packages additionally packs and installs all three artifacts in temporary directories, checks imports and entrypoints, exercises the installed CLI and Pi package discovery, and runs npm audit. It requires npm registry access but does not publish anything. Release checks remain in RELEASING.md.
Focused iteration
Core runs at most 20 test files in parallel, capped by Node's available CPU parallelism. Each file runs its tests serially; browser and worktree integration tests keep their existing deadlines.
Build before running compiled tests; otherwise test:run can exercise stale output. TEST_FILES paths are relative to the selected workspace. For example:
npm run build --workspace=packages/core
TEST_FILES='dist/test/starter.test.js' npm run test:run --workspace=packages/core
npm run build --workspace=packages/cli
TEST_FILES='dist/test/doctor.test.js' npm run test:run --workspace=packages/cli
Match additional checks to the affected boundary: npm run acceptance exercises deterministic runtime acceptance; npm run docs:check checks documentation links and markup conventions. Keep a regression that fails before a behavior fix and passes after it. Existing starter tests execute the role-call examples from shipped prompts; do not replace behavioral checks with prose-only assertions.
For cross-repository roles integration, build and pack both products locally, then run PI_OFFLINE=1 node scripts/verify-local-roles-adapter.mjs <local-tarball-directory>. The harness requires core, CLI, and roles tarballs, installs only those local products in a temporary fixture, exercises SDK and CLI sessions against a local synthetic HTTP provider, and removes the fixture. It does not verify paid providers or remote cache hits. PI_OFFLINE=1 node scripts/verify-pre6-parity.mjs <local-tarball-directory> [--old <pre6-tarball-directory>] uses the same local products to check restored pre-6 behavior through the real pi and piewf CLIs, including cold resume from the interactive picker through a pseudo-terminal (Linux script); optional pre-6 tarballs run as controls.
What a green check does not prove
Core test:herdr skips navigator integration unless run inside a real Herdr pane with HERDR_ENV=1. Run npm run test:herdr --workspace=packages/core there after building when changing that UI; do not set the variable merely to bypass the skip. Ordinary CI does not prove live Herdr behavior. Inspect the actual TUI or browser target for UI changes.
Model-backed evaluations are separate, opt-in checks. Deterministic tests do not prove live provider behavior. Run these only with an explicitly approved provider/model and external cost:
npm run evals -- --provider "$PROVIDER" --model "$MODEL" --case direct-answer,parallel
PI_WORKFLOW_EVAL_AMBIENT=1 npm run evals:ambient -- --provider "$PROVIDER" --model "$MODEL"
Before handoff, report the user outcome exercised, exact checks and failures, and any skipped or unverified paths. A reviewer verdict is not execution evidence; optional improvements are not blockers. Do not merge or deploy as part of verification.