@alphacoder-v0/pi-loops 0.20.0 → 0.22.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 (48) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/CONTRACT.md +139 -0
  3. package/README.md +11 -7
  4. package/README.zh-CN.md +6 -4
  5. package/docs/cli.md +26 -3
  6. package/docs/downstream.md +60 -0
  7. package/docs/goal.md +4 -2
  8. package/docs/loops.md +55 -9
  9. package/docs/recipes.md +94 -18
  10. package/docs/session-archive.md +8 -3
  11. package/docs/web-ui-parity.md +11 -1
  12. package/package.json +5 -2
  13. package/recipes/_tracker/issue-tracker-local.md +3 -1
  14. package/recipes/autoresearch/experiment.md +30 -6
  15. package/recipes/autoresearch/recipe.toml +2 -0
  16. package/recipes/changelog-draft/recipe.toml +4 -0
  17. package/recipes/changelog-draft/release.md +8 -3
  18. package/recipes/ci-sweeper/recipe.toml +4 -0
  19. package/recipes/ci-sweeper/sweep.md +2 -1
  20. package/recipes/daily-digest/digest.md +5 -4
  21. package/recipes/daily-digest/recipe.toml +2 -0
  22. package/recipes/deps-sweeper/README.md +11 -0
  23. package/recipes/deps-sweeper/audit.md +99 -0
  24. package/recipes/deps-sweeper/recipe.toml +17 -0
  25. package/recipes/ecosystem/recipe.toml +3 -0
  26. package/recipes/ecosystem/scan.md +1 -1
  27. package/recipes/issue-loop/implement.md +17 -4
  28. package/recipes/issue-loop/labels.sh +18 -0
  29. package/recipes/issue-loop/recipe.toml +2 -0
  30. package/recipes/issue-loop/triage.md +16 -2
  31. package/recipes/pr-watch/recipe.toml +3 -0
  32. package/skills/pi-loops/SKILL.md +3 -2
  33. package/src/archive.ts +97 -2
  34. package/src/cli.ts +160 -24
  35. package/src/goal.ts +11 -0
  36. package/src/hooks.ts +25 -1
  37. package/src/host-runtime.ts +4 -8
  38. package/src/inbox.ts +118 -55
  39. package/src/job-signal.ts +78 -0
  40. package/src/pi-loops.ts +177 -109
  41. package/src/presence.ts +5 -0
  42. package/src/protocol.ts +45 -2
  43. package/src/recipe.ts +158 -2
  44. package/src/scheduler.ts +6 -2
  45. package/src/store.ts +8 -3
  46. package/src/tools.ts +17 -11
  47. package/src/triggers.ts +8 -3
  48. package/src/web.mjs +73 -7
