@rthadani/pi-orchestrator

A pi extension that ships a default agent workflow, injects it where an AGENTS.md goes, and enforces delegation before direct file work.

Packages

Package details

extension

Install @rthadani/pi-orchestrator from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@rthadani/pi-orchestrator
Package
@rthadani/pi-orchestrator
Version
2.3.0
Published
Nov 2, 2026
Downloads
986/mo · 658/wk
Author
rthadani
License
MIT
Types
extension
Size
78.5 KB
Dependencies
0 dependencies · 1 peer
Pi manifest JSON
{
  "extensions": [
    "./extensions/pi-orchestrator.ts"
  ]
}

Security note

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

README

pi-orchestrator

A pi extension that ships a default agent workflow, injects it into the system prompt where an AGENTS.md would go, and enforces it by blocking direct code changes, not reads or builds, until a subagent has been spawned. Installing the package gives you a working plan -> implement -> review loop without writing an AGENTS.md. The model roster lives in pi-failover, which is a required companion and registers model_roster itself.

What it does

Two mechanisms. Both are needed, and neither alone was enough.

Injection. The winning workflow is added to systemPromptOptions.contextFiles, which pi renders inside <project_context> as <project_instructions path="...">. That is the same slot, framing, and position as AGENTS.md, and it carries the same authority. A custom prompt section always renders after <cwd> - last in the prompt, behind the skills list - which is the weakest position available and is why the workflow used to be readable but ignored.

Enforcement. tool_call handlers can block a call. Until a subagent spawn has happened in that session, the gate blocks a project code write and nothing else.

What the gate blocks

  • Edits and writes to a source file: a code extension (.ts, .tsx, .js, .py, .go, .rs, .java, .sh, and the like), or any file under src/, app/, lib/, packages/, extensions/. A docs extension wins over the directory rule, so src/README.md stays a direct edit.
  • Commands that change tracked repository state: git commit, push, reset, checkout, rebase, merge, cherry-pick, revert, clean, git stash drop, git tag -d.
  • Project-local dependency installs: npm i, yarn add, pnpm add, pip install, cargo add, go get.
  • A formatter or linter in its write mode: prettier --write, eslint --fix, gofmt -w, rustfmt.
  • Migrations.
  • An irreversible delete (rm, shred, truncate, dd), or a redirect into a source file.

What passes directly

  • Reads, grep, find, ls, LSP tools, ctx_search, web tools.
  • Builds and type checks (npx tsc, npm run build), and test runs (npm test, vitest, pytest).
  • Tooling installs: pi install, pi uninstall, npm i -g.
  • git status|log|diff|show|branch|remote|fetch.
  • Documentation and config edits (.md, .json, .yaml, .toml), wherever they live.
  • Anything under the pi agent dir (~/.pi/agent, or PI_CODING_AGENT_DIR).

Classification is permissive on purpose. An unknown tool, an unparseable command, or a write we cannot classify is allowed, so a misclassification errs toward letting work through, never toward blocking it: a wrong block costs a scout/oracle/worker round trip for work that never needed one, which is the failure that made ordinary tasks unusable. subagent and ask_user always pass. Child agent sessions are exempt, because pi-subagents runs children as pi sessions inside the same process and gating them would deadlock the scout the gate exists to force. Counts are per session and per spawn, so a failed or abandoned spawn cannot leave the gate stuck open.

A completed top-level code write re-arms that session: the next code change must delegate again, which is what keeps implementation on worker instead of in the orchestrator. An inspection or a test run does not re-arm.

Workflow. The packaged default in assets/ORCHESTRATOR.example.md is injected only when all four required companion extensions are installed; otherwise it is withheld, and a withheld workflow is never enforced. Your own ORCHESTRATOR.md in the current directory, a parent, or the pi agent dir replaces the default outright and is always injected verbatim, never withheld. /workflow doctor reports what is present, whether the workflow is withheld, and whether the gate is armed.

Model roster. This package does not render or inject the model roster. pi-failover owns that section: it registers model_roster itself, so the workflow can name capability classes instead of hardcoding model names.

