@wassname2/pi-supervise 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,264 @@
1
+ # pi-intercom-supervisor
2
+
3
+ Original ask:
4
+ > https://github.com/monotykamary/pi-supervisor but with pi-intercom and linked to matrix or telegram
5
+ > - wassname
6
+
7
+ [monotykamary/pi-supervisor](https://github.com/monotykamary/pi-supervisor) runs the supervisor as a
8
+ hidden in-memory session. This runs it as a second real pi session and uses
9
+ [pi-intercom](https://github.com/tintinweb/pi-intercom) as the wire between the two. You can see the
10
+ supervisor, talk to it, and bridge it to your phone with any chat extension, because it is just a pi
11
+ session. That swap deleted the context pipeline, the four reframe tiers, the JSON verdict parser,
12
+ the widget and the plugin API: 5201 lines of `src` down to 1712, tests excluded on both sides.
13
+
14
+ ```
15
+ pi install https://github.com/wassname/pi-intercom-supervisor # needs npm:pi-intercom too
16
+
17
+ # terminal 1, the worker # terminal 2, the supervisor
18
+ pi pi
19
+ /supervise make the results table
20
+ > do the work
21
+ ```
22
+
23
+ You type one command, in one session. That session becomes the supervisor and its target becomes
24
+ the worker, so the role is decided at pairing time and neither session is one until then. The
25
+ worker needs no command, only the extension loaded, and it prints `supervised by <id>: <goal>` so
26
+ you can see which one was picked.
27
+
28
+ `/supervise` takes the only free session in this directory, so the whole line is the goal. With
29
+ more than one it shows a picker, since guessing is worse than asking. Run `/name worker`
30
+ in the one you want and then `/supervise @worker make the table`, or use `@` and the id it printed,
31
+ which is the start of the `pi --session <id>` line in that terminal. The `@` is what keeps a goal
32
+ with spaces in it out of the target: a first word that names no session is refused, where it used
33
+ to be folded into the goal without a word. `/supervise stop` ends it.
34
+
35
+ A goal that is one word with a slash or a dot in it is a path, and the file is the goal:
36
+ `/supervise docs/GOAL.md`, or `/supervise goal docs/GOAL.md` to change it later. A goal worth
37
+ grading against runs to paragraphs of acceptance evidence, and retyping it each run is how the copy
38
+ you steer by drifts from the copy you grade by. A missing file stops the pairing and names the path.
39
+
40
+ `/supervise goal <new goal>` changes the goal in place. The worker hears it, a fresh view follows,
41
+ and the supervisor keeps its memory of the steers it has already sent, which stopping and pairing
42
+ again would throw away.
43
+
44
+ A supervisor that comes back from a crash, a credit failure or a `/reload` says the goal and the
45
+ answer shape again, then asks the worker for a view, so restarting the session is the whole
46
+ recovery. Only the worker makes views, and a supervisor holding a stale one answers from the stale
47
+ one, which is how it invents a fact. `/supervise look` does the same by hand if you want it now.
48
+ The repeat matters because `/reload` is how a changed prompt reaches a running session, and the
49
+ full brief goes out only at pairing.
50
+
51
+ A live pairing puts two words in the footer, `👁 watching 3` in the supervisor and `👁 watched` in
52
+ the worker, where 3 is the instruction count. The notice printed at pairing scrolls away, and after
53
+ that a paired session looks like any other prompt, which is how you end up staring at a supervisor
54
+ that stopped supervising an hour ago. It stays that short because `pi-powerline-footer` appends it
55
+ to its own line for the whole session, unlike a status that shows only during a run. Same mechanism as
56
+ [@diegopetrucci/pi-oracle](https://www.npmjs.com/package/@diegopetrucci/pi-oracle) uses for its
57
+ runs.
58
+
59
+ With no target named, `/supervise` asks rather than guesses: it broadcasts a roll call and waits
60
+ half a second. A session knows things about itself that nothing outside it can see, so each one
61
+ answers for itself. A `pi-subagents` child run stays quiet because it reads `PI_SUBAGENT_CHILD` in
62
+ its own environment, a session already paired stays quiet because it is taken, and a registration
63
+ whose process has gone cannot answer at all.
64
+
65
+ One session answering is the ordinary case and it pairs with no question asked. Otherwise you get a
66
+ picker, and every session in the directory is on it, including the quiet ones, marked. Excluding
67
+ them outright reads as `0 free sessions` and leaves you nowhere, which is what happened the first
68
+ time this shipped: the worker was running an older copy of this extension and could not answer a
69
+ roll call it had never heard of.
70
+
71
+ Guessing from the outside is what this replaces, and every version of it was wrong. The name is no
72
+ test: `pi-subagents` sets an id starting `subagent` only when it passes an intercom target, and
73
+ `pi-intercom` calls any unnamed session `subagent-chat-<id>`. The process tree is no test either: a
74
+ child pi is often started through an intermediate `node` process (`pi-subagents`
75
+ `async-execution.ts:459`), which breaks the pi-to-pi parent chain the check looked for. That left
76
+ five child runs offered as workers in one directory.
77
+
78
+ Naming a target with `@` skips the roll call, because you named it. The pair acknowledgement is
79
+ then the test of whether it can take the job.
80
+
81
+ Working on this extension: `pi install /path/to/pi-supervise` points pi at the working tree, so
82
+ `/reload` picks up an edit with no commit and no push.
83
+
84
+ ![worker and supervisor side by side: the steer lands in the worker's transcript while the supervisor waits for the next view](media/screenshot.png)
85
+
86
+ The supervisor tools are hidden in a session that is not supervising. A worker that can see
87
+ `steer` and `worker_view` starts guessing: one here, given an ordinary coding task, spent twelve
88
+ turns reasoning "these are supervisor tools ... so I might be the supervisor", called `worker_view`,
89
+ and took "the worker has not stopped since pairing" as proof of a pairing it never had.
90
+
91
+ Both sessions load this extension and pick their role at runtime. The worker runs as ordinary pi.
92
+ When it stops, the supervisor gets a view of it and calls `steer` with one concrete next action, or
93
+ `done`. If it needs a human it replies in plain text instead of calling a tool, and that is what
94
+ reaches your phone. Supervising takes the writing tools off that session, `bash` and the edit tools,
95
+ and gives them back when supervision ends: it shares a working directory with the worker, and two
96
+ agents writing the same files is not supervision. It keeps `read` and `grep`, because checking a
97
+ claim against a file is the job. Policy comes from `<cwd>/.pi/SUPERVISOR.md`, then `<agent dir>/SUPERVISOR.md`,
98
+ then a built-in default, same precedence as the original, so an existing file keeps working. A
99
+ verdict here is a tool call, so nothing can fail to parse.
100
+
101
+ ## How it works
102
+
103
+ The pi-intercom extension channel carries the data. It never enters a transcript and never starts a
104
+ turn, so each side triggers its own turn locally with `pi.sendUserMessage`, and the steer lands in
105
+ the worker's user and assistant trajectory where it belongs. Pairing is on `fromSessionId`, which
106
+ the broker stamps from its own registry, so a payload cannot forge it. The broker has no socket
107
+ authentication, so the trust level is any process running as you, and every session that loads this
108
+ extension sees every message. Do not load it in a session you would not trust with the transcript.
109
+
110
+ Each view carries only the turns since the last one, the way you read the new lines on a screen
111
+ rather than the scrollback. The supervisor is a real session that keeps every view it has read, so
112
+ re-sending the whole transcript would put a second copy of its own context in front of it, and the
113
+ cost would grow with every review. A one-line goal repeats in full. A multi-line goal shows its
114
+ first line with `[...]`; its complete rubric enters the supervisor context at pairing, goal change,
115
+ reload, supervisor compaction, and before every fifth review. A worker compaction restarts the
116
+ view's turn count, and the view says so and carries the summary.
117
+
118
+ The view body is [pi-vcc](https://github.com/sting8k/pi-vcc)'s `compile()`, the same algorithmic
119
+ compactor (no LLM calls) you can run as your own. On top of it the view carries what a compactor
120
+ has no reason to track: tool calls with no result, child pi processes, and whether anything
121
+ changed since the last review.
122
+
123
+ pi-vcc drops the worker's reasoning, which you do see on screen, so this keeps the last two blocks
124
+ by rewriting them as text before compiling. They stay where they happened, next to the tool call
125
+ each one produced, because that is the order you read a session in. A worker going in circles says
126
+ so there first, while it names the approach it is about to retry, before any file or commit
127
+ changes. Two, cut to the last 400 characters each: one worker session here held 161 reasoning
128
+ blocks, and all of them together would be a second transcript.
129
+
130
+ The complete goal is never cut when it enters the supervisor context. A multi-line goal uses only
131
+ its first line plus `[...]` in ordinary views; that is a locator, while the complete rubric arrives
132
+ at the fixed review cadence. The byte limit trims the transcript instead.
133
+
134
+ Every view names the worker's model and how full its context is, read off the worker's own intercom
135
+ presence record. Steering a small fast model wants smaller steps than steering a frontier one, and
136
+ a worker near the top of its context is about to compact and lose detail.
137
+
138
+ Resume and `/reload` both restore the goal and the round count out of the transcript, which is
139
+ where they belong. The paired session ID is not restored that way, because it addresses a live
140
+ process that may have exited while you were away. Each side asks the broker who is actually
141
+ connected and then says which it is: still supervising, or dropped because the other session is
142
+ gone. A resumed supervisor also has its writing tools taken off again, which the `/supervise`
143
+ handler alone would not do.
144
+
145
+ The worker sends a view when it is paired, again whenever it stops, and every half hour in between.
146
+ The pairing one exists because a worker paired while it sits at the prompt never settles, so waiting
147
+ for its first stop can mean waiting for ever.
148
+
149
+ Only the view starts a supervisor turn. The brief, the reanchor after a reload and a goal change
150
+ all say "a view follows", so none of them is a thing to judge, and each goes in as a custom message
151
+ with `triggerTurn: false` (`pi-coding-agent/core/agent-session.d.ts:398`). They are in the context
152
+ and they start nothing. As a user message the brief was a turn, and a supervisor holding three
153
+ verdict tools and no view still answers it: 98 `let_it_run` calls in one turn, once every two
154
+ seconds, each over "waiting for the first view". No wording fixed that, because the turn should not
155
+ have existed.
156
+
157
+ Every verdict result has to name a way to end the turn. A turn ends when the assistant writes text
158
+ and calls no tool, so a result that does not say so leaves another tool call as the only move, and
159
+ one that forbids text ("Say nothing more until the next view arrives") forbids the exit outright.
160
+ That cost a spare verdict on every look of an overnight run: a true reason ("job 23 progressing
161
+ normally, 438/600 steps"), then a second true reason two seconds later. A second `let_it_run` in one
162
+ look is answered rather than cut, saying the verdict is already recorded and naming the way out. It
163
+ is not an error, because nothing happened: `let_it_run` reaches nobody.
164
+
165
+ A runaway is still cut with pi's own `ctx.abort()`, past five verdicts in one look, which no
166
+ sign-off explains and one run reached 645 of. Words in a tool result cannot stop a loop,
167
+ because the loop is what reads them. The result is still recorded, since the agent loop pushes tool
168
+ results before it streams the next assistant message (`pi-agent-core/agent-loop.js:124-129`). "One
169
+ look" means one view: the count resets when a view arrives as well as on `agent_start`, because a
170
+ view that lands while the supervisor is busy is queued as a follow up, and a follow up runs inside
171
+ the agent loop already going, so `agent_start` does not fire again.
172
+
173
+ A stop and a check in ask for different things. A stop is a decision point. A check in leans on `let_it_run`,
174
+ because interrupting a working agent costs it its train of thought. The original also ran two
175
+ mechanical checks mid-turn, five tool errors in a row and five reads of one file with no edit.
176
+ Those are deleted. A failed tool result appears once in the fresh VCC turns, rather than becoming
177
+ persistent state. Ported and kept: the process tree check (`src/subagents.ts`, `ps` for child pi
178
+ processes, so a settled worker with a subagent still running is not called finished) and the
179
+ SUPERVISOR.md precedence.
180
+
181
+ `src/prompts.ts` holds every word the supervisor reads, in the order it reads them. Only the nudge
182
+ and the view repeat per look; the policy and verdict rules are sent once. A multi-line goal returns
183
+ at pairing, goal change, reload, compaction, and before every fifth review. The verdict rules live
184
+ in the tool descriptions, which the API sends at every model call, so a supervisor compaction
185
+ cannot lose them.
186
+
187
+ The process check is a snapshot where the original polls for two minutes. pi awaits the settle
188
+ handler, so polling there holds the worker's own settle for the whole poll. The loop already does
189
+ the waiting: the supervisor sees the process listed, `done` is refused, and it steers the worker to
190
+ wait instead. That puts the waiting in the transcript where you can read it.
191
+
192
+ ## Decisions
193
+
194
+ No round cap, no budget, no automatic stop. Supervision runs until you type `/supervise stop` or
195
+ the supervisor calls `done` on evidence. Ending early is the failure this exists to prevent: at a
196
+ fixed model, scaffolds that keep re-prompting scored 8.7% on MLE-bench against 0.8% for scaffolds
197
+ that let the model stop ([arXiv:2410.07095](https://arxiv.org/abs/2410.07095)). A cap would be a
198
+ competing stopping objective.
199
+
200
+ There are three verdicts, not two. `let_it_run` was added after watching a real run: with only
201
+ `steer` and `done` on offer, a supervisor with nothing to say wrote "the harness demands a tool
202
+ call ... the least-bad option is a steer that adds something new", ran a command that printed
203
+ nothing, and reported the job was at "turn 47 of 80". The log said 75 of 80. Forcing a verdict at
204
+ every look is what bought that number, so now the usual answer at a check in is to say nothing.
205
+ It was called `wait` until wassname read one and could not tell whether the supervisor had chosen
206
+ to wait or the harness was waiting on something.
207
+
208
+ Two guards remain and both prevent a false ending. `steer` is refused while no goal is set, so the
209
+ supervisor asks you or calls `set_goal`, which sends the goal to the worker and heads every later
210
+ view. `done` is refused while a tool call has no result or a child pi process is running. Neither is
211
+ as strong as it sounds. The first stops an ungrounded instruction, not an ungrounded goal, since the
212
+ supervisor can call `set_goal` with something vague; quoting it to you is the only real check. The
213
+ second proves no tracked work is missing, not that nothing is running.
214
+
215
+ Against a supervisor that circles, the view reports how many reviews in a row produced no new file
216
+ or commit, and an instruction that reuses a recent one's vocabulary comes back named. Neither
217
+ stops anything. Word overlap runs about 0.44 on a rewording against under 0.2 on two different
218
+ instructions, and 0 on a paraphrase sharing no words, so it is a floor on repetition, not a bound.
219
+
220
+ Watching is cheap enough to put a good model on it. One 16 hour pairing on the same model both
221
+ sides, deepseek-v4-pro, 75 looks and 23 steers: the worker spent $16.11 over 789 model calls and
222
+ the supervisor $0.38 over 182, about 40x less. The supervisor reads a lot and writes almost
223
+ nothing, and most of what it reads is a cache hit at $0.004/Mtok against $0.44 for fresh input.
224
+
225
+ So the reason to keep its context short is not the bill, it is that a model judging one view should
226
+ not read 234k tokens to do it. Before each call, a look older than the last three loses its view
227
+ body and its thinking, and keeps its verdict. The verdicts are the supervisor's running notes on the
228
+ worker and they are 7% of the characters; the stale views and thinking are 88%. On that session the
229
+ pruner holds the context near 25k instead of climbing, 82% off at the last look.
230
+ `npx tsx scripts/measure-prune.ts <session.jsonl>` replays a real session and prints the table.
231
+
232
+ The other saving is not asking at all. A timer look at a worker that has done nothing since the last
233
+ look shows the supervisor a view it has already read, which was 13 of that session's 92 verdicts.
234
+ Those looks are skipped, comparing the view without its status line, which carries a clock. After
235
+ three skips one goes out anyway, because a worker that has not moved in two hours is worth seeing.
236
+
237
+ A stopped worker is the exception: it is always reported, and the timer keeps running for it. The
238
+ timer used to stop when the worker did, on the reasoning that a stopped worker cannot change. That
239
+ is true and beside the point, since a stopped worker is the state an instruction is for. It cost one
240
+ run two and a half hours of silence, `let_it_run` on a stopped worker with nothing left to raise it
241
+ again, so the result of `let_it_run` now says in words that a stopped worker stays stopped.
242
+
243
+ Nor is a human message in the worker a handover. They say a word and go to bed, so the brief, the
244
+ stopped nudge and the `let_it_run` description all say that the human being present is never a
245
+ reason to stand back. Every status line carries `no new turn for 2h27m`, measured from the worker's
246
+ last session entry, which covers both sitting at the prompt and hanging inside a command that never
247
+ returns. `RESEARCH_JOURNAL.md` has the sessions these came out of.
248
+
249
+ ## Limits
250
+
251
+ The `done` guard sees a child process named `pi` directly under the worker, so it misses a detached
252
+ job, a queue, a training run, and a subagent started through an intermediate `node` process. Between
253
+ looks the supervisor is blind, so half an hour is the worst case for spotting a wrong path. Shorten
254
+ `WATCH_INTERVAL_MS` to pay more for a closer eye. Views are cut to 15 KB, oldest turns first,
255
+ because the broker drops anything over 16 KiB and never tells the extension. The last pair wins and
256
+ nothing authenticates it. The stagnation count lives in memory and restarts with the worker.
257
+
258
+ ## Testing
259
+
260
+ ```
261
+ npm test # 93 tests, free apart from a ps call
262
+ npx tsx scripts/e2e.ts # two real pi processes and a real model, costs cents
263
+ PI_SUPERVISOR_DEBUG=1 pi ... # trace the wire to stderr, since the channel is invisible
264
+ ```
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@wassname2/pi-supervise",
3
+ "version": "0.0.1",
4
+ "description": "pi extension: supervised pi sessions (renamed to pi-intercom-supervisor, kept for git: install)",
5
+ "author": "wassname",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/wassname/pi-supervise.git"
10
+ },
11
+ "homepage": "https://github.com/wassname/pi-supervise#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/wassname/pi-supervise/issues"
14
+ },
15
+ "keywords": [
16
+ "pi-package",
17
+ "pi",
18
+ "pi-extension",
19
+ "supervisor",
20
+ "pi-supervise",
21
+ "multi-agent"
22
+ ],
23
+ "pi": {
24
+ "extensions": [
25
+ "./src/index.ts"
26
+ ],
27
+ "image": "https://cdn.jsdelivr.net/gh/wassname/pi-intercom-supervisor@main/media/screenshot.png"
28
+ },
29
+ "files": [
30
+ "src",
31
+ "!src/*.test.ts",
32
+ "README.md"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "scripts": {
38
+ "pretest": "tsx scripts/make-fixture.ts",
39
+ "test": "tsx --test src/*.test.ts"
40
+ },
41
+ "dependencies": {
42
+ "@sting8k/pi-vcc": ">=0.5.0"
43
+ },
44
+ "peerDependencies": {
45
+ "@earendil-works/pi-coding-agent": "*",
46
+ "pi-intercom": "*",
47
+ "typebox": "*"
48
+ },
49
+ "devDependencies": {
50
+ "@earendil-works/pi-coding-agent": "^0.84.1",
51
+ "pi-intercom": "^0.10.0",
52
+ "tsx": "^4.20.0",
53
+ "typebox": "^1.3.12"
54
+ },
55
+ "license": "MIT"
56
+ }