@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
@@ -4,28 +4,37 @@ Everything lives under `~/.pi/agent/loops/` (override: `PI_LOOPS_DIR`).
4
4
 
5
5
  | File | Purpose |
6
6
  |---|---|
7
- | `config.toml` | `allow_project_hooks`, `[triggers] poll_interval_secs = 600` / `run_timeout_secs = 900`, `[cron] catch_up = true` / `max_concurrent_runs = 3`, `[hooks] mode = "sync"`, `[host] auto = true`, `[limits] daily_budget_usd = 0`, `[danger] allow = []` |
7
+ | `config.toml` | `allow_project_hooks`, `[triggers] poll_interval_secs = 600` / `run_timeout_secs = 900`, `[cron] catch_up = true` / `max_concurrent_runs = 3`, `[hooks] mode = "sync"`, `[host] auto = true`, `[limits] daily_budget_usd = 0`, `[danger] allow = []`, `[engine] window_guard = true`, `[page] port = 4173` / `host = "127.0.0.1"` / `allow_host = []` |
8
8
  | `logs/pi-<pid>.log` | what each pi process diagnosed: jobs disabled, writes that failed, sub-agent warnings. Rotated at 2 MB, newest 5 processes kept |
9
9
  | `mcp.toml` | MCP servers — see [mcp.md](mcp.md) |
10
10
  | `hooks.toml` | lifecycle hooks — see [hooks.md](hooks.md) |
11
- | `ui.json` | preferences that outlive a session: `panel` (written by `/cron panel on\|off`), and `model` / `thinking` — the last ones you chose in the browser front end, applied to the next session that does not say otherwise |
11
+ | `ui.json` | preferences that outlive a session: `panel` (written by `/cron panel on\|off`); `model` / `thinking` — the last ones you chose in the browser page, applied to the next session that does not say otherwise; `host` (`"stopped"` after a manual stop); `launcher` (your answer to the launcher offer); `heldJobsNotice` (the one-time notice about plain jobs of closed conversations); `lastPage` (the conversation `GET /` opens) |
12
12
  | `jobs.json`, `triggers.json` | cron jobs and trigger rules (machine-global, each with a `cwd`) |
13
+ | `jobs.json.bak` | the last `jobs.json` that read cleanly, kept so a damaged store can be restored |
13
14
  | `state/<id>.md` | loop notes; plain Markdown, editable |
14
15
  | `inbox.jsonl` | the inbox (past 1 MB the oldest triaged findings are dropped; nothing still new) |
15
16
  | `runs.jsonl`, `triggers-audit.jsonl` | run log (halved past 1 MB) and trigger audit (2 MB) |
16
17
  | `spend.json` | per-day totals of what rotation dropped from the run log, so a daily budget still counts it |
17
18
  | `sessions/<job-id>/`, `sessions/triggers-<project>-<hash>/` | sub-agent transcripts (20 per job, 40 per project's checks; the hash of the project's path keeps two projects named alike apart) |
