@alphacoder-v0/pi-loops 0.22.8 → 0.24.1

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 (92) hide show
  1. package/CHANGELOG.md +717 -0
  2. package/CONTRACT.md +7 -7
  3. package/README.md +114 -78
  4. package/README.zh-CN.md +62 -41
  5. package/docs/cli.md +352 -91
  6. package/docs/configuration.md +86 -24
  7. package/docs/design.md +404 -61
  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 +41 -28
  17. package/docs/web-ui-parity.md +702 -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/browser-open.ts +169 -0
  32. package/src/cli-entry.mjs +12 -3
  33. package/src/cli.ts +960 -143
  34. package/src/config.ts +53 -5
  35. package/src/conversation-rows.ts +469 -0
  36. package/src/danger.ts +227 -6
  37. package/src/engine.ts +1240 -0
  38. package/src/goal.ts +71 -0
  39. package/src/held-read.ts +27 -0
  40. package/src/hooks.ts +49 -18
  41. package/src/host-control-channel.ts +455 -40
  42. package/src/host-control.ts +602 -111
  43. package/src/host-entry.mjs +5 -3
  44. package/src/host-runtime.ts +455 -42
  45. package/src/host.ts +659 -123
  46. package/src/inbox-card.ts +278 -0
  47. package/src/inbox.ts +139 -62
  48. package/src/job-edit.ts +7 -1
  49. package/src/job-health.ts +14 -2
  50. package/src/job-lines.ts +201 -13
  51. package/src/job-owner.ts +74 -13
  52. package/src/job-tune.ts +110 -0
  53. package/src/lease.ts +537 -0
  54. package/src/lock.ts +7 -2
  55. package/src/mcp-pool.ts +66 -14
  56. package/src/mcp.ts +1 -1
  57. package/src/open-file.ts +54 -0
  58. package/src/page-children.ts +1507 -0
  59. package/src/page-devices.ts +192 -0
  60. package/src/page-server.ts +3150 -0
  61. package/src/path-precheck.ts +215 -0
  62. package/src/paths.ts +24 -0
  63. package/src/pi-floor.ts +10 -0
  64. package/src/pi-loops.ts +1370 -473
  65. package/src/pi-resolver.d.mts +1 -0
  66. package/src/pi-resolver.mjs +110 -10
  67. package/src/presence.ts +12 -63
  68. package/src/protocol.ts +10 -1
  69. package/src/recipe.ts +43 -1
  70. package/src/requests.ts +354 -0
  71. package/src/rule-lines.ts +68 -0
  72. package/src/run-card.ts +105 -0
  73. package/src/runner.ts +18 -1
  74. package/src/schedule.ts +1 -2
  75. package/src/scheduler.ts +595 -245
  76. package/src/sdk-runner.ts +321 -27
  77. package/src/seen.ts +278 -0
  78. package/src/session-head.ts +21 -0
  79. package/src/share.ts +12 -2
  80. package/src/snapshot.ts +3 -2
  81. package/src/store.ts +81 -8
  82. package/src/tools.ts +196 -65
  83. package/src/trigger-runtime.ts +164 -127
  84. package/src/triggers.ts +39 -7
  85. package/src/trust.ts +46 -2
  86. package/src/ui-prefs.ts +22 -7
  87. package/src/watch-plan.ts +174 -0
  88. package/src/web.d.mts +3 -0
  89. package/src/web.mjs +2261 -2340
  90. package/src/window-executor.ts +459 -0
  91. package/src/window-guard.ts +158 -0
  92. 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
 
@@ -45,11 +46,11 @@ one run with `pi -e .`, which installs nothing. Keep one copy, not two: two copi
45
46
  same tools, and pi refuses to load the second — `Tool "cron_create" conflicts with …`, and it
46
47
  exits.
