@alphacoder-v0/pi-loops 0.22.8 → 0.24.0

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.
Files changed (91) hide show
  1. package/CHANGELOG.md +664 -0
  2. package/CONTRACT.md +7 -7
  3. package/README.md +91 -63
  4. package/README.zh-CN.md +59 -39
  5. package/docs/cli.md +272 -83
  6. package/docs/configuration.md +86 -24
  7. package/docs/design.md +395 -56
  8. package/docs/downstream.md +2 -2
  9. package/docs/goal.md +12 -3
  10. package/docs/hooks.md +6 -7
  11. package/docs/loops.md +288 -59
  12. package/docs/mcp.md +22 -14
  13. package/docs/recipes.md +6 -0
  14. package/docs/screenshot.png +0 -0
  15. package/docs/triggers.md +120 -26
  16. package/docs/troubleshooting.md +37 -26
  17. package/docs/web-ui-parity.md +691 -65
  18. package/package.json +4 -3
  19. package/recipes/autoresearch/recipe.toml +1 -0
  20. package/recipes/changelog-draft/recipe.toml +1 -0
  21. package/recipes/ci-sweeper/recipe.toml +1 -0
  22. package/recipes/daily-digest/recipe.toml +1 -0
  23. package/recipes/deps-sweeper/recipe.toml +1 -0
  24. package/recipes/ecosystem/recipe.toml +1 -0
  25. package/recipes/issue-loop/recipe.toml +1 -0
  26. package/recipes/pr-watch/recipe.toml +1 -0
  27. package/skills/pi-loops/SKILL.md +6 -4
  28. package/src/activity.ts +199 -0
  29. package/src/archive.ts +21 -9
  30. package/src/args.ts +6 -2
  31. package/src/cli-entry.mjs +12 -3
  32. package/src/cli.ts +746 -125
  33. package/src/config.ts +53 -5
  34. package/src/conversation-rows.ts +469 -0
  35. package/src/danger.ts +227 -6
  36. package/src/engine.ts +1240 -0
  37. package/src/goal.ts +71 -0
  38. package/src/held-read.ts +27 -0
  39. package/src/hooks.ts +49 -18
  40. package/src/host-control-channel.ts +455 -40
  41. package/src/host-control.ts +602 -111
  42. package/src/host-entry.mjs +5 -3
  43. package/src/host-runtime.ts +455 -42
  44. package/src/host.ts +659 -123
  45. package/src/inbox-card.ts +278 -0
  46. package/src/inbox.ts +139 -62
  47. package/src/job-edit.ts +7 -1
  48. package/src/job-health.ts +14 -2
  49. package/src/job-lines.ts +201 -13
  50. package/src/job-owner.ts +74 -13
  51. package/src/job-tune.ts +110 -0
  52. package/src/lease.ts +537 -0
  53. package/src/lock.ts +7 -2
  54. package/src/mcp-pool.ts +66 -14
  55. package/src/mcp.ts +1 -1
  56. package/src/open-file.ts +54 -0
  57. package/src/page-children.ts +1507 -0
  58. package/src/page-devices.ts +192 -0
  59. package/src/page-server.ts +3150 -0
  60. package/src/path-precheck.ts +215 -0
  61. package/src/paths.ts +24 -0
  62. package/src/pi-floor.ts +10 -0
  63. package/src/pi-loops.ts +1301 -450
  64. package/src/pi-resolver.d.mts +1 -0
  65. package/src/pi-resolver.mjs +110 -10
  66. package/src/presence.ts +12 -63
  67. package/src/protocol.ts +10 -1
  68. package/src/recipe.ts +43 -1
  69. package/src/requests.ts +354 -0
  70. package/src/rule-lines.ts +68 -0
  71. package/src/run-card.ts +105 -0
  72. package/src/runner.ts +18 -1
  73. package/src/schedule.ts +1 -2
  74. package/src/scheduler.ts +595 -245
  75. package/src/sdk-runner.ts +321 -27
  76. package/src/seen.ts +278 -0
  77. package/src/session-head.ts +21 -0
  78. package/src/share.ts +12 -2
  79. package/src/snapshot.ts +3 -2
  80. package/src/store.ts +81 -8
  81. package/src/tools.ts +196 -65
  82. package/src/trigger-runtime.ts +164 -127
  83. package/src/triggers.ts +39 -7
  84. package/src/trust.ts +46 -2
  85. package/src/ui-prefs.ts +16 -5
  86. package/src/watch-plan.ts +174 -0
  87. package/src/web.d.mts +3 -0
  88. package/src/web.mjs +2260 -2340
  89. package/src/window-executor.ts +459 -0
  90. package/src/window-guard.ts +158 -0
  91. package/docs/agents/issue-tracker.md +0 -34
package/CONTRACT.md CHANGED
@@ -37,7 +37,7 @@ talks to it only through a recipe directory, `hooks.toml`, and the `pi-loops inb
37
37
 
38
38
  ### 2.2 Unit formats
39
39
 
40
- - **Recipe**: `recipe.toml` is TOML. Required: `name` (a lowercase letter, then lowercase letters, digits and hyphens), `summary`, `levels` (non-empty, from the three levels), at least one `[[job]]`. Each `[[job]]` requires `name`, `schedule` (a recurring schedule) and `playbook` (a file in the same directory). Optional keys: `needs_tracker`, `setup`, `files`, `tier`, `useful_when`, `needs`, `needs_propose`, `budget_hint_usd`; per job `verify`, `timeout`, `thinking`, `tools`, `prompt`.
40
+ - **Recipe**: `recipe.toml` is TOML. Required: `name` (a lowercase letter, then lowercase letters, digits and hyphens), `summary`, `levels` (non-empty, from the three levels), at least one `[[job]]`. Each `[[job]]` requires `name`, `schedule` (a recurring schedule) and `playbook` (a file in the same directory). Optional keys: `line`, `needs_tracker`, `setup`, `files`, `tier`, `useful_when`, `needs`, `needs_propose`, `budget_hint_usd`; per job `verify`, `timeout`, `thinking`, `tools`, `prompt`.
41
41
  - **Playbook**: Markdown with pi's skill frontmatter (`name`, `description`), one level line, and one `## Never` section.
42
42
  - **Finding**: one JSON object with the fields `id`, `created_at` (RFC 3339), `status`, `kind` (`checkpoint` | `news`), `source`, `run_id`, `cwd`, `text`, `verified` (`true` | `false` | `null`), `dismiss_reason` (string | `null`). Fields are only added at the end; none is renamed, removed or changed in type.
43
43
  - **Event**: `run_start` and `run_end`. The payload has `run_job` and `run_id`; on `run_end` also `run_ok`, `run_findings`, `run_error`, `run_cost_usd`. Each field also enters the command's environment as `PI_<FIELD IN UPPER CASE>`, unset when the payload has `null`.
