Implement retained nested goal supervisor

Main coordinates a retained supervisor that manages the nested implementation worker and writes the only approval checkpoint.

Signed-off-by: PI[goal-worker] <288921227+claudypoo@users.noreply.github.com>
Co-authored-by: PI[goal-worker] <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
wassname
2026-09-05 18:17:27 +08:00
co-authored by PI[goal-worker]
parent f87b8aac2f
commit 9fbc156860
18 changed files with 809 additions and 384 deletions
+44 -19
View File
@@ -1,6 +1,6 @@
# pi-goals
Make a short list of goals in one Markdown plan file. The main Pi agent supervises a cheaper retained worker through pi-subagents.
Make a short list of goals in one Markdown plan file. The main Pi agent is a thin coordinator for a retained supervisor, which controls a nested retained implementation worker through pi-subagents.
The plan file looks like this:
@@ -47,12 +47,11 @@ plan resync after compaction follows [tmonk/pi-goal-x](https://github.com/tmonk/
## Install
Requires `pi-subagents` 0.65.1 or newer. Install `pi-processes` so the supervisor can check managed processes. Install `pi-vcc` as a Pi extension for main-session compaction; pi-goals also loads its package in the worker.
Requires `pi-subagents` 0.65.1 or newer. Install `pi-processes` so the supervisor can check managed processes.
```bash
pi install npm:pi-subagents
pi install npm:@aliou/pi-processes
pi install npm:@sting8k/pi-vcc
pi install npm:@wassname2/pi-goals
```
@@ -60,7 +59,7 @@ Or for development:
```bash
git clone https://github.com/wassname/pi-goals && cd pi-goals && npm install
pi -e npm:pi-subagents -e npm:@sting8k/pi-vcc -e ./src/index.ts
pi -e npm:pi-subagents -e ./src/index.ts
```
## Use
@@ -73,31 +72,57 @@ pi -e npm:pi-subagents -e npm:@sting8k/pi-vcc -e ./src/index.ts
1. Plan. The agent explores read-only and drafts the plan.
2. Review. After Pi settles, the full plan is printed in the transcript. Check that User-visible
result names the final artifact or behavior you expect. The menu offers Ready, Refine, Edit, or
Cancel. Refine collects short notes. Edit opens the full plan in Pi's editor.
3. Work. Ready forks the approved-plan conversation into a cheaper `goal-worker`. pi-vcc compacts
inherited context before the first worker turn when there is enough context to compact; a small
exact fork is recorded as already below the compaction minimum. The main agent becomes the research
supervisor. Worker completion wakes it through pi-subagents. It calls `CheckGoalWork` before deciding
that all subagents and managed processes stopped, steers or resumes the retained worker with
`GuideGoalWorker`, reads the evidence, and calls `CompleteGoal` to sign off. FleetView and
`/subagents-fleet` show the worker. Every human reply and Refine note in plan mode is saved verbatim
under `## Interview`.
result names the final artifact or behavior you expect. Ready forks the retained supervisor and
preserves the main context. Ready (compact) first forks that supervisor from the full main context,
then requests Pi's normal compaction of the main session only. It never compacts the retained
supervisor or worker. Refine collects short notes. Edit opens the full plan in Pi's editor.
3. Work. The topology is:
```text
main coordinator
└── retained supervisor
└── retained implementation worker
```
The retained `goal-supervisor` rereads the full current plan on each direction or review, controls
the nested `goal-worker`, inspects the actual repository and saved evidence, then writes a private approval checkpoint in
`.pi/pi-goals/approvals/`. The worker is the implementation writer. Main and supervisor block direct
`edit`, `write`, and write-like shell commands, but can inspect and run standard verification
commands. This is not a filesystem sandbox: allowed scripts and custom tools can still mutate.
`CompleteGoal` is mechanical. It checks that worker/supervisor work is idle and that the approval
record still matches the exact goal block, clean worktree, and committed HEAD/tree before ticking.
`CheckGoalWork`, FleetView, and `/subagents-fleet` inspect the retained tree and transcripts. Every
human reply and Refine note in plan mode is saved verbatim under `## Interview`. Pi and pi-subagents
own normal compaction and retained-run recovery.
Other commands: `/goals clear` disconnects this session from its active plan, preserving the
versioned file on disk. `/goals auto [minutes|off]` changes the supervisor check interval; Ready
enables a 60-minute interval. `/goals model <model-ref>` picks the cheaper worker model. Select the
stronger supervisor with Pi's normal `/model` command. Checks continue until all goals close, the
human uses `auto off`, or the plan is cleared.
enables a 60-minute interval. `/goals model <model-ref>` picks the retained supervisor model. Checks
continue until all goals close, the human uses `auto off`, or the plan is cleared.
## Prompts
Planning and sign-off prompts live in [`src/prompts.ts`](src/prompts.ts). Worker registration and pi-subagents RPC calls live in [`src/worker.ts`](src/worker.ts). [`src/worker-runtime.ts`](src/worker-runtime.ts) compacts the initial fork with pi-vcc.
Planning and coordinator sign-off prompts live in [`src/prompts.ts`](src/prompts.ts). Runtime-agent registration and RPC calls live in [`src/worker.ts`](src/worker.ts). The supervisor-only nested-worker registration and approval tool live in [`src/supervisor-runtime.ts`](src/supervisor-runtime.ts).
## Manual check
1. Reload pi-goals with pi-subagents, create a small plan, and choose **Ready**. Open FleetView or run
`subagent({ action: "status", view: "fleet" })`. It should show `goal-supervisor` and its nested
`goal-worker`, not sibling runs from the main session.
2. Ask the main session to edit a project file. Its direct `edit`, `write`, or shell redirection call
should be blocked. Call `CompleteGoal` before a supervisor review. It should fail because no matching
private approval exists.
3. Let the worker implement, commit, and save verify output. Ask the supervisor to inspect the plan,
repository, evidence, and output. Its nested worker instruction should appear in the nested
transcript. After it calls `ApproveGoal`, inspect the JSON under `.pi/pi-goals/approvals/`.
4. Call `CompleteGoal` with the exact goal text. It should tick only while the checkpoint's goal-block
hash and committed clean repository still match. Change the plan block or worktree and retry; it
should fail closed until a new supervisor review.
## Develop
```bash
pi -e npm:pi-subagents -e npm:@sting8k/pi-vcc -e ./src/index.ts # load locally
pi -e npm:pi-subagents -e ./src/index.ts # load locally
npm test # all unit, flow, and Pi RPC tests
npm run test:rpc # Pi RPC review flow with a local offline model
npm run typecheck
-12
View File
@@ -8,9 +8,6 @@
"name": "@wassname2/pi-goals",
"version": "0.2.2",
"license": "MIT",
"dependencies": {
"@sting8k/pi-vcc": "^0.6.0"
},
"devDependencies": {
"@biomejs/biome": "^2.4.8",
"@earendil-works/pi-coding-agent": "^0.84.1",
@@ -2391,15 +2388,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@sting8k/pi-vcc": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@sting8k/pi-vcc/-/pi-vcc-0.6.1.tgz",
"integrity": "sha512-tAUkNk5Cvl3jS5/kze7l7sg7RA0w+CPZEa5GwN5/enzyyZBEyS47DMd5kklUr0v0sE1/z9s7oSzH+bmdYIEilw==",
"peerDependencies": {
"@earendil-works/pi-coding-agent": ">=0.74.0 <1.0.0",
"typebox": ">=1.1.24 <2.0.0"
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+2 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@wassname2/pi-goals",
"version": "0.2.2",
"description": "One plan file per session with a main research supervisor and retained pi-subagents worker.",
"description": "One plan file per session with a main coordinator, retained supervisor, and nested pi-subagents worker.",
"author": "wassname",
"license": "MIT",
"type": "module",
@@ -21,9 +21,6 @@
"supervisor",
"subagent"
],
"dependencies": {
"@sting8k/pi-vcc": "^0.6.0"
},
"peerDependencies": {
"@earendil-works/pi-coding-agent": "*",
"typebox": "*"
@@ -46,11 +43,11 @@
"lint:fix": "biome check --fix src/ test/"
},
"devDependencies": {
"@biomejs/biome": "^2.4.8",
"@earendil-works/pi-coding-agent": "^0.84.1",
"@types/node": "^20.0.0",
"typebox": "^1.3.7",
"typescript": "^5.0.0",
"@biomejs/biome": "^2.4.8",
"vitest": "^4.0.18"
},
"pi": {
@@ -1,9 +0,0 @@
{
"childSession": "/tmp/pi-goals-worker-runtime-wLPtUJ/.agent/sessions/--tmp-pi-goals-worker-runtime-wLPtUJ--/2026-09-05T09-09-45-566Z_01a070d4-bdda-7529-be64-eb1ecc38c053/forks/2026-09-05T09-10-07-859Z_01a070d5-14f3-7529-be64-eb21d70ab321.jsonl",
"marker": {
"version": 1,
"compacted": false,
"reason": "below-compactable-size"
},
"cwd": "/tmp/pi-goals-worker-runtime-wLPtUJ"
}
@@ -0,0 +1,79 @@
# Nested supervisor validation
2026-09-05T18:16:51+08:00
$ rg installed pi-subagents API declarations
/home/code/.pi/agent/npm/node_modules/pi-subagents/docs/agents.md:261:subagentOnlyExtensions: ./tools/child-only-search.ts
/home/code/.pi/agent/npm/node_modules/pi-subagents/docs/agents.md:282:allowNestedSubagents: true
/home/code/.pi/agent/npm/node_modules/pi-subagents/docs/agents.md:288:Simple-scalar list fields accept either a comma-separated form or a newline block list with one `- item` per line. This applies to `tools`, `excludeTools`, `defaultReads`, `skill`/`skills`, `skillPath`, `fallbackModels`, `extensions`, and `subagentOnlyExtensions`:
/home/code/.pi/agent/npm/node_modules/pi-subagents/docs/agents.md:307:| `allowNestedSubagents` | Set `true` to authorize the child-safe nested `subagent` runtime without making omitted `tools` an allowlist. Inherited depth and capability ceilings remain authoritative. |
/home/code/.pi/agent/npm/node_modules/pi-subagents/docs/agents.md:309:| `subagentOnlyExtensions` | Extension paths loaded only in this agent's child sessions. Tools registered there are unavailable to the main agent unless also installed through normal Pi extension configuration. |
/home/code/.pi/agent/npm/node_modules/pi-subagents/docs/agents.md:389:- `allowNestedSubagents: true`: explicitly enables child-safe nested fanout without turning omitted `tools` into an allowlist. Depth and inherited capability ceilings still apply.
/home/code/.pi/agent/npm/node_modules/pi-subagents/docs/agents.md:393:An allowlisted name does not load the extension that registers it. Load that provider through `extensions`, `subagentOnlyExtensions`, a path-like `tools` entry, or (background children only) normal Pi extension discovery.
/home/code/.pi/agent/npm/node_modules/pi-subagents/docs/agents.md:411:- `allowNestedSubagents: true` with `tools` omitted: normal builtin tools (and, for background children, ambient extensions) remain inherited, and the child-safe nested `subagent` runtime is added.
/home/code/.pi/agent/npm/node_modules/pi-subagents/docs/agents.md:412:- `tools: read, fixture_search` plus `subagentOnlyExtensions: ./tools/fixture-search.ts`: the provider loads only in this agent's child sessions, and the registered `fixture_search` name survives the strict allowlist.
/home/code/.pi/agent/npm/node_modules/pi-subagents/docs/agents.md:414:Direct MCP tools require [pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter). Subagents only receive direct MCP tools when `mcp:` entries are listed in their frontmatter; global `directTools: true` in `mcp.json` is not enough by itself. The generic `mcp` proxy tool can still be used for discovery when available. The adapter caches tool metadata at startup, so after connecting a new MCP server for the first time, restart Pi before relying on direct tools. Server `includeTools` and `excludeTools` policies are enforced while resolving cached metadata for children: both accept exact names and `*`/`?` glob patterns against raw, generated-resource, and server/short/none-prefixed names, with `excludeTools` taking precedence. `mcp:` entries must name servers from the adapter's configuration files. A server that exists only in the adapter's runtime snapshot (registered at runtime, not persisted) cannot be provided to a child: children are pi sessions inside the parent or the runner process, not `pi` processes that could receive an MCP config argument, so such a launch fails with an error saying that MCP tools must come from an ambient adapter extension in a background child. An `mcp:` entry named `subagent` does not authorize nested fanout; declare the builtin `subagent` tool or set `allowNestedSubagents: true`. If a resolved direct MCP name is missing from the child registry, pi-subagents keeps the launch failed under the strict allowlist and identifies the condition as a host/pi-mcp-adapter registration problem; verify that the adapter registers the selected tools before child startup.
/home/code/.pi/agent/npm/node_modules/pi-subagents/docs/agents.md:428:When `extensions` is present, normal discovered extensions are disabled. The listed extensions, path-like `tools` entries, required pi-subagents runtime extensions, and `subagentOnlyExtensions` still load.
/home/code/.pi/agent/npm/node_modules/pi-subagents/docs/agents.md:430:Use `subagentOnlyExtensions` when a custom extension tool should exist only inside child sessions. It is scoped by agent config: every run of that agent receives those extension paths, while other agents do not unless they declare the same field. The current model does not have a separate named-subagent audience inside one agent definition.
/home/code/.pi/agent/npm/node_modules/pi-subagents/docs/agents.md:434:Before the first model turn, the child runtime compares every explicit tool name with Pi's final filtered registry. A missing provider fails the run with the unavailable names and concrete `subagentOnlyExtensions`/`extensions` guidance, instead of letting a direct or chained child silently continue without its requested tools.
/home/code/.pi/agent/npm/node_modules/pi-subagents/docs/extension-api.md:67:An independently installed Pi extension can register an agent with the installed `pi-subagents` owner through the process-local `pi-subagents:runtime-agent-register:v1` event. Emit after extension setup, such as during `session_start`. Event delivery is synchronous, so the owner writes the result onto the request before `emit()` returns.
/home/code/.pi/agent/npm/node_modules/pi-subagents/docs/extension-api.md:91:pi.events.emit("pi-subagents:runtime-agent-register:v1", request);
/home/code/.pi/agent/npm/node_modules/pi-subagents/docs/extension-api.md:312:Children do not gain provider tools or extensions automatically. Add `bg_wait` to the child agent's `tools` allowlist and load each provider through `extensions` or `subagentOnlyExtensions`. The parent's effective `waitTool` setting reaches every child through its typed runtime config; `PI_SUBAGENT_WAIT_TOOL_ENABLED` keeps precedence in the parent.
$ env -u PI_SUBAGENT_CHILD npm test
> @wassname2/pi-goals@0.2.2 test
> vitest run
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
Test Files 8 passed (8)
Tests 41 passed (41)
Start at 18:16:52
Duration 1.48s (transform 906ms, setup 0ms, import 2.21s, tests 1.78s, environment 1ms)
$ env -u PI_SUBAGENT_CHILD npm run typecheck
> @wassname2/pi-goals@0.2.2 typecheck
> tsc --noEmit
$ env -u PI_SUBAGENT_CHILD npm run lint
> @wassname2/pi-goals@0.2.2 lint
> biome check src/ test/
Checked 14 files in 65ms. No fixes applied.
$ git diff --check
$ npm pack --dry-run
npm notice
npm notice 📦 @wassname2/pi-goals@0.2.2
npm notice Tarball Contents
npm notice 5.6kB README.md
npm notice 1.4kB package.json
npm notice 3.6kB src/approval.ts
npm notice 31.4kB src/index.ts
npm notice 13.5kB src/prompts.ts
npm notice 4.2kB src/supervisor-runtime.ts
npm notice 8.0kB src/worker.ts
npm notice Tarball Details
npm notice name: @wassname2/pi-goals
npm notice version: 0.2.2
npm notice filename: wassname2-pi-goals-0.2.2.tgz
npm notice package size: 21.3 kB
npm notice unpacked size: 67.6 kB
npm notice shasum: 339a8e72bf68600fc6033c3feda3c0c0581f147d
npm notice integrity: sha512-8mpaKrYqBa4RN[...]+zW55PiHKifPw==
npm notice total files: 7
npm notice
wassname2-pi-goals-0.2.2.tgz
## Live nested-runtime probe
Not run. A real `goal-supervisor` → `goal-worker` probe requires an interactive Pi session with an available model and pi-subagents loaded. This non-interactive repository environment can validate registration and RPC payloads, but cannot prove that a model launches and directs the nested run. The README manual check remains required for that observation.
-- PI[goal-worker]
@@ -1,45 +0,0 @@
$ npm test
> @wassname2/pi-goals@0.2.2 test
> vitest run
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
Test Files 8 passed (8)
Tests 37 passed (37)
Start at 17:10:33
Duration 1.55s (transform 942ms, setup 0ms, import 1.95s, tests 1.35s, environment 1ms)
$ npm run typecheck
> @wassname2/pi-goals@0.2.2 typecheck
> tsc --noEmit
$ npm run lint
> @wassname2/pi-goals@0.2.2 lint
> biome check src/ test/
Checked 13 files in 20ms. No fixes applied.
$ git diff --check
$ npm pack --dry-run
npm notice 📦 @wassname2/pi-goals@0.2.2
npm notice Tarball Contents
npm notice 4.1kB README.md
npm notice 1.5kB package.json
npm notice 27.3kB src/index.ts
npm notice 13.3kB src/prompts.ts
npm notice 1.2kB src/worker-runtime.ts
npm notice 6.2kB src/worker.ts
npm notice Tarball Details
npm notice name: @wassname2/pi-goals
npm notice version: 0.2.2
npm notice filename: wassname2-pi-goals-0.2.2.tgz
npm notice package size: 18.0 kB
npm notice unpacked size: 53.5 kB
npm notice shasum: e90e0986903238e68218ea42dd0e00fec6c56ddd
npm notice integrity: sha512-CpJr+pjGMxZ0v[...]vSTliF8yvr+Rg==
npm notice total files: 6
wassname2-pi-goals-0.2.2.tgz
@@ -1,67 +0,0 @@
# Main-agent supervisor with a retained pi-subagents worker
The main Pi session keeps the high-level research context and uses the stronger model. A cheaper `goal-worker` child implements the plan. pi-subagents owns the child fork, continuation, background completion, and Fleet visibility. pi-vcc compacts long context.
## User-visible result
After Ready, the main session supervises a retained worker: it reviews every 60 minutes, responds when the worker finishes and no other work remains, and signs off goals from inspected evidence.
## User voice
- > “Another take on my pi-intercom-supervisor but using pi-subagents not a seperate user started terminal. I want to keep it simple by using pi-vcc pi-subagents where possibe”
- > “fork and compact because I hope to use a smarter model for the supervisor with better research taste.”
- > “only check in sees a summary, and ideally compacts at 100k”
## Goals
1. [x] goal: Keep the stronger main session as supervisor and retain a cheaper child worker
- subtle failure mode: each instruction starts a fresh child that forgets earlier work.
- discriminator: a real Ready flow forks `goal-worker`; later guidance resumes its latest run ID and Fleet shows the child.
- tasks:
1. [x] register `goal-worker` through the pi-subagents runtime API with a separate `/goals model` setting
2. [x] fork on Ready and store each replacement run ID returned by resume
3. [x] keep implementation and plan edits in the child; keep evidence review and `CompleteGoal` in the main session
- evidence:
- `slop/audits/20260905_goal-worker-runtime-proof.json`: a real Ready flow created a child fork and wrote `"reason": "below-compactable-size"`.
- `slop/audits/20260905_subagent-supervisor-validation.txt`: `Tests 37 passed (37)`, including spawn, continuation, steering, and a completion-before-RPC-reply race.
2. [x] goal: Compact context without adding a custom transport
- subtle failure mode: the worker inherits the full expensive transcript, or a different compactor silently handles it.
- discriminator: the child runtime records either pi-vcc compaction or that the exact fork is below Pi's compaction minimum; the main session requests pi-vcc near 100k tokens.
- tasks:
1. [x] load pi-vcc in the child and compact the initial fork before its first turn
2. [x] fail if a different child compactor reports success
3. [x] compact the main supervisor near 100k and warn if pi-vcc did not handle it
- evidence:
- `slop/audits/20260905_goal-worker-runtime-proof.json`: the real child runtime recorded its explicit below-minimum outcome rather than silently claiming compaction.
- `test/worker-runtime.test.ts` covers pi-vcc success, below-minimum context, retained resume, and wrong-compactor failure; `test/goals-flow.test.ts` covers the 100k main-session request.
3. [x] goal: Check hourly and after worker completion without a self-review loop
- subtle failure mode: each supervisor settle schedules a new immediate review, or a child completion is mistaken for all work being idle.
- discriminator: one timer keeps its original hourly cadence; pi-subagents completion wakes the main session once; `CheckGoalWork` reports exact subagent and process state before a restart decision.
- tasks:
1. [x] keep one 60-minute timer active until goals close, auto is disabled, or the plan is cleared
2. [x] use native pi-subagents completion delivery instead of a second idle wake
3. [x] query public pi-subagents and pi-processes status; treat omitted or missing status as unknown
- evidence:
- `test/goals-flow.test.ts` keeps the first timer deadline across an intervening settle and checks idle status after native completion.
- `test/worker.test.ts` distinguishes active, nested-active, idle, omitted/unknown, and missing pi-processes status.
- `slop/audits/20260905_subagent-supervisor-validation.txt`: typecheck, lint, whitespace check, and package dry-run passed.
## UAT / verification
- Run `npm test`, `npm run typecheck`, `npm run lint`, `git diff --check`, and `npm pack --dry-run`; save exact output.
- Real runtime: load pi-goals with pi-subagents and pi-vcc, approve a plan, observe a real `goal-worker` fork and the fork-preparation record, then stop the test run.
- Inspect the final diff for duplicate wake paths, silent status fallbacks, and instructions that tell the main supervisor to implement worker tasks.
## Appendix (context, not approved)
A normal Pi TUI cannot switch into the child's full interactive session. Fleet can inspect and steer it. This design keeps the visitable persistent context in the main session and uses retained child continuation for implementation.
pi-subagents always sends an async completion to the parent. Therefore worker completion is the idle-review wake. Adding another `agent_settled` wake would create a completion → supervisor → settle loop.
`CheckGoalWork` sees parent-process pi-processes state. A process started inside the worker remains covered indirectly because the top-level worker run stays active while its child work runs.
Sources read: pi-subagents `README.md`, `docs/extension-api.md`, `docs/observability.md`, `docs/workflows.md`, execution controls; pi-processes request/list client; pi-vcc package behavior; `pi-supervise/RESEARCH_JOURNAL.md`.
-- Pi/Codex
+98
View File
@@ -0,0 +1,98 @@
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
const GOAL_LINE = /^\s*(?:\d+\.|[-*])\s*\[([ xX/-])\]\s*goal:\s*(.*)$/i;
export interface ApprovalRecord {
version: 1;
verdict: "accept";
goal: string;
planPath: string;
goalBlockHash: string;
repoRoot: string;
head: string;
tree: string;
cleanWorktree: true;
inspected: { plan: true; repository: true; evidence: true; verifyOutput: true };
supervisor: { sessionId: string; runId: string | null };
timestamp: string;
}
function command(repoRoot: string, args: string[]): string {
return execFileSync("git", args, { cwd: repoRoot, encoding: "utf8" }).trim();
}
export function repositoryState(cwd: string): { repoRoot: string; head: string; tree: string; cleanWorktree: boolean } {
const repoRoot = command(cwd, ["rev-parse", "--show-toplevel"]);
const head = command(repoRoot, ["rev-parse", "HEAD"]);
const tree = command(repoRoot, ["rev-parse", "HEAD^{tree}"]);
const cleanWorktree = command(repoRoot, ["status", "--porcelain=v1"]) === "";
return { repoRoot, head, tree, cleanWorktree };
}
export function goalBlock(plan: string, goal: string): string | null {
const lines = plan.split("\n");
const wanted = goal.trim().toLowerCase();
const hits = lines.flatMap((line, index) => {
const match = GOAL_LINE.exec(line);
return match && (match[1] === " " || match[1] === "/") && match[2].trim().toLowerCase() === wanted ? [index] : [];
});
if (hits.length !== 1) return null;
const start = hits[0];
let end = lines.length;
for (let index = start + 1; index < lines.length; index++) {
if (GOAL_LINE.test(lines[index])) {
end = index;
break;
}
}
return lines.slice(start, end).join("\n");
}
export function hashGoalBlock(block: string): string {
return createHash("sha256").update(block).digest("hex");
}
export function approvalPath(cwd: string, sessionId: string, goal: string): string {
const goalId = createHash("sha256").update(goal.trim().toLowerCase()).digest("hex").slice(0, 16);
return join(cwd, ".pi", "pi-goals", "approvals", `${sessionId}-${goalId}.json`);
}
export function writeApproval(path: string, record: ApprovalRecord): void {
mkdirSync(dirname(path), { recursive: true });
const temporary = `${path}.${process.pid}.tmp`;
try {
writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\n`);
renameSync(temporary, path);
} finally {
if (existsSync(temporary)) rmSync(temporary, { force: true });
}
}
export function readApproval(path: string): ApprovalRecord | null {
if (!existsSync(path)) return null;
try {
return JSON.parse(readFileSync(path, "utf8")) as ApprovalRecord;
} catch {
return null;
}
}
export function approvalMatches(record: ApprovalRecord | null, input: { goal: string; planPath: string; goalBlockHash: string; repoRoot: string; head: string; tree: string; cleanWorktree: boolean }): boolean {
return record?.version === 1
&& record.verdict === "accept"
&& record.goal === input.goal
&& resolve(record.planPath) === resolve(input.planPath)
&& record.goalBlockHash === input.goalBlockHash
&& resolve(record.repoRoot) === resolve(input.repoRoot)
&& record.head === input.head
&& record.tree === input.tree
&& record.cleanWorktree === true
&& input.cleanWorktree
&& record.inspected.plan === true
&& record.inspected.repository === true
&& record.inspected.evidence === true
&& record.inspected.verifyOutput === true;
}
+139 -76
View File
@@ -1,14 +1,14 @@
/**
* PI: pi-goals owns one versioned plan per session. The main agent supervises a cheaper retained
* pi-subagents worker, reviews progress, and decides CompleteGoal sign-off.
* PI: pi-goals owns one versioned plan per session. The main agent is a thin coordinator for a
* retained pi-subagents supervisor, which owns a nested retained implementation worker and approval.
*
* Each /goals call makes `.pi/plan/<session_id>-vN.md`. The selected version survives resume and
* compaction. Old plans stay on disk but inactive. A session with no selected plan has no widget,
* supervision, worker, or CompleteGoal sign-off.
*
* TypeScript reads only goal checkbox lines for the widget. Models read the plan as prose. The
* worker edits the project and records evidence. The main agent keeps the high-level context and
* directs the worker. pi-subagents owns the worker session, fork, persistence, resume, events,
* worker edits the project and records evidence. The supervisor inspects it and writes a private
* approval checkpoint. pi-subagents owns the supervisor and worker sessions, forks, resume, events,
* and Fleet controls.
*
* -- Pi/Codex
@@ -18,13 +18,14 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync
import { join, resolve } from "node:path";
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import { approvalMatches, approvalPath, goalBlock, hashGoalBlock, readApproval, repositoryState } from "./approval.js";
import { completeGoalDescription, completeGoalParamDescription, planDrafting, planningState, resync } from "./prompts.js";
import {
processWorkState,
registerGoalWorker,
resumeGoalWorker,
startGoalWorker,
steerGoalWorker,
registerGoalSupervisor,
resumeGoalSupervisor,
startGoalSupervisor,
steerGoalSupervisor,
subagentWorkState,
} from "./worker.js";
@@ -38,7 +39,6 @@ const PLAN_SHAPE = `${PLAN_DIR}/<session_id>-vN.md`;
// Plan mode blocks edit/write except for its plan file. bash remains available for read-only inspection. -- Pi/Codex
const PLAN_MODE_BLOCKED_TOOLS = ["edit", "write"];
const AUTO_DEFAULT_INTERVAL_MS = 60 * 60 * 1_000;
const SUPERVISOR_COMPACT_TOKENS = 100_000;
// A checkbox line beginning "goal:", used by the widget and supervisor scheduling.
// Everything else reads the file as prose.
@@ -90,6 +90,11 @@ export function nextPlanVersion(planNames: string[], sessionId: string): number
type Phase = "planning" | "working" | null;
/** Goal workers run in child Pi sessions, so they must not receive the main coordinator's tool gate. */
export function isSupervisorProcess(isSubagentChild = process.env.PI_SUBAGENT_CHILD === "1"): boolean {
return !isSubagentChild;
}
interface PlanState {
phase: Phase;
workerModel: string | null;
@@ -100,7 +105,7 @@ interface PlanState {
}
export default function piGoalsExtension(pi: ExtensionAPI): void {
if (process.env.PI_SUBAGENT_CHILD === "1") return;
if (!isSupervisorProcess()) return;
let state: PlanState = {
phase: null,
workerModel: null,
@@ -112,7 +117,6 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
let planningContextPending = false;
let autoTimer: ReturnType<typeof setTimeout> | null = null;
let supervisorWakePending = false;
let supervisorCompactionPending = false;
let workerRegistration: { dispose(): void } | null = null;
let workerRegistrationError: string | null = null;
let unsubscribeWorkerCompletion: (() => void) | null = null;
@@ -143,10 +147,10 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
workerRegistration = null;
workerRegistrationError = null;
try {
workerRegistration = registerGoalWorker(pi.events, state.workerModel);
workerRegistration = registerGoalSupervisor(pi.events, state.workerModel);
} catch (error) {
workerRegistrationError = error instanceof Error ? error.message : String(error);
if (state.phase === "working") ctx.ui.notify(`Goal worker unavailable: ${workerRegistrationError}`, "warning");
if (state.phase === "working") ctx.ui.notify(`Goal supervisor unavailable: ${workerRegistrationError}`, "warning");
}
}
@@ -163,8 +167,8 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
workerCompletionsDuringLaunch.clear();
try {
const runId = state.workerRunId
? await resumeGoalWorker(pi.events, state.workerRunId, task, signal)
: await startGoalWorker(pi.events, ctx.cwd, task, signal);
? await resumeGoalSupervisor(pi.events, state.workerRunId, task, signal)
: await startGoalSupervisor(pi.events, ctx.cwd, task, signal);
rememberWorkerRun(runId);
return runId;
} finally {
@@ -197,13 +201,29 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
return scanGoals(readPlan(ctx)).some((goal) => goal.status === "active" || goal.status === "open");
}
function supervisorTask(ctx: ExtensionContext, instruction: string): string {
const plan = readPlan(ctx);
const checkpoints = scanGoals(plan)
.filter((goal) => goal.status === "active" || goal.status === "open")
.map((goal) => `- ${JSON.stringify(goal.subject)}: ${approvalPath(ctx.cwd, ctx.sessionManager.getSessionId(), goal.subject)}`)
.join("\n");
return `${instruction}\n\nYou are the retained goal-supervisor. Here is the complete current plan; inspect its exact goal blocks and cited evidence before directing or approving work.\nPlan path: ${planPath(ctx)}\nPrivate approval checkpoints, one per current goal:\n${checkpoints || "(no open goals)"}\n\n${plan}`;
}
function wakeSupervisor(ctx: ExtensionContext, reason: string): void {
if (supervisorWakePending || state.phase !== "working" || !activeGoals(ctx)) return;
supervisorWakePending = true;
pi.sendUserMessage(
`<system-reminder>${reason}\nYou are the goal supervisor. Read ${planRel(ctx)} and inspect the cited evidence. Use CheckGoalWork before concluding that work stopped, and GuideGoalWorker to steer or resume the cheaper worker. Sign off a goal only after its discriminator is positively proved. Do not perform implementation work yourself.</system-reminder>`,
{ deliverAs: "followUp" },
);
void (async () => {
try {
const task = supervisorTask(ctx, `${reason}\nReview the current goal and either continue, redirect, or approve it through ApproveGoal.`);
if (state.workerPending && state.workerRunId) await steerGoalSupervisor(pi.events, state.workerRunId, task);
else await startOrResumeWorker(ctx, task);
} catch (error) {
ctx.ui.notify(`Goal supervisor check failed: ${error instanceof Error ? error.message : String(error)}`, "warning");
} finally {
supervisorWakePending = false;
}
})();
}
function scheduleSupervisorCheck(ctx: ExtensionContext): void {
@@ -216,25 +236,6 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
autoTimer.unref();
}
function compactSupervisor(ctx: ExtensionContext): void {
const usage = ctx.getContextUsage();
if (supervisorCompactionPending || usage?.tokens === null || usage?.tokens === undefined || usage.tokens < SUPERVISOR_COMPACT_TOKENS) return;
supervisorCompactionPending = true;
ctx.compact({
customInstructions: "__pi_vcc__ keep:1",
onComplete: (result) => {
supervisorCompactionPending = false;
if ((result.details as { compactor?: string } | undefined)?.compactor !== "pi-vcc") {
ctx.ui.notify("Goal supervisor compaction did not use pi-vcc. Install and load @sting8k/pi-vcc.", "warning");
}
},
onError: (error) => {
supervisorCompactionPending = false;
ctx.ui.notify(`Goal supervisor compaction failed: ${error.message}`, "warning");
},
});
}
function updateWidget(ctx: ExtensionContext): void {
if (state.phase === "planning") {
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("warning", "planning"));
@@ -248,16 +249,18 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
return;
}
const done = goals.filter((g) => g.status === "done").length;
const auto = state.autoIntervalMs === null ? "" : ` · supervise ${state.autoIntervalMs / 60_000}m`;
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("accent", ` ${done}/${goals.length} goals${auto}`));
const liveGoals = goals.filter((g) => g.status === "active" || g.status === "open");
const stateLabel = liveGoals.length > 0 ? " · supervising…" : " · complete";
const auto = liveGoals.length > 0 && state.autoIntervalMs !== null ? ` · supervise ${state.autoIntervalMs / 60_000}m` : "";
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("accent", ` ${done}/${goals.length} goals${stateLabel}${auto}`));
const mark: Record<GoalStatus, string> = { done: "✔", active: "▸", open: "◻", cancelled: "✗" };
// Only live goals get lines so finished work never pushes current work off screen. The active
// goal also shows its open subtasks: this file is the task list, so the widget is the task list.
// No path line: the session id makes it too long to be useful in the widget.
const plan = readPlan(ctx);
const lines: string[] = [];
for (const g of goals.filter((g) => g.status === "active" || g.status === "open")) {
lines.push(`${mark[g.status]} ${g.subject}`);
const lines: string[] = liveGoals.length === 0 ? ["✔ complete"] : [];
for (const g of liveGoals) {
lines.push(`${mark[g.status]} ${g.status === "active" ? "supervising… " : ""}${g.subject}`);
if (g.status === "active") lines.push(...openSubtasks(plan, g.line).slice(0, 3).map((s) => ctx.ui.theme.fg("muted", ` ${s}`)));
}
ctx.ui.setWidget(WIDGET_KEY, lines);
@@ -314,7 +317,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
state = { ...state, workerModel: ref || null, workerRunId: null, workerPending: false };
persist();
setupWorker(ctx);
ctx.ui.notify(ref ? `Goal-worker model set to ${ref}` : "Goal-worker model reset to pi-subagents default", "info");
ctx.ui.notify(ref ? `Goal-supervisor model set to ${ref}` : "Goal-supervisor model reset to pi-subagents default", "info");
return;
}
state = { ...state, phase: "planning", workerRunId: null, workerPending: false, planVersion: nextVersion(ctx), autoIntervalMs: null };
@@ -348,7 +351,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
supervisorWakePending = false;
if (state.phase === "working") {
return {
systemPrompt: `${ctx.getSystemPrompt()}\n\nYou are the research supervisor for ${planRel(ctx)}. Keep the high-level goal and the human's intent stable. The retained goal-worker owns implementation and plan updates; direct it with GuideGoalWorker instead of implementing work yourself. Read cited artifacts before calling CompleteGoal. A pi-subagents completion can wake you while nested subagents or managed processes are still running, so call CheckGoalWork before concluding that work stopped. At hourly checks, inspect progress and steer the worker only when a concrete correction is useful. Keep work going until all goals are proved or the human stops it. -- Pi/Codex`,
systemPrompt: `${ctx.getSystemPrompt()}\n\nYou are the thin human-facing coordinator for ${planRel(ctx)}. The retained goal-supervisor owns nested-worker control and acceptance. Keep the human intent stable, inspect progress with read-only tools, and direct the supervisor through GuideGoalWorker. Built-in edit/write and write-like shell commands are blocked. CompleteGoal is a mechanical sign-off only: it fails closed unless the supervisor's private approval checkpoint still matches the exact goal, plan block, committed HEAD/tree, and clean worktree. This is not a filesystem sandbox: allowed verification scripts and other custom tools can still mutate. Do not approve implementation by prose alone. -- Pi/Codex`,
};
}
if (!planningContextPending) return;
@@ -380,14 +383,24 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
});
pi.on("tool_call", async (event, ctx) => {
if (state.phase !== "planning") return;
if (PLAN_MODE_BLOCKED_TOOLS.includes(event.toolName)) {
const target = (event.input as { path?: string }).path;
if (target && resolve(ctx.cwd, target) === resolve(planPath(ctx))) return;
return { block: true, reason: `Planning is read-only: only ${planRel(ctx)} may be written. Agree the plan, then choose Ready.` };
if (state.phase === "planning") {
if (PLAN_MODE_BLOCKED_TOOLS.includes(event.toolName)) {
const target = (event.input as { path?: string }).path;
if (target && resolve(ctx.cwd, target) === resolve(planPath(ctx))) return;
return { block: true, reason: `Planning is read-only: only ${planRel(ctx)} may be written. Agree the plan, then choose Ready.` };
}
if (event.toolName === "bash" && !isPlanningReadOnlyCommand(String((event.input as { command?: string }).command))) {
return { block: true, reason: "Planning is read-only: inspect facts without writes or pipes, then put the change in the plan." };
}
return;
}
if (event.toolName === "bash" && !isPlanningReadOnlyCommand(String((event.input as { command?: string }).command))) {
return { block: true, reason: "Planning is read-only: inspect facts without writes or pipes, then put the change in the plan." };
if (state.phase === "working") {
if (PLAN_MODE_BLOCKED_TOOLS.includes(event.toolName)) {
return { block: true, reason: "Working supervision is read-only: direct implementation and evidence writes to GuideGoalWorker. CompleteGoal is the explicit sign-off control." };
}
if (event.toolName === "bash" && !isSupervisorReadOnlyCommand(String((event.input as { command?: string }).command))) {
return { block: true, reason: "Working supervision allows inspection and standard verification commands only. Direct file changes belong to GuideGoalWorker; this is not a full sandbox for custom tools or allowed scripts." };
}
}
});
@@ -400,7 +413,6 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
// PI: Print after Pi settles. agent_end is still streaming, so its message queues behind the menu.
pi.on("agent_settled", async (_event, ctx) => {
if (state.phase === "working") {
compactSupervisor(ctx);
scheduleSupervisorCheck(ctx);
return;
}
@@ -416,7 +428,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
printed = plan;
pi.sendMessage({ customType: "plan", content: plan, display: true });
}
const choice = await ctx.ui.select(`Plan drafted in ${planRel(ctx)}.`, ["Ready", "Refine", "Edit", "Cancel"]);
const choice = await ctx.ui.select(`Plan drafted in ${planRel(ctx)}.`, ["Ready", "Ready (compact)", "Refine", "Edit", "Cancel"]);
if (choice === "Refine") {
const notes = await ctx.ui.editor("What should change about the plan?", "");
if (!notes?.trim()) continue;
@@ -438,18 +450,35 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
ctx.ui.notify("Plan discarded.", "info");
return;
}
if (choice !== "Ready") return;
state = { ...state, phase: "working", autoIntervalMs: AUTO_DEFAULT_INTERVAL_MS };
resyncReason = "The plan was approved.";
persist();
updateWidget(ctx);
try {
await startOrResumeWorker(ctx, `Work the goals in ${planRel(ctx)}. Mark one open goal active, execute its subtasks, and record exact evidence in the plan Log. Report progress and evidence to the main research supervisor. Do not sign off goals.`);
scheduleSupervisorCheck(ctx);
} catch (error) {
ctx.ui.notify(`Goal worker could not start: ${error instanceof Error ? error.message : String(error)}`, "warning");
wakeSupervisor(ctx, "The approved goal worker failed to start.");
}
if (choice !== "Ready" && choice !== "Ready (compact)") return;
const startWorking = async (): Promise<boolean> => {
state = { ...state, phase: "working", autoIntervalMs: AUTO_DEFAULT_INTERVAL_MS };
resyncReason = "The plan was approved.";
persist();
updateWidget(ctx);
try {
await startOrResumeWorker(ctx, supervisorTask(ctx, "Start by launching or resuming the nested goal-worker. Then supervise the current plan."));
scheduleSupervisorCheck(ctx);
return true;
} catch (error) {
ctx.ui.notify(`Goal supervisor could not start: ${error instanceof Error ? error.message : String(error)}`, "warning");
state = { ...state, phase: "planning", autoIntervalMs: null };
persist();
updateWidget(ctx);
return false;
}
};
const started = await startWorking();
if (!started || choice === "Ready") return;
ctx.compact({
onComplete: () => {
resyncReason = "The main coordinator was compacted after the retained supervisor started.";
ctx.ui.notify("Main-session compaction completed; the retained supervisor and worker kept their contexts.", "info");
},
onError: (error) => {
ctx.ui.notify(`Main-session compaction failed; the retained supervisor continues: ${error.message}`, "warning");
},
});
return;
}
});
@@ -501,23 +530,24 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
pi.registerTool({
name: "GuideGoalWorker",
label: "Guide goal worker",
description: "Send one concrete instruction to the retained goal-worker. A live worker is steered; a completed worker is resumed with its saved context.",
label: "Guide goal supervisor",
description: "Send one concrete instruction to the retained goal-supervisor. A live supervisor is steered; a completed supervisor is resumed with its saved context and the full current plan.",
parameters: Type.Object({
instruction: Type.String({ description: "The next research or implementation action, with the evidence that should distinguish success from failure." }),
}),
async execute(_id, params, signal, _onUpdate, ctx) {
if (state.phase !== "working") return result("Approve a plan with Ready before directing the goal worker.", true);
if (state.phase !== "working") return result("Approve a plan with Ready before directing the goal supervisor.", true);
const task = supervisorTask(ctx, params.instruction);
try {
if (state.workerPending) {
if (!state.workerRunId) throw new Error("Goal-worker state says running but has no run ID.");
await steerGoalWorker(pi.events, state.workerRunId, params.instruction, signal);
return result(`Instruction delivered to live goal worker ${state.workerRunId}.`);
if (!state.workerRunId) throw new Error("Goal-supervisor state says running but has no run ID.");
await steerGoalSupervisor(pi.events, state.workerRunId, task, signal);
return result(`Instruction delivered to live goal supervisor ${state.workerRunId}.`);
}
const runId = await startOrResumeWorker(ctx, params.instruction, signal);
return result(`Goal worker resumed as ${runId}.`);
const runId = await startOrResumeWorker(ctx, task, signal);
return result(`Goal supervisor resumed as ${runId}.`);
} catch (error) {
return result(`Goal-worker guidance failed: ${error instanceof Error ? error.message : String(error)}`, true);
return result(`Goal-supervisor guidance failed: ${error instanceof Error ? error.message : String(error)}`, true);
}
},
});
@@ -531,11 +561,32 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
}),
async execute(_id, params, _signal, _onUpdate, ctx) {
if (state.phase !== "working") return result("Planning is not approved. Choose Ready before signing off a goal.", true);
const workState = await subagentWorkState(pi.events);
if (workState !== "idle") return result(`Goal sign-off blocked while supervisor work is ${workState}.`, true);
const plan = readPlan(ctx);
if (!plan.trim()) return result(`No plan file at ${planRel(ctx)}. Run /goals to draft one.`, true);
const block = goalBlock(plan, params.goal);
if (!block) return result(`No unique open goal line matched "${params.goal}" in ${planRel(ctx)}.`, true);
let repository: ReturnType<typeof repositoryState>;
try {
repository = repositoryState(ctx.cwd);
} catch (error) {
return result(`Goal sign-off could not inspect the repository: ${error instanceof Error ? error.message : String(error)}`, true);
}
if (!repository.cleanWorktree) return result("Goal sign-off blocked: worktree is dirty.", true);
const approval = readApproval(approvalPath(ctx.cwd, ctx.sessionManager.getSessionId(), params.goal));
if (!approvalMatches(approval, {
goal: params.goal,
planPath: planPath(ctx),
goalBlockHash: hashGoalBlock(block),
repoRoot: repository.repoRoot,
head: repository.head,
tree: repository.tree,
cleanWorktree: repository.cleanWorktree,
})) return result("Goal sign-off blocked: no matching supervisor approval checkpoint. Request a fresh supervisor review.", true);
const ticked = tickGoal(plan, params.goal);
if (!ticked) return result(`No unique exact goal line matched "${params.goal}" in ${planRel(ctx)}.`, true);
writePlan(ctx, appendLog(ticked, `${stamp()} signed off "${params.goal}" by the main research supervisor`));
writePlan(ctx, appendLog(ticked, `${stamp()} mechanically signed off "${params.goal}" after matching supervisor approval`));
updateWidget(ctx);
return result(`Sign-off accepted. Goal ticked [x] in ${planRel(ctx)}.`);
},
@@ -553,6 +604,18 @@ function isPlanningReadOnlyCommand(command: string): boolean {
return command.split(/&&|;/).every((part) => /^(?:cd\b|pwd|ls\b|git\s+(?:status|log|diff|show|branch)\b|rg\b|grep\b|find\b|head\b|tail\b|wc\b|stat\b|test\b)\b/.test(part.trim()));
}
/** The supervisor may inspect and use ordinary project checks. It is not a shell sandbox: package
* scripts and custom tools retain their normal process permissions, so implementation still belongs
* to the worker by contract as well as this direct-tool gate. */
export function isSupervisorReadOnlyCommand(command: string): boolean {
if (/[|><`$]/.test(command)) return false;
const safeArgs = "(?:\\s+[A-Za-z0-9_./:=,'\"@+%-]+)*";
const inspection = new RegExp(`^(?:cd|pwd|ls|rg|grep|find|head|tail|wc|stat|test)${safeArgs}$`);
const git = new RegExp(`^git\\s+(?:status|log|diff|show|branch|ls-files|grep|check-ignore)${safeArgs}$`);
const verification = new RegExp(`^(?:npm\\s+test|npm\\s+run\\s+(?:test|typecheck|lint)|npx\\s+tsc\\s+--noEmit)${safeArgs}$`);
return command.split(/&&|;/).every((part) => inspection.test(part.trim()) || git.test(part.trim()) || verification.test(part.trim()));
}
/** Local time, not UTC: agents freehand-stamp their manual ## Log lines from the local clock they
* see, so a UTC tool stamp made the trail read as two different afternoons (dogfood finding). */
function stamp(): string {
+14 -13
View File
@@ -3,8 +3,8 @@
*
* Design: the plan file is for LLMs and the human, not for TypeScript. No parser and no schema;
* the skeleton below is a convention the drafting prompt teaches, the worker maintains with its
* normal Edit tool, and the main research supervisor reads natively. The harness provides format
* guidance, one full-plan resync after context loss, and a retained pi-subagents worker.
* normal Edit tool, and the retained goal-supervisor reads natively. The harness provides format
* guidance, one full-plan resync after context loss, and retained pi-subagents supervisor and worker sessions.
*
* THE FOLD: everything above "## Log" is the short current-goal section. Everything below it
* (Log, Learnings, Appendix) is durable memory: unlimited, read on demand, and sent in full at
@@ -69,7 +69,7 @@ Style: Make it easy for a busy and forgetfull user to review. Use ASD-STE100 Sim
the same word for the same thing, and define a new terms at first use. Use redundant context for skim readers e.g. "our output - the cells, CV tag" is easy to read and reminds context. This covers the context
paragraph and the appendix too, not just the checklist. No all-caps headers and no bold spam. Just write less, add your voice less, persuade less, and burden the reader less.
Write the plan file in roughly this shape -- the file is read directly by the human and the main supervisor, so clarity beats conformance; small deviations are fine):
Write the plan file in roughly this shape -- the file is read directly by the human and the retained supervisor, so clarity beats conformance; small deviations are fine):
# <short plan title>
@@ -89,7 +89,7 @@ Write the plan file in roughly this shape -- the file is read directly by the hu
- subtle failure mode: <a way this could look done but isn't>
- discriminator: <the concrete observation that tells real success from that failure>
- verify: <optional shell command that exits 0 only when the discriminator passes; omit if not
testable. The worker runs it and saves its output; the main supervisor reads the evidence>
testable. The worker runs it and saves its output; the retained supervisor reads the evidence>
- tasks:
1. [ ] <subtask>
- evidence: (empty until sign-off)
@@ -119,7 +119,7 @@ Conventions:
none of the failure modes could fake. Ruling out failures is necessary, not sufficient.
- Make the discriminator a concrete, checkable observation about a real artifact (a file, a test
result, a committed diff, a metric), never about the plan file's own checkbox.
- evidence stays empty at planning; the worker fills it and the main research supervisor checks it.
- evidence stays empty at planning; the worker fills it and the retained supervisor checks it.
Cite durable artifacts a future reader can open: committed files, test names, git diffs. .pi/ is
usually gitignored, so files there prove things only at supervisor review time, not in history.
- User-visible result: restate the original deliverable, not the proposed implementation. Every goal
@@ -177,13 +177,14 @@ export const completeGoalDescription =
"table plus how to read it, a metric plus what it shows -- not a bare claim). Quote verbatim from " +
"output you actually observed; never reconstruct numbers from memory. If you couldn't see an " +
"output, rerun it or write that you couldn't -- an honest gap beats a plausible fabrication. If " +
"the goal names a verify: command, run it yourself first and save its output to a file cited in " +
"the evidence. The main research supervisor must reject a claimed pass with no saved " +
"output. The read must show success POSITIVELY happened, not just that failures were avoided. " +
"Check that the claimed result uses the artifact and outcome named in User-visible result and does " +
"not substitute an agent-inferred deliverable. Then call this with the goal's text (the line after " +
"'goal:'). You are the main research supervisor: reread the complete plan and inspect the live " +
"working tree, including uncommitted changes, before calling. The tool appends the sign-off to ## Log " +
"and ticks the goal [x]. If the evidence is missing, direct the goal-worker to obtain it instead.";
"the goal names a verify: command, direct the worker to run it and save its output to a file cited " +
"in the evidence. The supervisor may run an allowed read-only verification command, but must not " +
"create the evidence file itself. The retained goal-supervisor must reject a claimed pass with no " +
"saved output. The read must show success POSITIVELY happened, not just that failures were avoided. " +
"The supervisor records an approval checkpoint only after it inspected the current plan, repository, " +
"evidence, and verify output with no active nested worker and a clean committed worktree. Then the main " +
"coordinator calls this tool with the exact goal text. This tool independently checks that checkpoint " +
"against the exact current goal block, HEAD/tree, and clean worktree before it appends the sign-off to " +
"## Log and ticks the goal [x]. If any check differs, it fails closed and requires a fresh supervisor review.";
export const completeGoalParamDescription = "The goal's text: the line after 'goal:' in the plan file.";
+87
View File
@@ -0,0 +1,87 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import { goalBlock, hashGoalBlock, repositoryState, writeApproval } from "./approval.js";
import { isSupervisorReadOnlyCommand } from "./index.js";
import { registerGoalWorker, subagentWorkState } from "./worker.js";
const APPROVE_GOAL = "ApproveGoal";
function result(text: string, isError = false) {
return { content: [{ type: "text" as const, text }], details: {}, isError };
}
export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
let workerRegistration: { dispose(): void } | null = null;
pi.on("session_start", async (_event, ctx) => {
workerRegistration?.dispose();
workerRegistration = registerGoalWorker(pi.events, null);
ctx.ui.notify("Goal supervisor can now launch its retained worker.", "info");
});
pi.on("session_shutdown", async () => {
workerRegistration?.dispose();
workerRegistration = null;
});
pi.on("tool_call", async (event) => {
if (event.toolName === "edit" || event.toolName === "write") {
return { block: true, reason: "Goal supervision is read-only. Direct project changes to the nested goal-worker." };
}
if (event.toolName === "bash" && !isSupervisorReadOnlyCommand(String((event.input as { command?: string }).command))) {
return { block: true, reason: "Goal supervision allows inspection and standard verification commands only. This is not a full sandbox for custom tools or allowed scripts." };
}
});
pi.registerTool({
name: APPROVE_GOAL,
label: "Approve goal",
description: "Record an auditable approval only after the supervisor inspected the plan, repository, cited evidence, and saved verification output. It fails closed while nested work is active or the repository is dirty.",
parameters: Type.Object({
goal: Type.String({ description: "Exact current goal text from the approved plan." }),
planPath: Type.String({ description: "Absolute path to the current plan file." }),
checkpointPath: Type.String({ description: "Exact private approval-record path supplied by the main coordinator for this goal." }),
inspectedPlan: Type.Literal(true),
inspectedRepository: Type.Literal(true),
inspectedEvidence: Type.Literal(true),
inspectedVerifyOutput: Type.Literal(true),
}),
async execute(_id, params, _signal, _onUpdate, ctx) {
const workState = await subagentWorkState(pi.events);
if (workState !== "idle") return result(`Cannot approve while nested worker status is ${workState}.`, true);
const planPath = resolve(params.planPath);
let plan: string;
let repository: ReturnType<typeof repositoryState>;
try {
plan = readFileSync(planPath, "utf8");
repository = repositoryState(ctx.cwd);
} catch (error) {
return result(`Cannot inspect approval inputs: ${error instanceof Error ? error.message : String(error)}`, true);
}
if (!repository.cleanWorktree) return result("Cannot approve with a dirty worktree. Commit the worker changes first.", true);
const block = goalBlock(plan, params.goal);
if (!block) return result(`Cannot approve: no unique open goal matches "${params.goal}".`, true);
const record = {
version: 1 as const,
verdict: "accept" as const,
goal: params.goal,
planPath,
goalBlockHash: hashGoalBlock(block),
repoRoot: repository.repoRoot,
head: repository.head,
tree: repository.tree,
cleanWorktree: true as const,
inspected: { plan: true as const, repository: true as const, evidence: true as const, verifyOutput: true as const },
supervisor: { sessionId: ctx.sessionManager.getSessionId(), runId: process.env.PI_SUBAGENT_RUN_ID ?? null },
timestamp: new Date().toISOString(),
};
const path = resolve(params.checkpointPath);
const approvalRoot = resolve(ctx.cwd, ".pi", "pi-goals", "approvals");
if (!path.startsWith(`${approvalRoot}/`)) return result("Approval checkpoint must stay in private .pi/pi-goals/approvals state.", true);
writeApproval(path, record);
return result(`Approval recorded at ${path} for "${params.goal}".`);
},
});
}
-33
View File
@@ -1,33 +0,0 @@
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
const PREPARED_FORK = "pi-goals-worker-fork-prepared";
export default function workerRuntime(pi: ExtensionAPI): void {
pi.on("session_start", async (_event, ctx) => {
const prepared = ctx.sessionManager
.getEntries()
.some((entry: { type?: string; customType?: string }) => entry.type === "custom" && entry.customType === PREPARED_FORK);
if (prepared) return;
await new Promise<void>((resolve, reject) => {
ctx.compact({
customInstructions: "__pi_vcc__ keep:0",
onComplete: (result) => {
if ((result.details as { compactor?: string } | undefined)?.compactor !== "pi-vcc") {
reject(new Error("Goal-worker fork was not compacted by pi-vcc."));
return;
}
pi.appendEntry(PREPARED_FORK, { version: 1, compacted: true, compactor: "pi-vcc" });
resolve();
},
onError: (error) => {
if (error.message !== "Nothing to compact (session too small)") {
reject(error);
return;
}
pi.appendEntry(PREPARED_FORK, { version: 1, compacted: false, reason: "below-compactable-size" });
resolve();
},
});
});
});
}
+48 -17
View File
@@ -6,6 +6,7 @@ const RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
const RPC_REPLY_PREFIX = "subagents:rpc:v1:reply:";
const RPC_VERSION = 1;
const RPC_TIMEOUT_MS = 15_000;
export const SUPERVISOR_AGENT = "goal-supervisor";
export const WORKER_AGENT = "goal-worker";
interface EventBus {
@@ -38,26 +39,56 @@ interface AsyncSnapshot {
export type WorkState = "active" | "idle" | "unknown";
export const workerSystemPrompt = `You are the implementation worker for one supervised Pi session.
Work autonomously from the approved plan. Keep the plan current, run the real checks, and leave
specific evidence in its Log. The human's latest message outranks the plan; update affected goals
instead of defending an obsolete decision. The main Pi agent is the research supervisor and owns
direction and goal sign-off. Send contact_supervisor progress updates when evidence changes the research direction,
when an hourly check asks for one, or when you need a decision. Do not claim a goal is complete;
report the evidence and let the supervisor decide. Continue until the plan is complete or the human
stops the session. -- Pi/Codex`;
export const supervisorSystemPrompt = `You are the retained goal supervisor. The main Pi session is a thin human-facing coordinator.
You own the current plan review and the retained implementation worker. At every review, reread the full
current plan named in your task, identify the exact goal block, inspect the repository, cited artifacts, and
saved verification output, then launch, resume, or steer the nested goal-worker as needed. Do not edit project
files. Use read/search and standard verification commands only. The worker is the sole implementation writer and
must commit its changes before you consider approval. When no nested work is active, HEAD is committed, the worktree
is clean, and you have explicitly inspected the plan, repository, evidence, and verification output, call ApproveGoal.
Otherwise return continue or redirect the worker. Do not claim acceptance in prose: only ApproveGoal creates the durable
approval checkpoint. -- Pi/Codex`;
export const workerSystemPrompt = `You are the retained implementation worker for one goal supervisor.
Work autonomously from the approved plan. The latest human message outranks the plan. Keep the plan current, run the real checks, commit the implementation,
and leave specific evidence in its Log. Your goal supervisor owns direction and approval. Send contact_supervisor
progress updates when evidence changes the research direction, when an hourly check asks for one, or when you need a
decision. Do not claim a goal is complete; report the evidence and let the supervisor decide. -- Pi/Codex`;
export function registerGoalSupervisor(events: EventBus, model: string | null): Registration {
const supervisorRuntime = fileURLToPath(new URL("./supervisor-runtime.ts", import.meta.url));
const request: Record<string, unknown> = {
version: 1,
name: SUPERVISOR_AGENT,
definition: {
description: "Read-only supervisor that owns a nested retained implementation worker.",
systemPrompt: supervisorSystemPrompt,
allowNestedSubagents: true,
subagentOnlyExtensions: [supervisorRuntime],
...(model ? { model } : {}),
systemPromptMode: "replace",
inheritProjectContext: true,
inheritGlobalContext: true,
inheritSkills: true,
defaultContext: "fork",
defaultAsync: true,
defaultProgress: true,
},
};
events.emit(REGISTER_EVENT, request);
const result = request.result as { ok?: boolean; registration?: Registration; error?: Error } | undefined;
if (!result) throw new Error("pi-subagents is not installed or not ready.");
if (!result.ok || !result.registration) throw result.error ?? new Error("pi-subagents rejected the goal-supervisor agent.");
return result.registration;
}
export function registerGoalWorker(events: EventBus, model: string | null): Registration {
const runtimeExtension = fileURLToPath(new URL("./worker-runtime.ts", import.meta.url));
const piVccExtension = fileURLToPath(import.meta.resolve("@sting8k/pi-vcc"));
const request: Record<string, unknown> = {
version: 1,
name: WORKER_AGENT,
definition: {
description: "Implementation worker directed by the main goal supervisor.",
description: "Implementation worker directed by the retained goal supervisor.",
systemPrompt: workerSystemPrompt,
allowNestedSubagents: true,
subagentOnlyExtensions: [piVccExtension, runtimeExtension],
...(model ? { model } : {}),
systemPromptMode: "replace",
inheritProjectContext: true,
@@ -112,9 +143,9 @@ function asyncRunId(data: RpcData): string {
return runId;
}
export async function startGoalWorker(events: EventBus, cwd: string, task: string, signal?: AbortSignal): Promise<string> {
export async function startGoalSupervisor(events: EventBus, cwd: string, task: string, signal?: AbortSignal): Promise<string> {
const data = await rpc(events, "spawn", {
agent: WORKER_AGENT,
agent: SUPERVISOR_AGENT,
task,
cwd,
context: "fork",
@@ -124,11 +155,11 @@ export async function startGoalWorker(events: EventBus, cwd: string, task: strin
return asyncRunId(data);
}
export async function resumeGoalWorker(events: EventBus, runId: string, task: string, signal?: AbortSignal): Promise<string> {
export async function resumeGoalSupervisor(events: EventBus, runId: string, task: string, signal?: AbortSignal): Promise<string> {
return asyncRunId(await rpc(events, "resume", { id: runId, message: task }, signal));
}
export async function steerGoalWorker(events: EventBus, runId: string, task: string, signal?: AbortSignal): Promise<void> {
export async function steerGoalSupervisor(events: EventBus, runId: string, task: string, signal?: AbortSignal): Promise<void> {
await rpc(events, "steer", { id: runId, message: task, mode: "steer" }, signal);
}
+167 -25
View File
@@ -1,9 +1,11 @@
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { describe, expect, it, vi } from "vitest";
import piGoalsExtension from "../src/index.js";
import { approvalPath, goalBlock, hashGoalBlock, repositoryState, writeApproval } from "../src/approval.js";
import piGoalsExtension, { isSupervisorProcess } from "../src/index.js";
function setup(
selectChoices: Array<string | undefined>,
@@ -11,8 +13,13 @@ function setup(
editPlan?: () => Promise<string | undefined>,
contextTokens = 0,
completeWorkerBeforeReply = false,
compactError?: Error,
) {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-flow-"));
writeFileSync(join(cwd, ".gitignore"), ".pi/\n");
execFileSync("git", ["init", "-q"], { cwd });
execFileSync("git", ["add", ".gitignore"], { cwd });
execFileSync("git", ["-c", "user.name=test", "-c", "user.email=test@example.com", "commit", "-qm", "initial"], { cwd });
const commands = new Map<string, any>();
const hooks = new Map<string, any>();
const tools = new Map<string, any>();
@@ -21,6 +28,9 @@ function setup(
const messages: Array<{ content: string; display?: boolean }> = [];
const rpcRequests: any[] = [];
const compactCalls: any[] = [];
const notifications: string[] = [];
const statuses: Array<string | undefined> = [];
const widgets: Array<string[] | undefined> = [];
const eventHandlers = new Map<string, Set<(data: unknown) => void>>();
const eventBus = {
on(name: string, handler: (data: unknown) => void) {
@@ -64,15 +74,16 @@ function setup(
getContextUsage: () => ({ tokens: contextTokens }),
compact: (options: any) => {
compactCalls.push(options);
options.onComplete?.({ summary: "summary", details: { compactor: "pi-vcc" } });
if (compactError) options.onError?.(compactError);
else options.onComplete?.({ summary: "summary" });
},
getSystemPrompt: () => "base prompt",
sessionManager: { getSessionId: () => "session-a", getEntries: () => entries },
ui: {
theme: { fg: (_kind: string, text: string) => text },
setStatus: () => {},
setWidget: () => {},
notify: () => {},
setStatus: (_key: string, text: string | undefined) => statuses.push(text),
setWidget: (_key: string, lines: string[] | undefined) => widgets.push(lines),
notify: (text: string) => notifications.push(text),
select: async () => {
eventLog.push("select");
return selectChoices.shift();
@@ -96,7 +107,29 @@ function setup(
sendUserMessage: (message: string) => messages.push({ content: message }),
};
piGoalsExtension(pi as unknown as ExtensionAPI);
return { commands, compactCalls, ctx, cwd, entries, events: eventLog, eventBus, hooks, messages, rpcRequests, tools };
return { commands, compactCalls, ctx, cwd, entries, events: eventLog, eventBus, hooks, messages, notifications, rpcRequests, statuses, tools, widgets };
}
function writeSupervisorApproval(flow: ReturnType<typeof setup>, goal: string): void {
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
const plan = readFileSync(planPath, "utf8");
const block = goalBlock(plan, goal);
if (!block) throw new Error("test plan has no open goal");
const repository = repositoryState(flow.cwd);
writeApproval(approvalPath(flow.cwd, "session-a", goal), {
version: 1,
verdict: "accept",
goal,
planPath,
goalBlockHash: hashGoalBlock(block),
repoRoot: repository.repoRoot,
head: repository.head,
tree: repository.tree,
cleanWorktree: true,
inspected: { plan: true, repository: true, evidence: true, verifyOutput: true },
supervisor: { sessionId: "supervisor-session", runId: "supervisor-run" },
timestamp: "2026-09-05T00:00:00.000Z",
});
}
describe("/goals draft flow", () => {
@@ -192,10 +225,10 @@ describe("/goals draft flow", () => {
await flow.hooks.get("agent_settled")({}, flow.ctx);
expect(flow.events).toEqual(["display", "select"]);
expect(flow.rpcRequests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-worker", context: "fork" } });
expect(flow.rpcRequests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-supervisor", context: "fork" } });
expect(flow.messages.filter((message) => !message.display)).toHaveLength(1);
const supervisor = await flow.hooks.get("before_agent_start")({}, flow.ctx);
expect(supervisor.systemPrompt).toContain("research supervisor");
expect(supervisor.systemPrompt).toContain("thin human-facing coordinator");
} finally {
rmSync(flow.cwd, { recursive: true, force: true });
}
@@ -246,18 +279,16 @@ describe("/goals draft flow", () => {
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n");
await flow.hooks.get("agent_settled")({}, flow.ctx);
await flow.commands.get("goals").handler("auto 1", flow.ctx);
const checks = () => flow.messages.filter((message) => message.content.includes("supervisor check is due"));
await vi.advanceTimersByTimeAsync(30_000);
await flow.hooks.get("agent_settled")({}, flow.ctx);
await vi.advanceTimersByTimeAsync(30_000);
expect(checks()).toHaveLength(1);
await flow.hooks.get("before_agent_start")({}, flow.ctx);
expect(flow.rpcRequests).toHaveLength(2);
expect(flow.rpcRequests.at(-1)).toMatchObject({ method: "steer", params: { id: "worker-1", message: expect.stringContaining("supervisor check is due") } });
for (let n = 2; n <= 3; n++) {
for (let n = 3; n <= 4; n++) {
await vi.advanceTimersByTimeAsync(60_000);
expect(checks()).toHaveLength(n);
await flow.hooks.get("before_agent_start")({}, flow.ctx);
expect(flow.rpcRequests).toHaveLength(n);
await flow.hooks.get("agent_settled")({}, flow.ctx);
}
} finally {
@@ -266,16 +297,40 @@ describe("/goals draft flow", () => {
}
});
it("compacts the main supervisor with pi-vcc near 100k tokens", async () => {
const flow = setup(["Ready"], [], undefined, 100_000);
it("starts the supervisor before Ready (compact), while Ready preserves main context", async () => {
const ready = setup(["Ready"]);
const compacted = setup(["Ready (compact)"]);
try {
for (const flow of [ready, compacted]) {
await flow.commands.get("goals").handler("objective", flow.ctx);
writeFileSync(join(flow.cwd, ".pi/plan/session-a-v1.md"), "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n");
await flow.hooks.get("agent_settled")({}, flow.ctx);
}
expect(ready.compactCalls).toHaveLength(0);
expect(ready.rpcRequests).toHaveLength(1);
expect(compacted.compactCalls).toHaveLength(1);
expect(compacted.rpcRequests).toHaveLength(1);
const resync = await compacted.hooks.get("context")({ messages: [] }, compacted.ctx);
expect(resync.messages.at(-1).content[0].text).toContain("The main coordinator was compacted after the retained supervisor started.");
} finally {
rmSync(ready.cwd, { recursive: true, force: true });
rmSync(compacted.cwd, { recursive: true, force: true });
}
});
it("keeps the already-started supervisor when requested main compaction fails", async () => {
const flow = setup(["Ready (compact)"], [], undefined, 0, false, new Error("compactor unavailable"));
try {
await flow.commands.get("goals").handler("objective", flow.ctx);
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n");
await flow.hooks.get("agent_settled")({}, flow.ctx);
writeFileSync(join(flow.cwd, ".pi/plan/session-a-v1.md"), "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n");
await flow.hooks.get("agent_settled")({}, flow.ctx);
expect(flow.compactCalls).toHaveLength(1);
expect(flow.compactCalls[0].customInstructions).toBe("__pi_vcc__ keep:1");
expect(flow.rpcRequests).toHaveLength(1);
expect(flow.notifications).toContain("Main-session compaction failed; the retained supervisor continues: compactor unavailable");
const working = await flow.hooks.get("before_agent_start")({}, flow.ctx);
expect(working.systemPrompt).toContain("thin human-facing coordinator");
} finally {
rmSync(flow.cwd, { recursive: true, force: true });
}
@@ -337,7 +392,7 @@ describe("/goals draft flow", () => {
}
});
it("resumes the retained worker and lets the main supervisor sign off", async () => {
it("resumes the retained supervisor and lets the coordinator mechanically sign off", async () => {
const flow = setup(["Ready"]);
try {
await flow.hooks.get("session_start")({}, flow.ctx);
@@ -345,27 +400,114 @@ describe("/goals draft flow", () => {
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: produce report\n - discriminator: report.txt contains PASS\n - evidence:\n - report.txt: `PASS`\n\n## Log\n");
await flow.hooks.get("agent_settled")({}, flow.ctx);
expect(flow.rpcRequests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-worker", context: "fork" } });
expect(flow.rpcRequests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-supervisor", context: "fork" } });
flow.eventBus.emit("subagent:async-complete", { runId: "worker-1", results: [{ success: true }] });
const resumed = await flow.tools.get("GuideGoalWorker").execute("", { instruction: "Verify report.txt." }, undefined, undefined, flow.ctx);
expect(resumed.isError).toBe(false);
expect(flow.rpcRequests[1]).toMatchObject({ method: "resume", params: { id: "worker-1", message: "Verify report.txt." } });
expect(flow.rpcRequests[1]).toMatchObject({ method: "resume", params: { id: "worker-1", message: expect.stringContaining("Verify report.txt.") } });
expect(flow.entries.at(-1)?.data).toMatchObject({ workerRunId: "worker-2", workerPending: true });
writeSupervisorApproval(flow, "produce report");
const signoff = await flow.tools.get("CompleteGoal").execute("", { goal: "produce report" }, undefined, undefined, flow.ctx);
expect(signoff.isError).toBe(false);
expect(readFileSync(planPath, "utf-8")).toContain("1. [x] goal: produce report");
expect(readFileSync(planPath, "utf-8")).toContain("signed off \"produce report\" by the main research supervisor");
expect(readFileSync(planPath, "utf-8")).toContain("mechanically signed off \"produce report\" after matching supervisor approval");
await flow.hooks.get("session_start")({}, flow.ctx);
await flow.tools.get("GuideGoalWorker").execute("", { instruction: "Report current status." }, undefined, undefined, flow.ctx);
expect(flow.rpcRequests.at(-1)).toMatchObject({ method: "steer", params: { id: "worker-2", message: "Report current status." } });
expect(flow.rpcRequests.at(-1)).toMatchObject({ method: "steer", params: { id: "worker-2", message: expect.stringContaining("Report current status.") } });
} finally {
rmSync(flow.cwd, { recursive: true, force: true });
}
});
it("blocks main implementation while allowing supervisor inspection, control, and sign-off", async () => {
const flow = setup(["Ready"]);
try {
await flow.commands.get("goals").handler("objective", flow.ctx);
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n\n## Log\n");
await flow.hooks.get("agent_settled")({}, flow.ctx);
const edit = await flow.hooks.get("tool_call")({ toolName: "edit", input: { path: "README.md" } }, flow.ctx);
const write = await flow.hooks.get("tool_call")({ toolName: "write", input: { path: "README.md" } }, flow.ctx);
const shellWrite = await flow.hooks.get("tool_call")({ toolName: "bash", input: { command: "printf changed > README.md" } }, flow.ctx);
const inspect = await flow.hooks.get("tool_call")({ toolName: "read", input: { path: "README.md" } }, flow.ctx);
const verify = await flow.hooks.get("tool_call")({ toolName: "bash", input: { command: "git status && npm test && npm run typecheck && npm run lint" } }, flow.ctx);
const work = await flow.tools.get("CheckGoalWork").execute("", {}, undefined, undefined, flow.ctx);
const guide = await flow.tools.get("GuideGoalWorker").execute("", { instruction: "Save the verification output." }, undefined, undefined, flow.ctx);
writeSupervisorApproval(flow, "make the output");
const signoff = await flow.tools.get("CompleteGoal").execute("", { goal: "make the output" }, undefined, undefined, flow.ctx);
expect(edit?.block).toBe(true);
expect(write?.block).toBe(true);
expect(shellWrite?.block).toBe(true);
expect(inspect).toBeUndefined();
expect(verify).toBeUndefined();
expect(work.isError).toBe(false);
expect(guide.isError).toBe(false);
expect(signoff.isError).toBe(false);
expect(readFileSync(planPath, "utf-8")).toContain("1. [x] goal: make the output");
} finally {
rmSync(flow.cwd, { recursive: true, force: true });
}
});
it("fails closed without a matching supervisor approval checkpoint", async () => {
const flow = setup(["Ready"]);
try {
await flow.commands.get("goals").handler("objective", flow.ctx);
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n - evidence: verify.log: PASS\n");
await flow.hooks.get("agent_settled")({}, flow.ctx);
const missing = await flow.tools.get("CompleteGoal").execute("", { goal: "make the output" }, undefined, undefined, flow.ctx);
expect(missing.isError).toBe(true);
expect(missing.content[0].text).toContain("no matching supervisor approval checkpoint");
writeSupervisorApproval(flow, "make the output");
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n - evidence: verify.log: PASS after rerun\n");
const stale = await flow.tools.get("CompleteGoal").execute("", { goal: "make the output" }, undefined, undefined, flow.ctx);
expect(stale.isError).toBe(true);
expect(stale.content[0].text).toContain("no matching supervisor approval checkpoint");
writeSupervisorApproval(flow, "make the output");
writeFileSync(join(flow.cwd, "uncommitted.txt"), "dirty\n");
const dirty = await flow.tools.get("CompleteGoal").execute("", { goal: "make the output" }, undefined, undefined, flow.ctx);
expect(dirty.isError).toBe(true);
expect(dirty.content[0].text).toContain("worktree is dirty");
} finally {
rmSync(flow.cwd, { recursive: true, force: true });
}
});
it("labels live goals supervising and all-done goals complete", async () => {
const flow = setup(["Ready"]);
try {
await flow.commands.get("goals").handler("objective", flow.ctx);
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n");
await flow.hooks.get("agent_settled")({}, flow.ctx);
await flow.hooks.get("turn_end")({}, flow.ctx);
expect(flow.widgets.at(-1)).toEqual(["▸ supervising… make the output"]);
expect(flow.statuses.at(-1)).toContain("supervising…");
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [x] goal: make the output\n");
await flow.hooks.get("turn_end")({}, flow.ctx);
expect(flow.widgets.at(-1)).toEqual(["✔ complete"]);
expect(flow.statuses.at(-1)).toContain("1/1 goals · complete");
expect(flow.statuses.at(-1)).not.toContain("supervising…");
} finally {
rmSync(flow.cwd, { recursive: true, force: true });
}
});
it("does not install the supervisor gate in a worker child", () => {
expect(isSupervisorProcess(false)).toBe(true);
expect(isSupervisorProcess(true)).toBe(false);
});
it("gives the agent a planning snapshot and blocks work routes", async () => {
const flow = setup([]);
try {
+3 -3
View File
@@ -24,8 +24,8 @@ describe("planning prompt", () => {
expect(planDrafting).toContain("Take it from the original request, not from your implementation plan");
expect(planDrafting).toContain("Future work may not defer any artifact or action named there");
expect(resync("plan", ".pi/plan/test.md", "Compacted.")).toContain("amend the plan rather than preserving an obsolete decision");
expect(workerSystemPrompt).toContain("latest message outranks the plan");
expect(workerSystemPrompt).toContain("main Pi agent is the research supervisor");
expect(completeGoalDescription).toContain("inspect the live working tree");
expect(workerSystemPrompt).toContain("latest human message outranks the plan");
expect(workerSystemPrompt).toContain("goal supervisor owns direction and approval");
expect(completeGoalDescription).toContain("approval checkpoint only after it inspected");
});
});
+98
View File
@@ -0,0 +1,98 @@
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { approvalPath, readApproval } from "../src/approval.js";
import supervisorRuntime from "../src/supervisor-runtime.js";
class Events {
private handlers = new Map<string, Set<(data: unknown) => void>>();
on(event: string, handler: (data: unknown) => void): () => void {
const handlers = this.handlers.get(event) ?? new Set();
handlers.add(handler);
this.handlers.set(event, handlers);
return () => handlers.delete(handler);
}
emit(event: string, data: unknown): void {
for (const handler of [...(this.handlers.get(event) ?? [])]) handler(data);
}
}
function setup() {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-supervisor-"));
writeFileSync(join(cwd, ".gitignore"), ".pi/\n");
execFileSync("git", ["init", "-q"], { cwd });
execFileSync("git", ["add", ".gitignore"], { cwd });
execFileSync("git", ["-c", "user.name=test", "-c", "user.email=test@example.com", "commit", "-qm", "initial"], { cwd });
const hooks = new Map<string, any>();
const tools = new Map<string, any>();
const events = new Events();
let workerDefinition: Record<string, unknown> | undefined;
events.on("pi-subagents:runtime-agent-register:v1", (raw) => {
const request = raw as { definition: Record<string, unknown>; result?: unknown };
workerDefinition = request.definition;
request.result = { ok: true, registration: { dispose() {} } };
});
events.on("subagents:rpc:v1:request", (raw) => {
const request = raw as any;
events.emit(`subagents:rpc:v1:reply:${request.requestId}`, {
success: true,
data: { text: "idle", asyncSnapshot: { kind: "pi-subagents.async-status-snapshot", version: 1, omitted: { runs: 0, children: 0, byteLimitExceeded: false }, runs: [] } },
});
});
const ctx = {
cwd,
sessionManager: { getSessionId: () => "supervisor-session" },
ui: { notify() {} },
};
const pi = {
events,
on: (name: string, handler: any) => hooks.set(name, handler),
registerTool: (tool: any) => tools.set(tool.name, tool),
};
supervisorRuntime(pi as any);
return { cwd, ctx, hooks, tools, workerDefinition: () => workerDefinition };
}
describe("supervisor-only runtime", () => {
it("registers the nested worker and blocks direct supervisor writes", async () => {
const runtime = setup();
try {
await runtime.hooks.get("session_start")({}, runtime.ctx);
expect(runtime.workerDefinition()?.description).toContain("Implementation worker");
expect((await runtime.hooks.get("tool_call")({ toolName: "edit", input: { path: "README.md" } }, runtime.ctx))?.block).toBe(true);
expect((await runtime.hooks.get("tool_call")({ toolName: "bash", input: { command: "git status && npm test" } }, runtime.ctx))).toBeUndefined();
} finally {
rmSync(runtime.cwd, { recursive: true, force: true });
}
});
it("writes an approval only after inspecting the plan and confirming a clean worktree at a commit", async () => {
const runtime = setup();
try {
await runtime.hooks.get("session_start")({}, runtime.ctx);
const planPath = join(runtime.cwd, ".pi/plan/session-a-v1.md");
mkdirSync(join(runtime.cwd, ".pi/plan"), { recursive: true });
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: ship it\n - evidence: verify.log: PASS\n");
const checkpoint = approvalPath(runtime.cwd, "main-session", "ship it");
const accepted = await runtime.tools.get("ApproveGoal").execute("", {
goal: "ship it",
planPath,
checkpointPath: checkpoint,
inspectedPlan: true,
inspectedRepository: true,
inspectedEvidence: true,
inspectedVerifyOutput: true,
}, undefined, undefined, runtime.ctx);
expect(accepted.isError).toBe(false);
expect(readApproval(checkpoint)).toMatchObject({ version: 1, verdict: "accept", goal: "ship it", supervisor: { sessionId: "supervisor-session" } });
expect(readFileSync(checkpoint, "utf8")).toContain('"goalBlockHash"');
} finally {
rmSync(runtime.cwd, { recursive: true, force: true });
}
});
});
-45
View File
@@ -1,45 +0,0 @@
import { describe, expect, it } from "vitest";
import workerRuntime from "../src/worker-runtime.js";
function setup(entries: object[], details: object = { compactor: "pi-vcc" }, compactError?: Error) {
const hooks = new Map<string, any>();
const appended: Array<{ type: string; data: unknown }> = [];
const pi = {
on: (name: string, handler: any) => hooks.set(name, handler),
appendEntry: (type: string, data: unknown) => appended.push({ type, data }),
};
const ctx = {
sessionManager: { getEntries: () => [...entries, ...appended.map(({ type, data }) => ({ type: "custom", customType: type, data }))] },
compact: ({ customInstructions, onComplete, onError }: any) => {
if (compactError) onError(compactError);
else onComplete({ summary: "summary", firstKeptEntryId: "", tokensBefore: 1000, details, customInstructions });
},
};
workerRuntime(pi as any);
return { hooks, ctx, appended };
}
describe("goal-worker fork compaction", () => {
it("requires pi-vcc before the first worker turn and records completion", async () => {
const runtime = setup([]);
await runtime.hooks.get("session_start")({}, runtime.ctx);
expect(runtime.appended).toEqual([{ type: "pi-goals-worker-fork-prepared", data: { version: 1, compacted: true, compactor: "pi-vcc" } }]);
});
it("records when the exact fork is already too small to compact", async () => {
const runtime = setup([], undefined, new Error("Nothing to compact (session too small)"));
await runtime.hooks.get("session_start")({}, runtime.ctx);
expect(runtime.appended).toEqual([{ type: "pi-goals-worker-fork-prepared", data: { version: 1, compacted: false, reason: "below-compactable-size" } }]);
});
it("does not prepare the retained worker again after resume", async () => {
const runtime = setup([{ type: "custom", customType: "pi-goals-worker-fork-prepared" }]);
await runtime.hooks.get("session_start")({}, runtime.ctx);
expect(runtime.appended).toEqual([]);
});
it("fails if another compactor handled the fork", async () => {
const runtime = setup([], { compactor: "other" });
await expect(runtime.hooks.get("session_start")({}, runtime.ctx)).rejects.toThrow("not compacted by pi-vcc");
});
});
+30 -15
View File
@@ -1,11 +1,13 @@
import { describe, expect, it } from "vitest";
import {
processWorkState,
registerGoalSupervisor,
registerGoalWorker,
resumeGoalWorker,
startGoalWorker,
steerGoalWorker,
resumeGoalSupervisor,
startGoalSupervisor,
steerGoalSupervisor,
subagentWorkState,
supervisorSystemPrompt,
workerSystemPrompt,
} from "../src/worker.js";
@@ -31,8 +33,8 @@ function replyToRpc(events: Events, inspect: (request: any) => object): void {
});
}
describe("goal worker registration", () => {
it("registers one retained implementation worker", () => {
describe("goal hierarchy registration", () => {
it("registers a retained supervisor that can load the worker-only runtime", () => {
const events = new Events();
let definition: Record<string, unknown> | undefined;
events.on("pi-subagents:runtime-agent-register:v1", (raw) => {
@@ -41,17 +43,30 @@ describe("goal worker registration", () => {
request.result = { ok: true, registration: { dispose() {} } };
});
registerGoalWorker(events, "provider/cheap-model");
registerGoalSupervisor(events, "provider/cheap-model");
expect(definition?.model).toBe("provider/cheap-model");
expect(definition?.defaultContext).toBe("fork");
expect(definition?.defaultProgress).toBe(true);
expect(definition?.allowNestedSubagents).toBe(true);
expect(definition?.subagentOnlyExtensions).toEqual([
expect.stringContaining("pi-vcc"),
expect.stringContaining("worker-runtime.ts"),
]);
expect(workerSystemPrompt).toContain("main Pi agent is the research supervisor");
expect(definition?.subagentOnlyExtensions).toEqual([expect.stringContaining("supervisor-runtime.ts")]);
expect(supervisorSystemPrompt).toContain("nested goal-worker");
expect(supervisorSystemPrompt).toContain("ApproveGoal");
expect(workerSystemPrompt).toContain("retained implementation worker");
});
it("registers the implementation worker without nested supervisor tools", () => {
const events = new Events();
let definition: Record<string, unknown> | undefined;
events.on("pi-subagents:runtime-agent-register:v1", (raw) => {
const request = raw as { definition: Record<string, unknown>; result?: unknown };
definition = request.definition;
request.result = { ok: true, registration: { dispose() {} } };
});
registerGoalWorker(events, null);
expect(definition?.allowNestedSubagents).toBeUndefined();
expect(definition).not.toHaveProperty("subagentOnlyExtensions");
});
it("fails clearly when pi-subagents is absent", () => {
@@ -68,11 +83,11 @@ describe("goal worker RPC", () => {
return { text: "ok", details: { asyncId: `run-${requests.length}` } };
});
await expect(startGoalWorker(events, "/repo", "start")).resolves.toBe("run-1");
await expect(resumeGoalWorker(events, "run-1", "continue")).resolves.toBe("run-2");
await steerGoalWorker(events, "run-2", "report");
await expect(startGoalSupervisor(events, "/repo", "start")).resolves.toBe("run-1");
await expect(resumeGoalSupervisor(events, "run-1", "continue")).resolves.toBe("run-2");
await steerGoalSupervisor(events, "run-2", "report");
expect(requests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-worker", cwd: "/repo", context: "fork", async: true } });
expect(requests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-supervisor", cwd: "/repo", context: "fork", async: true } });
expect(requests[1]).toMatchObject({ method: "resume", params: { id: "run-1", message: "continue" } });
expect(requests[2]).toMatchObject({ method: "steer", params: { id: "run-2", message: "report", mode: "steer" } });
});