Own visible supervision in pi-goals

Co-Authored-By: PI[Kimi K3] <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
wassname
2026-09-07 21:41:26 +08:00
co-authored by PI[Kimi K3]
parent 6c86405841
commit 19fa8d7a7b
14 changed files with 398 additions and 292 deletions
+5 -6
View File
@@ -8,20 +8,19 @@ Plan in one Pi session, then do the work there while a stronger visible Pi sessi
2. Pi asks only material questions, writes the plan, and shows **Ready / Refine / Edit / Cancel**.
3. **Ready** opens a second Herdr pane. The new Pi session explicitly forks the planning session and compacts that fork.
4. The original session becomes the implementation worker. It keeps the full conversation and normal tools.
5. The fork becomes a read-only supervisor. `pi-supervise` gives it compact worker views and carries its instructions to the worker through `pi-intercom`.
6. The supervisor compacts again when its context reaches 100k tokens.
7. The supervisor records a private approval only after it sees a stopped worker, no active work, a clean commit, evidence, and saved verification output. `CompleteGoal` checks that approval against the exact plan block and Git tree before it ticks `[x]`.
5. The fork becomes a read-only supervisor. Worker views and supervisor instructions use a session-scoped mailbox under ignored `.pi/goals-supervision/`.
6. Ready waits for the supervisor's durable readiness receipt; the worker does not begin before the fork has compacted and started.
7. The supervisor compacts again when its context reaches 100k tokens.
8. The supervisor records a private approval only after it sees a stopped worker, no active work, a clean commit, evidence, and saved verification output. `CompleteGoal` checks that approval against the exact plan block and Git tree before it ticks `[x]`.
The two Pi sessions are visible. You can switch to the supervisor pane and talk to it directly.
## Install
This branch requires Herdr 0.7.5 or newer and these Pi packages:
This branch requires Herdr 0.7.5 or newer and one Pi package:
```bash
pi install npm:@wassname2/pi-goals
pi install npm:@wassname2/pi-supervise
pi install npm:pi-intercom
```
For a local checkout:
-1
View File
@@ -19,7 +19,6 @@
"uat",
"evidence",
"supervisor",
"pi-intercom",
"herdr"
],
"peerDependencies": {
@@ -0,0 +1,33 @@
# One-package visible supervision
## Goal
Replace the pi-goals → pi-supervise → pi-intercom runtime chain with one pi-goals extension in two Pi processes. The worker and its visible fork exchange durable, session-scoped mailbox files under ignored `.pi/`.
## Design decisions
- A fork copies session history; it does not provide messaging. The mailbox is the explicit local-process channel.
- Ready waits for the supervisor's durable `ready.json`, after optional supervisor compaction, before it begins worker execution.
- Worker views are written on Ready, settle, 50 turns, and 60 minutes. The supervisor polls views and writes one steer request. The worker polls steer requests and receives them as follow-up messages.
- The canonical plan remains a direct path in the supervisor prompt. It is not a summary artifact.
- Keep the approval checkpoint: stopped view, no active work, clean commit, plan evidence, and tracked verification output.
- No external `pi-supervise` or `pi-intercom` runtime dependency remains.
## Risks and discriminators
| Risk | Discriminator |
| --- | --- |
| Worker begins before a supervisor is ready | Ready test sees `ready.json` before state changes to working or sends the execution prompt. |
| Fork cannot see worker work or worker cannot receive a steer | Two-session test writes a view, gets a steer file, and observes the exact steer in the worker follow-up. |
| Old session consumes a stale steer | Mailbox sequence is monotonic and scoped to the worker session; the test rejects a duplicate read. |
| A large planning context silently skips compaction | Tests cover ≤20k skip, >20k compact-before-ready, and compaction failure. |
## Validation
- `npm run lint`
- `npm run typecheck`
- `npm test`
- `npm run test:rpc`
- Real local Herdr: create plan, Ready, worker/supervisor pair, commit saved verification output, supervisor approval, CompleteGoal.
-- PI[Kimi K3]
+15 -8
View File
@@ -1,17 +1,18 @@
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { supervisorReady } from "./mailbox.js";
const execFileAsync = promisify(execFile);
const STARTUP_TIMEOUT_MS = 5 * 60_000;
interface LaunchSupervisorInput {
cwd: string;
sourceSessionFile: string;
workerSessionId: string;
workerIntercomId: string;
planPath: string;
approvalId: string;
mailboxPath: string;
extensionPath: string;
superviseExtensionPath: string | null;
model: string | null;
}
@@ -49,16 +50,14 @@ export function supervisorCommand(input: LaunchSupervisorInput): string {
const env = [
"PI_GOALS_ROLE=supervisor",
`PI_GOALS_WORKER_ID=${input.workerSessionId}`,
`PI_GOALS_WORKER_INTERCOM_ID=${input.workerIntercomId}`,
`PI_GOALS_PLAN_PATH=${input.planPath}`,
`PI_GOALS_APPROVAL_ID=${input.approvalId}`,
`PI_GOALS_OWNER_SESSION_ID=${input.workerSessionId}`,
`PI_GOALS_MAILBOX_PATH=${input.mailboxPath}`,
];
const args = [
"pi",
"--no-extensions",
"-e", "npm:pi-intercom",
"-e", process.env.PI_GOALS_SUPERVISE_EXTENSION ?? input.superviseExtensionPath ?? "npm:@wassname2/pi-supervise@0.0.4",
"-e", input.extensionPath,
"--fork", input.sourceSessionFile,
"--name", `goals-supervisor-${input.workerSessionId.slice(0, 8)}`,
@@ -67,18 +66,26 @@ export function supervisorCommand(input: LaunchSupervisorInput): string {
return `env ${[...env, ...args].map(shellQuote).join(" ")}`;
}
export async function waitForSupervisorReady(mailboxPath: string, timeoutMs = STARTUP_TIMEOUT_MS): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (!supervisorReady(mailboxPath)) {
if (Date.now() >= deadline) throw new Error("The visible supervisor did not become ready.");
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
export async function openSupervisorPane(input: LaunchSupervisorInput): Promise<string> {
if (process.env.HERDR_ENV !== "1") throw new Error("Ready needs a Herdr session so pi-goals can open the supervisor session.");
await herdr(["--version"], false);
const split = await herdr(["pane", "split", "--current", "--direction", "right", "--cwd", input.cwd, "--no-focus"]);
const paneId = findPaneId(split);
if (!paneId) throw new Error("Herdr did not return the new supervisor pane ID.");
await herdr(["pane", "run", paneId, supervisorCommand(input)]);
try {
await herdr(["pane", "run", paneId, supervisorCommand(input)]);
await waitForSupervisorReady(input.mailboxPath);
return paneId;
} catch (error) {
await closeSupervisorPane(paneId);
throw error;
throw new Error(`Supervisor startup incomplete in Herdr pane ${paneId}; inspect that pane. ${error instanceof Error ? error.message : String(error)}`);
}
}
+72 -18
View File
@@ -1,6 +1,6 @@
/**
* PI: pi-goals owns one versioned plan per session. After Ready, the main session implements the
* plan while a compacted, visible fork supervises it through pi-supervise.
* plan while a compacted, visible fork supervises it through a durable mailbox.
*
* Each /goals call makes `.pi/plan/<session_id>-vN.md`. The selected version survives resume and
* compaction. Old plans stay on disk but inactive. A session with no selected plan has no widget,
@@ -22,9 +22,10 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
import { Type } from "typebox";
import { approvalMatches, approvalPath, goalBlock, hashGoalBlock, readApproval, repositoryState } from "./approval.js";
import { closeSupervisorPane, openSupervisorPane } from "./herdr.js";
import { createMailbox, workerSteersAfter, writeWorkerView } from "./mailbox.js";
import { completeGoalDescription, completeGoalParamDescription, planDrafting, planningState, resync } from "./prompts.js";
import { SUPERVISOR_STARTUP_TIMEOUT_MS, workerPiSupervise } from "./supervise.js";
import { isVisibleSupervisor, registerVisibleSupervisor } from "./supervisor-session.js";
import { workerView } from "./worker-view.js";
const STATE = "pi-goals-state";
const STATUS_KEY = "pi-goals";
@@ -95,6 +96,8 @@ interface PlanState {
supervisorModel: string | null;
supervisorPaneId: string | null;
approvalId: string | null;
mailboxPath: string | null;
lastSteer: number;
planVersion: number | null;
}
@@ -109,6 +112,8 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
supervisorModel: null,
supervisorPaneId: null,
approvalId: null,
mailboxPath: null,
lastSteer: 0,
planVersion: null,
};
let planningContextPending = false;
@@ -135,15 +140,12 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
for (const goal of scanGoals(readPlan(ctx))) {
rmSync(approvalPath(ctx.cwd, ctx.sessionManager.getSessionId(), goal.subject), { force: true });
}
state = { ...state, approvalId: randomUUID() };
const approvalId = randomUUID();
const mailbox = createMailbox(ctx.cwd, ctx.sessionManager.getSessionId(), approvalId, planPath(ctx));
state = { ...state, approvalId, mailboxPath: mailbox.path, lastSteer: 0 };
persist();
}
function loadedPiSuperviseExtensionPath(): string | null {
const tool = pi.getAllTools().find((candidate) => candidate.name === "worker_view") as { sourceInfo?: { path?: unknown } } | undefined;
return typeof tool?.sourceInfo?.path === "string" ? tool.sourceInfo.path : null;
}
function repositoryRoot(cwd: string): string {
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" }).trim();
}
@@ -152,7 +154,6 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
repositoryRoot(ctx.cwd);
const sourceSessionFile = ctx.sessionManager.getSessionFile();
if (!sourceSessionFile) throw new Error("The current session is not persisted, so it cannot be forked.");
const worker = await workerPiSupervise(pi);
beginReview(ctx);
let paneId: string | null = null;
try {
@@ -160,14 +161,12 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
cwd: ctx.cwd,
sourceSessionFile,
workerSessionId: ctx.sessionManager.getSessionId(),
workerIntercomId: worker.intercomId,
planPath: planPath(ctx),
approvalId: state.approvalId!,
mailboxPath: state.mailboxPath!,
extensionPath: fileURLToPath(import.meta.url),
superviseExtensionPath: loadedPiSuperviseExtensionPath(),
model: state.supervisorModel,
});
await worker.waitForPair(SUPERVISOR_STARTUP_TIMEOUT_MS);
} catch (error) {
if (paneId) throw new Error(`Supervisor startup failed in Herdr pane ${paneId}; it remains open for inspection. ${error instanceof Error ? error.message : String(error)}`);
throw error;
@@ -176,7 +175,43 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
persist();
}
let workerTurns = 0;
let viewTimer: ReturnType<typeof setInterval> | undefined;
let steerTimer: ReturnType<typeof setInterval> | undefined;
function mailbox(ctx: ExtensionContext) {
if (!state.approvalId || !state.mailboxPath) throw new Error("No active supervisor mailbox.");
return createMailbox(ctx.cwd, ctx.sessionManager.getSessionId(), state.approvalId, planPath(ctx));
}
function publishWorkerView(ctx: ExtensionContext, reason: "ready" | "settled" | "turns" | "interval"): void {
if (state.phase !== "working") return;
writeWorkerView(mailbox(ctx), reason, workerView(ctx.sessionManager.getBranch(), reason));
}
function deliverWorkerSteers(ctx: ExtensionContext): void {
if (state.phase !== "working") return;
for (const steer of workerSteersAfter(mailbox(ctx).path, state.lastSteer)) {
state = { ...state, lastSteer: steer.sequence };
persist();
pi.sendUserMessage(`[supervisor] ${steer.instruction}`, { deliverAs: "followUp" });
}
}
function startWorkerTimers(ctx: ExtensionContext): void {
if (!viewTimer) viewTimer = setInterval(() => publishWorkerView(ctx, "interval"), 60 * 60_000);
if (!steerTimer) steerTimer = setInterval(() => deliverWorkerSteers(ctx), 1_000);
}
function stopWorkerTimers(): void {
if (viewTimer) clearInterval(viewTimer);
if (steerTimer) clearInterval(steerTimer);
viewTimer = undefined;
steerTimer = undefined;
}
async function stopSupervisor(): Promise<boolean> {
stopWorkerTimers();
if (!state.supervisorPaneId) return true;
try {
await closeSupervisorPane(state.supervisorPaneId);
@@ -233,7 +268,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
ctx.ui.notify("Could not close the visible supervisor; the plan remains connected.", "warning");
return;
}
state = { ...state, phase: null, supervisorPaneId: null, approvalId: null, planVersion: null };
state = { ...state, phase: null, supervisorPaneId: null, approvalId: null, mailboxPath: null, lastSteer: 0, planVersion: null };
persist();
updateWidget(ctx);
ctx.ui.notify(`Disconnected from ${currentPlan}; the file remains on disk.`, "info");
@@ -249,7 +284,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
return;
}
const ref = arg.slice("model".length).trim();
state = { ...state, supervisorModel: ref || null, supervisorPaneId: null, approvalId: null };
state = { ...state, supervisorModel: ref || null, supervisorPaneId: null, approvalId: null, mailboxPath: null, lastSteer: 0 };
persist();
ctx.ui.notify(`Goal-supervisor model ${ref ? `set to ${ref}` : "reset to the current Pi default"}.`, "info");
return;
@@ -258,7 +293,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
ctx.ui.notify("Could not close the visible supervisor; no new plan was started.", "warning");
return;
}
state = { ...state, phase: "planning", supervisorPaneId: null, approvalId: null, planVersion: nextVersion(ctx) };
state = { ...state, phase: "planning", supervisorPaneId: null, approvalId: null, mailboxPath: null, lastSteer: 0, planVersion: nextVersion(ctx) };
planningContextPending = true;
resyncReason = null;
writePlan(ctx, "");
@@ -288,7 +323,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
pi.on("before_agent_start", async (_event, ctx) => {
if (state.phase === "working") {
return {
systemPrompt: `${ctx.getSystemPrompt()}\n\nYou are the implementation worker for ${planRel(ctx)}. Keep the full conversation and do the work directly. A stronger read-only supervisor watches this session through pi-supervise and can steer you. Commit clean evidence before asking for sign-off. Stop when a goal appears complete so the supervisor can inspect a settled worker view. Call CompleteGoal only after the supervisor says it recorded approval. -- Pi/Codex`,
systemPrompt: `${ctx.getSystemPrompt()}\n\nYou are the implementation worker for ${planRel(ctx)}. Keep the full conversation and do the work directly. A stronger read-only supervisor watches this session through its durable mailbox and can steer you. Commit clean evidence before asking for sign-off. Stop when a goal appears complete so the supervisor can inspect a settled worker view. Call CompleteGoal only after the supervisor says it recorded approval. -- PI[Kimi K3]`,
};
}
if (!planningContextPending) return;
@@ -317,6 +352,11 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
pi.on("turn_end", async (_event, ctx) => {
updateWidget(ctx);
if (state.phase !== "working") return;
workerTurns++;
if (workerTurns < 50) return;
workerTurns = 0;
publishWorkerView(ctx, "turns");
});
pi.on("tool_call", async (event, ctx) => {
@@ -341,6 +381,11 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
// PI: Print after Pi settles. agent_end is still streaming, so its message queues behind the menu.
pi.on("agent_settled", async (_event, ctx) => {
if (state.phase === "working") {
deliverWorkerSteers(ctx);
publishWorkerView(ctx, "settled");
return;
}
if (state.phase !== "planning" || !ctx.hasUI) return;
let printed = "";
while (true) {
@@ -369,7 +414,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
}
if (choice === "Cancel") {
rmSync(planPath(ctx), { force: true });
state = { ...state, phase: null, supervisorPaneId: null, approvalId: null, planVersion: null };
state = { ...state, phase: null, supervisorPaneId: null, approvalId: null, mailboxPath: null, lastSteer: 0, planVersion: null };
persist();
updateWidget(ctx);
ctx.ui.notify("Plan discarded.", "info");
@@ -381,12 +426,14 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
state = { ...state, phase: "working" };
resyncReason = "The plan was approved.";
persist();
startWorkerTimers(ctx);
publishWorkerView(ctx, "ready");
updateWidget(ctx);
ctx.ui.notify(`Visible supervisor opened in Herdr pane ${state.supervisorPaneId}.`, "info");
pi.sendUserMessage("The plan is approved. Begin implementation as the worker.");
} catch (error) {
ctx.ui.notify(`Goal supervisor could not start: ${error instanceof Error ? error.message : String(error)}`, "warning");
state = { ...state, phase: "planning", supervisorPaneId: null, approvalId: null };
state = { ...state, phase: "planning", supervisorPaneId: null, approvalId: null, mailboxPath: null, lastSteer: 0 };
persist();
updateWidget(ctx);
}
@@ -404,13 +451,20 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
supervisorModel: last?.data?.supervisorModel ?? null,
supervisorPaneId: last?.data?.supervisorPaneId ?? null,
approvalId: last?.data?.approvalId ?? null,
mailboxPath: last?.data?.mailboxPath ?? null,
lastSteer: last?.data?.lastSteer ?? 0,
planVersion: last?.data?.planVersion ?? null,
};
planningContextPending = state.phase === "planning";
resyncReason = state.phase === "working" ? "New session." : null;
if (state.phase === "working") startWorkerTimers(ctx);
updateWidget(ctx);
});
pi.on("session_shutdown", async () => {
stopWorkerTimers();
});
pi.registerTool({
name: "CompleteGoal",
label: "Goal signoff",
+106
View File
@@ -0,0 +1,106 @@
import { mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
const VERSION = 1;
const VIEWS = "views";
const STEERS = "steers";
export interface SupervisorMailbox {
path: string;
workerSessionId: string;
planPath: string;
approvalId: string;
}
export interface WorkerView {
version: 1;
sequence: number;
reason: "ready" | "settled" | "turns" | "interval";
text: string;
timestamp: string;
}
export interface WorkerSteer {
version: 1;
sequence: number;
instruction: string;
timestamp: string;
}
function writeJson(path: string, value: object): void {
const temporary = `${path}.${process.pid}.tmp`;
writeFileSync(temporary, `${JSON.stringify(value)}\n`);
renameSync(temporary, path);
}
function readJson<T>(path: string): T | null {
try {
return JSON.parse(readFileSync(path, "utf8")) as T;
} catch (error: unknown) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
throw error;
}
}
function sequence(name: string, prefix: string): number | null {
const match = new RegExp(`^${prefix}-(\\d+)\\.json$`).exec(name);
return match ? Number(match[1]) : null;
}
function nextSequence(directory: string, prefix: string): number {
return Math.max(0, ...readdirSync(directory).flatMap((name) => {
const value = sequence(name, prefix);
return value === null ? [] : [value];
})) + 1;
}
export function createMailbox(cwd: string, workerSessionId: string, approvalId: string, planPath: string): SupervisorMailbox {
const path = resolve(cwd, ".pi", "goals-supervision", workerSessionId, approvalId);
mkdirSync(join(path, VIEWS), { recursive: true });
mkdirSync(join(path, STEERS), { recursive: true });
return { path, workerSessionId, approvalId, planPath };
}
export function readyMailbox(path: string): void {
writeJson(join(path, "ready.json"), { version: VERSION, timestamp: new Date().toISOString() });
}
export function supervisorReady(path: string): boolean {
return readJson<{ version?: number }>(join(path, "ready.json"))?.version === VERSION;
}
export function writeWorkerView(mailbox: SupervisorMailbox, reason: WorkerView["reason"], text: string): WorkerView {
const directory = join(mailbox.path, VIEWS);
const view: WorkerView = { version: 1, sequence: nextSequence(directory, "view"), reason, text, timestamp: new Date().toISOString() };
writeJson(join(directory, `view-${view.sequence}.json`), view);
return view;
}
export function workerViewsAfter(path: string, after: number): WorkerView[] {
const directory = join(path, VIEWS);
return readdirSync(directory)
.flatMap((name) => {
const value = sequence(name, "view");
return value !== null && value > after ? [readJson<WorkerView>(join(directory, name))] : [];
})
.filter((view): view is WorkerView => view !== null)
.sort((a, b) => a.sequence - b.sequence);
}
export function writeWorkerSteer(path: string, instruction: string): WorkerSteer {
const directory = join(path, STEERS);
const steer: WorkerSteer = { version: 1, sequence: nextSequence(directory, "steer"), instruction, timestamp: new Date().toISOString() };
writeJson(join(directory, `steer-${steer.sequence}.json`), steer);
return steer;
}
export function workerSteersAfter(path: string, after: number): WorkerSteer[] {
const directory = join(path, STEERS);
return readdirSync(directory)
.flatMap((name) => {
const value = sequence(name, "steer");
return value !== null && value > after ? [readJson<WorkerSteer>(join(directory, name))] : [];
})
.filter((steer): steer is WorkerSteer => steer !== null)
.sort((a, b) => a.sequence - b.sequence);
}
+2 -2
View File
@@ -3,7 +3,7 @@
*
* Design: the plan file is for LLMs and the human, not for TypeScript. No parser and no schema;
* the skeleton below is a convention the drafting prompt teaches. The main session implements it,
* while a visible forked Pi session supervises through pi-supervise.
* while a visible forked Pi session supervises through a durable mailbox.
*
* THE FOLD: everything above "## Log" is the short current-goal section. Everything below it
* (Log, Learnings, Appendix) is durable memory: unlimited, read on demand, and sent in full at
@@ -161,7 +161,7 @@ export function resync(plan: string, planRel: string, why: string): string {
<system-reminder>
${why} This is the whole plan file (${planRel}), appendix included. You are the implementation worker.
Keep the high-level goal and human intent stable and do the work directly. A visible read-only Pi
session supervises you through pi-supervise. The human's latest message outranks the plan: if it
session supervises you through a durable mailbox. The human's latest message outranks the plan: if it
changes scope, amend the plan rather than preserving an obsolete decision.
${plan}
-52
View File
@@ -1,52 +0,0 @@
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
const PAIR_EVENT = "pi-supervise:pair:v1";
const WORKER_STATE_EVENT = "pi-supervise:worker-state:v1";
const WORKER_PAIRED_EVENT = "pi-supervise:worker-paired:v1";
const API_READY_EVENT = "pi-supervise:api-ready:v1";
const TIMEOUT_MS = 15_000;
export const SUPERVISOR_STARTUP_TIMEOUT_MS = 5 * 60_000;
type Events = { emit(name: string, value: unknown): boolean; on(name: string, handler: (value: any) => void): void };
function wait<T>(start: (resolve: (value: T) => void, reject: (error: Error) => void) => void, message: string, timeoutMs = TIMEOUT_MS): Promise<T> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
start((value) => { clearTimeout(timer); resolve(value); }, (error) => { clearTimeout(timer); reject(error); });
});
}
export function pairWithPiSupervise(pi: ExtensionAPI, workerIntercomId: string, goal: string): Promise<void> {
const events = (pi as unknown as { events: Events }).events;
return wait((resolve, reject) => events.emit(PAIR_EVENT, { version: 1, workerIntercomId, goal, resolve, reject }), "pi-supervise did not accept the visible-supervisor pairing request.");
}
export interface WorkerPiSupervise {
intercomId: string;
waitForPair(timeoutMs?: number): Promise<void>;
}
export function workerPiSupervise(pi: ExtensionAPI, timeoutMs = TIMEOUT_MS): Promise<WorkerPiSupervise> {
const events = (pi as unknown as { events: Events }).events;
let paired = false;
let resolvePair: (() => void) | undefined;
events.on(WORKER_PAIRED_EVENT, () => {
paired = true;
resolvePair?.();
});
return wait((resolve, reject) => {
let resolved = false;
const request = () => events.emit(WORKER_STATE_EVENT, (state: { intercomId?: string; paired?: boolean }) => {
if (resolved) return;
if (!state.intercomId) return reject(new Error("pi-supervise returned no worker intercom ID."));
if (state.paired) return reject(new Error("This worker is already paired with a supervisor. Stop that supervision before selecting Ready."));
resolved = true;
resolve({
intercomId: state.intercomId,
waitForPair: (pairTimeoutMs = timeoutMs) => paired ? Promise.resolve() : wait((pairResolve) => { resolvePair = pairResolve; }, "The visible supervisor did not pair with this worker.", pairTimeoutMs),
});
});
events.on(API_READY_EVENT, request);
request();
}, "pi-supervise did not publish this worker's intercom state.", timeoutMs);
}
+46 -28
View File
@@ -3,18 +3,19 @@ import { resolve } from "node:path";
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import { approvalPath, goalBlock, hashGoalBlock, repositoryState, verifyOutputPath, writeApproval } from "./approval.js";
import { pairWithPiSupervise } from "./supervise.js";
import { readyMailbox, workerViewsAfter, writeWorkerSteer } from "./mailbox.js";
const BOOTSTRAPPED = "pi-goals-visible-supervisor-v1";
const BOOTSTRAPPED = "pi-goals-visible-supervisor-v2";
const INITIAL_COMPACT_AT_TOKENS = 20_000;
const COMPACT_AT_TOKENS = 100_000;
const WRITER_TOOLS = new Set(["bash", "edit", "write", "multi_edit", "multiedit", "apply_patch", "notebook_edit", "edit_file", "write_file", "quick_edit", "target_edit"]);
interface SupervisorConfig {
workerSessionId: string;
workerIntercomId: string;
ownerSessionId: string;
planPath: string;
approvalId: string;
mailboxPath: string;
}
function result(text: string, isError = false) {
@@ -30,10 +31,10 @@ function requiredEnv(name: string): string {
function config(): SupervisorConfig {
return {
workerSessionId: requiredEnv("PI_GOALS_WORKER_ID"),
workerIntercomId: requiredEnv("PI_GOALS_WORKER_INTERCOM_ID"),
ownerSessionId: requiredEnv("PI_GOALS_OWNER_SESSION_ID"),
planPath: resolve(requiredEnv("PI_GOALS_PLAN_PATH")),
approvalId: requiredEnv("PI_GOALS_APPROVAL_ID"),
mailboxPath: resolve(requiredEnv("PI_GOALS_MAILBOX_PATH")),
};
}
@@ -67,9 +68,9 @@ function latestWorkerView(ctx: ExtensionContext): string | null {
}
function supervisorPrompt(settings: SupervisorConfig): string {
return `You are the visible pi-goals supervisor for ${settings.planPath}. You are a stronger, read-only reviewer. The other Pi session is the implementation worker and keeps the full conversation. You keep the high-level intent from the compacted planning conversation and pi-supervise worker views. The complete plan at ${settings.planPath} is the source of truth; read it directly after every compaction.
return `You are the visible pi-goals supervisor for ${settings.planPath}. You are a stronger, read-only reviewer. The other Pi session is the implementation worker and keeps the full conversation. You keep the high-level intent from the compacted planning conversation and worker views. The complete plan at ${settings.planPath} is the source of truth; read it directly after every compaction.
Use pi-supervise to inspect and steer the worker. Give one concrete instruction when work is incomplete. Do not edit files. For each open goal, inspect its exact plan block, repository state, cited evidence, and a saved nonempty verification-output file. When its discriminator is positively satisfied and the worker view says no work is active, call ApproveGoal with that repository-relative path. Then call steer and tell the worker to call CompleteGoal with the exact goal text. Do not call done until every plan goal is [x]. -- PI[gpt-5.6-sol]`;
Use SteerWorker to give one concrete instruction when work is incomplete. Do not edit files. For each open goal, inspect its exact plan block, repository state, cited evidence, and a saved nonempty verification-output file. When its discriminator is positively satisfied and the worker view says no work is active, call ApproveGoal with that repository-relative path. Then call SteerWorker and tell the worker to call CompleteGoal with the exact goal text. Do not call done until every plan goal is [x]. -- PI[Kimi K3]`;
}
export function isVisibleSupervisor(): boolean {
@@ -80,6 +81,15 @@ export function registerVisibleSupervisor(pi: ExtensionAPI): void {
const settings = config();
let compacting = false;
let bootstrapping = false;
let deliveredView = 0;
let viewTimer: ReturnType<typeof setInterval> | undefined;
const deliverWorkerViews = (): void => {
for (const view of workerViewsAfter(settings.mailboxPath, deliveredView)) {
deliveredView = view.sequence;
pi.sendUserMessage(view.text, { deliverAs: "followUp" });
}
};
const bootstrap = async (ctx: ExtensionContext): Promise<void> => {
if (bootstrapping) return;
@@ -87,9 +97,14 @@ export function registerVisibleSupervisor(pi: ExtensionAPI): void {
if (entries.some((entry: { type?: string; customType?: string }) => entry.type === "custom" && entry.customType === BOOTSTRAPPED)) return;
bootstrapping = true;
try {
await pairWithPiSupervise(pi, settings.workerIntercomId, settings.planPath);
pi.appendEntry(BOOTSTRAPPED, { version: 1, workerSessionId: settings.workerSessionId, planPath: settings.planPath });
pi.sendUserMessage("Supervision is paired. Inspect the worker and give its next concrete instruction.");
const active = pi.getActiveTools();
pi.setActiveTools(active.filter((tool) => !WRITER_TOOLS.has(tool.toLowerCase())));
const writers = pi.getActiveTools().filter((tool) => WRITER_TOOLS.has(tool.toLowerCase()));
if (writers.length) throw new Error(`Could not remove supervisor writing tools: ${writers.join(", ")}`);
pi.appendEntry(BOOTSTRAPPED, { version: 2, workerSessionId: settings.workerSessionId, planPath: settings.planPath });
readyMailbox(settings.mailboxPath);
viewTimer = setInterval(deliverWorkerViews, 1_000);
deliverWorkerViews();
} catch (error) {
ctx.ui.notify(`Supervisor startup failed: ${error instanceof Error ? error.message : String(error)}`, "error");
}
@@ -119,16 +134,15 @@ export function registerVisibleSupervisor(pi: ExtensionAPI): void {
pi.on("session_start", async (_event, ctx) => {
setImmediate(() => { bootstrapAfterInitialCompaction(ctx); });
});
pi.on("before_agent_start", async (_event, ctx) => {
return { systemPrompt: `${ctx.getSystemPrompt()}\n\n${supervisorPrompt(settings)}` };
pi.on("session_shutdown", async () => {
if (viewTimer) clearInterval(viewTimer);
});
pi.on("before_agent_start", async (_event, ctx) => ({ systemPrompt: `${ctx.getSystemPrompt()}\n\n${supervisorPrompt(settings)}` }));
pi.on("agent_settled", async (_event, ctx) => {
if (compacting || (ctx.getContextUsage()?.tokens ?? 0) < COMPACT_AT_TOKENS) return;
compacting = true;
ctx.compact({
customInstructions: `Keep the user's high-level intent, current plan state, unresolved risks, approval decisions, and the supervisor's own concise findings. Remove old worker views and implementation detail.`,
customInstructions: `Keep the user's high-level intent, current plan state, unresolved risks, approval decisions, and the supervisor's own concise findings. Remove old worker views and implementation detail. The canonical plan remains ${settings.planPath}.`,
onComplete: () => {
compacting = false;
ctx.ui.notify("Supervisor context compacted at 100k tokens.", "info");
@@ -140,6 +154,20 @@ export function registerVisibleSupervisor(pi: ExtensionAPI): void {
});
});
pi.registerTool({
name: "SteerWorker",
label: "Steer worker",
executionMode: "sequential",
description: "Write one concrete instruction for the implementation worker.",
parameters: Type.Object({ instruction: Type.String({ description: "Concrete next instruction for the worker." }) }),
async execute(_id, params) {
const instruction = params.instruction.trim();
if (!instruction) return result("A worker instruction cannot be empty.", true);
const steer = writeWorkerSteer(settings.mailboxPath, instruction);
return result(`Worker instruction ${steer.sequence} recorded.`);
},
});
pi.registerTool({
name: "ApproveGoal",
label: "Approve goal",
@@ -171,20 +199,10 @@ export function registerVisibleSupervisor(pi: ExtensionAPI): void {
if (!verifiedOutput) return result("Cannot approve without a nonempty repository-relative verification-output file.", true);
const path = approvalPath(ctx.cwd, settings.ownerSessionId, params.goal);
writeApproval(path, {
version: 3,
verdict: "accept",
approvalId: settings.approvalId,
goal: params.goal,
planPath: settings.planPath,
goalBlockHash: hashGoalBlock(block),
repoRoot: repository.repoRoot,
head: repository.head,
tree: repository.tree,
cleanWorktree: true,
inspected: { plan: true, repository: true, evidence: true, verifyOutput: true },
verifyOutputPath: verifiedOutput,
supervisor: { sessionId: ctx.sessionManager.getSessionId(), runId: null },
timestamp: new Date().toISOString(),
version: 3, verdict: "accept", approvalId: settings.approvalId, goal: params.goal, planPath: settings.planPath,
goalBlockHash: hashGoalBlock(block), repoRoot: repository.repoRoot, head: repository.head, tree: repository.tree,
cleanWorktree: true, inspected: { plan: true, repository: true, evidence: true, verifyOutput: true }, verifyOutputPath: verifiedOutput,
supervisor: { sessionId: ctx.sessionManager.getSessionId(), runId: null }, timestamp: new Date().toISOString(),
});
return result(`Approval recorded for "${params.goal}". Now steer the worker to call CompleteGoal.`);
},
+47
View File
@@ -0,0 +1,47 @@
export interface SessionBlock {
type?: string;
id?: string;
name?: string;
text?: string;
}
export interface SessionMessage {
role?: string;
content?: string | SessionBlock[];
toolCallId?: string;
}
export interface SessionEntry {
type?: string;
summary?: string;
message?: SessionMessage;
}
function text(message: SessionMessage): string {
if (typeof message.content === "string") return message.content;
return (message.content ?? []).flatMap((block) => {
if (block.type === "text" && block.text) return [block.text];
if (block.type === "toolCall") return [`tool: ${block.name ?? "unknown"}`];
return [];
}).join("\n");
}
function outstandingTools(entries: SessionEntry[]): string[] {
const calls = new Map<string, string>();
const results = new Set<string>();
for (const entry of entries) {
for (const block of Array.isArray(entry.message?.content) ? entry.message.content : []) {
if (block.type === "toolCall" && block.id) calls.set(block.id, block.name ?? "unknown");
}
if (entry.message?.role === "toolResult" && entry.message.toolCallId) results.add(entry.message.toolCallId);
}
return [...calls].filter(([id]) => !results.has(id)).map(([, name]) => name);
}
export function workerView(entries: SessionEntry[], reason: "ready" | "settled" | "turns" | "interval"): string {
const summary = [...entries].reverse().find((entry) => entry.type === "compaction" && entry.summary)?.summary;
const recent = entries.flatMap((entry) => entry.type === "message" && entry.message ? [text(entry.message)] : []).filter(Boolean).slice(-12).join("\n\n").slice(-12_000);
const outstanding = outstandingTools(entries);
const state = reason === "settled" ? "stopped" : reason === "ready" ? "is ready to begin" : "is still working";
return `The worker ${state}.\n\nreview trigger: ${reason}\ntool calls with no result: ${outstanding.join(", ") || "none"}\n\n${summary ? `last compaction summary:\n${summary}\n\n` : ""}recent worker transcript:\n${recent || "none"}`;
}
+20 -26
View File
@@ -1,11 +1,11 @@
import { execFileSync } from "node:child_process";
import { EventEmitter } from "node:events";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { afterEach, describe, expect, it, vi } from "vitest";
import { approvalPath, goalBlock, hashGoalBlock, repositoryState, writeApproval } from "../src/approval.js";
import { writeWorkerSteer } from "../src/mailbox.js";
const openSupervisorPane = vi.fn(async () => "pane-2");
const closeSupervisorPane = vi.fn(async () => undefined);
@@ -33,6 +33,7 @@ function setup(selectChoices: Array<string | undefined>, editorChoices: Array<st
getSessionId: () => "session-a",
getSessionFile: () => join(cwd, "session.jsonl"),
getEntries: () => entries,
getBranch: () => [],
},
ui: {
theme: { fg: (_kind: string, text: string) => text },
@@ -43,14 +44,8 @@ function setup(selectChoices: Array<string | undefined>, editorChoices: Array<st
editor: async () => editorChoices.shift(),
},
};
const events = new EventEmitter();
events.on("pi-supervise:worker-state:v1", (reply) => reply({ intercomId: "worker-intercom" }));
openSupervisorPane.mockImplementation(async () => {
queueMicrotask(() => events.emit("pi-supervise:worker-paired:v1", { supervisorIntercomId: "supervisor-intercom" }));
return "pane-2";
});
openSupervisorPane.mockImplementation(async () => "pane-2");
const pi = {
events,
registerCommand: (name: string, command: any) => commands.set(name, command),
on: (name: string, handler: any) => hooks.set(name, handler),
appendEntry: (customType: string, data: unknown) => entries.push({ type: "custom", customType, data }),
@@ -60,7 +55,7 @@ function setup(selectChoices: Array<string | undefined>, editorChoices: Array<st
sendUserMessage: (content: string) => messages.push({ content }),
};
piGoalsExtension(pi as unknown as ExtensionAPI);
return { commands, ctx, cwd, entries, events, hooks, messages, notifications, tools };
return { commands, ctx, cwd, entries, hooks, messages, notifications, tools };
}
function writePlan(cwd: string, content: string): string {
@@ -110,8 +105,8 @@ describe("/goals flow", () => {
cwd: flow.cwd,
sourceSessionFile: join(flow.cwd, "session.jsonl"),
workerSessionId: "session-a",
workerIntercomId: "worker-intercom",
planPath,
mailboxPath: expect.stringContaining(".pi/goals-supervision/session-a/"),
}));
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "working", supervisorPaneId: "pane-2" });
expect(flow.messages.at(-1)?.content).toBe("The plan is approved. Begin implementation as the worker.");
@@ -123,35 +118,34 @@ describe("/goals flow", () => {
}
});
it("returns to planning when the worker is already paired", async () => {
it("starts work only after the supervisor launcher resolves", async () => {
const flow = setup(["Ready"]);
try {
flow.events.removeAllListeners("pi-supervise:worker-state:v1");
flow.events.on("pi-supervise:worker-state:v1", (reply) => reply({ intercomId: "worker-intercom", paired: true }));
let ready: (() => void) | undefined;
openSupervisorPane.mockImplementationOnce(() => new Promise((resolve) => { ready = () => resolve("pane-2"); }));
await flow.commands.get("goals").handler("make the file", flow.ctx);
approvedPlan(flow.cwd);
await flow.hooks.get("agent_settled")({}, flow.ctx);
expect(openSupervisorPane).not.toHaveBeenCalled();
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning", supervisorPaneId: null });
expect(flow.notifications.at(-1)).toContain("already paired");
const starting = flow.hooks.get("agent_settled")({}, flow.ctx);
await new Promise((resolve) => setImmediate(resolve));
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning" });
ready!();
await starting;
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "working", supervisorPaneId: "pane-2" });
} finally {
rmSync(flow.cwd, { recursive: true, force: true });
}
});
it("waits for the worker's real paired acknowledgement before beginning work", async () => {
it("delivers a mailbox instruction to the worker", async () => {
const flow = setup(["Ready"]);
try {
openSupervisorPane.mockImplementationOnce(async () => "pane-2");
await flow.commands.get("goals").handler("make the file", flow.ctx);
approvedPlan(flow.cwd);
const ready = flow.hooks.get("agent_settled")({}, flow.ctx);
await new Promise((resolve) => setImmediate(resolve));
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning" });
expect(flow.messages.some((message) => message.content === "The plan is approved. Begin implementation as the worker.")).toBe(false);
flow.events.emit("pi-supervise:worker-paired:v1", { supervisorIntercomId: "supervisor-intercom" });
await ready;
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "working", supervisorPaneId: "pane-2" });
await flow.hooks.get("agent_settled")({}, flow.ctx);
const mailboxPath = (flow.entries.at(-1)?.data as { mailboxPath: string }).mailboxPath;
writeWorkerSteer(mailboxPath, "Run the focused test.");
await flow.hooks.get("agent_settled")({}, flow.ctx);
expect(flow.messages.some((message) => message.content === "[supervisor] Run the focused test.")).toBe(true);
} finally {
rmSync(flow.cwd, { recursive: true, force: true });
}
+23 -16
View File
@@ -2,18 +2,18 @@ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { closeSupervisorPane, openSupervisorPane, supervisorCommand } from "../src/herdr.js";
import { closeSupervisorPane, openSupervisorPane, supervisorCommand, waitForSupervisorReady } from "../src/herdr.js";
import { createMailbox, readyMailbox } from "../src/mailbox.js";
function input() {
function input(mailboxPath = "/repo/.pi/goals-supervision/worker/approval") {
return {
cwd: "/repo",
sourceSessionFile: "/sessions/worker.jsonl",
workerSessionId: "worker-12345678",
workerIntercomId: "intercom-12345678",
planPath: "/repo/.pi/plan/worker-v1.md",
approvalId: "approval-1",
mailboxPath,
extensionPath: "/repo/src/index.ts",
superviseExtensionPath: null,
model: "provider/supervisor",
};
}
@@ -21,22 +21,27 @@ function input() {
afterEach(() => vi.unstubAllEnvs());
describe("supervisor pane command", () => {
it("forks the planning session with an explicit supervisor role and model", () => {
it("forks the planning session with only pi-goals and its mailbox", () => {
const command = supervisorCommand(input());
expect(command).toContain("'PI_GOALS_ROLE=supervisor'");
expect(command).toContain("'PI_GOALS_WORKER_INTERCOM_ID=intercom-12345678'");
expect(command).toContain("'pi' '--no-extensions' '-e' 'npm:pi-intercom' '-e' 'npm:@wassname2/pi-supervise@0.0.4' '-e' '/repo/src/index.ts'");
expect(command).toContain("'PI_GOALS_MAILBOX_PATH=/repo/.pi/goals-supervision/worker/approval'");
expect(command).toContain("'pi' '--no-extensions' '-e' '/repo/src/index.ts'");
expect(command).toContain("'--fork' '/sessions/worker.jsonl'");
expect(command).toContain("'--model' 'provider/supervisor'");
expect(command).not.toContain("Initialize supervision startup.");
expect(command).not.toContain("pi-subagents");
expect(command).not.toContain("pi-supervise");
expect(command).not.toContain("pi-intercom");
});
it("uses the loaded pi-supervise extension before the npm fallback", () => {
const loaded = { ...input(), superviseExtensionPath: "/repo/vendor/pi-supervise/src/index.ts" };
expect(supervisorCommand(loaded)).toContain("'-e' '/repo/vendor/pi-supervise/src/index.ts'");
vi.stubEnv("PI_GOALS_SUPERVISE_EXTENSION", "/repo/override/pi-supervise/src/index.ts");
expect(supervisorCommand(loaded)).toContain("'-e' '/repo/override/pi-supervise/src/index.ts'");
it("waits for an explicit mailbox readiness receipt", async () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-herdr-"));
try {
const mailbox = createMailbox(cwd, "worker", "approval", join(cwd, "plan.md"));
await expect(waitForSupervisorReady(mailbox.path, 10)).rejects.toThrow("did not become ready");
readyMailbox(mailbox.path);
await expect(waitForSupervisorReady(mailbox.path, 10)).resolves.toBeUndefined();
} finally {
rmSync(cwd, { recursive: true, force: true });
}
});
it("accepts Herdr's text version output and stale pane cleanup", async () => {
@@ -53,10 +58,12 @@ exit 2
vi.stubEnv("HERDR_ENV", "1");
vi.stubEnv("HERDR_BIN_PATH", bin);
try {
await expect(openSupervisorPane(input())).resolves.toBe("new-pane");
const mailbox = createMailbox(cwd, "worker", "approval", join(cwd, "plan.md"));
readyMailbox(mailbox.path);
await expect(openSupervisorPane({ ...input(mailbox.path), cwd })).resolves.toBe("new-pane");
await expect(closeSupervisorPane("new-pane")).resolves.toBeUndefined();
vi.stubEnv("HERDR_SMOKE_RUN_FAIL", "1");
await expect(openSupervisorPane(input())).rejects.toThrow("run failed");
await expect(openSupervisorPane({ ...input(mailbox.path), cwd })).rejects.toThrow("run failed");
} finally {
rmSync(cwd, { recursive: true, force: true });
}
-47
View File
@@ -1,47 +0,0 @@
import { EventEmitter } from "node:events";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { describe, expect, it } from "vitest";
import { workerPiSupervise } from "../src/supervise.js";
const API_READY = "pi-supervise:api-ready:v1";
const WORKER_STATE = "pi-supervise:worker-state:v1";
const WORKER_PAIRED = "pi-supervise:worker-paired:v1";
function pi(events: EventEmitter): ExtensionAPI {
return { events } as unknown as ExtensionAPI;
}
describe("pi-supervise worker API", () => {
it("discovers pi-supervise when it loads after pi-goals", async () => {
const events = new EventEmitter();
const worker = workerPiSupervise(pi(events));
events.on(WORKER_STATE, (reply) => reply({ intercomId: "worker-id", paired: false }));
events.emit(API_READY);
expect((await worker).intercomId).toBe("worker-id");
});
it("discovers an already-loaded pi-supervise and accepts duplicate paired events once", async () => {
const events = new EventEmitter();
events.on(WORKER_STATE, (reply) => reply({ intercomId: "worker-id", paired: false }));
const worker = await workerPiSupervise(pi(events));
let acknowledgements = 0;
const paired = worker.waitForPair().then(() => { acknowledgements += 1; });
events.emit(WORKER_PAIRED, { supervisorIntercomId: "supervisor-id" });
events.emit(WORKER_PAIRED, { supervisorIntercomId: "supervisor-id" });
await paired;
expect(acknowledgements).toBe(1);
});
it("rejects Ready when another supervisor already owns the worker", async () => {
const events = new EventEmitter();
events.on(WORKER_STATE, (reply) => reply({ intercomId: "worker-id", paired: true }));
await expect(workerPiSupervise(pi(events))).rejects.toThrow("already paired");
});
it("times out when the visible supervisor never pairs", async () => {
const events = new EventEmitter();
events.on(WORKER_STATE, (reply) => reply({ intercomId: "worker-id", paired: false }));
const worker = await workerPiSupervise(pi(events), 1);
await expect(worker.waitForPair()).rejects.toThrow("did not pair");
});
});
+29 -88
View File
@@ -5,69 +5,59 @@ import { join } from "node:path";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { afterEach, describe, expect, it, vi } from "vitest";
import { approvalPath } from "../src/approval.js";
import { createMailbox, supervisorReady, workerSteersAfter } from "../src/mailbox.js";
import { registerVisibleSupervisor } from "../src/supervisor-session.js";
function setup(cwd: string, planPath: string, tokens: number | null = 10, onCompact: (options: any) => void = (options) => options.onComplete()) {
const mailbox = createMailbox(cwd, "worker-session", "approval-1", planPath);
vi.stubEnv("PI_GOALS_WORKER_ID", "worker-session");
vi.stubEnv("PI_GOALS_WORKER_INTERCOM_ID", "worker-intercom");
vi.stubEnv("PI_GOALS_OWNER_SESSION_ID", "worker-session");
vi.stubEnv("PI_GOALS_PLAN_PATH", planPath);
vi.stubEnv("PI_GOALS_APPROVAL_ID", "approval-1");
vi.stubEnv("PI_GOALS_MAILBOX_PATH", mailbox.path);
const hooks = new Map<string, any>();
const tools = new Map<string, any>();
const entries: any[] = [];
const paired: Array<{ workerIntercomId: string; goal: string }> = [];
const messages: string[] = [];
let branch: any[] = [];
let activeTools = ["read", "grep", "bash", "write", "edit"];
const ctx = {
cwd,
getSystemPrompt: () => "base",
getContextUsage: () => tokens === null ? undefined : ({ tokens }),
compact: vi.fn(onCompact),
sessionManager: {
getEntries: () => entries,
getBranch: () => branch,
getSessionId: () => "supervisor-session",
},
sessionManager: { getEntries: () => entries, getBranch: () => branch, getSessionId: () => "supervisor-session" },
ui: { notify: vi.fn() },
};
const pi = {
events: {
on() {},
emit(name: string, request: any) {
if (name !== "pi-supervise:pair:v1") return;
paired.push({ workerIntercomId: request.workerIntercomId, goal: request.goal });
request.resolve();
},
},
on: (name: string, handler: any) => hooks.set(name, handler),
registerTool: (tool: any) => tools.set(tool.name, tool),
appendEntry: (customType: string, data: unknown) => entries.push({ type: "custom", customType, data }),
sendUserMessage: (message: string) => messages.push(message),
getActiveTools: () => activeTools,
setActiveTools: (next: string[]) => { activeTools = next; },
};
registerVisibleSupervisor(pi as unknown as ExtensionAPI);
return { branch: (value: any[]) => { branch = value; }, ctx, entries, hooks, messages, paired, tools };
return { activeTools: () => activeTools, branch: (value: any[]) => { branch = value; }, ctx, entries, hooks, mailbox, messages, tools };
}
afterEach(() => vi.unstubAllEnvs());
describe("visible supervisor session", () => {
it("pairs from session startup before asking the supervisor to work", async () => {
it("writes readiness only after removing writing tools", async () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-supervisor-"));
try {
const runtime = setup(cwd, join(cwd, ".pi/plan/worker-v1.md"));
await runtime.hooks.get("session_start")({}, runtime.ctx);
await new Promise((resolve) => setImmediate(resolve));
expect(runtime.ctx.compact).not.toHaveBeenCalled();
expect(runtime.entries.at(-1)).toMatchObject({ customType: "pi-goals-visible-supervisor-v1" });
expect(runtime.paired).toEqual([{ workerIntercomId: "worker-intercom", goal: join(cwd, ".pi/plan/worker-v1.md") }]);
expect(runtime.messages).toEqual(["Supervision is paired. Inspect the worker and give its next concrete instruction."]);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
expect(supervisorReady(runtime.mailbox.path)).toBe(true);
expect(runtime.activeTools()).toEqual(["read", "grep"]);
expect(runtime.entries.at(-1)).toMatchObject({ customType: "pi-goals-visible-supervisor-v2" });
} finally { rmSync(cwd, { recursive: true, force: true }); }
});
it("compacts a large planning fork before pairing", async () => {
it("compacts a large planning fork before writing readiness", async () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-supervisor-"));
try {
let complete: (() => void) | undefined;
@@ -75,41 +65,31 @@ describe("visible supervisor session", () => {
await runtime.hooks.get("session_start")({}, runtime.ctx);
await new Promise((resolve) => setImmediate(resolve));
expect(runtime.ctx.compact).toHaveBeenCalledOnce();
expect(runtime.paired).toHaveLength(0);
expect(supervisorReady(runtime.mailbox.path)).toBe(false);
complete!();
await new Promise((resolve) => setImmediate(resolve));
expect(runtime.paired).toHaveLength(1);
expect(runtime.messages).toEqual(["Supervision is paired. Inspect the worker and give its next concrete instruction."]);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
expect(supervisorReady(runtime.mailbox.path)).toBe(true);
} finally { rmSync(cwd, { recursive: true, force: true }); }
});
it("does not start work when initial compaction fails", async () => {
it("does not become ready when initial compaction fails", async () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-supervisor-"));
try {
const runtime = setup(cwd, join(cwd, ".pi/plan/worker-v1.md"), null, (options) => options.onError(new Error("offline")));
await runtime.hooks.get("session_start")({}, runtime.ctx);
await new Promise((resolve) => setImmediate(resolve));
expect(runtime.ctx.compact).toHaveBeenCalledOnce();
expect(runtime.paired).toHaveLength(0);
expect(supervisorReady(runtime.mailbox.path)).toBe(false);
expect(runtime.ctx.ui.notify).toHaveBeenCalledWith("Supervisor startup compaction failed: offline", "error");
} finally {
rmSync(cwd, { recursive: true, force: true });
}
} finally { rmSync(cwd, { recursive: true, force: true }); }
});
it("does not pair twice across session startup and later turns", async () => {
it("writes a durable worker instruction", async () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-supervisor-"));
try {
const runtime = setup(cwd, join(cwd, ".pi/plan/worker-v1.md"));
await runtime.hooks.get("session_start")({}, runtime.ctx);
await new Promise((resolve) => setImmediate(resolve));
await runtime.hooks.get("before_agent_start")({}, runtime.ctx);
expect(runtime.paired).toHaveLength(1);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
const runtime = setup(cwd, join(cwd, "plan.md"));
const steered = await runtime.tools.get("SteerWorker").execute("id", { instruction: "Run the saved verification." });
expect(steered.isError).toBe(false);
expect(workerSteersAfter(runtime.mailbox.path, 0)).toMatchObject([{ sequence: 1, instruction: "Run the saved verification." }]);
} finally { rmSync(cwd, { recursive: true, force: true }); }
});
it("records approval only from a stopped view with evidence and no active work", async () => {
@@ -124,49 +104,10 @@ describe("visible supervisor session", () => {
execFileSync("mkdir", ["-p", join(cwd, ".pi/plan")]);
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [ ] goal: make the file\n - discriminator: output exists\n - evidence:\n - `result.txt`: contains ok\n\n## Log\n");
const runtime = setup(cwd, planPath);
runtime.branch([{
type: "message",
message: { role: "user", content: [{ type: "text", text: "The worker stopped.\n\ntool calls with no result: none\nchild pi processes still running: none" }] },
}]);
const approved = await runtime.tools.get("ApproveGoal").execute("id", {
goal: "make the file",
verifyOutputPath: "verify.txt",
}, undefined, undefined, runtime.ctx);
runtime.branch([{ type: "message", message: { role: "user", content: [{ type: "text", text: "The worker stopped.\n\ntool calls with no result: none" }] } }]);
const approved = await runtime.tools.get("ApproveGoal").execute("id", { goal: "make the file", verifyOutputPath: "verify.txt" }, undefined, undefined, runtime.ctx);
expect(approved.isError).toBe(false);
expect(existsSync(approvalPath(cwd, "worker-session", "make the file"))).toBe(true);
const missingOutput = await runtime.tools.get("ApproveGoal").execute("id", {
goal: "make the file", verifyOutputPath: "missing.txt",
}, undefined, undefined, runtime.ctx);
expect(missingOutput.isError).toBe(true);
expect(missingOutput.content[0].text).toContain("verification-output");
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [ ] goal: make the file\n - evidence:\n - \n - tasks:\n - write result.txt\n");
const missingEvidence = await runtime.tools.get("ApproveGoal").execute("id", {
goal: "make the file", verifyOutputPath: "verify.txt",
}, undefined, undefined, runtime.ctx);
expect(missingEvidence.isError).toBe(true);
expect(missingEvidence.content[0].text).toContain("nonblank evidence entry");
} finally {
rmSync(cwd, { recursive: true, force: true });
}
});
it("rejects approval while the worker view has an unfinished tool call", async () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-supervisor-"));
try {
const planPath = join(cwd, "plan.md");
writeFileSync(planPath, "1. [ ] goal: wait\n - evidence:\n - result\n");
const runtime = setup(cwd, planPath);
runtime.branch([{
type: "message",
message: { role: "user", content: [{ type: "text", text: "The worker stopped.\n\ntool calls with no result: bash\nchild pi processes still running: none" }] },
}]);
const rejected = await runtime.tools.get("ApproveGoal").execute("id", {
goal: "wait", verifyOutputPath: "verify.txt",
}, undefined, undefined, runtime.ctx);
expect(rejected.isError).toBe(true);
expect(rejected.content[0].text).toContain("bash");
} finally {
rmSync(cwd, { recursive: true, force: true });
}
} finally { rmSync(cwd, { recursive: true, force: true }); }
});
});