Simplify nested goal supervision

Co-Authored-By: PI[gpt-5.6-sol] <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
wassname
2026-09-06 13:36:12 +08:00
co-authored by PI[gpt-5.6-sol]
parent 844099bdf0
commit 48e2247c00
19 changed files with 384 additions and 474 deletions
+15 -11
View File
@@ -1,6 +1,6 @@
# pi-goals
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.
Make a short list of goals in one Markdown plan file. The main Pi agent is a thin coordinator for a retained supervisor, which runs one foreground implementation worker at a time through pi-subagents.
The plan file looks like this:
@@ -74,21 +74,25 @@ pi -e npm:pi-subagents -e .
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. Ready forks the retained supervisor and
preserves the main context. Ready (compact) forks the supervisor, compacts its planning history
before its first turn, then compacts the main session. It never compacts the retained worker.
before its first turn, then compacts the main session. The implementation worker starts later.
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
└── foreground implementation worker
```
The retained `goal-supervisor` rereads the full current plan on each direction or review, waits for
its nested `goal-worker`, then inspects the actual repository and saved evidence before it writes a private approval checkpoint in
`.pi/pi-goals/approvals/`. The worker is the implementation writer. Main and supervisor block direct
The retained `goal-supervisor` rereads the full current plan on each direction or review and runs
its packaged `pi-goals-worker-v1` in the foreground. The worker must finish before the supervisor
can inspect the repository and evidence or write a private approval checkpoint in
`.pi/pi-goals/approvals/`. A correction starts a new foreground worker. Pi-goals stores no nested
worker ID or status. The versioned name avoids ordinary name collisions. A user or project agent
with the same name still overrides the package agent. 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. On revival, the supervisor checks the retained worker ID against Pi's run registry; a missing run is terminal and permits one replacement worker. This is not a filesystem sandbox: allowed scripts and custom tools can still mutate.
commands. This is not a filesystem sandbox: allowed scripts and custom tools can still mutate.
`CompleteGoal` is mechanical. It checks that worker/supervisor/process work is idle and that the
latest review ID, goal block, clean worktree, and committed HEAD/tree still match. The review ID
prevents stale approval; it is not a security boundary against a worker that deliberately writes Pi state.
@@ -103,13 +107,13 @@ sets the implementation-worker model. Checks continue until all goals close, `au
## Prompts
Planning and coordinator sign-off prompts live in [`src/prompts.ts`](src/prompts.ts). Supervisor registration and RPC calls live in [`src/worker.ts`](src/worker.ts). The packaged worker definition lives in [`agents/goal-worker.md`](agents/goal-worker.md), and the supervisor-only approval tool lives in [`src/supervisor-runtime.ts`](src/supervisor-runtime.ts).
Planning and coordinator sign-off prompts live in [`src/prompts.ts`](src/prompts.ts). Supervisor registration and RPC calls live in [`src/worker.ts`](src/worker.ts). The packaged worker contract lives in [`agents/pi-goals-worker-v1.md`](agents/pi-goals-worker-v1.md). The supervisor-only launch and approval gates 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.
`subagent({ action: "status", view: "fleet" })`. It should show `goal-supervisor` and its foreground
`pi-goals-worker-v1`, 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.
@@ -123,7 +127,7 @@ Planning and coordinator sign-off prompts live in [`src/prompts.ts`](src/prompts
## Develop
```bash
pi -e npm:pi-subagents -e . # load the extension and packaged worker locally
pi -e . # after installing pi-subagents above
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
-21
View File
@@ -1,21 +0,0 @@
---
name: goal-worker
description: Implementation worker directed by the retained goal supervisor
thinking: high
systemPromptMode: replace
inheritProjectContext: true
inheritGlobalContext: true
inheritSkills: true
tools: read, grep, find, ls, bash, edit, write, contact_supervisor
defaultContext: fork
async: true
defaultProgress: true
---
You are the retained implementation worker for one goal supervisor.
Work autonomously from the approved plan. Keep the plan current, run the real checks, commit the implementation, 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 retained goal supervisor owns direction and approval; the main Pi agent only coordinates with the human and performs mechanical 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
+22
View File
@@ -0,0 +1,22 @@
---
name: pi-goals-worker-v1
description: Foreground implementation worker for the retained pi-goals supervisor
thinking: high
systemPromptMode: replace
inheritProjectContext: true
inheritGlobalContext: true
inheritSkills: true
tools: read, grep, find, ls, bash, edit, write
excludeTools: contact_supervisor, subagent
defaultContext: fork
async: false
defaultProgress: true
---
You are the implementation worker for one retained goal supervisor.
Work autonomously from the approved plan. Keep the plan current, run the real checks, commit the implementation, 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 retained goal supervisor owns direction and approval.
Do not ask for routine decisions or start subagents. Finish the task or return one concrete blocker. Do not claim a goal is complete; report the evidence and let the supervisor decide.
-- Pi/Codex
@@ -1,77 +0,0 @@
from __future__ import annotations
import subprocess
from pathlib import Path
root = Path(__file__).resolve().parents[2]
audit_path = root / "slop/audits/20260905_file-word-count.md"
paths = subprocess.check_output(["git", "ls-files"], cwd=root, text=True).splitlines()
def text_rows() -> tuple[list[tuple[str, int]], list[str]]:
rows: list[tuple[str, int]] = []
excluded: list[str] = []
for relative in paths:
raw = (root / relative).read_bytes()
try:
text = raw.decode("utf-8", "strict")
except UnicodeDecodeError:
excluded.append(relative)
continue
if "\0" in text:
excluded.append(relative)
continue
rows.append((relative, len(text.split())))
rows.sort(key=lambda item: (-item[1], item[0]))
return rows, excluded
def render(rows: list[tuple[str, int]], excluded: list[str]) -> str:
table = "\n".join(f"| `{relative}` | {words} |" for relative, words in rows)
excluded_display = ", ".join(f"`{relative}`" for relative in excluded) or "none"
return f"""# Git-tracked text files by word count
Definition: a tracked entry is text when it decodes as strict UTF-8 and contains no NUL character. A word is one non-empty run separated by Unicode whitespace (`len(text.split())`). Counts sort descending, then paths sort ascending.
Generation and independent verification commands:
```sh
python3 slop/audits/20260905_file-word-count-generate.py
python3 slop/audits/20260905_file-word-count-verify.py
```
| file | words |
| --- | ---: |
{table}
Generation summary:
- tracked entries: {len(paths)}
- text files/table rows: {len(rows)}
- excluded non-text entries: {len(excluded)} ({excluded_display})
Independent verification output:
```text
tracked entries: {len(paths)}
text files/table rows: {len(rows)}/{len(rows)}
excluded non-text entries: {len(excluded)} ({", ".join(excluded)})
file-set mismatch: 0
count mismatch: 0
order mismatch: 0
PASS
```
The verifier reads `git ls-files` again, uses `re.finditer(r"\\S+", text)` instead of `split()`, parses this table, and compares the full ordered `(path, count)` sequence.
-- PI[gpt-5.6-sol]
"""
for _ in range(10):
rows, excluded = text_rows()
audit_path.write_text(render(rows, excluded), encoding="utf-8")
if text_rows() == (rows, excluded):
break
else:
raise RuntimeError("The audit's own word count did not reach a fixed point.")
@@ -1,37 +0,0 @@
from __future__ import annotations
import re
import subprocess
from pathlib import Path
root = Path(__file__).resolve().parents[2]
audit = root / "slop/audits/20260905_file-word-count.md"
paths = subprocess.check_output(["git", "ls-files"], cwd=root, text=True).splitlines()
expected: list[tuple[str, int]] = []
excluded: list[str] = []
for relative in paths:
raw = (root / relative).read_bytes()
try:
text = raw.decode("utf-8", "strict")
except UnicodeDecodeError:
excluded.append(relative)
continue
if "\0" in text:
excluded.append(relative)
continue
expected.append((relative, len(list(re.finditer(r"\S+", text)))))
expected.sort(key=lambda item: (-item[1], item[0]))
rows = re.findall(r"^\| `([^`]+)` \| (\d+) \|$", audit.read_text(encoding="utf-8"), flags=re.MULTILINE)
actual = [(path, int(words)) for path, words in rows]
expected_paths = {path for path, _ in expected}
actual_paths = {path for path, _ in actual}
count_mismatch = sum(1 for path, words in actual if path in expected_paths and dict(expected)[path] != words)
print(f"tracked entries: {len(paths)}")
print(f"text files/table rows: {len(expected)}/{len(actual)}")
print(f"excluded non-text entries: {len(excluded)} ({', '.join(excluded)})")
print(f"file-set mismatch: {len(expected_paths ^ actual_paths)}")
print(f"count mismatch: {count_mismatch}")
print(f"order mismatch: {int(actual != expected)}")
if actual != expected:
raise SystemExit("FAIL")
print("PASS")
-84
View File
@@ -1,84 +0,0 @@
# Git-tracked text files by word count
Definition: a tracked entry is text when it decodes as strict UTF-8 and contains no NUL character. A word is one non-empty run separated by Unicode whitespace (`len(text.split())`). Counts sort descending, then paths sort ascending.
Generation and independent verification commands:
```sh
python3 slop/audits/20260905_file-word-count-generate.py
python3 slop/audits/20260905_file-word-count-verify.py
```
| file | words |
| --- | ---: |
| `package-lock.json` | 6025 |
| `src/index.ts` | 3977 |
| `docs/spec/2026-06-15_pi-goals.md` | 2869 |
| `test/goals-flow.test.ts` | 2783 |
| `src/prompts.ts` | 2022 |
| `src/supervisor-runtime.ts` | 931 |
| `src/worker.ts` | 827 |
| `docs/reviews/pi-goals-kimi-k3.md` | 823 |
| `docs/slop/plans/20260826_pi-plan-aligned-planning.md` | 814 |
| `README.md` | 798 |
| `scripts/inconclusive-fail-forward.diff` | 719 |
| `test/supervisor-runtime.test.ts` | 714 |
| `docs/spec/2026-06-29_complete-goal-fail-forward.md` | 705 |
| `docs/reviews/goals_menu2.md` | 698 |
| `docs/spec/2026-08-14_per-session-plan.md` | 664 |
| `docs/reviews/review.md` | 539 |
| `test/worker.test.ts` | 527 |
| `test/rpc-review.test.ts` | 517 |
| `src/approval.ts` | 439 |
| `slop/plans/20260905_goal-steward.md` | 410 |
| `slop/audits/20260905_nested-supervisor-validation.txt` | 408 |
| `docs/slop/plans/20260706_plan-flow-and-judge-review.md` | 403 |
| `docs/reviews/pi-goals-grok-4-6-retry.md` | 386 |
| `slop/audits/20260905_file-word-count.md` | 377 |
| `docs/reviews/goals_menu2_r2.md` | 367 |
| `scripts/check-judge-footprint.sh` | 305 |
| `test/fold.test.ts` | 273 |
| `slop/audits/20260905_file-word-count-generate.py` | 268 |
| `slop/audits/20260905_goal-steward-validation.md` | 246 |
| `slop/audits/20260905_pi-goals-line-count-table.md` | 239 |
| `scripts/stale-fixme-removal.diff` | 237 |
| `test/prompts.test.ts` | 207 |
| `docs/slop/audit/20260826_pi-plan-aligned-planning.md` | 186 |
| `agents/goal-worker.md` | 161 |
| `AGENTS.md` | 159 |
| `test/tick-goal.test.ts` | 157 |
| `slop/audits/20260905_file-word-count-verify.py` | 149 |
| `slop/audits/20260905_steward-probe.json` | 146 |
| `package.json` | 137 |
| `scripts/check-stale-fixmes.sh` | 124 |
| `test/append-log.test.ts` | 102 |
| `test/package-agent.test.ts` | 89 |
| `slop/audits/20260905_pi-goals-file-types.txt` | 84 |
| `slop/audits/20260905_pi-goals-text-line-counts.txt` | 84 |
| `test/fixtures/offline-model.ts` | 52 |
| `biome.json` | 40 |
| `tsconfig.json` | 28 |
| `.gitignore` | 6 |
| `ARCHIVED.md` | 5 |
Generation summary:
- tracked entries: 50
- text files/table rows: 49
- excluded non-text entries: 1 (`media/screenshot.png`)
Independent verification output:
```text
tracked entries: 50
text files/table rows: 49/49
excluded non-text entries: 1 (media/screenshot.png)
file-set mismatch: 0
count mismatch: 0
order mismatch: 0
PASS
```
The verifier reads `git ls-files` again, uses `re.finditer(r"\S+", text)` instead of `split()`, parses this table, and compares the full ordered `(path, count)` sequence.
-- PI[gpt-5.6-sol]
@@ -0,0 +1,53 @@
$ 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 9 passed (9)
Tests 43 passed (43)
Start at 13:31:44
Duration 1.61s (transform 1.12s, setup 0ms, import 2.36s, tests 2.13s, 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 15 files in 18ms. 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 6.1kB README.md
npm notice 969B agents/pi-goals-worker-v1.md
npm notice 1.5kB package.json
npm notice 4.0kB src/approval.ts
npm notice 34.8kB src/index.ts
npm notice 13.6kB src/prompts.ts
npm notice 8.3kB src/supervisor-runtime.ts
npm notice 7.8kB 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: 24.1 kB
npm notice unpacked size: 76.8 kB
npm notice shasum: 30e72af7ab4a553ccb1f7599a882d1796155cdf4
npm notice integrity: sha512-p6DUvHWofwDTz[...]IZmG7JiD+/wFw==
npm notice total files: 8
npm notice
wassname2-pi-goals-0.2.2.tgz
@@ -0,0 +1,31 @@
# Nested foreground runtime smoke
Command:
```bash
node /tmp/pi-goals-real-rpc-smoke.mjs
```
Result: PASS.
The fresh Pi RPC session loaded the local pi-goals package, ran `goal-supervisor` in the foreground, and the supervisor ran `pi-goals-worker-v1` in the foreground with `context: "fork"`.
Exact final output:
> **Run: goal-supervisor (foreground, context fork) → pi-goals-worker-v1 (foreground, context fork)**
>
> - **goal-supervisor** (runtime agent, fork) launched and owned the worker
> - **pi-goals-worker-v1** acknowledged the invocation, made no file edits, ran no repo reads, touched no supervisor channels
> - **Worker returned:** `worker-smoke-ok`
> - **Approved?** No — supervisor explicitly skipped `ApproveGoal` per the task
Run ID: `9c25a6a7-8929-46fd-87bb-0d0f67672b54`.
Saved runtime artifacts:
- `/home/code/.pi/agent/sessions/--home-code-.pi-agent-git-github.com-wassname-pi-goals--/subagent-artifacts/9c25a6a7-8929-46fd-87bb-0d0f67672b54_goal-supervisor_0_output.md`
- `/home/code/.pi/agent/sessions/--home-code-.pi-agent-git-github.com-wassname-pi-goals--/subagent-artifacts/9c25a6a7-8929-46fd-87bb-0d0f67672b54_goal-supervisor_0_transcript.jsonl`
This smoke tested nested discovery and foreground execution. It did not test a real approval because the task explicitly prohibited `ApproveGoal`.
-- PI[gpt-5.6-sol]
@@ -0,0 +1,65 @@
---
requested_model: deepseek/deepseek-v4-pro-0813
mode: code review
input: src/worker.ts, src/supervisor-runtime.ts
trace: omitted from git (11 MB raw provider transcript)
generated: 2026-09-06T04:44:52.809370+00:00
---
# MoA fragility review
Decision: reject the current fix and replace duplicate async lifecycle state with one synchronous worker tool.
Strongest objection: if a truly synchronous worker RPC is unavailable, this simplification blocks the intended parallel supervision model.
Next check: read the goal-worker tool implementation and the three failing test transcripts before deleting code.
Smallest recommended architecture:
The supervisor extension must not store worker lifecycle state. Lifecycle is owned by the subagent runtime. Move ownership into one tool boundary.
1. Delete NESTED_STATE persistence, event listeners, pending reconciliation, CheckWorkerState, and the replacement guard from supervisor-runtime.ts.
2. Add a single supervisor tool:
- RunGoalWorker: starts and awaits a goal-worker synchronously, using the aggregate output as a tool result.
- Keep one in-memory boolean `workerRunning`, guarded at tool execute start, not relying on event ordering.
3. If that synchronous tool cannot be supported:
- StartGoalWorker returns a run ID as ordinary tool output.
- WaitGoalWorker(runId) blocks on terminal status check.
- ApproveGoal always calls bg_wait on the ID from StartGoalWorker or WaitGoalWorker; otherwise approval fails.
Because existing failure 2 came from the runtime blocking on a mismatched ID, the important property is:
- an ID not produced by StartGoalWorker/WaitGoalWorker may not be used for bg_wait;
- a failed wait must clear any in-process guard immediately;
- an await cover failure must be treated as a terminal error, not as `pending`.
Exact deletions/changes:
In `src/supervisor-runtime.ts`:
- Remove `NESTED_STATE`, `NestedState`, `nested`, `persist`, `targetRun`, `completeNested`, all `subagent:async-*`, process-terminal listeners, and `retainedRunState` reconciliation.
- Remove `pi.events.on("tool_call")` blocks. Replace with allow/deny only: deny edit/write, allow read-only bash, allow RunGoalWorker, allow bg_wait, allow ApproveGoal, deny subagent action tools.
- Replace CheckWorkerState with nothing. State inspection is only through normal async progress updates.
- ApproveGoal asserts no active await cover currently exists from RunGoalWorker or WaitGoalWorker, processWorkState is idle, worktree is clean, and evidence inspection claims are backed by the actual tool result from RunGoalWorker.
In `src/worker.ts`:
- Drop `retainedRunState` and any pending-closure logic.
- Keep `asyncSnapshot` only for processWorkState, if needed.
Why this removes fragility:
- Duplicate state is gone.
- Lifecycle is only stored in the runtimes tool execution stack.
- Revival cannot resurrect a wrong worker ID unless a new tool starts it.
- Race between event handler and spawn disappears because Start or Wait returns a result synchronously to the model.
Why this may be worse:
- Synchronous wait loses the supervisor's ability to issue corrections inline during progress.
- Parallel instrumented runs cannot be sustained within one tool without exposing `bg_wait` to the model.
- If the model calls WaitGoalWorker with an incorrect ID, it will now fail directly, but the failure must not be caught and retried with a cached ID.
Acceptance test to catch all observed failures:
- Send the supervisor script: `StartGoalWorker``WaitGoalWorker(id)``RunGoalWorker(correction)``ApproveGoal`, where a midway kill drops the terminal event and forces session revival, and then assert the code path stores no `NESTED_STATE`, does not even mention it in the extension memory, and either the worker returns a tool result or the revived session remains in the same `WaitGoalWorker` tool with no retry on an ID not yielded by that tool.
## Completion
- outcome: `completed_after_follow_up`
- trace: omitted from git (11 MB raw provider transcript); this file preserves the complete review answer
@@ -0,0 +1,20 @@
## Review
No issues found.
- Correct: The packaged worker is discoverable in pi-subagents 0.65.1 child-safe fanout. `package.json` exposes `pi.subagents.agents`, which the installed discovery code consumes (`pi-subagents/src/agents/agents.ts:510-538,597-657`), while the child fanout executor uses normal `discoverAgents` (`pi-subagents/src/extension/fanout-child.ts:145-190`).
- Correct: The supervisor gate requires the exact packaged agent, nonempty task, `async:false`, `context:"fork"`, and the configured model with no extra fields (`src/supervisor-runtime.ts:83-108`). The installed executor honors explicit foreground mode (`pi-subagents/src/runs/foreground/subagent-executor.ts:6511-6515,6917-6920`).
- Correct: Foreground completion is tied to the real `tool_result`. `activeWorkerCalls` is removed only when that result arrives, successful completion is recorded, and approval requires a later turn (`src/supervisor-runtime.ts:75-115,132-138`). Same-message worker launch plus approval is independently rejected by inspecting the assistant message.
- Correct: Stale local launch reservations self-heal: errors clear on `tool_result`, and `turn_start` clears any reservation for which no result hook arrived (`src/supervisor-runtime.ts:75-115`). The tests cover duplicate launch, failed-result recovery, and next-turn recovery (`test/supervisor-runtime.test.ts:57-76`).
- Correct: `CompleteGoal` remains blocked while the retained supervisor is pending, while any subagent/process work is active or unknown, or until a matching approval checkpoint exists (`src/index.ts`, `CompleteGoal`). Foreground nested work therefore cannot race sign-off because its containing supervisor run remains pending.
- Correct: `supervisor-runtime.ts` does not perform runtime-agent registration. The main extension exits in child processes through `isSupervisorProcess`, while installed pi-subagents itself is inert when `PI_SUBAGENT_CHILD=1` (`src/index.ts`, `isSupervisorProcess`; installed `pi-subagents/index.ts:3-8`).
- Correct: The former nested async worker ID/pending lifecycle is absent. The remaining `workerRunId`/`workerPending` state belongs only to the retained supervisor lifecycle, matching the documented topology.
Residual risks:
- `test/package-agent.test.ts` verifies packaging statically rather than launching the packaged worker through the real child-safe fanout runtime. The installed 0.65.1 source supports the configuration, but retaining an RPC integration check is advisable.
- The focused approval tests mock Pis `tool_call`/`tool_result` ordering. A real RPC test remains the strongest guard against upstream lifecycle-event changes.
- Tests were inspected but not executed in this review environment; the supervisor should run `npm test`, `npm run typecheck`, and `npm run lint`.
- Merge verdict: **OK with residual test-environment risks.**
-- PI[reviewer/gpt-5.6-sol]
+3 -3
View File
@@ -1,6 +1,6 @@
/**
* 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.
* retained pi-subagents supervisor, which owns one foreground implementation worker at a time 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,
@@ -196,7 +196,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
try {
const runId = state.workerRunId
? await resumeGoalSupervisor(pi.events, state.workerRunId, task, signal)
: await startGoalSupervisor(pi.events, ctx.cwd, task, compactPlanning, signal);
: await startGoalSupervisor(pi.events, ctx.cwd, task, compactPlanning, state.workerModel, signal);
rememberWorkerRun(runId);
return runId;
} finally {
@@ -518,7 +518,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
persist();
updateWidget(ctx);
try {
await directSupervisor(ctx, "Start by launching or resuming the nested goal-worker. Then supervise the current plan.", undefined, choice === "Ready (compact)");
await directSupervisor(ctx, "Start by launching the foreground implementation worker. Then supervise the current plan.", undefined, choice === "Ready (compact)");
scheduleSupervisorCheck(ctx);
return true;
} catch (error) {
+1 -1
View File
@@ -14,7 +14,7 @@
* SETUP (plan mode) 1. planDrafting — draft goals into the plan file (read-only), sent once
* EXEC, after compact 2. resync — the WHOLE file back, once
* SIGN-OFF, agent-side 3. completeGoal* — the one blessed tool's description
* SUPERVISION worker.ts - retained implementation worker
* SUPERVISION worker.ts - retained supervisor and foreground worker
*
* The goal's test is the DISCRIMINATOR: the concrete observation that tells real success from the
* named subtle failure mode. Evidence is empty at planning and filled at sign-off.
+55 -61
View File
@@ -4,52 +4,46 @@ 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 { processWorkState, retainedRunState } from "./worker.js";
import { GOAL_WORKER_AGENT, processWorkState } from "./worker.js";
const NESTED_STATE = "pi-goals-nested-worker";
const COMPACTED_STATE = "pi-goals-supervisor-compacted";
interface NestedState {
runId: string | null;
pending: boolean;
}
function result(text: string, isError = false) {
return { content: [{ type: "text" as const, text }], details: {}, isError };
}
function targetRun(input: Record<string, unknown>): string | null {
const value = input.id ?? input.runId;
return typeof value === "string" && value ? value : null;
interface GoalBindings {
compactPlanning?: boolean;
workerModel?: string | null;
}
function compactPlanningRequested(): boolean {
function goalBindings(): GoalBindings {
const raw = process.env.PI_SUBAGENT_EXTENSION_BINDINGS;
if (!raw) return false;
const bindings = JSON.parse(raw) as { "pi-goals/1"?: { compactPlanning?: unknown } };
return bindings["pi-goals/1"]?.compactPlanning === true;
if (!raw) return {};
const binding = (JSON.parse(raw) as { "pi-goals/1"?: GoalBindings })["pi-goals/1"] ?? {};
if (binding.workerModel !== undefined && binding.workerModel !== null && typeof binding.workerModel !== "string") throw new Error("pi-goals workerModel binding must be a string or null.");
return binding;
}
function messageLaunchesWorker(ctx: { sessionManager: { getBranch(): unknown[] } }): boolean {
const entry = [...ctx.sessionManager.getBranch()].reverse().find((candidate) => {
const value = candidate as { type?: unknown; message?: { role?: unknown } };
return value.type === "message" && value.message?.role === "assistant";
}) as { message?: { content?: unknown } } | undefined;
if (!Array.isArray(entry?.message?.content)) return false;
return entry.message.content.some((part) => {
const value = part as { type?: unknown; name?: unknown; arguments?: Record<string, unknown> };
return value.type === "toolCall" && value.name === "subagent" && value.arguments?.agent === GOAL_WORKER_AGENT;
});
}
export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
let nested: NestedState = { runId: null, pending: false };
let compacting = false;
let compactionDone = Promise.resolve();
const persist = () => pi.appendEntry<NestedState>(NESTED_STATE, nested);
pi.events.on("subagent:async-started", (raw) => {
const event = raw as { id?: unknown; agent?: unknown };
if (event.agent !== "goal-worker" || typeof event.id !== "string") return;
nested = { runId: event.id, pending: true };
persist();
});
const completeNested = (raw: unknown) => {
const event = raw as { id?: unknown; runId?: unknown };
if ((event.runId ?? event.id) !== nested.runId) return;
nested = { ...nested, pending: false };
persist();
};
pi.events.on("subagent:async-complete", completeNested);
pi.events.on("subagent:process-terminal", completeNested);
let currentTurn = -1;
let completedWorkerTurn: number | null = null;
let workerModel: string | null = null;
const activeWorkerCalls = new Set<string>();
pi.on("session_before_compact", async (event) => {
if (!compacting) return;
@@ -67,15 +61,9 @@ export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
pi.on("session_start", async (_event, ctx) => {
const entries = ctx.sessionManager.getEntries();
const last = entries
.filter((entry: { type?: string; customType?: string }) => entry.type === "custom" && entry.customType === NESTED_STATE)
.pop() as { data?: NestedState } | undefined;
nested = last?.data ?? nested;
if (nested.pending && nested.runId && (await retainedRunState(pi.events, nested.runId)) === "idle") {
nested = { ...nested, pending: false };
persist();
}
if (!compactPlanningRequested()) return;
const bindings = goalBindings();
workerModel = bindings.workerModel ?? null;
if (bindings.compactPlanning !== true) return;
if (entries.some((entry: { type?: string; customType?: string }) => entry.type === "custom" && entry.customType === COMPACTED_STATE)) return;
compacting = true;
compactionDone = new Promise<void>((resolvePromise, reject) => {
@@ -97,6 +85,11 @@ export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
await compactionDone;
});
pi.on("turn_start", async (event) => {
activeWorkerCalls.clear();
currentTurn = event.turnIndex;
});
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." };
@@ -106,34 +99,33 @@ export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
}
if (event.toolName !== "subagent") return;
const input = event.input as Record<string, unknown>;
const action = typeof input.action === "string" ? input.action : null;
if (!action) {
if (input.agent === "goal-worker" && !nested.pending && input.workflowScript === undefined && input.workflowScriptPath === undefined) return;
return { block: true, reason: nested.pending ? "Wait for the retained goal-worker instead of starting another worker." : "The supervisor may start only goal-worker." };
const allowedKeys = new Set(["agent", "task", "async", "context", ...(workerModel ? ["model"] : [])]);
const unexpectedKeys = Object.keys(input).filter((key) => !allowedKeys.has(key));
const validWorker = input.agent === GOAL_WORKER_AGENT
&& typeof input.task === "string"
&& input.task.trim().length > 0
&& input.async === false
&& input.context === "fork"
&& (workerModel ? input.model === workerModel : input.model === undefined)
&& unexpectedKeys.length === 0;
if (!validWorker) {
const model = workerModel ? `, model:${JSON.stringify(workerModel)}` : "";
return { block: true, reason: `Launch only ${GOAL_WORKER_AGENT} with task, async:false, context:"fork"${model}, and no other fields.` };
}
if (action === "list") return;
if (action === "status") return { block: true, reason: "Do not poll the retained worker. Use its native progress and completion updates." };
if (["resume", "steer", "interrupt", "stop"].includes(action) && targetRun(input) === nested.runId) {
if (nested.pending) return;
return { block: true, reason: "The retained goal-worker is terminal; start a replacement worker for a correction." };
}
return { block: true, reason: "The supervisor may inspect or control only its retained goal-worker." };
if (activeWorkerCalls.size > 0) return { block: true, reason: "A foreground goal-worker is already running." };
activeWorkerCalls.add(event.toolCallId);
completedWorkerTurn = null;
});
pi.registerTool({
name: "CheckWorkerState",
label: "Check retained worker",
description: "Return concise retained-worker state after a needs-attention notice or scheduled review. This does not return transcript text.",
parameters: Type.Object({}),
async execute() {
const state = nested.runId ? (nested.pending ? "active" : "terminal") : "not-started";
return result(`retained-worker=${state}${nested.runId ? `; run=${nested.runId}` : ""}`);
},
pi.on("tool_result", async (event) => {
if (!activeWorkerCalls.delete(event.toolCallId)) return;
if (!event.isError) completedWorkerTurn = currentTurn;
});
pi.registerTool({
name: "ApproveGoal",
label: "Approve goal",
executionMode: "sequential",
description: "Record approval after inspecting the plan, repository, evidence, and saved verification output. Active or unknown work blocks approval.",
parameters: Type.Object({
approvalId: Type.String({ minLength: 1, description: "Exact approval ID from the latest main-coordinator direction." }),
@@ -146,7 +138,9 @@ export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
inspectedVerifyOutput: Type.Literal(true),
}),
async execute(_id, params, _signal, _onUpdate, ctx) {
if (nested.pending) return result("Cannot approve while the retained worker is pending.", true);
if (messageLaunchesWorker(ctx) || activeWorkerCalls.size > 0 || completedWorkerTurn === null || completedWorkerTurn >= currentTurn) {
return result("Cannot approve in a worker-launch message or before reviewing a finished worker on a later turn.", true);
}
const processes = processWorkState(pi.events);
if (processes !== "idle") return result(`Cannot approve: processes=${processes}.`, true);
const planPath = resolve(params.planPath);
+33 -49
View File
@@ -7,6 +7,7 @@ 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 GOAL_WORKER_AGENT = "pi-goals-worker-v1";
interface EventBus {
on(event: string, handler: (data: unknown) => void): () => void;
@@ -39,44 +40,43 @@ interface AsyncSnapshot {
export type WorkState = "active" | "idle" | "unknown";
export const supervisorSystemPrompt = `You are the retained goal supervisor. The main Pi session only coordinates with the human.
Your forked planning history is compacted before your first turn. Launch one goal-worker, then call bg_wait with its run ID
so this supervisory turn stays alive until the worker completes or needs attention. Do not poll status or repeatedly steer
an active worker. Use CheckWorkerState once only after a needs-attention notice or a scheduled review. If a terminal worker
needs a correction, launch one replacement goal-worker instead of resuming its old run ID. Read the current plan, repository,
cited evidence, and saved verification output yourself after the worker finishes. Do not edit project files. Use read/search
and standard verification commands only. The worker must commit its changes before approval. When no nested work is active,
HEAD is committed, the worktree is clean, and the evidence proves the discriminator, call ApproveGoal with the current
approval ID. Otherwise give the retained worker one concrete correction. Only ApproveGoal creates acceptance. -- Pi/Codex`;
Your forked planning history may be compacted before your first turn. Launch ${GOAL_WORKER_AGENT} in the foreground with exactly
agent, task, async:false, context:"fork", and, when named in the current direction, that worker model. Wait for its result; do not use
bg_wait or worker run IDs. Read the current plan, repository, cited evidence, and saved verification output yourself after the
worker finishes. Do not edit project files. Use read/search and standard verification commands only. The worker must commit its
changes before approval. If the evidence needs a correction, launch a new foreground ${GOAL_WORKER_AGENT} with one concrete task
and wait for it. On a later turn, when HEAD is committed, the worktree is clean, and the evidence proves the discriminator, call
ApproveGoal with the current approval ID. Only ApproveGoal creates acceptance. -- 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,
tools: ["read", "grep", "find", "ls", "bash", "subagent", "bg_wait", "CheckWorkerState", "ApproveGoal"],
allowNestedSubagents: true,
subagentOnlyExtensions: [supervisorRuntime],
...(model ? { model } : {}),
systemPromptMode: "replace",
thinking: "low",
inheritProjectContext: false,
inheritGlobalContext: false,
inheritSkills: false,
defaultContext: "fork",
defaultAsync: true,
defaultProgress: true,
},
};
function registerRuntimeAgent(events: EventBus, name: string, definition: Record<string, unknown>): Registration {
const request: Record<string, unknown> = { version: 1, name, definition };
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.");
if (!result.ok || !result.registration) throw result.error ?? new Error(`pi-subagents rejected the ${name} agent.`);
return result.registration;
}
export function registerGoalSupervisor(events: EventBus, model: string | null): Registration {
const supervisorRuntime = fileURLToPath(new URL("./supervisor-runtime.ts", import.meta.url));
return registerRuntimeAgent(events, SUPERVISOR_AGENT, {
description: "Read-only supervisor that owns a foreground implementation worker.",
systemPrompt: supervisorSystemPrompt,
tools: ["read", "grep", "find", "ls", "bash", "subagent", "ApproveGoal"],
allowNestedSubagents: true,
subagentOnlyExtensions: [supervisorRuntime],
...(model ? { model } : {}),
systemPromptMode: "replace",
thinking: "low",
inheritProjectContext: false,
inheritGlobalContext: false,
inheritSkills: false,
defaultContext: "fork",
defaultAsync: true,
defaultProgress: true,
});
}
async function rpc(events: EventBus, method: "spawn" | "resume" | "steer" | "status" | "stop", params: Record<string, unknown>, signal?: AbortSignal): Promise<RpcData> {
if (signal?.aborted) throw new Error("Goal-worker request aborted.");
const requestId = randomUUID();
@@ -114,7 +114,7 @@ function asyncRunId(data: RpcData): string {
return runId;
}
export async function startGoalSupervisor(events: EventBus, cwd: string, task: string, compactPlanning: boolean, signal?: AbortSignal): Promise<string> {
export async function startGoalSupervisor(events: EventBus, cwd: string, task: string, compactPlanning: boolean, workerModel: string | null, signal?: AbortSignal): Promise<string> {
const data = await rpc(events, "spawn", {
agent: SUPERVISOR_AGENT,
task,
@@ -122,7 +122,7 @@ export async function startGoalSupervisor(events: EventBus, cwd: string, task: s
context: "fork",
async: true,
mission: false,
extensionBindings: { "pi-goals/1": { compactPlanning } },
extensionBindings: { "pi-goals/1": { compactPlanning, workerModel } },
}, signal);
return asyncRunId(data);
}
@@ -157,15 +157,6 @@ function validSnapshot(snapshot: AsyncSnapshot | undefined): snapshot is AsyncSn
return snapshot?.kind === "pi-subagents.async-status-snapshot" && snapshot.version === 1 && snapshot.omitted.runs === 0 && snapshot.omitted.children === 0 && !snapshot.omitted.byteLimitExceeded;
}
function findNode(nodes: AsyncNode[], runId: string): AsyncNode | undefined {
for (const node of nodes) {
if (node.id === runId) return node;
const child = node.children && findNode(node.children, runId);
if (child) return child;
}
return undefined;
}
async function asyncSnapshot(events: EventBus): Promise<AsyncSnapshot | undefined> {
return (await rpc(events, "status", {})).asyncSnapshot;
}
@@ -176,13 +167,6 @@ export async function subagentWorkState(events: EventBus): Promise<WorkState> {
return snapshot.runs.some(activeNode) ? "active" : "idle";
}
export async function retainedRunState(events: EventBus, runId: string): Promise<WorkState> {
const snapshot = await asyncSnapshot(events);
if (!validSnapshot(snapshot)) return "unknown";
const node = findNode(snapshot.runs, runId);
return node && activeNode(node) ? "active" : "idle";
}
export interface ProcessInfo {
status: string;
}
+5 -1
View File
@@ -195,11 +195,15 @@ describe("/goals draft flow", () => {
});
it("keeps supervisor and implementation-worker models separate", async () => {
const flow = setup([]);
const flow = setup(["Ready"]);
try {
await flow.commands.get("goals").handler("model provider/supervisor", flow.ctx);
await flow.commands.get("goals").handler("worker-model provider/worker", flow.ctx);
expect(flow.entries.at(-1)?.data).toMatchObject({ supervisorModel: "provider/supervisor", workerModel: "provider/worker" });
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: work\n");
await flow.hooks.get("agent_settled")({}, flow.ctx);
expect(flow.rpcRequests.at(-1)).toMatchObject({ params: { extensionBindings: { "pi-goals/1": { workerModel: "provider/worker" } } } });
} finally {
rmSync(flow.cwd, { recursive: true, force: true });
}
+10 -10
View File
@@ -4,20 +4,20 @@ import { describe, expect, it } from "vitest";
interface PackageManifest {
files: string[];
pi: { subagents: { agents: string[] } };
pi: { extensions: string[]; subagents: { agents: string[] } };
}
describe("packaged goal worker", () => {
it("exposes goal-worker through pi-subagents package discovery", () => {
describe("package manifest", () => {
it("includes the versioned foreground worker for child-process discovery", () => {
const root = resolve(import.meta.dirname, "..");
const manifest = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8")) as PackageManifest;
expect(manifest.files).toContain("agents");
const worker = readFileSync(resolve(root, "agents/pi-goals-worker-v1.md"), "utf8");
expect(manifest.files).toEqual(["src", "agents", "README.md"]);
expect(manifest.pi.extensions).toEqual(["./src/index.ts"]);
expect(manifest.pi.subagents.agents).toEqual(["./agents"]);
const definition = readFileSync(resolve(root, "agents", "goal-worker.md"), "utf8");
expect(definition).toMatch(/^---\nname: goal-worker\n/);
expect(definition).toContain("tools: read, grep, find, ls, bash, edit, write, contact_supervisor");
expect(definition).toContain("defaultContext: fork");
expect(definition).toContain("retained implementation worker");
expect(worker).toContain("name: pi-goals-worker-v1");
expect(worker).toContain("async: false");
expect(worker).toContain("tools: read, grep, find, ls, bash, edit, write");
expect(worker).toContain("excludeTools: contact_supervisor, subagent");
});
});
+4 -4
View File
@@ -1,9 +1,8 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { completeGoalDescription, planDrafting, planningState, resync } from "../src/prompts.js";
const workerSystemPrompt = readFileSync(new URL("../agents/goal-worker.md", import.meta.url), "utf8");
describe("planning prompt", () => {
it("requires fact finding or a focused question before a goal", () => {
expect(planDrafting).toContain("Use read-only repository tools or web search when either can\nresolve a fact.");
@@ -26,8 +25,9 @@ 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("human's latest message outranks the plan");
expect(workerSystemPrompt).toContain("retained goal supervisor owns direction and approval");
const worker = readFileSync(resolve(import.meta.dirname, "../agents/pi-goals-worker-v1.md"), "utf8");
expect(worker).toContain("human's latest message outranks the plan");
expect(worker).toContain("retained goal supervisor owns direction and approval");
expect(completeGoalDescription).toContain("approval checkpoint only after it inspected");
});
});
+42 -79
View File
@@ -5,6 +5,7 @@ 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";
import { GOAL_WORKER_AGENT } from "../src/worker.js";
class Events {
private handlers = new Map<string, Set<(data: unknown) => void>>();
@@ -21,7 +22,7 @@ class Events {
}
}
function setup(asyncSnapshot = { kind: "pi-subagents.async-status-snapshot", version: 1, omitted: { runs: 0, children: 0, byteLimitExceeded: false }, runs: [] }) {
function setup() {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-supervisor-"));
writeFileSync(join(cwd, "README.md"), "test\n");
execFileSync("git", ["init", "-q"], { cwd });
@@ -30,21 +31,15 @@ function setup(asyncSnapshot = { kind: "pi-subagents.async-status-snapshot", ver
const hooks = new Map<string, any>();
const tools = new Map<string, any>();
const entries: any[] = [];
const branch: any[] = [];
const compactCalls: any[] = [];
const events = new Events();
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 },
});
});
events.on("processes:request:list", (raw) => {
(raw as { reply(value: object[]): void }).reply([]);
});
const ctx = {
cwd,
sessionManager: { getSessionId: () => "supervisor-session", getEntries: () => entries },
sessionManager: { getSessionId: () => "supervisor-session", getEntries: () => entries, getBranch: () => branch },
compact: (options: any) => compactCalls.push(options),
ui: { notify() {} },
};
@@ -55,7 +50,7 @@ function setup(asyncSnapshot = { kind: "pi-subagents.async-status-snapshot", ver
registerTool: (tool: any) => tools.set(tool.name, tool),
};
supervisorRuntime(pi as any);
return { cwd, ctx, events, hooks, tools, entries, compactCalls };
return { cwd, ctx, events, hooks, tools, entries, branch, compactCalls };
}
describe("supervisor-only runtime", () => {
@@ -64,9 +59,19 @@ describe("supervisor-only runtime", () => {
try {
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 branch new-name" } }, runtime.ctx))?.block).toBe(true);
expect((await runtime.hooks.get("tool_call")({ toolName: "subagent", input: { agent: "worker" } }, runtime.ctx))?.block).toBe(true);
expect((await runtime.hooks.get("tool_call")({ toolName: "subagent", input: { agent: "goal-worker" } }, runtime.ctx))).toBeUndefined();
expect((await runtime.hooks.get("tool_call")({ toolName: "subagent", input: { action: "status", id: "nested-1" } }, runtime.ctx))?.block).toBe(true);
expect((await runtime.hooks.get("tool_call")({ toolName: "subagent", toolCallId: "wrong", input: { agent: "goal-worker", task: "work", async: false, context: "fork" } }, runtime.ctx))?.block).toBe(true);
expect((await runtime.hooks.get("tool_call")({ toolName: "subagent", toolCallId: "implicit", input: { agent: GOAL_WORKER_AGENT, task: "work" } }, runtime.ctx))?.block).toBe(true);
expect((await runtime.hooks.get("tool_call")({ toolName: "subagent", toolCallId: "model", input: { agent: GOAL_WORKER_AGENT, task: "work", async: false, context: "fork", model: "other/model" } }, runtime.ctx))?.block).toBe(true);
expect((await runtime.hooks.get("tool_call")({ toolName: "subagent", toolCallId: "override", input: { agent: GOAL_WORKER_AGENT, task: "work", async: false, context: "fork", worktree: true } }, runtime.ctx))?.block).toBe(true);
expect(await runtime.hooks.get("tool_call")({ toolName: "subagent", toolCallId: "worker", input: { agent: GOAL_WORKER_AGENT, task: "work", async: false, context: "fork" } }, runtime.ctx)).toBeUndefined();
expect((await runtime.hooks.get("tool_call")({ toolName: "subagent", toolCallId: "duplicate", input: { agent: GOAL_WORKER_AGENT, task: "work", async: false, context: "fork" } }, runtime.ctx))?.block).toBe(true);
await runtime.hooks.get("tool_result")({ toolName: "subagent", toolCallId: "worker", isError: true }, runtime.ctx);
expect(await runtime.hooks.get("tool_call")({ toolName: "subagent", toolCallId: "stale", input: { agent: GOAL_WORKER_AGENT, task: "work", async: false, context: "fork" } }, runtime.ctx)).toBeUndefined();
await runtime.hooks.get("turn_start")({ turnIndex: 1 }, runtime.ctx);
expect(await runtime.hooks.get("tool_call")({ toolName: "subagent", toolCallId: "recovered", input: { agent: GOAL_WORKER_AGENT, task: "work", async: false, context: "fork" } }, runtime.ctx)).toBeUndefined();
await runtime.hooks.get("tool_result")({ toolName: "subagent", toolCallId: "recovered", isError: true }, runtime.ctx);
expect((await runtime.hooks.get("tool_call")({ toolName: "subagent", toolCallId: "async", input: { agent: GOAL_WORKER_AGENT, task: "work", async: true, context: "fork" } }, runtime.ctx))?.block).toBe(true);
expect((await runtime.hooks.get("tool_call")({ toolName: "subagent", toolCallId: "resume", input: { action: "resume", id: "nested-1" } }, 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 });
@@ -75,10 +80,13 @@ describe("supervisor-only runtime", () => {
it("compacts a requested fork before the first supervisor turn", async () => {
const previous = process.env.PI_SUBAGENT_EXTENSION_BINDINGS;
process.env.PI_SUBAGENT_EXTENSION_BINDINGS = JSON.stringify({ "pi-goals/1": { compactPlanning: true } });
process.env.PI_SUBAGENT_EXTENSION_BINDINGS = JSON.stringify({ "pi-goals/1": { compactPlanning: true, workerModel: "provider/worker" } });
const runtime = setup();
try {
await runtime.hooks.get("session_start")({}, runtime.ctx);
expect(await runtime.hooks.get("tool_call")({ toolName: "subagent", toolCallId: "wrong-model", input: { agent: GOAL_WORKER_AGENT, task: "work", async: false, context: "fork", model: "other/model" } }, runtime.ctx)).toMatchObject({ block: true });
expect(await runtime.hooks.get("tool_call")({ toolName: "subagent", toolCallId: "worker", input: { agent: GOAL_WORKER_AGENT, task: "work", async: false, context: "fork", model: "provider/worker" } }, runtime.ctx)).toBeUndefined();
await runtime.hooks.get("tool_result")({ toolName: "subagent", toolCallId: "worker", isError: true }, runtime.ctx);
expect(runtime.compactCalls).toHaveLength(1);
const replacement = await runtime.hooks.get("session_before_compact")({
preparation: { firstKeptEntryId: "old", tokensBefore: 70_000 },
@@ -95,65 +103,6 @@ describe("supervisor-only runtime", () => {
}
});
it("reconciles a missing retained worker and permits approval or one replacement", async () => {
const runtime = setup();
try {
runtime.entries.push({ type: "custom", customType: "pi-goals-nested-worker", data: { runId: "missing-worker", pending: true } });
await runtime.hooks.get("session_start")({}, runtime.ctx);
const state = await runtime.tools.get("CheckWorkerState").execute("", {}, undefined, undefined, runtime.ctx);
expect(state.content[0].text).toBe("retained-worker=terminal; run=missing-worker");
expect(await runtime.hooks.get("tool_call")({ toolName: "subagent", input: { agent: "goal-worker" } }, runtime.ctx)).toBeUndefined();
const blocked = await runtime.hooks.get("tool_call")({ toolName: "subagent", input: { action: "resume", id: "missing-worker" } }, runtime.ctx);
expect(blocked?.reason).toContain("terminal");
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 accepted = await runtime.tools.get("ApproveGoal").execute("", {
approvalId: "review-1",
goal: "ship it",
planPath,
checkpointPath: approvalPath(runtime.cwd, "main-session", "ship it"),
inspectedPlan: true,
inspectedRepository: true,
inspectedEvidence: true,
inspectedVerifyOutput: true,
}, undefined, undefined, runtime.ctx);
expect(accepted.isError).toBe(false);
} finally {
rmSync(runtime.cwd, { recursive: true, force: true });
}
});
it("keeps a revived worker pending when the run registry is incomplete", async () => {
const runtime = setup({ kind: "pi-subagents.async-status-snapshot", version: 1, omitted: { runs: 1, children: 0, byteLimitExceeded: false }, runs: [] });
try {
runtime.entries.push({ type: "custom", customType: "pi-goals-nested-worker", data: { runId: "unknown-worker", pending: true } });
await runtime.hooks.get("session_start")({}, runtime.ctx);
const state = await runtime.tools.get("CheckWorkerState").execute("", {}, undefined, undefined, runtime.ctx);
expect(state.content[0].text).toBe("retained-worker=active; run=unknown-worker");
expect((await runtime.hooks.get("tool_call")({ toolName: "subagent", input: { agent: "goal-worker" } }, runtime.ctx))?.block).toBe(true);
const blocked = await runtime.tools.get("ApproveGoal").execute("", {}, undefined, undefined, runtime.ctx);
expect(blocked.content[0].text).toContain("retained worker is pending");
} finally {
rmSync(runtime.cwd, { recursive: true, force: true });
}
});
it("blocks approval while its retained worker is pending", async () => {
const runtime = setup();
try {
runtime.events.emit("subagent:async-started", { id: "nested-1", agent: "goal-worker" });
const state = await runtime.tools.get("CheckWorkerState").execute("", {}, undefined, undefined, runtime.ctx);
expect(state.content[0].text).toBe("retained-worker=active; run=nested-1");
const blocked = await runtime.tools.get("ApproveGoal").execute("", {}, undefined, undefined, runtime.ctx);
expect(blocked.isError).toBe(true);
expect(blocked.content[0].text).toContain("retained worker is pending");
} 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();
const previousRunId = process.env.PI_SUBAGENT_RUN_ID;
@@ -163,11 +112,7 @@ describe("supervisor-only runtime", () => {
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");
runtime.events.emit("subagent:async-started", { id: "nested-1", agent: "goal-worker" });
runtime.events.emit("subagent:process-terminal", { runId: "nested-1", state: "observed" });
const state = await runtime.tools.get("CheckWorkerState").execute("", {}, undefined, undefined, runtime.ctx);
expect(state.content[0].text).toBe("retained-worker=terminal; run=nested-1");
const accepted = await runtime.tools.get("ApproveGoal").execute("", {
const params = {
approvalId: "review-1",
goal: "ship it",
planPath,
@@ -176,8 +121,26 @@ describe("supervisor-only runtime", () => {
inspectedRepository: true,
inspectedEvidence: true,
inspectedVerifyOutput: true,
}, undefined, undefined, runtime.ctx);
};
await runtime.hooks.get("turn_start")({ turnIndex: 0 }, runtime.ctx);
await runtime.hooks.get("tool_call")({ toolName: "subagent", toolCallId: "worker", input: { agent: GOAL_WORKER_AGENT, task: "work", async: false, context: "fork" } }, runtime.ctx);
expect((await runtime.tools.get("ApproveGoal").execute("", params, undefined, undefined, runtime.ctx)).isError).toBe(true);
await runtime.hooks.get("tool_result")({ toolName: "subagent", toolCallId: "worker", isError: false }, runtime.ctx);
expect((await runtime.tools.get("ApproveGoal").execute("", params, undefined, undefined, runtime.ctx)).isError).toBe(true);
await runtime.hooks.get("turn_start")({ turnIndex: 1 }, runtime.ctx);
runtime.branch.push({
type: "message",
message: { role: "assistant", content: [
{ type: "toolCall", name: "ApproveGoal", arguments: params },
{ type: "toolCall", name: "subagent", arguments: { agent: GOAL_WORKER_AGENT, task: "more work", async: false, context: "fork" } },
] },
});
expect((await runtime.tools.get("ApproveGoal").execute("", params, undefined, undefined, runtime.ctx)).isError).toBe(true);
await runtime.hooks.get("turn_start")({ turnIndex: 2 }, runtime.ctx);
runtime.branch.push({ type: "message", message: { role: "assistant", content: [{ type: "toolCall", name: "ApproveGoal", arguments: params }] } });
const accepted = await runtime.tools.get("ApproveGoal").execute("", params, undefined, undefined, runtime.ctx);
expect(runtime.tools.get("ApproveGoal").executionMode).toBe("sequential");
expect(accepted.isError).toBe(false);
expect(readApproval(checkpoint)).toMatchObject({ version: 2, approvalId: "review-1", goal: "ship it", supervisor: { sessionId: "supervisor-session", runId: "supervisor-run" } });
expect(readFileSync(checkpoint, "utf8")).toContain('"goalBlockHash"');
+25 -36
View File
@@ -1,9 +1,9 @@
import { describe, expect, it } from "vitest";
import {
GOAL_WORKER_AGENT,
processWorkState,
registerGoalSupervisor,
resumeGoalSupervisor,
retainedRunState,
startGoalSupervisor,
steerGoalSupervisor,
stopGoalSupervisor,
@@ -35,32 +35,35 @@ function replyToRpc(events: Events, inspect: (request: any) => object): void {
}
describe("goal hierarchy registration", () => {
it("registers a retained supervisor that can load the worker-only runtime", () => {
it("registers the supervisor contract and names its packaged foreground worker", () => {
const events = new Events();
let definition: Record<string, unknown> | undefined;
const definitions = new Map<string, Record<string, unknown>>();
events.on("pi-subagents:runtime-agent-register:v1", (raw) => {
const request = raw as { definition: Record<string, unknown>; result?: unknown };
definition = request.definition;
const request = raw as { name: string; definition: Record<string, unknown>; result?: unknown };
definitions.set(request.name, request.definition);
request.result = { ok: true, registration: { dispose() {} } };
});
registerGoalSupervisor(events, "provider/cheap-model");
registerGoalSupervisor(events, "provider/supervisor");
expect(definition?.model).toBe("provider/cheap-model");
expect(definition?.defaultContext).toBe("fork");
expect(definition?.thinking).toBe("low");
expect(definition?.inheritProjectContext).toBe(false);
expect(definition?.inheritGlobalContext).toBe(false);
expect(definition?.inheritSkills).toBe(false);
expect(definition?.defaultProgress).toBe(true);
expect(definition?.allowNestedSubagents).toBe(true);
expect(definition?.tools).toEqual(["read", "grep", "find", "ls", "bash", "subagent", "bg_wait", "CheckWorkerState", "ApproveGoal"]);
expect(definition?.subagentOnlyExtensions).toEqual([expect.stringContaining("supervisor-runtime.ts")]);
expect(supervisorSystemPrompt).toContain("Launch one goal-worker");
expect(supervisorSystemPrompt).toContain("Do not poll status");
expect(supervisorSystemPrompt).toContain("bg_wait");
expect(supervisorSystemPrompt).toContain("forked planning history is compacted");
const supervisor = definitions.get("goal-supervisor");
expect(supervisor).toMatchObject({
model: "provider/supervisor",
defaultContext: "fork",
defaultAsync: true,
thinking: "low",
inheritProjectContext: false,
inheritGlobalContext: false,
inheritSkills: false,
defaultProgress: true,
allowNestedSubagents: true,
tools: ["read", "grep", "find", "ls", "bash", "subagent", "ApproveGoal"],
});
expect(supervisor?.subagentOnlyExtensions).toEqual([expect.stringContaining("supervisor-runtime.ts")]);
expect(supervisorSystemPrompt).toContain(GOAL_WORKER_AGENT);
expect(supervisorSystemPrompt).toContain("async:false");
expect(supervisorSystemPrompt).toContain("ApproveGoal");
expect(definitions.has(GOAL_WORKER_AGENT)).toBe(false);
});
});
@@ -73,12 +76,12 @@ describe("goal worker RPC", () => {
return { text: "ok", details: { asyncId: `run-${requests.length}` } };
});
await expect(startGoalSupervisor(events, "/repo", "start", true)).resolves.toBe("run-1");
await expect(startGoalSupervisor(events, "/repo", "start", true, "provider/worker")).resolves.toBe("run-1");
await expect(resumeGoalSupervisor(events, "run-1", "continue")).resolves.toBe("run-2");
await steerGoalSupervisor(events, "run-2", "report");
await stopGoalSupervisor(events, "run-2");
expect(requests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-supervisor", cwd: "/repo", context: "fork", async: true, extensionBindings: { "pi-goals/1": { compactPlanning: true } } } });
expect(requests[0]).toMatchObject({ method: "spawn", params: { agent: "goal-supervisor", cwd: "/repo", context: "fork", async: true, extensionBindings: { "pi-goals/1": { compactPlanning: true, workerModel: "provider/worker" } } } });
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" } });
expect(requests[3]).toMatchObject({ method: "stop", params: { id: "run-2" } });
@@ -97,20 +100,6 @@ describe("goal worker RPC", () => {
await expect(subagentWorkState(events)).resolves.toBe(expected);
}
});
it("reconciles one retained run without treating other work as its worker", async () => {
const snapshot = {
kind: "pi-subagents.async-status-snapshot",
version: 1,
omitted: { runs: 0, children: 0, byteLimitExceeded: false },
runs: [{ id: "other", state: "running" }, { id: "finished", state: "complete" }, { id: "parent", state: "complete", children: [{ id: "nested", state: "running" }] }],
};
const events = new Events();
replyToRpc(events, () => ({ text: "status", asyncSnapshot: snapshot }));
await expect(retainedRunState(events, "missing")).resolves.toBe("idle");
await expect(retainedRunState(events, "finished")).resolves.toBe("idle");
await expect(retainedRunState(events, "nested")).resolves.toBe("active");
});
});
describe("managed process status", () => {