package/CHANGELOG.md CHANGED
@@ -3,6 +3,162 @@
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
+ ## [Unreleased]
7
+
8
+ ## [0.22.1] - 2026-09-16
9
+
10
+ ### Fixed
11
+ - **The launcher survives the package being installed by the other route.** The `sh` script
12
+ `install-launcher` writes named one entry file and nothing else, so `pi install npm:…` over a
13
+ `git:` copy — or the reverse — left every `pi-loops` ending in Node's `Cannot find module
14
+ …/src/cli-entry.mjs` stack trace, `pi-loops upgrade` included; the comment claiming it "survives
15
+ the package moving" was the only thing that did. It now carries both places pi installs this
16
+ package to (`npm/node_modules/<name>` and `git/<host>/<owner>/<repo>`, under the agent directory
17
+ it was written with) as literal paths and runs the first one that is there, falls back to the
18
+ `node` on `PATH` when the node it recorded has gone too, and when no copy is in any of them says
19
+ so in one line — write the launcher again with `install-launcher` — instead of a stack trace.
20
+
21
+ ## [0.22.0] - 2026-09-16
22
+
23
+ ### Added
24
+ - **One shape for a finding.** A line of `inbox.jsonl` is now the very object `pi-loops inbox
25
+ --json` prints — the ten fields of [docs/downstream.md](docs/downstream.md) §3, in their order,
26
+ under their names — instead of a third spelling of the same record (`trace_id` for the run id,
27
+ camelCase in memory, keys left out when they were empty). The entry in memory has those names
28
+ too, so both translations are a copy and a subset: nothing is renamed on the way in or out, and
29
+ a field that has to be added is added in one place. Lines written by earlier versions are still
30
+ read, and rewritten in the new shape the next time a claim or a dismiss touches them. `kind` is
31
+ now on every finding (`news` when the text is not a checkpoint) rather than only on checkpoints.
32
+ Keeping pi-loops' own fields — `job_id`, `session_id`, `claimed_by`, `verified_reason`,
33
+ `dismissed_at` — on the same line after the ten is this version's choice, not something the
34
+ interface asks for: they are private state and a program that reads them is reading what it was
35
+ told not to.
36
+
37
+ ### Changed
38
+ - **The contract is written once.** `CONTRACT.md` §2 fixes what a program may depend on — the
39
+ instance, the shapes of a recipe, a playbook, a finding and a run event, the permitted actions,
40
+ the three interfaces — and [docs/downstream.md](docs/downstream.md) is now the same three in
41
+ use: an example of each and a pointer to the rule, instead of a second wording of it. The page
42
+ ships with the package alongside `CONTRACT.md`, and `test/downstream-doc.test.ts` reads both:
43
+ every name §2.2 fixes exists in the source, and the page's JSON example is `FINDING_FIELDS`
44
+ exactly.
45
+ - **A run's two hook events are built once.** `run_start` and `run_end` were assembled twice,
46
+ near-verbatim, in the interactive extension and in the headless host — so which process held the
47
+ clock could change what a rule saw, and nothing but a comment said it should not. Both now call
48
+ `runStartEvent` / `runEndEvent` in `src/hooks.ts`, and the `run_*` fields
49
+ [docs/downstream.md](docs/downstream.md) §2 names are pinned by a test there.
50
+
51
+ ### Fixed
52
+ - **A number is refused where no list was printed, by the resolver itself.** `pi-loops inbox claim 1`
53
+ was refused by one line in the CLI while the resolver underneath still resolved `1` to the first
54
+ finding, and the model-facing tools guarded jobs and rules the same way, each on its own.
55
+ `resolveInboxRef`, `resolveJobRef` and `resolveRuleRef` now resolve an all-digit reference to
56
+ nothing unless the caller says `ordinals: true`, which only `/inbox`, `/cron` and `/triggers`
57
+ do — they number what they show. The CLI and the tools no longer carry their own check, and the
58
+ messages a person sees are unchanged.
59
+ - **daily-digest reads nothing under the loops directory.** Its playbook told a run to read
60
+ `runs.jsonl` and `jobs.json`, two files [docs/downstream.md](docs/downstream.md) says change
61
+ without notice; it now asks the `cron_list` tool, which lists each job's `last_error` and, new,
62
+ its `consecutive_failures`, and `pi-loops inbox list --json` for the findings still waiting. The
63
+ contract check that installs every packaged recipe and searches its files for the names of the
64
+ private files (`test/contract/1-second-implementation.sh`) is what caught it, and what keeps the
65
+ next recipe from doing the same.
66
+ - **One archive, one set of answers.** `/session-export` and `pi-loops export`, and
67
+ `/session-import` and `pi-loops import`, decided the same three things twice and had drifted
68
+ apart on all three; each decision now lives once, in `src/archive.ts`. What changes:
69
+ `pi-loops export` follows the session that created a job or rule, the way the slash command
70
+ always has, instead of archiving every loop in the project — including a colleague's, and
71
+ including none of your own reached through a worktree or a symlinked path, which its
72
+ string comparison of directories missed. `/session-import` takes `--activate-triggers=ask` and
73
+ the space-separated `--activate-triggers ask`, both of which it used to refuse. `pi-loops import`
74
+ defaults to `ask` like the slash command (it defaulted to `off`), asks after the import rather
75
+ than before it, with the counts, and rolls a half-written import back — a failed store write
76
+ used to leave the jobs in, the loop state on disk, and a session file nothing pointed at.
77
+
78
+ ## [0.21.0] - 2026-09-16
79
+
80
+ ### Added
81
+ - **What a program may depend on.** [docs/downstream.md](docs/downstream.md) is the whole list of
82
+ what a program that is not pi-loops may rely on: a recipe directory, `run_start` / `run_end` in
83
+ `hooks.toml` with their `PI_RUN_*` variables, and `pi-loops inbox list | claim <id> | dismiss <id>
84
+ [--reason <text>] --json` — a new command that does what `/inbox` does with no pi open — and it
85
+ says that nothing else is one: no file under `~/.pi/agent/loops`, no slash command's wording,
86
+ not the browser page. The list is small on purpose: every name on it is a promise never to
87
+ rename or remove, so it holds only what a closed loop needs. That the list is *enough* is
88
+ proved rather than asserted: `test/downstream/fixture-recipe/` is a recipe that uses only it —
89
+ installed from its directory, its run commits a file and files a checkpoint, and its `run_end`
90
+ hook finds that finding by `PI_RUN_ID` and claims it — and `test/downstream/closed-loop.sh`
91
+ checks the file, the commit and the claim from the outside with `sh` and `jq`, importing none
92
+ of this package's code; the model is a loopback stand-in (`npm run check:downstream`, part of
93
+ `npm run ci`). The third item was added for that proof: with only the first two, a hook could
94
+ count a run's findings but neither read nor mark one. A test keeps the page honest the other
95
+ way: every `PI_RUN_*` variable, manifest key and finding field it names must exist in `src/`.
96
+ - **A dismiss can say why, and the loop hears it.** `/inbox dismiss <n> <reason>` keeps the
97
+ reason on the entry (`dismiss_reason`, with `dismissed_at`) and puts it in front of the next run
98
+ of the loop that reported the finding, in a `[dismissed]` block between its notes and its task:
99
+ do not report this again unless what it describes has changed, and carry into your notes what
100
+ you need to remember that. Shown to that one run only — the notes are the loop's only memory,
101
+ and a channel the notes cap does not bound would be one it could not edit. Up to eight per run,
102
+ newest kept. A bare dismiss and `/inbox clear` stay silent as before; `/inbox all` shows the
103
+ reason on the dismissed line. Until now the only answer to a finding was to claim it or to say
104
+ nothing, and a loop that misjudged what counts as a finding went on misjudging it every night.
105
+ - **`deps-sweeper` recipe.** Mondays at eight: the project's audit and outdated commands, and one
106
+ finding per advisory or major-version gap not already in its notes. Under `propose`, patch and
107
+ minor updates are applied in a worktree, the project's check command run, and a pull request
108
+ opened — one per run, never a major, never a package on the playbook's deny list, and the same
109
+ package attempted twice without a green check is a stop. Eight recipes now ship.
110
+ - **Was it worth running.** `/cron` shows, per loop, the last thirty days of findings filed and
111
+ what a person did with them (`30d: 6 findings · 6 dismissed (4 with a reason)`), and marks
112
+ `[quiet ×N]` once the N newest runs found nothing. `/cron cost` puts the same counts beside each
113
+ job's spend. `cron_list` gives the model both (`signal_30d`, `quiet_streak`). Computed from the
114
+ run log and the inbox (`src/job-signal.ts`); nothing is stored and no schedule is changed — a
115
+ quiet watch is supposed to be quiet, and the number is what to read before making it hourly.
116
+ - **Every checkpoint says three things.** A finding that asks a person to decide now carries, in
117
+ every packaged playbook, the decision, what waits on it, and what happens if nobody acts
118
+ (`… · waits: your merge · if not: the branch stays; later runs build on it`). docs/recipes.md
119
+ has the shape. News — a merged pull request, a red check — stays as it was.
120
+ - **autoresearch looks back.** Five retired experiments in a row since the last kept one is a
121
+ finding asking whether the contract is wrong, and the loop stays out of the idea classes it has
122
+ exhausted; ten in a row stops it until `RESEARCH.md` changes. Before this an hourly loop in the
123
+ wrong search space ran until the budget cap.
124
+ - **What a recipe may never do, before it is installed.** `/recipe show` and the install
125
+ confirmation print each playbook's `## Never` section — the safety envelope of an unattended
126
+ run, which until now was read after the install, if at all — with a `tier` line and the
127
+ situations the recipe is for (`useful_when` in the manifest). `/recipe list` shows the eight in
128
+ two groups: three starters that only read and file findings (`daily-digest`, `pr-watch`,
129
+ `changelog-draft`), and the advanced ones that write to a worktree, a tracker or a pull request.
130
+ The four playbooks that kept their prohibitions in the body (`triage`, `implement`, `release`,
131
+ `experiment`) gain the section, with nothing new in it; `/recipe update` carries it into
132
+ installed copies.
133
+ - **A recipe is checked against the project before it is installed.** The confirmation ends
134
+ with *Before the first run*: whether `gh` is there and logged in, whether there is an `origin`,
135
+ a CI workflow, a lockfile, whether the tracker is GitHub — each a line with ✓ or ✗ and what the
136
+ miss means for the runs. Nothing blocks the install. A manifest names its checks (`needs`,
137
+ `needs_propose`); a recipe that reads the tracker is checked for `gh` only when the tracker
138
+ description uses it, so a local Markdown tracker is asked for nothing. `/recipe show` prints
139
+ the same lines. Until now the first sign of a missing login was an empty inbox the next morning.
140
+ - **The inbox tells a decision from news.** A finding in the checkpoint shape
141
+ (` · waits: … · if not: …`) is stored with `kind: "checkpoint"` when the run's findings are
142
+ appended. `/inbox` lists checkpoints first, marks them `⚑`, and says `3 new, 1 needs a decision`
143
+ in its header; the footer badge and the browser panel say `(1 decision)`. Claiming one tells the
144
+ turn that the claim is the person's approval of the decision the finding recommends, so it is
145
+ carried out rather than investigated again. Entries written before this read as news.
146
+ - **Esc pauses the goal.** Stopping a turn with Esc while a `/goal` is pursued pauses the goal —
147
+ appended to the session, so `--resume` finds it paused — until `/goal resume`. Until now the
148
+ aborted turn was merely not judged, and the next message ended with the evaluator sending the
149
+ agent back to work. Esc during an evaluation stopped it before and still does.
150
+
151
+ ### Fixed
152
+ - **issue-loop installs on a local Markdown tracker.** Its setup script ran `gh label create`
153
+ whatever the tracker was, so a project with `.scratch/issues/` and no `gh` stopped at "setup
154
+ script failed; no jobs were created". The script now reads `docs/agents/issue-tracker.md`: a
155
+ local tracker has no labels to create and it says so; a GitHub tracker without `gh`, or with
156
+ `gh` not logged in, is told what to run by hand and the install goes on — the first run reports
157
+ what it cannot do. The local tracker description gains the two operations it never named:
158
+ requesting changes (a comment and the status back to `agent-working`) and merging (`git merge`,
159
+ status `done`, file to `closed/`), and the implement playbook reads review feedback from the
160
+ item's comments where there is no pull request.
161
+
6
162
  ## [0.20.0] - 2026-09-14