The workflow will not run without a coding-max class. The default workflow routes implementation across three classes, flash, coding, and the opt-in coding-max, so ~/.pi/agent/failover.json must define all three (an opus, sonnet, and flash class alone is not enough). With no coding-max set, every worker spawn that names it fails to resolve, and the opt-in justification rule in the workflow has nothing to opt into. See Classes the workflow requires for the shape to copy.

Why it exists

A workflow file like ORCHESTRATOR.md should name capability classes (opus, sonnet, flash, coding, coding-max are conventional names, not required ones) rather than model names. That way swapping a model in failover.json never requires editing the workflow file: pi-failover's roster section always describes what each class currently resolves to.

Install

pi install git:github.com/rthadani/pi-orchestrator

pi-failover is a required companion, so install it too:

pi install git:github.com/rthadani/pi-failover

Then define the classes the workflow calls by. Without the coding-max set the shipped workflow cannot route worker for expensive work, so treat failover config as part of the install, not an optional tweak:

Classes the workflow requires

Class Used by Notes
flash scout, cheap worker, tests and simplicity reviewers the default for trivial and mechanical work
coding worker for non-trivial work keep the cheap-but-capable models first; failover advances on failure, so position 1 is the price you pay by default
coding-max worker, opt-in only the premium coder (for example a kimi-k2.7-code-style model) with a cheaper model behind it, so a class-level outage still fails over

The minimum shape, inside sets in ~/.pi/agent/failover.json:

{
  "name": "coding-max",
  "models": [
    { "name": "coding-max-primary", "provider": "moonshotai", "api": "openai-completions", "model": "kimi-k2.7-code", "cost": { "input": 0.95, "output": 4, "cacheRead": 0.19, "cacheWrite": 0 } },
    { "name": "coding-max-fallback", "provider": "deepseek", "api": "openai-completions", "model": "deepseek-v4-pro", "cost": { "input": 0.435, "output": 0.87, "cacheRead": 0.003625, "cacheWrite": 0 } }
  ]
}

The cost block is what makes the price visible in the rendered roster, which is where the workflow's approval question and its cost delta come from.

Behaviour

The workflow is resolved once per resolved path and cached. It is re-read only when that file's mtime or size changes. If the file is missing, unreadable, or empty, the extension injects no workflow, enforces nothing, and warns once at load; it never throws into a session.

Enforcement has one switch: WORKFLOW_ENFORCE=off (also 0, false, no) disables the gate for the process while leaving injection intact. There is no per-task bypass: the gate classifies the shape of a write, not the significance of the work, so it cannot decide that one particular task is small enough to skip delegation, and a bypass command is exactly what gets used to skip the work.

/workflow reports which workflow source won (your ORCHESTRATOR.md with its path, or the packaged default with its mtime); /workflow init copies the template to a file you own; /workflow doctor audits which required extensions are installed, whether the workflow is currently withheld or injecting, and whether the gate is armed for this session. Set ORCHESTRATOR_DEBUG=1 to log the workflow report at load.

Default workflow

The package ships a full default orchestration workflow (assets/ORCHESTRATOR.example.md). It is injected into the system prompt only when no ORCHESTRATOR.md is found. When you create one, it replaces the default outright: the default and your file are never both injected, so they cannot contradict each other.

Search order, nearest wins:

  1. ORCHESTRATOR.md in the current working directory
  2. ORCHESTRATOR.md in any parent directory, up to the filesystem root
  3. ORCHESTRATOR.md in the agent dir (~/.pi/agent, or PI_CODING_AGENT_DIR), the global fallback

pi still loads AGENTS.md (and the other context files) itself, so AGENTS.md is not a substitute for ORCHESTRATOR.md and does not suppress the default workflow.

To start customizing, run:

/workflow init

That copies the packaged example to ./ORCHESTRATOR.md (or to a path you pass, e.g. /workflow init /some/where/ORCHESTRATOR.md). It refuses to overwrite an existing file. Edit the file, then restart or reload pi.

A shipped-default-only setup needs the four required extensions installed: with no ORCHESTRATOR.md anywhere, the packaged default is injected in full once they are present. To disable the shipped default, create an empty ORCHESTRATOR.md.

The shipped asset is not meant to be edited in place: package updates overwrite it. Use /workflow init to start a copy you own.

