@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/src/prompts.ts ADDED
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Every word the supervisor session reads, in the order it reads them, so this file is the run:
3
+ *
4
+ * 1. loadSupervisorPrompt, the policy, read once at /supervise
5
+ * 2. BRIEF, sent once at pairing, carrying that policy
6
+ * 3. TOOL_*, the three verdicts, in context at every model call because tools always are
7
+ * 4. REVIEW_NUDGE, sent with every view, and short because 1 to 3 already said the rest
8
+ * 5. NO_GOAL and DONE_BLOCKED, refusals, read only when a tool is refused
9
+ * 6. DEFAULT_SUPERVISOR_PROMPT, the policy used when no SUPERVISOR.md exists
10
+ */
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
14
+
15
+ /**
16
+ * Same precedence as @monotykamary/pi-supervisor, so an existing SUPERVISOR.md keeps working:
17
+ * <cwd>/.pi/SUPERVISOR.md, then <agent dir>/SUPERVISOR.md, then the default below.
18
+ *
19
+ * getAgentDir is pi's own, so a profile that moves the agent dir moves this with it. That matters:
20
+ * the only SUPERVISOR.md on this machine lives in a profile, not in ~/.pi/agent.
21
+ */
22
+ export function loadSupervisorPrompt(cwd: string): { prompt: string; source: string } {
23
+ for (const path of [join(cwd, ".pi", "SUPERVISOR.md"), join(getAgentDir(), "SUPERVISOR.md")]) {
24
+ if (existsSync(path)) return { prompt: readFileSync(path, "utf-8").trim(), source: path };
25
+ }
26
+ return { prompt: DEFAULT_SUPERVISOR_PROMPT, source: "built-in" };
27
+ }
28
+
29
+ /**
30
+ * Put in the supervisor's context when pairing, so it knows its job before the first view arrives.
31
+ *
32
+ * The last line has been wrong three times. "Reply with exactly: watching" taught a text answer,
33
+ * and deepseek-v4-flash then answered two real views with the plain word "wait" and no tool call
34
+ * (session 019ff4eb-c66c, 2026-08-12). Ordering a let_it_run call taught the opposite: with no view
35
+ * to read, one supervisor answered its own brief 98 times in a row (019ffa5f, 2026-08-13). Asking
36
+ * for no answer at all still got two calls, because the brief arrived as a user message and a user
37
+ * message is a turn. It now arrives without one, so the view is the first thing there is to answer.
38
+ */
39
+ export const BRIEF = (policy: string, goal: string, worker: string) =>
40
+ `${policy}
41
+
42
+ You are now supervising the pi session "${worker}".
43
+
44
+ The goal is between the tags below, exactly as the human typed it. Nothing outside the tags is
45
+ part of the goal. A multi-line goal appears as a one-line locator in ordinary views. Its full text
46
+ returns after a goal change, reload, compaction, and before every fifth review.
47
+
48
+ <goal>
49
+ ${goal || "not given, so infer it from the first view you receive and call set_goal"}
50
+ </goal>
51
+
52
+ Your verdict is a tool call, not text: let_it_run, steer or done. If the policy above tells you to reply
53
+ with JSON, ignore that part: it belongs to a different supervisor and nothing parses it here.
54
+
55
+ You see the worker twice: when it stops, and on a check in while it is still working. Each view
56
+ carries only what is new since your last look, so read it against what you already know rather
57
+ than expecting the whole session again.
58
+
59
+ The view names the worker's model and how full its context is. A small or fast model needs one
60
+ small step per instruction. A worker near the top of its context is about to compact, so tell it
61
+ to write down what matters before it loses the detail.
62
+
63
+ There is no round limit and no budget. Supervision runs until the human stops it. Ending early is
64
+ the failure this exists to prevent, so never stop because it feels like enough.
65
+
66
+ The human typing in the worker session is not a handover, and it is not a reason to stand back.
67
+ They say a word and go to bed; the worker is then stopped with nobody driving it, which is the
68
+ state you exist for. They will stop you themselves when they want you stopped. Judge the worker
69
+ against the goal and nothing else.
70
+
71
+ Every view says how long the worker has gone with no new turn. A worker that has produced nothing
72
+ for a long time is stuck, or waiting for you, or in one command that will not return. Say which
73
+ one you think it is, and use the number rather than guessing from the turns.
74
+
75
+ The worker sends its first view as it pairs. It is below this message, and it is what you
76
+ answer.`;
77
+
78
+ /**
79
+ * Sent on a resume or a /reload that finds the pairing still alive.
80
+ *
81
+ * Short on purpose: the policy is already in the transcript above it. Only the answer shape and the
82
+ * goal repeat, because those are what a supervisor drops first, and because a /reload is how a
83
+ * changed prompt reaches a running session. Without this, fixing the wording of the brief needs
84
+ * /supervise stop and a fresh pairing, which throws away the supervisor's memory of its own steers.
85
+ */
86
+ export const REANCHOR = (goal: string, rounds: number) =>
87
+ `Supervising again, after a reload or a restart.
88
+
89
+ <goal>
90
+ ${goal || "not set"}
91
+ </goal>
92
+
93
+ ${rounds} instructions so far.
94
+
95
+ A view of the worker follows. Answer it with one tool call: steer, done or let_it_run. The word on its
96
+ own does nothing; only the call reaches the worker.`;
97
+
98
+ /** Sent when the human runs /supervise goal, so the supervisor does not judge against the old one. */
99
+ export const GOAL_CHANGED = (goal: string) =>
100
+ `The human changed the goal. From now on judge the worker against what is between the tags,
101
+ and against nothing else:
102
+
103
+ <goal>
104
+ ${goal}
105
+ </goal>
106
+
107
+ A fresh view follows. Answer it with one tool call: steer, done or let_it_run.`;
108
+
109
+ /**
110
+ * The three verdicts. These live in the tool descriptions, which the API sends at every model call,
111
+ * so they are the only instructions here that a supervisor compaction cannot lose.
112
+ */
113
+ export const TOOL_LET_IT_RUN =
114
+ "Use when the current worker view gives quoted evidence that no instruction is needed."
115
+ + " The call sends no message to the worker. Call it once, then end the current supervisor response."
116
+ // Repeated here because a tool description survives a compaction and the brief does not. The
117
+ // live failure was a let_it_run reasoned "human is actively directing", two hours before dawn.
118
+ + " A human message does not end supervision; only an explicit stop command ends supervision.";
119
+
120
+ /**
121
+ * How a look ends, and it must appear in every verdict's result.
122
+ *
123
+ * A tool result reads as a prompt to act again. A turn ends only when the assistant writes text and
124
+ * calls no tool, so a result that does not name that exit leaves another tool call as the only move.
125
+ * The let_it_run result used to say "Say nothing more until the next view arrives", which forbids the
126
+ * exit outright: session 019ffa73 answered with a second let_it_run on all sixteen looks before 11:05Z
127
+ * and aborted every one. The steer result said nothing about ending, and cost a spare let_it_run on
128
+ * 22 of 22 steers in the fifteen hours after.
129
+ */
130
+ export const END_TURN =
131
+ `End the current supervisor response now: write one short line or no text, then make no further tool call.`;
132
+
133
+ export const LET_IT_RUN_ACK = (reason: string, workerStopped = false) =>
134
+ `No supervisor instruction was sent for the current worker view. Supervisor-provided reason, not independently verified: ${reason}\n\nThe supervisor has completed its verdict for the current worker view. ${END_TURN}
135
+ ${workerStopped ? STOPPED_WARNING : "A later worker view starts the next supervisor review."}`;
136
+
137
+ /**
138
+ * Added to the let_it_run result when the view said the worker had stopped.
139
+ *
140
+ * Session 019ffa73, 2026-08-14: the worker stopped, the supervisor answered let_it_run "waiting for
141
+ * the worker to re-queue", and both sat still for two and a half hours. A stopped worker does not
142
+ * resume on its own, so letting it run leaves it stopped. The timer now looks again either way, and
143
+ * this says why that look will show the same thing.
144
+ */
145
+ export const STOPPED_WARNING =
146
+ `The current worker view reports that worker execution stopped. A stopped worker does not resume
147
+ without a new user or supervisor message. If the goal remains unmet, send a concrete continuation
148
+ instruction. A human message does not end supervision. A later worker view will report the worker state.`;
149
+
150
+ /** The answer to a second let_it_run in one look. Costs a round trip and no error line. */
151
+ export const LET_IT_RUN_AGAIN =
152
+ `The supervisor already recorded a verdict for the current worker view. This second let_it_run call
153
+ sent no instruction. ${END_TURN}`;
154
+
155
+ /** The answer after a supervisor directive is sent. A repeat warning is appended after it. */
156
+ export const STEER_ACK = (round: number, workerId: string) =>
157
+ `Supervisor instruction ${round} was sent to worker session ${workerId}. Worker receipt and execution
158
+ are not confirmed. The supervisor has completed its verdict for the current worker view. ${END_TURN}`;
159
+ export const TOOL_STEER =
160
+ "Send one concrete next action to the worker. The extension sends the message to the paired worker session; worker receipt and execution require a later worker view.";
161
+ export const TOOL_DONE =
162
+ "Declare the goal met and stop supervising. Only call this with quoted evidence from the view.";
163
+
164
+ /**
165
+ * Sent with every view, so it is deliberately short.
166
+ *
167
+ * What used to be here and is now sent once: the verdict rules (BRIEF, and the tool descriptions,
168
+ * which survive a compaction) and the instructions already sent (the supervisor's own steer calls
169
+ * are in its context; the steer tool warns about a repeat when it happens). A multi-line goal
170
+ * stays as a one-line locator inside the view.
171
+ *
172
+ * A check in is not a decision point. Interrupting a working agent is expensive and usually wrong,
173
+ * so the two triggers ask for different things.
174
+ */
175
+ /**
176
+ * The two openers, and the test the context pruner uses to find a view it can drop.
177
+ *
178
+ * They are constants because two things read them: the nudge that writes a view, and the pruner
179
+ * that later collapses it. Matching the prose in two places would let them drift silently, and a
180
+ * pruner that stops recognising views just quietly stops working.
181
+ */
182
+ export const VIEW_STOPPED = "The worker stopped.";
183
+ export const VIEW_CHECKIN = "Checking in on the worker, which is still going.";
184
+ export const isViewText = (text: string) => text.startsWith(VIEW_STOPPED) || text.startsWith(VIEW_CHECKIN);
185
+
186
+ /** What an old view is replaced with. Short, and it says where the content went. */
187
+ export const VIEW_PRUNED =
188
+ "[an earlier view of the worker, dropped once you had judged it. Your verdict on it follows.]";
189
+
190
+ export const REVIEW_NUDGE = (view: string, rounds: number, stopped: boolean) =>
191
+ stopped
192
+ ? `${VIEW_STOPPED}
193
+
194
+ ${view}
195
+
196
+ ${rounds} instructions so far. The status line says how long it has had no new turn. It will not start
197
+ again by itself, and the human being present does not count as somebody driving it. Answer with one
198
+ tool call: steer, done or let_it_run. The word on its own does nothing; only the call reaches the
199
+ worker.`
200
+ : `${VIEW_CHECKIN}
201
+
202
+ ${view}
203
+
204
+ Call let_it_run unless the view gives concrete evidence that the worker needs an instruction.`;
205
+
206
+ /** Refusal shown when done is called while the worker still has work running. */
207
+ export const DONE_BLOCKED = (what: string) =>
208
+ `Cannot finish: the worker still has work running (${what}). Wait for the next view.`;
209
+
210
+ /** Refusal shown when the supervisor tries to steer with no goal set. */
211
+ export const NO_GOAL = `No goal is set, so you must not steer or finish. Inventing a task is worse
212
+ than doing nothing. Either call set_goal with the goal you infer from the worker's view, which
213
+ tells the human what you chose, or reply in plain text asking them for it. Your reply reaches
214
+ their phone.`;
215
+
216
+ /**
217
+ * Default supervisor prompt. A project SUPERVISOR.md overrides it, same as @monotykamary/pi-supervisor.
218
+ * Unlike that extension there is no JSON verdict to parse, because the verdict is a tool call.
219
+ */
220
+ export const DEFAULT_SUPERVISOR_PROMPT = `You supervise a coding agent from outside its session.
221
+ Your job is to make it reach the goal without the human stepping in.
222
+
223
+ Judge from the view only. You cannot see the worker's files unless you read them yourself.
224
+
225
+ Call steer when the work is incomplete, when the worker asked a question you can answer with a
226
+ sensible default, or when it claims success without evidence. One concrete next action per steer.
227
+ Never repeat a steer that had no effect; change the approach instead.
228
+
229
+ The view line "child pi processes still running" means the worker delegated to a subagent that is
230
+ still working. It stopped, the subagent did not. Do not call done, it will be refused. Steer the
231
+ worker to wait for that subagent and report what it produced.
232
+
233
+ The view line "no new file or commit for N reviews in a row" means your last N instructions
234
+ moved nothing the worker's session can show. Two or more is your signal to change approach, ask the
235
+ human, or check whether the goal is already met. Sometimes it is honest work on one file, so read
236
+ the recent turns before you decide.
237
+
238
+ Call done only when all of these hold:
239
+ 1. the worker named the artifact file it produced, with a path
240
+ 2. the worker quoted text from that file, rather than summarising it
241
+ 3. nothing in the view contradicts the claim
242
+
243
+ A confident summary is not evidence. When in doubt, steer.
244
+
245
+ When the worker does machine learning or data research, a wrong result looks exactly like a right
246
+ one. These steers come from wassname's ml-debug skill, roughly in the order they bite. Each one is
247
+ something you can see in the view.
248
+ - It concluded without reading its data. Steer it to paste the lines it read into the chat, a raw
249
+ sample and the metric line, not a summary of them. Quoting is the point, twice over: you and the
250
+ human can then check the same text, and an agent that has to quote has to look (Karpathy inspects
251
+ the data before touching the model; Nanda: read your data, often it is quite bad). A conclusion
252
+ with no quoted output, a ranking with no per-item evidence, or "the method failed" with no sample
253
+ of what the output looked like, all mean it has not looked.
254
+ - It reports a surprising win. Most true results are boring, so an exciting one is more likely to
255
+ be false (Neel Nanda). Steer it to rule out a bug, leakage or a broken evaluation first.
256
+ - It reports a failure and moves on, or calls the failure a property of the method. Assume a bug:
257
+ bugs are far more common, and far cheaper to find, than a real negative result (Andy Jones).
258
+ Steer it to write two or three diagnoses, one of them a bug in its own code, put a rough
259
+ probability on each, and run the cheapest test that tells them apart. Broken research code fails
260
+ silently and still runs, so "it ran" is not evidence that it worked.
261
+ - It is about to start another long run without saying what each outcome would mean. Steer it to
262
+ write that prediction first (Rahtz: think more, experiment less). On a shared GPU that is the
263
+ cheapest hour you can buy.
264
+ - It compares two methods from one run each. Seed variance alone splits identical configurations
265
+ into different distributions (Henderson), so steer it to say what varies before it ranks
266
+ anything.
267
+ - It changed two things in one run and credits one of them. Changing anything changes everything
268
+ (Sculley et al., CACE). Steer it to say what it can actually attribute, or to rerun with one
269
+ change.
270
+ - It saw a number it cannot explain and carried on. An anomaly it did not go looking for is the
271
+ cheapest bug it will ever find, so steer it to chase that before anything else.
272
+
273
+ Three more ways work gets faked, from @monotykamary/pi-supervisor's cheating list. Steer, and ask
274
+ for the output that would settle it.
275
+ - the worker edits a test to weaken an assertion, or skips a failing one, and calls that progress
276
+ - it reports a number without the command output it came from, or edits the measurement instead of
277
+ the thing being measured
278
+ - it runs a smaller dataset or part of the suite, then reports as if it ran the whole thing
279
+
280
+ Do not answer questions that need real human knowledge: passwords, credentials, spending money,
281
+ or a choice between two designs the human cares about. For those, reply in plain text saying what
282
+ you need. Your reply reaches the human's phone.`;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * What the two sessions say to each other over the pi-intercom extension channel.
3
+ *
4
+ * The channel never enters a transcript and never starts a turn, so each side triggers its own
5
+ * turn locally with pi.sendUserMessage after it receives one of these.
6
+ */
7
+
8
+ export const NAMESPACE = "wassname/pi-intercom-supervisor/v1";
9
+
10
+ // No round cap, no budget, on purpose. Supervision runs until the human stops it with
11
+ // /supervise stop, because premature stopping is the failure this whole thing exists to prevent
12
+ // (wassname's SUPERVISOR.md, citing arXiv:2410.07095: 8.7% vs 0.8% on MLE-bench).
13
+
14
+ export type Wire =
15
+ /** Roll call, broadcast, so "to" is the wildcard rather than a session. Only /supervise sends it. */
16
+ | { t: "who"; to: "*" }
17
+ /** The answer to a roll call: I load this extension, I am free, and I am not a child run. */
18
+ | { t: "here"; to: string }
19
+ | { t: "pair"; to: string; goal: string }
20
+ | { t: "paired"; to: string }
21
+ | { t: "goal"; to: string; goal: string }
22
+ /** stopped: the worker settled, so this is a decision point. false: a check in mid-turn. */
23
+ | { t: "view"; to: string; view: string; stopped: boolean }
24
+ /** Supervisor asks for a view now. Its own turn cannot make one: the worker publishes them. */
25
+ | { t: "look"; to: string }
26
+ | { t: "directive"; to: string; text: string }
27
+ | { t: "done"; to: string; reason: string }
28
+ | { t: "unpair"; to: string };
29
+
30
+ /** Validates the field each kind carries, so a malformed peer cannot inject "[supervisor] undefined". */
31
+ export function isWire(payload: unknown): payload is Wire {
32
+ if (typeof payload !== "object" || payload === null) return false;
33
+ const { t, to, goal, view, stopped, text, reason } = payload as Record<string, unknown>;
34
+ if (typeof to !== "string") return false;
35
+ if (t === "pair" || t === "goal") return typeof goal === "string";
36
+ if (t === "view") return typeof view === "string" && typeof stopped === "boolean";
37
+ if (t === "directive") return typeof text === "string" && text.trim().length > 0;
38
+ if (t === "done") return typeof reason === "string";
39
+ return t === "unpair" || t === "paired" || t === "look" || t === "who" || t === "here";
40
+ }
41
+
42
+ /**
43
+ * Words two instructions share, over the words either uses. Stopwords and short words dropped.
44
+ *
45
+ * Six remembered instructions do not stop repetition, because the same order rephrased reads as
46
+ * new. This catches the rephrasing that shares vocabulary; it cannot catch a true paraphrase.
47
+ */
48
+ export function overlap(a: string, b: string): number {
49
+ const words = (s: string) =>
50
+ new Set(
51
+ s
52
+ .toLowerCase()
53
+ .split(/[^a-z0-9_./-]+/)
54
+ .filter((w) => w.length > 3 && !STOPWORDS.has(w)),
55
+ );
56
+ const [x, y] = [words(a), words(b)];
57
+ if (!x.size || !y.size) return 0;
58
+ const shared = [...x].filter((w) => y.has(w)).length;
59
+ return shared / (x.size + y.size - shared);
60
+ }
61
+
62
+ const STOPWORDS = new Set([
63
+ "then", "with", "that", "this", "from", "into", "your", "each", "have", "then", "should", "please",
64
+ "make", "sure", "also", "them", "they", "what", "when", "here", "there", "which", "will", "would",
65
+ ]);
66
+
67
+ /**
68
+ * Two instructions sharing this much vocabulary get flagged back to the supervisor.
69
+ *
70
+ * Measured on rewordings of one instruction: about 0.44. On two different instructions: under 0.2.
71
+ * A true paraphrase that shares no words scores 0 and slips through, so this is a floor on
72
+ * repetition, not a bound.
73
+ */
74
+ export const OVERLAP_WARN = 0.4;
75
+
76
+ export interface SuperviseState {
77
+ role: "none" | "worker" | "supervisor";
78
+ /** Intercom session ID of the other side. The broker stamps this, so it cannot be forged. */
79
+ pairedId: string;
80
+ goal: string;
81
+ steerRounds: number;
82
+ /** Recent steer texts, so the supervisor can see repetition after its own context is compacted. */
83
+ recentSteers: string[];
84
+ }
85
+
86
+ export const EMPTY_STATE: SuperviseState = {
87
+ role: "none",
88
+ pairedId: "",
89
+ goal: "",
90
+ steerRounds: 0,
91
+ recentSteers: [],
92
+ };
93
+
94
+ /** How many past steers to keep and show back. Enough to spot a loop, small enough to stay cheap. */
95
+ export const STEER_MEMORY = 6;
96
+
97
+ /** Session entry type used to persist state, so a compaction or reload cannot reset the count. */
98
+ export const STATE_ENTRY = "supervise-state";
99
+
100
+ /** Rebuild state from session entries. The last one written wins. */
101
+ export function restoreState(entries: Array<{ type: string; customType?: string; data?: unknown }>): SuperviseState {
102
+ let state = EMPTY_STATE;
103
+ for (const entry of entries) {
104
+ if (entry.type === "custom" && entry.customType === STATE_ENTRY && entry.data) {
105
+ // Merge over the defaults so a record written before a field existed still loads.
106
+ state = { ...EMPTY_STATE, ...(entry.data as Partial<SuperviseState>) };
107
+ }
108
+ }
109
+ return state;
110
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Child pi processes, so a settled worker with a subagent still running is not called finished.
3
+ *
4
+ * The in-session check only sees tool calls that never got a result. A subagent spawned as its own
5
+ * process leaves no such trace, so the worker settles and the view looks quiet.
6
+ *
7
+ * This is a snapshot, not a wait. The original polls here for up to two minutes, and pi awaits the
8
+ * settle handler (agent-session.js:330), so that holds the worker's own settle for the whole poll.
9
+ * The loop already does the waiting: the supervisor sees the process listed, done is refused, and
10
+ * it steers instead. That leaves the waiting in the transcript where you can read it.
11
+ *
12
+ * Ported from @monotykamary/pi-supervisor (MIT), src/subagent-detector.ts. Extension agnostic: it
13
+ * does not matter who spawned them. Nothing is caught here, so a broken ps is loud.
14
+ */
15
+ import { exec } from "node:child_process";
16
+ import { promisify } from "node:util";
17
+
18
+ const execAsync = promisify(exec);
19
+
20
+ interface PiProcess {
21
+ pid: number;
22
+ ppid: number;
23
+ }
24
+
25
+ async function piProcesses(): Promise<PiProcess[]> {
26
+ if (process.platform !== "darwin" && process.platform !== "linux") return [];
27
+ const { stdout } = await execAsync(`ps -eo ppid,pid,comm | grep -E "\\bpi\\b" || true`);
28
+ return stdout
29
+ .trim()
30
+ .split("\n")
31
+ .map((line) => line.trim().split(/\s+/))
32
+ .filter((parts) => parts.length >= 3 && parts[2] === "pi")
33
+ .map((parts) => ({ ppid: Number(parts[0]), pid: Number(parts[1]) }));
34
+ }
35
+
36
+ export async function childPiProcesses(): Promise<number[]> {
37
+ return (await piProcesses()).filter((p) => p.ppid === process.pid).map((p) => p.pid);
38
+ }