47
48
 
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
49
+ Then start `pi` once, and that is the whole install. That first session puts the `pi-loops` command
50
+ on your `PATH` and says so in one line, so from then on `pi-loops` works from any shell (see
51
+ [The browser window, and the command line](#the-browser-window-and-the-command-line)). `/cron`,
52
+ `/inbox`, `/triggers` and `/goal` are registered by the extension itself, so they work with or
53
+ without that command. The package also ships a skill (`skills/pi-loops`), so the agent knows
53
54
  when to reach for `cron_create`, `new_trigger` and the inbox on its own.
54
55
 
55
56
  ## Your first loop
@@ -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,31 +168,39 @@ 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
 
184
180
  `pi-loops` is the part that needs a launcher: `pi install` puts the package under pi's managed
185
- directory rather than on your `PATH`, so the command does not exist yet. You are not asked to fix
186
- that by hand — the first pi session after `pi install` offers to put `pi-loops` on your `PATH`, and
187
- keeps that launcher working afterwards if pi ever moves the package. Say no and nothing is written,
188
- and you are not asked again; `/pi-loops install-launcher` writes one whenever you change your mind:
181
+ directory rather than on your `PATH`. You do not fix that by hand. The first interactive pi session
182
+ after `pi install` writes `pi-loops` into the first of `~/.local/bin` and `/usr/local/bin` that is
183
+ already on your `PATH`, says so in one line, and keeps that launcher working afterwards if pi ever
184
+ moves the package:
185
+
186
+ ```text
187
+ [pi-loops] added pi-loops to ~/.local/bin; /pi-loops uninstall-launcher removes it
188
+ ```
189
+
190
+ It never replaces a `pi-loops` it did not write. When neither directory is on your `PATH`, or on a
191
+ machine where pi itself is never opened, write it yourself and pass `--dir <dir>` to say where:
189
192
 
190
193
  ```text
191
- /pi-loops install-launcher # from inside pi, where the extension is already loaded
194
+ /pi-loops install-launcher --dir ~/bin # from inside pi, where the extension is already loaded
192
195
  ```
193
196
 
194
197
  ```bash
195
- # or from a shell, on a machine where pi is never opened — the same command, by absolute path
198
+ # or from a shell — the same command, by absolute path
196
199
  cd ~/.pi/agent/npm/node_modules/@alphacoder-v0/pi-loops # installed from npm
197
200
  cd ~/.pi/agent/git/github.com/alphacoder-v0/pi-loops # installed from GitHub
198
- node src/cli-entry.mjs install-launcher
201
+ node src/cli-entry.mjs install-launcher --dir ~/bin
199
202
  ```
200
203
 
201
- Any of them writes a launcher into the first of `~/.local/bin` and `/usr/local/bin` that is already
202
- on your `PATH`, and refuses rather than guessing if neither is — pass `--dir <dir>` to say where.
203
204
  After that, `pi-loops` works from anywhere:
204
205
 
205
206
  ```bash
@@ -208,17 +209,17 @@ pi-loops --tui # the terminal one, when you want it
208
209
  pi-loops --continue # pick up the newest session in this directory
209
210
  ```
210
211
 
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).
212
+ `pi-loops sessions|inspect|export|import` and `pi-loops start|stop|restart|status|abort` need no
213
+ pi session open at all. They are for backups from cron or CI, restoring on a fresh machine, and
214
+ 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
215
 
215
- At a local terminal the bare command opens the browser front end; over ssh, or with no terminal at
216
+ At a local terminal the bare command opens the browser page; over ssh, or with no terminal at
216
217
  all, it runs pi itself, because a browser on the far machine helps nobody. Pass `--web` or `--tui`
217
218
  when that guess is wrong. Anything else you pass goes straight to pi (`pi-loops --model
218
219
  anthropic/claude-opus-5 -e .`).
219
220
 
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
221
+ Both windows are complete pi sessions — each conversation in the page is a `pi --mode rpc` that
222
+ pi-loops runs for it — so the session file, `--resume`, your models, tools and extensions are the same either way, and the model
222
223
  and thinking level you last chose start the next session whichever window it opens in. The browser
223
224
  one is a session and not a viewer: what a person can still do after the window changed is a release
224
225
  gate, and [docs/web-ui-parity.md](docs/web-ui-parity.md) has it line by line.
@@ -228,15 +229,21 @@ earlier one in this project, **compact** summarises what is there and says what
228
229
  or as `/clear`, `/new`, `/resume` and `/compact <what to keep>` in the composer. None of them
229
230
  deletes anything; the session you leave is a file that `resume` lists.
230
231
 
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.
232
+ The page is served by pi-loops in the background on **`http://127.0.0.1:4173/`**, so `pi-loops`
233
+ opens it and exits, and the page stays open when that terminal closes. Its token lives in a file, so
234
+ the address is the same one tomorrow and is worth bookmarking (`[page] port` in `config.toml` moves
235
+ it if 4173 is spoken for). `pi-loops` prints one link with a one-use key and opens your browser on
236
+ it, on macOS, Windows, WSL and Linux; when no browser opens, paste that link. The first visit leaves
237
+ a cookie and you never see the token again. There is no way to drop the token: the page is always
238
+ on, so it is always behind it.
239
+
240
+ A browser that shows "This browser has not been here before." needs a fresh link: run
241
+ `pi-loops open` in a terminal, or `/pi-loops web` inside pi, which also copies the link to the
242
+ clipboard and needs no `pi-loops` on your PATH.
236
243
 
237
244
  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
245
+ tailnet does TLS and identity. On the same wifi, `[page] host = "0.0.0.0"` in `config.toml` works
246
+ too. Press **add device** in a browser that is already signed in and point the phone at the
240
247
  QR it shows: nothing to type, that device stays signed in, and the page can go on the home screen.
241
248
 
242
249
  ### Keeping it up to date
@@ -262,32 +269,58 @@ says what is in each one.
262
269
 
263
270
  ### Uninstall
264
271
 
272
+ Stop pi-loops and remove the `pi-loops` launcher before removing the extension. pi-loops keeps
273
+ running in the background after the extension is removed, and nothing else stops it; the launcher
274
+ needs the package to run. `uninstall-launcher` removes only a launcher that pi-loops wrote:
275
+
265
276
  ```bash
277
+ pi-loops stop
278
+ pi-loops uninstall-launcher # add --dir <dir> if install-launcher --dir <dir> wrote it
266
279
  pi remove npm:@alphacoder-v0/pi-loops # or git:github.com/alphacoder-v0/pi-loops, or the checkout path — whatever you installed
267
280
  ```
268
281
 
269
- State stays in `~/.pi/agent/loops` until you delete it.
282
+ State stays in `~/.pi/agent/loops` until you delete it. After a reinstall, `/pi-loops install-launcher`
283
+ in pi puts the command back, because a removed launcher is not written again by itself.
284
+
285
+ To go back to 0.22.8, stop pi-loops while this version is still installed, then install 0.22.8:
286
+
287
+ ```bash
288
+ pi-loops stop
289
+ pi install npm:@alphacoder-v0/pi-loops@<version> # <version> is 0.22.8
290
+ ```
291
+
292
+ 0.22.8 reads your jobs and rules as they are. It does not run the plain jobs of closed
293
+ conversations, and it ignores the files that only later versions write, so nothing still waiting
294
+ for a conversation is delivered: rule results, plain-job prompts waiting for their conversation, claimed
295
+ findings, `/goal` continuations, recipe hand-offs, `inject_and_run` pushes and messages sent from
296
+ the browser. Let them drain first (`pi-loops status` lists them). It also ignores the built-in
297
+ tools later versions record on a loop, so a loop made in a chat with fewer of them runs with pi's
298
+ default tools; and it has no window guard, so a plain job's prompt in an open window runs as your
299
+ own message would, with no question before a dangerous command.
300
+
301
+ The stop is remembered in `~/.pi/agent/loops/ui.json`, and 0.22.8 keeps it. After you upgrade again,
302
+ pi-loops stays stopped until `pi-loops start`, or until `pi-loops` opens the browser page, which starts it.
270
303
 
271
304
  ## Commands
272
305
 
273
306
  | Command | What it does |
274
307
  |---|---|
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 |
308
+ | `/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
309
  | `/cron`, `/cron all` | This project's jobs, or every project on this machine |
277
310
  | `/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
311
  | `/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 |
312
+ | `/cron run <ref>` | Ask pi-loops to run a loop now; plain jobs run now only in the window holding their conversation |
280
313
  | `/cron state <ref>` | The loop's notes — the Markdown it carries from one run to the next |
281
314
  | `/cron runs [ref]` | The run log: when it fired, what it cost, what it found |
282
315
  | `/cron trace <job> [k] [checker]` | The k-th latest run's full sub-agent transcript, maker or checker |
283
316
  | `/cron set <job> …` | Change a job in place — `--prompt`, `--schedule`, model, thinking, timeout, name — keeping its id, and therefore its notes |
284
317
  | `/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 |
318
+ | `/cron scheduler` | This window, its conversation, and pi-loops status |
319
+ | `/pi-loops start\|stop\|restart\|status` | Start, stop, restart or show pi-loops |
287
320
  | `/cron clear <ref>` | Release a run marker left behind by a process that is gone |
288
321
  | `/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 |
322
+ | `/cron panel on\|off` | The side panel above the editor: Rules, Loops, Runs, MCP |
323
+ | `/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
324
  | `/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
325
  | `/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
326
  | `/new-trigger <natural language>` | Create a condition-based rule ("when ~/build.done exists, run cargo test") |
@@ -299,7 +332,8 @@ State stays in `~/.pi/agent/loops` until you delete it.
299
332
  | `/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) |
300
333
  | `/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)) |