Required extensions

The workflow requires four companion extensions. None are installed automatically by this package; /workflow doctor reports what is installed and whether the workflow is withheld.

Extension Why the workflow needs it Install
pi-subagents the oracle/worker/reviewer/scout/researcher roles pi install npm:pi-subagents
pi-boomerang long autonomous runs without raw output in context pi install npm:pi-boomerang
context-mode bulky-I/O rules via ctx_execute/ctx_batch_execute/ctx_search pi install npm:context-mode
pi-failover the failover provider, which injects the model roster, provides failover/<name> routing, and must define the flash, coding, and coding-max classes the workflow names pi install git:github.com/rthadani/pi-failover + the required classes
ask_user (optional) the one-focused-question rule pi install npm:pi-ask-user

pi-subagents, pi-boomerang, context-mode, and pi-failover are required: when any one is not installed, the packaged workflow is withheld (not injected) until it is. The check is on installation (pi's settings packages plus the model registry's failover provider), never on runtime tool visibility, so a lazy extension that has not yet registered its tools does not suppress the workflow. ask_user is optional and never gates anything; the one-question-at-a-time rule degrades gracefully without the tool.

Your own ORCHESTRATOR.md is never withheld: when one is present it is injected verbatim, exactly as written, regardless of which extensions are installed.

What it deliberately does not do

It makes no routing decisions and never contacts a provider. It does not render or inject the model roster: that section belongs to pi-failover. Policy (which class to pick when, escalation rules, review discipline) lives in your ORCHESTRATOR.md, or in the packaged default workflow when you have none.

It also does not judge your task. It recognises the shape of a code write, not the significance of a task: it cannot tell a new feature from a one-line typo fix, so it enforces the shape of the work (delegate, then do) for the one class it can name, project code writes, and lets everything else through. The orchestration judgement for genuine feature work still lives in the injected prompt, not in the gate. The remaining hole is a code write no text pattern can name: node -e 'fs.writeFileSync(...)', cp src/a.ts src/b.ts, a codemode script, or a JS ctx_execute body are all allowed, because calling them writes would be a guess. Treat the gate as a nudge toward the workflow, not a sandbox.

Publishing

Publishing authenticates with npm Trusted Publishing (OIDC): no npm token is stored in this repository, and provenance is generated automatically.

One-time bootstrap (hard prerequisite): npm cannot create a brand-new package over OIDC, so the first version must be published once outside this workflow (per npm's own guidance). After that first publish, every later version publishes from CI. Register the trusted publisher on npmjs.com: package Settings -> Trusted Publishing -> GitHub Actions, with user rthadani, repository pi-orchestrator, workflow filename publish.yml, and an empty environment. The workflow filename is part of the trust binding: renaming or moving .github/workflows/publish.yml breaks publishing until the registration is updated.

Publishing happens from CI when a version tag is pushed. From main, with a clean tree and no unpushed commits:

npm run release patch     # or minor, major

That script runs the tests, bumps version, commits, tags v<version>, and pushes both the commit and the tag. If you would rather do it by hand, bump version in package.json, commit, then run git tag v<version> && git push origin v<version>.

The workflow then refuses to publish if:

  • the tag and the package version disagree;
  • that version is already on the registry (npm cannot overwrite a published version, so this fails before the upload rather than during it).

A successful tag push publishes to npm with provenance, then creates a GitHub Release at that tag with notes generated from the commits since the previous tag. A manual run (workflow_dispatch) defaults to a dry run, uploads nothing, and creates no release. Requirements: npm CLI >= 11.5.1 (the workflow upgrades npm itself) and a public repository.

Tests

npm test

The harness needs no install and no dependencies; it exercises workflow resolution, installation gating, and section injection with offline fixtures. It also runs on prepublishOnly, so a manual npm publish cannot ship a failing build.

Notes

Pi loads extensions at startup, so restart after installing. Do not keep a second copy in ~/.pi/agent/extensions/, or the provider/section registers twice.

Upgrade pi-orchestrator and pi-failover together, because older mixes write the same section key. Upgrading across the coding-max change also means adding that class to failover.json first: the workflow names it, so an unchanged config leaves the opt-in path unresolvable.