@alphacoder-v0/pi-loops 0.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1989 -0
- package/LICENSE +21 -0
- package/README.md +370 -0
- package/README.zh-CN.md +606 -0
- package/docs/cli.md +252 -0
- package/docs/configuration.md +110 -0
- package/docs/design.md +155 -0
- package/docs/goal.md +59 -0
- package/docs/hooks.md +110 -0
- package/docs/loops.md +315 -0
- package/docs/mcp.md +90 -0
- package/docs/screenshot.png +0 -0
- package/docs/session-archive.md +64 -0
- package/docs/triggers.md +140 -0
- package/docs/troubleshooting.md +111 -0
- package/docs/web-ui-parity.md +305 -0
- package/examples/README.md +18 -0
- package/examples/mcp-notify-server.mjs +35 -0
- package/examples/mcp.toml +8 -0
- package/package.json +71 -0
- package/skills/pi-loops/SKILL.md +50 -0
- package/src/archive.ts +476 -0
- package/src/args.ts +217 -0
- package/src/cli-entry.mjs +23 -0
- package/src/cli.ts +610 -0
- package/src/config.ts +103 -0
- package/src/danger.ts +128 -0
- package/src/goal.ts +178 -0
- package/src/hooks.ts +436 -0
- package/src/host-control-channel.ts +243 -0
- package/src/host-control.ts +203 -0
- package/src/host-entry.mjs +14 -0
- package/src/host-runtime.ts +222 -0
- package/src/host.ts +329 -0
- package/src/inbox.ts +232 -0
- package/src/job-edit.ts +113 -0
- package/src/job-health.ts +29 -0
- package/src/lock.ts +193 -0
- package/src/log.ts +108 -0
- package/src/mcp-pool.ts +141 -0
- package/src/mcp.ts +885 -0
- package/src/paths.ts +33 -0
- package/src/pi-loops.ts +2292 -0
- package/src/pi-resolver.mjs +49 -0
- package/src/presence.ts +143 -0
- package/src/protocol.ts +198 -0
- package/src/redact.ts +61 -0
- package/src/register-pi.mjs +3 -0
- package/src/runner.ts +149 -0
- package/src/schedule.ts +431 -0
- package/src/scheduler.ts +981 -0
- package/src/sdk-runner.ts +536 -0
- package/src/share.ts +121 -0
- package/src/slots.ts +76 -0
- package/src/snapshot.ts +40 -0
- package/src/store.ts +599 -0
- package/src/subagent-guard.ts +47 -0
- package/src/thinking.ts +30 -0
- package/src/toml.ts +244 -0
- package/src/tools.ts +455 -0
- package/src/transcript.ts +84 -0
- package/src/trigger-runtime.ts +711 -0
- package/src/triggers.ts +558 -0
- package/src/trust.ts +47 -0
- package/src/ts-entry.mjs +67 -0
- package/src/ui-prefs.ts +84 -0
- package/src/version.ts +14 -0
- package/src/web.mjs +4484 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pi-loops authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# pi-loops
|
|
2
|
+
|
|
3
|
+
English · [中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
An automation layer for [pi](https://github.com/earendil-works/pi), shipped as a plain pi extension.
|
|
6
|
+
Nothing in pi is patched.
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
A loop wakes up with the notes its last run left, does the work in a sub-agent that starts clean,
|
|
11
|
+
and files what it found. You read the findings when you want to, and claim the ones worth a turn.
|
|
12
|
+
It all keeps running after you close pi.
|
|
13
|
+
|
|
14
|
+
## Why this exists
|
|
15
|
+
|
|
16
|
+
Some work should happen while nobody is watching: the issues opened overnight, the dependency that
|
|
17
|
+
picked up a CVE, the test that started failing on main. An agent can do any of it, but only when you
|
|
18
|
+
sit down and ask — so the asking is the work. And the two obvious ways to automate it put the result
|
|
19
|
+
in the wrong place: a prompt on a timer interrupts you with something you did not want *now*, and a
|
|
20
|
+
script writes to a log nobody opens.
|
|
21
|
+
|
|
22
|
+
> "Stop prompting the agent. Build loops that prompt the agent for you."
|
|
23
|
+
> — Addy Osmani, *Loop Engineering*
|
|
24
|
+
|
|
25
|
+
So the run happens in a sub-agent that never touches your conversation — nothing lands while you are
|
|
26
|
+
mid-thought. What it finds waits in an inbox rather than a log: an inbox line is one you can claim, a
|
|
27
|
+
log line is one you have to go looking for. And it starts from what the last run wrote down, so what
|
|
28
|
+
reaches you is what changed rather than everything that is there.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
Node ≥ 22.6 (pi loads the TypeScript sources directly) and a pi to load it into — 0.85 is what this
|
|
33
|
+
is tested against, and older ones are untested rather than refused. Nothing else: pi-loops has no
|
|
34
|
+
runtime dependencies. What you do want first is a provider you can actually talk to — run `pi`, send
|
|
35
|
+
one message, make sure you get an answer. pi-loops runs sub-agents while you are not watching, and
|
|
36
|
+
the first sign of credentials that do not work should not be an empty inbox tomorrow morning.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pi install git:github.com/alphacoder-v0/pi-loops@v0.17.3 # from GitHub, at a pinned tag
|
|
40
|
+
pi install /path/to/pi-loops # or a local checkout — `pi install .` in this repo
|
|
41
|
+
pi -e /path/to/pi-loops # or none of them: try it for one run, installing nothing
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Install one of them, not two. Two copies register the same tools, and pi refuses to load the
|
|
45
|
+
second — `Tool "cron_create" conflicts with …`, and it exits. If you are working on the code, the
|
|
46
|
+
checkout is the one to keep.
|
|
47
|
+
|
|
48
|
+
Then restart pi, and that is the whole install: `/cron`, `/inbox`, `/triggers` and `/goal` are
|
|
49
|
+
registered by the extension itself, so they work with nothing on your `PATH` and no launcher. The
|
|
50
|
+
`pi-loops` command is a separate thing, needed only for the browser window and the shell
|
|
51
|
+
subcommands — [The browser window, and the command line](#the-browser-window-and-the-command-line)
|
|
52
|
+
sets it up when you want it. The package also ships a skill (`skills/pi-loops`), so the agent knows
|
|
53
|
+
when to reach for `cron_create`, `new_trigger` and the inbox on its own.
|
|
54
|
+
|
|
55
|
+
## Your first loop
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
/cron add --stateful --name todo "0 9 * * *" read TODO.md and report any unchecked item that was not in your notes last run
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Every morning a fresh sub-agent runs with the notes it wrote last time, does the work, and hands
|
|
62
|
+
back two things: the notes for tomorrow, and any finding worth your attention. The notes go to a
|
|
63
|
+
Markdown file, the findings go to the inbox, and your conversation is never touched. (The run says
|
|
64
|
+
which is which in tags that a program reads rather than you — [docs/loops.md](docs/loops.md) has
|
|
65
|
+
them.) Here are two runs of that loop, half an hour apart — the first:
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
cron todo · 5s · $0.000 · 0 findings · state updated
|
|
69
|
+
md5 unchanged (`06ff2ec8af3668bb89ecc6580110ecad`), git rev still `15b6562`. No new unchecked items — nothing to report.
|
|
70
|
+
/cron trace todo · /inbox
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The second, after one commit:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
cron todo · 4s · $0.000 · 1 finding · state updated
|
|
77
|
+
md5 changed (06ff2ec8… → fe00aaad…). One new unchecked item appeared.
|
|
78
|
+
• TODO.md: new unchecked item — cache the /search results for 60s (commit 395a124 "todo: cache search results")
|
|
79
|
+
/cron trace todo · /inbox
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The first run found nothing worth saying and said nothing; the second noticed one change and
|
|
83
|
+
reported only that. That is what the notes between runs buy — without them every run reports the
|
|
84
|
+
whole file every morning, and you stop reading it by Thursday. What they filed waits in the inbox:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
/inbox
|
|
88
|
+
Inbox (/tmp/acme-api, 3 new, times +00:00):
|
|
89
|
+
1. [inb-aae794d5] TODO: rate-limit the /search endpoint (unchecked, no owner) (acme-api, cron:cron-b91def87, 2026-09-12 05:25)
|
|
90
|
+
2. [inb-3216a92d] TODO: retry the payment webhook on 5xx (unchecked, no owner) (acme-api, cron:cron-b91def87, 2026-09-12 05:25)
|
|
91
|
+
3. [inb-74b73c72] TODO.md: new unchecked item — cache the /search results for 60s (commit 395a124 "todo: cache search results") (acme-api, cron:todo, 2026-09-12 05:28)
|
|
92
|
+
claim with /inbox claim <n>, dismiss with /inbox dismiss <n>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The third finding is the run above; the two over it came from another loop in the same checkout,
|
|
96
|
+
three minutes earlier. The inbox is one queue per project rather than one per job, so everything
|
|
97
|
+
watching this repo lands in the same list and you triage it in one pass.
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
/cron # what is scheduled here, and when it next runs
|
|
101
|
+
/cron run 1 # do not wait until 9am — run it now and watch
|
|
102
|
+
/inbox claim 1 # hand finding #1 to the agent as a real turn
|
|
103
|
+
/inbox dismiss 2 # not interesting
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Loops worth stealing
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
/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
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The others are variations on this shape. The notes carry a revision, and only what changed since it
|
|
113
|
+
earns a line in your inbox.
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
/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
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The watermark here is a list rather than a revision, and the loop appends to it as it reports. The
|
|
120
|
+
notes are plain Markdown you can read and correct (`/cron state deps`).
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
/cron add --verify --name ci every 30m run the test suite and report only tests that changed status since your notes
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`--verify` implies `--stateful` and puts a second, adversarial sub-agent between the findings and
|
|
127
|
+
you: a flake that failed once is exactly what should be stopped there, with the reason in
|
|
128
|
+
`/cron trace ci 1 checker`. It fails open — if the checker itself breaks, the findings still reach
|
|
129
|
+
the inbox, marked unverified, because a broken checker must not silence the loop.
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
/cron add --stateful --cwd /srv/acme-api --model openai/gpt-5.5 "0 7 * * *" summarize what changed in this repo since your notes
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
A job records its directory and model at creation, so it is not tied to the window it was typed in:
|
|
136
|
+
`--cwd` runs it in another checkout (absolute, or relative to this project — no shell, so nothing
|
|
137
|
+
expands `~`), and `--model` pins it whatever this session is on (`/cron set <ref> --model -` unpins).
|
|
138
|
+
|
|
139
|
+
Two that are not loops:
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
/new-trigger when ~/build.done exists, run cargo test and show me the result
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
A condition instead of a clock: a sub-agent re-checks it every `[triggers] poll_interval_secs` (600
|
|
146
|
+
by default) and acts when it holds — once, unless you ask for a repeat.
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
/cron add in 45m remind me to look at the deploy
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
No `--stateful`, so this is a plain job: in 45 minutes the prompt lands in *this* conversation and
|
|
153
|
+
is answered there rather than filed — a reminder belongs in the chat, a nightly report does not.
|
|
154
|
+
|
|
155
|
+
## Before you leave it running overnight
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
/cron cost # what automation has spent today
|
|
159
|
+
/cron disable --all # pause every job in this project
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Set a cap in `~/.pi/agent/loops/config.toml` before you rely on it:
|
|
163
|
+
|
|
164
|
+
```toml
|
|
165
|
+
[limits]
|
|
166
|
+
daily_budget_usd = 5.0
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
When the last pi quits, a headless host takes over the clock so the 9am run happens whether or not
|
|
170
|
+
you are at the machine, and hands it back the next time you open pi (`/cron host`,
|
|
171
|
+
`pi-loops host status`). If you would rather it did not, put `[host] auto = false` in the same file.
|
|
172
|
+
|
|
173
|
+
## The browser window, and the command line
|
|
174
|
+
|
|
175
|
+
`pi-loops` is the part that needs a launcher: `pi install` puts the package under pi's managed
|
|
176
|
+
directory rather than on your `PATH`, so the command does not exist yet — the one thing
|
|
177
|
+
`install-launcher` cannot do for itself. Either way round works:
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
/pi-loops install-launcher # from inside pi, where the extension is already loaded
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# or from a shell, in the directory pi installed the package into
|
|
185
|
+
cd ~/.pi/agent/git/github.com/alphacoder-v0/pi-loops
|
|
186
|
+
node src/cli-entry.mjs install-launcher
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Either writes a launcher into the first of `~/.local/bin` and `/usr/local/bin` that is already on
|
|
190
|
+
your `PATH`, and refuses rather than guessing if neither is — pass `--dir <dir>` to say where.
|
|
191
|
+
After that, `pi-loops` works from anywhere:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
pi-loops # start a session — browser here, terminal over ssh
|
|
195
|
+
pi-loops --tui # the terminal one, when you want it
|
|
196
|
+
pi-loops --continue # pick up the newest session in this directory
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`pi-loops sessions|inspect|export|import` and `pi-loops host status|abort|stop` need no pi session
|
|
200
|
+
open at all. They are for backups from cron or CI, restoring on a fresh machine, and looking in on
|
|
201
|
+
the headless host. See [docs/cli.md](docs/cli.md).
|
|
202
|
+
|
|
203
|
+
At a local terminal the bare command opens the browser front end; over ssh, or with no terminal at
|
|
204
|
+
all, it runs pi itself, because a browser on the far machine helps nobody. Pass `--web` or `--tui`
|
|
205
|
+
when that guess is wrong. Anything else you pass goes straight to pi (`pi-loops --model
|
|
206
|
+
anthropic/claude-opus-5 -e .`).
|
|
207
|
+
|
|
208
|
+
Both windows are complete pi sessions — the browser one runs `pi --mode rpc` behind a page — so the
|
|
209
|
+
session file, `--resume`, your models, tools and extensions are the same either way, and the model
|
|
210
|
+
and thinking level you last chose start the next session whichever window it opens in. The browser
|
|
211
|
+
one is a session and not a viewer: what a person can still do after the window changed is a release
|
|
212
|
+
gate, and [docs/web-ui-parity.md](docs/web-ui-parity.md) has it line by line.
|
|
213
|
+
|
|
214
|
+
Even starting over stays in the window — **clear** begins a new session, **resume** goes back to an
|
|
215
|
+
earlier one in this project, **compact** summarises what is there and says what it did, as buttons
|
|
216
|
+
or as `/clear`, `/new`, `/resume` and `/compact <what to keep>` in the composer. None of them
|
|
217
|
+
deletes anything; the session you leave is a file that `resume` lists.
|
|
218
|
+
|
|
219
|
+
It serves **`http://127.0.0.1:4173/`** by default, with a token that lives in a file, so the address
|
|
220
|
+
is the same one tomorrow and is worth bookmarking (`--port` moves it if 4173 is spoken for). The
|
|
221
|
+
first visit leaves a cookie and you never see the token again. Running `pi-loops` while one is
|
|
222
|
+
already up opens that window instead of failing on the port; `--no-auth` drops even that, on a
|
|
223
|
+
machine only you use.
|
|
224
|
+
|
|
225
|
+
From a phone, the best route is `tailscale serve --bg 4173`: this server stays on loopback and the
|
|
226
|
+
tailnet does TLS and identity. On the same wifi, `pi-loops --host 0.0.0.0` works too (and refuses
|
|
227
|
+
`--no-auth`). Press **add device** in a browser that is already signed in and point the phone at the
|
|
228
|
+
QR it shows: nothing to type, that device stays signed in, and the page can go on the home screen.
|
|
229
|
+
|
|
230
|
+
### Keeping it up to date
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
pi-loops upgrade # take the newest release
|
|
234
|
+
pi-loops upgrade --check # just say whether there is one
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
It reads the release tags from the repository this copy came from, compares them with what you are
|
|
238
|
+
running, and installs the newest — which `pi update --extensions` will not do, because pi pins the
|
|
239
|
+
ref you asked for and keeps the clone on *that* ref ([docs/cli.md](docs/cli.md)).
|
|
240
|
+
|
|
241
|
+
Restart pi (or run `pi-loops` again) to load it. The launcher does not need reinstalling: pi keeps
|
|
242
|
+
each git package at `~/.pi/agent/git/<host>/<owner>/<repo>`, so a version change keeps the path.
|
|
243
|
+
|
|
244
|
+
Releases are tags on GitHub; [CHANGELOG.md](CHANGELOG.md) says what is in each one.
|
|
245
|
+
|
|
246
|
+
### Uninstall
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
pi remove git:github.com/alphacoder-v0/pi-loops # or the checkout path — whatever you installed
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
State stays in `~/.pi/agent/loops` until you delete it.
|
|
253
|
+
|
|
254
|
+
## Commands
|
|
255
|
+
|
|
256
|
+
| Command | What it does |
|
|
257
|
+
|---|---|
|
|
258
|
+
| `/cron add [--stateful] [--verify] "<schedule>" <prompt>` | Schedule a job: plain jobs inject their result into this chat, `--stateful` makes a loop with memory and inbox routing, `--verify` adds the checker |
|
|
259
|
+
| `/cron`, `/cron all` | This project's jobs, or every project on this machine |
|
|
260
|
+
| `/cron enable\|disable\|remove <ref>` | Pause, resume or delete one job — and write an audit entry into the session, as `add` does |
|
|
261
|
+
| `/cron disable --all` | Pause every job in this project (`--all-projects` for the machine); `/cron enable --all` resumes |
|
|
262
|
+
| `/cron run <ref>` | Fire one job now instead of waiting for its schedule |
|
|
263
|
+
| `/cron state <ref>` | The loop's notes — the Markdown it carries from one run to the next |
|
|
264
|
+
| `/cron runs [ref]` | The run log: when it fired, what it cost, what it found |
|
|
265
|
+
| `/cron trace <job> [k] [checker]` | The k-th latest run's full sub-agent transcript, maker or checker |
|
|
266
|
+
| `/cron set <job> …` | Change a job in place — `--prompt`, `--schedule`, model, thinking, timeout, name — keeping its id, and therefore its notes |
|
|
267
|
+
| `/cron cost [today\|7d\|all]` | What automation has cost, against `[limits] daily_budget_usd` |
|
|
268
|
+
| `/cron scheduler` | Which pi process currently owns the timer |
|
|
269
|
+
| `/cron host [start\|stop]` | The headless host that keeps the clock after the last pi quits |
|
|
270
|
+
| `/cron clear <ref>` | Release a run marker left behind by a process that is gone |
|
|
271
|
+
| `/cron gc` | Remove plain jobs whose session was deleted (they are parked as disabled first) |
|
|
272
|
+
| `/cron panel on\|off` | The side panel above the editor: Triggers, Inbox, Cron, MCP |
|
|
273
|
+
| `/cron snapshot` | Write what only this process knows — connected MCP servers and their tools, active tools, hooks, who owns the clock — into the session as a `pi_loops_snapshot` entry, for a front end that is not a terminal |
|
|
274
|
+
| `/inbox [list\|all\|claim <n>\|dismiss <n>\|clear] [--all]` | Triage findings from stateful loops. This project's by default, `--all` for every project — the same scoping `/cron` and `/triggers` use |
|
|
275
|
+
| `/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 |
|
|
276
|
+
| `/new-trigger <natural language>` | Create a condition-based rule ("when ~/build.done exists, run cargo test") |
|
|
277
|
+
| `/triggers [status\|rules\|enable\|disable\|remove\|running\|audit [N]\|abort]` | Dynamic rules: what exists, what is running, what happened |
|
|
278
|
+
| `/triggers run <id>` | Check one rule now instead of waiting for its poll slot |
|
|
279
|
+
| `/triggers set <id> --model\|--thinking\|--timeout\|--host` | Change what a rule runs with — the settings that decide how an unattended action behaves |
|
|
280
|
+
| `/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 |
|
|
281
|
+
| `/triggers panel [on\|off]` | The same panel, toggled from the trigger side |
|
|
282
|
+
| `/session-export [path]`, `/session-import <path>` | Portable `.pisession` archive: transcript + jobs + rules + loop state |
|
|
283
|
+
| `/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](docs/session-archive.md)) |
|
|
284
|
+
| `/pi-loops [install-launcher]` | Version and paths; `install-launcher` puts the `pi-loops` command on your `PATH` |
|
|
285
|
+
|
|
286
|
+
A schedule is a 5-field cron expression, or one of `hourly` / `daily` / `weekly` (also `每天`), or
|
|
287
|
+
`every 30m`, `in 10m`, `at <ISO>`. The `@hourly` / `@daily` / `@weekly` / `@monthly` spellings work
|
|
288
|
+
too, and they are not the same thing: `daily` is 09:00, `@daily` is midnight. All of it runs on this
|
|
289
|
+
machine's clock ([which clock, and what the twice-yearly change does to
|
|
290
|
+
it](docs/loops.md#time-and-which-clock-it-is)). A `<ref>` is the number on screen, the job id, an
|
|
291
|
+
unambiguous prefix of it, or the `--name` you gave it — and `/crontab` and `/loop` are aliases of
|
|
292
|
+
`/cron`.
|
|
293
|
+
|
|
294
|
+
Tools for the model: `cron_create`, `cron_list`, `cron_remove`, `set_cron_job_state`,
|
|
295
|
+
`new_trigger`, `list_triggers`, `remove_trigger`, `set_trigger_state`, plus every tool of every
|
|
296
|
+
configured MCP server. Five of them stop and ask you: creating or removing a trigger, removing a
|
|
297
|
+
cron job — `cron_remove` twice over, a preview the model must show you before the approval itself —
|
|
298
|
+
and any state change that turns automation *on*, or touches another project's. They are the
|
|
299
|
+
operations that decide what runs while nobody is watching.
|
|
300
|
+
|
|
301
|
+
## Documentation
|
|
302
|
+
|
|
303
|
+
Loops and the inbox are the centre of this. On the same clock there are also plain cron jobs that
|
|
304
|
+
inject a prompt into this chat, triggers that wait for a condition instead of a time, MCP push
|
|
305
|
+
notifications, and lifecycle hooks — each has a page here.
|
|
306
|
+
|
|
307
|
+
- [docs/loops.md](docs/loops.md) — cron jobs, stateful loops, the inbox, maker/checker
|
|
308
|
+
- [docs/triggers.md](docs/triggers.md) — dynamic triggers and the trigger runtime
|
|
309
|
+
- [docs/mcp.md](docs/mcp.md) — MCP notification sources and tool registration (`mcp.toml`)
|
|
310
|
+
- [docs/hooks.md](docs/hooks.md) — lifecycle hooks (`hooks.toml`), including `run_start` / `run_end` for scheduled runs
|
|
311
|
+
- [docs/goal.md](docs/goal.md) — `/goal`: holding a session to a stop condition
|
|
312
|
+
- [docs/session-archive.md](docs/session-archive.md) — `/session-export`, `/session-import`
|
|
313
|
+
- [docs/cli.md](docs/cli.md) — the `pi-loops` command line: export, import, and looking in on the host
|
|
314
|
+
- [docs/web-ui-parity.md](docs/web-ui-parity.md) — what the browser front end owes you, line by line
|
|
315
|
+
- [docs/configuration.md](docs/configuration.md) — paths, `config.toml`, flags, environment
|
|
316
|
+
- [docs/design.md](docs/design.md) — architecture: what each piece is built out of, and the decisions behind it
|
|
317
|
+
- [docs/troubleshooting.md](docs/troubleshooting.md) — symptom first: nothing fired, a run failed, findings never arrived, the window stopped answering
|
|
318
|
+
- [examples/](examples/README.md) — a dependency-free MCP push server to try notifications with
|
|
319
|
+
- [CHANGELOG.md](CHANGELOG.md), [AGENTS.md](AGENTS.md) for contributors
|
|
320
|
+
|
|
321
|
+
## Where things live
|
|
322
|
+
|
|
323
|
+
| Path | What |
|
|
324
|
+
|---|---|
|
|
325
|
+
| `~/.pi/agent/loops/jobs.json` | cron jobs (machine-global, each with its `cwd`) |
|
|
326
|
+
| `~/.pi/agent/loops/state/<id>.md` | loop notes — plain Markdown |
|
|
327
|
+
| `~/.pi/agent/loops/inbox.jsonl` | the inbox |
|
|
328
|
+
| `~/.pi/agent/loops/runs.jsonl`, `sessions/<id>/` | run log and full sub-agent transcripts |
|
|
329
|
+
| `~/.pi/agent/loops/logs/pi-<pid>.log` | what each pi process's automation did |
|
|
330
|
+
| `~/.pi/agent/loops/triggers.json`, `triggers-audit.jsonl` | dynamic rules and trigger audit |
|
|
331
|
+
| `~/.pi/agent/loops/{config,mcp,hooks}.toml` | configuration |
|
|
332
|
+
| `~/.pi/agent/loops/scheduler.<host>.json` | which pi process currently owns the timer |
|
|
333
|
+
|
|
334
|
+
Set `PI_LOOPS_DIR` to relocate all of it. The notes are yours to edit when the agent got something
|
|
335
|
+
wrong, and `logs/pi-<pid>.log` is the file to read after an overnight failure.
|
|
336
|
+
|
|
337
|
+
## Automation outlives the window it was set up in
|
|
338
|
+
|
|
339
|
+
Scheduled work is only worth trusting if it survives you closing the editor. So "pi was restarted"
|
|
340
|
+
is treated here as the normal case rather than the exception.
|
|
341
|
+
|
|
342
|
+
Jobs are machine-global, recorded per host, and never expire. Any open pi can own the timer:
|
|
343
|
+
leadership is a file with a heartbeat, and when the process holding it exits or dies, the next tick
|
|
344
|
+
in another window picks it up. A project's trigger checks run in a pi that is open in that project,
|
|
345
|
+
so a result that belongs in a conversation lands in the right one. A tick a loop missed while
|
|
346
|
+
nothing was running is caught up once, collapsed rather than replayed; a plain job's is not, because
|
|
347
|
+
its prompt was written for a conversation that is gone (`--catchup` and `--no-catchup` override
|
|
348
|
+
either way).
|
|
349
|
+
|
|
350
|
+
Sub-agents are sessions opened inside the interactive pi through its SDK, not child processes: they
|
|
351
|
+
share its live MCP servers — the browser tab that is already logged in, the database session that is
|
|
352
|
+
already open — along with its extensions, model and thinking level. The headless host has no parent
|
|
353
|
+
session to share, so its runs connect their own MCP clients from `mcp.toml`: a server that was live
|
|
354
|
+
only because you had a window open is not live at 3am.
|
|
355
|
+
|
|
356
|
+
[docs/design.md](docs/design.md) has the architecture and the reasoning behind each of these.
|
|
357
|
+
|
|
358
|
+
## Non-invasive by construction
|
|
359
|
+
|
|
360
|
+
Only pi's public extension API is used. `find <pi install> -newer package.json` is empty after
|
|
361
|
+
installing pi-loops; `~/.pi/agent` gains one `packages` entry and the `loops/` directory.
|
|
362
|
+
Uninstalling is `pi remove`.
|
|
363
|
+
|
|
364
|
+
## Acknowledgements
|
|
365
|
+
|
|
366
|
+
Inspired by, and rewritten from, [pie](https://github.com/c4pt0r/pie).
|
|
367
|
+
|
|
368
|
+
## License
|
|
369
|
+
|
|
370
|
+
MIT
|