@@ -59,7 +59,7 @@ A finding is written only by a run, and its `text` is what the run said. `status
59
59
 
60
60
  ### 2.6 Deterministic rules
61
61
 
62
- - `kind`: `text` contains `· waits:` (preceded by the start of the text or whitespace, case-insensitive) → `checkpoint`; otherwise `news`.
62
+ - `kind`: a finding filed from a rule result is always `news`; otherwise `text` contains `· waits:` (preceded by the start of the text or whitespace, case-insensitive) → `checkpoint`, else `news`.
63
63
  - Level line: the first line that is exactly `Autonomy: <one word>`. When that word is one of the three levels, it is the level; otherwise the playbook has no level.
64
64
  - Manifest validity: the required keys and values of 2.2. TOML is parsed as standard TOML.
65
65
 
@@ -90,9 +90,9 @@ Three sentences each: what it is, what it depends on, and why the contract does
90
90
  - **An install leaves an `.orig/` copy and `.recipe.json` in the project and lists the directory in `.git/info/exclude`.** Depends on the three-way merge of `/recipe update` and on keeping playbooks out of the repository's history. Another merge strategy leaves 2.1 and 2.3 as they are.
91
91
  - **The values of `needs` and `needs_propose` are the preflight checks this version performs (`gh`, `git-remote`, `ci-workflows`, `lockfile`, `tracker-github`).** Depends on this version's set of checks. A recipe that names a check this version does not have is refused; that binds the recipe to a version and does not change the contract.
92
92
  - **A recipe name is at most 64 characters.** Depends on the install directory name staying readable.
93
- - **A rule's command times out after 5000 ms, its stdout is cut at 4000 characters, summaries at 2000, and the host drains for 3 seconds before it exits; output goes to `logs/pi-<pid>.log` or `host.log`.** Depends on log rotation. Changing any of these numbers leaves the event fields of 2.2 as they are.
93
+ - **A rule's command times out after 5000 ms, its stdout is cut at 4000 characters, summaries at 2000, and pi-loops drains for 3 seconds before it exits; output goes to `logs/pi-<pid>.log` or `host.log`.** Depends on log rotation. Changing any of these numbers leaves the event fields of 2.2 as they are.
94
94
  - **Creating a job needs a pi (interactive, or `pi --mode rpc`).** `pi-loops recipe add` in a terminal only copies files and prints the `/cron add` lines. Depends on the decision that a job belongs to a session. The "create the jobs" of 2.3 is done by `/recipe add` inside pi.
95
- - **A headless host runs the jobs while no pi is open.** Depends on the process hand-off. Events and findings have the same shape in both processes.
95
+ - **An always-on pi-loops runs every loop and rule check, whether or not a pi is open.** Depends on pi-loops starting on demand, and on the session lease. Events and findings have the same shape in both processes.
96
96
 
97
97
  ## 4. Not done
98
98
 
@@ -108,9 +108,9 @@ Three sentences each: what it is, what it depends on, and why the contract does
108
108
  The scripts are in `test/contract/`. `sh test/contract/run.sh` runs all five; each also runs alone.
109
109
 
110
110
  1. **Second implementation** `test/contract/1-second-implementation.sh`: `test/downstream/closed-loop.sh` (sh and jq install a recipe, let it run, find its finding by `run_id` and claim it, touching no private file), then python3 `tomllib` reads every packaged `recipe.toml` and checks the required keys of 2.2; then every packaged recipe is installed with `pi-loops recipe add` and its files are searched for the names of the loops directory's files. Pass: every step exits 0 and no playbook names a private file. A failure means private state has leaked into the contract, or a recipe depends on it.
111
- 2. **Stopped machine** `test/contract/2-stopped.sh`: with no pi and no host running, write a `hooks.toml` rule, install a directory with `pi-loops recipe add`, and dismiss a finding with `pi-loops inbox dismiss`. Pass: all three succeed. A failure means a write depends on a mechanism.
112
- 3. **Cross-check** `test/contract/3-cross-check.sh`: four rules, each run through two implementations that share nothing, over the same samples; pass when the outputs are equal line for line. TOML: `src/toml.ts` against python3 `tomllib`, over every `recipe.toml` in the repository and `examples/mcp.toml`. `kind`: `src/protocol.ts` against `grep -E`, over every `<inbox>` text the repository has shown. Level line: `src/recipe.ts` against `awk`, over every playbook in `recipes/` and in this repository's `.agents/skills/`. Finding: `pi-loops inbox list --all --json` against `jq` reading the ten fields of 2.2 off each line of `inbox.jsonl` directly. Event: the same loop run once by the host and once by a pi driven over rpc; the two `run_end` payloads have the same fields and the same run values.
113
- 4. **Private state cleared** `test/contract/4-private-state.sh`: after one run, delete everything in the loops directory that 2.1 does not name as instance data and start the host again. Pass: it runs the loop again and the earlier findings are unchanged field for field.
111
+ 2. **Stopped machine** `test/contract/2-stopped.sh`: with no pi and pi-loops not running, write a `hooks.toml` rule, install a directory with `pi-loops recipe add`, and dismiss a finding with `pi-loops inbox dismiss`. Pass: all three succeed. A failure means a write depends on a mechanism.
112
+ 3. **Cross-check** `test/contract/3-cross-check.sh`: four rules, each run through two implementations that share nothing, over the same samples; pass when the outputs are equal line for line. TOML: `src/toml.ts` against python3 `tomllib`, over every `recipe.toml` in the repository and `examples/mcp.toml`. `kind`: `src/protocol.ts` against `grep -E`, over every `<inbox>` text the repository has shown. Level line: `src/recipe.ts` against `awk`, over every playbook in `recipes/` and in this repository's `.agents/skills/`. Finding: `pi-loops inbox list --all --json` against `jq` reading the ten fields of 2.2 off each line of `inbox.jsonl` directly. Event: the same loop run once by pi-loops started directly and once by pi-loops started on demand by a pi driven over rpc; the two `run_end` payloads have the same fields and the same run values.
113
+ 4. **Private state cleared** `test/contract/4-private-state.sh`: after one run, delete everything in the loops directory that 2.1 does not name as instance data and start pi-loops again. Pass: it runs the loop again and the earlier findings are unchanged field for field.
114
114
  5. **Contract size** `test/contract/5-size.sh`: section 2 has no more non-empty lines than the count frozen in this file's header. Pass when it does not. A line added there must name, in the CHANGELOG, the mechanism it replaced.
115
115
 
116
116
  ## 6. Mapping of requirements and features
package/README.md CHANGED
@@ -5,7 +5,7 @@ English · [中文](README.zh-CN.md)
5
5
  An automation layer for [pi](https://github.com/earendil-works/pi), shipped as a plain pi extension.
6
6
  Nothing in pi is patched.
7
7
 
8
- ![Two runs of the same loop in pi's browser window: the first ends "0 findings — nothing to report", the second reports one new TODO item, and /inbox lists what is waiting](docs/screenshot.png)
8
+ ![The pi-loops page: a decision and two findings from the project's loops beside a conversation, and the two loops on the right](docs/screenshot.png)
9
9
 
10
10
  A loop wakes up with the notes its last run left, does the work in a sub-agent that starts clean,
11
11
  and files what it found. You read the findings when you want to, and claim the ones worth a turn.
@@ -31,8 +31,9 @@ reaches you is what changed rather than everything that is there.
31
31
 
32
32
  Node ≥ 22.6 (pi loads the TypeScript sources directly) and pi ≥ 0.84.3 to load it into — 0.85 is
33
33
  what this is tested against, and an older pi is refused at load with a line that names the version
34
- it needs, rather than a link error naming one missing export. Nothing else: pi-loops has no
35
- runtime dependencies. What you do want first is a provider you can actually talk to — run `pi`, send
34
+ it needs, rather than a link error naming one missing export. The process that runs your loops always
35
+ runs on node, also when pi runs under bun: it then uses the `node` on your PATH. Nothing else:
36
+ pi-loops has no runtime dependencies. What you do want first is a provider you can actually talk to — run `pi`, send
36
37
  one message, make sure you get an answer. pi-loops runs sub-agents while you are not watching, and
37
38
  the first sign of credentials that do not work should not be an empty inbox tomorrow morning.
38
39
 
@@ -60,36 +61,28 @@ when to reach for `cron_create`, `new_trigger` and the inbox on its own.
60
61
 
61
62
  Every morning a fresh sub-agent runs with the notes it wrote last time, does the work, and hands
62
63
  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:
64
+ Markdown file and the findings go to the inbox. (The run says which is which in tags that a program
65
+ reads rather than you — [docs/loops.md](docs/loops.md) has them.) Your conversation gets one line
66
+ for each of the loop's first three runs, so you see that it works, and one for any run that fails;
67
+ every other run is a line in the panel above the editor and in the page's loop activity strip. Here
68
+ are two runs of that loop, half an hour apart, as the panel lists them:
66
69
 
67
70
  ```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