301
334
  | `/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](docs/recipes.md)) |
302
- | `/pi-loops [install-launcher]` | Version and paths; `install-launcher` puts the `pi-loops` command on your `PATH` |
335
+ | `/pi-loops web` | Open this conversation in the browser page, and copy its one-use link |
336
+ | `/pi-loops [install-launcher \| uninstall-launcher]` | Version and paths; `install-launcher` puts the `pi-loops` command on your `PATH`, `uninstall-launcher` removes it |
303
337
 
304
338
  A schedule is a 5-field cron expression, or one of `hourly` / `daily` / `weekly` (also `每天`), or
305
339
  `every 30m`, `in 10m`, `at <ISO>`. The `@hourly` / `@daily` / `@weekly` / `@monthly` spellings work
@@ -329,7 +363,7 @@ notifications, and lifecycle hooks — each has a page here.
329
363
  - [docs/goal.md](docs/goal.md) — `/goal`: holding a session to a stop condition
330
364
  - [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
365
  - [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
366
+ - [docs/cli.md](docs/cli.md) — the `pi-loops` command line: export, import, the inbox, and looking in on pi-loops
333
367
  - [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
368
  - [docs/web-ui-parity.md](docs/web-ui-parity.md) — what the browser front end owes you, line by line
335
369
  - [docs/configuration.md](docs/configuration.md) — paths, `config.toml`, flags, environment
@@ -349,9 +383,11 @@ notifications, and lifecycle hooks — each has a page here.
349
383
  | `~/.pi/agent/loops/logs/pi-<pid>.log` | what each pi process's automation did |
350
384
  | `~/.pi/agent/loops/triggers.json`, `triggers-audit.jsonl` | dynamic rules and trigger audit |
351
385
  | `~/.pi/agent/loops/{config,mcp,hooks}.toml` | configuration |
352
- | `~/.pi/agent/loops/scheduler.json` | which pi process currently owns the timer |
386
+ | `~/.pi/agent/loops/host-service.json`, `host.log` | pi-loops record and log |
387
+ | `~/.pi/agent/loops/leases/`, `requests/` | where each conversation is open, and what waits to be delivered to it |
388
+ | `~/.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
389
 
