@alphacoder-v0/pi-loops 0.22.8 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +664 -0
  2. package/CONTRACT.md +7 -7
  3. package/README.md +91 -63
  4. package/README.zh-CN.md +59 -39
  5. package/docs/cli.md +272 -83
  6. package/docs/configuration.md +86 -24
  7. package/docs/design.md +395 -56
  8. package/docs/downstream.md +2 -2
  9. package/docs/goal.md +12 -3
  10. package/docs/hooks.md +6 -7
  11. package/docs/loops.md +288 -59
  12. package/docs/mcp.md +22 -14
  13. package/docs/recipes.md +6 -0
  14. package/docs/screenshot.png +0 -0
  15. package/docs/triggers.md +120 -26
  16. package/docs/troubleshooting.md +37 -26
  17. package/docs/web-ui-parity.md +691 -65
  18. package/package.json +4 -3
  19. package/recipes/autoresearch/recipe.toml +1 -0
  20. package/recipes/changelog-draft/recipe.toml +1 -0
  21. package/recipes/ci-sweeper/recipe.toml +1 -0
  22. package/recipes/daily-digest/recipe.toml +1 -0
  23. package/recipes/deps-sweeper/recipe.toml +1 -0
  24. package/recipes/ecosystem/recipe.toml +1 -0
  25. package/recipes/issue-loop/recipe.toml +1 -0
  26. package/recipes/pr-watch/recipe.toml +1 -0
  27. package/skills/pi-loops/SKILL.md +6 -4
  28. package/src/activity.ts +199 -0
  29. package/src/archive.ts +21 -9
  30. package/src/args.ts +6 -2
  31. package/src/cli-entry.mjs +12 -3
  32. package/src/cli.ts +746 -125
  33. package/src/config.ts +53 -5
  34. package/src/conversation-rows.ts +469 -0
  35. package/src/danger.ts +227 -6
  36. package/src/engine.ts +1240 -0
  37. package/src/goal.ts +71 -0
  38. package/src/held-read.ts +27 -0
  39. package/src/hooks.ts +49 -18
  40. package/src/host-control-channel.ts +455 -40
  41. package/src/host-control.ts +602 -111
  42. package/src/host-entry.mjs +5 -3
  43. package/src/host-runtime.ts +455 -42
  44. package/src/host.ts +659 -123
  45. package/src/inbox-card.ts +278 -0
  46. package/src/inbox.ts +139 -62
  47. package/src/job-edit.ts +7 -1
  48. package/src/job-health.ts +14 -2
  49. package/src/job-lines.ts +201 -13
  50. package/src/job-owner.ts +74 -13
  51. package/src/job-tune.ts +110 -0
  52. package/src/lease.ts +537 -0
  53. package/src/lock.ts +7 -2
  54. package/src/mcp-pool.ts +66 -14
  55. package/src/mcp.ts +1 -1
  56. package/src/open-file.ts +54 -0
  57. package/src/page-children.ts +1507 -0
  58. package/src/page-devices.ts +192 -0
  59. package/src/page-server.ts +3150 -0
  60. package/src/path-precheck.ts +215 -0
  61. package/src/paths.ts +24 -0
  62. package/src/pi-floor.ts +10 -0
  63. package/src/pi-loops.ts +1301 -450
  64. package/src/pi-resolver.d.mts +1 -0
  65. package/src/pi-resolver.mjs +110 -10
  66. package/src/presence.ts +12 -63
  67. package/src/protocol.ts +10 -1
  68. package/src/recipe.ts +43 -1
  69. package/src/requests.ts +354 -0
  70. package/src/rule-lines.ts +68 -0
  71. package/src/run-card.ts +105 -0
  72. package/src/runner.ts +18 -1
  73. package/src/schedule.ts +1 -2
  74. package/src/scheduler.ts +595 -245
  75. package/src/sdk-runner.ts +321 -27
  76. package/src/seen.ts +278 -0
  77. package/src/session-head.ts +21 -0
  78. package/src/share.ts +12 -2
  79. package/src/snapshot.ts +3 -2
  80. package/src/store.ts +81 -8
  81. package/src/tools.ts +196 -65
  82. package/src/trigger-runtime.ts +164 -127
  83. package/src/triggers.ts +39 -7
  84. package/src/trust.ts +46 -2
  85. package/src/ui-prefs.ts +16 -5
  86. package/src/watch-plan.ts +174 -0
  87. package/src/web.d.mts +3 -0
  88. package/src/web.mjs +2260 -2340
  89. package/src/window-executor.ts +459 -0
  90. package/src/window-guard.ts +158 -0
  91. package/docs/agents/issue-tracker.md +0 -34
package/docs/hooks.md CHANGED
@@ -30,7 +30,7 @@ Authorization = "Bearer your-token"
30
30
  as `hook <source> <event>: <output>` — redacted and rotated like everything else there, and cut
31
31
  off after 4000 characters so a chatty hook cannot rotate away the night's history. `echo` and
32
32
  read the file is the usual way to find out what a hook did. stderr is still kept for the failure
33
- message only. The headless host does the same into `host.log`, which is where the question is
33
+ message only. pi-loops does the same into `host.log`, which is where the question is
34
34
  usually asked.
35
35
  - Webhooks receive `Content-Type: application/json` with a payload whose every field is always
36
36
  present, `null` when it does not apply. `message_kind` is `user` | `assistant` | `tool_result`
@@ -76,9 +76,8 @@ Authorization = "Bearer your-token"
76
76
  you are talking to fires them. Sub-agent sessions — loop runs, checkers, trigger checks — fire
77
77
  nothing: they are not the session you are in.
78
78
  - **`run_start` and `run_end` are about a scheduled run**, and they fire wherever the run happens:
79
- in the pi you are talking to, and in the headless host ([loops.md](loops.md)). Whether a 3am job
80
- ran under a host or under a pi you left open is an accident of who held the clock, and a hook rule
81
- should not be able to tell.
79
+ which is pi-loops: every loop runs in the background ([loops.md](loops.md)). A plain job is a message in a
80
+ conversation, not a run, and fires neither.
82
81
 
83
82
  They exist because a scheduled run is not a turn. It happens with no conversation at all, or
84
83
  beside one — and overloading `agent_*` would mean a rule you wrote about your own turns quietly