18
- | `scheduler.json` | timer owner on this machine: pid, heartbeat |
19
- | `next-runs.json` | job id → when it runs next, as the timer owner computed it (`{ at, next }`). Written only when an answer changes, so it can be older than the last tick; the browser panel reads it rather than parsing schedules a second time |
20
- | `presence/` | one file per live pi process: pid, session, cwd (who acts for which project) |
21
- | `host.json`, `host.log` | the headless host that keeps the clock while no pi is open: pid, and its log |
22
- | `polls.json` | last dynamic check per project (shared, so a hand-over never double-checks) |
19
+ | `next-runs.json` | job id → when it runs next, as computed by pi-loops, or by a window while pi-loops is not running (`{ at, next }`). Written only when an answer changes, so it can be older than the last tick; the browser panel reads it rather than parsing schedules a second time |
20
+ | `presence/` | records written by pi-loops 0.22 windows, read only during an upgrade |
21
+ | `host-service.json`, `host.log` | pi-loops process record (pid, version, heartbeat, the page's address and token, or why the page is waiting; 0600) and log |
22
+ | `host-spawns.json` | pi-loops start attempts, the retry backoff and the crash pause |
23
+ | `host-service.sock` | pi-loops' control socket (0600): status, abort, run, stop, and opening a conversation in the page |
24
+ | `pages/<session>.json` | one per conversation open in the page: its pi's pid and start time, and when a tab last used it (0600). A pi-loops that restarts stops the pis these name, rather than adopt them |
25
+ | `pages/args/<session>.json` | the pi arguments of the launch that last opened that conversation in the page (0600). Kept after its pi stops: a pi the page starts again for it gets the same ones |
26
+ | `leases/<session>.json` | which process holds each conversation |
27
+ | `requests/<session>.jsonl` | requests addressed to a conversation (rule results), each kept until it is in the conversation or in the inbox as a dead letter |
28
+ | `seen.json` | when each project was last looked at (`/inbox`, or the page's Mark all seen), for the "since you last looked" line and the page's away card |
29
+ | `scheduler.json` | written by pi-loops during an upgrade only, so pi-loops 0.22 windows do not run loops beside it |
30
+ | `*.lock` | short-lived locks around writes |
31
+ | `polls.json` | last dynamic check per project (shared with 0.22 windows during an upgrade, so a project is not checked twice in one interval) |
23
32
  | `dedup.json` | machine-wide trigger dedup window (5 minutes) |
24
- | `web-token` | the browser front end's token, mode 0600. It lives in a file rather than being made per launch so the address stays the same one and a signed-in device stays signed in across restarts and upgrades. Delete it to sign every device out |
33
+ | `web-token` | the browser page's token, mode 0600. It lives in a file rather than being made per start so the address stays the same one and a signed-in device stays signed in across restarts and upgrades. Delete it and run `pi-loops restart` to sign every device out. The first 0.24 start replaces a token from an earlier version once, and writes `web-token.v024` beside it to record that; a token you write into `web-token` after that is kept |
25
34
 
26
35
  Project-level: `<project>/.pi/mcp.toml` (trusted projects only) and `<project>/.pi/hooks.toml`
27
36
  (when allowed).
28
- An invalid `[triggers] poll_interval_secs` (or `--trigger-poll-secs`) is reported at startup
37
+ An invalid `[triggers] poll_interval_secs` is reported at startup
29
38
  and ignored.
30
39
 
31
40
  ## `[cron] max_concurrent_runs`
@@ -55,22 +64,57 @@ indistinguishable from a goal that was never set. They can therefore take the to
55
64
  which is why the in-flight count is reported against it (`4 of 3 sub-agent slot(s) in use`) rather than assumed
56
65
  to be under it. If you have a goal running and want headroom for it, size the setting one higher.
57
66
 
67
+ ## `[host] auto`
68
+
69
+ `true`, the default: a pi window starts pi-loops in the background when there is a loop, a rule, a
70
+ push server or a closed conversation's plain job to run, and pi-loops keeps running after every
71
+ window has closed. `false` means what it meant in 0.22.8: loops run only while a pi window is open.
72
+ The first window still starts pi-loops; loops, rule checks and `/cron run <loop>` work in that
73
+ window; and pi-loops leaves once the last window has closed, after giving the window that started it
74
+ a minute to announce itself. Nothing runs with no window open, and the line a quitting window prints
75
+ says so. The browser page counts as a window: `pi-loops` opening it starts pi-loops, and a
76
+ conversation open in the page, or a tab on one, keeps it running. pi-loops leaves once the last
77
+ terminal window, page conversation and tab are gone. A `pi-loops start --foreground` run by a service manager, or a `pi-loops start` /
78
+ `/pi-loops start` you typed, is yours to stop, and stays either way. An invalid value is reported at startup and leaves the default.
79
+
80
+ ## `[engine] window_guard`
81
+
82
+ A plain job's prompt runs under the job's own `--tools` and the dangerous-command policy
83
+ ([loops.md](loops.md)) wherever it runs, and so does the turn an MCP server's `inject_and_run` push
84
+ starts, under the dangerous-command policy alone ([mcp.md](mcp.md)). In the background nobody can be asked, so a call outside
85
+ them is refused. In an open window you can be: the same call shows a question with a 60-second
86
+ countdown and two answers, `No, refuse it` focused and `Yes, run it once` under it, so Enter
87
+ refuses. The yes answer runs it this once; anything else, Escape or no answer refuses it with the
88
+ wording the background uses, so the model does the safe part and reports. `[danger] allow` applies in both places. The guard
89
+ asks nothing about pi-loops' own tools: `cron_list` and `list_triggers` only read; `cron_create` in
90
+ such a turn makes a job capped to the turn's tools, so it widens nothing; and `new_trigger`, the
91
+ trigger tools, `cron_remove` and `set_cron_job_state` ask through their own confirm, once, the same
92
+ question with the same minute, where the background would refuse. The cap holds until the turn settles, through pi's own retries and follow-ups. A row
93
+ written into `requests/<session>.jsonl` by anything but this window, with a source that would make
94
+ it yours, runs as a scheduled turn with no declared tools: the danger check asks you. Your own
95
+ turns, a `/goal` continuation, a claimed finding, a recipe hand-off and `/new-trigger` never ask:
96
+ they are yours, and a message you send into a scheduled turn makes the rest of it yours too.
97
+
98
+ `window_guard = false` turns the cap off in windows (the background is unchanged): a scheduled
99
+ prompt in an open window then runs as your own message would, as it did in pi-loops 0.22. An
100
+ invalid value is reported at startup and leaves the guard on.
101
+
58
102
  ## Flags and environment
59
103
 
60
104
  | | |
61
105
  |---|---|
62
- | `--trigger-poll-secs <n>` | dynamic trigger poll interval for this run |
106
+ | `--trigger-poll-secs <n>` | ignored: pi-loops checks rules every `[triggers] poll_interval_secs`; passing it warns at session start |
63
107
  | `PI_LOOPS_DIR` | relocate the data directory |
64
- | `PI_WEB_TOKEN` | use this instead of the token in `web-token`. Letters, digits, `-` and `_`, at least 8 of them: it is substituted into a JavaScript string in the page, and a quote there would end the string early |
65
108
  | `PI_ALLOW_PROJECT_HOOKS=1` | allow project hooks. `true` works too; anything else is off |
66
- | `PI_LOOPS_HOST=1` | let a `pi -p` run host the timer for as long as it lives (the headless host below is the normal answer) |
109
+ | `PI_LOOPS_HOST=off` | prevent this process from starting pi-loops |
110
+ | `PI_LOOPS_PAGE_PORT=<n>` | the browser page's port, 0 to 65535 (0 takes a free one); it wins over `[page] port`. Read when pi-loops in the background starts, so set it for `pi-loops restart` |
67
111
  | `PI_LOOPS_DEBUG=1` | add a line per provider retry, compaction and tool call of a sub-agent run to `logs/pi-<pid>.log` |
68
- | `PI_BIN` | the pi executable to start, when it is not the `pi` on your `PATH` |
112
+ | `PI_BIN` | the pi executable to start for `pi-loops --tui` and for each conversation in the page; without it the page starts the pi that pi-loops in the background runs on, never the one on `PATH` |
69
113
 
70
114
  ## What is remembered between sessions
71
115
 
72
116
  A session opens on pi's default model, so choosing the same one every morning was the first thing
73
- the browser front end asked of anybody. Changing the model or the thinking level there records it in
117
+ the browser page asked of anybody. Changing the model or the thinking level there records it in
74
118
  `ui.json`, and `pi-loops` applies it when it starts the next session — the terminal window too,
75
119
  since the launcher is what applies it.
76
120
 
@@ -80,10 +124,33 @@ their conversation was had with. Delete the keys from `ui.json` to go back to pi
80
124
 
81
125
  A `--model` — yours or the remembered one — lasts as long as the process, not as long as the
82
126
  session: pi re-resolves it every time the session inside the process is replaced. So **resume** in
83
- the browser front end re-applies the model the session it is going back to was last using, which is
127
+ the browser page re-applies the model the session it is going back to was last using, which is
84
128
  what a fresh `pi --resume` would have done.
85
129
 
86
- ## The browser front end
130
+ ## `[page]`
131
+
132
+ The browser page's address. pi-loops in the background serves it, so it is set here rather than on
133
+ the command line, and a change takes effect after `pi-loops restart`. Full explanation in
134
+ [cli.md](cli.md#the-address).
135
+
136
+ ```toml
137
+ [page]
138
+ port = 4173
139
+ host = "127.0.0.1"
140
+ allow_host = []
141
+ ```
142
+
143
+ | | |
144
+ |---|---|
145
+ | `port` | default 4173, fixed on purpose so the address is worth bookmarking. Unset, only the default loops directory uses 4173: a loops directory moved with `PI_LOOPS_DIR` takes a free port, so two of them do not fight over one. Set here, it is used for any loops directory, so a moved one keeps its address across restarts. `PI_LOOPS_PAGE_PORT` wins over both. A port another program holds costs the page, never the loops: `pi-loops status` and `host.log` say so |
146
+ | `host` | the address to bind. Anything but loopback puts the page on the local network, unencrypted: `host.log` says `page on the local network, unencrypted: http://<ip>:<port>/`, and anyone on the wire can read the cookie. `tailscale serve` gets a phone in over TLS with the page still on loopback |
147
+ | `allow_host` | names the page answers to in the `Host` header besides loopback and a tailnet's, for a reverse proxy in front. A name resolved by public DNS puts the token alone in charge of keeping strangers out |
148
+
149
+ Tailnet names (`*.ts.net`) are accepted from a tailnet connection without `allow_host`, which is
150
+ what makes `tailscale serve --bg 4173` work with the page still on loopback. An invalid value is
151
+ reported at startup and leaves the default.
152
+
153
+ ### The browser page
87
154
 
88
155
  `pi-loops` takes these for itself and passes everything else to pi. Full explanation in
89
156
  [cli.md](cli.md); this is the list.
@@ -91,15 +158,10 @@ what a fresh `pi --resume` would have done.
91
158
  | | |
92
159
  |---|---|
93
160
  | `--web` / `--tui` | which window, when the guess is wrong |
94
- | `--port <n>` | default 4173, fixed on purpose so the address is worth bookmarking |
95
- | `--host <addr>` | bind somewhere other than loopback, so a phone on the same network can reach it. Prints the addresses it can be reached on, and says that they are unencrypted |
96
- | `--allow-host <name,…>` | accept these values in the `Host` header, for a reverse proxy in front. A name resolved by public DNS puts the token back in charge of keeping strangers out |
97
- | `--no-auth` | no token and no cookie. Loopback only, whatever route a request took, and refused outright with `--host` |
98
- | `--no-open` | do not open a browser |
161
+ | `--no-open` | print the page's address without opening a browser |
99
162
  | `--loops-dir <dir>` | the same thing `PI_LOOPS_DIR` does |
100
-
101
- Tailnet names (`*.ts.net`) are accepted from a tailnet connection without `--allow-host`, which is
102
- what makes `tailscale serve --bg 4173` work with the server still on loopback.
163
+ | `--port`, `--host`, `--allow-host` | refused (exit 2) with the `[page]` lines that replace them |
164
+ | `--no-auth` | refused (exit 2): gone in 0.24, the page is always behind its token |
103
165
 
104
166
  ## Per-job options (`/cron add`)
105
167