+ Runs
72
+ 09:30 todo · 4s · 1 finding
73
+ 09:00 todo · 5s · 0 findings
71
74
  ```
72
75
 
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
76
+ The first run found nothing worth saying and filed nothing; the second noticed one change, a new
77
+ unchecked item in TODO.md, and filed only that. That is what the notes between runs buy — without them every run reports the
84
78
  whole file every morning, and you stop reading it by Thursday. What they filed waits in the inbox:
85
79
 
86
80
  ```text
87
81
  /inbox
88
82
  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>
83
+ 1. [inb-aae794d5] TODO: rate-limit the /search endpoint (unchecked, no owner) (acme-api, cron-b91def87, 2026-09-12 05:25)
84
+ 2. [inb-3216a92d] TODO: retry the payment webhook on 5xx (unchecked, no owner) (acme-api, cron-b91def87, 2026-09-12 05:25)
85
+ 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)
93
86
  ```
94
87
 
95
88
  The third finding is the run above; the two over it came from another loop in the same checkout,
@@ -175,9 +168,12 @@ Set a cap in `~/.pi/agent/loops/config.toml` before you rely on it:
175
168
  daily_budget_usd = 5.0
176
169
  ```
177
170
 
178
- When the last pi quits, a headless host takes over the clock so the 9am run happens whether or not
179
- you are at the machine, and hands it back the next time you open pi (`/cron host`,
180
- `pi-loops host status`). If you would rather it did not, put `[host] auto = false` in the same file.
171
+ A pi window starts pi-loops in the background when there is work to run. It runs every loop, rule
172
+ check and push, whether or not a window is open. It also runs the plain jobs of closed
173
+ conversations, in those conversations; each such run is a model turn. A window runs the plain jobs
174
+ of the conversation it holds. After a reboot, work resumes when a pi window opens, or at login if
175
+ you run `pi-loops start --foreground` from your own systemd unit ([docs/cli.md](docs/cli.md)).
176
+ `/pi-loops status` shows its status; `[host] auto = false` disables automatic starts.
181
177
 
182
178
  ## The browser window, and the command line
183
179
 
@@ -208,17 +204,17 @@ pi-loops --tui # the terminal one, when you want it
208
204
  pi-loops --continue # pick up the newest session in this directory
209
205
  ```
210
206
 
211
- `pi-loops sessions|inspect|export|import` and `pi-loops host status|abort|stop` need no pi session
212
- open at all. They are for backups from cron or CI, restoring on a fresh machine, and looking in on
213
- the headless host. See [docs/cli.md](docs/cli.md).
207
+ `pi-loops sessions|inspect|export|import` and `pi-loops start|stop|restart|status|abort` need no
208
+ pi session open at all. They are for backups from cron or CI, restoring on a fresh machine, and
209
+ looking in on pi-loops, which the page also shows as its Background view (`pi-loops status --url`). See [docs/cli.md](docs/cli.md).
214
210
 
215
- At a local terminal the bare command opens the browser front end; over ssh, or with no terminal at
211
+ At a local terminal the bare command opens the browser page; over ssh, or with no terminal at
216
212
  all, it runs pi itself, because a browser on the far machine helps nobody. Pass `--web` or `--tui`
217
213
  when that guess is wrong. Anything else you pass goes straight to pi (`pi-loops --model
218
214
  anthropic/claude-opus-5 -e .`).
219
215
 
220
- Both windows are complete pi sessions — the browser one runs `pi --mode rpc` behind a page — so the
221
- session file, `--resume`, your models, tools and extensions are the same either way, and the model
216
+ Both windows are complete pi sessions — each conversation in the page is a `pi --mode rpc` that
217
+ pi-loops runs for it — so the session file, `--resume`, your models, tools and extensions are the same either way, and the model
222
218
  and thinking level you last chose start the next session whichever window it opens in. The browser
223
219
  one is a session and not a viewer: what a person can still do after the window changed is a release
224
220
  gate, and [docs/web-ui-parity.md](docs/web-ui-parity.md) has it line by line.
@@ -228,15 +224,15 @@ earlier one in this project, **compact** summarises what is there and says what
228
224
  or as `/clear`, `/new`, `/resume` and `/compact <what to keep>` in the composer. None of them
229
225
  deletes anything; the session you leave is a file that `resume` lists.
230
226
 
231
- It serves **`http://127.0.0.1:4173/`** by default, with a token that lives in a file, so the address
232
- is the same one tomorrow and is worth bookmarking (`--port` moves it if 4173 is spoken for). The
233
- first visit leaves a cookie and you never see the token again. Running `pi-loops` while one is
234
- already up opens that window instead of failing on the port; `--no-auth` drops even that, on a
235
- machine only you use.
227
+ The page is served by pi-loops in the background on **`http://127.0.0.1:4173/`**, so `pi-loops`
228
+ opens it and exits, and the page stays open when that terminal closes. Its token lives in a file, so
229
+ the address is the same one tomorrow and is worth bookmarking (`[page] port` in `config.toml` moves
230
+ it if 4173 is spoken for). The first visit leaves a cookie and you never see the token again. There
231
+ is no way to drop the token: the page is always on, so it is always behind it.
236
232
 
237
233
  From a phone, the best route is `tailscale serve --bg 4173`: this server stays on loopback and the
238
- tailnet does TLS and identity. On the same wifi, `pi-loops --host 0.0.0.0` works too (and refuses
239
- `--no-auth`). Press **add device** in a browser that is already signed in and point the phone at the
234
+ tailnet does TLS and identity. On the same wifi, `[page] host = "0.0.0.0"` in `config.toml` works
235
+ too. Press **add device** in a browser that is already signed in and point the phone at the
240
236
  QR it shows: nothing to type, that device stays signed in, and the page can go on the home screen.
241
237
 
242
238
  ### Keeping it up to date
@@ -262,32 +258,62 @@ says what is in each one.
262
258
 
263
259
  ### Uninstall
264
260
 
261
+ Stop pi-loops before removing the extension. It keeps running in the background after the extension
262
+ is removed, and nothing else stops it:
263
+
265
264
  ```bash
265
+ pi-loops stop
266
266
  pi remove npm:@alphacoder-v0/pi-loops # or git:github.com/alphacoder-v0/pi-loops, or the checkout path — whatever you installed
267
267
  ```
268
268
 
269
- State stays in `~/.pi/agent/loops` until you delete it.
269
+ State stays in `~/.pi/agent/loops` until you delete it. The `pi-loops` launcher stays too. No
270
+ command removes it, so delete the file `install-launcher` wrote:
271
+
272
+ ```bash
273
+ rm ~/.local/bin/pi-loops # or /usr/local/bin/pi-loops, if ~/.local/bin was not on your PATH
274
+ ```
275
+
276
+ If you gave `install-launcher --dir <dir>`, the launcher is `<dir>/pi-loops`.
277
+
278
+ To go back to 0.22.8, stop pi-loops while this version is still installed, then install 0.22.8:
279
+
280
+ ```bash
281
+ pi-loops stop
282
+ pi install npm:@alphacoder-v0/pi-loops@<version> # <version> is 0.22.8
283
+ ```
284
+
285
+ 0.22.8 reads your jobs and rules as they are. It does not run the plain jobs of closed
286
+ conversations, and it ignores the files that only later versions write, so nothing still waiting
287
+ for a conversation is delivered: rule results, plain-job prompts waiting for their conversation, claimed
288
+ findings, `/goal` continuations, recipe hand-offs, `inject_and_run` pushes and messages sent from
289
+ the browser. Let them drain first (`pi-loops status` lists them). It also ignores the built-in
290
+ tools later versions record on a loop, so a loop made in a chat with fewer of them runs with pi's
291
+ default tools; and it has no window guard, so a plain job's prompt in an open window runs as your
292
+ own message would, with no question before a dangerous command.
293
+
294
+ The stop is remembered in `~/.pi/agent/loops/ui.json`, and 0.22.8 keeps it. After you upgrade again,
295
+ pi-loops stays stopped until `pi-loops start`, or until `pi-loops` opens the browser page, which starts it.
270
296
 
271
297
  ## Commands