@@ -96,15 +95,15 @@ Authorization = "Bearer your-token"
96
95
  command = '[ "$PI_RUN_OK" = false ] && notify-send "loop $PI_RUN_JOB failed" "$PI_RUN_ERROR"'
97
96
  ```
98
97
 
99
- In the host, where there is no conversation, `session_id` is the run id and `cwd` — including any
98
+ In pi-loops, where there is no conversation, `session_id` is the run id and `cwd` — including any
100
99
  `cwd = "project"` rule — is the job's directory.
101
100
 
102
101
  - `run_*` hooks are always queued off the run, whatever `[hooks] mode` says, in both processes — a
103
102
  webhook that hangs must not hold up the clock, and the run it announces has already started or
104
103
  already finished. `mode = "sync"` is about ordering within a conversation turn, and a run is not
105
- one. The host drains for up to 3 seconds when it exits. Failures go to the host log,
104
+ one. pi-loops drains for up to 3 seconds when it exits. Failures go to pi-loops log,
106
105
  `~/.pi/agent/loops/host.log`.
107
106
 
108
- - Project hooks in the host additionally need the job's cwd to be a directory you trusted in pi
107
+ - Project hooks in the background additionally need the job's cwd to be a directory you trusted in pi
109
108
  (that exact directory, not an ancestor): a job's cwd can be chosen by a model, and nobody is
110
109
  there to answer a trust prompt.
package/docs/loops.md CHANGED
@@ -8,16 +8,87 @@ Everything here is `/cron`; `/crontab` and `/loop` are the same command under ot
8
8
  /cron add "*/30 * * * *" summarize the repo state
9
9
  ```
10
10
 
11
- When due, the prompt is injected into the session that created the job as a user message with the
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. A plain job belongs to the session that created it:
11
+ When due, the prompt is written as a request addressed to the session that created the job
12
+ (`requests/<session>.jsonl`), and the window holding that session hands it to the agent as a user
13
+ message with the engine prefix `[Trigger <run-id>] ` once the agent is idle; the agent runs one turn
14
+ (an *inject-and-run* job). While the agent is busy the prompt waits in the queue, where Esc cannot
15
+ drop it and a window that quits leaves it for the next one, and runs once the agent is idle, as a
16
+ message you queued in a chat does: Esc, and the browser's Stop, end the current turn only. In
17
+ exchange the queue is visible and cancellable: the window says `queued: cron <name> runs after the
18
+ current turn` when the prompt is queued behind a busy turn, the status line shows `queued N` while
19
+ rows wait for this conversation, and `/cron queue` lists them numbered with their source, age and
20
+ text; `/cron queue cancel <n|id>` takes one out and `/cron queue clear` all of them, each cancel
21
+ audited. While the prompt waits, `/cron` marks the job `[queued <how long>]`,
22
+ with the reason when the queue knows one (`backing off until <when>` after a hand-over that failed),
23
+ and `cron_list` reports `queued` with `since`. A hand-over pi does not take (no model, no
24
+ credentials, another extension's input handler swallowing it: pi starts no turn and tells the
25
+ extension nothing) is judged ten seconds after pi's last input event, with pi idle, and offered
26
+ again after a minute, then five; the third refusal makes the prompt a finding in the inbox and frees
27
+ the job, and a slot due while a refused prompt waits replaces it instead of being skipped onto it,
28
+ so one refusal costs one slot, as it did in 0.22. A prompt pi dropped before writing it (Esc, or an
29
+ input handler that rewrote the `[Trigger <id>] ` prefix away) is offered again at most three times,
30
+ each turn under the job's cap, then goes to the inbox the same way. The prompt of a job you disabled
31
+ waits in the queue until you enable it again; a prompt `/cron run` queued runs even if the job was
32
+ disabled since, because you asked for it. A one-shot job is removed once its prompt is in the
33
+ conversation, not when it is queued. A plain job belongs to the session that created it:
14
34
  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.
