3 Commits
13 changed files with 1196 additions and 392 deletions
+13 -5
View File
@@ -122,13 +122,13 @@ resync-after-compaction from [tmonk/pi-goal-x](https://github.com/tmonk/pi-goal-
## Install
Requires Herdr. Includes [edxeth/pi-subagents](https://github.com/edxeth/pi-subagents), pi-intercom and pi-schedule-prompt.
Requires Herdr. Includes [nicobailon/pi-subagents](https://github.com/nicobailon/pi-subagents) 0.66.0, pi-intercom and pi-schedule-prompt.
```bash
pi install git:github.com/wassname/pi-goals
```
Copy [`agents/goals-worker.md`](agents/goals-worker.md) into `~/.pi/agent/agents/`, then start a fresh Pi session. Use one pi-goals installation and disable separately installed copies of its bundled companions; duplicate scheduler instances send duplicate prompts.
Start a fresh Pi session. No worker agent file is needed: `OpenGoalWorker` uses Nico's public `project.open` surface, then the peer explicitly attaches with `AttachGoalPlan`. Use one pi-goals installation and disable separately installed copies of its bundled companions; duplicate scheduler instances send duplicate prompts.
The bundled pi-schedule-prompt 0.4.1 reads project schedules even when Pi project trust is declined. Until that upstream issue is fixed, use this bundle only in repositories you trust.
@@ -138,8 +138,6 @@ For development, register the checkout so workers also discover its extensions:
git clone https://github.com/wassname/pi-goals
cd pi-goals && npm install
pi install .
mkdir -p ~/.pi/agent/agents
cp agents/goals-worker.md ~/.pi/agent/agents/
pi
```
@@ -149,7 +147,17 @@ pi
/goals
```
`/goals` shows actions for the current mode. Drafts offer Edit, Discuss and Approve. Discuss returns to chat and waits for your input. Menu New asks for optional instructions before creating a plan; submit blank to use the conversation, or cancel to leave things unchanged. Typed `/goals new <instructions>` still starts directly. Quit (`exit` or `clear`) leaves the original plan unchanged, removes this session's goal check-in, and clears goal state without a model call. Matching check-in names with missing or different session bindings are left unchanged with a warning. Worker processes are unchanged; manage them through `/subagents`. New creates a separate draft without overwriting earlier plans, named `.pi/plan/<last-six-session-characters>-vN.md` using the next version after existing files. The title stays inside the plan; old files are not renamed. The widget shows a plain `✓` and the relative plan path (the fallback for unverified terminal links).
`/goals` shows actions for the current mode. Drafts offer Edit, Discuss and Approve. Discuss returns to chat and waits for your input. Menu New asks for optional instructions before creating a plan; submit blank to use the conversation, or cancel to leave things unchanged. Typed `/goals new <instructions>` still starts directly. Quit (`exit` or `clear`) leaves the original plan unchanged, removes this session's goal check-in, and clears goal state without a model call. Matching check-in names with missing or different session bindings are left unchanged with a warning. Worker processes are unchanged; inspect their native panes and use their exact Intercom identities for steering. New creates a separate draft without overwriting earlier plans, named `.pi/plan/<last-six-session-characters>-vN.md` using the next version after existing files. The title stays inside the plan; old files are not renamed. The widget shows a plain `✓` and the relative plan path for inside-project plans. External plans use the filename with an `(external)` marker; `/goals status` keeps the full location. These are plain labels, not terminal links.
### Native worker lifecycle and limits
The parent and worker keep separate native conversations. Worker attachment and stop notices use stock Intercom extension channels; assignments, reports and corrections remain visible Pi messages. A pane-open receipt, idle state or delivery receipt does not approve a goal.
`OpenGoalWorker` opens a blank peer and waits for verified Intercom capability before sending work. For independent work after review, use `action: "fresh"` with the exact inspected `reviewedThrough` entry ID. This uses Pi's new session in the same pane; the previous conversation stays in saved history. Revisions still use the same Intercom session. Drafts, pending input, changed history and a local worker pause block replacement.
For recovery, use `action: "recover"`, `writersStopped: true` and the owned saved session after inspecting other writers. Recovery restores context without replaying a task or changing its model. A prospective session path is not durable history. A live binding without a responsive, capable Pi peer remains unconfirmed; no shell restart or second backend is invented.
Without an explicit model preference, a new context uses the current Pi profile's normal defaults. A session-local human choice remains in that earlier session's history; recovery retains it. Requested-model automation currently fails closed: Pi's asynchronous public setter lacks a guard against overwriting a concurrent human selection. No fallback task is launched and no stale preference is reapplied. This model-selection requirement remains unfinished. — Pi/OpenAI
## Context delivery
-25
View File
@@ -1,25 +0,0 @@
---
name: goals-worker
description: Implement the approved goal, save actual verification evidence, and report to the main-chat supervisor.
mode: interactive
async: true
session-mode: lineage-only
extensions: all
tools: all
skills: all
trust-project: true
inherit-append-system: true
auto-exit: false
parent-close-policy: continue
spawning: false
---
Implement only the goal delegated by the parent. Read the supplied plan and applicable AGENTS.md and skills. Preserve unrelated work. Use normal tools and extensions; this is not a stripped-down Pi profile.
Save the actual deliverable and verification output. Verify the outcome, not merely that a command ran. Ignored and uncommitted files are valid evidence. Do not clean or commit unrelated files to satisfy a Git-state gate.
Call AttachGoalPlan with the supplied absolute plan path. Send the supervisor an Intercom report with the artifact paths, verification performed, observed result, remaining uncertainty and any blocker. Use the exact supervisor session ID supplied in the task; confirm it in Intercom's session list. Investigate failures before declaring yourself blocked. Respect explicit user pauses. Do not approve your own goal or launch another writer. Completion approval belongs to the parent.
This worker uses a clean model context linked to the parent, not a full transcript fork. The parent supplies the approved plan and task. Send completion through Intercom and leave this pane open for follow-up messages. Do not call caller_ping, exit or shutdown: an unsent editor draft may exist even though it is absent from model context. Saved-session resume applies only after this session has stopped. If the user takes over interactively, follow their direction.
Prepared by Pi/OpenAI for pi-goals.
+692 -118
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -28,7 +28,6 @@
},
"files": [
"src",
"agents",
"README.md"
],
"publishConfig": {
@@ -56,7 +55,7 @@
"pi": {
"extensions": [
"./src/index.ts",
"./node_modules/pi-subagents/src/index.ts",
"./node_modules/pi-subagents/index.ts",
"./node_modules/pi-intercom/index.ts",
"./node_modules/pi-schedule-prompt/src/index.ts"
],
@@ -66,7 +65,7 @@
]
},
"dependencies": {
"pi-subagents": "git+https://github.com/edxeth/pi-subagents.git#953c6f6d2fc7d8a5c956c30cd77c51bad697c2a4",
"pi-subagents": "0.66.0",
"pi-intercom": "0.13.0",
"pi-schedule-prompt": "0.4.1"
},
+4 -7
View File
@@ -6,12 +6,11 @@ import { execFileSync } from 'node:child_process';
import { homedir, tmpdir } from 'node:os';
const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const sdkRoot = process.argv[2];
const noSandbox = process.argv.includes('--no-sandbox');
if (!sdkRoot) throw new Error('Usage: node scripts/prepare-trial.mjs INSTALLED_PI_ROOT');
const revision = execFileSync('git', ['-C', repo, 'rev-parse', 'HEAD'], {encoding:'utf8'}).trim();
const root = mkdtempSync(join(tmpdir(), 'goals-edxeth-trial-'));
const root = mkdtempSync(join(tmpdir(), 'goals-nico-trial-'));
const cwd = join(root, 'project'); const agentDir = join(root, 'agent');
mkdirSync(cwd); mkdirSync(agentDir, {mode:0o700}); mkdirSync(join(agentDir,'agents'));
mkdirSync(cwd); mkdirSync(agentDir, {mode:0o700});
const sourceAgent = process.env.PI_CODING_AGENT_DIR || join(homedir(),'.pi','agent');
const sourceSettings = JSON.parse(readFileSync(join(sourceAgent,'settings.json'),'utf8'));
const sdk = await import(pathToFileURL(join(sdkRoot,'dist/index.js')).href);
@@ -23,13 +22,11 @@ for (const p of retained) if (!p.installedPath) throw new Error(`Missing install
writeFileSync(join(agentDir,'settings.json'), JSON.stringify({...sourceSettings, packages:[...retained.map((p)=>p.installedPath), repo]},null,2));
// Private copies, not symlinks: a trial OAuth refresh must not write the active auth file.
for (const file of ['auth.json','models.json']) if (existsSync(join(sourceAgent,file))) copyFileSync(join(sourceAgent,file),join(agentDir,file));
const workerDefinition = readFileSync(join(repo,'agents/goals-worker.md'),'utf8');
writeFileSync(join(agentDir,'agents/goals-worker.md'), noSandbox ? workerDefinition.replace('mode: interactive', 'mode: interactive\nflags: --no-sandbox') : workerDefinition);
execFileSync('git',['init','--quiet',cwd]);
writeFileSync(join(cwd,'AGENTS.md'), 'Isolated functional trial. Work only in this project. Do not operate other Herdr panes, use live research sessions, or change global settings. Preserve evidence. The main chat supervises; the goals-worker implements.\n');
writeFileSync(join(cwd,'.gitignore'), 'evidence/\n');
const manifest={root,cwd,agentDir,repo,revision,noSandbox,retainedPackages:retained.map((p)=>p.source),replacedPackages:packages.filter((p)=>!retained.includes(p)).map((p)=>p.source)};
const manifest={root,cwd,agentDir,repo,revision,retainedPackages:retained.map((p)=>p.source),replacedPackages:packages.filter((p)=>!retained.includes(p)).map((p)=>p.source)};
writeFileSync(join(root,'manifest.json'),JSON.stringify(manifest,null,2));
const quote=(s)=>`'${s.replaceAll("'", "'\\''")}'`;
writeFileSync(join(root,'start.zsh'), `#!/usr/bin/env zsh\nset -e\ncd ${quote(cwd)}\nexport PI_CODING_AGENT_DIR=${quote(agentDir)}\nexport PI_SUBAGENT_MUX=herdr\nexport PI_ORCHESTRATOR_MODE=0\nexec pi --approve${noSandbox ? ' --no-sandbox' : ''}\n`,{mode:0o700});
writeFileSync(join(root,'start.zsh'), `#!/usr/bin/env zsh\nset -e\ncd ${quote(cwd)}\nexport PI_CODING_AGENT_DIR=${quote(agentDir)}\nexec pi\n`,{mode:0o700});
console.log(JSON.stringify({root,cwd,agentDir,start:join(root,'start.zsh'),manifest:join(root,'manifest.json')},null,2));
+256 -72
View File
@@ -1,9 +1,11 @@
// Pi/OpenAI: Plan and supervise in the main chat; delegate implementation to a visible worker.
import { createHash } from "node:crypto";
import { createHash, randomUUID } from "node:crypto";
import { type FSWatcher, mkdirSync, readdirSync, readFileSync, watch, writeFileSync } from "node:fs";
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { type ExtensionAPI, type ExtensionContext, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
import { INTERCOM_EXTENSION_REGISTER_EVENT, type IntercomExtensionChannel, type IntercomExtensionRegistration } from "pi-intercom/extension-api.js";
import { CronStorage } from "pi-schedule-prompt/src/storage.js";
import { openProjectPane } from "pi-subagents/project-panes";
import { Type } from "typebox";
import { noticeDisplay } from "./notice-display.js";
import { FOLD_LINE, foldPlan, GOAL_LINE, goalAcceptanceSignature } from "./plan.js";
@@ -25,6 +27,7 @@ import {
goalToolBlocked,
manualReview,
messages,
nativeMessages,
pausedRole,
pauseExitNotice,
planChangedReview,
@@ -41,25 +44,38 @@ import {
soloRole,
supervisor,
upkeep,
workerAssignment,
workerReview,
} from "./prompts.js";
const STATE = "pi-goals-main-supervisor-v1";
const WORKER = "goals-worker";
const CONTROL = "goals-worker-control";
const WIDGET_GOAL_LIMIT = 3;
type Mode = "chat" | "planning" | "supervising" | "paused" | "solo";
type GoalStatus = "open" | "active" | "done" | "cancelled";
interface Peer {
sessionId: string; sessionFile: string; leafId: string | null; paneId: string;
durable: boolean; empty: boolean; started?: boolean; parentSession?: string; plan?: string; parentId?: string; requestId?: string; model?: string;
}
interface WorkerRequest {
id: string; action: "start" | "fresh" | "recover"; task?: string; model?: string;
reviewedThrough?: string; writersStopped?: boolean; sessionFile?: string; savedDigest?: string; savedId?: string; savedIntercom?: string;
phase: "probe" | "control" | "switch"; previous?: Peer;
}
interface State {
mode: Mode;
plan?: string;
worker?: { id?: string; sessionFile: string };
helpers: { id?: string; sessionFile: string }[];
worker?: { sessionFile?: string; intercomId?: string; paneId?: string; requestId?: string; parentId?: string; identity?: Peer; pending?: WorkerRequest };
parent?: { intercomId: string; requestId: string; selfId?: string; started?: boolean };
workerStopped?: boolean;
pausedFrom?: "solo" | "supervising";
signoffs: Record<string, { evidence: string[]; observation: string; signature: string }>;
finalReview?: { planDigest: string };
child?: boolean;
lastControl?: string;
}
const initial = (): State => ({ mode: "chat", helpers: [], signoffs: {} });
const initial = (): State => ({ mode: "chat", signoffs: {} });
const digest = (text: string) => createHash("sha256").update(text).digest("hex");
const key = (text: string) => text.trim().toLowerCase();
function goals(text: string) {
@@ -71,6 +87,15 @@ function goals(text: string) {
});
}
const requirements = (text: string) => goals(text).map(g => goalAcceptanceSignature(text, g.subject)).join("\n");
function savedWorker(path: string) {
const text = readFileSync(path, "utf8");
const entries = text.trim().split("\n").map(line => JSON.parse(line));
const header = entries[0];
if (header?.type !== "session" || typeof header.id !== "string" || !entries.some(entry => entry.message?.role === "assistant")) throw new Error(nativeMessages.notDurable);
const state = entries.filter(entry => entry.type === "custom" && entry.customType === STATE).at(-1)?.data as State | undefined;
if (!state?.child || !state.parent) throw new Error(nativeMessages.notOwned);
return { header, state, digest: digest(text) };
}
const result = (text: string) => ({ content: [{ type: "text" as const, text }], details: {} });
export default function mainSupervisor(pi: ExtensionAPI) {
@@ -79,13 +104,17 @@ export default function mainSupervisor(pi: ExtensionAPI) {
let generation = 0;
let workerRevision = 0;
let finalReviewTurnDigest: string | undefined;
const pendingLaunches = new Map<string, { plan: string; generation: number; launches: { agent?: string; sessionFile?: string }[] }>();
let opening = false;
let channel: IntercomExtensionChannel | undefined;
let ownIntercomId: string | undefined;
let liveContext: ExtensionContext | undefined;
let control: { from: string; request: WorkerRequest; plan: string; expected: Peer; cancelled?: boolean } | undefined;
let replacing: "new" | "resume" | undefined;
let notice = true;
let fullPlanContextDue = true;
let planWatcher: FSWatcher | undefined;
let planEditTimer: ReturnType<typeof setTimeout> | undefined;
let planHash = "";
const childEnvironment = process.env.PI_SUBAGENT_AGENT === WORKER;
const save = () => pi.appendEntry(STATE, structuredClone(state));
// Missing, empty and failed reads are unavailable snapshots, never an empty authoritative plan.
const readPlan = () => {
@@ -153,7 +182,9 @@ export default function mainSupervisor(pi: ExtensionAPI) {
}).filter(Boolean);
lines.push(`${counts.join(", ")}`);
}
lines.unshift(relative(ctx.cwd, state.plan!)); // Readable path fallback; terminal link activation is not verified.
const planPath = relative(ctx.cwd, state.plan!);
const external = isAbsolute(planPath) || planPath === ".." || planPath.startsWith(`..${sep}`);
lines.unshift(external ? `${basename(state.plan!)} (external)` : planPath);
ctx.ui.setWidget("goals", lines);
}
function watchPlan(ctx: ExtensionContext) {
@@ -199,13 +230,6 @@ export default function mainSupervisor(pi: ExtensionAPI) {
for (const entry of ctx.sessionManager.getBranch()) {
if (entry.type === "custom" && entry.customType === STATE) state = structuredClone(entry.data as State);
}
if (childEnvironment) {
state.child = true;
state.mode = "solo";
// Lineage-only workers attach the explicit task path using AttachGoalPlan.
save();
}
state.helpers ??= []; // sessions persisted before helper bookkeeping
notice = true;
turnsStale = 0;
lastWorkingSet = "";
@@ -215,11 +239,6 @@ export default function mainSupervisor(pi: ExtensionAPI) {
refresh(ctx);
watchPlan(ctx);
}
function compatible() {
const tools = pi.getAllTools();
const properties = (name: string) => (tools.find((t) => t.name === name)?.parameters as { properties?: Record<string, unknown> } | undefined)?.properties;
return properties("subagent")?.title && properties("subagent")?.agent && properties("subagent_resume")?.sessionFile && properties("subagent_kill")?.id;
}
function send(content: string, triggerTurn = true) {
// sendMessage(triggerTurn:true) bypasses before_agent_start in Pi 0.85.1.
// A normal saved prompt prepares the current role before starting the turn.
@@ -230,11 +249,11 @@ export default function mainSupervisor(pi: ExtensionAPI) {
} else pi.sendMessage({ customType: "pi-goals-supervision", content, display: true }, { deliverAs: "nextTurn" });
}
async function confirmOwnership(ctx: ExtensionContext, target: string, text: string, solo = true): Promise<boolean> {
if (pendingLaunches.size > 0) { ctx.ui.notify("A worker launch/resume is still pending; inspect its result before takeover.", "warning"); return false; }
if (opening || state.worker?.pending) { ctx.ui.notify("A worker launch/resume is still pending; inspect its result before takeover.", "warning"); return false; }
const stamp = generation;
const revision = workerRevision;
const confirmation = solo ? "Worker confirmed stopped" : "Previous supervisor confirmed stopped";
const choice = await ctx.ui.select(solo ? "Confirm all other writers for the current and target plans are stopped (inspect /subagents and their panes). A missing handle is not proof. Take over in this session?" : "Confirm no other supervisor owns this plan. Preserve any existing worker session and reconnect rather than starting another writer.", [confirmation, "Cancel"]);
const choice = await ctx.ui.select(solo ? "Confirm all other writers for the current and target plans are stopped (inspect Intercom and their native panes). A missing handle is not proof. Take over in this session?" : "Confirm no other supervisor owns this plan. Preserve any existing worker session and reconnect rather than starting another writer.", [confirmation, "Cancel"]);
if (stamp !== generation || revision !== workerRevision) return false;
if (choice !== confirmation) return false;
if (readFileSync(target, "utf8") !== text) { ctx.ui.notify("Plan changed during takeover; confirm again.", "warning"); return false; }
@@ -245,7 +264,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx); watchPlan(ctx);
send(`${removeGoalSchedule(ctx.sessionManager.getSessionId())}\n\n${soloNotice(state.plan!)}`);
}
const help = "/goals new [initial idea] | edit | discuss | review | ready | status | stop | resume | solo | attach <plan.md> [solo] | model <model> | quit (exit/clear)\n/subagents opens the worker controls. Stop pauses work. Quit/exit/clear preserves the plan and clears goal state without a model call; worker processes are unchanged. No forced compaction or model switch; the worker pane's own model is chosen with /model in that pane. Hourly check-ins are one session-bound schedule_prompt job; plan-change reviews are the plan-watcher event hook.";
const help = "/goals new [initial idea] | edit | discuss | review | ready | status | stop | resume | solo | attach <plan.md> [solo] | model <model> | quit (exit/clear)\nOpenGoalWorker opens a native project pane; use Intercom to steer the verified worker session. Stop pauses work. Quit/exit/clear preserves the plan and clears goal state without a model call; worker processes are unchanged. No forced compaction or model switch; the worker pane's own model is chosen with /model in that pane. Hourly check-ins are one session-bound schedule_prompt job; plan-change reviews are the plan-watcher event hook.";
async function ready(ctx: ExtensionContext, menu: boolean, edit = false) {
if (state.mode !== "planning") { ctx.ui.notify("Ready applies to a draft; use status or resume.", "warning"); return; }
const text = planText();
@@ -265,14 +284,152 @@ export default function mainSupervisor(pi: ExtensionAPI) {
}
if (choice !== "Ready") return;
}
if (!compatible()) { ctx.ui.notify("Requires edxeth/pi-subagents 2.9.x, not nicobailon/pi-subagents. Draft preserved; /goals solo is available.", "error"); return; }
state.mode = "supervising"; generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx); watchPlan(ctx);
send(`${checkIn(ctx)}\n\n${readyApproved(WORKER, state.plan!, state.worker?.sessionFile, text, ctx.sessionManager.getSessionId())}`);
}
pi.on("session_start", (_e, ctx) => restore(ctx));
function identity(ctx: ExtensionContext): Peer {
const sessionFile = ctx.sessionManager.getSessionFile() ?? "";
let durable = false;
try { durable = savedWorker(sessionFile).header.id === ctx.sessionManager.getSessionId(); } catch { /* A prospective path is not saved history. */ }
return { sessionId: ctx.sessionManager.getSessionId(), sessionFile, leafId: ctx.sessionManager.getLeafId(), paneId: process.env.HERDR_PANE_ID ?? "", durable,
empty: !ctx.sessionManager.getBranch().some(entry => entry.type === "message"), parentSession: ctx.sessionManager.getHeader()?.parentSession,
plan: state.plan, parentId: state.parent?.intercomId, requestId: state.parent?.requestId, started: state.parent?.started, model: ctx.model ? ctx.model.provider + "/" + ctx.model.id : undefined };
}
const publish = (payload: unknown) => { channel?.publish(payload, { audience: "capable" }); };
async function available() {
const stamp = generation, ctx = liveContext, current = channel;
if (!ctx || !current?.snapshot().connected || !current.snapshot().supported) return;
const peers = await current.listSessions().catch(() => []);
if (stamp !== generation || current !== channel) return;
const self = peers.filter(peer => peer.pid === process.pid);
if (self.length === 1 && pi.getCommands().some(command => command.name === CONTROL)) { ownIntercomId = self[0].id; publish({ type: "available", identity: identity(ctx) }); }
}
function probe() {
if (state.mode === "supervising" && state.worker?.pending && state.worker.paneId) publish({ type: "inspect", paneId: state.worker.paneId, requestId: state.worker.pending.id });
}
function cancelControl() {
if (control) control.cancelled = true;
if (state.worker?.pending) {
try { publish({ type: "cancel", paneId: state.worker.paneId, requestId: state.worker.pending.id }); } catch { /* Local pause still takes effect when the peer is disconnected. */ }
state.worker.requestId = state.worker.pending.previous?.requestId ?? state.worker.requestId;
state.worker.pending = undefined;
}
}
function registerChannel(ctx: ExtensionContext) {
liveContext = ctx;
const registration: IntercomExtensionRegistration = {
namespace: "pi-goals", ownerEligible: false,
onReady: (value) => { channel = value; void available(); },
onEvent: (event) => {
if (event.type === "connection" && event.connected) { void available(); probe(); }
if (event.type === "session_left" && event.sessionId === state.worker?.intercomId && state.mode === "supervising" && state.worker.pending?.phase !== "switch") send(workerReview(state.plan!, event.sessionId, nativeMessages.disconnected));
if (event.type !== "message" || !event.payload || typeof event.payload !== "object") return;
const data = event.payload as { type?: string; to?: string; requestId?: string; plan?: string; paneId?: string; sessionFile?: string; text?: string; identity?: Peer; request?: WorkerRequest; expected?: Peer };
if (data.type === "inspect" && data.paneId && data.paneId === process.env.HERDR_PANE_ID && typeof data.requestId === "string") {
publish({ type: "peer", to: event.fromSessionId, requestId: data.requestId, identity: identity(ctx) }); return;
}
if (data.type === "cancel" && data.paneId === process.env.HERDR_PANE_ID && control?.from === event.fromSessionId && data.requestId === control.request.id) { control.cancelled = true; return; }
if (data.type === "control" && ownIntercomId && data.to === ownIntercomId && data.expected && data.expected.paneId === process.env.HERDR_PANE_ID && data.request && ["start", "fresh", "recover"].includes(data.request.action) && typeof data.request.id === "string" && typeof data.plan === "string") {
if (control || !pi.getCommands().some(command => command.name === CONTROL)) return;
control = { from: event.fromSessionId, request: data.request, plan: data.plan, expected: data.expected };
pi.sendUserMessage("/" + CONTROL, { expandPromptTemplates: true, deliverAs: "followUp" }); return;
}
const worker = state.worker, pending = worker?.pending;
if (state.child || !worker || !state.plan) return;
if (data.type === "available" && data.identity?.paneId === worker.paneId && pending) { probe(); return; }
if (data.type === "peer" && pending && data.identity && data.to === worker.parentId && data.requestId === pending.id && data.identity.paneId === worker.paneId && state.mode === "supervising") {
const peer = data.identity;
if (event.fromSessionId === worker.parentId) return;
if (peer.parentId === worker.parentId && peer.requestId === pending.id && peer.started) { worker.intercomId = event.fromSessionId; worker.identity = peer; worker.sessionFile = peer.sessionFile; worker.requestId = pending.id; worker.pending = undefined; save(); send(workerReview(state.plan, event.fromSessionId, nativeMessages.actionApplied("observed without replay", peer))); return; }
if (pending.phase === "control") return;
if (pending.phase === "probe" && pending.action === "fresh" && (peer.sessionId !== pending.previous?.sessionId || peer.sessionFile !== pending.previous?.sessionFile)) { worker.pending = undefined; save(); send(workerReview(state.plan, event.fromSessionId, nativeMessages.controlChanged)); return; }
if (pending.phase === "switch") {
const arrived = pending.action === "fresh" ? peer.sessionId !== pending.previous?.sessionId && peer.parentSession === pending.previous?.sessionFile && peer.requestId === pending.id : peer.sessionId === pending.savedId && peer.sessionFile === pending.sessionFile;
if (!arrived) return;
}
worker.intercomId = event.fromSessionId; worker.identity = peer; worker.sessionFile = peer.sessionFile;
const phase = pending.phase; pending.phase = "control"; workerRevision++; save();
publish({ type: "control", to: event.fromSessionId, plan: state.plan, expected: peer, request: { ...pending, action: phase === "switch" || pending.action === "recover" && peer.sessionId === pending.savedId ? "start" : pending.action } }); return;
}
if (!worker.parentId || !data.requestId || data.to !== worker.parentId || event.fromSessionId === data.to || (data.requestId !== worker.requestId && data.requestId !== pending?.id) || data.plan !== state.plan) return;
if (data.type === "switching" && event.fromSessionId === worker.intercomId && pending && data.requestId === pending.id) { pending.phase = "switch"; save(); return; }
if (data.type === "rejected" && event.fromSessionId === worker.intercomId && pending && data.requestId === pending.id) { worker.requestId = pending.previous?.requestId ?? worker.requestId; worker.pending = undefined; save(); send(workerReview(state.plan, event.fromSessionId, data.text ?? nativeMessages.controlRejected), state.mode === "supervising"); return; }
if (data.type === "attached" && typeof data.sessionFile === "string" && isAbsolute(data.sessionFile) && (!worker.intercomId || worker.intercomId === event.fromSessionId)) {
if (worker.pending && data.requestId !== worker.pending.id) { send(workerReview(state.plan, event.fromSessionId, nativeMessages.attached(data.sessionFile)), false); return; }
const action = worker.pending?.action;
worker.requestId = data.requestId;
worker.intercomId = event.fromSessionId; worker.sessionFile = data.sessionFile; if (data.identity) worker.identity = data.identity; worker.pending = undefined; workerRevision++; save();
send(workerReview(state.plan, event.fromSessionId, action ? nativeMessages.actionApplied(action, data.identity) : nativeMessages.attached(data.sessionFile)), Boolean(action) && state.mode === "supervising");
}
if (data.type === "stopped" && event.fromSessionId === worker.intercomId && typeof data.text === "string") {
if (data.identity) { worker.identity = data.identity; worker.sessionFile = data.identity.sessionFile; save(); }
send(workerReview(state.plan, event.fromSessionId, data.text), state.mode === "supervising");
}
},
};
pi.events.emit(INTERCOM_EXTENSION_REGISTER_EVENT, registration);
}
pi.registerCommand(CONTROL, {
description: nativeMessages.controlDescription,
handler: async (_args, ctx) => {
const operation = control;
if (!operation) return; // Never execute arbitrary slash-command payloads.
const { request, expected, from, plan } = operation;
const controlKey = request.id + ":" + request.action;
if (state.lastControl === controlKey) { control = undefined; return; }
let authorized = false;
const reject = (text: string) => publish({ type: "rejected", to: from, requestId: request.id, plan, text });
try {
await ctx.waitForIdle();
const current = identity(ctx);
if (operation.cancelled || state.mode === "paused" && !(request.action === "start" && request.savedId === current.sessionId) || ctx.hasPendingMessages() || ctx.ui.getEditorText().length || current.sessionId !== expected.sessionId || current.leafId !== expected.leafId || current.sessionFile !== expected.sessionFile) throw new Error(nativeMessages.controlChanged);
if (!isAbsolute(plan) || !goals(readFileSync(plan, "utf8")).length) throw new Error(messages.invalidAttachment);
const peers = await channel?.listSessions();
const self = peers?.find(peer => peer.pid === process.pid);
if (!self || from === self.id || !peers?.some(peer => peer.id === from)) throw new Error(nativeMessages.parentUnavailable);
if (operation.cancelled || ctx.hasPendingMessages() || ctx.ui.getEditorText().length || identity(ctx).leafId !== expected.leafId) throw new Error(nativeMessages.controlChanged);
if (state.child && state.parent?.intercomId !== from || !state.child && (state.mode !== "chat" || !current.empty)) throw new Error(nativeMessages.notOwned);
authorized = true;
if (request.action === "start") {
const recovering = request.savedId === current.sessionId;
if (!recovering && (!current.empty || state.parent?.started)) throw new Error(nativeMessages.controlChanged);
if (request.model) throw new Error(nativeMessages.modelRaceBoundary);
state = { ...(recovering ? state : initial()), mode: recovering ? state.mode : "solo", child: true, plan, lastControl: controlKey, parent: { intercomId: from, requestId: request.id, selfId: self.id, started: true } };
generation++; notice = true; fullPlanContextDue = true; save();
publish({ type: "attached", to: from, requestId: request.id, plan, sessionFile: ctx.sessionManager.getSessionFile(), identity: identity(ctx) });
if (!recovering && request.task) pi.sendUserMessage(workerAssignment(plan, from, request.id, request.task));
} else {
if (!current.empty && (!current.durable || request.reviewedThrough !== current.leafId)) throw new Error(nativeMessages.reviewRequired);
if (request.action === "fresh" && (!state.child || !current.durable)) throw new Error(nativeMessages.notDurable);
if (request.action === "recover") {
if (!request.writersStopped || !request.sessionFile || !request.savedIntercom) throw new Error(nativeMessages.stopRequired);
const saved = savedWorker(request.sessionFile);
if (saved.digest !== request.savedDigest || saved.header.id !== request.savedId || saved.header.cwd !== ctx.cwd || saved.state.parent?.intercomId !== from || peers.some(peer => peer.id === request.savedIntercom && peer.id !== self.id)) throw new Error(nativeMessages.controlChanged);
}
publish({ type: "switching", to: from, requestId: request.id, plan });
replacing = request.action === "fresh" ? "new" : "resume";
const switched = request.action === "fresh"
? await ctx.newSession({ parentSession: current.sessionFile, setup: async manager => { manager.appendCustomEntry(STATE, { ...initial(), mode: "solo", child: true, plan, parent: { intercomId: from, requestId: request.id } }); } })
: await ctx.switchSession(request.sessionFile!);
if (switched.cancelled) { replacing = undefined; state.lastControl = controlKey; save(); reject(nativeMessages.controlCancelled); }
}
} catch (error) { if (authorized) { state.lastControl = controlKey; save(); } reject(String(error)); }
finally { replacing = undefined; control = undefined; }
},
});
const reportStop = (text: string) => {
if (!state.child || !state.parent) return;
try {
if (!channel?.snapshot().connected) throw new Error("disconnected");
channel.publish({ type: "stopped", to: state.parent.intercomId, requestId: state.parent.requestId, plan: state.plan, text, identity: liveContext ? identity(liveContext) : undefined }, { audience: "capable" });
} catch { send(nativeMessages.reportUnavailable, false); }
};
pi.on("session_start", (_e, ctx) => {
restore(ctx); registerChannel(ctx);
});
pi.on("session_tree", (_e, ctx) => restore(ctx));
pi.on("session_shutdown", () => { generation++; finalReviewTurnDigest = undefined; planWatcher?.close(); planWatcher = undefined; clearTimeout(planEditTimer); planEditTimer = undefined; });
pi.on("session_shutdown", (event) => { if (!replacing || event?.reason !== replacing) reportStop(nativeMessages.shuttingDown); channel = undefined; liveContext = undefined; ownIntercomId = undefined; generation++; finalReviewTurnDigest = undefined; planWatcher?.close(); planWatcher = undefined; clearTimeout(planEditTimer); planEditTimer = undefined; });
// Only successful compaction needs resync; failed/cancelled attempts leave pending context alone.
// Defer to prompt preparation: same-run continuation retains Pi's current role/context.
pi.on("session_compact", () => { notice = true; fullPlanContextDue = true; });
@@ -301,7 +458,10 @@ export default function mainSupervisor(pi: ExtensionAPI) {
if (snapshot.text === undefined || state.finalReview.planDigest !== digest(snapshot.text)) return;
if (content === `[pi-goals]\n${finalReview(state.plan!, snapshot.text)}`) finalReviewTurnDigest = state.finalReview.planDigest;
});
pi.on("agent_end", (_e, ctx) => { finalReviewTurnDigest = undefined; refresh(ctx); if (!planWatcher && state.mode === "supervising") watchPlan(ctx); });
pi.on("agent_end", (event, ctx) => {
const last = event.messages.filter(message => message.role === "assistant").at(-1);
reportStop(last?.role === "assistant" ? last.errorMessage || last.content.filter(part => part.type === "text").map(part => part.text).join("\n") || last.stopReason : nativeMessages.noAssistant);
finalReviewTurnDigest = undefined; refresh(ctx); if (!planWatcher && state.mode === "supervising") watchPlan(ctx); });
let proposedDraft = "";
let proposing = false;
pi.on("agent_settled", async (_e, ctx) => {
@@ -325,7 +485,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
return { systemPrompt: `${event.systemPrompt}\n\n${state.child ? childPlanRole : ""}\n${snapshot.error}` };
}
clearChangedFinalReview(snapshot.text);
const role = state.child ? childPlanRole : state.mode === "supervising"
const role = state.child ? childPlanRole + (state.mode === "paused" ? "\n" + pausedRole : "") : state.mode === "supervising"
? supervisor(WORKER, state.plan!, ctx.sessionManager.getSessionId())
: state.mode === "planning" ? planning(state.plan!) : state.mode === "paused" ? pausedRole : soloRole;
fullPlanContextDue ||= requirements(snapshot.text) !== requirements(lastWorkingSet);
@@ -348,40 +508,9 @@ export default function mainSupervisor(pi: ExtensionAPI) {
pendingUpkeep = undefined;
return { systemPrompt: `${event.systemPrompt}\n\n${role}`, ...(message ? { message } : {}) };
});
pi.on("tool_call", (event, ctx) => {
if (event.toolName === "subagent" && event.input) {
const prefix = `${basename(ctx.cwd)} · `;
const launches = Array.isArray(event.input.children) ? event.input.children : [event.input];
for (const launch of launches) {
if (launch && typeof launch.title === "string" && !launch.title.startsWith(prefix)) launch.title = prefix + launch.title;
}
}
if (state.child || (event.toolName !== "subagent" && event.toolName !== "subagent_resume")) return;
// Solo means this chat took over implementation: no concurrent writer may be delegated.
if (state.mode === "planning" || state.mode === "paused" || state.mode === "solo") return { block: true, reason: goalToolBlocked(state.mode) };
if (state.plan) {
const input = event.input as { agent?: string; sessionFile?: string; children?: { agent?: string; sessionFile?: string }[] };
const launches = input.children ?? [input];
pendingLaunches.set(event.toolCallId, { plan: state.plan, generation, launches: launches.map(launch => ({ agent: launch.agent, sessionFile: launch.sessionFile })) });
state.workerStopped = false; workerRevision++; save();
}
});
pi.on("tool_execution_end", (event) => {
const pending = pendingLaunches.get(event.toolCallId);
pendingLaunches.delete(event.toolCallId);
if (!pending || state.child || pending.plan !== state.plan || pending.generation !== generation || event.isError) return;
type ChildResult = { id?: string; sessionFile?: string; agent?: string };
const details = (event.result as { details?: ChildResult & { children?: ChildResult[] } }).details;
if (!details) return;
for (const [index, child] of (details.children ?? [details]).entries()) {
if (!child.id || !child.sessionFile) continue;
const record = { id: child.id, sessionFile: child.sessionFile };
const launch = pending.launches[index];
const implementation = (child.agent ?? launch?.agent) === WORKER || launch?.sessionFile === state.worker?.sessionFile && Boolean(state.worker);
if (state.worker?.sessionFile === record.sessionFile || !state.worker && implementation) state.worker = record;
else state.helpers = [...state.helpers.filter(h => h.sessionFile !== record.sessionFile), record];
}
workerRevision++; save();
pi.on("tool_call", (event) => {
if (event.toolName !== "subagent" && event.toolName !== "OpenGoalWorker") return;
if (state.child || ["planning", "paused", "solo"].includes(state.mode)) return { block: true, reason: goalToolBlocked(state.child ? "worker" : state.mode) };
});
pi.registerCommand("goals", {
@@ -389,7 +518,10 @@ export default function mainSupervisor(pi: ExtensionAPI) {
getArgumentCompletions: (prefix) => ["new", "attach", "edit", "discuss", "review", "ready", "status", "stop", "resume", "solo", "model", "help", "exit", "clear", "quit"].filter((verb) => verb.startsWith(prefix)).map((verb) => ({ value: verb, label: verb })),
handler: async (args, ctx) => {
try {
if (state.child) { ctx.ui.notify("This is the delegated worker. Goal approval belongs to its parent.", "info"); return; }
if (state.child) {
if (["stop", "resume"].includes(args.trim())) { cancelControl(); state.mode = args.trim() === "stop" ? "paused" : "solo"; generation++; save(); ctx.ui.notify(nativeMessages.workerPause(state.mode === "paused"), "info"); return; }
ctx.ui.notify("This is the delegated worker. Goal approval belongs to its parent.", "info"); return;
}
let command = args.trim();
if (!command) {
const actions = [
@@ -427,10 +559,10 @@ export default function mainSupervisor(pi: ExtensionAPI) {
`Plan: ${state.plan ?? "none"}`,
`Preferred worker model (plan): ${notedPlanValue("preferred worker model") ?? "not stated; use /goals model <model>"}`,
`Recorded worker session: ${state.worker?.sessionFile ?? "not recorded"}`,
`Helper subagent sessions: ${state.helpers.length} recorded (liveness via /subagents)`,
`Worker Intercom: ${state.worker?.intercomId ?? "unconfirmed"}; native pane: ${state.worker?.paneId ?? "unconfirmed"}`,
notedPlanValue("worker session") ? `Worker session noted in plan: ${notedPlanValue("worker session")}` : "",
`Hourly check-in: schedule_prompt job ${JSON.stringify(`goals-${ctx.sessionManager.getSessionId()}`)} (list/remove via schedule_prompt; plan-change reviews are the plan-watcher event hook)`,
"Liveness is owned by edxeth; inspect /subagents.",
"Inspect the exact Intercom session and native pane; a binding or idle status is not completion.",
].filter(Boolean).join("\n"), "info");
return;
}
@@ -452,7 +584,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
writeFileSync(state.plan, lines.join("\n"));
planHash = digest(planViews(planText()).notify);
refresh(ctx);
ctx.ui.notify(ref ? `Preferred worker model set to ${ref} in plan preferences. The supervisor selects it at launch and verifies the resolved model; the worker pane's own model is chosen with /model in that pane.` : "Preferred worker model cleared.", "info");
ctx.ui.notify(ref ? `Preferred worker model set to ${ref} in plan preferences. project.open has no model override; choose /model in the native worker pane and verify its resolved model.` : "Preferred worker model cleared.", "info");
return;
}
if (command === "attach" || command.startsWith("attach ")) {
@@ -470,13 +602,14 @@ export default function mainSupervisor(pi: ExtensionAPI) {
if (!(await confirmOwnership(ctx, target, text, solo))) return;
const retained = target === state.plan ? state.signoffs : {};
const worker = noted ? { sessionFile: resolve(ctx.cwd, noted) } : state.workerStopped ? state.worker : undefined;
state = { mode: solo ? "solo" : "planning", plan: target, signoffs: retained, worker, helpers: [], workerStopped: solo || (!noted && state.workerStopped) };
state = { mode: solo ? "solo" : "planning", plan: target, signoffs: retained, worker, workerStopped: solo || (!noted && state.workerStopped) };
generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx); watchPlan(ctx);
if (solo) enterSolo(ctx);
else send(attachNotice(target, false, noted));
return;
}
if (command === "exit") {
cancelControl();
const storage = new CronStorage(ctx.cwd);
const session = ctx.sessionManager.getSessionId();
const matching = storage.getAllJobs().filter(j => j.name === `goals-${session}`);
@@ -486,7 +619,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
storage.removeJob(job.id); // Scheduler re-reads storage before firing; removed jobs cannot prompt.
pi.events.emit("cron:change", { type: "remove", jobId: job.id });
}
state = initial(); generation++; workerRevision++; pendingLaunches.clear(); pendingUpkeep = undefined; notice = true;
state = initial(); generation++; workerRevision++; pendingUpkeep = undefined; notice = true;
save(); refresh(ctx); watchPlan(ctx);
ctx.ui.notify("Goals cleared; original plan file unchanged.", "info");
return;
@@ -494,6 +627,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
if (command === "stop") {
if (state.mode === "planning") { ctx.ui.notify("A draft cannot pause; use /goals quit to clear goal state and preserve the draft.", "warning"); return; }
if (state.mode !== "solo" && state.mode !== "supervising") return;
cancelControl();
state.pausedFrom = state.mode;
state.mode = "paused"; generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx); watchPlan(ctx);
const pause = pauseExitNotice(state.worker, false);
@@ -505,7 +639,6 @@ export default function mainSupervisor(pi: ExtensionAPI) {
if (command === "resume") {
if (state.mode !== "paused" || !state.plan) { ctx.ui.notify("Only a paused approved plan can resume. A draft needs Ready.", "warning"); return; }
if (state.pausedFrom === "solo") { enterSolo(ctx); return; }
if (!compatible()) { ctx.ui.notify("edxeth tools unavailable; plan remains paused.", "error"); return; }
state.mode = "supervising"; generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx); watchPlan(ctx);
send(`${checkIn(ctx)}\n\n${resumeNotice(WORKER, state.plan, state.worker)}`);
return;
@@ -532,22 +665,73 @@ export default function mainSupervisor(pi: ExtensionAPI) {
version++;
}
}
state = { mode: "planning", plan: path, signoffs: {}, worker: state.worker, helpers: state.helpers, workerStopped: state.workerStopped }; generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx); watchPlan(ctx);
state = { mode: "planning", plan: path, signoffs: {}, worker: state.worker, workerStopped: state.workerStopped }; generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx); watchPlan(ctx);
send(planningSeed(objective, path));
} catch (error) { ctx.ui.notify(String(error), "error"); }
},
});
pi.registerTool({
name: "OpenGoalWorker", label: "Open native goal worker", description: nativeMessages.openDescription,
parameters: Type.Object({ task: Type.Optional(Type.String()), action: Type.Optional(Type.Union([Type.Literal("start"), Type.Literal("fresh"), Type.Literal("recover")])), model: Type.Optional(Type.String()), reviewedThrough: Type.Optional(Type.String()), writersStopped: Type.Optional(Type.Boolean()), sessionFile: Type.Optional(Type.String()) }),
async execute(_id, params, signal, _update, ctx) {
if (state.child || state.mode !== "supervising" || !state.plan) return result(goalToolBlocked(state.mode));
const action = params.action ?? "start";
if (opening || state.worker?.pending || action === "start" && state.worker) return result(nativeMessages.alreadyRecorded);
const preference = action === "recover" ? null : notedPlanValue("preferred worker model");
if (params.model || preference && (preference.includes("/") || !/^(?:none|\(none|default|inherit|not stated)\b/i.test(preference))) return result(nativeMessages.modelRaceBoundary);
if (action !== "recover" && !params.task) return result(nativeMessages.taskRequired);
if (action === "fresh" && (!state.worker?.identity || !params.reviewedThrough)) return result(nativeMessages.reviewRequired);
if (!channel?.snapshot().connected || !channel.snapshot().supported) return result(nativeMessages.intercomNotReady);
const stamp = generation, plan = state.plan;
const peers = await channel.listSessions().catch(() => undefined);
if (!peers) return result(nativeMessages.intercomNotReady);
const self = peers.filter(peer => peer.pid === process.pid);
if (self.length !== 1) return result(nativeMessages.noIdentity);
if (stamp !== generation || opening || signal?.aborted) return result(messages.cancelled);
const requestId = randomUUID();
const request: WorkerRequest = { id: requestId, action, task: params.task, model: params.model, reviewedThrough: params.reviewedThrough, writersStopped: params.writersStopped, phase: "probe", previous: state.worker?.identity };
if (action === "recover") {
if (!params.writersStopped || params.model || params.task) return result(nativeMessages.stopRequired);
try {
request.sessionFile = params.sessionFile || state.worker?.sessionFile;
if (!request.sessionFile || !isAbsolute(request.sessionFile)) return result(nativeMessages.notDurable);
const saved = savedWorker(request.sessionFile);
request.savedId = saved.header.id; request.savedDigest = saved.digest;
request.savedIntercom = saved.state.parent?.selfId ?? (request.sessionFile === state.worker?.sessionFile ? state.worker.intercomId : undefined);
if (saved.header.cwd !== ctx.cwd || saved.state.plan !== plan || saved.state.parent?.intercomId !== self[0].id || !request.savedIntercom) return result(nativeMessages.notOwned);
} catch (error) { return result(String(error)); }
}
state.worker = { ...state.worker, requestId: state.worker?.requestId ?? requestId, parentId: self[0].id, pending: request }; state.workerStopped = false; workerRevision++; opening = true; save();
try {
const pane = await openProjectPane({ cwd: ctx.cwd, focus: false, signal }); // No prompt before verified capability/model selection.
if (pane.ok && state.plan === plan && state.worker?.pending?.id === requestId) { state.worker.paneId = pane.data.binding.paneId; save(); probe(); }
return result(pane.ok ? JSON.stringify({ disposition: pane.data.disposition, paneId: pane.data.binding.paneId, projectRoot: pane.data.binding.projectRoot, bindingPath: pane.data.bindingPath }) + nativeMessages.openReceipt : JSON.stringify(pane));
} catch (error) { return result(nativeMessages.openFailed + String(error)); }
finally { opening = false; }
},
});
pi.registerTool({
name: "AttachGoalPlan", label: "Attach delegated plan", description: attachGoalPlanDescription,
parameters: Type.Object({ path: Type.String() }),
parameters: Type.Object({ path: Type.String(), parent: Type.Optional(Type.String()), requestId: Type.Optional(Type.String()) }),
async execute(_id, params, _signal, _update, ctx) {
if (!state.child) return result(messages.childAttachOnly);
if (!state.child && (state.mode !== "chat" || !params.parent || !params.requestId)) return result(messages.childAttachOnly);
if (state.child && state.plan && state.plan !== params.path) return result(messages.invalidAttachment);
try {
if (!isAbsolute(params.path)) return result(messages.invalidAttachment);
const items = goals(readFileSync(params.path, "utf8"));
if (!items.length || items.some(g => !g.subject)) return result(messages.invalidAttachment);
} catch { return result(messages.invalidAttachment); }
if (!state.child) {
if (!channel?.snapshot().connected) return result(nativeMessages.intercomNotReady);
const stamp = generation;
const peers = await channel.listSessions().catch(() => undefined);
if (!peers) return result(nativeMessages.intercomNotReady);
if (stamp !== generation) return result(messages.cancelled);
if (!peers?.some(peer => peer.id === params.parent && peer.pid !== process.pid)) return result(nativeMessages.parentUnavailable);
state = { ...initial(), child: true, mode: "solo", parent: { intercomId: params.parent!, requestId: params.requestId!, selfId: peers.find(peer => peer.pid === process.pid)?.id, started: true } };
}
state.plan = params.path; generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx);
if (state.parent) channel?.publish({ type: "attached", to: state.parent.intercomId, requestId: state.parent.requestId, plan: state.plan, sessionFile: ctx.sessionManager.getSessionFile(), identity: identity(ctx) }, { audience: "capable" });
return result(childPlanAttached(params.path));
},
});
+8
View File
@@ -0,0 +1,8 @@
// Nico publishes TypeScript source, not declarations; its transitive internal graph
// targets a different Pi SDK. Type only the public v1 operation used here.
declare module "pi-subagents/project-panes" {
export function openProjectPane(options: { cwd: string; message?: string; focus?: boolean; signal?: AbortSignal }): Promise<
| { ok: true; data: { bindingPath: string; disposition: "opened" | "already-open"; binding: { paneId: string; projectRoot: string; command: string } } }
| { ok: false; error: { code: string; message: string } }
>;
}
+46 -12
View File
@@ -151,16 +151,19 @@ export function planningSeed(objective: string, planPath: string): string {
export const planDocument = (objective: string) => `# ${objective.split("\n")[0] || "Goal plan"}\n\n## Objective\n${objective}\n\n## Goals\n\n## Log\n`;
export const discuss = "Type your changes in chat; the draft stays open.";
// Ready and explicit child attachment: stock lineage-only sessions do not inherit the shared plan.
export const attachGoalPlanDescription = "Delegated goals-worker only: attach the absolute plan path explicitly supplied in your task. Read it without rewriting it. Restores plan context; grants no parent completion authority. No discovery or worker launch.";
export const childPlanRole = "You are the delegated implementation worker. Maintain task ticks, evidence and Log entries for your delegated work in the supplied plan. Preserve agreed goals, requirements and discriminators; the supervisor owns goal-status changes and completion approval. Do not launch a second writer. Call AttachGoalPlan with the explicit plan path in your task before implementation (also after reconnect if unbound). Immediately report your actual Intercom UUID, saved-session path and current provider/model to the supplied supervisor ID. Identify unavailable fields as unknown; do not equate runtime IDs, session filenames and Intercom IDs. Send progress, completion and blocker reports there with artifact paths, then stay open for live messages. Do not exit or use caller_ping; unsent editor drafts are not visible in model context.";
// Ready and explicit native peer attachment. No worker environment or agent-file contract.
export const attachGoalPlanDescription = "Attach the absolute plan path explicitly supplied by the parent. On first attachment, also supply its exact Intercom parent UUID and requestId from the startup task. Read it without rewriting it. Restores plan context; grants no parent completion authority. No discovery or worker launch.";
export const childPlanRole = "You are the delegated implementation worker. Save evidence and report progress for your delegated work; leave plan maintenance to the parent. Preserve agreed goals, requirements and discriminators; the supervisor owns goal-status changes and completion approval. Do not launch a second writer. Call AttachGoalPlan with the explicit plan path in your task before implementation (also after reconnect if unbound). Immediately report your actual Intercom UUID, saved-session path and current provider/model to the supplied supervisor ID. Identify unavailable fields as unknown; do not equate runtime IDs, session filenames and Intercom IDs. Send progress, completion and blocker reports there with artifact paths, then stay open for live messages. Do not exit or use caller_ping; unsent editor drafts are not visible in model context.";
export function readyApproved(workerName: string, planPath: string, notedWorker: string | undefined, plan: string, supervisorId: string): string {
const launch = notedWorker
? `Inspect the recorded worker session ${notedWorker}; if still live, let it continue or message it. Only after confirming it stopped use subagent_resume with that sessionFile. Never restart completed work.`
: `Delegate the first unfinished goal to agent '${workerName}' with subagent; provide name, title and a bounded task.`;
return `[pi-goals: approval — Ready]\nReady approved this plan: ${planPath}. Stay here as supervisor. ${launch} Include the absolute plan path, require AttachGoalPlan, and first use intercom status/list to discover and confirm your own actual Intercom UUID, then give that address to the child. Your Pi session ID is ${supervisorId}; it is not necessarily your Intercom UUID. The child sends its completion report there and stays open. Require an initial worker report with its actual Intercom UUID, saved-session path and current provider/model; the async launch may return only a runtime ID. Record each distinct identity in plan preferences, marking child-reported fields as such until verified. Do not start a second writer. Inspect actual outputs when the child reports.\n\n${quotedPlan(planPath, foldPlan(plan), "working set before Log")}`;
? `Inspect the recorded worker session ${notedWorker}; if live, steer that exact Intercom session. Do not open or replace its conversation. For stopped-writer recovery use OpenGoalWorker action recover with writersStopped=true after inspecting the actual old runtime and saved history. It restores context without replaying a task.`
: `Use OpenGoalWorker with a bounded first task for '${workerName}'. It uses Nico project.open, not subagent.`;
return `[pi-goals: approval — Ready]\nReady approved this plan: ${planPath}. Stay here as supervisor. ${launch} Confirm your actual Intercom UUID with status/list; your Pi session ID ${supervisorId} is a distinct field. Await explicit worker attachment and a report with actual Intercom UUID, saved-session path and resolved model. Inspect results and steer corrections in that same open session. A receipt or idle pane is not completion. Only after reviewing the exact saved session may OpenGoalWorker action fresh start independent context in that pane, retaining its saved history; supply reviewedThrough from the inspected latest entry.\n\n${quotedPlan(planPath, foldPlan(plan), "working set before Log")}`;
}
export function workerAssignment(plan: string, parent: string, requestId: string, task: string): string {
return `You are already attached as the delegated goals-worker in this native Nico project pane for plan ${plan}; assignment ${requestId}. This grants worker context only, never supervisor approval authority. Read the complete supplied plan, applicable AGENTS.md and skills. Confirm the exact parent Intercom UUID ${parent} in the live roster; send it your initial actual Intercom UUID, saved-session path, resolved provider/model and thinking level. Do not infer one identity from another. Use normal tools; no model switch was requested by this launch. Implement only this assignment:\n\n${task}\n\nSave actual artifacts and verification output. Report blocked, error and result evidence through Intercom to that exact parent. The parent independently inspects and may send a concrete correction here. Do not approve goals or launch another writer. Respect human pauses and intervention. Keep this conversation open with the final review visible; do not exit, reset, switch session or close the pane.`;
}
// Supervision and turn-event upkeep (not a scheduled wake-up).
const supervisorJob = "Your job is to be an autonomous research partner and supervisor with responsibility for the user's goals. Keep perspective, bring diligence, and use research taste and wisdom to sustain work overnight and keep it on track. Resolve routine implementation decisions yourself; ask the user only when their judgment or authorization is needed. Let each check-in follow what changed or needs attention, rather than repeat the previous recap.";
export function supervisor(workerName: string, planPath: string, supervisorId: string): string {
@@ -169,8 +172,8 @@ You can be playful: let the humor come from what actually happened. Avoid repeat
You can speculate and brainstorm around uncertainty or unexpected results. Label guesses as guesses, consider alternative explanations, and look for a useful way to tell them apart. Keep exploration brief, open-minded and fun: take a step back, play with surprising ideas, question the current framing, and enjoy exploring the broader perspective while staying connected to the agreed goal.
(b •_•)b -- wassname
Take uncertainty as an invitation to investigate, not something to hide. Have room to play with ideas, question yourself and the worker, and appreciate a good surprise. Investigate surprising results, find mistaken assumptions, make complicated ideas simpler, and disagree usefully rather than agree politely. Keep the work moving without turning supervision into paperwork. A little affectionate teasing is welcome when it fits, and workers can push back too. Keep the humor friendly and the criticism specific. -- Pi/Astra
Use stock subagent for launch and subagent_resume with the returned sessionFile only after confirming the worker stopped. A stored handle is not proof of liveness; missing runtime state is not proof it stopped. Use pi-intercom list/status to identify the actual live child session before live steering; receipt alone does not prove action. Your Pi session ID is ${supervisorId}, not necessarily your Intercom UUID. Use intercom status/list to discover and confirm your own actual Intercom UUID before supplying the worker's report address. Require its completion report through Intercom while its pane stays open. A recap alone sends no instruction. Record '- worker session:' and '- worker intercom session:' in plan preferences from actual launch results and received-message identity; never confuse the runtime ID with the Intercom ID. Ensure the child calls AttachGoalPlan with the supplied path. Inspect results before CompleteGoal, then continue only unfinished goals.
Use the worker model requested in plan preferences, verify the resolved model, and report unavailable choices instead of silently substituting. Keep normal tools, not edxeth's restricted orchestrator mode. After reload or compaction reread the plan. Failed compaction, exhausted credits or lost connection do not erase progress: diagnose the actual error, restore an available authorized model/credits and resume the same saved session; never restart long work. Stock edxeth can crash the parent when a worker exits after parent reload: preserve drafts and stop workers before /reload. If it already happened, restart the saved parent session; do not repeat completed work.`;
Use OpenGoalWorker for the first native project pane and stock Intercom for exact-session assignment/report/steering. Do not use subagent as a second backend. A stored binding is not proof of liveness; missing runtime state is not proof of stop. Verify actual Intercom identities with list/status; your Pi session ID is ${supervisorId}, a distinct field. Require artifact paths, saved verification and blocker/error reports. When the worker stops for any reason, inspect actual artifacts and saved messages before approving or correcting it in the same open session. A recap or receipt alone sends no instruction and proves no action. Record actual pane identity, '- worker session:' and '- worker intercom session:' with provenance. CompleteGoal belongs only to this parent or explicitly confirmed solo self-verification.
Keep normal tools and honor human model changes. project.open has no model override: inspect the native worker's resolved model; if a requested model is unavailable, report it rather than silently substituting. After compaction reread the plan. Lost connection or exhausted credits does not erase work. Preserve drafts and saved sessions; confirm other writers stopped before solo takeover. OpenGoalWorker action recover restores an owned durable session only after confirmed stopped writers, without replaying work or changing its model. Action fresh requires the exact last reviewed entry and an idle draft-free peer, and retains the old history. Revisions use ordinary Intercom in the same context. Requested-model automation currently fails closed at the public setter race; no preference means keep the native default. Never replace an unreviewed conversation or start a duplicate writer.`;
}
// Routine notices quote only selected goal lines; full context stops at Log.
const goalLines = (text: string) => foldPlan(text).split("\n").filter(line => GOAL_LINE.test(line)).join("\n");
@@ -183,6 +186,10 @@ export function planContext(mode: string, path: string | undefined, text: string
export function planChangedReview(planPath: string, text = ""): string {
return `[pi-goals: reminder — plan changed]\nPlan changed: inspect current requirements, completion claims and evidence at ${planPath}. Evidence-only edits do not revoke execution approval. Continue only unfinished authorized work; respect pauses and do not assume approval for changed scope. Manual ticks are claims, not sign-off. Do not start a duplicate writer.${text ? `\n\n${quotedPlan(planPath, goalLines(text), "selected goal lines")}` : ""}`;
}
export function workerReview(plan: string, session: string, text: string): string {
return `Worker event for ${plan}, exact Intercom session ${session}:\n${text}\nThis is a report, not completion approval. Inspect actual artifacts and saved messages; if correction is needed, send it to the same session. Preserve its visible review conversation. Respect pauses; do not reply merely to acknowledge.`;
}
export function manualReview(planPath: string, text: string): string {
return `[pi-goals: reminder — requested review]\nReview requested: inspect the plan and actual evidence. Do not launch a duplicate writer.\n\n${quotedPlan(planPath, goalLines(text), "unfinished or unreviewed goal lines")}`;
}
@@ -227,11 +234,11 @@ export function completionResult(goal: string, sessionId: string, remaining: boo
// Pause/resume and solo recovery. Stored stop confirmation is invalidated on every worker launch.
export const pausedRole = "Goal work is paused. Do not launch, resume or authorize work. Incoming reports are observations, not permission. Help inspect or stop existing workers if requested.";
export function pauseExitNotice(worker: { id?: string; sessionFile: string } | undefined, exited: boolean): string {
return `Goals ${exited ? "exited to ordinary chat" : "paused locally"}; plan and evidence retained. ${worker ? worker.id ? `Inspect and stop runtime id ${worker.id} through subagent_kill or its pane; confirm the actual result.` : `Only saved session ${worker.sessionFile} is recorded, not a kill id. Locate its live pane/session and confirm termination; never pass the file path to subagent_kill.` : "No worker recorded: inspect /subagents if a launch was interrupted; absence is not proof of stop."} Remote stop is NOT yet confirmed. Resume only after explicit authorization.`;
export function pauseExitNotice(worker: { intercomId?: string; sessionFile?: string; paneId?: string } | undefined, exited: boolean): string {
return `Goals ${exited ? "exited to ordinary chat" : "paused locally"}; plan and evidence retained. ${worker ? `Locate the recorded native pane ${worker.paneId ?? "unknown"}, Intercom session ${worker.intercomId ?? "unknown"}, saved session ${worker.sessionFile ?? "unknown"}. Send an explicit pause there; inspect and confirm actual stop without closing the review conversation.` : "No worker recorded: inspect Intercom and native panes; absence is not proof of stop."} Remote stop is NOT yet confirmed. Resume only after explicit authorization.`;
}
export function resumeNotice(workerName: string, planPath: string, worker: { sessionFile: string } | undefined): string {
return `User authorized continuation of ${planPath}. Inspect worker state before any launch/resume. ${worker ? `Use the existing session ${worker.sessionFile}; if live, inspect/message it; only if confirmed stopped use subagent_resume.` : `Use '${workerName}' only after confirming no prior writer exists.`} Continue only unfinished goals; retain saved progress and scheduler edits.`;
export function resumeNotice(workerName: string, planPath: string, worker: { sessionFile?: string; intercomId?: string } | undefined): string {
return `User authorized continuation of ${planPath}. Inspect worker state before any launch. ${worker ? `Use the existing session ${worker.sessionFile ?? "unknown"} and exact Intercom UUID ${worker.intercomId ?? "unknown"}; if live, inspect/message it. Do not open a replacement. For saved-session recovery use OpenGoalWorker action recover only after confirming all other writers stopped; do not pass a task or model to replay.` : `Use OpenGoalWorker for '${workerName}' only after confirming no prior writer exists.`} Continue only unfinished goals; retain saved progress and scheduler edits.`;
}
export const soloRole = "Solo mode: implement the approved plan directly; do not delegate a concurrent writer. Verify artifacts before CompleteGoal; completion is self-verification, not independent supervisor review. Continue only unfinished goals and keep plan/evidence current.";
export function soloNotice(planPath: string): string {
@@ -240,3 +247,30 @@ export function soloNotice(planPath: string): string {
export function attachNotice(planPath: string, solo: boolean, notedWorker: string | undefined): string {
return `Attached to the existing plan ${planPath}; read it and its evidence without restarting completed work or re-deriving settled decisions. ${notedWorker ? `Recorded worker session: ${notedWorker}; inspect liveness before resume.` : ""} ${solo ? soloRole : "Present /goals review or /goals ready; no implementation before approval."}`;
}
export const nativeMessages = {
actionApplied: (action: string, peer: unknown) => `Native ${action} confirmed in the exact peer: ${JSON.stringify(peer)}. Recovery submits no task and does not change the model. Inspect the observed identity and durable history before further steering.`,
workerPause: (paused: boolean) => `Worker ${paused ? "paused" : "unpaused"} locally; no new task submitted and no approval authority granted.`,
notDurable: "No durable owned worker transcript is available; a prospective saved path is not recovery evidence.",
notOwned: "The exact session/plan/parent ownership does not match; no worker action taken.",
controlDescription: "Verified native worker control only; arbitrary command text never starts work.",
controlRejected: "Native worker control rejected; inspect actual peer state before retrying.",
controlChanged: "Worker identity, history, input, pause or writer state changed; no replacement/task authorized.",
controlCancelled: "Native session replacement was cancelled; prior history remains active.",
reviewRequired: "Fresh work requires the exact last reviewed session entry ID; preserve the current conversation until reviewed.",
stopRequired: "Recovery requires confirmed stopped writers and a durable owned saved session. No task replay or model override is accepted on recovery.",
taskRequired: "Supply an explicit bounded task for a new or fresh worker context.",
modelRaceBoundary: "Requested-model startup is held: public setModel cannot atomically preserve a concurrent human selection. No task was submitted under a fallback.",
openDescription: "After Ready, use action start for a blank native peer and bounded task. Use fresh only after inspecting the recorded session and supplying its exact reviewedThrough leaf ID; old history is retained. Use recover with writersStopped=true and the owned saved session, never a task replay. Requested models currently fail closed at an unresolved public setter race. A pane receipt is not actual attachment or completion.",
disconnected: "Intercom disconnected; liveness and stop are unconfirmed. Inspect the saved session and pane; do not launch a replacement.",
shuttingDown: "Worker session shutting down; inspect its last saved messages. No goal sign-off inferred.",
noAssistant: "Worker run ended without an assistant result; inspect saved messages.",
alreadyRecorded: "A worker is already recorded or opening. Inspect its native pane and exact Intercom session; do not create a duplicate or replace its conversation.",
noIdentity: "Intercom identity unavailable; no worker opened.",
openReceipt: "\nAwait verified peer capability and actual action confirmation. An existing binding is only a surface; draft, history and ownership checks still apply. Do not resend or infer work from this receipt.",
openFailed: "Native open failed; inspect binding and possible live writer before retry or solo takeover: ",
parentUnavailable: "Parent Intercom identity is not live; no worker attachment changed.",
intercomNotReady: "Intercom is still connecting. Call intercom status/list, verify the live parent identity, then retry this operation in the same session. No attachment or launch changed.",
reportUnavailable: "Automatic worker notice could not reach Intercom. The saved result remains here; restore the connection and report to the exact parent. Do not infer delivery or completion.",
attached: (sessionFile: string) => `Worker attached. Saved session: ${sessionFile}. Attachment is not completion.`,
};
-16
View File
@@ -1,16 +0,0 @@
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
// Pi/gpt-6-astra: compatibility schemas only; this RPC fixture must never launch a worker.
export default function subagentSchema(pi: ExtensionAPI): void {
for (const [name, parameters] of [
["subagent", Type.Object({ agent: Type.String(), title: Type.String() })],
["subagent_resume", Type.Object({ sessionFile: Type.String() })],
["subagent_kill", Type.Object({ id: Type.String() })],
] as const) {
pi.registerTool({
name, label: name, description: "Schema-only RPC fixture; do not execute.", parameters,
async execute() { throw new Error("Worker execution forbidden in RPC review test"); },
});
}
}
+173 -131
View File
@@ -3,13 +3,16 @@ import { access, readFile, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, join, relative } from "node:path";
import { createEditTool, type ExtensionAPI, SessionManager, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
import { openProjectPane } from "pi-subagents/project-panes";
import { afterEach, expect, it, vi } from "vitest";
import goalsExtension from "../src/index.js";
import { upkeep } from "../src/prompts.js";
import { upkeep, workerAssignment } from "../src/prompts.js";
vi.mock("pi-subagents/project-panes", () => ({ openProjectPane: vi.fn(async () => ({ ok: true, data: { bindingPath: "/project/.pi/subagents/project-pane.json", disposition: "opened", binding: { paneId: "native-pane", projectRoot: "/project", command: "pi" } } })) }));
const roots: string[] = [];
const shutdowns: Array<() => void> = [];
afterEach(() => { for (const shutdown of shutdowns.splice(0)) shutdown(); vi.unstubAllEnvs(); for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
afterEach(() => { for (const shutdown of shutdowns.splice(0)) shutdown(); vi.unstubAllEnvs(); vi.mocked(openProjectPane).mockClear(); for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function waitFor(predicate: () => boolean, ms = 1500): Promise<void> {
const start = Date.now();
@@ -19,13 +22,14 @@ async function waitFor(predicate: () => boolean, ms = 1500): Promise<void> {
}
}
function fixture(child = false) {
vi.stubEnv("PI_SUBAGENT_AGENT", child ? "goals-worker" : "");
const cwd = mkdtempSync(join(tmpdir(), "goals-main-test-")); roots.push(cwd);
const entries: any[] = []; const hooks = new Map<string, any>(); const commands = new Map<string, any>(); const tools = new Map<string, any>();
const entries: any[] = child ? [{ type: "custom", customType: "pi-goals-main-supervisor-v1", data: { mode: "solo", child: true, signoffs: {} } }] : []; const hooks = new Map<string, any>(); const commands = new Map<string, any>(); const tools = new Map<string, any>();
const messages: any[] = [];
const ctx = { cwd, sessionManager: { getBranch: () => entries, getSessionId: () => "copy-only" }, hasUI: true, hasPendingMessages: vi.fn(() => false), ui: {
theme: { fg: (_color: string, text: string) => text }, notify: vi.fn(), setStatus: vi.fn(), setWidget: vi.fn(), select: vi.fn(async (_title: string, _options: string[]) => "Ready"), editor: vi.fn(),
const ctx = { cwd, sessionManager: { getBranch: () => entries, getSessionId: () => "copy-only", getSessionFile: () => join(cwd, "session.jsonl"), getLeafId: () => "reviewed-leaf", getHeader: () => ({ id: "copy-only", cwd }) }, hasUI: true, waitForIdle: vi.fn(async () => {}), newSession: vi.fn(async (_options: any) => ({ cancelled: false })), switchSession: vi.fn(async (_path: string) => ({ cancelled: false })), hasPendingMessages: vi.fn(() => false), ui: {
getEditorText: vi.fn(() => ""), theme: { fg: (_color: string, text: string) => text }, notify: vi.fn(), setStatus: vi.fn(), setWidget: vi.fn(), select: vi.fn(async (_title: string, _options: string[]) => "Ready"), editor: vi.fn(),
} };
let registration: any;
const channel = { snapshot: vi.fn(() => ({ connected: true, supported: true })), listSessions: vi.fn(async () => [{ id: "parent-intercom", pid: process.pid }, { id: "live-parent", pid: process.pid + 1 }]), publish: vi.fn() };
const pi = {
on: (event: string, hook: any) => hooks.set(event, hook),
appendEntry: (customType: string, data: any) => entries.push({ type: "custom", customType, data }),
@@ -35,12 +39,9 @@ function fixture(child = false) {
registerEntryRenderer: vi.fn(),
sendMessage: (message: any, options: any) => messages.push({ message, options }),
sendUserMessage: (content: string, options: any) => messages.push({ message: { content }, options, savedPrompt: true }),
events: { emit: vi.fn() },
getAllTools: vi.fn(() => [
{ name: "subagent", parameters: { properties: { agent: {}, title: {} } } },
{ name: "subagent_resume", parameters: { properties: { sessionFile: {} } } },
{ name: "subagent_kill", parameters: { properties: { id: {} } } },
]),
events: { emit: vi.fn((name, data) => { if (name === "intercom:extension-register") { registration = data; data.onReady(channel); } }) },
getAllTools: vi.fn((): any[] => []),
getCommands: () => [...commands.keys()].map(name => ({name})),
};
goalsExtension(pi as unknown as ExtensionAPI);
hooks.get("session_start")({}, ctx);
@@ -64,13 +65,13 @@ function fixture(child = false) {
renameSync(tmp, path);
await delay(25);
};
const start = (toolCallId: string, input: any = { agent: "goals-worker", title: "Implement" }, toolName = "subagent") => hooks.get("tool_call")({ toolCallId, toolName, input }, ctx);
const finish = (toolCallId: string, details: any, toolName = "subagent", isError = false) => hooks.get("tool_execution_end")({ toolCallId, toolName, result: { content: [], details }, isError }, ctx);
const launch = (details: any, agent = "goals-worker", toolName = "subagent") => {
start(details.id, { agent, title: "Work", sessionFile: details.sessionFile }, toolName);
finish(details.id, details, toolName);
const start = (_id: string) => hooks.get("tool_call")({ toolName: "OpenGoalWorker" }, ctx);
const launch = async (details: { id: string; sessionFile: string }) => {
await tools.get("OpenGoalWorker").execute("open", { task: "Implement first output" }, undefined, undefined, ctx);
const state = entries.at(-1).data;
registration.onEvent({ type: "message", fromSessionId: details.id, payload: { type: "attached", to: state.worker.parentId, requestId: state.worker.requestId, plan: state.plan, sessionFile: details.sessionFile } });
};
return { ctx, pi, hooks, tools, commands, messages, command, get path() { return path; }, plan, draft, shutdown, changed, atomicWrite, get entries() { return entries.filter(entry => entry.customType === "pi-goals-main-supervisor-v1"); }, start, finish, launch };
return { ctx, pi, hooks, tools, commands, messages, command, get path() { return path; }, plan, draft, shutdown, changed, atomicWrite, get entries() { return entries.filter(entry => entry.customType === "pi-goals-main-supervisor-v1"); }, start, launch, channel, event: (event: any) => registration.onEvent(event) };
}
it.each([
@@ -161,7 +162,7 @@ it("edits even an empty draft directly without a model call", async () => {
it("clear preserves the plan without a backup, warns for misbound jobs and allows a separate new draft", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.launch({ id: "stale", sessionFile: "/tmp/old-worker.jsonl" });
await f.launch({ id: "stale", sessionFile: "/tmp/old-worker.jsonl" });
const jobs = [
{ id: "owned", name: "goals-copy-only", session: "copy-only", enabled: true },
{ id: "older", name: "older-plan", session: "copy-only", enabled: true },
@@ -171,9 +172,9 @@ it("clear preserves the plan without a backup, warns for misbound jobs and allow
const schedule = join(f.ctx.cwd, ".pi/schedule-prompts.json"); writeFileSync(schedule, JSON.stringify({ version: 1, jobs }));
const before = f.messages.length; await f.command("clear");
expect(f.messages).toHaveLength(before);
expect(f.entries.at(-1).data).toEqual({ mode: "chat", helpers: [], signoffs: {} });
expect(f.entries.at(-1).data).toEqual({ mode: "chat", signoffs: {} });
expect(JSON.parse(readFileSync(schedule, "utf8")).jobs).toEqual(jobs.slice(1));
expect(f.pi.events.emit).toHaveBeenCalledExactlyOnceWith("cron:change", { type: "remove", jobId: "owned" });
expect(f.pi.events.emit).toHaveBeenCalledWith("cron:change", { type: "remove", jobId: "owned" });
expect(f.ctx.ui.notify).toHaveBeenCalledWith("Goal check-ins left unchanged (session binding missing or different): foreign, unbound. Inspect /schedule-prompt.", "warning");
const directory = join(f.ctx.cwd, ".pi/plan");
expect(readdirSync(directory)).toEqual([basename(f.path)]);
@@ -192,7 +193,7 @@ it.each(["missing", "empty"])("clear resets a %s plan without a model call", asy
const f = fixture(); await f.draft(); const before = f.messages.length;
if (kind === "missing") rmSync(f.path); else writeFileSync(f.path, "");
await f.command("clear");
expect(f.entries.at(-1).data).toEqual({ mode: "chat", helpers: [], signoffs: {} });
expect(f.entries.at(-1).data).toEqual({ mode: "chat", signoffs: {} });
expect(f.messages).toHaveLength(before);
});
@@ -244,13 +245,6 @@ it("keeps Ready in the same chat, sends saved notices and never installs a conte
f.shutdown();
});
it("preserves a draft when the wrong subagent package is loaded, and offers explicit solo", async () => {
const f = fixture(); await f.draft(); f.pi.getAllTools.mockReturnValue([]);
await f.command("ready"); expect(f.entries.at(-1).data.mode).toBe("planning");
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped");
await f.command("solo"); expect(f.entries.at(-1).data.mode).toBe("solo");
});
it("rejects a plan changed while the human was reviewing it", async () => {
const f = fixture(); await f.draft();
f.ctx.ui.select.mockImplementation(async () => { writeFileSync(f.path, "- [ ] goal: substituted\n"); return "Ready"; });
@@ -259,11 +253,11 @@ it("rejects a plan changed while the human was reviewing it", async () => {
it("reloads a paused plan without launching, and retains the public worker session handle", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.launch({ id: "child-1", sessionFile: "/tmp/child.jsonl" });
await f.launch({ id: "child-1", sessionFile: "/tmp/child.jsonl" });
await f.command("stop");
expect(f.messages.at(-1).message.content).toContain("Remote stop is NOT yet confirmed");
f.hooks.get("session_start")({}, f.ctx);
expect(f.hooks.get("tool_call")({ toolName: "subagent_resume" }).block).toBe(true);
expect(f.hooks.get("tool_call")({ toolName: "OpenGoalWorker" }).block).toBe(true);
await f.command("resume");
expect(f.messages.at(-1).message.content).toContain("/tmp/child.jsonl");
await f.command("exit"); expect(f.entries.at(-1).data.mode).toBe("chat");
@@ -313,7 +307,7 @@ it("requires actual nonempty evidence, distinguishes manual ticks, and retains s
expect(reminder).toContain("[x] goal: second output");
expect(reminder).not.toContain("first output");
writeFileSync(f.path, readFileSync(f.path, "utf8").replace("[x] goal: first", "[ ] goal: first"));
f.hooks.get("agent_end")({}, f.ctx);
f.hooks.get("agent_end")({ messages: [] }, f.ctx);
expect(f.ctx.ui.setStatus).toHaveBeenLastCalledWith("goals", "👀 0/2 goals");
f.shutdown();
});
@@ -410,7 +404,7 @@ it("gives pause scheduler guidance but clears on exit without a model prompt", a
const before = f.messages.length;
await f.command("exit");
expect(f.messages).toHaveLength(before);
expect(f.entries.at(-1).data).toEqual({ mode: "chat", helpers: [], signoffs: {} });
expect(f.entries.at(-1).data).toEqual({ mode: "chat", signoffs: {} });
});
it("requires a full-plan review turn before recording the final goal", async () => {
@@ -510,7 +504,7 @@ it("recovers from an unreadable plan after compaction instead of restarting work
it("requires confirmed worker stop before solo takeover and never lets two writers run together", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.launch({ id: "child-1", sessionFile: "/tmp/child.jsonl" });
await f.launch({ id: "child-1", sessionFile: "/tmp/child.jsonl" });
f.ctx.ui.select.mockResolvedValueOnce("Cancel");
await f.command("solo");
expect(f.entries.at(-1).data.mode).toBe("supervising"); // cancelled
@@ -518,8 +512,8 @@ it("requires confirmed worker stop before solo takeover and never lets two write
await f.command("solo");
expect(f.entries.at(-1).data.mode).toBe("solo");
expect(f.hooks.get("tool_call")({ toolName: "subagent" }).block).toBe(true);
expect(f.hooks.get("tool_call")({ toolName: "subagent_resume" }).block).toBe(true);
expect(f.hooks.get("tool_call")({ toolName: "subagent_kill" })).toBeUndefined();
expect(f.hooks.get("tool_call")({ toolName: "OpenGoalWorker" }).block).toBe(true);
expect(f.hooks.get("tool_call")({ toolName: "read" })).toBeUndefined();
mkdirSync(join(f.ctx.cwd, "evidence")); writeFileSync(join(f.ctx.cwd, "evidence/pass.log"), "bytes\n");
const text = (await f.tools.get("CompleteGoal").execute("t", { goal: "first output", evidence: ["evidence/pass.log"], observation: "inspected" }, undefined, undefined, f.ctx)).content[0].text;
expect(text).toContain("self-verification");
@@ -586,7 +580,7 @@ it("records the preferred worker model as a visible plan preference", async () =
it.each(["solo", "attach"])("%s takeover cannot bypass confirmation or survive a lifecycle change during the menu", async kind => {
const f = fixture(); await f.draft(); await f.command("ready");
f.launch({ id: "child", sessionFile: "/tmp/prior.jsonl" });
await f.launch({ id: "child", sessionFile: "/tmp/prior.jsonl" });
let answer!: (choice: string) => void;
f.ctx.ui.select.mockImplementationOnce(() => new Promise(resolve => { answer = resolve; }));
const takeover = f.command(kind === "solo" ? "solo" : `attach ${f.path} solo`);
@@ -609,19 +603,18 @@ it("attach solo requires stop confirmation for a noted worker even in a fresh se
expect(readFileSync(path, "utf8")).toContain("worker session: /tmp/known.jsonl");
});
it("retains the stopped session reference without permanently blocking another plan", async () => {
it("retains the stopped session reference and refuses replacement in the first-session port", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.launch({ id: "child", sessionFile: "/tmp/prior.jsonl" });
await f.launch({ id: "child", sessionFile: "/tmp/prior.jsonl" });
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo");
const other = join(f.ctx.cwd, "another.md"); writeFileSync(other, "- [ ] goal: next\n## Log\n");
f.ctx.ui.select.mockResolvedValueOnce("Previous supervisor confirmed stopped");
await f.command(`attach ${other}`);
expect(f.entries.at(-1).data).toMatchObject({ mode: "planning", plan: other, workerStopped: true, worker: { sessionFile: "/tmp/prior.jsonl" } });
await f.command("ready");
f.start("resume", { sessionFile: "/tmp/prior.jsonl" }, "subagent_resume");
expect(f.entries.at(-1).data.workerStopped).toBe(false);
await f.command("solo");
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("still pending"), "warning");
const response = await f.tools.get("OpenGoalWorker").execute("open", { task: "next task" }, undefined, undefined, f.ctx);
expect(response.content[0].text).toContain("already recorded");
expect(f.entries.at(-1).data.workerStopped).toBe(true);
});
it("solo closes a pending plan watcher and sends removal-only scheduler guidance", async () => {
@@ -644,7 +637,7 @@ it.each(["missing", "empty", "directory"])("%s plan snapshots never erase signof
if (failure === "empty") writeFileSync(f.path, "");
else { rmSync(f.path); if (failure === "directory") mkdirSync(f.path); }
await delay(250); // also exercise unavailable read after debounce has expired
f.hooks.get("agent_end")({}, f.ctx);
f.hooks.get("agent_end")({ messages: [] }, f.ctx);
expect(f.entries.at(-1).data.signoffs["first output"]).toBeDefined();
f.hooks.get("session_compact")();
const unavailable = f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
@@ -699,22 +692,7 @@ it("cancelled goals do not prevent final cleanup, and solo writes self-verificat
expect(text).toContain("Preserved context");
});
it("prefixes single and batch launch titles with the project without changing handles or duplicating prefixes", () => {
const f = fixture();
const single = { name: "report-worker", title: "Restore PCA" };
const event = { toolName: "subagent", input: single };
f.hooks.get("tool_call")(event, f.ctx);
const expected = `${f.ctx.cwd.split("/").at(-1)} · Restore PCA`;
expect(single).toEqual({ name: "report-worker", title: expected });
f.hooks.get("tool_call")(event, f.ctx);
expect(single.title).toBe(expected);
const children = [{ name: "test-worker", title: "Check results" }, { ...single }];
f.hooks.get("tool_call")({ toolName: "subagent", input: { children } }, f.ctx);
expect(children[0].title).toBe(`${f.ctx.cwd.split("/").at(-1)} · Check results`);
expect(children[1]).toEqual(single);
});
it("lineage-only child attaches its plan without a widget, retains task context, and cannot complete", async () => {
it("persisted child attaches its plan without a widget, retains task context, and cannot complete", async () => {
const f = fixture(true);
const supplied = join(f.ctx.cwd, "supplied.md");
const text = "- [/] goal: exact file\n - [ ] verify bytes\n## Log\n - [ ] archived task\n";
@@ -764,6 +742,10 @@ it.each(["solo", "supervising"])("%s widget omits long tasks without altering th
if (mode === "solo") { f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo"); }
else await f.command("ready");
expect(f.ctx.ui.setWidget.mock.lastCall?.[1]).toEqual([relative(f.ctx.cwd, f.path), "◼ G1: first output", "◻ G2: second output"]);
f.ctx.cwd = join(f.ctx.cwd, "another-project", "nested");
await f.command("status");
expect(f.ctx.ui.setWidget.mock.lastCall?.[1]).toEqual([`${basename(f.path)} (external)`, "◼ G1: first output", "◻ G2: second output"]);
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining(`Plan: ${f.path}`), "info");
expect(readFileSync(f.path, "utf8")).toBe(text);
});
@@ -853,43 +835,12 @@ it.each(["supervising", "solo"])("%s repeats concise upkeep every eight unchange
}
});
it("extra subagent launches are recorded as helpers and never steal the implementation identity", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.launch({ id: "impl", sessionFile: "/tmp/impl.jsonl" });
expect(f.entries.at(-1).data).toMatchObject({ worker: { id: "impl", sessionFile: "/tmp/impl.jsonl" }, helpers: [] });
f.launch({ id: "reviewer", sessionFile: "/tmp/review.jsonl" }, "reviewer");
expect(f.entries.at(-1).data).toMatchObject({ worker: { id: "impl" }, helpers: [{ id: "reviewer", sessionFile: "/tmp/review.jsonl" }] });
// a repeated helper launch updates its record instead of duplicating it
f.launch({ id: "reviewer-2", sessionFile: "/tmp/review.jsonl" }, "reviewer");
expect(f.entries.at(-1).data.helpers).toEqual([{ id: "reviewer-2", sessionFile: "/tmp/review.jsonl" }]);
// resuming the worker keeps the binding and refreshes its id
f.launch({ id: "impl-2", sessionFile: "/tmp/impl.jsonl" }, "goals-worker", "subagent_resume");
expect(f.entries.at(-1).data).toMatchObject({ worker: { id: "impl-2", sessionFile: "/tmp/impl.jsonl" }, helpers: [{ id: "reviewer-2" }] });
});
it("pending call IDs survive concurrent launches until every execution ends", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.start("call-a");
f.start("call-b", { agent: "reviewer", title: "Review" });
f.finish("call-a", { id: "a", sessionFile: "/tmp/a.jsonl" });
f.finish("untracked", { id: "noise", sessionFile: "/tmp/noise.jsonl" });
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped");
await f.command("solo");
expect(f.entries.at(-1).data.mode).toBe("supervising"); // one launch still pending
expect(f.ctx.notify ?? f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("still pending"), "warning");
f.finish("call-b", { id: "b", sessionFile: "/tmp/b.jsonl" });
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped");
await f.command("solo");
expect(f.entries.at(-1).data.mode).toBe("solo");
expect(f.entries.at(-1).data).toMatchObject({ worker: { id: "a" }, helpers: [{ id: "b" }] });
});
it("a launch started during takeover invalidates the menu without disabling plan watching", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
let answer!: (choice: string) => void;
f.ctx.ui.select.mockImplementationOnce(() => new Promise(resolve => { answer = resolve; }));
const solo = f.command("solo");
f.launch({ id: "late-child", sessionFile: "/tmp/late.jsonl" });
await f.launch({ id: "late-child", sessionFile: "/tmp/late.jsonl" });
answer("Worker confirmed stopped"); await solo;
expect(f.entries.at(-1).data.mode).toBe("supervising");
expect(f.entries.at(-1).data.workerStopped).toBe(false);
@@ -927,13 +878,13 @@ it("does not approve cancelled goals or display current completion for an unavai
expect(reply.content[0].text).toContain("no sign-off recorded");
expect(readFileSync(f.path, "utf8")).toContain("[-]");
rmSync(f.path);
f.hooks.get("agent_end")({}, f.ctx);
f.hooks.get("agent_end")({ messages: [] }, f.ctx);
expect(f.ctx.ui.setWidget).toHaveBeenLastCalledWith("goals", [expect.stringContaining("unavailable")]);
});
it("keeps interactive workers open", () => {
const agent = readFileSync(new URL("../agents/goals-worker.md", import.meta.url), "utf8");
expect(agent).toContain("auto-exit: false");
const task = workerAssignment("/plan.md", "parent", "request", "bounded task");
expect(task).toContain("do not exit, reset, switch session or close the pane");
});
it.each(["stop", "exit", "edit", "session_tree"])("discards pending upkeep after %s instead of reviving stale work", async change => {
@@ -1060,41 +1011,6 @@ it("rejects blank goal subjects on Ready and CompleteGoal", async () => {
expect(f.entries.at(-1).data.signoffs).toEqual({});
});
it.each(["denied", "cancelled"])("settles a %s preflight on execution_end without tool_result", async reason => {
const f = fixture(); await f.draft(); await f.command("ready");
f.start("refused");
f.finish("refused", { error: reason }, "subagent", true);
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo");
expect(f.entries.at(-1).data.mode).toBe("solo");
expect(f.entries.at(-1).data.worker).toBeUndefined();
});
it("batch results and a reviewer arriving first do not take the implementation binding", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.launch({ id: "early", sessionFile: "/tmp/early.jsonl", agent: "reviewer" }, "reviewer");
expect(f.entries.at(-1).data.worker).toBeUndefined();
f.start("batch", { children: [{ agent: "reviewer", title: "Review" }, { agent: "goals-worker", title: "Implement" }] });
f.finish("batch", { status: "started", children: [
{ id: "review", sessionFile: "/tmp/review.jsonl", agent: "reviewer" },
{ id: "impl", sessionFile: "/tmp/impl.jsonl", agent: "goals-worker" },
] });
expect(f.entries.at(-1).data.worker).toEqual({ id: "impl", sessionFile: "/tmp/impl.jsonl" });
expect(f.entries.at(-1).data.helpers).toHaveLength(2);
await f.command("stop");
expect(f.messages.at(-1).message.content).toContain("impl");
});
it.each(["replace", "tree", "pause"])("does not attach a launch result after %s changed its originating generation", async change => {
const f = fixture(); await f.draft(); await f.command("ready");
f.start("old");
if (change === "replace") { await f.command("exit"); await f.command("new next output"); }
if (change === "tree") f.hooks.get("session_tree")({}, f.ctx);
if (change === "pause") await f.command("stop");
f.finish("old", { id: "old-worker", sessionFile: "/tmp/old-worker.jsonl", agent: "goals-worker" });
expect(f.entries.at(-1).data.worker).toBeUndefined();
expect(f.entries.at(-1).data.helpers).toEqual([]);
});
it.each([
["criterion", false], ["scope", false], ["other goal", true], ["task", true], ["evidence", true], ["Log", true],
])("%s edits retain signoff=%s according to reviewed acceptance", async (change, retained) => {
@@ -1108,7 +1024,7 @@ it.each([
task: ["[ ] original task", "[x] maintained task"], evidence: ["original evidence", "additional evidence"], Log: ["## Log", "## Log\n- historical note"],
};
writeFileSync(f.path, signed.replace(...replacements[change as string]));
f.hooks.get("agent_end")({}, f.ctx);
f.hooks.get("agent_end")({ messages: [] }, f.ctx);
expect(Boolean(f.entries.at(-1).data.signoffs["first output"])).toBe(retained);
f.hooks.get("session_start")({}, f.ctx);
expect(Boolean(f.entries.at(-1).data.signoffs["first output"])).toBe(retained);
@@ -1121,3 +1037,129 @@ it("passive pause is visible immediately while its model notice waits safely for
expect(f.messages.at(-1).options).toEqual({ deliverAs: "nextTurn" });
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("Remote stop is NOT yet confirmed"), "info");
});
// The native surface has one project binding; these replace old launch-schema/helper tests.
it("opens no-focus, records explicit attachment only, and wakes review only for the exact worker", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.channel.listSessions.mockRejectedValueOnce(new Error("Intercom is not connected"));
const waiting = await f.tools.get("OpenGoalWorker").execute("open", { task: "first" }, undefined, undefined, f.ctx);
expect(waiting.content[0].text).toContain("still connecting"); expect(openProjectPane).not.toHaveBeenCalled();
await f.launch({ id: "worker-id", sessionFile: "/tmp/native-worker.jsonl" });
expect(openProjectPane).toHaveBeenCalledWith(expect.objectContaining({ cwd: f.ctx.cwd, focus: false }));
expect(vi.mocked(openProjectPane).mock.calls[0][0]).not.toHaveProperty("message");
const worker = f.entries.at(-1).data.worker;
expect(worker).toMatchObject({ paneId: "native-pane", intercomId: "worker-id", sessionFile: "/tmp/native-worker.jsonl" });
const notice = { type: "stopped", to: worker.parentId, requestId: worker.requestId, plan: f.path, text: "Blocked: input missing" };
const count = f.messages.length;
for (const fromSessionId of [worker.parentId, "foreign-id"]) f.event({ type: "message", fromSessionId, payload: notice });
f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, plan: "/foreign.md" } });
f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, requestId: "stale" } });
expect(f.messages).toHaveLength(count);
f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, identity: { sessionId: "worker-session", sessionFile: "/tmp/native-worker.jsonl", paneId: "native-pane", leafId: "reviewed-leaf", requestId: worker.requestId, durable: true } } });
await f.tools.get("OpenGoalWorker").execute("fresh", { action: "fresh", task: "later work", reviewedThrough: "reviewed-leaf" }, undefined, undefined, f.ctx);
f.event({ type: "message", fromSessionId: "worker-id", payload: notice });
expect(f.entries.at(-1).data.worker.pending).toBeDefined();
const pendingId = f.entries.at(-1).data.worker.pending.id;
f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, type: "attached", sessionFile: "/tmp/native-worker.jsonl" } });
expect(f.entries.at(-1).data.worker.pending.id).toBe(pendingId);
expect(f.messages.at(-1).options).toEqual({ deliverAs: "nextTurn" });
expect(f.messages.at(-1).message.content).not.toContain("Native fresh confirmed");
f.event({ type: "message", fromSessionId: "worker-id", payload: notice });
expect(f.messages.at(-1)).toMatchObject({ savedPrompt: true, message: { content: expect.stringContaining("Blocked: input missing") } });
expect(f.entries.at(-1).data.signoffs).toEqual({});
await f.command("stop");
f.event({ type: "message", fromSessionId: "worker-id", payload: notice });
expect(f.messages.at(-1).options).toEqual({ deliverAs: "nextTurn" });
await f.command("clear");
const cleared = f.messages.length; f.event({ type: "message", fromSessionId: "worker-id", payload: notice });
expect(f.messages).toHaveLength(cleared);
});
it("ordinary project peer explicitly attaches as worker, never gaining approval authority", async () => {
const f = fixture(); const path = join(f.ctx.cwd, "supplied.md"); writeFileSync(path, f.plan);
const tool = f.tools.get("AttachGoalPlan");
await tool.execute("attach", { path }, undefined, undefined, f.ctx);
expect(f.entries).toHaveLength(0);
f.channel.listSessions.mockRejectedValueOnce(new Error("Intercom is not connected"));
const waiting = await tool.execute("attach", { path, parent: "live-parent", requestId: "assignment-id" }, undefined, undefined, f.ctx);
expect(waiting.content[0].text).toContain("still connecting"); expect(f.entries).toHaveLength(0);
await tool.execute("attach", { path, parent: "live-parent", requestId: "assignment-id" }, undefined, undefined, f.ctx);
expect(f.entries.at(-1).data).toMatchObject({ child: true, parent: { intercomId: "live-parent", requestId: "assignment-id" }, plan: path });
expect(f.channel.publish).toHaveBeenCalledWith(expect.objectContaining({ type: "attached", to: "live-parent", sessionFile: f.ctx.sessionManager.getSessionFile() }), { audience: "capable" });
await f.command("ready"); await f.command("solo");
const reply = await f.tools.get("CompleteGoal").execute("complete", { goal: "first output", evidence: [path], observation: "claim" }, undefined, undefined, f.ctx);
expect(reply.content[0].text).toContain("only to the active parent");
f.hooks.get("session_start")({}, f.ctx); f.hooks.get("session_compact")();
expect(f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx).systemPrompt).toContain("delegated implementation worker");
f.hooks.get("agent_end")({ messages: [{ role: "assistant", content: [{ type: "text", text: "Result at output.txt" }], stopReason: "stop" }] }, f.ctx);
expect(f.channel.publish).toHaveBeenLastCalledWith(expect.objectContaining({ type: "stopped", text: "Result at output.txt" }), { audience: "capable" });
});
it("pending or failed native opening never permits an unconfirmed second writer", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
const tool = f.tools.get("OpenGoalWorker");
expect(tool.parameters.properties.task.minLength ?? 0).toBe(0);
expect((await tool.execute("empty", { task: "" }, undefined, undefined, f.ctx)).content[0].text).toContain("task");
let release!: () => void;
vi.mocked(openProjectPane).mockImplementationOnce(() => new Promise((_resolve, reject) => { release = () => reject(new Error("connection lost after open")); }));
const opening = f.tools.get("OpenGoalWorker").execute("open", { task: "first" }, undefined, undefined, f.ctx);
await waitFor(() => Boolean(release));
await f.command("solo");
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("still pending"), "warning");
release(); expect((await opening).content[0].text).toContain("possible live writer");
const again = await f.tools.get("OpenGoalWorker").execute("open", { task: "again" }, undefined, undefined, f.ctx);
expect(again.content[0].text).toContain("already recorded");
expect(openProjectPane).toHaveBeenCalledTimes(1);
const worker = f.entries.at(-1).data.worker, saved = join(f.ctx.cwd, "owned-worker.jsonl");
writeFileSync(saved, [{ type: "session", id: "old-worker", cwd: f.ctx.cwd }, { type: "custom", customType: "pi-goals-main-supervisor-v1", data: { child: true, mode: "solo", plan: f.path, parent: { intercomId: worker.parentId, selfId: "old-worker" } } }, { type: "message", message: { role: "assistant", content: [{ type: "text", text: "prior work" }] } }].map(entry => JSON.stringify(entry)).join("\n"));
f.event({ type: "message", fromSessionId: "old-worker", payload: { type: "attached", to: worker.parentId, requestId: worker.requestId, plan: f.path, sessionFile: saved } });
expect((await tool.execute("replay", { action: "recover", task: "repeat work", writersStopped: true }, undefined, undefined, f.ctx)).content[0].text).toContain("No task replay");
await tool.execute("recover", { action: "recover", task: "", model: "", sessionFile: "", reviewedThrough: "", writersStopped: true }, undefined, undefined, f.ctx);
expect(f.entries.at(-1).data.worker.pending).toMatchObject({ action: "recover", sessionFile: saved, task: "" });
expect(openProjectPane).toHaveBeenCalledTimes(2);
});
it("verified native control preserves drafts/history and fresh context uses only the public replacement API", async () => {
const f = fixture(); const path = join(f.ctx.cwd, "owned.md"); writeFileSync(path, f.plan);
await f.tools.get("AttachGoalPlan").execute("attach", { path, parent: "live-parent", requestId: "previous" }, undefined, undefined, f.ctx);
const file = f.ctx.sessionManager.getSessionFile();
writeFileSync(file, [{ type: "session", id: "copy-only", cwd: f.ctx.cwd }, ...f.entries, { type: "message", id: "reviewed-leaf", message: { role: "assistant", content: [{ type: "text", text: "reviewed output" }] } }].map(entry => JSON.stringify(entry)).join("\n"));
const before = readFileSync(file, "utf8");
f.event({ type: "message", fromSessionId: "live-parent", payload: { type: "inspect", paneId: process.env.HERDR_PANE_ID, requestId: "next" } });
const expected = f.channel.publish.mock.calls.at(-1)![0].identity;
const request = { id: "next", action: "fresh", phase: "control", reviewedThrough: "reviewed-leaf", previous: expected, task: "independent work" };
const dispatch = async (from = "live-parent") => {
f.event({ type: "message", fromSessionId: from, payload: { type: "control", to: "parent-intercom", plan: path, expected, request } });
await f.commands.get("goals-worker-control").handler("", f.ctx);
};
f.ctx.ui.getEditorText.mockReturnValue("unsent draft"); await dispatch();
expect(f.ctx.newSession).not.toHaveBeenCalled(); expect(f.ctx.ui.getEditorText()).toBe("unsent draft"); expect(readFileSync(file, "utf8")).toBe(before);
f.ctx.ui.getEditorText.mockReturnValue(""); f.ctx.hasPendingMessages.mockReturnValueOnce(true); await dispatch();
expect(f.ctx.newSession).not.toHaveBeenCalled();
await dispatch("foreign"); expect(f.ctx.newSession).not.toHaveBeenCalled();
request.id = "reviewed-next"; await dispatch();
expect(f.ctx.newSession).toHaveBeenCalledWith(expect.objectContaining({ parentSession: file, setup: expect.any(Function) }));
expect(f.ctx.switchSession).not.toHaveBeenCalled(); expect(readFileSync(file, "utf8")).toBe(before);
const appendCustomEntry = vi.fn(); await f.ctx.newSession.mock.calls[0][0].setup({ appendCustomEntry });
expect(appendCustomEntry).toHaveBeenCalledWith("pi-goals-main-supervisor-v1", expect.objectContaining({ child: true, mode: "solo", plan: path, parent: { intercomId: "live-parent", requestId: "reviewed-next" } }));
expect(f.messages.filter(message => message.savedPrompt).every(message => message.message.content === "/goals-worker-control")).toBe(true);
request.id = "replacement-fails"; f.ctx.newSession.mockRejectedValueOnce(new Error("replacement rejected")); await dispatch();
expect(f.ctx.newSession).toHaveBeenCalledTimes(2);
expect(f.channel.publish).toHaveBeenLastCalledWith(expect.objectContaining({ type: "rejected", text: expect.stringContaining("replacement rejected") }), { audience: "capable" });
f.hooks.get("session_shutdown")({ reason: "new" }, f.ctx);
expect(f.channel.publish).toHaveBeenLastCalledWith(expect.objectContaining({ type: "stopped", text: expect.stringContaining("shutting down") }), { audience: "capable" });
});
it("model requests fail closed and an unavailable control command never falls through to inference", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
const before = f.entries.length;
for (const model of ["offline/requested", "missing/unavailable"]) {
const reply = await f.tools.get("OpenGoalWorker").execute("open", { task: "must not run", model }, undefined, undefined, f.ctx);
expect(reply.content[0].text).toContain("public setModel");
}
expect(f.entries).toHaveLength(before); expect(openProjectPane).not.toHaveBeenCalled();
f.commands.delete("goals-worker-control");
const count = f.messages.length;
f.event({ type: "message", fromSessionId: "live-parent", payload: { type: "control", to: "parent-intercom", plan: f.path, expected: { paneId: process.env.HERDR_PANE_ID }, request: { id: "unknown-command", action: "start" } } });
expect(f.messages).toHaveLength(count);
});
+1 -2
View File
@@ -10,6 +10,5 @@ it("declares current entry and bundled extension resources that exist after inst
expect(manifest.dependencies[name]).toBeTruthy();
expect(manifest.bundledDependencies).toContain(name);
}
expect(manifest.dependencies["pi-subagents"]).toContain("953c6f6d2fc7d8a5c956c30cd77c51bad697c2a4");
expect(existsSync("agents/goals-worker.md")).toBe(true);
expect(manifest.dependencies["pi-subagents"]).toBe("0.66.0");
});
-1
View File
@@ -97,7 +97,6 @@ describe("RPC review flow", () => {
const pi = spawn(resolve("node_modules/.bin/pi"), [
"--mode", "rpc", "--no-session", "--no-extensions", "--model", "offline/test",
"-e", resolve("test/fixtures/offline-model.ts"),
"-e", resolve("test/fixtures/subagent-schema.ts"),
"-e", resolve("src/index.ts"),
], {
cwd,
+1
View File
@@ -6,6 +6,7 @@
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"paths": { "pi-subagents/project-panes": ["./src/project-panes.d.ts"] },
"outDir": "dist",
"rootDir": "src",
"declaration": true