272
298
 
273
299
  | Command | What it does |
274
300
  |---|---|
275
- | `/cron add [--stateful] [--verify] "<schedule>" <prompt>` | Schedule a job: a plain job belongs to this chat and injects its result here (it sleeps while this session is not open), `--stateful` makes a loop that runs wherever the clock is, with memory and inbox routing, `--verify` adds the checker |
301
+ | `/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 |
276
302
  | `/cron`, `/cron all` | This project's jobs, or every project on this machine |
277
303
  | `/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 |
278
304
  | `/cron disable --all` | Pause every job in this project (`--all-projects` for the machine); `/cron enable --all` resumes |
279
- | `/cron run <ref>` | Fire one job now instead of waiting for its schedule |
305
+ | `/cron run <ref>` | Ask pi-loops to run a loop now; plain jobs run now only in the window holding their conversation |
280
306
  | `/cron state <ref>` | The loop's notes — the Markdown it carries from one run to the next |
281
307
  | `/cron runs [ref]` | The run log: when it fired, what it cost, what it found |
282
308
  | `/cron trace <job> [k] [checker]` | The k-th latest run's full sub-agent transcript, maker or checker |
283
309
  | `/cron set <job> …` | Change a job in place — `--prompt`, `--schedule`, model, thinking, timeout, name — keeping its id, and therefore its notes |
284
310
  | `/cron cost [today\|7d\|all]` | What automation has cost, against `[limits] daily_budget_usd` |
285
- | `/cron scheduler` | Which pi process currently owns the timer |
286
- | `/cron host [start\|stop]` | The headless host that keeps the clock after the last pi quits |
311
+ | `/cron scheduler` | This window, its conversation, and pi-loops status |
312
+ | `/pi-loops start\|stop\|restart\|status` | Start, stop, restart or show pi-loops |
287
313
  | `/cron clear <ref>` | Release a run marker left behind by a process that is gone |
288
314
  | `/cron gc` | Remove plain jobs whose session was deleted (they are parked as disabled first) |
289
- | `/cron panel on\|off` | The side panel above the editor: Triggers, Inbox, Cron, MCP |
290
- | `/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 |
315
+ | `/cron panel on\|off` | The side panel above the editor: Rules, Loops, Runs, MCP |
316
+ | `/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 |
291
317
  | `/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 |
292
318
  | `/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 |
293
319
  | `/new-trigger <natural language>` | Create a condition-based rule ("when ~/build.done exists, run cargo test") |
@@ -329,7 +355,7 @@ notifications, and lifecycle hooks — each has a page here.
329
355
  - [docs/goal.md](docs/goal.md) — `/goal`: holding a session to a stop condition
330
356
  - [docs/recipes.md](docs/recipes.md) — `/recipe`: a project run on loops, installed in one command; the issue loop and autoresearch, and how to write your own
331
357
  - [docs/session-archive.md](docs/session-archive.md) — `/session-export`, `/session-import`
332
- - [docs/cli.md](docs/cli.md) — the `pi-loops` command line: export, import, the inbox, and looking in on the host
358
+ - [docs/cli.md](docs/cli.md) — the `pi-loops` command line: export, import, the inbox, and looking in on pi-loops
333
359
  - [docs/downstream.md](docs/downstream.md) — what a program may depend on: a recipe directory, `run_start` / `run_end`, `pi-loops inbox --json`; nothing else
334
360
  - [docs/web-ui-parity.md](docs/web-ui-parity.md) — what the browser front end owes you, line by line
335
361
  - [docs/configuration.md](docs/configuration.md) — paths, `config.toml`, flags, environment
@@ -349,9 +375,11 @@ notifications, and lifecycle hooks — each has a page here.
349
375
  | `~/.pi/agent/loops/logs/pi-<pid>.log` | what each pi process's automation did |
350
376
  | `~/.pi/agent/loops/triggers.json`, `triggers-audit.jsonl` | dynamic rules and trigger audit |
351
377
  | `~/.pi/agent/loops/{config,mcp,hooks}.toml` | configuration |
352
- | `~/.pi/agent/loops/scheduler.json` | which pi process currently owns the timer |
378
+ | `~/.pi/agent/loops/host-service.json`, `host.log` | pi-loops record and log |
379
+ | `~/.pi/agent/loops/leases/`, `requests/` | where each conversation is open, and what waits to be delivered to it |
380
+ | `~/.pi/agent/loops/pages/` | one record per conversation open in the browser page: its pi's pid, and when a tab last used it |
353
381
 
354
- Set `PI_LOOPS_DIR` to relocate all of it. The notes are yours to edit when the agent got something
382
+ [docs/configuration.md](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
355
383
  wrong, and `logs/pi-<pid>.log` is the file to read after an overnight failure.
356
384
 
357
385
  ## Automation outlives the window it was set up in
@@ -359,19 +387,19 @@ wrong, and `logs/pi-<pid>.log` is the file to read after an overnight failure.
359
387
  Scheduled work is only worth trusting if it survives you closing the editor. So "pi was restarted"
360
388
  is treated here as the normal case rather than the exception.
361
389
 
362
- Jobs are machine-global and never expire. Any open pi can own the timer:
363
- leadership is a file with a heartbeat, and when the process holding it exits or dies, the next tick
364
- in another window picks it up. A project's trigger checks run in a pi that is open in that project,
365
- so a result that belongs in a conversation lands in the right one. A tick a loop missed while
366
- nothing was running is caught up once, collapsed rather than replayed; a plain job's is not, because
367
- its prompt was written for a conversation that is gone (`--catchup` and `--no-catchup` override
368
- either way).
369
-
370
- Sub-agents are sessions opened inside the interactive pi through its SDK, not child processes: they
371
- share its live MCP servers — the browser tab that is already logged in, the database session that is
372
- already open — along with its extensions, model and thinking level. The headless host has no parent
373
- session to share, so its runs connect their own MCP clients from `mcp.toml`: a server that was live
374
- only because you had a window open is not live at 3am.
390
+ 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;
391
+ a window runs the plain jobs of the conversation open in it, and pi-loops runs those of
392
+ closed conversations. Rule results go to the conversation that created the rule; if it is closed,
393
+ pi-loops appends the result to it. The inbox gets a result only when that conversation cannot be
394
+ written. A tick a loop
395
+ missed while pi-loops was not running is caught up once, collapsed rather than replayed; a plain
396
+ job's is not, because its prompt was written for a conversation that has moved on (`--catchup` and
397
+ `--no-catchup` override either way).
398
+
399
+ Sub-agents are sessions opened through pi's SDK inside the process that runs them, not child
400
+ processes. Loops and rule checks run in the background, which has no window to share: their runs connect
401
+ their own MCP clients from `mcp.toml` and the project's own servers, so a server that is live only
402
+ because you had a window open is not live to a loop at 3am.
375
403
 
376
404
  [docs/design.md](docs/design.md) has the architecture and the reasoning behind each of these.
377
405
 