35
+ One window holds a conversation at a time. Open the same conversation in a second window and that
36
+ window is a viewer: it fires none of the conversation's plain jobs, `/cron` there marks each one
37
+ `[session <id> — open in a terminal]` (or `in the page`) with its next run, and `/cron run` says
38
+ where to run it.
39
+ When the holding window closes, the viewer takes the conversation over on its next tick. If the
40
+ other window added turns after the viewer opened the conversation, the viewer's copy is older than
41
+ the file, and a turn fired there would leave those turns on a dead branch. So that window says to
42
+ `/resume` the conversation, and its plain jobs and rule results wait until you do: `/cron` there
43
+ marks each one `[session <id> — this window shows an older copy; /resume it to run here]`, and
44
+ `/cron run` says the same.
45
+ When no window holds the conversation, pi-loops fires the job in it: it takes the conversation for
46
+ that one turn, runs the prompt there on the conversation's own model (a plain job pins none, and a
47
+ `model` a 0.22 chat stored on one is ignored) as the conversation's own turn, so `cron_create`,
48
+ `cron_list all_projects` and the other tools act for that conversation and a control-plane confirm
49
+ is refused with a sentence that says the turn ran in the background with nobody to ask,
50
+ and gives it back, so the answer is in the conversation when you resume it. `/cron` in another
51
+ window marks such a job `[session <id> — in the background]`, with its next run. pi-loops leaves a
52
+ conversation alone while someone else has written its file in the last two minutes (its own last
53
+ write does not count). A window that opens the conversation during pi-loops' turn takes it back at
54
+ once: the turn stops at its next step, the slot is owed, and that window runs the job straight away,
55
+ even long after its due time and with catch-up off, because the slot was tried, not missed. A turn
56
+ pi-loops stops itself (`pi-loops stop`, an upgrade, the daily budget) is owed the same way and runs
57
+ on pi-loops' next tick. A turn you stop (`/triggers abort`, `pi-loops abort`) is done: the slot is
58
+ spent and the prompt does not run again. Each turn is a run: it appears in `/cron runs` with its
59
+ cost, a failure sets the job's error and the same backoff a loop gets, and a one-shot that fails is
60
+ tried once more before it is retired. A conversation pi-loops has never seen open in a window (its
61
+ file is unknown) is asleep: `[session <id> — resume it to run]` with no next run, and `/cron run`
62
+ refuses it. When pi-loops is not running, the job shows `paused: <why>` instead. The daily budget (`[limits] daily_budget_usd`) covers these
63
+ turns as it covers loop runs, counting only what each turn adds, not the conversation's history. The
64
+ pi-loops loads a conversation's extensions once for its turns and releases them after an hour without
65
+ one. Something whose result should not land in a conversation (a nightly
66
+ digest, a watch) is a loop: `--stateful`, with the inbox as its outlet.
67
+
68
+ A plain job's turn is a scheduled turn wherever it runs, and it runs under the job's own `--tools`
69
+ and the dangerous-command policy (below) in an open window as in the background. So does the turn an
70
+ MCP server's `inject_and_run` push starts ([mcp.md](mcp.md)), with no declared tools: the
71
+ dangerous-command policy alone. The difference is
72
+ who answers. In the background a call outside them is refused. In a window, while the job's turn
73
+ runs, such a call asks you first: a question with a 60-second countdown and two answers, `No, refuse
74
+ it` focused and `Yes, run it once` under it, so Enter refuses. The yes answer runs it this once;
75
+ anything else, Escape or no answer refuses it and tells the model why, so it does the safe part and reports:
76
+ your No is reported as your refusal, no answer as nobody being there. A
77
+ message you send into that turn makes the rest of it yours and nothing asks again. Your own turns, a
78
+ `/goal` continuation, a claimed finding, a recipe hand-off, `/new-trigger` and `/cron run` never ask.
79
+ pi-loops' own tools are not asked about a second time: `cron_create` in such a turn makes a job
80
+ capped to the turn's tools, as a job a background run makes is capped to that run's, and
81
+ `new_trigger`, the trigger tools, `cron_remove` and `set_cron_job_state` ask through their own
82
+ confirm, once, where the background would refuse.
83
+ With you present, in your own turn, `cron_create` asks you before it creates anything: pi's selector
84
+ in a terminal, the same question drawn as a card in the browser page. The first answer is the
85
+ model's own choice: *Put it in my inbox* (a loop, with its notes between runs, with or without a
86
+ checker) or *Show it in this conversation* (a plain job of this conversation). The tool returns
87
+ `created cron job …` or `cancelled: …` in the same call; Cancel, Esc and thirty minutes with no
88
+ answer write nothing. The question shows no directory, so a `cwd` outside this project is refused
89
+ before it is asked ([design.md](design.md), decision 24).
90
+ `[engine] window_guard = false` in `config.toml` turns the cap off in windows
91
+ ([configuration.md](configuration.md)).
21
92
 
22
93
  Schedules are local time: 5-field cron (`*/n`, ranges, lists, `mon-fri`, `jan`), the aliases
