@alphacoder-v0/pi-loops 0.17.6 → 0.19.2

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 (48) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +13 -6
  3. package/README.zh-CN.md +19 -6
  4. package/docs/agents/issue-tracker.md +34 -0
  5. package/docs/cli.md +22 -2
  6. package/docs/configuration.md +1 -1
  7. package/docs/design.md +63 -6
  8. package/docs/loops.md +25 -17
  9. package/docs/recipes.md +239 -0
  10. package/docs/triggers.md +1 -1
  11. package/docs/troubleshooting.md +3 -2
  12. package/docs/web-ui-parity.md +9 -3
  13. package/package.json +2 -1
  14. package/recipes/_tracker/issue-tracker-github.md +31 -0
  15. package/recipes/_tracker/issue-tracker-local.md +38 -0
  16. package/recipes/_tracker-setup.md +12 -0
  17. package/recipes/autoresearch/README.md +27 -0
  18. package/recipes/autoresearch/RESEARCH.template.md +37 -0
  19. package/recipes/autoresearch/example/RESEARCH.md +31 -0
  20. package/recipes/autoresearch/example/eval.py +60 -0
  21. package/recipes/autoresearch/example/eval.sh +3 -0
  22. package/recipes/autoresearch/example/solution.py +20 -0
  23. package/recipes/autoresearch/experiment.md +82 -0
  24. package/recipes/autoresearch/recipe.toml +15 -0
  25. package/recipes/changelog-draft/recipe.toml +9 -0
  26. package/recipes/changelog-draft/release.md +35 -0
  27. package/recipes/issue-loop/README.md +16 -0
  28. package/recipes/issue-loop/implement.md +135 -0
  29. package/recipes/issue-loop/labels.sh +21 -0
  30. package/recipes/issue-loop/recipe.toml +18 -0
  31. package/recipes/issue-loop/triage.md +147 -0
  32. package/skills/pi-loops/SKILL.md +3 -1
  33. package/src/archive.ts +5 -5
  34. package/src/args.ts +1 -8
  35. package/src/cli.ts +83 -6
  36. package/src/job-edit.ts +0 -3
  37. package/src/job-health.ts +6 -7
  38. package/src/job-owner.ts +29 -0
  39. package/src/pi-loops.ts +339 -25
  40. package/src/protocol.ts +2 -3
  41. package/src/recipe.ts +651 -0
  42. package/src/scheduler.ts +13 -13
  43. package/src/session-head.ts +98 -0
  44. package/src/store.ts +9 -17
  45. package/src/tools.ts +17 -19
  46. package/src/trigger-runtime.ts +3 -5
  47. package/src/triggers.ts +3 -4
  48. package/src/web.mjs +40 -17
package/CHANGELOG.md CHANGED
@@ -3,6 +3,97 @@
3
3
  All notable changes to pi-loops are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer.
5
5
 
