@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

Packages

Package details

extensionskill

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 /dispatch panel, 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_TURNS and 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. No dispatch_* 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). Enter on a run opens its full record, x on a running job cancels it after asking, and b on 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, and Esc lands on its row, where t and T steer 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 /insights in the web console (below); press i (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.disarmed mark 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 reviewed triggers.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 on progress (the job gets a report_progress tool to name its phases) and statusComment (one forge comment the worker edits as the run goes, numbers only), and recover (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 exact dispatch_limit_edit call that applies it. dispatch_limit_add and dispatch_limit_edit take 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. j shows each project with its members and this month's spend, and Enter on one filters the runs list. dispatch_projects lists them, and dispatch_project_add, dispatch_project_edit and dispatch_project_delete change projects.json behind your confirm (docs/projects.md).
  • Hosts. u shows 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 on u in 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_capacity returns 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), b in the list shows each project's share and spend, the applied plan with the reasons it gave, and the history. r on a history row reverts to it after a yes or no. dispatch_priorities_set sets a plan of weights inside the envelope, confirmed in an interactive session or delegated headless with the explicit process grant; dispatch_envelope_set changes the envelope behind your confirm; dispatch_allocations reads the split. pi's own write and edit tools are blocked on the envelope, projects.json, scoped-limits.json, triggers.json and the settings file (docs/allocation.md). /dispatch priorities shows the split as text, and /dispatch priorities set shop=3 platform=1 sets 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.waitFor holds 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_waits lists held jobs and dispatch_wait_cancel stops 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=1 in 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 no dispatch_* tool can set a trigger's job cap, allowed models, fallbacks, MCP servers, tool exclusions, staged packages, extension exclusions or extension tool exclusions, while dispatch_trigger_edit can still move its flow, provider and model behind a confirm. A pi process's own bash, write and edit tools 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 valid PI_DISPATCH_RUN_ROOTS in the pi process environment. Fire now and reruns also require an explicit valid PI_DISPATCH_RUN_PER_HOUR. dispatch_run keeps 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's ai-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. Read SECURITY.md before enabling it. The bundled operate-pi-dispatch skill 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.