@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.

Packages

Package details

extensionskill

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.

The pi-loops page: a decision and two findings from the project's loops beside a conversation, and the two loops on the right

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), including run_start / run_end for 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-loops command 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