6
+ ## [0.19.2] - 2026-09-14
7
+
8
+ ### Fixed
9
+ - **A plain job of another session was promised a next run, could be run into the wrong chat, and
10
+ was called dormant as if that were a fault** (#15). A plain job belongs to the session that
11
+ created it — its result is a message in that conversation — and dispatch has always honoured
12
+ that; the lists did not. `cron_list` printed a `next_run` for a job this process would never
13
+ dispatch (the same class the `[other host]` fix closed for the machine half of the predicate),
14
+ the browser panel showed the time it read from `next-runs.json`, and `/cron run` injected the job
15
+ into whatever chat was open. On 2026-09-14 a daily digest missed its slot exactly this way, and
16
+ the only surface that knew was the terminal's `[dormant …]` marker. Now one predicate
17
+ (`src/job-owner.ts`) answers "does it run here" for dispatch, `/cron`, `cron_list`, the panel and
18
+ `/cron run`: another session lists it as `[session <id> — resume it to run]` with no next run,
19
+ `cron_list` says the same and sets `owner_session` / `asleep`, the panel says what would wake it,
20
+ and `/cron run` refuses it with the same sentence. "Asleep" replaces "dormant" in the vocabulary:
21
+ it is waiting for its conversation, not broken. The `catch_up` description now says when a
22
+ plain job's missed slot is honoured (when its session is next opened); the skill tells the model
23
+ that anything meant to run unattended is a loop. Parking stays what it was and is now documented
24
+ as such: a plain job is disabled once its session's file has been deleted, not before.
25
+
26
+ ## [0.19.1] - 2026-09-14
27
+
28
+ ### Changed
29
+ - **`pi-loops sessions` says what a session is, not only which.** It printed the id, the mtime and
30
+ the cwd, so telling two sessions apart meant opening them. It now prints one line a person can
31
+ tell them apart by: the short id, when the session started, the automation it has
32
+ (`[2 cron, 1 trigger]`, `[automation off]`), and what was first said in it — the name it was
33
+ given, or the first message cut at eighty characters, by characters and never inside one.
34
+ `--all` puts the cwd after the id. `src/session-head.ts` reads the head of a session file for
35
+ it; the browser front end's own copy of that reading is unchanged.
36
+ - **The session commands are two words in the browser front end.** `/sessions` lists this
37
+ project's sessions in the feed, and `/session export [path]` / `/session import <path>` are the
38
+ archive commands, delivered as `/session-export` / `/session-import`. pi's built-in `/session` is
39
+ a terminal command that does not exist over rpc — that is why the extension's commands are
40
+ hyphenated in the terminal, and why the page can spell them plainly.
41
+
42
+ ## [0.19.0] - 2026-09-14
43
+
44
+ ### Removed
45
+ - **The machine, as a concept.** Every job and rule used to carry the hostname that created it;
46
+ another machine sharing the `$HOME` ignored it and listed it as `[other host: <name>]`,
47
+ `/cron set <ref> --host here|-` and `/triggers set --host` re-homed it, `cron_list` printed
48
+ `other_host:`, the leader and next-runs files were named `scheduler.<host>.json` and
49
+ `next-runs.<host>.json`, and an imported archive was re-stamped for the importing machine. None
50
+ of it matches the way pi-loops is used: pi runs on one machine, the loops run
51
+ there, and a phone or a laptop reaches the browser front end over a tailnet without moving the
52
+ execution anywhere. All of it is gone. A `host` field an older build left in `jobs.json`,
53
+ `triggers.json` or a `.pisession` archive is ignored when read and dropped on the next write; the
54
+ leader file is `scheduler.json`, the next-runs file `next-runs.json`. Two machines syncing one
55
+ `$HOME` now both run every job — not supported, and said so rather than half-handled
56
+ (`docs/design.md` §19). "Host" means one thing here now: the headless process.
57
+
58
+ ## [0.18.0] - 2026-09-14
59
+
60
+ ### Added
61
+ - **Recipes: a project run on loops, installed in one command.** A recipe is a directory —
62
+ `recipe.toml`, one playbook per job, an optional setup script — and `/recipe add <name>` installs
63
+ it into the open project: one question (the autonomy level, `report` / `propose` / `act`, written
64
+ as the first line of each playbook), one confirmation showing every file, the setup script and
65
+ every `/cron add` line, then the jobs through the same code path `/cron add` uses. The playbooks
66
+ are copied to `.agents/skills/<name>/` — where pi also discovers them as `/skill:` commands — and
67
+ the directory is listed in `.git/info/exclude`, so the repository is untouched; a job's prompt is
68
+ a pointer to its playbook, read fresh on every run, so editing the file changes the next run.
69
+ `/recipe update` is a three-way merge against the untouched copy kept at install (under `.orig/`), and a conflict
70
+ is handed to the agent to resolve with you rather than left as markers in a file a loop would
71
+ follow. `/recipe remove` takes the jobs and leaves the files unless `--purge`. Each `[[job]]` in a
72
+ manifest is validated by building the `/cron add` line it stands for and parsing it with the same
73
+ parser. The one step no template can do — describing this project's tracker — is handed to the
74
+ session with the prompt in `recipes/_tracker-setup.md`, the way `/inbox claim` hands over a
75
+ finding, written to `docs/agents/issue-tracker.md` in the layout Matt Pocock's engineering
76
+ skills use and excluded from the repository like the playbooks — and the wizard resumes on its
77
+ own once that turn ends with the file in place. `pi-loops recipe list|show|add` in a shell does the deterministic part and stops
78
+ before the setup script and the jobs. `docs/recipes.md`; vocabulary in `CONTEXT.md`, decisions in
79
+ `docs/design.md` §15–18.
80
+ - **Two recipes.** `issue-loop`: the tracker as a state machine (Matt Pocock's five triage states
81
+ plus `agent-working`, `in-review`, `agent-blocked`) that two loops turn, a triage loop that
82
+ reproduces, checks `.out-of-scope/` and writes agent briefs, and an implement loop that claims
83
+ one issue per run, builds it in a worktree and opens the pull request; a person promotes and
84
+ merges. `autoresearch`: one experiment per run against a `RESEARCH.md` contract you wrote, a
85
+ `results.tsv` ledger where retired ideas stay as negative evidence, a worktree per experiment,
86
+ and promotion only when the `--verify` checker has re-run the held-out command itself; with a
87
+ pure-Python exact k-NN example whose baseline is a speedup of 1.0. `changelog-draft` is packaged
88
+ as the release-side half of the issue loop.
89
+
90
+ ### Fixed
91
+ - **The browser front end reported a slash command as timed out while it waited in a dialog.** A
92
+ `/…` prompt is answered by pi when the command has finished, and a command that shows a setup
93
+ script and asks for a yes finishes when the person answers; sixty seconds later the feed said
94
+ `timed out after 60000ms` and the install went on to succeed. A slash command now gets fifteen
95
+ minutes; everything else keeps sixty seconds.
96
+
6
97
  ## [0.17.6] - 2026-09-14
7
98
 
8
99
  ### Added
package/README.md CHANGED
@@ -38,7 +38,7 @@ the first sign of credentials that do not work should not be an empty inbox tomo
38
38
 
39
39
  ```bash
40
40
  pi install npm:@alphacoder-v0/pi-loops # from npm, following new releases
41
- pi install git:github.com/alphacoder-v0/pi-loops@v0.17.6 # or from GitHub, at a pinned tag
41
+ pi install git:github.com/alphacoder-v0/pi-loops@v0.19.2 # or from GitHub, at a pinned tag
42
42
  pi install /path/to/pi-loops # or a local checkout — `pi install .` in this repo
43
43
  pi -e /path/to/pi-loops # or none of them: try it for one run, installing nothing
44
44
  ```
@@ -138,6 +138,11 @@ A job records its directory and model at creation, so it is not tied to the wind
138
138
  `--cwd` runs it in another checkout (absolute, or relative to this project — no shell, so nothing
139
139
  expands `~`), and `--model` pins it whatever this session is on (`/cron set <ref> --model -` unpins).
140
140
 
141
+ And two you do not write at all: `/recipe add issue-loop` runs the issue tracker as a state machine
142
+ that two loops turn, `/recipe add autoresearch` runs one experiment per run against a contract you
143
+ wrote — each a directory of playbooks copied into the project, installed with one question and one
144
+ confirmation ([docs/recipes.md](docs/recipes.md)).
145
+
141
146
  Two that are not loops:
142
147
 
143
148
  ```text
@@ -263,7 +268,7 @@ State stays in `~/.pi/agent/loops` until you delete it.
263
268
 
264
269
  | Command | What it does |
265
270
  |---|---|
266
- | `/cron add [--stateful] [--verify] "<schedule>" <prompt>` | Schedule a job: plain jobs inject their result into this chat, `--stateful` makes a loop with memory and inbox routing, `--verify` adds the checker |
271
+ | `/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 |
267
272
  | `/cron`, `/cron all` | This project's jobs, or every project on this machine |
268
273
  | `/cron enable\|disable\|remove <ref>` | Pause, resume or delete one job — and write an audit entry into the session, as `add` does |
269
274
  | `/cron disable --all` | Pause every job in this project (`--all-projects` for the machine); `/cron enable --all` resumes |
@@ -284,11 +289,12 @@ State stays in `~/.pi/agent/loops` until you delete it.
284
289
  | `/new-trigger <natural language>` | Create a condition-based rule ("when ~/build.done exists, run cargo test") |
285
290
  | `/triggers [status\|rules\|enable\|disable\|remove\|running\|audit [N]\|abort]` | Dynamic rules: what exists, what is running, what happened |
286
291
  | `/triggers run <id>` | Check one rule now instead of waiting for its poll slot |
287
- | `/triggers set <id> --model\|--thinking\|--timeout\|--host` | Change what a rule runs with — the settings that decide how an unattended action behaves |
292
+ | `/triggers set <id> --model\|--thinking\|--timeout` | Change what a rule runs with — the settings that decide how an unattended action behaves |
288
293
  | `/triggers sources`, `/triggers hooks` | Every source feeding the trigger runtime: each connected MCP server, whose notifications can fire a rule; the local crontab; the dynamic checker |
289
294
  | `/triggers panel [on\|off]` | The same panel, toggled from the trigger side |
290
- | `/session-export [path]`, `/session-import <path>` | Portable `.pisession` archive: transcript + jobs + rules + loop state |
295
+ | `/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) |
291
296
  | `/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)) |