package/README.zh-CN.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  给 [pi](https://github.com/earendil-works/pi) 的自动化层,做成一个 extension:cron 和有记忆的 loop、一个分诊用的 inbox、动态 trigger 与 MCP 推送通知、生命周期 hooks。pi 的代码一行没动。
6
6
 
7
- ![浏览器窗口里同一个 loop 的两次运行:第一次「0 findings,没什么可报的」,第二次报出一条新增的 TODO;右边是等着分诊的 inbox](docs/screenshot.png)
7
+ ![pi-loops 页面:项目里的 loop 报来的一条待决定和两条发现,旁边是一段对话,右边是这两个 loop](docs/screenshot.png)
8
8
 
9
9
  一个 loop 醒来时带着上次留下的笔记,在干净上下文的子代理里干完活,把 finding 归档;你想看的时候去看,值得认真处理的那条再 claim 成真正的一轮。加上 `--verify`,第二个对抗式子代理会先把每条 finding 挑一遍,留下的才到你面前。关掉 pi 之后,这些照样跑。
10
10
 
@@ -28,7 +28,7 @@ pi-loops --continue # 接着这个目录里最新的那个会
28
28
  pi-loops sessions [--all] # 这个目录(或整台机器)记下的会话
29
29
  pi-loops inspect <file> # 别人发来的 .pisession,导入前先看清里面是什么
30
30
  pi-loops upgrade # 装最新的发布版,当初从哪装就从哪装
31
- pi-loops host status # 看一眼没有 pi 开着时在跑的自动化
31
+ pi-loops status # 看一眼没有 pi 开着时在跑的自动化
32
32
  ```
33
33
 
34
34
  上面这几条都得先有 `pi-loops` 这个命令,它在 [上手](#上手) 里装。其余都是会话里的斜杠命令(`/cron`、`/inbox`、`/triggers`、`/goal`),装上扩展重启 pi 就能用。
@@ -39,7 +39,7 @@ pi-loops host status # 看一眼没有 pi 开着时在跑的自
39
39
 
40
40
  Node ≥ 22.6(pi 直接加载 TypeScript 源码),pi ≥ 0.84.3(实际在 0.85 上测;更老的 pi 会在加载时被明确拒绝,报一句写明所需版本的话,而不是一个缺导出的链接错误),以及**一个真的能说上话的 provider**——先跑一次 `pi`,发一句,确认有回答。pi-loops 会在你不看着的时候替你跑子代理;凭据没配好,别等到第二天早上看见空 inbox 才知道。
41
41
 
42
- pi-loops 自己没有任何运行时依赖。
42
+ pi-loops 自己没有任何运行时依赖。负责跑 loop 的 后台进程始终用 node 运行;pi 跑在 bun 下时,pi-loops 使用 PATH 上的 `node`。
43
43
 
44
44
  ### 2. 装上
45
45
 
@@ -86,9 +86,9 @@ pi-loops --model anthropic/claude-opus-5 -e .
86
86
 
87
87
  重新开始不用回终端:**clear** 开一个新会话,**resume** 回到这个项目里早先的某个会话,**compact** 压缩当前上下文并告诉你压成了多少。三个都是按钮,也都可以在输入框里直接敲(`/clear` 或 `/new`、`/resume`、`/compact 保住 API 的形状`)。谁都不删东西:离开的那个会话就是磁盘上的一个文件,`resume` 里列的就是它,标题是当时说的第一句话。
88
88
 
89
- 浏览器那个地址默认是 **`http://127.0.0.1:4173/`**(`--port` 可以换):端口不随机,token 存在文件里,所以这个地址明天还是它,可以直接收藏。第一次访问会留一个 cookie,之后再也看不到 token。已经开着一个时再敲 `pi-loops`,它会把已经在跑的那个打开给你。只有你自己用的机器上,`--no-auth` 可以把这层也去掉。
89
+ 浏览器那个页面由后台的 pi-loops 提供,地址是 **`http://127.0.0.1:4173/`**:`pi-loops` 把它打开就退出,关掉那个终端,页面照样开着。token 存在文件里,所以这个地址明天还是它,可以直接收藏(4173 被占了,用 `config.toml` 里的 `[page] port` 换一个)。第一次访问会留一个 cookie,之后再也看不到 token。token 去不掉:页面一直开着,所以一直挡在它后面。
90
90
 
91
- 手机上用:最好的路是 `tailscale serve --bg 4173`——服务器仍然只绑 127.0.0.1,TLS 和身份都交给 tailnet,手机打开 `https://<机器>.<tailnet>.ts.net/`。同一个 wifi 里也可以 `pi-loops --host 0.0.0.0`(这时候再加 `--no-auth`,它当场拒绝)。手机第一次进什么都不用输:在已经登录的浏览器里点 **add device**,拿手机扫那个二维码就进去了,之后一直是登录状态(不想扫也可以输下面那六位数)。加到主屏幕会以独立窗口打开(有 manifest,没有 service worker)。
91
+ 手机上用:最好的路是 `tailscale serve --bg 4173`——服务器仍然只绑 127.0.0.1,TLS 和身份都交给 tailnet,手机打开 `https://<机器>.<tailnet>.ts.net/`。同一个 wifi 里也可以在 `config.toml` 里写 `[page] host = "0.0.0.0"`,再 `pi-loops restart`。手机第一次进什么都不用输:在已经登录的浏览器里点 **add device**,拿手机扫那个二维码就进去了,之后一直是登录状态(不想扫也可以输下面那六位数)。加到主屏幕会以独立窗口打开(有 manifest,没有 service worker)。
92
92
 
93
93
  ### 4. 第一个 loop
94
94
 
@@ -99,7 +99,7 @@ pi-loops --model anthropic/claude-opus-5 -e .
99
99
  每天早上一个干净上下文的子代理带着上次写的笔记跑一遍,回复末尾给出 `<loop-state>…</loop-state>`(留给明天的笔记)和 `<inbox>一行 finding</inbox>`:状态写进一个 Markdown 文件,finding 进 inbox,你的对话一个字都不动。下面是一个盯着 TODO 文件的 loop 相隔半小时的两次运行,第一次:
100
100
 
101
101
  ```text
102
- cron todo · 5s · $0.000 · 0 findings · state updated
102
+ todo · 5s · $0.000 · 0 findings · state updated
103
103
  MD5 unchanged (`06ff2ec8af3668bb89ecc6580110ecad`), git rev still `15b6562`. No new unchecked items — nothing to report.
104
104
  /cron trace todo · /inbox
105
105
  ```
@@ -107,7 +107,7 @@ MD5 unchanged (`06ff2ec8af3668bb89ecc6580110ecad`), git rev still `15b6562`. No
107
107
  中间提交了一次之后,第二次:
108
108
 
109
109
  ```text
110
- cron todo · 4s · $0.000 · 1 finding · state updated
110
+ todo · 4s · $0.000 · 1 finding · state updated
111
111
  md5 changed (06ff2ec8… → fe00aaad…). One new unchecked item appeared.
112
112
  • TODO.md: new unchecked item — cache the /search results for 60s (commit 395a124 "todo: cache search results")
113
113
  /cron trace todo · /inbox
@@ -120,7 +120,6 @@ md5 changed (06ff2ec8… → fe00aaad…). One new unchecked item appeared.
120
120
  Inbox (acme-api, 3 new, times +00:00):
121
121
  1. [inb-aae794d5] TODO: rate-limit the /search endpoint (acme-api, cron:todo, 2026-09-12 05:25)
122
122
  2. [inb-74b73c72] TODO.md: new unchecked item — cache the /search results for 60s (acme-api, cron:todo, 2026-09-12 05:28)
123
- claim with /inbox claim <n>, dismiss with /inbox dismiss <n>
124
123
  ```
125
124
 
126
125
  ```text
@@ -144,7 +143,7 @@ claim with /inbox claim <n>, dismiss with /inbox dismiss <n>
144
143
  daily_budget_usd = 5.0
145
144
  ```
146
145
 
147
- 最后一个 pi 退出时,无头宿主会接手时钟,所以早上九点那次照跑(`/cron host`、`pi-loops host status`)。不想要就在同一个文件里写 `[host] auto = false`。
146
+ 只要有启用的 loop、规则或待处理推送,pi 窗口就会启动后台运行的 pi-loops,所以关闭窗口后早上九点的任务仍会运行(`/pi-loops`、`pi-loops status`)。不想自动启动就在同一个文件里写 `[host] auto = false`。
148
147
 
149
148
  ## 升级
150
149
 
@@ -163,7 +162,10 @@ pi-loops upgrade --check # 只看看有没有新的
163
162
 
164
163
  ## 卸载
165
164
 
165
+ 卸载扩展前先停止 pi-loops:
166
+
166
167
  ```bash
168
+ pi-loops stop
167
169
  pi remove npm:@alphacoder-v0/pi-loops # 当初怎么装的就怎么删:npm: 包名、git: ref 或检出路径
168
170
  pi remove git:github.com/alphacoder-v0/pi-loops
169
171
  pi -e /path/to/pi-loops # 或者:只在这次启动试用,什么都不装
@@ -172,6 +174,15 @@ pi update --extensions # 对齐已安装的包
172
174
 
173
175
  数据留在 `~/.pi/agent/loops`,想清就删目录。
174
176
 
177
+ 要退回 0.22.8:趁 0.23 还装着,先停止 pi-loops,再安装 0.22.8:
178
+
179
+ ```bash
180
+ pi-loops stop
181
+ pi install npm:@alphacoder-v0/pi-loops@<version> # <version> 填 0.22.8
182
+ ```
183
+
184
+ 0.22.8 能直接读你现有的 job 和规则,但不会运行已关闭对话里的 plain job,也不理会只有 0.23 才写的那些文件,所以 `deliveries/` 里还在排队的规则结果不会再送达。
185
+
175
186
  包里附带一个 skill(`skills/pi-loops`),让 agent 知道什么时候该用 `cron_create`、`new_trigger` 和 inbox。
176
187
 
177
188
  [docs/](docs/) 里分成 loops、triggers、goal、mcp、hooks、session-archive、cli、configuration、design、troubleshooting、web-ui-parity 几篇;变更记录在 [CHANGELOG.md](CHANGELOG.md),贡献者说明在 [AGENTS.md](AGENTS.md)。
@@ -247,11 +258,11 @@ playbook 拷到 `.agents/skills/<name>/`,通过 `.git/info/exclude` 排除在
247
258
  /cron state ci loop 的笔记(状态脊柱)
248
259
  /cron runs [ci] 最近运行,最新在前
249
260
  /cron trace ci 2 第 2 新那次运行的 transcript:prompt、工具调用、结果、回复
250
- /cron scheduler 谁在走时间、有哪些 run 在跑(`/cron status` 是 `/cron` 的别名)
261
+ /cron scheduler 当前窗口、它的会话、pi-loops 状态(`/cron status` 是 `/cron` 的别名)
251
262
  /cron cost [today|7d|all] 自动化花了多少,按任务分,对照 [limits] daily_budget_usd
252
263
  /cron disable --all 本项目全停(--all-projects 停整台机器);/cron clear <ref> 清掉死进程留下的 running 标记
253
264
  /cron gc 清掉会话已消失的任务(本项目;--all 才跨项目)
254
- /cron host [start|stop] 最后一个 pi 退出后接手时钟的无头宿主
265
+ /pi-loops [start|stop] 查看、启动或停止后台运行的 pi-loops
255
266
  /cron snapshot 把「只有这个进程知道的状态」写进会话:哪些 MCP 连上了、暴露了什么工具、
256
267
  当前 active tools、hooks、谁拥有时钟。给非终端的前端读的
257
268
 
@@ -385,9 +396,9 @@ backup.pisession 无压缩 ustar,0600,拒绝覆盖已有文
385
396
  - 结果默认只落在 TUI 卡片和 audit 里;规则带 `promote_to_chat` 时,它以 `[Trigger <trace_id>] …` 开头插进主对话,后面的 turn 看得见。
386
397
  - 5 分钟 dedup 窗口,`listChanged` 类通知用稳定 key 折叠成最新一条,自定义通知必须带 `_meta.pi_dedup_key`,否则在源头丢弃并计数。
387
398
  - 自然语言里出现「每小时 / daily / cron / 定时任务」之类时,`new_trigger` 会拒绝并让 agent 改用 `cron_create`。
388
- - 工具:`new_trigger`(condition / action / spec / fire_once / promote_to_chat)、`list_triggers`、`remove_trigger`(id | all)、`set_trigger_state`,描述与返回文本固定。建规则、删规则、重新启用自动化这三件事都要你点头:工具把原因摆出来等你确认,没有 UI 可点的场合(子会话、无头宿主)一律拒绝。
399
+ - 工具:`new_trigger`(condition / action / spec / fire_once / promote_to_chat)、`list_triggers`、`remove_trigger`(id | all)、`set_trigger_state`,描述与返回文本固定。建规则、删规则、重新启用自动化这三件事都要你点头:工具把原因摆出来等你确认,没有 UI 可点的场合(子会话、后台进程)一律拒绝。
389
400
  - 规则是机器全局的(带 cwd),`/triggers rules` 默认只看本项目;子代理进程里不注册这些工具,所以一条 trigger 的动作造不出新的 trigger。
390
- - 轮询间隔:`--trigger-poll-secs 60` 或 `config.toml` 的 `[triggers] poll_interval_secs`。
401
+ - 轮询间隔:`config.toml` 的 `[triggers] poll_interval_secs`,pi-loops 每分钟重读一次。`--trigger-poll-secs` 已不起作用,传了会提示一句。
391
402
 
392
403
  ### MCP 推送作为 trigger 源
393
404
 
@@ -446,7 +457,7 @@ stdio 服务器崩溃后自动重连;`Mcp-Session-Id` 会话头;服务器发
446
457
  - 单条规则写错只跳过那条并提示,其它照常加载。同一事件的规则按文件顺序串行执行,不阻塞 agent。
447
458
  - 超时或 Ctrl-C 时杀掉 hook 的整棵进程树,不只是 `sh`。
448
459
  - 项目级 `<repo>/.pi/hooks.toml` 默认忽略;用户 `hooks.toml` 顶层写 `allow_project_hooks = true`、或 `config.toml` 同名键、或 `PI_ALLOW_PROJECT_HOOKS=1`(`true` 也算,其它值都算关)才启用。**在无头宿主里还额外要求 pi 对那个任务的 cwd 有精确的信任记录**——`allow_project_hooks` 是对「你会打开的项目」的表态,不是对「模型顺手指过去的目录」的表态。
449
- - hook 打到 stdout 的东西写进本进程日志(`logs/pi-<pid>.log`,宿主写 `host.log`),截到 4000 字符——「打印点东西再去看」这个最常用的调试手段现在是通的。
460
+ - hook 打到 stdout 的东西写进本进程日志(`logs/pi-<pid>.log`,后台日志写入 `host.log`),截到 4000 字符——「打印点东西再去看」这个最常用的调试手段现在是通的。
450
461
  - 子代理进程里不触发 hooks,只有你交互的那个 pi 触发。
451
462
 
452
463
  ## 子代理每次收到的 prompt
@@ -473,7 +484,7 @@ Output protocol (mandatory):
473
484
  | **状态脊柱** — 每个 loop 一份 ≤2000 字符的笔记,run N+1 读到 run N 写的 | `<loop-state>` 标签解析后写入 Markdown,下次运行拼进 prompt 头部 | `~/.pi/agent/loops/state/<id>.md` |
474
485
  | **maker/checker** | `--verify`:第二个对抗式子代理逐条核实 findings,drop 的不进 inbox | `runs.jsonl` 的 `checker` 字段 |
475
486
  | **路由层** — 产出既不打断你也不沉进日志 | `<inbox>` 标签 → JSONL 追加,`new → claimed/dismissed` 生命周期,状态栏 `Inbox: N new` 角标;条目 id `inb-<32hex>`、来源 `cron:<id或name>`、`/inbox` 与 `/inbox all` 的行格式、错误措辞都固定下来,已核实的多一个 `✓` 标记 | `~/.pi/agent/loops/inbox.jsonl` |
476
- | stateful job 走 SubAgent、永不碰主对话 | 同进程子会话(pi SDK),干净上下文,共享父会话的 MCP 客户端、扩展、model/thinking;完整 transcript 保留在 `sessions/<id>/`,`/cron trace` 可看 | `~/.pi/agent/loops/sessions/<id>/*.jsonl` |
487
+ | stateful job 走 SubAgent、永不碰主对话 | 在后台运行的 pi-loops 里开的子会话(pi SDK),干净上下文;MCP 连接由 pi-loops 按 `mcp.toml` 和项目配置自己建立,不再借用某个窗口的连接;完整 transcript 保留在 `sessions/<id>/`,`/cron trace` 可看 | `~/.pi/agent/loops/sessions/<id>/*.jsonl` |
477
488
  | trigger 的产出看得见、可追溯 | 每次运行结束在 transcript 里落一张卡片(耗时、成本、findings、摘要),run log 记退出码与用量 | `runs.jsonl` |
478
489
  | 预览与 audit 一律脱敏 | `src/redact.ts` 一套 redactor,列表、卡片、run log、trace 全部过一遍 | — |
479
490
  | 普通 cron 走 inject-and-run | 不加 `--stateful` 的任务到点用 `pi.sendUserMessage` 注入创建它的会话,消息带 `[Trigger <trace>] ` 前缀(空闲直接发,忙则 followUp 排队) | — |
@@ -486,18 +497,23 @@ Output protocol (mandatory):
486
497
  ## 自动化要活过开它的那个窗口
487
498
 
488
499
  定时任务值不值得信,取决于你关掉编辑器之后会发生什么。所以这里把「pi 重启过」当成常态:
489
-
490
- 1. **任务是机器全局的**,存在 `~/.pi/agent/loops/jobs.json`,不绑会话、不绑目录(每个任务记住自己的 `cwd`,子代理在那里跑)。任何目录里打开的任何 pi 都能看到并执行 stateful 任务;普通任务因为要注入对话,只在创建它的那个会话里触发(`--resume` 回来就继续),会话没了,leader 会把它停用,`/cron gc` 清掉;子代理创建的普通任务归它所服务的那个父会话。
491
- 2. **loop 归本机唯一的 leader 跑;动态检查归「开在那个项目里的那个 pi」跑。** `scheduler.json` 里放 pid 加心跳,30 秒一 tick;leader 退出或者崩了,其它 pi 在下一 tick 接手。
492
-
493
- 每个进程每 tick 往 `presence/` 登记自己的 pid、会话和 cwd。一个项目的规则检查和推送评估,就交给开在那个项目里的 pi——优先创建规则的那个会话,其次 pid 最小的那个——所以 promote_to_chat 一定落在对的对话里。项目里一个 pi 都没开,才轮到 leader 代跑,结果进 inbox。
494
-
495
- 轮询间隔看共享的 `polls.json`,全机一份,交接不会重复检查。
496
- 3. **loop 错过的 tick 默认补发一次**(多次错过折叠成一次,就像 systemd `Persistent=true`),`--no-catchup` 关掉。普通注入任务反过来,默认不补,要补得写 `--catchup`:它是往对话里插一句话的,迟到的提醒不如不提醒。
497
- 4. **没有过期时间。** 任务只在你 `/cron remove` 时消失。
498
- 5. **有 run log 和完整 transcript。** `runs.jsonl` 记每次运行的退出码、耗时、成本、finding 数、有没有更新状态;子代理的 session 文件每个 loop 保留最近 20 份,`/cron trace <job> [k]` 直接看它调了什么工具、看到了什么,`pi --session <文件>` 可以整个接管回放。
499
-
500
- 子代理在父进程内运行:共享父进程活着的 MCP 服务器实例(浏览器标签页、数据库会话都是同一份)、`-e` 扩展、system prompt、skill 标志与模型,同一项目下继承信任。「一个 pi 进程都没有」也解决了:本机最后一个交互式 pi 退出时,如果还有 loop、规则或 MCP 服务器,它会拉起一个无头宿主进程(`src/host.ts`,同一套存储、同样的进程内运行器、自己的 MCP 客户端)继续走时钟;本来要进对话的结果进 inbox;下一个打开的 pi 抢回时钟,宿主退出。`/cron host [start|stop]` 看和控制(start = 即使 `[host] auto = false` 也在本 pi 退出时交接),`host.log` 是它的日志,重启机器后要等下一个 pi 打开才会再交接。
500
+ 任务和规则保存在本机的 loops 目录。
501
+
502
+ 一个 loops 目录最多运行一个后台运行的 pi-loops。pi 窗口在会话开始、每 30 秒一次的 tick,以及新建 loop、规则后,
503
+ 只要存在启用的 loop、规则或有待处理推送的 MCP 服务器,或者某个已关闭的会话有普通任务或排队的规则结果,
504
+ 就会按需启动 pi-loops。pi-loops 运行所有 stateful loop、动态规则检查和 MCP 推送,也在已关闭的会话里运行它们的
505
+ 普通任务,每次都是一轮模型调用;窗口运行在它里面打开的那个会话的普通任务。规则结果回到创建规则的
506
+ 会话:窗口持有该会话时送入会话,其他进程判定时排队给该窗口;会话已关闭时由 pi-loops 追加进会话。只有会话
507
+ 文件无法写入时,结果才进入 inbox。
508
+ 由 shell 创建且没有会话的规则,结果进入 inbox。
509
+
510
+ pi-loops 不会因窗口打开或关闭而退出。`/pi-loops` 显示 pid、pi-loops 版本、启动时间、正在运行的工作、窗口
511
+ 持有的会话和日志;`/pi-loops stop` 会记录手动停止,在 `/pi-loops start` 前窗口不会重新启动它。
512
+ `[host] auto = false` 或单进程环境变量 `PI_LOOPS_HOST=off` 禁止窗口自动启动。无法启动时会在 1、5、30
513
+ 分钟后重试,状态行显示暂停原因,`/pi-loops status` 和 `host.log` 提供详情。窗口连续三次探测不到控制 socket
514
+ 会替换失联的 pi-loops:先发 SIGTERM,10 秒后再发 SIGKILL;仍在启动的 pi-loops 不会因此被杀掉。新版窗口会替换旧版
515
+ pi-loops,旧版窗口不会替换新版 pi-loops。升级期间,pi-loops 0.22 窗口让后台进程运行 loop、自己待命,但仍会运行自己持有
516
+ 会话的普通任务;重启这些旧窗口才能让普通任务也采用新行为。机器重启后,要等 pi 窗口打开才会恢复运行。pi-loops 还有一个只读状态页,`pi-loops status --url` 打印带令牌的地址。
501
517
 
502
518
  ## 对 pi 的无侵入性
503
519
 
@@ -517,13 +533,17 @@ Output protocol (mandatory):
517
533
  | `~/.pi/agent/loops/spend.json` | 轮转掉的那部分花费按天留一份,预算上限不会因为 run log 被截断而失效 |
518
534
  | `~/.pi/agent/loops/logs/pi-<pid>.log` | 每个 pi 进程的自动化诊断,超 2 MB 保留后半,只留最近五个进程(进程还活着的那份不算) |
519
535
  | `~/.pi/agent/loops/sessions/<id>/*.jsonl` | 子代理完整 transcript,每个 loop 保留最近 20 份 |
520
- | `~/.pi/agent/loops/scheduler.json` | 当前 leader 的 pid / 心跳 |
536
+ | `~/.pi/agent/loops/host-service.json`、`host.log` | 后台运行的 pi-loops 记录与日志 |
537
+ | `~/.pi/agent/loops/leases/`、`requests/` | 每个会话在哪里打开,以及等着送进会话的内容 |
538
+ | `~/.pi/agent/loops/pages/` | 浏览器页面里开着的每个会话一条记录:它的 pi 的 pid,以及标签页最近一次用它的时间 |
521
539
  | `~/.pi/agent/loops/triggers.json` | 动态 trigger 规则(全局,带 cwd) |
522
540
  | `~/.pi/agent/loops/triggers-audit.jsonl` | trigger audit,超 2 MB 保留后半 |
523
541
  | `~/.pi/agent/loops/sessions/triggers-<项目>-<哈希>/*.jsonl` | 动态检查子代理的 transcript,每个项目一个目录(目录名带项目路径的哈希,同名的两个项目不会共用),保留最近 40 份 |
524
542
  | `~/.pi/agent/loops/config.toml` | `[triggers] poll_interval_secs`、`allow_project_hooks` |
525
543
  | `~/.pi/agent/loops/mcp.toml` · `hooks.toml` | MCP 推送源、生命周期 hooks |
526
544
 
545
+ 完整的文件列表见 [docs/configuration.md](docs/configuration.md)。
546
+
527
547
  跨进程写入靠 `mkdir` 锁 + 原子改名,没有原生依赖。
528
548
 
529
549
  ## 代码结构
@@ -531,12 +551,12 @@ Output protocol (mandatory):
531
551
  ```
532
552
  src/extension-entry.ts pi 加载的入口:先查 pi 版本(src/pi-floor.ts),够了再导入 src/pi-loops.ts
533
553
  src/pi-loops.ts 扩展本体:命令、工具、生命周期、状态栏角标、面板
534
- src/cli.ts `pi-loops`:会话入口(网页或终端)与 sessions / inspect / export / import / host
554
+ src/cli.ts `pi-loops`:会话入口(网页或终端)与 sessions / inspect / export / import / start / stop / status
535
555
  src/cli-entry.mjs bin
536
556
  src/ts-entry.mjs .mjs 入口怎么加载本包的 .ts:Node 自己剥类型,装在 node_modules 下就借 pi 的 jiti
537
557
 
538
558
  流水线
539
- src/scheduler.ts tick 循环、leader 选举、到期判定、错过补发、并发/重叠控制、子会话执行、写回
559
+ src/scheduler.ts tick 循环、到期判定、错过补发、并发/重叠控制、子会话执行、写回;pi-loops 在后台运行 loops,窗口跑其会话的普通任务
540
560
  src/trigger-runtime.ts 动态规则运行时:admit、投递(sub_agent / inject_summary / inject_and_run)、fire-once、promote、audit
541
561
  src/triggers.ts 规则本身:解析、prompt、id 提取、存储、dedup 窗口
542
562
  src/goal.ts /goal:停止条件状态机、评估器 prompt、续跑预算
@@ -557,12 +577,12 @@ src/subagent-guard.ts 把该策略装进每个子会话的合成扩展
557
577
  src/transcript.ts 把子代理 session 文件压成可读的几十行
558
578
 
559
579
  没有 pi 开着的时候
560
- src/host.ts 最后一个 pi 退出后接手时钟的无头宿主
561
- src/host-entry.mjs 宿主实际被拉起的那个进程,好让 host.ts 在 node_modules 下也能加载
562
- src/host-control.ts host.json、拉起/停止、交接判定
563
- src/host-control-channel.ts 宿主的 unix socket:snapshot、abort、stop
564
- src/host-runtime.ts 宿主里跑的东西(调度器 + triggers + 按请求服务的工具宿主)
565
- src/presence.ts 每个活着的 pi 登记一份:谁开在哪个项目,结果该落进哪个对话
580
+ src/host.ts 后台运行的 pi-loops:认领、自检、0.22 兼容边界
581
+ src/host-entry.mjs pi-loops 的启动入口,负责加载 node_modules 下的 TypeScript
582
+ src/host-control.ts 服务记录、启动、退出与恢复决策
583
+ src/host-control-channel.ts 控制 socket:状态、中止、停止、运行与升级
584
+ src/host-runtime.ts 后台运行逻辑(调度器、triggers、按请求提供的工具)
585
+ src/presence.ts pi-loops 0.22 窗口写入;升级时只读
566
586
  src/register-pi.mjs 在 pi 之外解析 pi 包的 node --import 钩子(宿主、测试用)
567
587
  src/pi-resolver.mjs 这个钩子去哪里找
568
588
 
@@ -606,13 +626,13 @@ TypeScript 的类型信息判断。
606
626
 
607
627
  - 子会话继承父会话的 model/thinking(任务固定了模型则用固定的);子会话里不再加载这个扩展本身,不会递归。
608
628
  - 子会话没有 UI 就没有审批弹窗:需要确认的工具在子会话里 **fail-closed 拒绝**。没人能点「同意」的时候,默认答案只能是「不」。要收紧就用 `--tools read,grep,ls`。
609
- - 状态栏角标最多五段,按需出现,之间用 ` · ` 隔开:`Inbox: N new`、`N job(s) failing (<name> ×K)`、`running: <loop>`(多个用逗号连起来,trigger 检查在这儿显示成 `trigger-check`)、`mcp: 1 source down` / `mcp: 2 sources down`、`loops standby`。
629
+ - 状态栏角标最多五段,按需出现,之间用 ` · ` 隔开:`Inbox: N new`、`N loops failing (<name> ×K)`、`running: <loop>`(多个用逗号连起来,trigger 检查在这儿显示成 `trigger-check`)、`mcp: 1 source down` / `mcp: 2 sources down`、`loops paused: the pi-loops could not start`。
610
630
  - 同一任务上一轮还在跑时新 tick 直接跳过并计数(`overlap-skipped`),不排队。
611
631
  - 同时最多 3 个子代理在跑(`[cron] max_concurrent_runs`)——loop 运行、trigger 检查、`/goal` 评估器**共用这一个池子**(`src/slots.ts`),三条路加起来一共 3 个。`/goal` 评估器和 `/cron run` 占槽但永不被拒(你直接要的东西,机器悄悄不做和从没设过是一样的),所以 `/triggers running` 有可能显示 `4 of 3 sub-agent slot(s) in use`。
612
632
  - `[limits] daily_budget_usd` 不只挡派发,也会**停掉正在跑的运行**:算的是运行日志里已落账的花费加上本进程在飞的(并行的兄弟运行,以及 `--verify` 那对共用 runId 的 maker/checker)。被预算停掉记为 aborted 而不是 failed,所以时隙还欠着、失败连击不累加。
613
633
  - inbox 的状态改写是「最后写者赢」:两个进程同时处理同一条时,后写的那个说了算,不做冲突检测。
614
- - 常驻面板是编辑器上方的 widget:`Triggers` 规则最多 5 条 + `Polling` 最近一次检查、`Inbox N new`、`Cron` 启停统计与任务最多 5 条、`MCP` 各服务器连接状态与工具数——这几段各自没内容时不显示;`Hooks`(cli_hooks 规则数与事件,没有就写 `none`)和 `Runtime`(dedup · cycle suppress · fire-once rules · inject-and-run)总是画出来。规则、任务、新 findings、MCP 服务器全都没有时整个面板不显示;`/cron panel off` 或 `/triggers panel off` 关闭,偏好存在 `ui.json`。
615
- - 跨设备访问不放 broker 在中间:让你从别处够到那个还待在原地的前端——`tailscale serve` 在 tailnet 上终结 TLS、代理到本机 loopback,手机就能开,中间不经过任何第三方;同一网段则用 `--host`。手机第一次进用配对码和二维码([docs/cli.md](docs/cli.md#from-a-phone))。真正没覆盖的是「手机两个网络都不在」,那种情况才需要中继。
634
+ - 常驻面板是编辑器上方的 widget:`Triggers` 规则最多 5 条 + `Polling` 最近一次检查、`Inbox N new`、`Cron` 启停统计与任务最多 5 条、`MCP` 各服务器连接状态与工具数、`Hooks` cli_hooks 规则数与事件——这几段各自没内容时不显示。规则、任务、新 findings、MCP 服务器全都没有时整个面板不显示;`/cron panel off` 或 `/triggers panel off` 关闭,偏好存在 `ui.json`。
635
+ - 跨设备访问不放 broker 在中间:让你从别处够到那个还待在原地的前端——`tailscale serve` 在 tailnet 上终结 TLS、代理到本机 loopback,手机就能开,中间不经过任何第三方;同一网段则在 `config.toml` 里设 `[page] host`。手机第一次进用配对码和二维码([docs/cli.md](docs/cli.md#from-a-phone))。真正没覆盖的是「手机两个网络都不在」,那种情况才需要中继。
616
636
  - 这是一个自动化层,不是一个 agent:写代码、读网页、管 skill 都是 pi 自己的本事,这里只决定什么时候、在哪里、带着什么上下文去跑它们。
617
637
 
618
638
  ## 致谢