@alphacoder-v0/pi-loops 0.19.3 → 0.22.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 (53) hide show
  1. package/CHANGELOG.md +164 -0
  2. package/CONTRACT.md +139 -0
  3. package/README.md +12 -7
  4. package/README.zh-CN.md +6 -4
  5. package/docs/cli.md +19 -1
  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 +140 -18
  10. package/docs/session-archive.md +8 -3
  11. package/docs/web-ui-parity.md +11 -1
  12. package/package.json +6 -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 +6 -1
  17. package/recipes/changelog-draft/release.md +25 -5
  18. package/recipes/ci-sweeper/README.md +9 -0
  19. package/recipes/ci-sweeper/recipe.toml +16 -0
  20. package/recipes/ci-sweeper/sweep.md +71 -0
  21. package/recipes/daily-digest/README.md +10 -0
  22. package/recipes/daily-digest/digest.md +65 -0
  23. package/recipes/daily-digest/recipe.toml +14 -0
  24. package/recipes/deps-sweeper/README.md +11 -0
  25. package/recipes/deps-sweeper/audit.md +99 -0
  26. package/recipes/deps-sweeper/recipe.toml +17 -0
  27. package/recipes/ecosystem/README.md +9 -0
  28. package/recipes/ecosystem/recipe.toml +15 -0
  29. package/recipes/ecosystem/scan.md +59 -0
  30. package/recipes/issue-loop/implement.md +17 -4
  31. package/recipes/issue-loop/labels.sh +18 -0
  32. package/recipes/issue-loop/recipe.toml +2 -0
  33. package/recipes/issue-loop/triage.md +16 -2
  34. package/recipes/pr-watch/README.md +9 -0
  35. package/recipes/pr-watch/recipe.toml +15 -0
  36. package/recipes/pr-watch/watch.md +70 -0
  37. package/skills/pi-loops/SKILL.md +3 -2
  38. package/src/archive.ts +97 -2
  39. package/src/cli.ts +103 -16
  40. package/src/goal.ts +11 -0
  41. package/src/hooks.ts +25 -1
  42. package/src/host-runtime.ts +4 -8
  43. package/src/inbox.ts +118 -55
  44. package/src/job-signal.ts +78 -0
  45. package/src/pi-loops.ts +177 -109
  46. package/src/presence.ts +5 -0
  47. package/src/protocol.ts +45 -2
  48. package/src/recipe.ts +158 -2
  49. package/src/scheduler.ts +6 -2
  50. package/src/store.ts +8 -3
  51. package/src/tools.ts +17 -11
  52. package/src/triggers.ts +8 -3
  53. package/src/web.mjs +73 -7
package/docs/loops.md CHANGED
@@ -65,21 +65,33 @@ transcript when the run finishes. Prompts are capped at 8 KB.
65
65
 
66
66
  ## The inbox
67
67
 
68
- Global JSONL, shared by every session and project, with a stable record shape (`id` = `inb-<32 hex>`,
69
- `created_at`, `source` = `cron:<job>`, `text`, `trace_id` = the run id, `session_id`, `status`
70
- `new → claimed | dismissed`) plus pi-loops' `job_id`, `cwd`, `claimed_by`, `verified`,
71
- `verified_reason`. Job ids are `cron-<32 hex>`; prefixes, names and list numbers resolve.
68
+ Global JSONL, shared by every session and project. Each line is one finding in the shape
69
+ [CONTRACT.md](../CONTRACT.md) §2.2 fixes, followed by pi-loops' own fields. Job ids are
70
+ `cron-<32 hex>`; prefixes, names and list numbers resolve.
72
71
 
73
72
  ```text
74
- /inbox Inbox (<project>, N new, times <offset>): "<n>. [<id prefix>] <finding> (<project>, <source>, <created_at>)"
73
+ /inbox Inbox (<project>, N new, K need a decision, times <offset>): "<n>. [<id prefix>] <finding> (<project>, <source>, <created_at>)"
75
74
  /inbox --all the same, every project on this machine
76
75
  /inbox all [--all] history including claimed and dismissed
77
76
  /inbox claim <n|id> mark claimed and start a real agent turn:
78
77
  "A recurring loop (<source>, running in <cwd>) reported this finding — investigate and address it: …"
79
78
  (the loop's cwd is part of the line; a checker-kept finding adds a line saying so)
80
- /inbox dismiss <n|id> /inbox clear [--all]
79
+ /inbox dismiss <n|id> [reason] mark dismissed; the reason, if you give one, is shown to the loop's next run
80
+ /inbox clear [--all]
81
81
  ```
82
82
 
83
+ A dismiss with words after the number — `/inbox dismiss 2 that file is generated, ignore it` — is
84
+ the one way a person talks back to a loop. The reason is stored on the entry and put in front of
85
+ the **next run of the loop that reported it**, between its notes and its task, in a `[dismissed]`
86
+ block that says not to report the finding again unless what it describes has changed, and to carry
87
+ into the notes whatever it needs to remember that. It is shown to that one run only: a loop has
88
+ nothing but its notes between runs, and a second memory the notes cap does not bound would be one
89
+ the loop could not edit or forget. Up to eight reasons per run, the newest kept. A bare
90
+ `/inbox dismiss 2` and `/inbox clear` stay silent, as before — "not interesting" is not something
91
+ the next run can act on. `/inbox all` shows the reason on the dismissed line. For a dismiss,
92
+ `--all` counts only before the reason begins (`/inbox dismiss --all 3 …` or `/inbox dismiss 3 --all …`);
93
+ a reason that mentions `--all` in passing does not re-number the list you were looking at.
94
+
83
95
  The file is machine-wide because loops are; triage is not. `/inbox` lists this project's findings