297
+ | `/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)) |
292
298
  | `/pi-loops [install-launcher]` | Version and paths; `install-launcher` puts the `pi-loops` command on your `PATH` |
293
299
 
294
300
  A schedule is a 5-field cron expression, or one of `hourly` / `daily` / `weekly` (also `每天`), or
@@ -317,6 +323,7 @@ notifications, and lifecycle hooks — each has a page here.
317
323
  - [docs/mcp.md](docs/mcp.md) — MCP notification sources and tool registration (`mcp.toml`)
318
324
  - [docs/hooks.md](docs/hooks.md) — lifecycle hooks (`hooks.toml`), including `run_start` / `run_end` for scheduled runs
319
325
  - [docs/goal.md](docs/goal.md) — `/goal`: holding a session to a stop condition
326
+ - [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
320
327
  - [docs/session-archive.md](docs/session-archive.md) — `/session-export`, `/session-import`
321
328
  - [docs/cli.md](docs/cli.md) — the `pi-loops` command line: export, import, and looking in on the host
322
329
  - [docs/web-ui-parity.md](docs/web-ui-parity.md) — what the browser front end owes you, line by line
@@ -337,7 +344,7 @@ notifications, and lifecycle hooks — each has a page here.
337
344
  | `~/.pi/agent/loops/logs/pi-<pid>.log` | what each pi process's automation did |
338
345
  | `~/.pi/agent/loops/triggers.json`, `triggers-audit.jsonl` | dynamic rules and trigger audit |
339
346
  | `~/.pi/agent/loops/{config,mcp,hooks}.toml` | configuration |
340
- | `~/.pi/agent/loops/scheduler.<host>.json` | which pi process currently owns the timer |
347
+ | `~/.pi/agent/loops/scheduler.json` | which pi process currently owns the timer |
341
348
 
342
349
  Set `PI_LOOPS_DIR` to relocate all of it. The notes are yours to edit when the agent got something
343
350
  wrong, and `logs/pi-<pid>.log` is the file to read after an overnight failure.
@@ -347,7 +354,7 @@ wrong, and `logs/pi-<pid>.log` is the file to read after an overnight failure.
347
354
  Scheduled work is only worth trusting if it survives you closing the editor. So "pi was restarted"
348
355
  is treated here as the normal case rather than the exception.
349
356
 
350
- Jobs are machine-global, recorded per host, and never expire. Any open pi can own the timer:
357
+ Jobs are machine-global and never expire. Any open pi can own the timer:
351
358
  leadership is a file with a heartbeat, and when the process holding it exits or dies, the next tick
352
359
  in another window picks it up. A project's trigger checks run in a pi that is open in that project,
353
360
  so a result that belongs in a conversation lands in the right one. A tick a loop missed while
package/README.zh-CN.md CHANGED
@@ -45,7 +45,7 @@ pi-loops 自己没有任何运行时依赖。
45
45
 
46
46
  ```bash