7
163
 
8
164
  ### Added
package/CONTRACT.md ADDED
@@ -0,0 +1,139 @@
1
+ # pi-loops — CONTRACT
2
+
3
+ Version 0.22 · 2026-09-16 · Status: draft · Section 2 frozen at 29 lines
4
+
5
+ This file does one thing. It says which things never change (the contract), which things can be
6
+ replaced at any time (the mechanisms), and how to prove that a mechanism has not quietly become
7
+ part of the contract (the refinement checks). Section 2 says what the project is. Every other
8
+ section can be deleted or rewritten without changing the project.
9
+
10
+ ## 0. One sentence
11
+
12
+ The loops directory on one machine holds jobs, findings and each loop's notes. pi-loops runs the
13
+ jobs on a clock, queues the findings, and emits an event before and after each run. A program
14
+ talks to it only through a recipe directory, `hooks.toml`, and the `pi-loops inbox` command.
15
+
16
+ ## 1. Terms
17
+
18
+ - **Loops directory**: the directory pi-loops reads and writes on one machine (default
19
+ `~/.pi/agent/loops`, overridden by `PI_LOOPS_DIR`).
20
+ - **Project**: the directory a job runs in (`cwd`).
21
+ - **Job**: one schedule and one prompt, belonging to one project.
22
+ - **Run**: one execution of a job, with one `run_id`.
23
+ - **Finding**: one line a run leaves for a person, with a status `new`, `claimed` or `dismissed`.
24
+ - **Notes**: the Markdown a loop's run leaves for its next run.
25
+ - **Recipe**: a directory: `recipe.toml` and the playbooks it names.
26
+ - **Playbook**: the Markdown a run reads, copied into the project at install.
27
+ - **Level**: the line `Autonomy: report | propose | act` in a playbook.
28
+ - **Event**: the payload a `hooks.toml` rule sees when a run starts and when it ends.
29
+
30
+ ## 2. Contract (MUST)
31
+
32
+ ### 2.1 Instance
33
+
34
+ - Instance data in the loops directory: the jobs, the trigger rules, the findings, the notes, the run records (including each run's transcript), `hooks.toml`, `config.toml`, `mcp.toml`. Every other file and directory there is private state and can be deleted at any time.
35
+ - Instance data in a project: the playbooks and recipe files installed under `.agents/skills/<recipe>/`, and, when a recipe declares `needs_tracker`, `docs/agents/issue-tracker.md`.
36
+ - Tool code does not enter an instance. Instance data does not enter the tool.
37
+
38
+ ### 2.2 Unit formats
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`.
41
+ - **Playbook**: Markdown with pi's skill frontmatter (`name`, `description`), one level line, and one `## Never` section.
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
+ - **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`.
44
+ - **Rule**: a `[[hook]]` in `hooks.toml` with `event` and either `command` or `webhook`.
45
+
46
+ ### 2.3 Permitted actions
47
+
48
+ - **Install a recipe**: copy the playbooks into the project with the level line written, and create the jobs the manifest declares. A manifest that breaks a rule is refused whole; no file is copied.
49
+ - **Run**: emit one `run_start` and one `run_end` with the same `run_id`. The run's findings enter the queue with `status = new` and `run_id` = that run.
50
+ - **Claim** and **dismiss**: take a finding's `id` or a unique prefix of it, and succeed once each, only on a `new` finding. Dismiss can carry a `reason`, written to `dismiss_reason`.
51
+
52
+ ### 2.4 Ledger
53
+
54
+ The events are the ledger of runs: each run has exactly one `run_start` / `run_end` pair. A finding's `status` is its own ledger: `new → claimed` or `new → dismissed`, never back.
55
+
56
+ ### 2.5 Write conditions
57
+
58
+ A finding is written only by a run, and its `text` is what the run said. `status` changes only through claim or dismiss. A rule that fails or hangs does not change the run's result. An install is complete or does not happen.
59
+
60
+ ### 2.6 Deterministic rules
61
+
62
+ - `kind`: `text` contains `· waits:` (preceded by the start of the text or whitespace, case-insensitive) → `checkpoint`; otherwise `news`.
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
+ - Manifest validity: the required keys and values of 2.2. TOML is parsed as standard TOML.
65
+
66
+ ### 2.7 Downstream interface
67
+
68
+ 1. A recipe is a directory. `/recipe add <dir>` and `pi-loops recipe add <dir>` install it.
69
+ 2. A `hooks.toml` rule with `event = "run_start" | "run_end"` receives the event of 2.2.
70
+ 3. `pi-loops inbox list [--all] [--cwd <dir>] --json`, `inbox claim <id> --json`, `inbox dismiss <id> [--reason <text>] --json`: exit 0 and one JSON object (`{"findings": [...]}` or `{"finding": {...}}`); on failure exit 1 and `{"error": "<why>"}`.
71
+
72
+ Any file, field, path or command not listed above is not an interface.
73
+
74
+ ### 2.8 Boundary
75
+
76
+ That is all. This section fits on one screen.
77
+
78
+ ## 3. Mechanisms (SHOULD, replaceable)
79
+
80
+ Three sentences each: what it is, what it depends on, and why the contract does not move when it is replaced.
81
+
82
+ - **Findings are stored in `inbox.jsonl`, one object of 2.2 per line.** Depends on line-atomic appends. Another container keeps the object; `inbox` returns it as before.
83
+ - **An id is `inb-` and 32 hex characters; a run_id is `run-` and the same.** Depends on `randomBytes(16)`. The contract asks only that ids be unique and resolvable by prefix.
84
+ - **`created_at` carries the local UTC offset.** Depends on `stamp()` in `schedule.ts`. The contract asks only for RFC 3339.
85
+ - **`source` is `cron:<job name or id>`.** Depends on the scheduler. The contract asks only for a string.
86
+ - **`inbox list` puts checkpoints first, then news, each oldest first.** Depends on the sort in `inbox.ts`. The contract asks only for the right set.
87
+ - **A finding's `text` is at most 500 characters, a run files at most 16, notes are at most 2000 characters, a prompt at most 8 KB.** Depends on the model's context and the queue staying readable. Changing a number does not touch 2.2.
88
+ - **`inbox.jsonl` drops its oldest handled entries past 1 MB.** Depends on disk and list length. The contract only requires that `new` findings can be listed.
89
+ - **Jobs live in `jobs.json` (`version: 2`), notes in `state/<id>.md`, run records in `runs.jsonl`.** Depends on the current store. A job's full description is its `/cron add` line; reinstalling the recipe recreates it.
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
+ - **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
+ - **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.
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.
96
+
97
+ ## 4. Not done
98
+
99
+ - No public format for `jobs.json`: a job is created by a recipe install or by a person in pi, and a reinstall recreates it (2.3 + mechanism "jobs live in `jobs.json`").
100
+ - No second on-disk shape for a finding (such as another tool's field names): a line is the object of 2.2, and another tool reads it as it is (2.2 + mechanism "`inbox.jsonl`").
101
+ - Playbooks do not read files in the loops directory: a run learns its own health from the `cron_list` tool and the findings from `pi-loops inbox` (2.7).
102
+ - `mcp.toml` is not a program interface: it is a person's configuration file (instance data in 2.1, not in 2.7).
103
+ - No command that creates a job from a terminal: `pi --mode rpc` already runs `/recipe add` without a terminal (mechanism "creating a job needs a pi").
104
+ - No multi-machine support: one loops directory belongs to one machine (design.md, decision 19).
105
+
106
+ ## 5. Refinement checks
107
+
108
+ The scripts are in `test/contract/`. `sh test/contract/run.sh` runs all five; each also runs alone.
109
+
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.
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
+
116
+ ## 6. Mapping of requirements and features
117
+
118
+ | Requirement or feature | Where it lands |
119
+ |---|---|
120
+ | `docs/downstream.md` | examples of 2.2 / 2.3 / 2.7; the rule is here |
121
+ | `docs/downstream.md` §1, the recipe directory | Contract 2.2 / 2.7.1 |
122
+ | `docs/downstream.md` §2, the hook events | Contract 2.2 / 2.7.2 |
123
+ | `docs/downstream.md` §3, the inbox command and the finding object | Contract 2.2 / 2.3 / 2.7.3 |
124
+ | `inb-<32 hex>`, 500 characters and the list order in `docs/downstream.md` | Mechanism |
125
+ | `docs/recipes.md`, "Writing a recipe" | Contract 2.2; `.orig/`, `.recipe.json`, exclude, preflight names → mechanism |
126
+ | Timeouts, truncation and log paths in `docs/hooks.md` | Mechanism |
127
+ | The on-disk line shape (`trace_id`) that `docs/loops.md` used to document | Removed, unreleased: a line is the object of 2.2 (CHANGELOG, "One shape for a finding") |
128
+ | The four caps in `docs/design.md`, decision 11 | Mechanism |
129
+ | `docs/design.md`, decisions 15–18 (recipes are files, copied into the project, deterministic wizard, the level is a sentence) | Contract 2.1 / 2.2 / 2.3; details → mechanism |
130
+ | `recipes/daily-digest` reads `runs.jsonl` and `jobs.json` | Not done, item 3 |
131
+ | `examples/mcp.toml` | Not done, item 4 |
132
+ | `test/downstream/` | Part of refinement check 1 |
133
+
134
+ ## 7. Revision rules
135
+
136
+ - The required fields and the actions of section 2 are only added, never changed or removed.
137
+ - No number derived from a mechanism appears in the contract.
138
+ - A mechanism paragraph can be replaced whole; replacing one only requires rerunning section 5.
139
+ - This file is part of the repository; a change to it goes through the ledger.
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.20.0 # or from GitHub, at a pinned tag
41
+ pi install git:github.com/alphacoder-v0/pi-loops@v0.22.1 # 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
  ```
@@ -103,6 +103,7 @@ watching this repo lands in the same list and you triage it in one pass.
103
103
  /cron run 1 # do not wait until 9am — run it now and watch
104
104
  /inbox claim 1 # hand finding #1 to the agent as a real turn
105
105
  /inbox dismiss 2 # not interesting
106
+ /inbox dismiss 3 that file is generated, ignore it # the loop is told why on its next run
106
107
  ```
107
108
 
108
109
  ## Loops worth stealing
@@ -138,11 +139,13 @@ A job records its directory and model at creation, so it is not tied to the wind
138
139
  `--cwd` runs it in another checkout (absolute, or relative to this project — no shell, so nothing
139
140
  expands `~`), and `--model` pins it whatever this session is on (`/cron set <ref> --model -` unpins).
140
141
 
141
- And seven you do not write at all: `/recipe add issue-loop` runs the issue tracker as a state
142
+ And eight you do not write at all: `/recipe add issue-loop` runs the issue tracker as a state
142
143
  machine that two loops turn, `/recipe add autoresearch` runs one experiment per run against a
143
- contract you wrote, and `daily-digest`, `pr-watch`, `ci-sweeper`, `changelog-draft` and
144
- `ecosystem` do what their names say — each a directory of playbooks copied into the project,
145
- installed with one question and one confirmation ([docs/recipes.md](docs/recipes.md)).
144
+ contract you wrote, and `daily-digest`, `pr-watch`, `ci-sweeper`, `deps-sweeper`,
145
+ `changelog-draft` and `ecosystem` do what their names say — each a directory of playbooks copied
146
+ into the project, installed with one question and one confirmation, and `/recipe show` says
147
+ what its runs may never do before you say yes ([docs/recipes.md](docs/recipes.md)). Three are
148
+ starters that only read and file findings: `daily-digest`, `pr-watch`, `changelog-draft`.
146
149
 
147
150
  Two that are not loops:
148
151
 
@@ -285,7 +288,7 @@ State stays in `~/.pi/agent/loops` until you delete it.
285
288
  | `/cron gc` | Remove plain jobs whose session was deleted (they are parked as disabled first) |
286
289
  | `/cron panel on\|off` | The side panel above the editor: Triggers, Inbox, Cron, MCP |
287
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 |
288
- | `/inbox [list\|all\|claim <n>\|dismiss <n>\|clear] [--all]` | Triage findings from stateful loops. This project's by default, `--all` for every project — the same scoping `/cron` and `/triggers` use |
291
+ | `/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 |
289
292
  | `/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 |
290
293
  | `/new-trigger <natural language>` | Create a condition-based rule ("when ~/build.done exists, run cargo test") |
291
294
  | `/triggers [status\|rules\|enable\|disable\|remove\|running\|audit [N]\|abort]` | Dynamic rules: what exists, what is running, what happened |
@@ -326,7 +329,8 @@ notifications, and lifecycle hooks — each has a page here.
326
329
  - [docs/goal.md](docs/goal.md) — `/goal`: holding a session to a stop condition
327
330
  - [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
328
331
  - [docs/session-archive.md](docs/session-archive.md) — `/session-export`, `/session-import`
329
- - [docs/cli.md](docs/cli.md) — the `pi-loops` command line: export, import, and looking in on the host
332
+ - [docs/cli.md](docs/cli.md) — the `pi-loops` command line: export, import, the inbox, and looking in on the host
333
+ - [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
330
334
  - [docs/web-ui-parity.md](docs/web-ui-parity.md) — what the browser front end owes you, line by line
331
335
  - [docs/configuration.md](docs/configuration.md) — paths, `config.toml`, flags, environment
332
336
  - [docs/design.md](docs/design.md) — architecture: what each piece is built out of, and the decisions behind it
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.20.0 # 或者从 GitHub 装,钉住一个 tag
48
+ pi install git:github.com/alphacoder-v0/pi-loops@v0.22.1 # 或者从 GitHub 装,钉住一个 tag
49
49
  pi install /path/to/pi-loops # 或者本地检出;本仓库里就是 pi install .
50
50
  ```
51
51
 
@@ -130,6 +130,7 @@ claim with /inbox claim <n>, dismiss with /inbox dismiss <n>
130
130
  /cron run 1 # 不用等到早上九点,现在就跑一次看看
131
131
  /inbox claim 1 # 把第 1 条作为真实的一轮交给 agent
132
132
  /inbox dismiss 2 # 不感兴趣
133
+ /inbox dismiss 3 那个文件是生成的,别管它 # 理由会放进这个 loop 下一次 run 的 prompt 里
133
134
  ```
134
135
 
135
136
  ### 5. 让它整夜跑之前
@@ -221,6 +222,7 @@ pi update --extensions # 对齐已安装的包
221
222
  /recipe 有哪些打包好的 recipe,这个项目装了哪些
222
223
  /recipe add issue-loop 把 issue tracker 跑成状态机:分诊成 agent brief、在 worktree 里实现、开 PR;推进和合并留给人
223
224
  /recipe add autoresearch 按你写的研究合同每次跑一个实验,账本记下每次尝试,只凭 held-out 数据提议晋升
225
+ /recipe add deps-sweeper 每周一跑 audit 和 outdated:新的安全通告和落后一个大版本的依赖各报一条;propose 级在 worktree 里做小版本升级并开 PR
224
226
  ```
225
227
 
226
228
  一个 recipe 是一组 loop 加上它们各自每次 run 都会读的 playbook。安装只问一个问题(自治级别
@@ -263,7 +265,7 @@ playbook 拷到 `.agents/skills/<name>/`,通过 `.git/info/exclude` 排除在
263
265
 
264
266
  /inbox 本项目的新 findings(每行标出项目;`--all` 看全部项目,与 /cron、/triggers 同一套作用域)
265
267
  /inbox claim 1 标记 claimed,并把它作为一个真实 user turn 交给主会话的 agent
266
- /inbox dismiss 2 · /inbox clear · /inbox all
268
+ /inbox dismiss 2 [理由] · /inbox clear · /inbox all 写了理由,报这条的 loop 下次 run 会看到;不写则沉默
267
269
  ```
268
270
 
269
271
  `/crontab` 和 `/loop` 是 `/cron` 的别名。
@@ -341,7 +343,7 @@ pi 内置的 `/export` 只导 HTML/JSONL 对话,`/import` 只导回对话—
341
343
 
342
344
  ```text
343
345
  /session-export [path] [--exclude-triggers] 默认 ./pi-session-<id前16位>.pisession
344
- /session-import <path> [--activate-triggers=on|off] [--cwd <dir>] [--resume]
346
+ /session-import <path> [--activate-triggers=off|ask|on] [--cwd <dir>] [--resume]
345
347
  ```
346
348
 
347
349
  ```text
@@ -356,7 +358,7 @@ backup.pisession 无压缩 ustar,0600,拒绝覆盖已有文
356
358
  导入的时候会重写这些:
357
359
 
358
360
  - session 换新 id,cwd 改成目标目录,header 里记下 `importedFrom` 来源。
359
- - 任务和规则默认全部 disabled。`--activate-triggers=on` 会弹一次确认,问你要不要把源会话里原本 enabled 的那些重新打开。
361
+ - 任务和规则默认全部 disabled。`--activate-triggers` 默认 `ask`:导入完之后弹一次确认,问你要不要把源会话里原本 enabled 的那些重新打开;`off` 不问,`on` 直接打开。
360
362
  - 运行标记、错误、重叠计数清零。
361
363
  - id 撞上本机已有的就重新生成,loop 状态文件跟着新 id 走。
362
364
  - 非 stateful 任务重新绑定到导入的 session。
package/docs/cli.md CHANGED
@@ -169,8 +169,13 @@ node src/cli-entry.mjs install-launcher
169
169
  node src/cli-entry.mjs install-launcher --dir ~/bin
170
170
  ```
171
171
 
172
- It writes a two-line `sh` script that names the node you ran it with and the package it lives in —
173
- a launcher rather than a symlink, so it keeps working if either moves for the other's reason.
172
+ It writes a small `sh` script that names the node you ran it with and the package it lives in — a
173
+ launcher rather than a symlink. Neither path is trusted for ever: if the package is not where it
174
+ was, the launcher looks in both of the directories above (the ones pi installs into, under the agent
175
+ directory this was written with), so installing again by the other route does not break it, and if
176
+ that node has gone it falls back to the `node` on your `PATH`. When no copy is in any of them it
177
+ prints one line saying to run `install-launcher` again — from inside pi, or from wherever pi has put
178
+ the package now — instead of a Node stack trace about a missing file.
174
179
 
175
180
  ## Upgrading
176
181
 
@@ -209,6 +214,8 @@ pi-loops inspect <file>
209
214
  pi-loops export [--session <id>] [--cwd <dir>] [--output <file>] [--exclude-triggers]
210
215
  pi-loops import <file> [--cwd <dir>] [--activate-triggers=off|ask|on]
211
216
  pi-loops host status | abort <run-id|trace-id> | stop
217
+ pi-loops recipe list | show <name|path> | add <name|path> [--cwd <dir>] [--level report|propose|act] [--overwrite]
218
+ pi-loops inbox list [--all] [--cwd <dir>] | claim <id> | dismiss <id> [--reason <text>] [--json]
212
219
  ```
213
220
 
214
221
  ## sessions, inspect
@@ -239,7 +246,10 @@ pi-loops export --session 01a084a0 --output ~/backups/api.pisession
239
246
  Restores an archive into `--cwd` (default: the current directory), rewriting ids, project and
240
247
  machine so the automation runs where it landed.
241
248
 
242
- Imported automation stays disabled unless `--activate-triggers=on`; `ask` prompts on a terminal.
249
+ `--activate-triggers=off|ask|on` decides what happens to the automation the archive carries, and
250
+ means the same thing here as in `/session-import`. The default is `ask`: the import leaves it
251
+ disabled and then asks once, where there is a terminal or a UI to ask in, whether to enable what
252
+ was enabled in the source. `off` never asks; `on` activates it as part of the import.
243
253
  Importing the same archive twice adds nothing the second time.
244
254
 
245
255
  ## host
@@ -280,3 +290,16 @@ record — and stops. The setup script and the jobs need a pi open in the projec
280
290
  them after you say yes. Without `--level` the lowest level the recipe supports is written; an edited
281
291
  copy is kept unless `--overwrite` says otherwise.
282
292
 
293
+ ## inbox
294
+
295
+ ```sh
296
+ pi-loops inbox list --json # this project's new findings, checkpoints first
297
+ pi-loops inbox claim inb-3f2a… --json # mark it claimed; acting on it is yours
298
+ pi-loops inbox dismiss inb-3f2a… --reason "that file is generated" --json
299
+ ```
300
+
301
+ `/inbox` with no pi open ([loops.md](loops.md#the-inbox)). `claim` and `dismiss` take an id or a
302
+ unique prefix of one and change the entry the way the slash command does; a claim here cannot start
303
+ a turn, so it marks the finding and leaves the acting to whoever asked. Without `--json` the same,
304
+ one line per finding, for a person. The JSON is fixed in [CONTRACT.md](../CONTRACT.md) §2.2 and shown in [downstream.md](downstream.md), which is
305
+ the page a program should be written against.
@@ -0,0 +1,60 @@
1
+ # What a program may depend on
2
+
3
+ Three things, and this page is the whole list. A program that is not pi-loops and does not import
4
+ its source may rely on these and on nothing else.
5
+
6
+ **Any file under `~/.pi/agent/loops/` is not an interface.** `jobs.json`, `inbox.jsonl`,
7
+ `runs.jsonl`, `state/`, `host.json`, the logs: private, and they change without notice. The one
8
+ file there a program writes is `hooks.toml`, and only for the two events below. The same goes for
9
+ every command, field and path this page does not name, the wording of the slash commands, and the
10
+ browser page.
11
+
12
+ The three are named in [CONTRACT.md](../CONTRACT.md) §2.7 and their shapes in §2.2 — that is where
13
+ the rule lives, and the text that moves when one of them changes. This page is the three in use.
14
+
15
+ ## 1. A recipe is a directory
16
+
17
+ `recipe.toml` and the Markdown playbooks it names, side by side; `/recipe add ./dir` and
18
+ `pi-loops recipe add ./dir` install it from a local path. What the manifest may say — every key it
19
+ requires and every one it allows — is [CONTRACT.md](../CONTRACT.md) §2.2; what a playbook is, and
20
+ how one is written, is one section of [recipes.md](recipes.md#writing-a-recipe). A manifest that
21
+ breaks a rule is refused before anything is copied.
22
+
23
+ ## 2. `run_start` and `run_end` in `hooks.toml`
24
+
25
+ A `[[hook]]` rule with `event = "run_start"` or `event = "run_end"` fires around every scheduled
26
+ run, in a pi window and in the headless host alike; what the payload carries is
27
+ [CONTRACT.md](../CONTRACT.md) §2.2, and the file, the rule keys and when the two fire are in
28
+ [hooks.md](hooks.md#exactly-when-hooks-fire). Each field reaches the rule's command as a `PI_*`
29
+ variable, unset where the payload has `null`, and all of them together as JSON in the file at
30
+ `PI_HOOK_PAYLOAD`; [hooks.md](hooks.md) shows a rule. A hook that fails or hangs never fails the run.
31
+
32
+ ## 3. `pi-loops inbox list | claim <id> | dismiss <id> --json`
33
+
34
+ No pi needs to be open. Exit 0 and one JSON object on stdout; on failure exit 1 and `{"error": "<why>"}`.
35
+
36
+ ```
37
+ pi-loops inbox list [--all] [--cwd <dir>] --json {"findings": [<finding>, …]}
38
+ pi-loops inbox claim <id> --json {"finding": <finding>} now "claimed"
39
+ pi-loops inbox dismiss <id> [--reason <text>] --json {"finding": <finding>} now "dismissed"
40
+ ```
41
+
42
+ `list` is what `/inbox` shows: the new findings of the project at `--cwd` (default: the current
43
+ directory), checkpoints first, then news, each oldest first; `--all` is every project. `claim` and
44
+ `dismiss` take a finding's `id` or a unique prefix of it (never a number), among the new findings
45
+ machine-wide, and do to the entry what `/inbox claim` and `/inbox dismiss` do — the two actions of
46
+ [CONTRACT.md](../CONTRACT.md) §2.3: `claim` marks it and stops, since there is no session to hand
47
+ it to; a `--reason` is shown to the loop's next run. The object they print is the one of §2.2,
48
+ field for field:
49
+
50
+ ```json
51
+ {"id": "inb-<32 hex>", "created_at": "2026-09-16T09:00:00.000+08:00", "status": "new",
52
+ "kind": "checkpoint", "source": "cron:nightly-audit", "run_id": "run-<32 hex>",
53
+ "cwd": "/path/to/project", "text": "one line, at most 500 characters", "verified": null, "dismiss_reason": null}
54
+ ```
55
+
56
+ Fields may be added; none of these will be renamed, removed or change type.
57
+
58
+ Not listed here means private. `test/downstream/` is the proof this list is enough: a recipe that
59
+ uses only it, checked from the outside with `sh` and `jq`. `test/contract/` holds the checks of
60
+ CONTRACT.md itself.
package/docs/goal.md CHANGED
@@ -31,10 +31,12 @@ is told to answer `{"ok": false, "reason": "insufficient evidence in transcript"
31
31
  | Evaluator failure | Pauses with the reason; it never loops on an evaluator that cannot decide |
32
32
  | Transcript | 40 000 characters, truncated from the front so the newest evidence always survives |
33
33
  | Tools | None. The evaluator reads, it does not act |
34
- | Interruption | A turn you aborted is not judged, and Esc during an evaluation stops it |
34
+ | Interruption | Esc on a turn pauses the goal — it stays paused across `--resume` until `/goal resume` — and Esc during an evaluation stops that evaluation the same way |
35
35
  | Its own timeout | 2 minutes; it is one read, not a piece of work |
36
36
 
37
- `/goal resume` after a budget limit starts the allowance again. `pause`, `resume` and `clear` are
37
+ `/goal resume` after a budget limit starts the allowance again, and after an Esc starts
38
+ evaluating again: stopping a turn is taken as stopping the goal, since the next message you sent
39
+ would otherwise end with the evaluator sending the agent back to work. `pause`, `resume` and `clear` are
38
40
  matched as whole words, so `/goal clear the type errors first` sets that as the condition rather
39
41
  than dropping the goal.
40
42