354
- Set `PI_LOOPS_DIR` to relocate all of it. The notes are yours to edit when the agent got something
390
+ [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
391
  wrong, and `logs/pi-<pid>.log` is the file to read after an overnight failure.
356
392
 
357
393
  ## Automation outlives the window it was set up in
@@ -359,19 +395,19 @@ wrong, and `logs/pi-<pid>.log` is the file to read after an overnight failure.
359
395
  Scheduled work is only worth trusting if it survives you closing the editor. So "pi was restarted"
360
396
  is treated here as the normal case rather than the exception.
361
397
 
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.
398
+ 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;
399
+ a window runs the plain jobs of the conversation open in it, and pi-loops runs those of
400
+ closed conversations. Rule results go to the conversation that created the rule; if it is closed,
401
+ pi-loops appends the result to it. The inbox gets a result only when that conversation cannot be
402
+ written. A tick a loop
403
+ missed while pi-loops was not running is caught up once, collapsed rather than replayed; a plain
404
+ job's is not, because its prompt was written for a conversation that has moved on (`--catchup` and
405
+ `--no-catchup` override either way).
406
+
407
+ Sub-agents are sessions opened through pi's SDK inside the process that runs them, not child
408
+ processes. Loops and rule checks run in the background, which has no window to share: their runs connect
409
+ their own MCP clients from `mcp.toml` and the project's own servers, so a server that is live only
410
+ because you had a window open is not live to a loop at 3am.
375
411
 
376
412
  [docs/design.md](docs/design.md) has the architecture and the reasoning behind each of these.
377
413