47
47
  pi install npm:@alphacoder-v0/pi-loops # 从 npm 装,跟着新版本走
48
- pi install git:github.com/alphacoder-v0/pi-loops@v0.17.6 # 或者从 GitHub 装,钉住一个 tag
48
+ pi install git:github.com/alphacoder-v0/pi-loops@v0.19.2 # 或者从 GitHub 装,钉住一个 tag
49
49
  pi install /path/to/pi-loops # 或者本地检出;本仓库里就是 pi install .
50
50
  ```
51
51
 
@@ -215,6 +215,19 @@ pi update --extensions # 对齐已安装的包
215
215
 
216
216
  任务在创建时就记下自己的目录和模型,所以它不绑在敲出它的那个窗口上。`--cwd` 让它在另一个检出里跑——绝对路径,或相对当前项目;这里没有 shell,`~` 不会展开。`--model` 把模型钉死,`/cron set <ref> --model -` 解钉。
217
217
 
218
+ ### 不用自己写的:recipe
219
+
220
+ ```text
221
+ /recipe 有哪些打包好的 recipe,这个项目装了哪些
222
+ /recipe add issue-loop 把 issue tracker 跑成状态机:分诊成 agent brief、在 worktree 里实现、开 PR;推进和合并留给人
223
+ /recipe add autoresearch 按你写的研究合同每次跑一个实验,账本记下每次尝试,只凭 held-out 数据提议晋升
224
+ ```
225
+
226
+ 一个 recipe 是一组 loop 加上它们各自每次 run 都会读的 playbook。安装只问一个问题(自治级别
227
+ `report` / `propose` / `act`)、确认一次(每个要写的文件、setup 脚本全文、每一行 `/cron add`),
228
+ playbook 拷到 `.agents/skills/<name>/`,通过 `.git/info/exclude` 排除在仓库之外——你随时可以改它,
229
+ 下一次 run 就照改后的做。见 [docs/recipes.md](docs/recipes.md)。
230
+
218
231
  ## 用法
219
232
 
220
233
  会话里的命令分几摊:`/cron` 管任务,`/inbox` 管分诊,`/goal` 管「做到什么算完」,`/triggers` 管不看钟的那些规则,`/session-share` 把这次会话脱敏后发出去。
@@ -228,7 +241,7 @@ pi update --extensions # 对齐已安装的包
228
241
  /cron · /cron list|ls|status 本项目的任务,[stateful] 标记;/cron all 看整台机器
229
242
  /cron enable|resume|disable|pause|remove <n|id|name>
230
243
  /cron set <ref> … 改已有任务而不换 id(笔记因此留着):--prompt、--schedule,
231
- 以及 --model、--thinking、--timeout、--name、--host 这几个钉子(`-` 解掉)
244
+ 以及 --model、--thinking、--timeout、--name 这几个钉子(`-` 解掉)
232
245
  /cron run 1 立刻跑一次(once 跑完即删,every 的间隔从现在重算,cron 的下次不变;停用的普通任务会被拒绝)
233
246
  /cron state ci loop 的笔记(状态脊柱)
234
247
  /cron runs [ci] 最近运行,最新在前
@@ -299,7 +312,7 @@ at 2026-09-08 → UTC 零点 ← 只给日期按 UTC,在北京就
299
312
 
300
313
  要表示时间就把时间写出来。另外 `in 10m` / `at` 在**创建那一刻**就被算成一个绝对时刻存下来,之后改时区不影响它;`every 30m` 是纯间隔,和时区、夏令时都无关。
301
314
 
302
- **跨机器的 loop**:没有 `host` 的任务会在任何共享这个 `$HOME` 的机器上跑——`/cron set <ref> --host -` 干的就是把 `host` 这个键从任务上摘掉。stateful loop 的笔记是模型自己写的自由文本,里面常有 watermark(「看到这里为止」),所以 prompt 会要求模型写时间时带上偏移。0.14.1 之前写下的笔记没有这个保障,跨时区又带 watermark 的 loop 值得用 `/cron state <id>` 看一眼。
315
+ **一台机器**:pi-loops 在运行 pi 的那台机器上跑,没有跨机器的概念(0.19.0 去掉了任务上的主机名戳;两台机器同步一个 `$HOME` 会各跑一遍,不支持)。要在别处看它,走 tailnet 开浏览器前端。stateful loop 的笔记是模型自己写的自由文本,里面常有 watermark(「看到这里为止」),而机器的时区会变(设了 `TZ`、带着笔记本出差、容器重建成 UTC),所以 prompt 会要求模型写时间时带上偏移。0.14.1 之前写下的笔记没有这个保障,时区变过又带 watermark 的 loop 值得用 `/cron state <id>` 看一眼。
303
316
 
304
317
  ## Maker/checker:先质疑每条 finding,再给你看
305
318
 
@@ -473,8 +486,8 @@ Output protocol (mandatory):
473
486
 
474
487
  定时任务值不值得信,取决于你关掉编辑器之后会发生什么。所以这里把「pi 重启过」当成常态:
475
488
 
476
- 1. **任务是机器全局的(按主机)**,存在 `~/.pi/agent/loops/jobs.json`,不绑会话、不绑目录(每个任务记住自己的 `cwd` 与 `host`,子代理在那里跑;共享 $HOME 的另一台机器会忽略它)。任何目录里打开的任何 pi 都能看到并执行 stateful 任务;普通任务因为要注入对话,只在创建它的那个会话里触发(`--resume` 回来就继续),会话没了,leader 会把它停用,`/cron gc` 清掉;子代理创建的普通任务归它所服务的那个父会话。
477
- 2. **loop 归本机唯一的 leader 跑;动态检查归「开在那个项目里的那个 pi」跑。** `scheduler.<host>.json` 里放 pid 加心跳,30 秒一 tick;leader 退出或者崩了,其它 pi 在下一 tick 接手。
489
+ 1. **任务是机器全局的**,存在 `~/.pi/agent/loops/jobs.json`,不绑会话、不绑目录(每个任务记住自己的 `cwd`,子代理在那里跑)。任何目录里打开的任何 pi 都能看到并执行 stateful 任务;普通任务因为要注入对话,只在创建它的那个会话里触发(`--resume` 回来就继续),会话没了,leader 会把它停用,`/cron gc` 清掉;子代理创建的普通任务归它所服务的那个父会话。
490
+ 2. **loop 归本机唯一的 leader 跑;动态检查归「开在那个项目里的那个 pi」跑。** `scheduler.json` 里放 pid 加心跳,30 秒一 tick;leader 退出或者崩了,其它 pi 在下一 tick 接手。
478
491
 
479
492
  每个进程每 tick 往 `presence/` 登记自己的 pid、会话和 cwd。一个项目的规则检查和推送评估,就交给开在那个项目里的 pi——优先创建规则的那个会话,其次 pid 最小的那个——所以 promote_to_chat 一定落在对的对话里。项目里一个 pi 都没开,才轮到 leader 代跑,结果进 inbox。
480
493
 
@@ -503,7 +516,7 @@ Output protocol (mandatory):
503
516
  | `~/.pi/agent/loops/spend.json` | 轮转掉的那部分花费按天留一份,预算上限不会因为 run log 被截断而失效 |
504
517
  | `~/.pi/agent/loops/logs/pi-<pid>.log` | 每个 pi 进程的自动化诊断,超 2 MB 保留后半,只留最近五个进程(进程还活着的那份不算) |
505
518
  | `~/.pi/agent/loops/sessions/<id>/*.jsonl` | 子代理完整 transcript,每个 loop 保留最近 20 份 |
506
- | `~/.pi/agent/loops/scheduler.<host>.json` | 当前 leader 的 pid / 心跳(每台机器一份) |
519
+ | `~/.pi/agent/loops/scheduler.json` | 当前 leader 的 pid / 心跳 |
507
520
  | `~/.pi/agent/loops/triggers.json` | 动态 trigger 规则(全局,带 cwd) |
508
521
  | `~/.pi/agent/loops/triggers-audit.jsonl` | trigger audit,超 2 MB 保留后半 |
509
522
  | `~/.pi/agent/loops/sessions/triggers-<项目>-<哈希>/*.jsonl` | 动态检查子代理的 transcript,每个项目一个目录(目录名带项目路径的哈希,同名的两个项目不会共用),保留最近 40 份 |
@@ -0,0 +1,34 @@
1
+ # Issue tracker: GitHub
2
+
3
+ Work items for this repository live as GitHub Issues in `alphacoder-v0/pi-loops`; pull requests
4
+ are the review surface. Use the `gh` CLI for every operation; it infers the repository from the
5
+ remote when run in a clone. Loops shell out to `gh`, so the account must be authenticated in the
6
+ environment a loop runs in — `gh auth status` answers that.
7
+
8
+ ## Operations
9
+
10
+ - **List**: `gh issue list --state open --limit 200 --json number,title,labels,updatedAt,author` (add `--label <state>` to filter by state)
11
+ - **Read**: `gh issue view <n> --comments --json number,title,body,labels,author,createdAt,comments`
12
+ - **Comment**: `gh issue comment <n> --body "..."` (a heredoc for multi-line bodies)
13
+ - **Change state**: `gh issue edit <n> --add-label "<new state>" --remove-label "<old state>"` — exactly one state label at a time
14
+ - **Claim**: `gh issue edit <n> --add-label agent-working --remove-label ready-for-agent --add-assignee @me`
15
+ - **Close**: `gh issue close <n> --comment "..."`
16
+ - **Pull requests**: `gh pr create`, `gh pr list --state open --json number,url,headRefName,reviewDecision,comments,updatedAt`, `gh pr view <n> --comments`, `gh pr diff <n>`
17
+
18
+ ## State vocabulary
19
+
20
+ One category label (`bug` | `enhancement`) and exactly one state label per open issue. The state
21
+ labels are `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `agent-working`,
22
+ `in-review`, `agent-blocked`, `wontfix`. They already exist in this repository; `labels.sh`
23
+ refreshes their colour and description and touches no issue. A playbook that moves an issue names
24
+ the label it moves to.
25
+
26
+ ## Blocking
27
+
28
+ A `Blocked by: #<n>, #<m>` line near the top of an issue body (or the tracker's native
29
+ dependency) gates it: the item is unblocked when every listed issue is closed.
30
+
31
+ ## Who counts as the agent
32
+
33
+ Comments and assignments made by loops are made as the account `gh` is signed in as — currently
34
+ `alphacoder-v0`. Every comment a loop posts starts with a line saying it was generated by AI.
package/docs/cli.md CHANGED
@@ -213,8 +213,12 @@ pi-loops host status | abort <run-id|trace-id> | stop
213
213
 
214
214
  ## sessions, inspect
215
215
 
216
- `sessions` lists the ids `export --session` accepts, newest first — without it an unknown id was
217
- the only error you could get. `inspect` prints an archive's schedules, prompts and rules without
216
+ `sessions` lists this project's sessions newest first, one line each: the first sixteen
217
+ characters of the id, when the session started, the automation it
218
+ has (`[2 cron, 1 trigger]`, or `[automation off]` when all of it is disabled), and what was first
219
+ said in it — the session's name if it was given one, otherwise the first message, cut at eighty
220
+ characters. `--all` lists every project and puts the cwd after the id. The id is what
221
+ `export --session` accepts; a unique prefix will do. `inspect` prints an archive's schedules, prompts and rules without
218
222
  writing anything, which is what you want before restoring on a machine you care about.
219
223
 
220
224
  ## export
@@ -260,3 +264,19 @@ accepted). `host stop` ends the host; the next pi to open would have taken the c
260
264
 
261
265
  The channel is a unix socket at `host.sock` in the loops directory, created 0600, and it is
262
266
  read-mostly on purpose: a host you could prompt would be a second chat.
267
+
268
+ ## recipe
269
+
270
+ ```sh
271
+ pi-loops recipe list # the packaged recipes, and which are installed in this directory
272
+ pi-loops recipe show issue-loop # its files, jobs, levels, setup script
273
+ pi-loops recipe add issue-loop [--cwd <dir>] [--level report|propose|act] [--overwrite]
274
+ ```
275
+
276
+ `add` is the deterministic half of `/recipe add` ([recipes.md](recipes.md)): it copies the
277
+ playbooks to `.agents/skills/<name>/`, lists that directory in `.git/info/exclude` and writes the
278
+ record — and stops. The setup script and the jobs need a pi open in the project: `/recipe add
279
+ <name>` there finds the copies in place, shows the script and the `/cron add` lines, and creates
280
+ them after you say yes. Without `--level` the lowest level the recipe supports is written; an edited
281
+ copy is kept unless `--overwrite` says otherwise.
282
+
@@ -15,7 +15,7 @@ Everything lives under `~/.pi/agent/loops/` (override: `PI_LOOPS_DIR`).
15
15
  | `runs.jsonl`, `triggers-audit.jsonl` | run log (halved past 1 MB) and trigger audit (2 MB) |
16
16
  | `spend.json` | per-day totals of what rotation dropped from the run log, so a daily budget still counts it |
17
17
  | `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.<host>.json` | timer owner on this host: pid, heartbeat |
18
+ | `scheduler.json` | timer owner on this machine: pid, heartbeat |
19
19
  | `presence/` | one file per live pi process: pid, session, cwd (who acts for which project) |
20
20
  | `host.json`, `host.log` | the headless host that keeps the clock while no pi is open: pid, and its log |
21
21
  | `polls.json` | last dynamic check per project (shared, so a hand-over never double-checks) |
package/docs/design.md CHANGED
@@ -24,19 +24,21 @@ The premise is that **"pi was restarted" is the normal case**. Automation that d
24
24
  it was configured in is automation you cannot rely on, so jobs and rules live on disk for the whole
25
25
  machine. Everything here follows from that, and each one has a price.
26
26
 
27
- 1. **Machine-global jobs and rules, each carrying a `cwd` and a `host`.** `/cron` and
27
+ 1. **Machine-global jobs and rules, each carrying a `cwd`.** `/cron` and
28
28
  `/triggers rules` show the current project by default, and say how many are elsewhere. A plain
29
- (inject) job belongs to the session that created it: listed as `[dormant …]` while that session
30
- is not open, parked as disabled once the session no longer exists (`/cron gc` removes it). A loop
29
+ (inject) job belongs to the session that created it — its result is a message in that
30
+ conversation — and everything that lists it agrees with dispatch about that: another session
31
+ shows it as `[session <id> — resume it to run]` with no next run, `cron_list` says the same to
32
+ the model, `/cron run` refuses it. Asleep is not broken. It is parked as disabled only once its
33
+ session's file has been deleted (`/cron gc` removes it). A loop
31
34
  whose `cwd` disappeared waits half an hour — a mount can be late at boot — and is disabled after
32
- that. Jobs and rules stamped with another host (a shared `$HOME`) are ignored here and listed
33
- with whose they are; leader election is per host.
35
+ that.
34
36
 
35
37
  The cost: a list has to be scoped, and a job you cannot see reads as a job that is gone. Hence
36
38
  the counts, the markers, and `/cron all`.
37
39
 
38
40
  2. **Who runs what.** Every process ticks. Loops run in the machine leader
39
- (`scheduler.<host>.json`, a pid and a heartbeat). A project's dynamic checks and push
41
+ (`scheduler.json`, a pid and a heartbeat). A project's dynamic checks and push
40
42
  evaluations run in a pi that is *open in that project* — preferring the session that created the
41
43
  rules, lowest pid otherwise (`presence/`) — so a promotion lands in the conversation it belongs
42
44
  to. Only a project with no pi open falls to the leader, and its results go to the inbox
@@ -117,6 +119,61 @@ machine. Everything here follows from that, and each one has a price.
117
119
  failed — the slot is still owed and the failure streak does not advance. Diagnostics go to
118
120
  `logs/pi-<pid>.log` rather than only to a chat notification that a new session erases.
119
121
 
122
+ 15. **Recipes are files, not code.** A recipe (`CONTEXT.md` has the vocabulary) is a directory:
123
+ a `recipe.toml` manifest whose `[[job]]` fields are the arguments of `/cron add` and nothing
124
+ else, one playbook per job, an optional setup script. Anything the wizard does with one is
125
+ something a person could have done by hand with `/cron add` and `cp`. The first catalogue is
126
+ seven: issue-loop, daily-digest, changelog-draft, pr-watch, ci-sweeper, autoresearch, and
127
+ ecosystem — the last capped at `propose`, because every outward word it drafts is a finding a
128
+ person claims before it is said.
129
+
130
+ The cost: a project cannot add a recipe by writing TypeScript, only by writing a directory,
131
+ and the wizard can only ask what a manifest can declare.
132
+
133
+ 16. **Playbooks are copied into the project and kept out of its history.** `/recipe add` copies
134
+ them to `<project>/.agents/skills/<recipe>/` — where pi also discovers them as `/skill:`
135
+ commands, so a person can run one step by hand — and lists the directory in
136
+ `.git/info/exclude`, so the repository is not touched. A loop's prompt is a pointer to that
137
+ file; the procedure is read fresh on every run. The tracker description the playbooks read
138
+ (`docs/agents/issue-tracker.md`, in the layout Matt Pocock's engineering skills use, so a
139
+ repository that has those reads the same file) is excluded the same way: it names an account
140
+ and a workflow, which are the project's to keep and not the repository's to publish.
141
+
142
+ The cost: two clones of one project each install the recipe, and a playbook edited on one
143
+ machine is not on the other. Upgrading pi-loops upgrades the packaged recipes, not the copies:
144
+ `/recipe update` is a three-way merge against the untouched copy kept at install (under
145
+ `.orig/` beside the playbooks), silent when the person changed nothing, and conflicts are handed to the
146
+ session as a prompt to resolve with the person rather than left as markers in a file the next
147
+ run will read.
148
+
149
+ 17. **The wizard is deterministic; one step is not.** Choosing a recipe, its autonomy level, the
150
+ copy and the `/cron add` lines run with no model and end in a confirmation that shows exactly
151
+ what will be created. The one thing a template cannot do — describe *this* project's tracker —
152
+ is handed to the running session with `sendUserMessage`, the way `/inbox claim` hands over a
153
+ finding: explore the remote, propose, confirm, write the file — and when that turn settles with
154
+ the file there, the wizard resumes by itself, so the command is typed once. `pi-loops recipe`
155
+ in the terminal does the deterministic part only.
156
+
157
+ 18. **One autonomy dial, three positions, in prose.** `report` reads and files findings; `propose`
158
+ may also write to the tracker and open draft pull requests but never reach a terminal state;
159
+ `act` may. A manifest declares which positions a recipe supports, the wizard defaults to the
160
+ lowest, and the chosen one is a line at the top of each playbook — text the model reads and a
161
+ person edits, enforced by nothing in code. The cost is exactly that: the level is an instruction,
162
+ not a permission, and the permissions that exist (the danger policy, the daily budget, branch
163
+ protection on the remote) are the ones that hold.
164
+
165
+ 19. **One machine.** Until 0.19.0 every job and rule carried the hostname that created it, another
166
+ machine sharing the `$HOME` ignored it and listed it as `[other host]`, `/cron set --host`
167
+ re-homed it, and the leader file was named per host. None of it matched the way pi-loops is
168
+ used: pi runs on one machine, the loops run there, and a phone or a
169
+ laptop reaches the browser front end over a tailnet (`tailscale serve`) without moving the
170
+ execution anywhere. So the concept is gone: no `host` on a job or a rule (a stamp an older
171
+ build left is dropped when the file is read), no `--host`, no marker, `scheduler.json` and
172
+ `next-runs.json` plain. "Host" now means one thing here, the headless process.
173
+
174
+ The cost: two machines syncing one `$HOME` both run every job. That is not supported, and it
175
+ is said rather than half-handled.
176
+
120
177
  ## Where the browser front end came from
121
178
 
122
179
  pi owns the `pi` command and its terminal, so a second front end cannot replace the first one. It
package/docs/loops.md CHANGED
@@ -10,8 +10,14 @@ Everything here is `/cron`; `/crontab` and `/loop` are the same command under ot
10
10
 
11
11
  When due, the prompt is injected into the session that created the job as a user message with the
12
12
  engine prefix `[Trigger <run-id>] `, and the agent runs one turn (an *inject-and-run* job). If the agent
13
- is busy the message is queued as a follow-up. Plain jobs fire only from the process whose current
14
- session created them; `--resume` brings that back.
13
+ is busy the message is queued as a follow-up. A plain job belongs to the session that created it:
14
+ its result is a message in that conversation, so it fires only in the process holding that session.
15
+ Open another session and `/cron` lists it as `[session <id> — resume it to run]` with no next run,
16
+ `cron_list` says the same to the model, and `/cron run` refuses it; `--resume` the session it
17
+ belongs to and it wakes, catching up a missed slot if it was created with `--catchup`. It is not
18
+ broken while it sleeps — it is waiting for its conversation. Something that should run whether or
19
+ not a window is open (a nightly digest, a watch) is a loop: `--stateful`, with the inbox as its
20
+ outlet.
15
21
 
16
22
  Schedules are local time: 5-field cron (`*/n`, ranges, lists, `mon-fri`, `jan`), the aliases
17
23
  (`hourly` / `every hour` / `once an hour` → `0 * * * *`; `daily` / `every day` → `0 9 * * *`;
@@ -106,8 +112,8 @@ and answer with one `<verdict n="i">keep|drop — reason</verdict>` per finding
106
112
 
107
113
  ## Restart behavior
108
114
 
109
- Jobs live in `~/.pi/agent/loops/jobs.json` and survive pi restarts. Exactly one pi process per
110
- host owns the timer (`scheduler.<host>.json`, 30-second ticks, 90-second heartbeat); when it exits
115
+ Jobs live in `~/.pi/agent/loops/jobs.json` and survive pi restarts. Exactly one pi process on the
116
+ machine owns the timer (`scheduler.json`, 30-second ticks, 90-second heartbeat); when it exits
111
117
  or dies another open pi takes over on its next tick. A tick that was missed while no pi was running
112
118
  is fired once at startup (collapsed, not replayed) for stateful loops unless `[cron] catch_up = false`;
113
119
  plain inject jobs do not catch up unless created with `--catchup` (`--no-catchup` turns it off for
@@ -116,12 +122,15 @@ loops). A run that died with its process is retried on the next tick; at most
116
122
  a server pushing many distinct events cannot open one sub-agent per event.
117
123
  Jobs run with the model and thinking level recorded
118
124
  on them (`/cron set <id> --model … --thinking … --timeout …`, `-` to follow the running session).
119
- `/cron scheduler` shows who owns the timer; `/cron` marks jobs as `[dormant …]` when their session
120
- is not open here, parks them as disabled once that session no longer exists (`/cron gc` removes
121
- them), and `[orphan: cwd missing]` when their checkout is gone — disabled half an hour later, since a
122
- mount can be late at boot. A job created by a sub-agent belongs to the session that ran it. A job
123
- stamped with another machine's hostname (a synced `$HOME`, a renamed machine, a rebuilt container) is listed as
124
- `[other host: <name>]` with no next run; `/cron set <ref> --host here` re-homes it.
125
+ `/cron scheduler` shows who owns the timer; `/cron` marks a plain job as `[session <id> — resume it
126
+ to run]` when its session is not open here, parks it as disabled once that session's file has been
127
+ deleted (`/cron gc` removes it), and marks a loop `[orphan: cwd missing]` when their checkout is gone — disabled half an hour later, since a
128
+ mount can be late at boot. A job created by a sub-agent belongs to the session that ran it.
129
+
130
+ pi-loops runs on one machine. Two machines syncing one `$HOME` would both run every job; that is
131
+ not supported (until 0.19.0 a hostname stamp on each job half-handled it, and is now ignored). To
132
+ reach a running pi from elsewhere, use the browser front end over your tailnet ([cli.md](cli.md));
133
+ the loops keep running where pi runs.
125
134
 
126
135
  ## Time, and which clock it is
127
136
 
@@ -147,14 +156,13 @@ Two things are deliberately still UTC, because neither is a time anybody reads:
147
156
  session file, which cannot hold the `+` and `:` an offset brings, and pi's own session header, whose
148
157
  format is pi's to decide.
149
158
 
150
- The offset in the run line above is the offset of **the machine that ran it**, not a fixed part of
151
- the prompt. That matters for one case in particular: a job with no `host` runs on any machine
152
- sharing the `$HOME` (`/cron set <ref> --host -` asks for that), so a stateful loop can alternate
153
- between machines in different timezones. Its notes are free text the model writes — watermarks,
159
+ The offset in the run line above is the offset of the machine **at the time it ran**, not a fixed
160
+ part of the prompt. A machine's clock is not a constant: `TZ` gets set, a laptop travels, a
161
+ container is rebuilt in UTC. A loop's notes are free text the model writes — watermarks,
154
162
  "everything up to here has been seen" — and a watermark without an offset is a watermark the next
155
- machine cannot read. The prompt asks for the offset for that reason. Notes written before this
156
- version do not have it; a loop that crosses timezones and keeps a watermark is worth one look at
157
- `/cron state <id>`.
163
+ run cannot read once the clock has moved. The prompt asks for the offset for that reason. Notes
164
+ written before 0.14.1 do not have it; a loop that keeps a watermark on a machine whose timezone has
165
+ changed is worth one look at `/cron state <id>`.
158
166
 
159
167
  ### Daylight saving
160
168