@alphacoder-v0/pi-loops
An automation layer for pi: cron + stateful loops with a triage inbox, maker/checker verification, dynamic triggers, MCP notifications and tools, lifecycle hooks, session archives. A non-invasive pi extension.
Package details
Install @alphacoder-v0/pi-loops from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@alphacoder-v0/pi-loops- Package
@alphacoder-v0/pi-loops- Version
0.24.2- Published
- Nov 11, 2026
- Downloads
- 909/mo · 85/wk
- Author
- alphacoder-v1
- License
- MIT
- Types
- extension, skill
- Size
- 2.6 MB
- Dependencies
- 0 dependencies · 4 peers
Pi manifest JSON
{
"image": "https://raw.githubusercontent.com/alphacoder-v0/pi-loops/main/docs/screenshot.png",
"skills": [
"./skills"
],
"extensions": [
"./src/extension-entry.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-loops
English · 中文
An automation layer for pi, shipped as a plain pi extension. Nothing in pi is patched.

A loop wakes up with the notes its last run left, does the work in a sub-agent that starts clean, and files what it found. You read the findings when you want to, and claim the ones worth a turn. It all keeps running after you close pi.
Why this exists
Some work should happen while nobody is watching: the issues opened overnight, the dependency that picked up a CVE, the test that started failing on main. An agent can do any of it, but only when you sit down and ask — so the asking is the work. And the two obvious ways to automate it put the result in the wrong place: a prompt on a timer interrupts you with something you did not want now, and a script writes to a log nobody opens.
"Stop prompting the agent. Build loops that prompt the agent for you." — Addy Osmani, Loop Engineering
So the run happens in a sub-agent that never touches your conversation — nothing lands while you are mid-thought. What it finds waits in an inbox rather than a log: an inbox line is one you can claim, a log line is one you have to go looking for. And it starts from what the last run wrote down, so what reaches you is what changed rather than everything that is there.
Install
Node ≥ 22.6 (pi loads the TypeScript sources directly) and pi ≥ 0.84.3 to load it into — 0.85 is
what this is tested against, and an older pi is refused at load with a line that names the version
it needs, rather than a link error naming one missing export. The process that runs your loops always
runs on node, also when pi runs under bun: it then uses the node on your PATH. Nothing else:
pi-loops has no runtime dependencies. What you do want first is a provider you can actually talk to — run pi, send
one message, make sure you get an answer. pi-loops runs sub-agents while you are not watching, and
the first sign of credentials that do not work should not be an empty inbox tomorrow morning.
pi install npm:@alphacoder-v0/pi-loops
Working on the code? Install your checkout instead — pi install . in this repo — or try it for
one run with pi -e ., which installs nothing. Keep one copy, not two: two copies register the
same tools, and pi refuses to load the second — Tool "cron_create" conflicts with …, and it
exits.
Then start pi once, and that is the whole install. That first session puts the pi-loops command
on your PATH and says so in one line, so from then on pi-loops works from any shell (see
The browser window, and the command line). /cron,
/inbox, /triggers and /goal are registered by the extension itself, so they work with or
without that command. The package also ships a skill (skills/pi-loops), so the agent knows
when to reach for cron_create, new_trigger and the inbox on its own.
Your first loop
/cron add --stateful --name todo "0 9 * * *" read TODO.md and report any unchecked item that was not in your notes last run
Every morning a fresh sub-agent runs with the notes it wrote last time, does the work, and hands back two things: the notes for tomorrow, and any finding worth your attention. The notes go to a Markdown file and the findings go to the inbox. (The run says which is which in tags that a program reads rather than you — docs/loops.md has them.) Your conversation gets one line for each of the loop's first three runs, so you see that it works, and one for any run that fails; every other run is a line in the panel above the editor and in the page's loop activity strip. Here are two runs of that loop, half an hour apart, as the panel lists them:
Runs
09:30 todo · 4s · 1 finding
09:00 todo · 5s · 0 findings
The first run found nothing worth saying and filed nothing; the second noticed one change, a new unchecked item in TODO.md, and filed only that. That is what the notes between runs buy — without them every run reports the whole file every morning, and you stop reading it by Thursday. What they filed waits in the inbox:
/inbox
Inbox (/tmp/acme-api, 3 new, times +00:00):
1. [inb-aae794d5] TODO: rate-limit the /search endpoint (unchecked, no owner) (acme-api, cron-b91def87, 2026-09-12 05:25)
2. [inb-3216a92d] TODO: retry the payment webhook on 5xx (unchecked, no owner) (acme-api, cron-b91def87, 2026-09-12 05:25)
3. [inb-74b73c72] TODO.md: new unchecked item — cache the /search results for 60s (commit 395a124 "todo: cache search results") (acme-api, todo, 2026-09-12 05:28)
The third finding is the run above; the two over it came from another loop in the same checkout, three minutes earlier. The inbox is one queue per project rather than one per job, so everything watching this repo lands in the same list and you triage it in one pass.
/cron # what is scheduled here, and when it next runs
/cron run 1 # do not wait until 9am — run it now and watch
/inbox claim 1 # hand finding #1 to the agent as a real turn
/inbox dismiss 2 # not interesting
/inbox dismiss 3 that file is generated, ignore it # the loop is told why on its next run
Loops worth stealing
/cron add --stateful --name main-watch "0 9 * * *" read the commits on main since the revision in your notes, report anything that changes the public API, and record the new head revision
The others are variations on this shape. The notes carry a revision, and only what changed since it earns a line in your inbox.
/cron add --stateful --name deps "0 8 * * 1" run npm audit and report advisories whose id is not already in your notes; append every id you report to that list
The watermark here is a list rather than a revision, and the loop appends to it as it reports. The
notes are plain Markdown you can read and correct (/cron state deps).
/cron add --verify --name ci every 30m run the test suite and report only tests that changed status since your notes
--verify implies --stateful and puts a second, adversarial sub-agent between the findings and
you: a flake that failed once is exactly what should be stopped there, with the reason in
/cron trace ci 1 checker. It fails open — if the checker itself breaks, the findings still reach
the inbox, marked unverified, because a broken checker must not silence the loop.
/cron add --stateful --cwd /srv/acme-api --model openai/gpt-5.5 "0 7 * * *" summarize what changed in this repo since your notes
A job records its directory and model at creation, so it is not tied to the window it was typed in:
--cwd runs it in another checkout (absolute, or relative to this project — no shell, so nothing
expands ~), and --model pins it whatever this session is on (/cron set <ref> --model - unpins).
And eight you do not write at all: /recipe add issue-loop runs the issue tracker as a state
machine that two loops turn, /recipe add autoresearch runs one experiment per run against a
contract you wrote, and daily-digest, pr-watch, ci-sweeper, deps-sweeper,
changelog-draft and ecosystem do what their names say — each a directory of playbooks copied
into the project, installed with one question and one confirmation, and /recipe show says
what its runs may never do before you say yes (docs/recipes.md). Three are
starters that only read and file findings: daily-digest, pr-watch, changelog-draft.
Two that are not loops:
/new-trigger when ~/build.done exists, run cargo test and show me the result
A condition instead of a clock: a sub-agent re-checks it every [triggers] poll_interval_secs (600
by default) and acts when it holds — once, unless you ask for a repeat.
/cron add in 45m remind me to look at the deploy
No --stateful, so this is a plain job: in 45 minutes the prompt lands in this conversation and
is answered there rather than filed — a reminder belongs in the chat, a nightly report does not.
Before you leave it running overnight
/cron cost # what automation has spent today
/cron disable --all # pause every job in this project
Set a cap in ~/.pi/agent/loops/config.toml before you rely on it:
[limits]
daily_budget_usd = 5.0
A pi window starts pi-loops in the background when there is work to run. It runs every loop, rule
check and push, whether or not a window is open. It also runs the plain jobs of closed
conversations, in those conversations; each such run is a model turn. A window runs the plain jobs
of the conversation it holds. After a reboot, work resumes when a pi window opens, or at login if
you run pi-loops start --foreground from your own systemd unit (docs/cli.md).
/pi-loops status shows its status; [host] auto = false disables automatic starts.
The browser window, and the command line
pi-loops is the part that needs a launcher: pi install puts the package under pi's managed
directory rather than on your PATH. You do not fix that by hand. The first interactive pi session
after pi install writes pi-loops into the first of ~/.local/bin and /usr/local/bin that is
already on your PATH, says so in one line, and keeps that launcher working afterwards if pi ever
moves the package:
[pi-loops] added pi-loops to ~/.local/bin; /pi-loops uninstall-launcher removes it
It never replaces a pi-loops it did not write. When neither directory is on your PATH, or on a
machine where pi itself is never opened, write it yourself and pass --dir <dir> to say where:
/pi-loops install-launcher --dir ~/bin # from inside pi, where the extension is already loaded
# or from a shell — the same command, by absolute path
cd ~/.pi/agent/npm/node_modules/@alphacoder-v0/pi-loops # installed from npm
cd ~/.pi/agent/git/github.com/alphacoder-v0/pi-loops # installed from GitHub
node src/cli-entry.mjs install-launcher --dir ~/bin
After that, pi-loops works from anywhere:
pi-loops # start a session — browser here, terminal over ssh
pi-loops --tui # the terminal one, when you want it
pi-loops --continue # pick up the newest session in this directory
pi-loops sessions|inspect|export|import and pi-loops start|stop|restart|status|abort need no
pi session open at all. They are for backups from cron or CI, restoring on a fresh machine, and
looking in on pi-loops, which the page also shows as its Background view (pi-loops status --url). See docs/cli.md.
At a local terminal the bare command opens the browser page; over ssh, or with no terminal at
all, it runs pi itself, because a browser on the far machine helps nobody. Pass --web or --tui
when that guess is wrong. Anything else you pass goes straight to pi (pi-loops --model anthropic/claude-opus-5 -e .).
Both windows are complete pi sessions — each conversation in the page is a pi --mode rpc that
pi-loops runs for it — so the session file, --resume, your models, tools and extensions are the same either way, and the model
and thinking level you last chose start the next session whichever window it opens in. The browser
one is a session and not a viewer: what a person can still do after the window changed is a release
gate, and docs/web-ui-parity.md has it line by line.
Even starting over stays in the window — clear begins a new session, resume goes back to an
earlier one in this project, compact summarises what is there and says what it did, as buttons
or as /clear, /new, /resume and /compact <what to keep> in the composer. None of them
deletes anything; the session you leave is a file that resume lists.
The page is served by pi-loops in the background on http://127.0.0.1:4173/, so pi-loops
opens it and exits, and the page stays open when that terminal closes. Its token lives in a file, so
the address is the same one tomorrow and is worth bookmarking ([page] port in config.toml moves
it if 4173 is spoken for). pi-loops prints one link with a one-use key and opens your browser on
it, on macOS, Windows, WSL and Linux; when no browser opens, paste that link. The first visit leaves
a cookie and you never see the token again. There is no way to drop the token: the page is always
on, so it is always behind it.
A browser that shows "This browser has not been here before." needs a fresh link: run
pi-loops open in a terminal, or /pi-loops web inside pi, which also copies the link to the
clipboard and needs no pi-loops on your PATH.
From a phone, the best route is tailscale serve --bg 4173: this server stays on loopback and the
tailnet does TLS and identity. On the same wifi, [page] host = "0.0.0.0" in config.toml works
too. Press add device in a browser that is already signed in and point the phone at the
QR it shows: nothing to type, that device stays signed in, and the page can go on the home screen.
Keeping it up to date
pi-loops upgrade # take the newest release
pi-loops upgrade --check # just say whether there is one
It reads the release tags from the repository this copy came from, compares them with what you are
running, and installs the newest the same way this copy was installed. For a GitHub install that is
the only way to move: pi update --extensions keeps the clone on the ref you pinned
(docs/cli.md). An npm install made without a version is one pi update --extensions
moves as well, and upgrade leaves it that way; one pinned to an exact version stays pinned, to the
new one.
Restart pi (or run pi-loops again) to load it. The launcher does not need reinstalling: pi keeps a
package at the same path whatever its version — ~/.pi/agent/npm/node_modules/<name> or
~/.pi/agent/git/<host>/<owner>/<repo>.
Releases are tags on GitHub, published to npm under the same version; CHANGELOG.md says what is in each one.
Uninstall
Stop pi-loops and remove the pi-loops launcher before removing the extension. pi-loops keeps
running in the background after the extension is removed, and nothing else stops it; the launcher
needs the package to run. uninstall-launcher removes only a launcher that pi-loops wrote:
pi-loops stop
pi-loops uninstall-launcher # add --dir <dir> if install-launcher --dir <dir> wrote it
pi remove npm:@alphacoder-v0/pi-loops # or git:github.com/alphacoder-v0/pi-loops, or the checkout path — whatever you installed
State stays in ~/.pi/agent/loops until you delete it. After a reinstall, /pi-loops install-launcher
in pi puts the command back, because a removed launcher is not written again by itself.
To go back to 0.22.8, stop pi-loops while this version is still installed, then install 0.22.8:
pi-loops stop
pi install npm:@alphacoder-v0/pi-loops@<version> # <version> is 0.22.8
0.22.8 reads your jobs and rules as they are. It does not run the plain jobs of closed
conversations, and it ignores the files that only later versions write, so nothing still waiting
for a conversation is delivered: rule results, plain-job prompts waiting for their conversation, claimed
findings, /goal continuations, recipe hand-offs, inject_and_run pushes and messages sent from
the browser. Let them drain first (pi-loops status lists them). It also ignores the built-in
tools later versions record on a loop, so a loop made in a chat with fewer of them runs with pi's
default tools; and it has no window guard, so a plain job's prompt in an open window runs as your
own message would, with no question before a dangerous command.
The stop is remembered in ~/.pi/agent/loops/ui.json, and 0.22.8 keeps it. After you upgrade again,
pi-loops stays stopped until pi-loops start, or until pi-loops opens the browser page, which starts it.
Commands
| Command | What it does |
|---|---|
/cron add [--stateful] [--verify] "<schedule>" <prompt> |
Schedule a job: a plain job belongs to this chat and injects its result here; --stateful makes a loop that runs in the background, with memory and inbox routing; --verify adds the checker |
/cron, /cron all |
This project's jobs, or every project on this machine |
/cron enable|disable|remove <ref> |
Pause, resume or delete one job — and write an audit entry into the session, as add does. remove takes a name of this project or the exact id of a job anywhere, so a name never reaches a checkout you are not in |
/cron disable --all |
Pause every job in this project (--all-projects for the machine); /cron enable --all resumes |
/cron run <ref> |
Ask pi-loops to run a loop now; plain jobs run now only in the window holding their conversation |
/cron state <ref> |
The loop's notes — the Markdown it carries from one run to the next |
/cron runs [ref] |
The run log: when it fired, what it cost, what it found |
/cron trace <job> [k] [checker] |
The k-th latest run's full sub-agent transcript, maker or checker |
/cron set <job> … |
Change a job in place — --prompt, --schedule, model, thinking, timeout, name — keeping its id, and therefore its notes |
/cron cost [today|7d|all] |
What automation has cost, against [limits] daily_budget_usd |
/cron scheduler |
This window, its conversation, and pi-loops status |
/pi-loops start|stop|restart|status |
Start, stop, restart or show pi-loops |
/cron clear <ref> |
Release a run marker left behind by a process that is gone |
/cron gc |
Remove plain jobs whose session was deleted (they are parked as disabled first) |
/cron panel on|off |
The side panel above the editor: Rules, Loops, Runs, MCP |
/cron snapshot |
Write what only this process knows — connected MCP servers and their tools, active tools, hooks, whether pi-loops is running and its pid — into the session as a pi_loops_snapshot entry, for a front end that is not a terminal |
/inbox [list|all|claim <n>|dismiss <n> [reason]|clear] [--all] |
Triage findings from stateful loops. This project's by default, --all for every project — the same scoping /cron and /triggers use. A reason given with dismiss is shown to the loop's next run |
/goal [<condition>], /goal pause|resume|clear |
Hold the session to a stop condition: after every turn an evaluator with no tools decides whether it is met, and sends the agent back to work if not (max 8 continuations). Bare /goal shows the one in force |
/new-trigger <natural language> |
Create a condition-based rule ("when ~/build.done exists, run cargo test") |
/triggers [status|rules|enable|disable|remove|running|audit [N]|abort] |
Dynamic rules: what exists, what is running, what happened |
/triggers run <id> |
Check one rule now instead of waiting for its poll slot |
/triggers set <id> --model|--thinking|--timeout |
Change what a rule runs with — the settings that decide how an unattended action behaves |
/triggers sources, /triggers hooks |
Every source feeding the trigger runtime: each connected MCP server, whose notifications can fire a rule; the local crontab; the dynamic checker |
/triggers panel [on|off] |
The same panel, toggled from the trigger side |
/session-export [path], /session-import <path> |
Portable .pisession archive: transcript + jobs + rules + loop state (in the browser front end, /session export and /session import too; /sessions lists) |
/session-share [--public] |
Upload a redacted transcript as a GitHub gist via gh, after showing you what it contains. (pi has its own /share, which sends the raw session elsewhere first — see docs/session-archive.md) |
/recipe [list|show|add|update|remove <name>] |
Install a packaged way of running this project on loops — the issue tracker as a state machine, or one research experiment per run — with one question and one confirmation (docs/recipes.md) |
/pi-loops web |
Open this conversation in the browser page, and copy its one-use link |
/pi-loops [install-launcher | uninstall-launcher] |
Version and paths; install-launcher puts the pi-loops command on your PATH, uninstall-launcher removes it |
A schedule is a 5-field cron expression, or one of hourly / daily / weekly (also 每天), or
every 30m, in 10m, at <ISO>. The @hourly / @daily / @weekly / @monthly spellings work
too, and they are not the same thing: daily is 09:00, @daily is midnight. All of it runs on this
machine's clock (which clock, and what the twice-yearly change does to
it). A <ref> is the number on screen, the job id, an
unambiguous prefix of it, or the --name you gave it — and /crontab and /loop are aliases of
/cron.
Tools for the model: cron_create, cron_list, cron_remove, set_cron_job_state,
new_trigger, list_triggers, remove_trigger, set_trigger_state, plus every tool of every
configured MCP server. Five of them stop and ask you, once each: creating or removing a trigger,
removing a cron job, and any state change that turns automation on, or touches another project's. They are the
operations that decide what runs while nobody is watching.
Documentation
Loops and the inbox are the centre of this. On the same clock there are also plain cron jobs that inject a prompt into this chat, triggers that wait for a condition instead of a time, MCP push notifications, and lifecycle hooks — each has a page here.
- docs/loops.md — cron jobs, stateful loops, the inbox, maker/checker
- docs/triggers.md — dynamic triggers and the trigger runtime
- docs/mcp.md — MCP notification sources and tool registration (
mcp.toml) - docs/hooks.md — lifecycle hooks (
hooks.toml), includingrun_start/run_endfor scheduled runs - docs/goal.md —
/goal: holding a session to a stop condition - docs/recipes.md —
/recipe: a project run on loops, installed in one command; the issue loop and autoresearch, and how to write your own - docs/session-archive.md —
/session-export,/session-import - docs/cli.md — the
pi-loopscommand line: export, import, the inbox, and looking in on pi-loops - docs/downstream.md — what a program may depend on: a recipe directory,
run_start/run_end,pi-loops inbox --json; nothing else - docs/web-ui-parity.md — what the browser front end owes you, line by line
- docs/configuration.md — paths,
config.toml, flags, environment - docs/design.md — architecture: what each piece is built out of, and the decisions behind it
- docs/troubleshooting.md — symptom first: nothing fired, a run failed, findings never arrived, the window stopped answering
- examples/ — a dependency-free MCP push server to try notifications with
- CHANGELOG.md, AGENTS.md for contributors
Where things live
| Path | What |
|---|---|
~/.pi/agent/loops/jobs.json |
cron jobs (machine-global, each with its cwd) |
~/.pi/agent/loops/state/<id>.md |
loop notes — plain Markdown |
~/.pi/agent/loops/inbox.jsonl |
the inbox |
~/.pi/agent/loops/runs.jsonl, sessions/<id>/ |
run log and full sub-agent transcripts |
~/.pi/agent/loops/logs/pi-<pid>.log |
what each pi process's automation did |
~/.pi/agent/loops/triggers.json, triggers-audit.jsonl |
dynamic rules and trigger audit |
~/.pi/agent/loops/{config,mcp,hooks}.toml |
configuration |
~/.pi/agent/loops/host-service.json, host.log |
pi-loops record and log |
~/.pi/agent/loops/leases/, requests/ |
where each conversation is open, and what waits to be delivered to it |
~/.pi/agent/loops/pages/ |
one record per conversation open in the browser page: its pi's pid, and when a tab last used it |
docs/configuration.md lists every file. Set PI_LOOPS_DIR to relocate all of it. The notes are yours to edit when the agent got something
wrong, and logs/pi-<pid>.log is the file to read after an overnight failure.
Automation outlives the window it was set up in
Scheduled work is only worth trusting if it survives you closing the editor. So "pi was restarted" is treated here as the normal case rather than the exception.
Jobs are machine-global and never expire. A window starts pi-loops in the background when there is work to run. It runs stateful loops, rule checks and MCP pushes;
a window runs the plain jobs of the conversation open in it, and pi-loops runs those of
closed conversations. Rule results go to the conversation that created the rule; if it is closed,
pi-loops appends the result to it. The inbox gets a result only when that conversation cannot be
written. A tick a loop
missed while pi-loops was not running is caught up once, collapsed rather than replayed; a plain
job's is not, because its prompt was written for a conversation that has moved on (--catchup and
--no-catchup override either way).
Sub-agents are sessions opened through pi's SDK inside the process that runs them, not child
processes. Loops and rule checks run in the background, which has no window to share: their runs connect
their own MCP clients from mcp.toml and the project's own servers, so a server that is live only
because you had a window open is not live to a loop at 3am.
docs/design.md has the architecture and the reasoning behind each of these.
Non-invasive by construction
Only pi's public extension API is used. find <pi install> -newer package.json is empty after
installing pi-loops; ~/.pi/agent gains one packages entry and the loops/ directory.
Uninstalling is pi remove.
Acknowledgements
Inspired by, and rewritten from, pie.
License
MIT