23
94
  (`hourly` / `every hour` / `once an hour` → `0 * * * *`; `daily` / `every day` → `0 9 * * *`;
@@ -31,8 +102,11 @@ Schedules are local time: 5-field cron (`*/n`, ranges, lists, `mon-fri`, `jan`),
31
102
  /cron add --stateful "0 9 * * *" check the repo issues and report anything new since the last run
32
103
  ```
33
104
 
34
- Each run is a fresh sub-session inside the interactive pi, through pi's SDK: full tools including the parent's live MCP servers, the session's model and thinking
35
- level, no conversation history, its own transcript file. Every run gets the same prompt: the output
105
+ Each run is a fresh sub-session in pi-loops in the background, through pi's SDK: the built-in
106
+ tools of the chat the loop was made in (or its `--tools`), the MCP servers it connects itself from
107
+ `mcp.toml` and the project, the model and thinking level of the chat it was made in, no
108
+ conversation history, its own transcript file. It does not get the window's `-e` extensions, flags
109
+ or live MCP connections. Every run gets the same prompt: the output
36
110
  protocol, preceded by one line of context (the job's name, when the run started, whether it is a
37
111
  catch-up):
38
112
 
@@ -59,9 +133,40 @@ Then:
59
133
  - missing or malformed tags never fail a run: the state stays untouched, nothing enters the inbox;
60
134
  - a run that is still going when the next tick arrives is skipped and counted (`skipped overlaps`).
61
135
 
62
- Every run leaves a trail: the sub-agent's full transcript (`/cron trace <job> [k]`, 20 per job), a
63
- run log with exit code, duration, cost and finding counts (`/cron runs`), and a card in the
64
- transcript when the run finishes. Prompts are capped at 8 KB.
136
+ Every run leaves a trail: the sub-agent's full transcript (`/cron trace <job> [k]`, 20 per job) and
137
+ a run log with exit code, duration, cost and finding counts (`/cron runs`). Prompts are capped at 8 KB.
138
+
139
+ ### Run cards
140
+
141
+ A run writes into the conversation that created its loop only when there is something to see:
142
+
143
+ - a loop's first three runs each get one compact card, so you see that it works and what it reports;
144
+ - every failed run gets a card, and a terminal also shows it as a warning;
145
+ - a run a person asked for (`/cron run <loop>`, or the run button in the page) gets a card in the
146
+ conversation that asked, whatever its place among the loop's runs and whichever conversation created
147
+ the loop. Its card also shows the first line of the run's reply, cut at 120 characters, with escape
148
+ codes and bidi controls taken out;
149
+ - a terminal and the page's pi save a card the same way: the page draws it when it is saved and when
150
+ it reads the conversation again. A window that does not hold the conversation only shows the line.
151
+ The page does not draw the cards of a conversation whose session file is over 32 MB: reading its
152
+ entries would hold up every page. A conversation a terminal holds also shows, in the page, the
153
+ pi-loops views that terminal wrote into it (`/cron`, `/inbox`), redacted and cut at 4000 characters;
154
+ - every other run writes nothing into the conversation. A terminal lists the latest three loop runs of
155
+ the project in the panel above the editor (`Runs`), the page lists the project's runs and rule results
156
+ of the last day in its loop activity strip above the composer, and `/cron runs` has them all.
157
+
158
+ A card is one line: `todo · 5s · $0.003 · 1 finding`, or `todo FAILED · 4s · <error>`; a loop with no name is labelled by the first 13 characters of its id (`cron-15c11c9f`), as every surface but `/cron`, `cron_list` and `inbox --json` labels it. It
159
+ is saved in the conversation, so a resumed conversation still shows how its loop started, and
160
+ `/session-share` leaves it out. A run that was stopped (by a person, a quit or a restart) is not a
161
+ failure and gets no card. When more than five carded runs finish between two looks at the run log,
162
+ five are shown and the rest is one line: `+4 more runs finished (4 failed) — /cron runs`.
163
+
164
+ Who asked for a run is in its run log record as `requestedBy: { sessionId }`. A scheduled run, and a
165
+ record written before 0.24.0, has none.
166
+
167
+ A run's place among its loop's runs is in its run log record (`ordinal`). A stopped run does not
168
+ count, so the run after it has the same place. A record written before 0.24 has no place and is not
169
+ one of the first three.
65
170
 
66
171
  ## The inbox
67
172
 
@@ -70,10 +175,14 @@ Global JSONL, shared by every session and project. Each line is one finding in t
70
175
  `cron-<32 hex>`; prefixes, names and list numbers resolve.
71
176
 
72
177
  ```text
73
- /inbox Inbox (<project>, N new, K need a decision, times <offset>): "<n>. [<id prefix>] <finding> (<project>, <source>, <created_at>)"
178
+ /inbox Inbox (<project>, N new, K need a decision, times <offset>): "<n>. [<id prefix>] <finding> (<project>, <loop label or source>, <created_at>)"
179
+ a decision is listed on up to three lines, its clauses under it:
180
+ "<n>. [<id prefix>] ⚑ <decision> (<project>, <source>, <created_at>)"
181
+ " waiting on: <what waits on it>"
182
+ " if you don't: <what happens if nobody acts>" (left out when the loop left it out)
74
183
  /inbox --all the same, every project on this machine
75
184
  /inbox all [--all] history including claimed and dismissed
76
- /inbox claim <n|id> mark claimed and start a real agent turn:
185
+ /inbox claim <n|id> mark claimed and queue a real agent turn, run once the agent is idle:
77
186
  "A recurring loop (<source>, running in <cwd>) reported this finding — investigate and address it: …"
78
187
  (the loop's cwd is part of the line; a checker-kept finding adds a line saying so)
79
188
  /inbox dismiss <n|id> [reason] mark dismissed; the reason, if you give one, is shown to the loop's next run
@@ -100,6 +209,43 @@ while an id or an id prefix still resolves machine-wide. `/inbox clear` dismisse
100
209
  not the unread findings of four other projects. A finding stored without a cwd belongs to no
101
210
  project and is listed in all of them.
102
211
 
212
+ A loop's finding names the conversation that created the loop and the transcript of the run that
213
+ filed it. What happened while nobody looked is counted per project: when a conversation starts,
214
+ one line counts the project's loop runs and new findings since `/inbox` was last listed in any
215
+ conversation of the project, or the page's **Mark all seen** was pressed for it:
216
+ `[inbox] since you last looked (yesterday 23:10): 41 loop runs (3 failed), 3 new findings (1 from
217
+ this conversation) — /inbox`. Every loop of the project counts, a recipe's included, whichever
218
+ conversation created it; `from this conversation` counts the findings whose loop or rule was made
219
+ in this one, and is said only when that is not every finding. A loop that belongs to its project rather than a conversation (a recipe installed in
220
+ a conversation pi had not saved yet, [recipes.md](recipes.md)) is counted in the project and is
221
+ from no conversation; its finding runs in the conversation you claim it from (**Yes, do it here**), and
222
+ `pi-loops inbox claim` marks it claimed with no conversation to act on it, so the caller handles it
223
+ ([cli.md](cli.md#inbox)). Such a loop is stored with `owner:
224
+ "project"` and no `createdBy`, and a `.pisession` archive keeps it the project's on import. A
225
+ rule check that filed a finding for two conversations is one finding. A stopped run
226
+ is a run, not a failure. A finding counts while it is new, and a finding stored without a cwd
227
+ counts in every project, as `/inbox` lists it. The time is local: `09:14` today, `yesterday
228
+ 23:10`, a weekday within the week, a date before that.
229
+
230
+ Listing `/inbox` clears the line for the project, in every conversation of it and in the page;
231
+ `/inbox --all` clears it for every project. The page's away card counts the same way, from the
232
+ same function (it says the findings and leaves the runs to the activity strip), and its **Mark all seen** clears the terminal's line too: the next session start
233
+ says nothing until something new happens. Marking moves only the cursor; the findings stay new.
234
+ The terminal's `/inbox` marks the time it finished listing, so a finding filed in the milliseconds
235
+ while it listed counts as seen; the page sends the time of the answer its card shows, so what
236
+ arrived after that is still counted.
237
+ The cursors are in `seen.json`, one per project. When nobody has looked yet (a new project, or the
238
+ first start after an upgrade from 0.22.x, which kept no cursor), the line counts the last day
239
+ (`(in the last day)`), never the whole run log. A cursor from an older 0.24 build, kept per
240
+ conversation, counts for the project its conversation's records are in, so nothing is counted
241
+ twice. A cursor more than five minutes ahead of the clock counts as none, and the next look
242
+ replaces it. A clock moved back can hide records written in the gap until the next look.
243
+
244
+ `/inbox open <n|id>` starts a new conversation from the run that filed the finding: pi copies the
245
+ run's transcript into a new session and switches to it, and the run's own transcript is never
246
+ written. A transcript a new finding points at is kept when older runs are pruned, up to 100 per
247
+ loop; one that is gone anyway is reported as such.
248
+
103
249
  A finding that asks for a decision is a **checkpoint**, and the inbox knows one by its shape:
104
250
  the ` · waits: … · if not: …` clauses every packaged playbook writes
105
251
  ([recipes.md](recipes.md#checkpoints)). The run's text is read once, when its findings are
@@ -108,12 +254,34 @@ lists checkpoints first, marked `⚑`, and says how many there are in its header
108
254
  decision`); the numbers on screen are the numbers `claim` and `dismiss` take, so the order is
109
255
  decided in one place. Claiming a checkpoint tells the turn that the claim is the person's approval
110
256
  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
257
+ `/inbox`, `pi-loops inbox list` and the page read a finding through one parser
258
+ (`src/inbox-card.ts`): a checkpoint's headline is the text before `· waits:`, and its two clauses
259
+ are shown apart as **waiting on** and **if you don't**; a clause the loop left out is not shown.
260
+ The stored line does not change. `pi-loops inbox list --json` prints the stored fields, unsplit.
261
+
262
+ The browser page shows this project's findings as cards above its conversations, decisions first.
263
+ **Yes, do it** (on news, **Look into it**) queues the claim in the conversation that created the
264
+ loop, as the same `inbox:<id>` row `/inbox claim` writes: a terminal that holds that conversation
265
+ answers it under the danger check, a conversation open in the page runs it as your turn, and one
266
+ nobody holds is started. When that conversation is gone, the claim goes to the conversation open
267
+ on the page, if it is in the finding's project. **No…** (on news, **Dismiss…**) takes a reason
268
+ chip or a typed reason of up to 500 characters, which goes to the loop's next run as the terminal's
269
+ reason does; with none it is a silent dismiss. **See the run**, on a finding that names its run,
270
+ shows the run's transcript, read-only. A card names its loop only when the list holds more than one
271
+ loop's findings, and says where a claim runs only when that is not the conversation on screen.
272
+
273
+ Claim and dismiss each succeed once, and only on a finding that is still new: two windows, a
274
+ window and a shell, or two pages that both listed it get one success between them, and the other
275
+ is told the finding was already claimed or dismissed. A claim queues its hand-off under the inbox's
276
+ lock, only once it has found the finding still new, so a claim that loses queues nothing and the
277
+ finding's turn runs once, also while another process holds the lock.
278
+
279
+ Corrupt lines are skipped on read and never deleted: a claim, a dismiss, `/inbox clear` and
280
+ rotation write them back as they were, in their place. The footer shows `Inbox: N new`, with
281
+ `(K decisions)` after it when any are checkpoints (the whole machine, as the badge always has), and `N loops failing (<worst> ×<count>)` once a loop has failed
114
282
  often enough for the scheduler to start backing off; the same clause is on the `[cron] … active
115
283
  here` line at session start, for this project. Until then a job that has failed forty nights in a
116
- row looked exactly like a healthy one. The side panel shows the inbox count too. Entries kept by
284
+ row looked exactly like a healthy one. Entries kept by
117
285
  the checker carry a `✓`.
118
286
 
119
287
  ## Maker/checker (`--verify`)
@@ -126,27 +294,31 @@ and answer with one `<verdict n="i">keep|drop — reason</verdict>` per finding
126
294
  `<rewrite n="i">…</rewrite>`).
127
295
 
128
296
  - Kept findings enter the inbox marked verified; the checker's reason travels with the claim.
129
- - Dropped findings and their reasons go to the run log and the run card; `/cron trace <job> <k> checker` shows the checker's transcript.
297
+ - Dropped findings and their reasons go to the run log; `/cron trace <job> <k> checker` shows the checker's transcript.
130
298
  - Findings without a verdict enter unverified.
131
299
  - **Fail-open:** if the checker fails or times out, every finding enters the inbox marked unverified. A broken checker must not silence the loop.
132
300
  - The checker never touches the state spine.
133
301
 
134
302
  ## Restart behavior
135
303
 
136
- Jobs live in `~/.pi/agent/loops/jobs.json` and survive pi restarts. Exactly one pi process on the
137
- machine owns the timer (`scheduler.json`, 30-second ticks, 90-second heartbeat); when it exits
138
- or dies another open pi takes over on its next tick. A tick that was missed while no pi was running
139
- is fired once at startup (collapsed, not replayed) for stateful loops unless `[cron] catch_up = false`;
140
- plain inject jobs do not catch up unless created with `--catchup` (`--no-catchup` turns it off for
141
- loops). A run that died with its process is retried on the next tick; at most
304
+ Jobs live in `~/.pi/agent/loops/jobs.json` and survive pi restarts. Stateful loops run in one
305
+ always-on process ([pi-loops in the background](#pi-loops-in-the-background)), which ticks every 30 seconds. A loop tick that was
306
+ missed while pi-loops was not running is fired once when it starts (collapsed, not replayed) unless
307
+ `[cron] catch_up = false`; a loop that does not catch up records `missed <time> while pi-loops was
308
+ not running (catch-up disabled)`. Plain inject jobs do not catch up unless created with `--catchup`
309
+ (`--no-catchup` turns it off for loops), and a missed one records `missed <time> while no pi was
310
+ running (catch-up disabled)` in a window, or `while pi-loops was not running` in the background. A run that died with its process is retried on the next tick; at most
142
311
  `[cron] max_concurrent_runs` (3) run at once. Trigger checks share `[cron] max_concurrent_runs`, so
143
312
  a server pushing many distinct events cannot open one sub-agent per event.
144
313
  Jobs run with the model and thinking level recorded
145
- on them (`/cron set <id> --model … --thinking … --timeout …`, `-` to follow the running session).
146
- `/cron scheduler` shows who owns the timer; `/cron` marks a plain job as `[session <id> — resume it
147
- to run]` when its session is not open here, parks it as disabled once that session's file has been
148
- deleted (`/cron gc` removes it), and marks a loop `[orphan: cwd missing]` when their checkout is gone — disabled half an hour later, since a
149
- mount can be late at boot. A job created by a sub-agent belongs to the session that ran it.
314
+ on them (`/cron set <id> --model … --thinking … --timeout …`; `-` clears one: a loop then uses pi's
315
+ default, a plain job its conversation's).
316
+ `/cron scheduler` shows this window (its pid, and whether its conversation's plain jobs run here),
317
+ pi-loops (pid, version, start time), the store and the log. `/cron` marks another conversation's plain
318
+ job `open in a terminal`, `in the page`, `in the background`, `paused: <why>`, or `resume it to run` (pi-loops never saw its file), parks it as disabled once that session's file has been
319
+ deleted (`/cron gc` removes it), and marks a loop `[orphan: cwd missing]` when its checkout is gone
320
+ — disabled half an hour later, since a mount can be late at boot. A job created by a sub-agent
321
+ belongs to the session that ran it.
150
322
 
151
323
  pi-loops runs on one machine. Two machines syncing one `$HOME` would both run every job; that is
152
324
  not supported (until 0.19.0 a hostname stamp on each job half-handled it, and is now ignored). To
@@ -248,7 +420,10 @@ audit like any other.
248
420
  an `every <dur>` job's interval restarts from now, and a `cron` job's next run is unchanged — the
249
421
  expression says when it runs, not the last run. A disabled plain job is refused rather than run: its
250
422
  prompt would land in whichever chat is open now rather than the one it was written for, and a job the
251
- dead-session sweep parked has to keep the marker `/cron gc` collects it by.
423
+ dead-session sweep parked has to keep the marker `/cron gc` collects it by. A plain job's `/cron run`
424
+ is your own turn: it runs uncapped and goes before anything the clock queued. Left queued when you
425
+ quit, it waits for a window, and the job's
426
+ schedule goes on in the background as if it were not there.
252
427
 
253
428
  A project is matched by realpath and containment, so a pi opened in a subdirectory, a worktree or
254
429
  through a symlink sees and runs that project's automation.
@@ -295,8 +470,9 @@ not finish, so its `<loop-state>` and `<inbox>` tags are not trusted.
295
470
  ## Stopping, removing, and jobs that keep failing
296
471
 
297
472
  `/cron disable --all` pauses every job in this project (`--all-projects` for the machine) and
298
- `/cron enable --all` resumes them — quitting pi hands the clock to the host rather than stopping
299
- anything, so this is how you actually go quiet.
473
+ `/cron enable --all` resumes them. Closing every pi window stops nothing, because pi-loops keeps
474
+ running, so this is how you actually go quiet; `/pi-loops stop` pauses every loop and check on the
475
+ machine until `/pi-loops start`.
300
476
 
301
477
  If a loop is stuck showing `running` after a process was killed and its pid reused, `/cron clear
302
478
  <ref>` releases the marker (it asks first).
@@ -310,6 +486,14 @@ this project is named with the project it was in.
310
486
  `/cron set` instead); `--purge` deletes them. `/cron gc` collects this project's jobs whose session
311
487
  is gone — `--all` every project's, `--purge` also the loop state and transcripts left behind by jobs that are gone.
312
488
 
489
+ A loop run you stop (`pi-loops abort <run-id>`, `/triggers abort <run-id>`, or the page) ends with a
490
+ run log record (`runs.jsonl`) that says `aborted` and `stoppedBy: "person"`. The
491
+ slot it was running is spent: the loop runs again at its next scheduled time, not on the next
492
+ tick. A stop is not a failure, so the run does not count toward `runs` or the failure streak. A
493
+ one-shot loop you stop is turned off with `last error: stopped by a person; /cron run <ref> runs it
494
+ now`, and keeps its notes. A run stopped by quitting, an upgrade or the daily budget gives its slot
495
+ back and runs again on the next tick, as before.
496
+
313
497
  A job that fails three times in a row is retried on a widening gap (5 minutes, doubling, up to six
314
498
  hours) instead of at every due tick, and says so; one success clears the streak.
315
499
 
@@ -320,8 +504,8 @@ of that says whether anyone wanted what it found. So `/cron` puts two more facts
320
504
  both computed from the run log and the inbox and nothing else:
321
505
 
322
506
  ```text
323
- 3. cron-b91def87 "pr-watch" enabled */15 * * * * [stateful] [quiet ×12]
324
- next 2026-09-15 10:15 · runs 412 · 30d: 6 findings · 6 dismissed (4 with a reason) · ~/code/acme-api
507
+ 3. cron-b91def87c2a04e5f9d3b6a7e1c0f4d28 "pr-watch" */15 * * * * [stateful] [quiet ×12]
508
+ next 2026-09-15 10:15 · runs 412 · 30d: 6 findings · 6 dismissed (4 with a reason)
325
509
  ```
326
510
 
327
511
  `30d: 6 findings · 6 dismissed` is the last thirty days: findings filed, and of those still in the
@@ -337,39 +521,84 @@ watch could be hourly (`/cron set <ref> --schedule "0 * * * *"`), or that the lo
337
521
  the wrong thing. `cron_list` gives the model both facts (`signal_30d`, `quiet_streak`), so asking
338
522
  "which of my loops are worth keeping" gets an answer from the numbers rather than the names.
339
523
 
524
+ The page's loop row shows the same numbers, only those that are not zero (`6 findings in 30 days · 6
525
+ dismissed · quiet ×12`), and at most one suggestion, which never applies itself:
526
+
527
+ - **Run hourly instead**, when the quiet streak is at least 12 and the loop runs more often than
528
+ hourly. An `every` loop becomes `every 1h`; a cron loop becomes `0 * * * *`. A cron loop's
529
+ clock restarts at the change, as `/cron set --schedule`'s does; an `every` loop's next run is
530
+ counted from its last run, so one that last ran more than an hour ago runs at the next tick. The
531
+ row says the new schedule and the next run, in the browser's local time.
532
+ - **Pause**, when the last thirty days have at least 5 findings and 5 dismissed, and none claimed.
533
+
534
+ A disabled loop, a plain job, a one-shot or a running loop gets none. Applying one changes the loop
535
+ and writes a `cron_control_plane` audit row (`set` for the schedule, `disable` for Pause, source
536
+ `page`). `/cron set --schedule` in a terminal writes the same `set` row (source `slash`) in
537
+ `triggers-audit.jsonl`; an edit that leaves the schedule alone writes none. **Keep** leaves the loop as it is and
538
+ records the answer on the job (`tuneKept`), so the same suggestion is not offered again. The
539
+ suggestion is named with the schedule it was made for, so a loop whose schedule changes can get a
540
+ new one.
541
+
542
+ A running loop's row has **Stop this run**, with a confirm in the row; see "Stopping, removing, and
543
+ jobs that keep failing" for what a stop does to the loop.
544
+
340
545
  ## When something looks wrong
341
546
 
342
547
  Every diagnostic a pi process produces is written to `logs/pi-<pid>.log` in the loops directory —
343
- jobs that were disabled, writes that failed, sub-agent warnings — so a loop that failed at 03:00
344
- still has something to read at 09:00. `/cron scheduler` prints the path. The headless host writes
345
- `host.log` the same way; both are rotated.
548
+ jobs that were disabled, writes that failed, sub-agent warnings. `/cron scheduler` prints the path.
549
+ pi-loops writes `host.log`; both logs are rotated.
346
550
 
347
551
  `PI_LOOPS_DEBUG=1` adds a line per tool call, provider retry and compaction to that log, which is
348
552
  the quickest way to tell a stuck run from a busy one.
349
553
 
350
554
  `/cron snapshot` writes a `pi_loops_snapshot` entry into the session: which MCP servers connected
351
- and what they exposed, the active tools, the hooks, whether this pi owns the clock, the last
352
- check. One is written automatically whenever that state changes. The TUI panel shows the same
353
- thing; the entry is for a front end that is not a terminal — the browser one in
555
+ and what they exposed, the active tools, the hooks, whether pi-loops is running and its pid, the last
556
+ check. One is written automatically whenever that state changes. The TUI panel is not the same:
557
+ it lists rules, loops, the latest runs, MCP servers and hooks; it shows no pid, and shows the last
558
+ check only when it went wrong. The entry is for a front end that is not a terminal — the browser one in
354
559
  [src/web.mjs](../src/web.mjs) reads it, and so can anything else that speaks to a session.
355
560
 
356
561
  `/triggers running` shows how long each run has been going and, for loop runs, the transcript that
357
562
  is being written right now (`pi --session <file>`), so "is it stuck or is it working" is answerable
358
- before the run ends. A run gets the tools the
359
- session that owns the clock has active (a job's `--tools` narrows that, never widens it), plus its
360
- own project's MCP servers, and the automation tools it calls act in its own project.
563
+ before the run ends. A loop's run in the background gets pi's default tools, or exactly the ones its
564
+ `--tools` names, plus its own project's MCP servers, and the automation tools it calls act in its
565
+ own project.
361
566
 
362
567
  An unattended run refuses a corpus of dangerous commands: sudo, `curl … | sh`, writing to a block
363
- device, `mkfs`, `chmod 777 /`, shutdown/reboot, `git push --force` on main/master, pipes into
364
- `eval`, the fork bomb, and `rm -r -f` aimed at `/`, an absolute path or `$HOME`. The model is told
365
- why and can do the safe part.
366
-
367
- When the last pi on the machine quits, it hands the clock to a headless host (`/cron host`): a
368
- small `node` process with the same stores and runner that keeps loops, trigger checks and MCP
369
- pushes going — findings still reach the inbox — until the next pi opens and takes the clock back.
370
- It is started only when there is work for it: an enabled loop or rule for this machine, or an MCP
371
- server whose pushes inject or have rules to match. `pi-loops host status` shows what it is doing
372
- while it runs, and `pi-loops host abort <id>` interrupts one run (see [cli.md](cli.md)). `[host] auto = false` turns that off;
373
- `/cron host start` hands off on quit anyway (this pi only), `/cron host stop` ends a running host
374
- and cancels the hand-off. `host.log` shows what it did; a host that died is reported by the next
375
- pi to open. After a reboot nothing runs until a pi opens (which hands off again when it quits).
568
+ device, `mkfs`, `chmod 777 /`, shutdown/reboot/halt as the command (also through `systemctl`; as an
569
+ argument, `git log --grep=reboot`, they are words), `git push --force` on main/master, pipes into
570
+ `eval`, the fork bomb, `rm -r -f` aimed at `/`, an absolute path or `$HOME`, and `pi-loops abort`,
571
+ `stop` or `restart` as a command (after `env`, `npx`, a path or `sh -c` too, and with a verb the shell
572
+ expands, `pi-loops $V`; `git add -A && git commit -m "pi-loops: stop retrying"` only mentions it). The model is told
573
+ why and can do the safe part. A plain job's turn in an open window meets the same corpus, and asks
574
+ you instead of refusing outright (see "Plain cron jobs" above).
575
+
576
+ ## pi-loops in the background
577
+
578
+ A pi window starts the always-on `node` process on session start and each 30-second tick, and just after
579
+ a loop or rule is added, when an enabled loop, enabled rule or relevant MCP push server exists, or a closed conversation
580
+ has a plain job or a queued rule result. pi-loops runs stateful loops, dynamic-rule checks, MCP
581
+ pushes, and the plain jobs of conversations no window holds. A window runs the plain jobs of the
582
+ conversation it holds. pi-loops does not exit when windows open or close. `/pi-loops status` shows
583
+ its pid, version, start time, work, held conversations and log. `/pi-loops stop` records a manual
584
+ stop; windows do not restart it until `/pi-loops start`, or until `pi-loops` opens the page and
585
+ says that loops and rules resume. `[host] auto = false` makes pi-loops run
586
+ only while a pi window is open, as 0.22.8 did: the first window starts it, it leaves once the last
587
+ window has closed, and nothing runs with none open ([configuration.md](configuration.md#host-auto));
588
+ `PI_LOOPS_HOST=off` keeps one process from starting it. While a stop or `PI_LOOPS_HOST=off` keeps pi-loops from running,
589
+ nothing it would run runs, and every surface says so instead of promising a schedule: the status line
590
+ reads `loops paused: <why>`, `/cron` marks each loop (and each plain job of a closed conversation)
591
+ `paused: <why>` with no next run, and the session-start line says this project's loops are paused. When pi-loops cannot start, it is retried after 1, 5
592
+ and 30 minutes, with the reason in `/pi-loops status` and `host.log`. If pi-loops crashes, it is restarted by
593
+ the next window tick, and one that crashes three times within five minutes is paused (`loops paused:
594
+ pi-loops keeps crashing`) for half an hour, then tried again on its own; each pause that follows the
595
+ last one at once is twice as long, a day at most, and `/pi-loops start` ends a pause at once. The error
596
+ is the first thing `host.log` says about each crash. A loop whose runs keep stopping with pi-loops is
597
+ parked after three such runs, each the only run in flight when pi-loops stopped, with the reason in
598
+ `/cron`; `/cron enable <id>` tries again. A window replaces pi-loops after three failed control-socket
599
+ probes only when pi-loops' heartbeat has stopped as well (SIGTERM, then SIGKILL after 10 seconds); a
600
+ pi-loops process that is still ticking but does not answer on its socket is left running and reported as not
601
+ answering, and a starting pi-loops process is not killed. pi-loops needs node: under bun it uses the `node` on
602
+ your PATH.
603
+ After a reboot, nothing runs until a pi opens. What pi-loops is doing is the Background view of the browser page, at `http://127.0.0.1:4173/background`;
604
+ `pi-loops status --url` prints the page's address with the token ([cli.md](cli.md#the-background-view)).
package/docs/mcp.md CHANGED
@@ -6,7 +6,9 @@ notifications and registers the servers' tools with pi.
6
6
  ## Configuration
7
7
 
8
8
  `~/.pi/agent/loops/mcp.toml`, plus `<project>/.pi/mcp.toml` for trusted projects (same server
9
- name → project wins). The schema:
9
+ name → project wins). A project that has nothing of pi's own to protect is one pi never asks about,
10
+ so there `.pi/mcp.toml` loads only once `/trust` has saved trust for it (pi applies that at its
11
+ next start) or pi was started with `--approve`; until then the window says it ignored the file. The schema:
10
12
 
11
13
  ```toml
12
14
  [[server]]
@@ -40,8 +42,11 @@ need `_meta.pi_dedup_key` or are dropped at the source and counted. Summaries co
40
42
  method name plus bounded, redacted metadata (`notifications/resources/updated uri=…`,
41
43
  `_meta.pi_summary` capped at 200 chars) — never raw params.
42
44
 
43
- Delivery per server: `inject_summary` (promotion only), `inject_and_run` (user message + one turn),
44
- or the default: evaluation against the dynamic rules by a sub-agent.
45
+ Delivery per server: `inject_summary` (promotion only), `inject_and_run` (a user message and one turn, queued as a request and run once the agent is idle),
46
+ or the default: evaluation against the dynamic rules by a sub-agent. An `inject_and_run` turn is a
47
+ scheduled turn, as a plain job's is: in an open window a command the dangerous-command policy
48
+ refuses asks you first, with a 60-second countdown, and in the background it is refused
49
+ ([loops.md](loops.md), [configuration.md](configuration.md)).
45
50
 
46
51
  ## Tools
47
52
 
@@ -68,21 +73,24 @@ exemption and are scanned like any other command. The commands reaching this gat
68
73
  model that may have read something hostile, so an entry has to mean one command and not a foothold;
69
74
  a run that needs two shapes of a command lists both.
70
75
 
71
- A sub-agent shares this process's live clients rather than opening its own, so a browser tab or
72
- database session opened in the chat is the one the loop sees. When a run's project is not this
73
- process's own, that project's `.pi/mcp.toml` servers are connected on demand and lent to the run
74
- (only if the user has trusted that project), so a loop is not at the mercy of which window owns
75
- the clock; the headless host does the same. Notifications are consumed by interactive processes only; a
76
- sub-agent ignores what its own connection pushes: sub-agents register no notification
77
- hooks. A push that injects into the chat (`inject_summary` / `inject_and_run`) reaches every window
78
- that has the server; a push evaluated against dynamic rules is
79
- evaluated once per project, by the pi that owns that project's checks.
76
+ A sub-agent shares the live clients of the process that runs it rather than opening its own. Loops
77
+ and rule checks run in the background, so a loop sees pi-loops' clients from the user-level `mcp.toml`,
78
+ not a browser tab or database session opened in a chat. A run's own project's `.pi/mcp.toml`
79
+ servers are connected on demand and lent to the run (only if the user has trusted that project).
80
+ A sub-agent ignores what its own connection pushes: sub-agents register no notification hooks. A
81
+ push that injects into the chat (`inject_summary` / `inject_and_run`) reaches every window that
82
+ holds a conversation and has the server; pi-loops files it in the inbox only when no window lists
83
+ that server on its lease. A push evaluated against dynamic rules is evaluated once per project, by
84
+ pi-loops. That includes a project's own `.pi/mcp.toml` servers: pi-loops keeps the push servers of
85
+ every trusted project that has enabled rules connected (the first eight such projects), and a push
86
+ from one is checked against that project's rules only. A project whose trust is revoked, or whose
87
+ last rule is removed or disabled, has its servers stopped within a minute.
80
88
 
81
89
  A repeated `[[server]]` name replaces the earlier entry (a diagnostic says so); the project file is
82
90
  `<project>/.pi/mcp.toml`.
83
91
  A machine-wide dedup window (`~/.pi/agent/loops/dedup.json`, 5 minutes) makes each push count once
84
- no matter how many pi windows are open, and results are promoted only into a chat that belongs to
85
- the rule's project (otherwise they go to the inbox). Crashed stdio servers are reconnected with
92
+ no matter how many pi windows are open, and a rule's result goes to the conversation that created
93
+ it (see [triggers.md](triggers.md)). Crashed stdio servers are reconnected with
86
94
  exponential backoff, 20 attempts by default, each distinct error reported once (rather than marking them
87
95
  disconnected). `/triggers sources` lists MCP servers first, then the cron hook, then the dynamic
88
96
  checker, in registration order; a stdio server's last stderr line is shown as `stderr:`
package/docs/recipes.md CHANGED
@@ -84,6 +84,11 @@ answered. `/recipe list` shows them in those two groups.
84
84
  `/recipe list` and `/recipe remove` can find them. Each prompt is a pointer:
85
85
  `Read .agents/skills/issue-loop/triage.md and do what it says for this repository.` The
86
86
  procedure is read fresh on every run; edit the file and the next run follows the edit.
87
+ The jobs belong to the conversation that ran `/recipe add`, which is told of their findings. A
88
+ conversation pi has not saved yet cannot own one: pi writes a conversation's file only once it
89
+ holds a message, and `/recipe add` is none (a starter's **Add** in a new page conversation is the
90
+ usual case). Then the jobs belong to the project, the install says so, and a finding runs in the
91
+ conversation it is claimed from; no conversation counts it as its own.
87
92
 
88
93
  `pi-loops recipe list|show|add` in a shell does step 6 and nothing after: the setup script and the
89
94
  jobs are things a person should see before they exist, and only the pi inside `/recipe add` can show
@@ -154,6 +159,7 @@ and should be read before it is installed).
154
159
  ```toml
155
160
  name = "issue-loop" # lowercase, digits, dashes; also the install directory
156
161
  summary = "one line"
162
+ line = "at most twelve words" # optional; the page's starter card and both recipe lists show it whole, else the summary
157
163
  tier = "advanced" # optional; "starter" reads and files findings, "advanced" (the default) writes somewhere
158
164
  useful_when = ["one sentence"] # optional; the situations it is for, printed by `show`
159
165
  needs_tracker = true # gate on docs/agents/issue-tracker.md
Binary file