@edgehero/pi-dispatch-admin
Go from sitting next to one AI coding agent to running many in the background: the web console and /dispatch panel of pi-dispatch, over the queue, spend caps, scoped limits, run history and insights, editable cron and GitHub, GitLab, Forgejo and Azure Dev
Package details
Install @edgehero/pi-dispatch-admin from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@edgehero/pi-dispatch-admin- Package
@edgehero/pi-dispatch-admin- Version
5.0.0- Published
- Nov 5, 2026
- Downloads
- 1,055/mo · 77/wk
- Author
- mashground
- License
- MIT
- Types
- extension, skill
- Size
- 19.5 MB
- Dependencies
- 3 dependencies · 1 peer
Pi manifest JSON
{
"image": "https://raw.githubusercontent.com/edgehero/pi-dispatch/main/docs/images/banner-still.png?v=2026-11-04b",
"skills": [
"./skills"
],
"extensions": [
"./dist/index.mjs"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-dispatch: go from sitting next to one AI coding agent to running many in the background
pi is an open source coding agent that you normally run in a terminal. pi-dispatch runs it for you
in the background: on a schedule, on your repo's issues and pull requests, or from the command line. Each job happens in a sealed box that is
thrown away afterwards, stops at the spending limit you set, and shows up live on one web page. You start in the loop, watching it work. You
move to near the loop, where it asks when it needs you, and to out of the loop, where a second agent reviews the work and then you decide, until you are looking after a swarm of
agents rather than one: dark engineering. pi-dispatch never merges anything, so what ships stays a person's decision. It runs on a provider key or a subscription you already have, falls back to other models you
list, and brings your own pi setup and extensions along with pi-dispatch import-pi. The main README
explains all of it.
This npm package,
@edgehero/pi-dispatch-admin, holds the two ways to work with a deployment: the web console (pi-dispatch-console, above) and the/dispatchpanel, a pi extension. The service itself is@edgehero/pi-dispatch(worker and CLI) and@edgehero/pi-dispatch-receiver(the webhook edge). The main repo has the job image, the docs (Docker, Podman, egress, every forge) and SECURITY.md.
How it works
Every trigger produces the same job, through the same path: one queue, one container, one budget.
CLI · cron · label / comment / PR on GitHub, GitLab, Forgejo or Azure DevOps
│ enqueue
▼
Valkey + BullMQ durable queue: survives reboots, absorbs bursts
│
▼
under the day, week and if not: refused here, before any spend
month caps (jobs, tokens,
dollars), the scoped limits,
the project's share of the
envelope and the turn budget?
│ yes
▼
one container per job Docker or Podman, removed after the job: all
capabilities dropped, no-new-privileges,
memory/CPU/pids limits, /job mounted read-only
│
▼
pi + your .pi/skills edits your code in place, opens a PR or MR, comments back
The container is the boundary, and it is pi's missing permission system. The worker builds the isolation
flags itself, so nothing an image contains can weaken them. The non root user comes from the image on
Docker Desktop (and for a worker running as uid 1001), and from the worker's own uid on a native Linux
daemon. That is why an image has to meet the checklist in
docs/job-image.md. What each
container runtime guarantees is declared in
docs/backends.md, and
pi-dispatch doctor --live reads it back from real containers.
Where it runs. On any Linux server or VM you control, or your own Mac or Windows machine with Docker Desktop: the worker is a long running process beside a container runtime on the same host. Serverless platforms, hosted sandbox services and a container daemon on another machine are not supported. pi-dispatch itself needs no AI key; the provider key is for pi inside each job. The details are in the main README.
Spend is checked before a container starts, so a runaway or a junk trigger costs a refusal, not a bill. The job image ships Playwright and Chromium, so a flow can build a frontend, screenshot it and iterate.
What the agent can see. Two credentials go into each job as environment values, the provider key and
the forge token, and the agent can read both. The guardrails tell it never to print or send them, but
that is prompt text, not enforcement. Set a spend limit at your provider. On GitHub, the default gh auth
source forwards your whole login, full scope and never expiring, and pi-dispatch doctor warns about
it; the GitHub App gives each job a one hour token for one repo instead. Network egress is denied by
default and goes through an allowlist proxy, and the allowed hosts are yours to list
(docs/egress.md).
Triggers: what starts a job
Every trigger is one { on, run } entry in a single triggers.json. The worker reads it live for cron,
the receiver reads it live for forge webhooks, and the panel and the web console can edit it. on is what fires it;
run is the skill it runs.
on.type |
Fires on | What the agent gets as its task |
|---|---|---|
cron |
your schedule | the task written in the file |
label |
a label on an issue (or an Azure work item), never a pull request | the issue title and body |
comment |
a comment containing your phrase, such as @pi |
the comment, plus the issue title and body |
pull_request |
a PR or MR event, including its close | the PR title and body |
issue |
an issue closing (once: true makes it a one-shot) |
the issue title and body |
Four forges: GitHub, GitLab, Forgejo (and Gitea), Azure DevOps. Who may fire a trigger is your forge's
decision, not this service's. On GitHub the label is the approval, because only collaborators can add
one. GitLab, Forgejo and Azure check the actor's permission through their APIs. A close trigger checks the
person who closed the item (Azure has no close trigger yet, and refuses one at load). Each forge's
action words are checked when the file loads, so a word from the wrong forge is refused instead of never
matching. The full reference is
docs/triggers.md.
Flows and workflows. run.flow names a skill committed to the target repo at
.pi/skills/<flow>/SKILL.md, read from the default branch. The repo owns the prompt, and merging it
is the repo's consent. A skill may call other skills, which is already a workflow. For typed multi-stage
workflows, stage a pi extension such as @juicesharp/rpiv-workflow into the deployment. It is pinned to an
exact version, installed on your host (never at job time, since jobs run offline), loaded in every
container and declinable per trigger. Anything you installed with pi install is staged automatically at
your host's version.
run.command is the second entry point. A trigger may name a registered command that a staged extension
provides ("command": "wf run nightly"). The job's whole prompt is that line, handled by the extension
with no model turn in between. It picks which vetted command runs, never what code runs, and it is never
AI triggerable: job chaining refuses a request naming one, and dispatch_run cannot express one.
Four things to know before you build on it:
- A trigger names a flow or a command, never a workflow. That entry point decides which stages run.
- A job is not an interactive session. The container hands pi one prompt and reads the exit line.
- One trigger is one job, one budget slot and one turn budget. Ten stages share the same
PI_MAX_TURNSand token budget, and running out ends the job as a policy refusal that is never retried. - State survives only on local jobs. A cron or CLI job has your folder mounted read-write, so state
persists. A forge job gets a fresh clone that is thrown away with the container
(
docs/workflows.md).
The panel: /dispatch
One command puts a live terminal view over the whole deployment:
- MCP servers, read-only. A trigger that names MCP servers says so on its row (
[mcp ...]), and its drill-in shows each one as the overlay offers it now: where it runs, how its tools are reached, and the variable names it is handed, or why every job of it would be refused. Nodispatch_*tool and no panel key changes them: a person switches a trigger's servers on its page in the web console, or in the file. - Status and spend. Queue and worker state, day, week and month spend meters, a daily token counter,
and the run history with tokens and cost per job. Each running job shows where it is: its turn, steps done and
failed, the tool running and for how long, and the phase it reported (or, when its worker has written nothing for
3 minutes, that the worker may be gone).
Enteron a run opens its full record,xon a running job cancels it after asking, andbon an opened run reopens its workspace. - A run's chat.
/dispatch chat <run-id | issue-or-PR-url | session-key>opens the run the way it can be talked to now: a running job opens on its live transcript, andEsclands on its row, wheretandTsteer it; a parked run shows its question and asks for your answer, then queues it after a confirm; a finished run opens in your own pi as a fork, on this host, with its tools off. It is a command you type, never a model callable tool (docs/chat.md). - Insights, the one analytics page. It is
/insightsin the web console (below); pressi(or type/dispatch insights) and the panel shows its address. It shows the budget dials, plan verdicts against API rates, daily, cumulative and per flow spend charts, five breakdowns (by flow, trigger, model, repo and project), the budget split with each project's share, spend and floor and the split's history, how busy each host was (jobs only), and the trigger and flow topology with spend on each trigger, plus a what if form that re-prices a flow on another model. A plan covered run never shows as $0.00, and an estimate is always marked as one (docs/insights.md).
- Triggers, editable live. Add, edit and delete triggers without a restart. Drill-ins show what
fires each one, what it runs and who may fire it. A one-shot shows armed or spent. A spent entry
stays in the list, matches nothing, and is re-armed when you delete its
on.disarmedmark from the file. Triggers that run third party code or a custom image carry a badge. No panel key and no model callable tool turns either on or off. The staged packages are a person's switch on the trigger's page in the web console, or an edit to the reviewedtriggers.json, and a custom image stays an edit to that file. A portfolio manager's cron trigger carries[portfolio]: its jobs write the budget split. The trigger tools can turn onprogress(the job gets areport_progresstool to name its phases) andstatusComment(one forge comment the worker edits as the run goes, numbers only), andrecover(a run the worker loses continues from its last checkpoint), behind your confirm. - Quiet hours. Pause windows per folder or repo, timezone aware. A paused job waits, never drops, and costs nothing.
- Scoped limits. Job caps per repo, folder or project (day, week, month), refused before any spend, plus a
ceiling on how many run at once, enforced by making jobs wait. Dollar caps per day, week or month for a
repo, a folder, a model (
model:<provider>/<id>) or a project (project:<id>). Write a repo with its forge (github:acme/web) to limit it on that forge only. Local jobs also have a fixed guard with no switch: one job per folder at a time (it lives in the worker process, and one worker per container daemon is the supported shape), because two agents editing one working tree race each other with no gate and no undo. - Job sizes. A project row can set the memory and CPUs of its jobs, and how much of a machine they may hold
(
hostShare) or keep room for (minJobs). The PROJECTS view (j) shows each project's size, the p95 of its runs' memory peaks and cores used, a suggested size and the exactdispatch_limit_editcall that applies it.dispatch_limit_addanddispatch_limit_edittake the four fields behind your confirm, and nothing applies a size by itself (docs/sizing.md). - Dollar windows. With dollar caps set, the panel shows each window's spend and holds against its cap,
and what the run records settled (
docs/costs.md). - Projects.
jshows each project with its members and this month's spend, andEnteron one filters the runs list.dispatch_projectslists them, anddispatch_project_add,dispatch_project_editanddispatch_project_deletechangeprojects.jsonbehind your confirm (docs/projects.md). - Hosts.
ushows each worker host: its slots in use of its limit, what its budget has promised its jobs, how many jobs wait for its budget, and its running jobs with project, size and age, then how busy it was over the last 7 days (busy share, jobs at once, time full, the wait and the top projects), labelled with when they were read. The 7 days are read when the view opens and again onuin it. Time before a host's history starts is said and not counted, but a host that is new, or was down, inside history that is here reads as idle for that time.dispatch_capacityreturns the same report (docs/multi-host.md). It counts jobs only: a machine busy with other work reads as idle.
- The budget split. With an allocation envelope (a dollar total per window and a floor per project),
bin the list shows each project's share and spend, the applied plan with the reasons it gave, and the history.ron a history row reverts to it after a yes or no.dispatch_priorities_setsets a plan of weights inside the envelope, confirmed in an interactive session or delegated headless with the explicit process grant;dispatch_envelope_setchanges the envelope behind your confirm;dispatch_allocationsreads the split. pi's ownwriteandedittools are blocked on the envelope,projects.json,scoped-limits.json,triggers.jsonand the settings file (docs/allocation.md)./dispatch prioritiesshows the split as text, and/dispatch priorities set shop=3 platform=1sets a plan. A run's detail names the plan a portfolio run wrote and the dollars it settled.
- The portfolio manager. A weekly flow you own reads the budget and writes a plan of weights. The
worker applies it inside the envelope, bounds each move and logs it, so you can revert it
(
docs/portfolio-manager.md). - Held and failed jobs. A trigger with
run.waitForholds its job in the queue, unstarted and unbilled, until a time passes or your check script exits 0. The panel shows a held section while anything waits (the target, the condition and how long), and a failed section for jobs the queue marked failed after their last attempt.dispatch_waitslists held jobs anddispatch_wait_cancelstops one behind your confirm (docs/wait-for.md). - AI operable, with explicit authority. Every model write uses the same action policy as the browser.
Interactive sessions confirm every change, including pause, priorities and paid starts. Headless sessions
need
PI_DISPATCH_AGENT_WRITES=1in the actual pi process before it starts, and can use only unattended actions. That includes explicit cap decreases, adding ordinary restrictive limit rows and pauses, turning existing run flags off, cancellation, failed-job removal and bounded priorities. Scope, size, minimums, inherited limits and mixed widening requests stay confirmed. The deployment file and pointer cannot grant this authority. Answers, draft decisions, standing-answer adoption and forge reruns remain person actions, and nodispatch_*tool can set a trigger's job cap, allowed models, fallbacks, MCP servers, tool exclusions, staged packages, extension exclusions or extension tool exclusions, whiledispatch_trigger_editcan still move its flow, provider and model behind a confirm. A pi process's ownbash,writeandedittools are outside this policy, so an unattended one must run with them disabled (SECURITY.md). Tool writes are audited as agents; human-confirmed intervention is tracked separately. - Paid starts stay bounded.
dispatch_run, Fire now and local or cron reruns need explicit validPI_DISPATCH_RUN_ROOTSin the pi process environment. Fire now and reruns also require an explicit validPI_DISPATCH_RUN_PER_HOUR.dispatch_runkeeps its default of three starts per hour when that value is absent or invalid; a valid zero disables it. They check the current committed flow'sai-trigger: allow, a clean tree and the shared hourly allowance again before enqueue. They accept no spend settings, cannot run commands or portfolio flows, and still pass the worker's budget gates. A paid run edits its folder in place. ReadSECURITY.mdbefore enabling it. The bundledoperate-pi-dispatchskill explains each action mark and refusal. - Agent reads expose operational facts. Failed jobs, all waiting jobs, hosts and setup have dedicated read tools. Runs report local retention when known; hosts report budgets, holds, CPU reserve, orphans, kept sandboxes, doctor counts and qualified venue declarations. Costs include running holds and bounded what-if estimates. Model results omit private job/operator text, question and draft content, variable names, host paths and what a run loaded. Setup suggestions come from the trusted command catalogue.
- Logs stay put. Raw container output shows only in the panel's viewer, never in model context.
A web console as well. The same package installs pi-dispatch-console, a separate process that serves a live web
console for the deployment on loopback or on a private unix socket, behind a named token per person. It shows the
whole fleet as it changes: an overview of what needs you, what waits and why, and the last hours as a timeline (on a
deployment that has not run yet, what stands before its first job), every run with filters and what it loaded (a run that
finished without its flow or with a failed extension says so), one search by
repository, target or job id, the last 24 hours counted by outcome and paging, a card for each run waiting for an
answer, spend and insights by range, queues, hosts and schedules, the configuration, what a trigger's jobs get and what a project's runs load on each host, and its own audit
log, plus /api/state as JSON for scripts. It also has Models (roles, fallbacks and canary comparisons), Updates (what jobs run and what is
newer), Watch (findings from your alerts), Sandboxes (kept workspaces and shells, and what pi can load in one), Zero touch (how often runs needed a person) and
Setup (a live checklist for a new deployment). It reads through the same code as the panel, so the two never disagree;
the panel shows the waiting runs too.
It can act as well as show: cancel, retry, remove and re-run jobs, answer a parked run, pause the fleet or one queue,
change what a trigger's jobs get and edit the configuration, each previewed (with how to undo it), confirmed and recorded in one audit log that holds
the panel's own writes too, and after each change the page names it, who made it and its audit row, with an Undo
where there is a way back. When serve starts it prints how to sign in (the commands that make a token or a one time
link, never a token), and pi-dispatch-console link --name <you> --port <n> prints the link for an SSH tunnel's local
port. The sign-in page says why a session ended. Every running job's progress shows in its row on the fleet page
and on its run page, and its transcript, redacted, streams live
on its run page for operators, and the panel's live tail
(l) reads the same transcript for a run on any host. Nothing listens unless you start it. In pi,
/dispatch console [page] [--link] prints a page's address, says whether the console is running, makes a one time
sign-in link for an operator token (shown only to you, and recorded in the audit log first), and opens your browser
when the session is local (not over SSH; --no-open keeps it closed). See
docs/console.md.
More than one machine
The panel reads the whole deployment, not one host. The status line names the workers once they have
names. The run detail names the machine that ran a job. The pause switch stops every queue, and the
scheduler view spans hosts. On a single host none of that shows, because there is nothing to tell apart.
The HOSTS view (u) shows each machine's slots, budget and running jobs now and how busy it was over the last 7
days; pi-dispatch capacity, doctor, the insights page and dispatch_capacity read the same capacity report, from
the run records (jobs only: a machine busy with other work reads as idle).
See docs/multi-host.md.
Install
pi install npm:@edgehero/pi-dispatch-admin # then, in pi: /dispatch
This is the default way to set up pi-dispatch. With nothing configured, /dispatch offers
Finish setup in browser, checks Docker or rootless Podman (and OpenShell when configured), installs
exact worker, admin and pi packages in a durable deployment folder, then runs the consented
up --console pass. It offers a deployment pointer and opens /setup with a private one-use link.
The checklist uses actual heartbeat and doctor evidence, keeps Starting distinct from Ready, and offers
individually previewed and confirmed service actions, a create-only provider key field and editable
first-trigger recipes. First run explains the explicit local host grant,
read-only/viewer/agent views, key and invitation privacy, and terminal recovery.
Guided setup retains the terminal workflow: runtime check and pinned install, up, deployment pointer,
provider-key guidance, optional worker service, GitHub credentials, receiver or polling, and a first local
cron trigger. GitHub setup asks for a webhook URL or --no-webhook; browser App creation remains open.
Every step says what it will do, asks first and can be declined. Nothing is written into your repo, and no credential passes through a dialog. A deployment whose queue is only down keeps the unreachable banner: setup appears when there is nothing, never over an outage.
Already have a deployment? The panel finds it through the deployment pointer, or through the same env
vars your worker uses (VALKEY_URL, PI_LOGS_DIR, PI_SETTINGS_FILE, PI_TRIGGERS_FILE,
PI_PAUSE_WINDOWS_FILE, PI_SCOPED_LIMITS_FILE, PI_SUBSCRIPTIONS_FILE, PI_PROJECTS_FILE,
PI_ENVELOPE_FILE). Your env always wins. What your env does not set comes
from the deployment's own .env, by the same rule pi-dispatch doctor uses: only the pointer's
deployment folder (never a .env in the folder pi started in, which any repository could ship), and only
when that file is yours and nobody else can write it. A key your env sets differently is named once as a
warning, never with its value. The switches that grant a capability (PI_BACKENDS, PI_EGRESS,
DOCKER_HOST, PI_DISPATCH_RUN_ROOTS) are never read from a file.
Paid model starts need explicit valid PI_DISPATCH_RUN_ROOTS in the actual pi process environment.
Fire now and local or cron reruns also require explicit valid PI_DISPATCH_RUN_PER_HOUR; dispatch_run
defaults to three starts per hour when that value is absent or invalid, and zero disables it.
Headless mutation additionally requires
PI_DISPATCH_AGENT_WRITES=1. None is loaded from the deployment pointer or its .env. A reviewed external
skill may use private owner context to propose a permitted operation, but it cannot grant authority or
send an answer. Upgrade the host runtime and console together before using an agent token.
Get the whole thing
→ https://github.com/edgehero/pi-dispatch
MIT, self hosted. Read SECURITY.md
before you rely on it: it states plainly what is and is not defended. Short version: the trust model is a
GitHub Action's. Whoever can merge to your default branch can instruct the agent.
Configuration in the browser
The operator console includes a Pulse Configuration page, a page per trigger and a structured editor for five trigger kinds. Edit and Delete work from each row, with a diff, confirmation and Undo. Human row numbers count from 1; tool indexes remain zero-based. The editor and its JSON fallback preserve the add tool's secret-bundle opt-out.
What its jobs get, on a trigger's page, says what a job of it would start with on each host, with a label for each host, and under each row what a recent run really had.
There a person changes the flow, the provider and model, the job cap, the allowed models and fallbacks, the MCP servers, the tools, the all-or-nothing switch for staged packages, each extension the trigger leaves out and each extension's tool it removes, with one preview that says each change in words, one Confirm, and Undo.
An extension left out does not load, and a tool removed leaves the rest of its extension running. Neither stops what an agent that can run commands, write files or edit files can reach (what exclusion does not stop). Every running machine must run a release that can read these two settings: the upgrade notes say which release, and what to do until then. While a running worker cannot read them, the page refuses the change and names it.
A viewer reads the same rows with no controls, an agent's page draws no card, and no tool reaches the action. The file works as it
always has (docs/console.md).
dispatch_trigger_edit also takes maxTurns and the four run-option flags. dispatch_trigger_fire(index)
shares the console's Fire now planner, reserve and refusals. It fires a served cron trigger after the same fresh folder, committed flow, clean tree and hourly checks
as dispatch_run. Interactive callers confirm; headless callers need the process write grant and both
paid-start grant values in the pi or console process environment.
Viewers see trigger facts and runs without private prompt text or write controls.
The Triggers page groups rules by schedule and forge, with retained history, spend and a timezone-aware schedule view. Its phone and viewer layouts keep the same facts readable. Check an event uses the receiver filters to explain a hypothetical match without a forge call, queue write or spend. It is a viewer read, available even on a read-only console; entered permissions remain assumptions and comment text is neither saved nor logged. Rules and filters still work without JavaScript.