84
96
  the way `/cron` lists its jobs, names the project on every line, and says how many are waiting
85
97
  elsewhere; `--all` lifts the filter. Numbers are the numbers on screen — `/inbox claim 3` claims
@@ -88,8 +100,17 @@ while an id or an id prefix still resolves machine-wide. `/inbox clear` dismisse
88
100
  not the unread findings of four other projects. A finding stored without a cwd belongs to no
89
101
  project and is listed in all of them.
90
102
 
91
- Corrupt lines are skipped on read and never deleted. The footer shows `Inbox: N new` (the whole
92
- machine, as the badge always has) and `N job(s) failing (<worst> ×<count>)` once a loop has failed
103
+ A finding that asks for a decision is a **checkpoint**, and the inbox knows one by its shape:
104
+ the ` · waits: … · if not: …` clauses every packaged playbook writes
105
+ ([recipes.md](recipes.md#checkpoints)). The run's text is read once, when its findings are
106
+ appended, and the entry carries `kind: "checkpoint"` from then on; news carries nothing. `/inbox`
107
+ lists checkpoints first, marked `⚑`, and says how many there are in its header (`3 new, 1 needs a
108
+ decision`); the numbers on screen are the numbers `claim` and `dismiss` take, so the order is
109
+ decided in one place. Claiming a checkpoint tells the turn that the claim is the person's approval
110
+ of the decision the finding recommends, and to carry it out rather than investigate it again.
111
+
112
+ Corrupt lines are skipped on read and never deleted. The footer shows `Inbox: N new`, with
113
+ `(K decisions)` after it when any are checkpoints (the whole machine, as the badge always has), and `N job(s) failing (<worst> ×<count>)` once a loop has failed
93
114
  often enough for the scheduler to start backing off; the same clause is on the `[cron] … active
94
115
  here` line at session start, for this project. Until then a job that has failed forty nights in a
95
116
  row looked exactly like a healthy one. The side panel shows the inbox count too. Entries kept by
@@ -167,7 +188,8 @@ changed is worth one look at `/cron state <id>`.
167
188
  ### Daylight saving
168
189
 
169
190
  Local time means the clock does what the clock does, and twice a year it does something strange.
170
- Measured, not assumed (`America/New_York`, 2026):
191
+ Measured, not assumed (`America/New_York`, 2026) — and pinned by `test/dst.test.ts`, which sets the
192
+ zone it needs rather than reading the one the machine happens to have.
171
193
 
172
194
  | | what happens |
173
195
  |---|---|
@@ -285,6 +307,30 @@ is gone — `--all` every project's, `--purge` also the loop state left behind b
285
307
  A job that fails three times in a row is retried on a widening gap (5 minutes, doubling, up to six
286
308
  hours) instead of at every due tick, and says so; one success clears the streak.
287
309
 
310
+ ## Was it worth running
311
+
312
+ A loop was judged, until now, by whether it ran: `runs 42`, a last error, the failing badge. None
313
+ of that says whether anyone wanted what it found. So `/cron` puts two more facts on a loop's lines,
314
+ both computed from the run log and the inbox and nothing else:
315
+
316
+ ```text
317
+ 3. cron-b91def87 "pr-watch" enabled */15 * * * * [stateful] [quiet ×12]
318
+ next 2026-09-15 10:15 · runs 412 · 30d: 6 findings · 6 dismissed (4 with a reason) · ~/code/acme-api
319
+ ```
320
+
321
+ `30d: 6 findings · 6 dismissed` is the last thirty days: findings filed, and of those still in the
322
+ inbox, how many a person claimed and how many they dismissed (with how many reasons). A loop whose
323
+ findings are all dismissed is noise at any price; one whose findings are all claimed is the one to
324
+ keep. `/cron cost` shows the same counts beside each job's spend, for its own window, so
325
+ `$0.410 28 run(s) pr-watch — 6 findings · 6 dismissed` is a line that answers itself.
326
+
327
+ `[quiet ×12]` means the twelve newest runs found nothing, whatever the window. It is not a fault —
328
+ a watch on a quiet thing is supposed to be quiet — and pi-loops does not slow the job down for it:
329
+ the schedule is the schedule. It is the number to read before deciding that a fifteen-minute
330
+ watch could be hourly (`/cron set <ref> --schedule "0 * * * *"`), or that the loop is looking at
331
+ the wrong thing. `cron_list` gives the model both facts (`signal_30d`, `quiet_streak`), so asking
332
+ "which of my loops are worth keeping" gets an answer from the numbers rather than the names.
333
+
288
334
  ## When something looks wrong
289
335
 
290
336
  Every diagnostic a pi process produces is written to `logs/pi-<pid>.log` in the loops directory —
package/docs/recipes.md CHANGED
@@ -14,16 +14,21 @@ anything a person could not have typed.
14
14
  /recipe remove issue-loop its jobs go; the files stay unless you say --purge
15
15
  ```
16
16
 
17
- Two recipes ship in this version, and the shape is meant to be copied:
18
-
19
- | recipe | jobs | what it runs | levels |
20
- |---|---|---|---|
21
- | [issue-loop](../recipes/issue-loop/) | `issue-triage` every 30 min, `issue-implement` every 30 min offset | the issue tracker as a state machine: triage into agent briefs, build accepted issues in a worktree, open pull requests | `propose`, `act` |
22
- | [autoresearch](../recipes/autoresearch/) | `autoresearch` every hour, with `--verify` | one experiment per run against a contract you wrote, a ledger of every attempt, promotion only on held-out evidence | `propose`, `act` |
23
-
24
- `changelog-draft` (a nightly changelog entry and version proposal) is packaged with its playbook
25
- and manifest but is still the release-side half of the issue loop; it and four more — a daily
26
- digest, a pull-request watch, a CI sweeper, an ecosystem scan — are the next batch.
17
+ Eight recipes ship, and the shape is meant to be copied. Three are **starters** — they read, and
18
+ file findings, and are the ones to install first; the rest are **advanced** — they write to a
19
+ worktree, a tracker or a pull request, and want their playbooks read before the level question is
20
+ answered. `/recipe list` shows them in those two groups.
21
+
22
+ | recipe | tier | jobs | what it runs | levels |
23
+ |---|---|---|---|---|
24
+ | [issue-loop](../recipes/issue-loop/) | advanced | `issue-triage` every 30 min, `issue-implement` every 30 min offset | the issue tracker as a state machine: triage into agent briefs, build accepted issues in a worktree, open pull requests | `propose`, `act` |
25
+ | [autoresearch](../recipes/autoresearch/) | advanced | `autoresearch` every hour, with `--verify` | one experiment per run against a contract you wrote, a ledger of every attempt, promotion only on held-out evidence | `propose`, `act` |
26
+ | [daily-digest](../recipes/daily-digest/) | starter | `daily-digest` at 09:00 | one finding saying what needs a look today — open items, a red CI, a loop that keeps failing — or none | `report` |
27
+ | [pr-watch](../recipes/pr-watch/) | starter | `pr-watch` every 15 min | the open pull requests, and only what changed about them: a red check, a conflict, a review waiting, an author who answered | `report`, `propose` |
28
+ | [ci-sweeper](../recipes/ci-sweeper/) | advanced | `ci-sweeper` every 15 min, 40-minute timeout | a red default branch reported, or repaired in a worktree and put up as a pull request; the same failure twice unfixed is a stop | `report`, `propose` |
29
+ | [changelog-draft](../recipes/changelog-draft/) | starter | `changelog-draft` weekdays at 18:00 | when the default branch is ahead of the last tag, the changelog entry and a version, drafted — or put in a release pull request | `report`, `propose` |
30
+ | [ecosystem](../recipes/ecosystem/) | advanced | `ecosystem` Mondays at 10:00 | who uses or forks this project, and the one reply or upstream invitation worth sending, drafted as a finding a person sends | `propose` |
31
+ | [deps-sweeper](../recipes/deps-sweeper/) | advanced | `deps-sweeper` Mondays at 08:00, 40-minute timeout | the advisories and major-version gaps not already in its notes; under `propose`, the patch and minor updates applied in a worktree, checked, and put up as one pull request | `report`, `propose` |
27
32
 
28
33
  ## What `/recipe add` does, in order
29
34
 
@@ -42,28 +47,45 @@ digest, a pull-request watch, a CI sweeper, an ecosystem scan — are the next b
42
47
  `--level act` skips it.
43
48
  3. **A name check.** A job with the same name already in `jobs.json` stops the install and says
44
49
  which recipe or `/cron add` it came from.
45
- 4. **The confirmation.** Every file that will be written and where, every `/cron add` line that
46
- will be run, the budget hint. The setup script is printed into the transcript first, whole and
50
+ 4. **The preflight.** What the project has and lacks for this recipe's runs, checked now and
51
+ printed in the confirmation under *Before the first run* — reported, never enforced, so a
52
+ missing `gh` login is a line you read tonight rather than an empty inbox tomorrow morning.
53
+ A manifest names its checks (`needs`, and `needs_propose` for the levels that push and open
54
+ pull requests); a recipe that reads the tracker is checked for `gh` only when
55
+ `docs/agents/issue-tracker.md` uses it, so a local Markdown tracker asks for nothing.
56
+
57
+ | check | what is looked at |
58
+ |---|---|
59
+ | `gh` | `gh` on the PATH and `gh auth status` succeeding (10 seconds, then "could not check") |
60
+ | `git-remote` | `git remote get-url origin` |
61
+ | `ci-workflows` | a `.yml` under `.github/workflows/` |
62
+ | `lockfile` | one of the package managers' lockfiles at the root |
63
+ | `tracker-github` | the tracker description names GitHub; a local one has no pull requests to watch |
64
+
65
+ `/recipe show` runs the same checks at the recipe's highest level.
66
+ 5. **The confirmation.** Every file that will be written and where, what the playbooks forbid a
67
+ run to do (each playbook's `## Never` section, the same lines `/recipe show` prints), every
68
+ `/cron add` line that will be run, the budget hint, the preflight. The setup script is printed into the transcript first, whole and
47
69
  as written — it will run with this session's environment, and a dialog clips what does not fit,
48
70
  so it is not put in one; a script too long to show is not run at all. A recipe installed from a path
49
71
  rather than by name is flagged as not shipped with pi-loops, with the playbooks to read first.
50
72
  Nothing has happened yet.
51
- 5. **The install.** The playbooks are copied to `.agents/skills/<recipe>/`, with an untouched copy of each
73
+ 6. **The install.** The playbooks are copied to `.agents/skills/<recipe>/`, with an untouched copy of each
52
74
  under `.orig/` and a `.recipe.json` record (version, level, when). The directory (and the
53
75
  tracker description) is listed in `.git/info/exclude` — git's own place for a rule that belongs to one clone — so the repository
54
76
  is not touched and nothing shows in `git status`. pi discovers the copies as `/skill:` commands,
55
77
  which is how a person runs one step by hand when the loop's judgement needs checking — once the
56
78
  project is trusted: a directory that gains `.agents/skills/` is one pi asks about at its next
57
79
  start (`/trust`). The loops do not depend on that; they read the files by path.
58
- 6. **The setup script**, if any, runs once in the project (issue-loop's creates the tracker's
80
+ 7. **The setup script**, if any, runs once in the project (issue-loop's creates the tracker's
59
81
  labels; it is idempotent). A failure stops before any job is created and prints the output; the
60
82
  files stay in place and `/recipe add` again resumes.
61
- 7. **The jobs**, through the same code path as `/cron add`, each carrying `recipe: "<name>"` so
83
+ 8. **The jobs**, through the same code path as `/cron add`, each carrying `recipe: "<name>"` so
62
84
  `/recipe list` and `/recipe remove` can find them. Each prompt is a pointer:
63
85
  `Read .agents/skills/issue-loop/triage.md and do what it says for this repository.` The
64
86
  procedure is read fresh on every run; edit the file and the next run follows the edit.
65
87
 
66
- `pi-loops recipe list|show|add` in a shell does steps 5 and nothing after: the setup script and the
88
+ `pi-loops recipe list|show|add` in a shell does step 6 and nothing after: the setup script and the
67
89
  jobs are things a person should see before they exist, and only the pi inside `/recipe add` can show
68
90
  them. It leaves the files in place and says so; `/recipe add` then finds nothing to copy.
69
91
 
@@ -84,6 +106,29 @@ were there before recipes: the [dangerous-command policy](loops.md#when-somethin
84
106
  night ([loops.md](loops.md#what-it-costs-and-capping-it)); each recipe's `show` says roughly what
85
107
  its default schedules cost.
86
108
 
109
+ ## Checkpoints
110
+
111
+ A checkpoint is a finding that asks a person to decide, and at three in the morning it is read by
112
+ someone who has not seen the tracker since yesterday. So every one of them, in every packaged
113
+ playbook, says the same three things in one line: **the decision**, **what waits on it**, and
114
+ **what happens if nobody acts**.
115
+
116
+ ```text
117
+ #14 brief posted — recommend ready-for-agent (bug, reproduced by npm test) · waits: the implement loop skips it until labeled · if not: stays needs-triage, no reminder
118
+ promote research/007-argpartition: dev 1.0 → 3.1, held-out 1.0 → 2.8 · waits: your merge · if not: the branch and worktree stay, later runs build on it as the best kept
119
+ CI red on main: test/web.test.ts — two fixes did not hold · waits: a person · if not: this signature is not retried
120
+ ```
121
+
122
+ The shape is also how the inbox tells a checkpoint from news: ` · waits:` marks the entry, it is
123
+ listed first with a `⚑`, and `/inbox` counts it apart (`3 new, 1 needs a decision`;
124
+ [loops.md](loops.md#the-inbox)).
125
+
126
+ The third clause is the one that matters overnight. A loop that waits for an answer says so and
127
+ keeps going with everything the answer does not block; a loop that would do something on silence
128
+ (retry, re-propose when the head moves, keep building on a kept branch) says that instead, so a
129
+ person who decides to do nothing knows what nothing buys. Findings that are only news — a merged
130
+ pull request, a red check, a digest line — are not checkpoints and do not carry the clauses.
131
+
87
132
  ## Editing, updating, removing
88
133
 
89
134
  The copies are yours. `/recipe update <name>` after a pi-loops upgrade is a three-way merge of the
@@ -108,7 +153,11 @@ and should be read before it is installed).
108
153
  ```toml
109
154
  name = "issue-loop" # lowercase, digits, dashes; also the install directory
110
155
  summary = "one line"
156
+ tier = "advanced" # optional; "starter" reads and files findings, "advanced" (the default) writes somewhere
157
+ useful_when = ["one sentence"] # optional; the situations it is for, printed by `show`
111
158
  needs_tracker = true # gate on docs/agents/issue-tracker.md
159
+ needs = ["gh", "ci-workflows"] # optional; preflight checks at every level (gh, git-remote, ci-workflows, lockfile, tracker-github)
160
+ needs_propose = ["git-remote"] # optional; the same, added at propose and act
112
161
  levels = ["propose", "act"] # which positions of the dial the playbooks understand
113
162
  setup = "labels.sh" # optional; shown, then run once, before the jobs exist
114
163
  files = ["RESEARCH.template.md"] # optional; copied beside the playbooks unchanged
@@ -126,7 +175,9 @@ same parser: a bad schedule, a `--timeout` that is not a duration, an expression
126
175
  matches, is refused when the manifest is read, not on the first run. A playbook is Markdown with
127
176
  pi's skill frontmatter (`name`, `description`) and an `Autonomy:` line the wizard will set; the
128
177
  rest is what a run should do, written for a sub-agent that has only its notes, the files, and the
129
- tools. The two packaged recipes are the reference for the voice.
178
+ tools. End it with a `## Never` section: the lines under it are what `/recipe show` and the
179
+ install confirmation print as the run's safety envelope, and every packaged playbook has one. The
180
+ packaged recipes are the reference for the voice.
130
181
 
131
182
  ---
132
183
 
@@ -194,12 +245,78 @@ mid-implementation resumes from its branch and notes; a check command failing tw
194
245
  is a block, not a third attempt; the loop and you editing the same issue is settled by the
195
246
  tracker, which wins.
196
247
 
248
+ With the local Markdown tracker the shape is the same and the pull request is not there: the
249
+ implement loop pushes its branch (or leaves it local), sets the item to `in-review` and files a
250
+ finding naming the branch. Requesting changes is a comment under the item's `## Comments` and the
251
+ status set back to `agent-working`; merging is `git merge` by a person, the status set to `done`
252
+ and the file moved to `closed/`. The setup script creates no labels there — it says so and exits —
253
+ and does the same when `gh` is missing or not logged in, so the install goes through and the
254
+ first run reports what it cannot do.
255
+
197
256
  Specific to this repository: the labels are on
198
257
  [github.com/alphacoder-v0/pi-loops](https://github.com/alphacoder-v0/pi-loops/labels), the check
199
258
  command is `npm run ci`, and a release needs a one-time password — so the release side can only
200
259
  propose, which is what `changelog-draft` does everywhere.
201
260
 
202
- ## autoresearch
261
+ ## daily-digest
262
+
263
+ One job at nine, one level. It reads the tracker, the latest run on the default branch, the last
264
+ day's commits, and the automation's own health — a loop that failed its last two runs, a finding
265
+ that has sat in the inbox for three days — and files **one** finding, the day first, then a line
266
+ per thing that needs a look. A quiet day files nothing, and a thing reported yesterday and
267
+ unchanged is not repeated: the notes carry a watermark and yesterday's lines. The loop writes
268
+ nowhere but the inbox.
269
+
270
+ ## pr-watch
271
+
272
+ Every fifteen minutes, each open pull request gets one state — `green`, `red-check`, `conflict`,
273
+ `awaiting-review`, `changes-requested`, `answered` — and a finding is filed only when a state
274
+ changed since the last run, or a stall has lasted another whole day. Under `propose` the loop may
275
+ leave one comment on a pull request that has waited on the same person for more than two days,
276
+ once per stall, remembered in its notes. It never merges, approves, rebases or pushes.
277
+
278
+ ## ci-sweeper
279
+
280
+ Every fifteen minutes, the latest run on the default branch. Green: nothing. Red: a **failure
281
+ signature** (job, step, first error line, normalised) is the memory — under `report` it is one
282
+ finding per signature and one more when the branch is green again; under `propose` the loop
283
+ reproduces it in a worktree, fixes the cause, runs the project's check command and opens a pull
284
+ request, and the same signature attempted twice without a green check is a stop with a finding
285
+ that says so. A test is never skipped or deleted to get to green, and the workflow file is never
286
+ edited to make it pass.
287
+
288
+ ## changelog-draft
289
+
290
+ Weekdays at six, if the default branch is ahead of the last tag by anything but chores: the
291
+ changelog entry in the project's own style and a version picked the way its history picks them.
292
+ Under `report` the draft is the finding; under `propose` it is also a branch, the entry written
293
+ in, the version bumped, and a pull request whose link leads the finding — a person merges it and
294
+ tags. Tagging and publishing are never the loop's: they need a key, a one-time password and a
295
+ decision.
296
+
297
+ ## ecosystem
298
+
299
+ Mondays, once. It searches for repositories that use, mention or fork this project, sorts them
300
+ into integrations, derivatives, mentions and noise, and for each integration or derivative looks
301
+ for **one** thing worth a word: a problem they hit that is fixed upstream, a fork commit worth
302
+ bringing back, an unanswered question. Each draft is a finding with the text ready to post;
303
+ claiming the finding is what sends it, as the person, from their chat. The loop itself never
304
+ posts, comments or opens anything anywhere, which is why its only level is `propose`.
305
+
306
+ ## deps-sweeper
307
+
308
+ Mondays at eight. The loop finds the project's package manager from its lockfile and runs the
309
+ audit and outdated commands that manager has. The **advisory id** and the **package at a major
310
+ version** are the memory: under `report` each advisory not in the notes is one finding, each direct
311
+ dependency a whole major behind is one finding once, and a finding is repeated only when it
312
+ changes — an advisory fixed, a major that moved again. Under `propose` the loop also applies the
313
+ patch and minor updates the manager offers, in a worktree, as **one** pull request per run: the
314
+ project's check command has to be green first, a package on the playbook's deny list is never
315
+ touched, a major is never applied (it is a finding, and a person's decision), and the same package
316
+ attempted twice without a green check is a stop with a finding that says so. The lockfile is
317
+ only ever written by the package manager, never by hand. Merging stays with a person at every level.
318
+
319
+ ## Another tracker
203
320
 
204
321
  The shape Karpathy's autoresearch and Arbor made familiar, without a kernel: a **research
205
322
  contract** a person writes, a **ledger** of every attempt including the failed ones, one isolated
@@ -221,6 +338,11 @@ was tuning against.
221
338
  better: `retired`, worktree removed, branch deleted, no finding — a retired experiment is the
222
339
  normal outcome and the row is what matters. Better on dev: measure on held-out; a dev-only gain
223
340
  is `retired` too, with that note.
341
+ - **A streak of retired rows is the ledger's own checkpoint.** Five in a row since the last kept
342
+ row files one finding asking whether the contract's directions, metric or budget are wrong, and
343
+ the loop keeps going only in idea classes it has not tried; ten in a row stops the loop until
344
+ `RESEARCH.md` changes (its hash is in the notes). An hourly loop in the wrong search space is the
345
+ expensive failure, and the person who wrote the contract is the one who can fix it.
224
346
  - **Promotion** is the checkpoint. Under `propose` the finding names the branch and both numbers
225
347
  and a person merges; under `act` the loop merges into the checkout's current branch and the
226
348
  finding says so. The job runs with `--verify`, and the playbook's *For the checker* section tells
@@ -15,7 +15,7 @@ pi-session-<first 16 chars of the id>.pisession uncompressed ustar, mode 0600,
15
15
 
16
16
  ```text
17
17
  /session-export [path] [--exclude-triggers]
18
- /session-import <path> [--activate-triggers=on|off] [--cwd <dir>] [--resume]
18
+ /session-import <path> [--activate-triggers=off|ask|on] [--cwd <dir>] [--resume]
19
19
  ```
20
20
 
21
21
  Both commands print a sensitivity warning before doing anything, success or failure.
@@ -24,8 +24,13 @@ the source machine's parent-session pointer dropped)
24
24
  into this project's session directory and rewrites the sidecars: automation disabled
25
25
  unless `--activate-triggers=on`, running markers / errors / overlap counters cleared, ids
26
26
  regenerated when they collide with existing ones (loop state follows the new id), non-stateful jobs
27
- rebound to the imported session. Afterwards you are asked once whether to re-enable what was
28
- enabled in the source. `--resume` switches to the imported session; otherwise `pi --session <path>`.
27
+ rebound to the imported session. `--resume` switches to the imported session; otherwise
28
+ `pi --session <path>`.
29
+
30
+ `--activate-triggers=off|ask|on` decides what happens to the automation the archive carries, and
31
+ means the same thing here as in `pi-loops import`. The default is `ask`: the import leaves it
32
+ disabled and then asks once, where there is a terminal or a UI to ask in, whether to enable what
33
+ was enabled in the source. `off` never asks; `on` activates it as part of the import.
29
34
 
30
35
  Validation: manifest schema, session checksum, path traversal, and size caps (session 50 MiB,
31
36
  sidecars 2 MiB). Every sidecar is validated before the session file is written, and store writes
@@ -17,7 +17,9 @@ weaker, and worth converting whenever one of them breaks.
17
17
  - [x] Enter does nothing while an input method is composing — typing Chinese, Japanese or Korean
18
18
  means Enter picks a candidate, not "send". `test/web-page.test.ts`: *Enter while an input
19
19
  method is mid-word does not send*.
20
- - [x] Submitting while a turn is running queues instead of racing it, the way the TUI does.
20
+ - [x] Submitting while a turn is running queues instead of racing it, the way the TUI does — and a
21
+ slash command typed into it runs while the turn runs, as it does in the terminal.
22
+ `test/web.test.ts`: *a command typed while a turn is running reaches pi as one it can run*.
21
23
  - [x] The queue is visible and can be cleared.
22
24
  - [x] Abort stops the current turn.
23
25
  - [x] Busy, queued count, and "pi exited" are all visible without opening a terminal.
@@ -49,6 +51,14 @@ weaker, and worth converting whenever one of them breaks.
49
51
  - [x] A tap on a composer button lands on that button, even with a soft keyboard open — tapping it
50
52
  blurs the box, which dismisses the keyboard, which moves everything.
51
53
  `test/web-page.test.ts`: *a phone's soft keyboard cannot steal the tap on send*.
54
+ - [x] What is in the composer survives a reload, and belongs to the tab that typed it: the header's
55
+ reload button, the stale-page banner and a phone that discards a backgrounded tab all come
56
+ back to the same text, and a second tab of the same address keeps its own.
57
+ `test/web-page.test.ts`: *a reload keeps what is in the composer, in that tab only*.
58
+ - [x] The composer grows with what is typed into it, up to the ceiling the stylesheet declares, and
59
+ comes back down when the text goes or is sent — a multi-line prompt is written and read at its
60
+ own height rather than through a one-line slot. `test/web-page.test.ts`: *the composer grows
61
+ with a multi-line prompt, and shrinks again*.
52
62
 
53
63
  ## What the feed shows
54
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alphacoder-v0/pi-loops",
3
- "version": "0.19.3",
3
+ "version": "0.22.0",
4
4
  "description": "An automation layer for pi: cron + stateful loops with a triage inbox, maker/checker verification, dynamic triggers, MCP notifications and tools, lifecycle hooks, session archives. A non-invasive pi extension.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,6 +35,7 @@
35
35
  "recipes",
36
36
  "README.md",
37
37
  "README.zh-CN.md",
38
+ "CONTRACT.md",
38
39
  "CHANGELOG.md",
39
40
  "LICENSE"
40
41
  ],
@@ -45,14 +46,17 @@
45
46
  "publishedToNpm": true
46
47
  },
47
48
  "scripts": {
48
- "ci": "npm run typecheck && npm run lint && npm run check:scripts && npm run check:docs && npm test",
49
+ "ci": "npm run typecheck && npm run lint && npm run check:scripts && npm run check:docs && npm test && npm run check:downstream && npm run check:contract",
49
50
  "typecheck": "node scripts/typecheck.mjs",
50
51
  "lint": "node scripts/lint.mjs",
51
52
  "check:docs": "node scripts/check-docs.mjs",
53
+ "check:downstream": "sh test/downstream/run.sh",
54
+ "check:contract": "sh test/contract/run.sh",
52
55
  "test": "node --import ./src/register-pi.mjs --test test/*.test.ts",
53
56
  "check:scripts": "node --check src/web.mjs && node --check src/cli-entry.mjs && node --check src/host-entry.mjs && node --check src/ts-entry.mjs && node --check src/pi-resolver.mjs && node --check src/register-pi.mjs && node --check examples/mcp-notify-server.mjs"
54
57
  },
55
58
  "peerDependencies": {
59
+ "@earendil-works/pi-ai": "*",
56
60
  "@earendil-works/pi-coding-agent": "*",
57
61
  "@earendil-works/pi-tui": "*",
58
62
  "typebox": "*"
@@ -29,7 +29,9 @@ checkpoint is a finding in the inbox that names the branch.
29
29
  - **Change state**: edit the `**Status:**` line — exactly one state
30
30
  - **Claim**: set `**Status:** agent-working` and add `**Assignee:** <who>`, and commit that edit on the work branch before any other work
31
31
  - **Close**: set the status to `wontfix` or `done` and move the file to `.scratch/issues/closed/`
32
- - **Review**: push the branch (or leave it local) and file a finding that names the branch and the item; a person merges with git
32
+ - **Review**: push the branch (or leave it local), set `**Status:** in-review`, and file a finding that names the branch and the item — `in-review` here means the branch exists and the finding has been filed; there is no pull request
33
+ - **Request changes** (a person): append a comment under `## Comments` saying what to change and set `**Status:** agent-working`; the implement loop reads comments newer than the timestamp in its notes and addresses them
34
+ - **Merge** (a person): merge the branch with git, set `**Status:** done`, and move the file to `closed/`; the loop's housekeeping removes the worktree on its next run
33
35
 
34
36
  ## State vocabulary
35
37
 
@@ -42,6 +42,20 @@ before anything slow. Your final reply is read by a program.
42
42
  trying first" and "not to try" lists bound the space. Write one sentence: what you will change
43
43
  and why it should move the metric. Append the row now with status `running` — a crash leaves a
44
44
  record, not a mystery.
45
+
46
+ **First, look back.** Count the `retired` rows since the last `kept` or `promoted` one (or the
47
+ baseline). A streak is the ledger telling you the space is wrong, not that the next idea is:
48
+ - **5 in a row**, and you have not said so since the streak began (your notes' `stalled-at`):
49
+ one finding — `autoresearch: 5 retired in a row (<the idea classes tried>) · waits: a
50
+ change to RESEARCH.md's directions, metric or budget · if not: I keep trying, but only in
51
+ classes not yet tried` — and choose this run's hypothesis outside every class in your notes'
52
+ `tried` line.
53
+ - **10 in a row**: stop choosing. One finding — `autoresearch: 10 retired in a row, stopping
54
+ until RESEARCH.md changes · waits: an edit to the contract · if not: nothing runs; the ledger
55
+ is at <path>` — write the notes with the contract's hash, and end the run. A later run whose
56
+ `RESEARCH.md` hash differs from the notes' starts the count again at zero; one whose hash is
57
+ the same files nothing and stops again.
58
+ A `kept` or `promoted` row resets the streak.
45
59
  6. **Branch.** `git worktree add <worktrees>/<NNN> -b research/<NNN>-<slug> <current branch>`
46
60
  (create the worktrees directory first if it is missing).
47
61
  Change only the editable files. Commit in the worktree before measuring.
@@ -53,7 +67,8 @@ before anything slow. Your final reply is read by a program.
53
67
  contract's promotion rule → status `kept` and go to 8; otherwise `retired` with note
54
68
  `dev-only gain` (the classic overfit; still negative evidence) and remove the worktree.
55
69
  8. **Promote.** Under `propose`: one finding —
56
- `promote research/<NNN>-<slug>: dev <x> → <y>, held-out <a> → <b> (<hypothesis>)` — and leave
70
+ `promote research/<NNN>-<slug>: dev <x> → <y>, held-out <a> → <b> (<hypothesis>) · waits: your
71
+ merge · if not: branch and worktree stay; later runs build on it as the best kept` — and leave
57
72
  the worktree and branch for the person. Under `act`: in the project checkout,
58
73
  `git merge --no-ff research/<NNN>-<slug>`, set status `promoted`, remove the worktree, keep the
59
74
  branch, and file the finding as `promoted …`.
@@ -68,15 +83,24 @@ A second sub-agent reviews the findings of this run before they reach the inbox.
68
83
  or `promoted` finding: check out the named branch in its worktree (or the merge in the checkout),
69
84
  run the contract's **held-out** command yourself, and keep the finding only if the number you get
70
85
  satisfies the promotion rule against the ledger's best row; drop it with the number otherwise. A
71
- `no RESEARCH.md` or protected-file finding is kept if the file state is as described.
86
+ `no RESEARCH.md` or protected-file finding is kept if the file state is as described; a `retired
87
+ in a row` finding is kept if the ledger shows the streak it names.
72
88
 
73
89
  ## Notes for the next run
74
90
 
75
91
  `best=<id> <heldout>` · `next=<NNN>` · `in-progress: <id> worktree=<path>` or `none` ·
76
- `tried: <one line per idea class you have exhausted>`. Under 2000 characters; the ledger holds
77
- the rest.
92
+ `tried: <one line per idea class you have exhausted>` · `stalled-at=<row id the 5-streak finding
93
+ was filed at, or none>` · `contract=<sha256 of RESEARCH.md>`. Under 2000 characters; the ledger
94
+ holds the rest.
78
95
 
79
96
  ## Inbox
80
97
 
81
- Only: a promotion (proposed or done), a contract problem, an eval that cannot run. Never a retired
82
- experiment — the ledger is where those live, and a person reads the ledger when they want to.
98
+ Only: a promotion (proposed or done), a contract problem, an eval that cannot run, a streak of
99
+ retired experiments long enough to question the contract. Never a single retired experiment — the
100
+ ledger is where those live, and a person reads the ledger when they want to.
101
+
102
+ ## Never
103
+
104
+ Never write the contract (`RESEARCH.md`) yourself, never edit a protected file. Never push; never
105
+ merge under `propose`; never run two experiments in one run. Never retry an eval that failed for
106
+ a reason that is not your change — that is one finding and a stop.
@@ -2,6 +2,8 @@
2
2
  # a ledger, a checker re-measures on held-out data before anything is proposed. docs/recipes.md.
3
3
  name = "autoresearch"
4
4
  summary = "One experiment per run against a research contract you wrote; a ledger of what was tried; promotion only on held-out evidence."
5
+ tier = "advanced"
6
+ useful_when = ["You have a metric a script can print and a search space wider than your evenings.", "You want every attempt in a ledger, the failed ones included."]
5
7
  needs_tracker = false
6
8
  levels = ["propose", "act"]
7
9
  files = ["RESEARCH.template.md", "example/RESEARCH.md", "example/solution.py", "example/eval.py", "example/eval.sh"]
@@ -1,7 +1,12 @@
1
1
  name = "changelog-draft"
2
2
  summary = "When the default branch is ahead of the last tag, draft the changelog entry and propose a version; a person tags."
3
+ tier = "starter"
4
+ useful_when = ["Releases wait on someone finding an evening to write the entry.", "You want the version and the entry proposed from the commits, and to tag it yourself."]
3
5
  needs_tracker = false
4
- levels = ["report"]
6
+ levels = ["report", "propose"]
7
+ needs = ["git-remote"]
8
+ needs_propose = ["gh"]
9
+ budget_hint_usd = 1
5
10
 
6
11
  [[job]]
7
12
  name = "changelog-draft"
@@ -7,7 +7,13 @@ description: One run of the release loop. Compares the default branch with the l
7
7
 
8
8
  Autonomy: report
9
9
 
10
- You are the release loop. You **propose**; a person releases. Tagging and publishing need things
10
+ The line above is the installed level.
11
+ - `report`: the draft goes in the inbox line.
12
+ - `propose`: the draft goes into a pull request too — a branch, the changelog entry written in,
13
+ the version bumped where the project keeps it — and the inbox line carries the link. A person
14
+ merges it and tags.
15
+
16
+ You are the release loop. You propose; a person releases. Tagging and publishing need things
11
17
  an unattended run does not have (a signing key, a one-time password, a decision about the version
12
18
  number), so the checkpoint is placed here, after everything that can be prepared has been.
13
19
 
@@ -28,8 +34,22 @@ number), so the checkpoint is placed here, after everything that can be prepared
28
34
  version the way the project's history does (a `feat:` bumps the minor of a 0.x project; only
29
35
  fixes bump the patch). Do not edit any file; the draft goes in the inbox line.
30
36
 
31
- 4. Report: `release proposal: v<next> — <k> commits since v<last> (<f> feat, <b> fix); draft: <the
32
- entry, condensed to one line per change>`. Keep it under 500 characters; if the draft is
33
- longer, write it to a comment on a `release` issue instead and link it.
37
+ 4. Under `propose`, before reporting: `git worktree add ../<repo>-release/<next> -b release/<next>
38
+ origin/<default branch>`, write the entry at the top of `CHANGELOG.md` in the project's style,
39
+ bump the version wherever the project keeps it (`package.json`, and any file the project's own
40
+ checks tie to it — run those checks), commit `chore: release <next>`, push `-u`, open a pull
41
+ request whose body starts with a line saying it was generated by AI and lists the commits.
42
+ If a release pull request of yours is already open for an older head, update its branch
43
+ instead of opening a second one. Never tag, never publish, never push the default branch.
44
+
45
+ 5. Report: `release proposal: v<next> — <k> commits since v<last> (<f> feat, <b> fix); draft: <the
46
+ entry, condensed to one line per change> · waits: your tag · if not: re-proposed when the head
47
+ moves, not before` — under `propose`, `PR <url>` first and the draft after. Keep it under 500
48
+ characters; if the draft is longer, the pull request has it whole and the finding says so.
49
+
50
+ Notes: `last-reported-head=<sha> last-tag=<tag> open-pr=<url or none>`.
51
+
52
+ ## Never
34
53
 
35
- Notes: `last-reported-head=<sha> last-tag=<tag>`.
54
+ Never tag, never publish, never push the default branch. Never open a second release pull
55
+ request while one of yours is open — update its branch instead.
@@ -0,0 +1,9 @@
1
+ # ci-sweeper — a red default branch, reported or repaired
2
+
3
+ | file | what it is |
4
+ |---|---|
5
+ | [recipe.toml](recipe.toml) | one job every 15 minutes, 40-minute timeout, `report` or `propose` |
6
+ | [sweep.md](sweep.md) | the playbook: latest run → failure signature → finding, or worktree, fix, checks, pull request |
7
+
8
+ The failure signature is the memory: the same red reported once, the same failure attempted at
9
+ most twice, and a test never skipped or deleted to get to green.
@@ -0,0 +1,16 @@
1
+ # ci-sweeper: a red default branch gets a finding, or a fix in a worktree and a pull request. docs/recipes.md.
2
+ name = "ci-sweeper"
3
+ summary = "When the default branch's checks go red, says so — or, at propose, reproduces the failure in a worktree, fixes it, and opens the pull request; twice failing to fix the same failure is a stop."
4
+ tier = "advanced"
5
+ useful_when = ["The default branch goes red and every open pull request looks red with it.", "The failures are the boring kind: a flaky assertion, a renamed fixture, a dependency that moved."]
6
+ needs_tracker = false
7
+ levels = ["report", "propose"]
8
+ needs = ["gh", "ci-workflows"]
9
+ needs_propose = ["git-remote"]
10
+ budget_hint_usd = 5
11
+
12
+ [[job]]
13
+ name = "ci-sweeper"
14
+ schedule = "*/15 * * * *"
15
+ playbook = "sweep.md"
16
+ timeout = "40m"