@alphacoder-v0/pi-loops 0.22.7 → 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.
- package/CHANGELOG.md +681 -0
- package/CONTRACT.md +7 -7
- package/README.md +91 -63
- package/README.zh-CN.md +59 -39
- package/docs/cli.md +272 -83
- package/docs/configuration.md +86 -24
- package/docs/design.md +395 -56
- package/docs/downstream.md +2 -2
- package/docs/goal.md +12 -3
- package/docs/hooks.md +6 -7
- package/docs/loops.md +288 -59
- package/docs/mcp.md +22 -14
- package/docs/recipes.md +6 -0
- package/docs/screenshot.png +0 -0
- package/docs/triggers.md +120 -26
- package/docs/troubleshooting.md +37 -26
- package/docs/web-ui-parity.md +691 -65
- package/package.json +4 -3
- package/recipes/autoresearch/recipe.toml +1 -0
- package/recipes/changelog-draft/recipe.toml +1 -0
- package/recipes/ci-sweeper/recipe.toml +1 -0
- package/recipes/daily-digest/recipe.toml +1 -0
- package/recipes/deps-sweeper/recipe.toml +1 -0
- package/recipes/ecosystem/recipe.toml +1 -0
- package/recipes/issue-loop/recipe.toml +1 -0
- package/recipes/pr-watch/recipe.toml +1 -0
- package/skills/pi-loops/SKILL.md +6 -4
- package/src/activity.ts +199 -0
- package/src/archive.ts +21 -9
- package/src/args.ts +6 -2
- package/src/cli-entry.mjs +12 -3
- package/src/cli.ts +746 -125
- package/src/config.ts +53 -5
- package/src/conversation-rows.ts +469 -0
- package/src/danger.ts +227 -6
- package/src/engine.ts +1240 -0
- package/src/goal.ts +71 -0
- package/src/held-read.ts +27 -0
- package/src/hooks.ts +49 -18
- package/src/host-control-channel.ts +455 -40
- package/src/host-control.ts +602 -111
- package/src/host-entry.mjs +5 -3
- package/src/host-runtime.ts +455 -41
- package/src/host.ts +659 -123
- package/src/inbox-card.ts +278 -0
- package/src/inbox.ts +139 -62
- package/src/job-edit.ts +7 -1
- package/src/job-health.ts +14 -2
- package/src/job-lines.ts +201 -13
- package/src/job-owner.ts +74 -13
- package/src/job-tune.ts +110 -0
- package/src/lease.ts +537 -0
- package/src/lock.ts +7 -2
- package/src/mcp-pool.ts +66 -14
- package/src/mcp.ts +1 -1
- package/src/open-file.ts +54 -0
- package/src/page-children.ts +1507 -0
- package/src/page-devices.ts +192 -0
- package/src/page-server.ts +3150 -0
- package/src/path-precheck.ts +215 -0
- package/src/paths.ts +24 -0
- package/src/pi-floor.ts +10 -0
- package/src/pi-loops.ts +1301 -450
- package/src/pi-resolver.d.mts +1 -0
- package/src/pi-resolver.mjs +110 -10
- package/src/presence.ts +12 -63
- package/src/protocol.ts +10 -1
- package/src/recipe.ts +43 -1
- package/src/requests.ts +354 -0
- package/src/rule-lines.ts +68 -0
- package/src/run-card.ts +105 -0
- package/src/runner.ts +18 -1
- package/src/schedule.ts +1 -2
- package/src/scheduler.ts +598 -245
- package/src/sdk-runner.ts +321 -27
- package/src/seen.ts +278 -0
- package/src/session-head.ts +21 -0
- package/src/share.ts +12 -2
- package/src/snapshot.ts +3 -2
- package/src/store.ts +81 -8
- package/src/tools.ts +196 -65
- package/src/trigger-runtime.ts +164 -127
- package/src/triggers.ts +39 -7
- package/src/trust.ts +46 -2
- package/src/ui-prefs.ts +16 -5
- package/src/watch-plan.ts +174 -0
- package/src/web.d.mts +3 -0
- package/src/web.mjs +2260 -2340
- package/src/window-executor.ts +459 -0
- package/src/window-guard.ts +158 -0
- 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.
|
|
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
|
-
|
|
80
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
35
|
-
|
|
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)
|
|
63
|
-
run log with exit code, duration, cost and finding counts (`/cron runs`)
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
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.
|
|
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
|
|
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.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
299
|
-
|
|
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-
|
|
324
|
-
next 2026-09-15 10:15 · runs 412 · 30d: 6 findings · 6 dismissed (4 with a reason)
|
|
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
|
|
344
|
-
|
|
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
|
|
352
|
-
check. One is written automatically whenever that state changes. The TUI panel
|
|
353
|
-
|
|
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
|
|
359
|
-
|
|
360
|
-
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
|
|
364
|
-
`
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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).
|
|
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
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
that
|
|
79
|
-
|
|
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
|
|
85
|
-
|
|
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
|
package/docs/screenshot.png
CHANGED
|
Binary file
|