Simplify visible supervision and keep checkpoints fresh

This commit is contained in:
wassname2
2026-09-08 14:44:58 +08:00
parent d5729ac106
commit cecb1e9119
46 changed files with 3359 additions and 444 deletions
+43
View File
@@ -1,5 +1,48 @@
# pi-goals contributor notes
## User intent for supervision
The following preferences are the user's words, recorded on
[`experiment/goals-owned-supervision`](https://github.com/wassname/pi-goals/blob/06794bf/AGENTS.md#user-intent-for-this-branch):
> To be clear, the hope is we can have a smart supervisor like you, with judgment and context. But it doesn't use many tokens as it checks in and sees an overview.
>
> It steers a smaller model, adding perspective and judgment.
>
> It compacts every 150k or similar to avoid cost and context rot.
>
> It has a goal / plan on a Ralph-loop-type repeat.
>
> That lets the worker be a cheaper model, and the supervisor more expensive, and still get a good outcome.
>
> Oh, and since it's two panes, the user can review both!
>
> Well, I want to see what the supervisor is thinking and saying. That's the whole point: all supervisor thinking and messages should be visible.
>
> So that should make it obvious that I need to see the messages, and the supervisor needs to use judgment. For example, it could say how we are tracking or whatever every time, and it would be useful, like in the recap.
>
> And it would only be a few output tokens.
— wassname; spelling and punctuation corrected in the source by Pi/OpenAI.
## Confirmed product preferences
- One installable pi-goals package, not separately configured supervisor packages.
- Run the same Pi profile and package set in two real interactive sessions, visible beside each other in Herdr. Supervisor mode is a role, not a separately assembled profile or installation. Fork the main planning session, activate supervisor mode and compact the fork.
- The supervisor retains the compacted planning session. Its repeated review loop reminds it that it is the supervisor and supplies the current canonical plan. Keep those directly available rather than relying on the compaction summary alone.
- Load Intercom once per Pi process. Do not add a second copy or extra standalone supervisor package when activating supervisor mode.
- The worker carries implementation detail. The supervisor gets incremental high-level views and retains its judgments, user intent and decisions.
- Remember the last model selected separately for planning, working and supervising.
- Default to at least three task-specific alignment questions before the final plan. An explicit current-plan request to skip questions waives that round, not future plans.
- Discuss returns the review menu to normal chat, preserving the draft. Do not immediately reopen the menu while the conversation is unfinished. Ready is the human's approval to start work.
- Live supervision must not use headless Pi RPC mode or a bespoke plan-lifecycle RPC layer. Use the real sessions and Intercom messages for views, steering and plan-bound approval checkpoints.
- Keep the design simple and reliable. The user reports that it is constantly breaking; adding more orchestration or approval forms is not progress. Preserve working components and remove unnecessary layers.
- Show useful supervisor assessments, advice and perspective, not only hidden tool arguments or delivery receipts. Keep the assessment brief. The supervisor's job is judgment and helping the worker stay on course, not filling forms; transport and approval bookkeeping are supporting details.
- After the initial fork compaction, compact the supervisor again above 100k current-context tokens (not cumulative usage), respecting the model's context limit. This is the latest user clarification of the earlier approximate 150k preference. Token/cost savings and the usefulness of advice need a real task trial; passing protocol tests alone does not establish either.
These are user preferences, not a claim that the current implementation satisfies every point.
Validate them in real panes as well as automated tests; record remaining gaps and actual per-role usage.
## Tests
Run `npm test` before a commit. It includes unit and flow tests plus the RPC review test.
+54 -18
View File
@@ -95,27 +95,60 @@ survive clear and reload. Cleared legacy sessions adopt the new defaults on relo
plans retain their settings so supervision is not attached midway through work.
Install/load **only pi-goals**. Its internal modules contain the supervisor; the package bundles
`pi-intercom` 0.10.0 and `@sting8k/pi-vcc` 0.5.0 as locked runtime dependencies. Pi's manifest loads
the bundled Intercom resources through `node_modules/` paths. VCC is used as a compiler, not loaded
as a separate extension. Pi core stays a peer dependency. **Herdr remains the supported terminal
host** for launching/focusing the supervisor. Load the package directory (`pi -e .`), not only
`src/index.ts`, so the manifest can supply Intercom too.
`pi-intercom` 0.10.0 and `@sting8k/pi-vcc` 0.5.0 as locked runtime dependencies. An already installed
Intercom is reused; otherwise pi-goals initializes its bundled copy after installed extensions load.
There is one Intercom registration per process, not an extra supervisor companion. VCC is used as
a compiler, not loaded as another extension. Pi core stays a peer dependency. **Herdr remains the
supported terminal host**. Both panes use the same Pi agent directory and configured package set;
explicit CLI resource choices are preserved without adding `-e` companions.
Ready is the human's plan approval. Pi-goals forks the planning session, initializes the supervisor
with the original plan and supervisor policy, and waits for acknowledged pairing before starting the
worker. The initial supervisor view can steer; it is not another mandatory plan-approval gate.
The supervisor fork is compacted unless its known context is already at most 20k tokens. Only the
supervisor is compacted. Its normal context policy checks near 100k tokens, subject to its model limit.
The supervisor fork is compacted unless its known context is already at most 20k tokens. If native Pi
reports that no older history is eligible for compaction, startup retains the fork unchanged and continues;
other compaction failures still block initialization. Pi-goals requests compaction only for the
supervisor. Later reviews compact above 100k **current-context** tokens (not cumulative
usage), or earlier for a smaller model context limit. Each model call explicitly supplies the
supervisor role and current canonical plan while retaining the compacted planning context and judgments.
The internal supervisor provides incremental VCC views and retains its decisions. The plan-aware
policy checks every 50 model turns or 60 minutes, or when the worker settles with no tracked
background work. Process/subagent providers that cannot answer are reported as unknown; they do not
prove the worker is finished. The `SUPERVISOR.md` policy lookup is unchanged: project `.pi/SUPERVISOR.md`, then the Pi agent directory, then the built-in policy. Auto-continue is
The internal supervisor provides incremental VCC views and retains its decisions. While an assessment
is active, later routine updates coalesce into one pending marker instead of queuing model turns or
replacing the active view. Once settled, it requests one fresh VCC overview from the worker's latest
compaction summary and current branch. This is a bounded high-level summary, not a lossless transcript;
truncation is labeled and the overview names the read-only source session for omitted detail.
Each explicit goal checkpoint carries a fresh bounded worker snapshot, including the latest user direction;
it becomes visible only when that checkpoint is assessed. The canonical plan and checkpoint identity remain
separate from replaceable routine status.
The plan-aware policy checks every 50 model turns or 60 minutes, or when the worker settles with no tracked
background work. Absent optional trackers contribute zero tracked work. Installed process/subagent
providers that cannot answer remain unknown and do not prove the worker is finished. Unregistered
detached work is not tracked. The `SUPERVISOR.md` policy lookup is unchanged: project `.pi/SUPERVISOR.md`, then the Pi agent directory, then the built-in policy. Auto-continue is
suspended while the steward is enabled so there is only one continuation policy.
At each review the supervisor is prompted to visibly give a brief progress assessment and useful
advice, not just a delivery receipt. Its advice and no-intervention assessment are also displayed
as durable messages. Supervisor mode allows native read/grep/find/ls and the narrow supervisor
tools only. Writes, bash (including `!`), process/subagent/scheduler and unknown extension tools
are blocked at execution as well as hidden, including on reload and in a stopped plan fork.
This is a model-tool policy, not an OS sandbox for arbitrary trusted extension code.
All worker/supervisor traffic uses Intercom. Local lifecycle calls are ordinary pi-goals module
calls, with no plan-lifecycle RPC dispatcher or headless live Pi process. Disconnection invalidates
pending approval and is shown explicitly; a send does not prove receipt or execution.
One `CompleteGoal` call asks this supervisor about direction and scope, then runs the normal fresh
read-only evidence judge. Approving one goal does not finish supervision. Cancelled, stale or
mismatched replies do not sign off goals. `/goals steward off` ends this plan's supervision and
read-only evidence judge. A prematurely checked submitted goal is reopened before review; only accepted
sign-off checks it again. The judge's checks section accepts ordinary numbered and indented Markdown lists,
but an empty section cannot borrow a list from a later heading. Approving one goal does not finish supervision. Cancelled, stale or
mismatched replies do not sign off goals. Goal/revision identity is bound in code to the checkpoint
actually presented to the supervisor, not copied into a form by the model. Supervisor model checkpoints
have no arbitrary thinking deadline: slow healthy reviews may finish. Explicit cancellation, replaced
plans, disconnects and actual settled provider failures still fail safely; startup/attachment deadlines
are separate. A genuinely settled empty response returns an incomplete assessment, not an invented
human-input dependency. Later worker progress/cadence can resume supervision without a human poke,
and failure does not immediately retry the same view. A required completion checkpoint may wait, but routine supervision does not block worker
work. `/goals steward off` ends this plan's supervision and
cancels pending goal requests; it does not close the human's terminal pane.
Navigation: `/goals supervisor` focuses the supervisor, `/goals worker` returns to the worker, and
@@ -149,8 +182,8 @@ old pi-subagents reviewer runs are not reused as supervisor sessions.
### Migrating an already-running installation
After validating this package, remove the old standalone supervisor and standalone Intercom entries
from Pi's package list, leaving pi-goals. **Reload existing workers before selecting Ready again**:
After validating this package, remove any old standalone supervisor entry. A compatible standalone
Intercom may remain: pi-goals reuses it rather than registering a second copy. **Reload existing workers before selecting Ready again**:
an old worker still has old launch arguments in memory and can launch both old and internal copies.
Reload both sides of a retained pairing. Do not add extra `-e` supervisor/Intercom arguments.
Duplicate Intercom registries are diagnosed and plan bootstrap is refused; Pi also reports conflicting
@@ -195,7 +228,7 @@ Planning/judge text lives in [`src/prompts.ts`](src/prompts.ts); supervisor poli
## Develop
```bash
pi -e . # package manifest includes bundled Intercom
pi -e . # one package; reuses installed or bundled Intercom
npm test # unit/flow/RPC + inherited node:test supervisor regressions
npm run test:rpc # real-Pi conversational review, local offline model
npm run test:supervisor # inherited lifecycle/VCC/correlation/recovery regressions
@@ -215,13 +248,16 @@ goal reviews, and judge isolation without relying on a live terminal.
The moved VCC dependency is source-only and has upstream type incompatibilities with current Pi/Intl
unions. `tsconfig.build.json` maps just its four imported API surfaces to narrow local declarations;
the actual pinned VCC source still runs in tests and production. All pi-goals source is typechecked
the actual pinned VCC source still runs in tests and production. The Intercom extension factory
has the same narrow declaration boundary; no dependency code is rewritten. All pi-goals source is typechecked
and linted; the node:test suite is run separately, not silently collected/skipped by Vitest.
Validation (2026-09-07): **67 Vitest tests and 118 internal supervisor tests passed, with no skips**,
Baseline validation (2026-09-07, before the current supervision changes): **67 Vitest tests and 118 internal supervisor tests passed, with no skips**,
including the packed real-Pi/Intercom flow. Typecheck, lint, build and diff checks passed. Independent
review and targeted recheck are complete. See the [saved validation and review disposition](docs/reviews/2026-09-07_single-package-role-models.md).
The current isolated-worktree validation is recorded in [the approved supervision plan](docs/slop/plans/20260908_simple-visible-supervision.md). Packed registration/reload is also tested with Intercom loaded before or after pi-goals. The full Intercom peer flow and Herdr UAT need host Unix-socket/control access; do not treat registration alone as a successful supervision trial.
Neither automated test proves visual Herdr rendering/navigation or measured token savings. The
previous live trial contained a historical tool call without a saved result, which can still block
supervisor `done`. Use `/goals clear` to explicitly disconnect; genuine outstanding-work checks have
@@ -0,0 +1,23 @@
## Review
Reviewed the worker report, follow-up-only diffs under `/tmp/pi-goals-checkpoint-fix/`, corresponding current source/tests, and the parents final latch correction. The saved `before/` files separate this work from the previously reviewed implementation; this is not a re-review of the broader uncommitted changes.
- **Correct — fresh, immutable checkpoints:** `src/internal/supervisor/index.ts:362396` captures a complete worker snapshot for each explicit review and rechecks request identity, cancellation, pairing generation, session identity and canonical-plan hash after capture. The branch is read after tracker queries in `captureWorkerView`. The snapshot replaces `worker_view` only when its checkpoint becomes active; routine updates cannot overwrite an active assessment. Covered by `test/internal-supervisor/plan.test.ts:73168`.
- **Correct — current direction and bounded transport:** `src/internal/supervisor/view.ts:267288` pins the latest non-supervisor user direction separately from older summaries, with explicit truncation. `src/internal/supervisor/protocol.ts:1939` projects checkpoint identity into replies and bounds the actual serialized request, including JSON escaping and Unicode. Its size calculation matches the installed Intercom brokers payload measurement. Success, duplicate rejection and failure responses omit the snapshot.
- **Correct — genuine settlement required:** `src/internal/supervisor/index.ts:899908,11981215` records empty final output at `agent_end` but acts only at `agent_settled`, and only for an unresolved assessment. Successful verdicts and separately queued checkpoints are excluded. The installed Pi implementation emits settlement after retry, compaction and queued-continuation processing (`node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js:744781`). Nonempty human-question handling remains distinct.
- **Fixed by parent; verified read-only — failure incorrectly latched a human dependency:** The earlier `failAssessment` set `awaitingUser=true`, suppressing later ordinary worker views indefinitely. Current `src/internal/supervisor/index.ts:634647` clears that latch and stale routine-refresh flags, then refreshes the footer. This permits later progress/cadence without immediately retrying the same dirty view, while retaining separately queued checkpoints. The two regressions at `test/internal-supervisor/plan.test.ts:188203` fail in `wait-latch-red.log` and pass in `wait-latch-green.log`. Explicit `needs_user` and nonempty-question pause paths remain present.
**No issues found.**
**Merge verdict: OK with notes.** The scoped changes are approved. Parent-owned final aggregate validation and live-pane acceptance remain outstanding evidence, not established by this read-only review.
### Validation and residual risks
- Inspected saved worker results: **89 focused tests**, **169 supervisor tests**, **51 integration tests**, plus clean typecheck/lint. These precede the parents latch correction.
- Inspected parents post-correction regression log: **2 passed**, covering empty-final and provider-error recovery, no immediate retry loop, and subsequent ordinary progress.
- No commands, edits, Herdr operations, settings changes, staging or push were performed by this reviewer.
- Parents final `npm test`, typecheck, lint and build were starting; their results were not available for this verdict. Complete `npm test` before committing, as required by `AGENTS.md`.
- Live current-pane behavior and supervision quality/cost still require parent validation. Bounded snapshots can explicitly truncate; historical replay is not live acceptance.
@@ -0,0 +1,26 @@
[
{
"timestamp": "2026-09-08T05:41:10.346Z",
"content": [
"Sign-off REJECTED. Missing:\nchecked-artifact list before VERDICT: accept\n\n--- judge ---\n...\nEL/converter.mjs`, lines 63-65, 74-75\n - Quote: `if (!Number.isFinite(numericValue)) { throw new Error('Value must be a finite number'); }` and `if (!Number.isFinite(result)) { throw new Error('Conversion resulted in a non-finite value'); }`\n - Confirms rejection of NaN, Infinity, and overflow-to-Infinity.\n\n6. **58 tests in converter.test.mjs using node:test**:\n - Path: `/tmp/pi-goals-herdr-uat.TGXaEL/converter.test.mjs`, lines 1-8\n - Quote: `import test from 'node:test'; import assert from 'node:assert'; import { convert } from './converter.mjs';`\n - Confirms real module import and node:test framework; grep found exactly 58 test cases.\n\n7. **Test output log saved and all 58 tests pass**:\n - Path: `/tmp/pi-goals-herdr-uat.TGXaEL/evidence/function-tests.log`, bottom\n - Quote: ` tests 58` ` pass 58` ` fail 0`\n - Confirms all 58 tests passed in a real node:test run.\n\n8. **Specific discriminator test cases all passing**:\n - Path: `/tmp/pi-goals-herdr-uat.TGXaEL/evidence/function-tests.log`\n - Quote: \n - `✔ 0 C = 32 F` (correct known conversion both directions)\n - `✔ Accept exact -273.15 C` (exact absolute-zero acceptance)\n - `✔ Reject -273.16 C (below absolute zero)` (below-limit rejection)\n - `✔ Reject trailing junk \"12junk\"` (regression test: rejects partial parseFloat)\n - `✔ Reject hexadecimal \"0x10\"` (invalid syntax rejection)\n - `✔ Reject NaN input` and `✔ Reject Infinity input` (nonfinite input rejection)\n - `✔ Reject conversion overflow to Infinity` (nonfinite result rejection)\n - `✔ All valid inputs produce finite outputs` (output finiteness assertion)\n - Confirms all subtle failure modes explicitly tested and passing.\n\n9. **Goal 2 (CLI) explicitly NOT implemented** (per pause gate):\n - Paths: find results show no cli.mjs, cli.test.mjs, cli-demo.log, or cli-tests.log in fixture root\n - Confirms the pause gate after goal 1 was respected; goal 2 awaits explicit user instruction.\n\nVERDICT: accept\nmissing:"
]
},
{
"timestamp": "2026-09-08T06:00:40.739Z",
"content": [
"Judge returned no VERDICT line. Accepted inconclusive — logged.\n\n--- judge ---\n(no output)\n\nGoal ticked [x] in .pi/plan/01a07f74-f42d-7eee-85b0-10df065920a0-v1.md."
]
},
{
"timestamp": "2026-09-08T06:09:00.490Z",
"content": [
"Supervisor: needs_work. Inspected cli.mjs, evidence/cli-tests.log and evidence/cli-demo.log. Core CLI behavior and junk regression are evidenced, but neither log includes the required nonfinite conversion-result case (a finite Number.MAX_VALUE string in C). Add a real child-process overflow rejection test and demo recording arguments, empty stdout, stderr and nonzero exit, then rerun and save actual logs. Also reconcile the stale worker view, which still says CLI work is paused, with the explicit user authorization before claiming this checkpoint complete."
]
},
{
"timestamp": "2026-09-08T06:25:30.356Z",
"content": [
"Supervisor review failed: Error: Supervisor request cancelled"
]
}
]
@@ -0,0 +1,85 @@
#!/usr/bin/env node
// Bounded trial accounting only. Reads explicitly named Pi session JSONL files, not RPC event logs.
import { readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
function session(path) {
const rows = readFileSync(path, "utf8").trim().split("\n").filter(Boolean).map(line => JSON.parse(line));
if (rows[0]?.type !== "session") throw new Error(`Not a Pi session JSONL file: ${path}`);
const byId = new Map();
for (const row of rows.slice(1)) if (typeof row.id === "string") byId.set(row.id, row);
return { header: rows[0], entries: [...byId.values()] }; // Replayed/replaced records count once by entry ID.
}
function snapshot(workerFile, manifestFile) {
const path = resolve(workerFile);
const worker = session(path);
const manifest = { workerFile: path, workerSessionId: worker.header.id, startMs: Date.now(), baselineIds: worker.entries.map(entry => entry.id) };
writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, { flag: "wx", mode: 0o600 });
return { manifestFile: resolve(manifestFile), workerSessionId: worker.header.id, startMs: manifest.startMs, note: "Snapshot taken before Ready. Keep worker idle until Ready; do not overwrite this boundary." };
}
const fields = ["input", "output", "cacheRead", "cacheWrite", "totalTokens"];
const empty = () => ({ entries: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, missingUsageEntries: [], missingFields: {}, recordedPositiveCostSubtotal: 0, zeroOrMissingCostEntries: [] });
function add(totals, entry, usage) {
totals.entries++; // Persisted accounting entries, including listed missing-usage entries; not provider calls.
if (!usage) { totals.missingUsageEntries.push(entry.id); return; }
for (const field of fields) {
if (Number.isFinite(usage[field]) && usage[field] >= 0) totals[field] += usage[field];
else totals.missingFields[field] = (totals.missingFields[field] ?? 0) + 1;
}
if (Number.isFinite(usage.cost?.total) && usage.cost.total > 0) totals.recordedPositiveCostSubtotal += usage.cost.total;
else totals.zeroOrMissingCostEntries.push(entry.id);
}
function roleUsage(path, baseline, endMs, role) {
const data = session(path);
const inherited = new Set(baseline.baselineIds);
const marker = role === "supervisor" ? data.entries.findIndex(entry => entry.type === "custom" && entry.customType === "pi-goals-supervisor" && resolve(entry.data?.binding?.workerSession ?? "/missing") === baseline.workerFile) : -1;
if (role === "supervisor" && marker < 0) throw new Error("No matching native supervisor bootstrap marker: refusing to count inherited planning usage as supervisor work");
const total = empty(); const assistant = empty(); const compaction = empty(); const nestedTools = empty();
const models = new Set(); const compactions = []; const entriesCounted = []; const missingTimestamps = [];
let selectedModel = null; let completionToolResults = 0;
data.entries.forEach((entry, index) => {
if (entry.type === "model_change") selectedModel = `${entry.provider}/${entry.modelId}`;
if (entry.type === "message" && entry.message?.role === "assistant" && entry.message.provider && entry.message.model) selectedModel = `${entry.message.provider}/${entry.message.model}`;
if (index <= marker || inherited.has(entry.id)) return;
const time = Date.parse(entry.timestamp);
if (!Number.isFinite(time)) { missingTimestamps.push(entry.id); return; }
if (time < baseline.startMs || time > endMs) return;
let category; let usage;
if (entry.type === "message" && entry.message?.role === "assistant" && entry.message.stopReason !== "pending") {
category = assistant; usage = entry.message.usage;
models.add(`${entry.message.provider ?? "unknown"}/${entry.message.model ?? "unknown"}`);
} else if (entry.type === "compaction" || entry.type === "branch_summary") {
category = compaction; usage = entry.usage;
compactions.push({ entryId: entry.id, type: entry.type, timestamp: entry.timestamp, selectedModel, modelAttribution: "selected model inferred from preceding session entries; a custom summarizer may use another model", tokensBefore: entry.tokensBefore ?? null, hasUsage: Boolean(usage) });
} else if (entry.type === "message" && entry.message?.role === "toolResult") {
if (entry.message.toolName === "CompleteGoal") completionToolResults++;
if (entry.message.usage) { category = nestedTools; usage = entry.message.usage; }
}
if (!category) return;
add(category, entry, usage); add(total, entry, usage); entriesCounted.push(entry.id);
});
return { role, sessionFile: resolve(path), sessionId: data.header.id, reportedAssistantModels: [...models], totalRecordedUsage: total, assistant, compaction, nestedTools, compactions, entriesCounted, excludedUnknownTimestampEntries: missingTimestamps, completionToolResults,
cost: total.zeroOrMissingCostEntries.length || total.missingUsageEntries.length ? "unavailable/incomplete: zero or missing pricing is not proof of zero cost" : "recorded positive costs only; not independently verified pricing",
compactionCoverage: "All post-boundary compaction/branch-summary usage is included when persisted. Missing entries are listed, not assumed free. Session records do not explicitly label initial versus later compaction; inspect the timestamp/marker sequence." };
}
const [command, first, second, out] = process.argv.slice(2);
let result;
if (command === "start" && first && second) result = snapshot(first, second);
else if (command === "finish" && first && second) {
const baseline = JSON.parse(readFileSync(first, "utf8"));
const worker = session(baseline.workerFile);
if (worker.header.id !== baseline.workerSessionId) throw new Error("Worker session identity changed since the boundary");
const endMs = Date.now();
result = { startMs: baseline.startMs, endMs,
worker: roleUsage(baseline.workerFile, baseline, endMs, "worker"), supervisor: roleUsage(resolve(second), baseline, endMs, "supervisor"),
currentContext: "These are cumulative recorded token metrics, NOT current context. The >100k compaction trigger uses ctx.getContextUsage().tokens. Native compaction tokensBefore is reported separately above.",
freshEvidenceJudge: { status: "instrumentation gap", explanation: "Current CompleteGoal runs a fresh pi -p --no-session judge and saves its text receipt, not provider Usage. Completion tool result counts do not establish judge invocation counts or tokens. Report judge tokens/cost unavailable unless separately instrumented in an authorized trial; do not hide this overhead in worker/supervisor totals." },
limitations: ["Wait for both trial sessions to settle/stop before finish so no partial JSONL/provider usage is mistaken for completed work.", "Inherited pre-fork history is excluded using both the worker baseline ID set and the supervisor bootstrap marker.", "No RPC message_update/message_end events are counted; duplicate persisted entry IDs are counted once.", "The entries counters are persisted accounting-entry counts, not provider-call counts. Native split-turn compaction may combine multiple calls in one entry, as may aggregated nested-tool usage. Entries with missing usage are counted and listed separately.", "Tool-result nested usage is separate. Do not add it again from another session without proving it is disjoint.", "Long complete VCC refreshes are ordinary supervisor calls and therefore included in supervisor usage, not assumed free."] };
} else throw new Error("Usage: node extract-trial-usage.mjs start WORKER_SESSION.jsonl BOUNDARY.json | finish BOUNDARY.json SUPERVISOR_SESSION.jsonl [USAGE_OUTPUT.json]");
const text = `${JSON.stringify(result, null, 2)}\n`;
if (command === "finish" && out) writeFileSync(out, text, { flag: "wx", mode: 0o600 });
process.stdout.write(text);
@@ -0,0 +1 @@
{"error":{"code":"agent_prompt_stalled","message":"agent prompt produced no observed working or blocked state within 5000 ms; current status is done"},"id":"cli:agent:prompt"}
@@ -0,0 +1,96 @@
{
"sourceSession": "/home/ubuntu/.pi/agent/sessions/--tmp-pi-goals-herdr-uat.TGXaEL--/2026-09-08T05-27-39-268Z_01a07f7c-7a04-7015-bfca-8af0a2fc2089.jsonl",
"compactions": [
{
"id": "4c93819c",
"timestamp": "2026-09-08T06:03:51.958Z",
"tokensBefore": 43242,
"firstKeptEntryId": "19aff06f",
"usage": {
"input": 6,
"output": 1811,
"cacheRead": 0,
"cacheWrite": 13474,
"reasoning": 12,
"totalTokens": 15291,
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0,
"total": 0
}
}
}
],
"advice": [
{
"timestamp": "2026-09-08T05:35:15.748Z",
"tool": "steer",
"arguments": {
"message": "Ready is selected: implement goal one only, with converter.mjs and converter.test.mjs, then run node --test converter.test.mjs and save actual output to evidence/function-tests.log. Report the artifact paths and quote the saved test results for sign-off; do not start any CLI work before the user's explicit instruction."
}
},
{
"timestamp": "2026-09-08T05:37:00.558Z",
"tool": "review_goal",
"arguments": {
"decision": "needs_work",
"reason": "Read converter.mjs: it directly returns `(numericValue * 9 / 5) + 32` and `(numericValue - 32) * 5 / 9` without a finite-result check. Goal one requires rejecting nonfinite conversion results. Add the result guard and an explicit overflow rejection test, rerun the function suite and save its actual log. Keep CLI work paused."
}
},
{
"timestamp": "2026-09-08T05:38:59.617Z",
"tool": "review_goal",
"arguments": {
"decision": "needs_work",
"reason": "converter.mjs now guards both results, but converter.test.mjs's 'Verify conversion output validation is enforced' only checks 1e10, 0 and 32 produce finite outputs. Add assert.throws(() => convert(Number.MAX_VALUE, 'C'), /non-finite/) to exercise actual result overflow, rerun node --test converter.test.mjs and save the actual log. The current test cannot distinguish the earlier broken implementation. Keep goal two paused."
}
},
{
"timestamp": "2026-09-08T05:39:48.084Z",
"tool": "review_goal",
"arguments": {
"decision": "approve",
"reason": "Goal one remains in scope. The inspected implementation has finite-result guards, and converter.test.mjs now asserts actual overflow rejection with Number.MAX_VALUE. evidence/function-tests.log records '✔ Reject conversion overflow to Infinity', ' tests 58', ' pass 58', and ' fail 0'. Approve this checkpoint only; preserve the user-required pause before any goal-two work."
}
},
{
"timestamp": "2026-09-08T05:41:29.463Z",
"tool": "let_it_run",
"arguments": {
"reason": "The worker states: \"Per the execution gate in the plan, I am now PAUSED and waiting for your explicit instruction before implementing Goal 2 (CLI).\" This matches the required user-controlled pause."
}
},
{
"timestamp": "2026-09-08T05:58:40.850Z",
"tool": "let_it_run",
"arguments": {
"reason": "The worker states: \"Per the execution gate in the plan, I am now PAUSED and waiting for your explicit instruction before implementing Goal 2 (CLI).\" No subsequent user authorization for goal two appears in the view."
}
},
{
"timestamp": "2026-09-08T06:00:19.055Z",
"tool": "review_goal",
"arguments": {
"decision": "approve",
"reason": "Previously inspected converter.mjs finite-result guards and converter.test.mjs assertion that convert(Number.MAX_VALUE, 'C') throws /non-finite/. The latest worker view names evidence/function-tests.log and quotes ' tests 58', ' pass 58', ' fail 0', plus '✔ Reject conversion overflow to Infinity (0.299328ms)'. Direction and scope match goal one; independent artifact verification should confirm sign-off. Goal two remains paused pending explicit user instruction."
}
},
{
"timestamp": "2026-09-08T06:00:53.932Z",
"tool": "let_it_run",
"arguments": {
"reason": "The user instructed: \"Keep all CLI work paused until I explicitly authorize goal two.\" The worker reports: \"I am now paused and waiting for your explicit instruction before implementing Goal 2 (the CLI). No CLI files have been created.\""
}
},
{
"timestamp": "2026-09-08T06:09:00.488Z",
"tool": "review_goal",
"arguments": {
"decision": "needs_work",
"reason": "Inspected cli.mjs, evidence/cli-tests.log and evidence/cli-demo.log. Core CLI behavior and junk regression are evidenced, but neither log includes the required nonfinite conversion-result case (a finite Number.MAX_VALUE string in C). Add a real child-process overflow rejection test and demo recording arguments, empty stdout, stderr and nonzero exit, then rerun and save actual logs. Also reconcile the stale worker view, which still says CLI work is paused, with the explicit user authorization before claiming this checkpoint complete."
}
}
]
}
@@ -0,0 +1,175 @@
> @wassname2/pi-goals@0.2.2 test
> vitest run && npm run test:supervisor
RUN v4.1.9 /home/ubuntu/.pi/agent/worktrees/pi-goals-simple-visible-supervision
Test Files 14 passed (14)
Tests 81 passed (81)
Start at 14:01:25
Duration 24.12s (transform 2.35s, setup 503ms, import 23.03s, tests 50.95s, environment 6ms)
> @wassname2/pi-goals@0.2.2 test:supervisor
> node --import tsx --test test/internal-supervisor/*.test.ts
✔ retries intercom registration when pi-intercom loads after pi-supervise (4.227298ms)
✔ a directive with no text is rejected, so the worker never sees undefined (0.426221ms)
✔ a directive from the paired supervisor becomes a real user message (37.225884ms)
✔ a directive to a busy worker interrupts, instead of waiting for the whole task (15.784109ms)
✔ a directive from an unpaired session is dropped (12.29568ms)
✔ a second pair takes over, and the first supervisor is told it lost the worker (26.396215ms)
✔ only the paired worker can end a run (8.158348ms)
✔ the programmatic pairing API waits for the worker acknowledgement (2.239757ms)
✔ the worker acknowledges a pair, so the supervisor knows it was heard (8.486998ms)
✔ a goal the supervisor inferred reaches the worker, which owns the view header (142.929505ms)
✔ the second view carries only what happened after the first (442.433685ms)
✔ a message addressed to a different session is ignored (10.742462ms)
✔ on settle the worker publishes a view built from the live branch (56.519214ms)
✔ the view is built from the live branch, not from every entry in the session (56.418381ms)
✔ an unpaired session publishes nothing on settle (0.741332ms)
✔ supervision never stops itself: no round limit at all (5.534566ms)
✔ goal, pairing and the steer count all survive a reload together (0.550053ms)
✔ a view arriving during unrelated supervisor thinking waits for a fresh complete overview (11.333382ms)
✔ the nudge repeats neither the instructions already sent nor the verdict rules (5.152421ms)
✔ a multi-line goal returns to supervisor context every fifth review and after compaction (32.247189ms)
✔ a one-line goal is not redundantly reinserted (27.177279ms)
✔ a check in and a worker that stopped ask for different things (10.584784ms)
✔ a loop still gets named after the supervisor compacts, from restored state (1.050267ms)
✔ a session that does not answer the roll call is not offered as a worker (501.888517ms)
✔ a child run stays out of the roll call, so it can never be picked (5.986158ms)
✔ a session already paired stays out of the roll call, and a free one answers (16.582061ms)
✔ /supervise look asks the worker for a fresh view, rather than the supervisor guessing (307.790991ms)
✔ let_it_run says the turn is over, so it is not called four times running (0.647696ms)
✔ a sign-off verdict is answered, not aborted, and a runaway is still cut (0.842584ms)
✔ every verdict result names the way to end the turn, steer included (0.574206ms)
✔ an old view is dropped from context once its verdict is in, and the verdict is kept (1.074702ms)
✔ a worker session never has its context rewritten (0.294353ms)
✔ a newly presented view starts a fresh look (5.78599ms)
✔ a tool a worker cannot use never aborts its turn (0.418385ms)
✔ a resume onto a session that is gone drops the pairing and says so (5.830953ms)
✔ a resume onto a live worker keeps supervising, and takes the writers back off (6.00274ms)
✔ state written before recentSteers existed still loads (0.169851ms)
✔ done unpairs the worker, so it stops publishing views (421.186666ms)
✔ with no goal the supervisor cannot steer, it must ask the human (0.899218ms)
✔ set_goal binds an inferred goal, and steering then works (500.8593ms)
✔ a goal given at pair time still allows steering (0.585863ms)
✔ done is refused while the worker has an unanswered tool call (12.548547ms)
✔ done is allowed once nothing is outstanding (6.609937ms)
✔ steer refuses when the session is not supervising (0.480846ms)
✔ a reworded repeat of an earlier instruction is sent, and named back to the supervisor (0.801901ms)
✔ overlap scores rewording high and a different instruction low (0.177702ms)
✔ the view of the old worker cannot be used to judge the new one (6.401726ms)
✔ with one other session here, /supervise needs no target and the whole line is the goal (500.788638ms)
✔ naming the worker still works, and the rest of the line is the goal (0.601582ms)
✔ with two free sessions here, /supervise asks which one, and pairs with the choice (501.732414ms)
✔ a goal that is a path is read from the file, so it is not pasted every run (2.041028ms)
✔ a long goal is one short line above the picker, and reaches the worker whole (501.665702ms)
✔ a session that stayed quiet is still on the list, because 0 free is a dead end (500.678627ms)
✔ a cancelled picker pairs with nothing (500.632721ms)
✔ supervising takes the writing tools away, and stopping gives them back (501.535796ms)
✔ stopping gives back the writers without undoing another extension's tools (501.829786ms)
✔ a first word that names no session is refused, rather than folded into the goal (0.789825ms)
✔ a goal with spaces needs no target, and @name takes the rest of the line as the goal (501.348506ms)
✔ the brief starts no turn, so there is no answer before the first view (6.442694ms)
✔ /supervise goal changes the goal without breaking the pairing (1.012968ms)
✔ the footer says which side of a pairing this session is, and clears when it ends (506.27294ms)
✔ a session that is not supervising never sees the supervisor tools (6.599768ms)
✔ worker_view refuses when there is no worker, rather than implying a pairing (0.619204ms)
✔ the view names the worker's model and how full its context is (58.171961ms)
✔ supervising a second session is refused while the first is still paired (0.831384ms)
✔ the supervisor gets a look at a working worker every half hour, without being asked (975.124652ms)
✔ a human message in the worker session is not a reason to stand back (6.169812ms)
✔ letting a stopped worker run says plainly that the worker stays stopped (11.286563ms)
✔ a stopped worker is looked at again, so let_it_run cannot silence the pairing (926.086028ms)
✔ a worker that pairs at the prompt and never takes a turn is still watched (605.707779ms)
✔ a worker that reloads at the prompt starts watching itself again (605.044296ms)
✔ a timer look at a worker that has not moved is not sent, until it has been skipped three times (2428.612949ms)
✔ the worker counts reviews in a row where nothing changed (579.699233ms)
✔ an unacknowledged pair gives up, and a takeover cancels that timer (4.893877ms)
✔ duplicate standalone Intercom registries are diagnosed and cannot bootstrap a plan (0.862724ms)
✔ retained non-plan supervision reconnects after the worker reloads, not on unrelated peer traffic (345.917589ms)
✔ retained non-plan supervision reconnects after the supervisor reloads, not on unrelated peer traffic (628.225316ms)
✔ busy supervisor retains its active view and requests one complete overview after settling (31.243343ms)
✔ plan bootstrap compacts only the supervisor and pairing alone never starts a worker or a review (32.264425ms)
✔ goal decisions are correlated, preserve the pair across two goals, and cannot call overall done (4.70302ms)
✔ abort and stop cancel pending requests; late decisions cannot approve a replacement (3.9249ms)
✔ 50 actual model turns trigger one view, independent of the number of messages (5.510548ms)
✔ unknown background providers are not proof of quiescence (0.401943ms)
✔ stale plan content invalidates a pending goal review (6.905509ms)
✔ small forks skip compaction, but real compaction failure prevents pairing (3.703867ms)
✔ native small-history result permits startup after an attempted compaction (50000) (1.967591ms)
✔ native small-history result permits startup after an attempted compaction (null) (2.078297ms)
✔ the hour timer and a coincident turn checkpoint produce a single view (6.466486ms)
✔ settled checks wait for tracked processes and subagents to finish (2.789561ms)
✔ bootstrap stop cannot resurrect a supervisor after compaction completes (2.36892ms)
✔ a restarted worker reconnects by exact saved session identity without a new supervisor (5.448449ms)
✔ unknown initial context must compact instead of taking the known-small shortcut (1.249941ms)
✔ null post-compaction usage cannot raise the next configured 100k checkpoint (2.734318ms)
✔ stopping a routine view during compaction invalidates its suspended continuation (3.144067ms)
✔ restart of a provisional bootstrap resumes compaction and pairing in the same saved session (3.524611ms)
✔ command preserves a stopped supervisor across reload (2.800035ms)
✔ done preserves a stopped supervisor across reload (2.91728ms)
✔ same-binding replay retains activation when the supervisor lost its acknowledgement (3.51941ms)
✔ model-unavailable stop validates binding, cancels pending reviews and ignores old directives (2.44535ms)
✔ supervisor mode is a native inspection allowlist at visibility and execution, including reload and stopped forks (2.534243ms)
✔ a cancelled checkpoint's delayed verdict cannot approve the replacement checkpoint (3.005501ms)
✔ each model call reanchors the canonical plan and role without losing compacted planning context or judgments (2.28053ms)
✔ routine assessments and steering display the actual advice rather than only a receipt (2.069783ms)
✔ current context above 100k compacts; cumulative usage and exactly 100k do not (1.465216ms)
✔ absent optional trackers count as zero tracked work; installed failed or busy trackers remain non-quiet (0.298529ms)
✔ a settled worker with no optional trackers sends one review, not repeated idle wakes (2.344359ms)
✔ disconnect cancels a checkpoint and blocks steering; local stop still clears ownership (1.802362ms)
✔ registered malformed subagent tracker stays unknown even when the process tracker is absent (0.320226ms)
✔ a busy supervisor defers the 100k compaction until its own run settles (1.549437ms)
✔ a healthy goal review can take longer than ten minutes without cancellation or another pairing (1.953315ms)
✔ busy plan supervisor refreshes cumulative VCC evidence without an idle feedback loop (5.894195ms)
✔ progress during a refresh remains pending with only one look in flight (4.562161ms)
✔ complete refresh uses the current worker branch after compaction (3.866041ms)
✔ complete refresh uses the current worker branch after rewind (2.442543ms)
✔ complete refresh uses the current worker branch after reload (3.280382ms)
✔ explicit checkpoints wait separately from routine coalescing and do not replace an active assessment (3.029511ms)
✔ actual supervisor provider failure is returned explicitly without another pair or an elapsed-time cancellation (3.241667ms)
✔ a provider error followed by native retry success does not cancel the supervisor checkpoint (5.838725ms)
✔ a failed full overview waits without spinning and recovers on routine progress (7.869651ms)
✔ a failed full overview waits without spinning and recovers on explicit look (3.50097ms)
✔ overview display metadata uses native context without requiring a remote roster lookup (4.49743ms)
✔ a superseded advance re-drives the current checkpoint after deferred compaction success (4.541734ms)
✔ a superseded advance re-drives the current checkpoint after deferred compaction rejection (3.364931ms)
✔ stopping current work during an obsolete advance prevents re-drive after compaction success (2.761416ms)
✔ stopping current work during an obsolete advance prevents re-drive after compaction rejection (9.510342ms)
✔ a child process named pi is found by ps, and stops being found when it exits (400.670699ms)
✔ the check is a snapshot, so it cannot hold up the worker's settle (325.855943ms)
✔ a one-line goal stays whole while a multi-line goal has a locator (7.139299ms)
✔ a view carries only the turns the supervisor has not been sent (2.42323ms)
✔ the last two reasoning blocks stay in the narrative, and older ones drop out (1.126327ms)
✔ a compaction restarts the view, so no turn falls into the gap (0.636179ms)
✔ pi-vcc reports the files the worker wrote, and separates them from the ones it read (1.636892ms)
✔ progressKey is unchanged when a review produced no new file or commit (0.641537ms)
✔ progressKey still sees a new file past pi-vcc's ten path display cap (0.746451ms)
✔ a commit counts as progress, even when no file was written since (0.993827ms)
✔ outstandingWork finds tool calls that never got a result (1.512703ms)
✔ buildView reports a tool call with no result, so done can be refused (0.974291ms)
✔ the view says how many reviews in a row changed nothing, and says nothing at zero (2.051843ms)
✔ the view merges the worker's compaction summary with the turns after it (0.46901ms)
✔ a turn the compaction summary already covers is not sent twice (0.347676ms)
✔ pi-vcc's sections and its transcript land on the right sides of the split (2.622939ms)
✔ the view does not tell the supervisor to use vcc_recall, a tool it does not have (0.426445ms)
✔ supervisor directives are not sent back as worker evidence (0.553966ms)
✔ bookkeeping tool calls are kept out of the transcript (0.416196ms)
✔ buildView reports the goal, status, and files without historical failures (0.446159ms)
✔ how long the worker has been quiet, measured from its own last entry (0.547843ms)
✔ buildView keeps the newest turns when it has to cut for the channel limit (42.609016ms)
✔ pi's own branch logic drops the abandoned fork, on a session file (2208.621704ms)
✔ a long goal cannot push the view past the broker limit (0.558345ms)
✔ a bounded complete overview explicitly labels a truncated worker compaction summary (0.355821ms)
tests 150
suites 0
pass 150
fail 0
cancelled 0
skipped 0
todo 0
duration_ms 20896.672059
@@ -0,0 +1,3 @@
> @wassname2/pi-goals@0.2.2 build
> tsc -p tsconfig.build.json
@@ -0,0 +1,5 @@
> @wassname2/pi-goals@0.2.2 lint
> biome check src/ test/
Checked 36 files in 92ms. No fixes applied.
@@ -0,0 +1,196 @@
> @wassname2/pi-goals@0.2.2 test
> vitest run && npm run test:supervisor
RUN v4.1.9 /home/ubuntu/.pi/agent/worktrees/pi-goals-simple-visible-supervision
Test Files 14 passed (14)
Tests 81 passed (81)
Start at 14:39:34
Duration 23.08s (transform 1.86s, setup 383ms, import 20.70s, tests 48.44s, environment 3ms)
> @wassname2/pi-goals@0.2.2 test:supervisor
> node --import tsx --test test/internal-supervisor/*.test.ts
✔ retries intercom registration when pi-intercom loads after pi-supervise (3.902174ms)
✔ a directive with no text is rejected, so the worker never sees undefined (0.328116ms)
✔ a directive from the paired supervisor becomes a real user message (21.426997ms)
✔ a directive to a busy worker interrupts, instead of waiting for the whole task (14.219762ms)
✔ a directive from an unpaired session is dropped (12.533882ms)
✔ a second pair takes over, and the first supervisor is told it lost the worker (27.9299ms)
✔ only the paired worker can end a run (7.03982ms)
✔ the programmatic pairing API waits for the worker acknowledgement (1.53026ms)
✔ the worker acknowledges a pair, so the supervisor knows it was heard (5.487175ms)
✔ a goal the supervisor inferred reaches the worker, which owns the view header (137.577579ms)
✔ the second view carries only what happened after the first (435.179753ms)
✔ a message addressed to a different session is ignored (10.696368ms)
✔ on settle the worker publishes a view built from the live branch (57.306992ms)
✔ the view is built from the live branch, not from every entry in the session (56.68447ms)
✔ an unpaired session publishes nothing on settle (0.581989ms)
✔ supervision never stops itself: no round limit at all (5.758318ms)
✔ goal, pairing and the steer count all survive a reload together (0.566147ms)
✔ a view arriving during unrelated supervisor thinking waits for a fresh complete overview (11.73855ms)
✔ the nudge repeats neither the instructions already sent nor the verdict rules (6.185005ms)
✔ a multi-line goal returns to supervisor context every fifth review and after compaction (31.516221ms)
✔ a one-line goal is not redundantly reinserted (28.008114ms)
✔ a check in and a worker that stopped ask for different things (11.439857ms)
✔ a loop still gets named after the supervisor compacts, from restored state (1.117781ms)
✔ a session that does not answer the roll call is not offered as a worker (501.3539ms)
✔ a child run stays out of the roll call, so it can never be picked (5.030483ms)
✔ a session already paired stays out of the roll call, and a free one answers (15.089392ms)
✔ /supervise look asks the worker for a fresh view, rather than the supervisor guessing (307.193495ms)
✔ let_it_run says the turn is over, so it is not called four times running (0.676302ms)
✔ a sign-off verdict is answered, not aborted, and a runaway is still cut (0.656312ms)
✔ every verdict result names the way to end the turn, steer included (0.473839ms)
✔ an old view is dropped from context once its verdict is in, and the verdict is kept (1.053468ms)
✔ a worker session never has its context rewritten (0.333986ms)
✔ a newly presented view starts a fresh look (5.745135ms)
✔ a tool a worker cannot use never aborts its turn (0.405935ms)
✔ a resume onto a session that is gone drops the pairing and says so (6.018145ms)
✔ a resume onto a live worker keeps supervising, and takes the writers back off (6.026155ms)
✔ state written before recentSteers existed still loads (0.179773ms)
✔ done unpairs the worker, so it stops publishing views (430.643048ms)
✔ with no goal the supervisor cannot steer, it must ask the human (0.730352ms)
✔ set_goal binds an inferred goal, and steering then works (501.012999ms)
✔ a goal given at pair time still allows steering (0.570176ms)
✔ done is refused while the worker has an unanswered tool call (12.627739ms)
✔ done is allowed once nothing is outstanding (6.242551ms)
✔ steer refuses when the session is not supervising (0.340749ms)
✔ a reworded repeat of an earlier instruction is sent, and named back to the supervisor (0.807133ms)
✔ overlap scores rewording high and a different instruction low (0.131635ms)
✔ the view of the old worker cannot be used to judge the new one (4.978392ms)
✔ with one other session here, /supervise needs no target and the whole line is the goal (500.193542ms)
✔ naming the worker still works, and the rest of the line is the goal (0.567943ms)
✔ with two free sessions here, /supervise asks which one, and pairs with the choice (501.080782ms)
✔ a goal that is a path is read from the file, so it is not pasted every run (1.947811ms)
✔ a long goal is one short line above the picker, and reaches the worker whole (501.036423ms)
✔ a session that stayed quiet is still on the list, because 0 free is a dead end (501.634751ms)
✔ a cancelled picker pairs with nothing (500.580837ms)
✔ supervising takes the writing tools away, and stopping gives them back (501.518809ms)
✔ stopping gives back the writers without undoing another extension's tools (500.660152ms)
✔ a first word that names no session is refused, rather than folded into the goal (0.57234ms)
✔ a goal with spaces needs no target, and @name takes the rest of the line as the goal (501.882273ms)
✔ the brief starts no turn, so there is no answer before the first view (6.132116ms)
✔ /supervise goal changes the goal without breaking the pairing (0.710704ms)
✔ the footer says which side of a pairing this session is, and clears when it ends (506.538204ms)
✔ a session that is not supervising never sees the supervisor tools (4.968927ms)
✔ worker_view refuses when there is no worker, rather than implying a pairing (0.621227ms)
✔ the view names the worker's model and how full its context is (56.12934ms)
✔ supervising a second session is refused while the first is still paired (0.813791ms)
✔ the supervisor gets a look at a working worker every half hour, without being asked (972.724402ms)
✔ a human message in the worker session is not a reason to stand back (5.144485ms)
✔ letting a stopped worker run says plainly that the worker stays stopped (10.435518ms)
✔ a stopped worker is looked at again, so let_it_run cannot silence the pairing (932.562879ms)
✔ a worker that pairs at the prompt and never takes a turn is still watched (604.177279ms)
✔ a worker that reloads at the prompt starts watching itself again (603.890441ms)
✔ a timer look at a worker that has not moved is not sent, until it has been skipped three times (2426.054837ms)
✔ the worker counts reviews in a row where nothing changed (587.040645ms)
✔ an unacknowledged pair gives up, and a takeover cancels that timer (4.99076ms)
✔ duplicate standalone Intercom registries are diagnosed and cannot bootstrap a plan (0.572811ms)
✔ retained non-plan supervision reconnects after the worker reloads, not on unrelated peer traffic (343.931651ms)
✔ retained non-plan supervision reconnects after the supervisor reloads, not on unrelated peer traffic (626.873765ms)
✔ busy supervisor retains its active view and requests one complete overview after settling (32.087166ms)
✔ each checkpoint freezes fresh worker evidence and direction without replacing a busy assessment (31.899373ms)
✔ checkpoint snapshot building cannot publish after abort (2.880596ms)
✔ checkpoint snapshot building cannot publish after stop (8.956519ms)
✔ checkpoint snapshot building cannot publish after reload (2.667688ms)
✔ checkpoint snapshot building cannot publish after plan change (2.877615ms)
✔ checkpoint capture includes user direction arriving while tracked work is queried (2.511844ms)
✔ checkpoint payload fits the serialized channel limit without truncating its identity (72.81385ms)
✔ settled empty final response fails the checkpoint explicitly ([]) (9.685596ms)
✔ settled empty final response fails the checkpoint explicitly (["text"]) (2.899287ms)
✔ settled empty final response fails the checkpoint explicitly (["text"]) (2.850772ms)
✔ settled empty final response fails the checkpoint explicitly (["thinking"]) (2.388046ms)
✔ failed assessment resumes on later worker progress without a human poke (stop) (2.318919ms)
✔ failed assessment resumes on later worker progress without a human poke (error) (1.930327ms)
✔ empty low-level response may continue through compaction, ask a human, or finish with a tool verdict (2.482665ms)
✔ a successful goal tool verdict is not undone by an empty final response (2.214831ms)
✔ a duplicate checkpoint rejection never echoes its snapshot or changes the active view (2.275401ms)
✔ an empty routine assessment cannot fail a separately queued checkpoint (2.55659ms)
✔ plan bootstrap compacts only the supervisor and pairing alone never starts a worker or a review (2.075322ms)
✔ goal decisions are correlated, preserve the pair across two goals, and cannot call overall done (3.23601ms)
✔ abort and stop cancel pending requests; late decisions cannot approve a replacement (2.16195ms)
✔ 50 actual model turns trigger one view, independent of the number of messages (2.424652ms)
✔ unknown background providers are not proof of quiescence (0.207091ms)
✔ stale plan content invalidates a pending goal review (2.674516ms)
✔ small forks skip compaction, but real compaction failure prevents pairing (2.267117ms)
✔ native small-history result permits startup after an attempted compaction (50000) (1.378746ms)
✔ native small-history result permits startup after an attempted compaction (null) (1.096433ms)
✔ the hour timer and a coincident turn checkpoint produce a single view (6.533516ms)
✔ settled checks wait for tracked processes and subagents to finish (3.623399ms)
✔ bootstrap stop cannot resurrect a supervisor after compaction completes (5.414626ms)
✔ a restarted worker reconnects by exact saved session identity without a new supervisor (2.799798ms)
✔ unknown initial context must compact instead of taking the known-small shortcut (1.042567ms)
✔ null post-compaction usage cannot raise the next configured 100k checkpoint (2.177116ms)
✔ stopping a routine view during compaction invalidates its suspended continuation (1.83495ms)
✔ restart of a provisional bootstrap resumes compaction and pairing in the same saved session (2.098918ms)
✔ command preserves a stopped supervisor across reload (1.88657ms)
✔ done preserves a stopped supervisor across reload (2.164052ms)
✔ same-binding replay retains activation when the supervisor lost its acknowledgement (2.445582ms)
✔ model-unavailable stop validates binding, cancels pending reviews and ignores old directives (1.882713ms)
✔ supervisor mode is a native inspection allowlist at visibility and execution, including reload and stopped forks (2.055621ms)
✔ a cancelled checkpoint's delayed verdict cannot approve the replacement checkpoint (2.097359ms)
✔ each model call reanchors the canonical plan and role without losing compacted planning context or judgments (1.652662ms)
✔ routine assessments and steering display the actual advice rather than only a receipt (1.941714ms)
✔ current context above 100k compacts; cumulative usage and exactly 100k do not (1.494223ms)
✔ absent optional trackers count as zero tracked work; installed failed or busy trackers remain non-quiet (0.277273ms)
✔ a settled worker with no optional trackers sends one review, not repeated idle wakes (1.891433ms)
✔ disconnect cancels a checkpoint and blocks steering; local stop still clears ownership (1.836482ms)
✔ registered malformed subagent tracker stays unknown even when the process tracker is absent (0.301608ms)
✔ a busy supervisor defers the 100k compaction until its own run settles (1.994581ms)
✔ a healthy goal review can take longer than ten minutes without cancellation or another pairing (1.921063ms)
✔ busy plan supervisor refreshes cumulative VCC evidence without an idle feedback loop (2.61882ms)
✔ progress during a refresh remains pending with only one look in flight (2.679516ms)
✔ complete refresh uses the current worker branch after compaction (2.362564ms)
✔ complete refresh uses the current worker branch after rewind (1.881952ms)
✔ complete refresh uses the current worker branch after reload (1.942139ms)
✔ explicit checkpoints wait separately from routine coalescing and do not replace an active assessment (2.066604ms)
✔ actual supervisor provider failure is returned explicitly without another pair or an elapsed-time cancellation (1.521288ms)
✔ a provider error followed by native retry success does not cancel the supervisor checkpoint (1.446067ms)
✔ a failed full overview waits without spinning and recovers on routine progress (3.125641ms)
✔ a failed full overview waits without spinning and recovers on explicit look (2.105449ms)
✔ overview display metadata uses native context without requiring a remote roster lookup (1.67603ms)
✔ a superseded advance re-drives the current checkpoint after deferred compaction success (2.494889ms)
✔ a superseded advance re-drives the current checkpoint after deferred compaction rejection (2.082903ms)
✔ stopping current work during an obsolete advance prevents re-drive after compaction success (1.93112ms)
✔ stopping current work during an obsolete advance prevents re-drive after compaction rejection (1.735847ms)
✔ goal_review accepts an optional bounded snapshot but rejects malformed snapshots (1.554983ms)
✔ checkpoint snapshot bounding counts JSON escapes and does not split Unicode characters (4.67607ms)
✔ a child process named pi is found by ps, and stops being found when it exits (400.386374ms)
✔ the check is a snapshot, so it cannot hold up the worker's settle (351.256976ms)
✔ latest user direction survives bounded summaries, compaction and later supervisor echoes (107.306515ms)
✔ oversized user direction is visibly bounded with a source reference (2.769796ms)
✔ a one-line goal stays whole while a multi-line goal has a locator (1.048328ms)
✔ a view carries only the turns the supervisor has not been sent (1.039401ms)
✔ the last two reasoning blocks stay in the narrative, and older ones drop out (0.805118ms)
✔ a compaction restarts the view, so no turn falls into the gap (0.380018ms)
✔ pi-vcc reports the files the worker wrote, and separates them from the ones it read (1.083438ms)
✔ progressKey is unchanged when a review produced no new file or commit (0.507079ms)
✔ progressKey still sees a new file past pi-vcc's ten path display cap (0.530678ms)
✔ a commit counts as progress, even when no file was written since (1.701632ms)
✔ outstandingWork finds tool calls that never got a result (1.014392ms)
✔ buildView reports a tool call with no result, so done can be refused (0.70119ms)
✔ the view says how many reviews in a row changed nothing, and says nothing at zero (0.40831ms)
✔ the view merges the worker's compaction summary with the turns after it (0.34633ms)
✔ a turn the compaction summary already covers is not sent twice (0.281564ms)
✔ pi-vcc's sections and its transcript land on the right sides of the split (1.499473ms)
✔ the view does not tell the supervisor to use vcc_recall, a tool it does not have (0.20563ms)
✔ supervisor directives are not sent back as worker evidence (0.354027ms)
✔ bookkeeping tool calls are kept out of the transcript (0.318495ms)
✔ buildView reports the goal, status, and files without historical failures (0.286487ms)
✔ how long the worker has been quiet, measured from its own last entry (0.341991ms)
✔ buildView keeps the newest turns when it has to cut for the channel limit (39.367034ms)
✔ pi's own branch logic drops the abandoned fork, on a session file (2082.572485ms)
✔ a long goal cannot push the view past the broker limit (0.59147ms)
✔ a bounded complete overview explicitly labels a truncated worker compaction summary (0.371831ms)
tests 171
suites 0
pass 171
fail 0
cancelled 0
skipped 0
todo 0
duration_ms 21133.203043
@@ -0,0 +1,3 @@
> @wassname2/pi-goals@0.2.2 typecheck
> tsc -p tsconfig.build.json --noEmit
@@ -0,0 +1,3 @@
Restored prior state: /home/ubuntu/.pi/agent/pi-goals/planning-model.json
Restored prior state: /home/ubuntu/.pi/agent/pi-goals/worker-model.json
Restored prior state: /home/ubuntu/.pi/agent/pi-goals/supervisor-model.json
@@ -0,0 +1,37 @@
does not end supervision. A
later worker view will
report the worker state.
[supervisor_brief]
Progress assessment: The
worker states: "Per the
execution gate in the plan,
I am now PAUSED and waiting
for your explicit
instruction before
implementing Goal 2 (CLI)."
This matches the required
user-controlled pause.
──────────────────────────────
$ printf
EXECUTION_SHOULD_BE_BLOCKED_
42
Supervisor mode is
inspection-only. Run
commands in the worker pane.
(exit 1)
──────────────────────────────
──────────────────────────────
──────────────────────────────
/tmp/pi-goals-herdr-uat.TGX...
↑84 ↓4.0k R534k W153k CH99....
👁 watching 1
@@ -0,0 +1,35 @@
supervisor message. If the
goal remains unmet, send a
concrete continuation
instruction. A human message
does not end supervision. A
later worker view will
report the worker state.
[supervisor_brief]
Progress assessment: The
worker states: "cli-demo.log
was hand-written (heredoc)
instead of captured from
actual CLI runs" and
"Waiting for explicit
operator resumption." The
outstanding operator
instruction was "Do not call
CompleteGoal or do further
work until the test operator
explicitly resumes you after
reloading the corrected
package."
Plan supervision stopped
──────────────────────────────
──────────────────────────────
/tmp/pi-goals-herdr-uat.TGX...
↑168 ↓8.0k R1.3M W442k CH99...
@@ -0,0 +1,355 @@
{
"startMs": 1788845259248,
"endMs": 1788849703205,
"worker": {
"role": "worker",
"sessionFile": "/home/ubuntu/.pi/agent/sessions/--tmp-pi-goals-herdr-uat.TGXaEL--/2026-09-08T05-19-26-254Z_01a07f74-f42d-7eee-85b0-10df065920a0.jsonl",
"sessionId": "01a07f74-f42d-7eee-85b0-10df065920a0",
"reportedAssistantModels": [
"github-copilot/claude-haiku-4-5-20251001",
"github-copilot/claude-haiku-4.5"
],
"totalRecordedUsage": {
"entries": 68,
"input": 427,
"output": 39611,
"cacheRead": 3479071,
"cacheWrite": 730435,
"totalTokens": 4249544,
"missingUsageEntries": [],
"missingFields": {},
"recordedPositiveCostSubtotal": 1.45943285,
"zeroOrMissingCostEntries": [
"6aeb938d"
]
},
"assistant": {
"entries": 68,
"input": 427,
"output": 39611,
"cacheRead": 3479071,
"cacheWrite": 730435,
"totalTokens": 4249544,
"missingUsageEntries": [],
"missingFields": {},
"recordedPositiveCostSubtotal": 1.45943285,
"zeroOrMissingCostEntries": [
"6aeb938d"
]
},
"compaction": {
"entries": 0,
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0,
"totalTokens": 0,
"missingUsageEntries": [],
"missingFields": {},
"recordedPositiveCostSubtotal": 0,
"zeroOrMissingCostEntries": []
},
"nestedTools": {
"entries": 0,
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0,
"totalTokens": 0,
"missingUsageEntries": [],
"missingFields": {},
"recordedPositiveCostSubtotal": 0,
"zeroOrMissingCostEntries": []
},
"compactions": [],
"entriesCounted": [
"6a6e04a7",
"b4a2f4f2",
"febe0d96",
"bf13d8c3",
"2b51e152",
"2adb0ad3",
"2dab558e",
"3d12be91",
"f8b5837f",
"341448f4",
"c2b896e6",
"e9d164a6",
"e93ba0f2",
"c38dc00e",
"934af358",
"837660f0",
"d3a4614c",
"843d9263",
"76712952",
"71541d12",
"140245f3",
"65409dac",
"d9245939",
"b1613a95",
"df541386",
"11f0c5ee",
"50ca611e",
"1c90f42a",
"bb8b64ac",
"7df8162f",
"4f48db60",
"8dfec4a3",
"7e694984",
"b708c0a6",
"fca206ff",
"cab1d0e9",
"9369d60e",
"4853512b",
"936ea424",
"9e5711e3",
"c6db50c8",
"42667d9f",
"760e0677",
"a56881eb",
"dfe7af6f",
"e4ad17b3",
"6c771286",
"de4ee95c",
"c67a026c",
"b385af99",
"6aeb938d",
"21facf63",
"0f468d8d",
"a22beee4",
"85e01443",
"ff92478f",
"bac91d19",
"e5dee53f",
"9e5d823a",
"21c9c61e",
"1e3eca90",
"4ac37cf2",
"b4d5514f",
"29b074e3",
"5d8f3026",
"beecb616",
"01d61a64",
"a5db8329"
],
"excludedUnknownTimestampEntries": [],
"completionToolResults": 8,
"cost": "unavailable/incomplete: zero or missing pricing is not proof of zero cost",
"compactionCoverage": "All post-boundary compaction/branch-summary usage is included when persisted. Missing entries are listed, not assumed free. Session records do not explicitly label initial versus later compaction; inspect the timestamp/marker sequence."
},
"supervisor": {
"role": "supervisor",
"sessionFile": "/home/ubuntu/.pi/agent/sessions/--tmp-pi-goals-herdr-uat.TGXaEL--/2026-09-08T05-27-39-268Z_01a07f7c-7a04-7015-bfca-8af0a2fc2089.jsonl",
"sessionId": "01a07f7c-7a04-7015-bfca-8af0a2fc2089",
"reportedAssistantModels": [
"github-copilot/gpt-6-astra"
],
"totalRecordedUsage": {
"entries": 42,
"input": 129,
"output": 5035,
"cacheRead": 1097576,
"cacheWrite": 422866,
"totalTokens": 1525606,
"missingUsageEntries": [],
"missingFields": {},
"recordedPositiveCostSubtotal": 0,
"zeroOrMissingCostEntries": [
"4eef3d84",
"e5bae9bb",
"63ff341f",
"efbc771f",
"139ffb13",
"c5e54d92",
"59b3a2c8",
"99860239",
"efddcca1",
"c7b02a2d",
"e389821d",
"fd1cd048",
"cc59835c",
"1189c2d8",
"3d700727",
"60e98242",
"f0ecb5f7",
"2591e9c8",
"14a00f13",
"2961318a",
"4c93819c",
"e02b8597",
"b5f9e2f5",
"7e91736c",
"06e83709",
"b4ecb5c3",
"3409e629",
"74ec901a",
"09f9fce1",
"b6482bac",
"1638dffa",
"cdb6286f",
"5b6914e0",
"43f6c1dc",
"a4f89f95",
"b4dfad7e",
"f506115f",
"91ac16f1",
"c3e8f1de",
"311303ce",
"55083f25",
"b989cb43"
]
},
"assistant": {
"entries": 41,
"input": 123,
"output": 3224,
"cacheRead": 1097576,
"cacheWrite": 409392,
"totalTokens": 1510315,
"missingUsageEntries": [],
"missingFields": {},
"recordedPositiveCostSubtotal": 0,
"zeroOrMissingCostEntries": [
"4eef3d84",
"e5bae9bb",
"63ff341f",
"efbc771f",
"139ffb13",
"c5e54d92",
"59b3a2c8",
"99860239",
"efddcca1",
"c7b02a2d",
"e389821d",
"fd1cd048",
"cc59835c",
"1189c2d8",
"3d700727",
"60e98242",
"f0ecb5f7",
"2591e9c8",
"14a00f13",
"2961318a",
"e02b8597",
"b5f9e2f5",
"7e91736c",
"06e83709",
"b4ecb5c3",
"3409e629",
"74ec901a",
"09f9fce1",
"b6482bac",
"1638dffa",
"cdb6286f",
"5b6914e0",
"43f6c1dc",
"a4f89f95",
"b4dfad7e",
"f506115f",
"91ac16f1",
"c3e8f1de",
"311303ce",
"55083f25",
"b989cb43"
]
},
"compaction": {
"entries": 1,
"input": 6,
"output": 1811,
"cacheRead": 0,
"cacheWrite": 13474,
"totalTokens": 15291,
"missingUsageEntries": [],
"missingFields": {},
"recordedPositiveCostSubtotal": 0,
"zeroOrMissingCostEntries": [
"4c93819c"
]
},
"nestedTools": {
"entries": 0,
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0,
"totalTokens": 0,
"missingUsageEntries": [],
"missingFields": {},
"recordedPositiveCostSubtotal": 0,
"zeroOrMissingCostEntries": []
},
"compactions": [
{
"entryId": "4c93819c",
"type": "compaction",
"timestamp": "2026-09-08T06:03:51.958Z",
"selectedModel": "github-copilot/gpt-6-astra",
"modelAttribution": "selected model inferred from preceding session entries; a custom summarizer may use another model",
"tokensBefore": 43242,
"hasUsage": true
}
],
"entriesCounted": [
"4eef3d84",
"e5bae9bb",
"63ff341f",
"efbc771f",
"139ffb13",
"c5e54d92",
"59b3a2c8",
"99860239",
"efddcca1",
"c7b02a2d",
"e389821d",
"fd1cd048",
"cc59835c",
"1189c2d8",
"3d700727",
"60e98242",
"f0ecb5f7",
"2591e9c8",
"14a00f13",
"2961318a",
"4c93819c",
"e02b8597",
"b5f9e2f5",
"7e91736c",
"06e83709",
"b4ecb5c3",
"3409e629",
"74ec901a",
"09f9fce1",
"b6482bac",
"1638dffa",
"cdb6286f",
"5b6914e0",
"43f6c1dc",
"a4f89f95",
"b4dfad7e",
"f506115f",
"91ac16f1",
"c3e8f1de",
"311303ce",
"55083f25",
"b989cb43"
],
"excludedUnknownTimestampEntries": [],
"completionToolResults": 0,
"cost": "unavailable/incomplete: zero or missing pricing is not proof of zero cost",
"compactionCoverage": "All post-boundary compaction/branch-summary usage is included when persisted. Missing entries are listed, not assumed free. Session records do not explicitly label initial versus later compaction; inspect the timestamp/marker sequence."
},
"currentContext": "These are cumulative recorded token metrics, NOT current context. The >100k compaction trigger uses ctx.getContextUsage().tokens. Native compaction tokensBefore is reported separately above.",
"freshEvidenceJudge": {
"status": "instrumentation gap",
"explanation": "Current CompleteGoal runs a fresh pi -p --no-session judge and saves its text receipt, not provider Usage. Completion tool result counts do not establish judge invocation counts or tokens. Report judge tokens/cost unavailable unless separately instrumented in an authorized trial; do not hide this overhead in worker/supervisor totals."
},
"limitations": [
"Wait for both trial sessions to settle/stop before finish so no partial JSONL/provider usage is mistaken for completed work.",
"Inherited pre-fork history is excluded using both the worker baseline ID set and the supervisor bootstrap marker.",
"No RPC message_update/message_end events are counted; duplicate persisted entry IDs are counted once.",
"The entries counters are persisted accounting-entry counts, not provider-call counts. Native split-turn compaction may combine multiple calls in one entry, as may aggregated nested-tool usage. Entries with missing usage are counted and listed separately.",
"Tool-result nested usage is separate. Do not add it again from another session without proving it is disjoint.",
"Long complete VCC refreshes are ordinary supervisor calls and therefore included in supervisor usage, not assumed free."
]
}
@@ -0,0 +1,221 @@
{
"startMs": 1788845259248,
"endMs": 1788846961538,
"worker": {
"role": "worker",
"sessionFile": "/home/ubuntu/.pi/agent/sessions/--tmp-pi-goals-herdr-uat.TGXaEL--/2026-09-08T05-19-26-254Z_01a07f74-f42d-7eee-85b0-10df065920a0.jsonl",
"sessionId": "01a07f74-f42d-7eee-85b0-10df065920a0",
"reportedAssistantModels": [
"github-copilot/claude-haiku-4-5-20251001"
],
"totalRecordedUsage": {
"entries": 31,
"input": 192,
"output": 19409,
"cacheRead": 1208000,
"cacheWrite": 141969,
"totalTokens": 1369570,
"missingUsageEntries": [],
"missingFields": {},
"recordedPositiveCostSubtotal": 0.39549825,
"zeroOrMissingCostEntries": []
},
"assistant": {
"entries": 31,
"input": 192,
"output": 19409,
"cacheRead": 1208000,
"cacheWrite": 141969,
"totalTokens": 1369570,
"missingUsageEntries": [],
"missingFields": {},
"recordedPositiveCostSubtotal": 0.39549825,
"zeroOrMissingCostEntries": []
},
"compaction": {
"entries": 0,
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0,
"totalTokens": 0,
"missingUsageEntries": [],
"missingFields": {},
"recordedPositiveCostSubtotal": 0,
"zeroOrMissingCostEntries": []
},
"nestedTools": {
"entries": 0,
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0,
"totalTokens": 0,
"missingUsageEntries": [],
"missingFields": {},
"recordedPositiveCostSubtotal": 0,
"zeroOrMissingCostEntries": []
},
"compactions": [],
"entriesCounted": [
"6a6e04a7",
"b4a2f4f2",
"febe0d96",
"bf13d8c3",
"2b51e152",
"2adb0ad3",
"2dab558e",
"3d12be91",
"f8b5837f",
"341448f4",
"c2b896e6",
"e9d164a6",
"e93ba0f2",
"c38dc00e",
"934af358",
"837660f0",
"d3a4614c",
"843d9263",
"76712952",
"71541d12",
"140245f3",
"65409dac",
"d9245939",
"b1613a95",
"df541386",
"11f0c5ee",
"50ca611e",
"1c90f42a",
"bb8b64ac",
"7df8162f",
"4f48db60"
],
"excludedUnknownTimestampEntries": [],
"completionToolResults": 3,
"cost": "recorded positive costs only; not independently verified pricing",
"compactionCoverage": "All post-boundary compaction/branch-summary usage is included when persisted. Missing entries are listed, not assumed free. Session records do not explicitly label initial versus later compaction; inspect the timestamp/marker sequence."
},
"supervisor": {
"role": "supervisor",
"sessionFile": "/home/ubuntu/.pi/agent/sessions/--tmp-pi-goals-herdr-uat.TGXaEL--/2026-09-08T05-27-39-268Z_01a07f7c-7a04-7015-bfca-8af0a2fc2089.jsonl",
"sessionId": "01a07f7c-7a04-7015-bfca-8af0a2fc2089",
"reportedAssistantModels": [
"github-copilot/gpt-6-astra"
],
"totalRecordedUsage": {
"entries": 15,
"input": 45,
"output": 1065,
"cacheRead": 353752,
"cacheWrite": 133546,
"totalTokens": 488408,
"missingUsageEntries": [],
"missingFields": {},
"recordedPositiveCostSubtotal": 0,
"zeroOrMissingCostEntries": [
"4eef3d84",
"e5bae9bb",
"63ff341f",
"efbc771f",
"139ffb13",
"c5e54d92",
"59b3a2c8",
"99860239",
"efddcca1",
"c7b02a2d",
"e389821d",
"fd1cd048",
"cc59835c",
"1189c2d8",
"3d700727"
]
},
"assistant": {
"entries": 15,
"input": 45,
"output": 1065,
"cacheRead": 353752,
"cacheWrite": 133546,
"totalTokens": 488408,
"missingUsageEntries": [],
"missingFields": {},
"recordedPositiveCostSubtotal": 0,
"zeroOrMissingCostEntries": [
"4eef3d84",
"e5bae9bb",
"63ff341f",
"efbc771f",
"139ffb13",
"c5e54d92",
"59b3a2c8",
"99860239",
"efddcca1",
"c7b02a2d",
"e389821d",
"fd1cd048",
"cc59835c",
"1189c2d8",
"3d700727"
]
},
"compaction": {
"entries": 0,
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0,
"totalTokens": 0,
"missingUsageEntries": [],
"missingFields": {},
"recordedPositiveCostSubtotal": 0,
"zeroOrMissingCostEntries": []
},
"nestedTools": {
"entries": 0,
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0,
"totalTokens": 0,
"missingUsageEntries": [],
"missingFields": {},
"recordedPositiveCostSubtotal": 0,
"zeroOrMissingCostEntries": []
},
"compactions": [],
"entriesCounted": [
"4eef3d84",
"e5bae9bb",
"63ff341f",
"efbc771f",
"139ffb13",
"c5e54d92",
"59b3a2c8",
"99860239",
"efddcca1",
"c7b02a2d",
"e389821d",
"fd1cd048",
"cc59835c",
"1189c2d8",
"3d700727"
],
"excludedUnknownTimestampEntries": [],
"completionToolResults": 0,
"cost": "unavailable/incomplete: zero or missing pricing is not proof of zero cost",
"compactionCoverage": "All post-boundary compaction/branch-summary usage is included when persisted. Missing entries are listed, not assumed free. Session records do not explicitly label initial versus later compaction; inspect the timestamp/marker sequence."
},
"currentContext": "These are cumulative recorded token metrics, NOT current context. The >100k compaction trigger uses ctx.getContextUsage().tokens. Native compaction tokensBefore is reported separately above.",
"freshEvidenceJudge": {
"status": "instrumentation gap",
"explanation": "Current CompleteGoal runs a fresh pi -p --no-session judge and saves its text receipt, not provider Usage. Completion tool result counts do not establish judge invocation counts or tokens. Report judge tokens/cost unavailable unless separately instrumented in an authorized trial; do not hide this overhead in worker/supervisor totals."
},
"limitations": [
"Wait for both trial sessions to settle/stop before finish so no partial JSONL/provider usage is mistaken for completed work.",
"Inherited pre-fork history is excluded using both the worker baseline ID set and the supervisor bootstrap marker.",
"No RPC message_update/message_end events are counted; duplicate persisted entry IDs are counted once.",
"The entries counters are persisted accounting-entry counts, not provider-call counts. Native split-turn compaction may combine multiple calls in one entry, as may aggregated nested-tool usage. Entries with missing usage are counted and listed separately.",
"Tool-result nested usage is separate. Do not add it again from another session without proving it is disjoint.",
"Long complete VCC refreshes are ordinary supervisor calls and therefore included in supervisor usage, not assumed free."
]
}
@@ -0,0 +1,10 @@
✔ failed assessment resumes on later worker progress without a human poke (stop) (22.112625ms)
✔ failed assessment resumes on later worker progress without a human poke (error) (3.617584ms)
tests 2
suites 0
pass 2
fail 0
cancelled 0
skipped 0
todo 0
duration_ms 1889.732341
@@ -0,0 +1,46 @@
✖ failed assessment resumes on later worker progress without a human poke (stop) (29.280507ms)
✖ failed assessment resumes on later worker progress without a human poke (error) (5.669199ms)
tests 2
suites 0
pass 0
fail 2
cancelled 0
skipped 0
todo 0
duration_ms 1895.833929
✖ failing tests:
test at test/internal-supervisor/plan.test.ts:1:12636
✖ failed assessment resumes on later worker progress without a human poke (stop) (29.280507ms)
AssertionError [ERR_ASSERTION]: ordinary worker progress must resume supervision without user input
1 !== 2
at TestContext.<anonymous> (/home/ubuntu/.pi/agent/worktrees/pi-goals-simple-visible-supervision/test/internal-supervisor/plan.test.ts:200:12)
at async Test.run (node:internal/test_runner/test:1208:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:385:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 1,
expected: 2,
operator: 'strictEqual',
diff: 'simple'
}
test at test/internal-supervisor/plan.test.ts:1:12636
✖ failed assessment resumes on later worker progress without a human poke (error) (5.669199ms)
AssertionError [ERR_ASSERTION]: ordinary worker progress must resume supervision without user input
1 !== 2
at TestContext.<anonymous> (/home/ubuntu/.pi/agent/worktrees/pi-goals-simple-visible-supervision/test/internal-supervisor/plan.test.ts:200:12)
at async Test.run (node:internal/test_runner/test:1208:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:831:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 1,
expected: 2,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,37 @@
hand-written (heredoc)
instead of captured from
actual CLI runs
2. Temporary files were
written outside the
fixture
3. Need to capture real
child-process demos from
within the fixture only
Files preserved:
- converter.mjs ✓
- converter.test.mjs ✓
- cli.mjs ✓
- cli.test.mjs ✓
- evidence/function-tests.lo
g ✓
- evidence/cli-tests.log ✓
- evidence/cli-demo.log
(needs regeneration from
real runs)
Waiting for explicit
operator resumption.
Persistent plan steward
disabled.
▸ Provide a CLI with
demonstrated valid and
invalid inputs.
──────────────────────────────
──────────────────────────────
/tmp/pi-goals-herdr-uat.TGX...
↑466 ↓43k R3.7M W749k CH99....
◷ 1/2 goals
@@ -0,0 +1,239 @@
# Simple, visible supervision
Approved for implementation by the user, superseding the earlier draft and its open questions. Work is based on tested checkpoint `d5729ac`, in the isolated `feature/simple-visible-supervision` worktree. User intent is recorded in [AGENTS.md](../../../AGENTS.md). Approval is not a claim that live acceptance has passed.
## User-visible result
One pi-goals package runs the same Pi profile/package set in a worker pane and a real visible supervisor pane. The supervisor retains planning context, repeatedly sees its role and the canonical plan, and gives brief useful assessments and advice. Reliability and fewer layers take priority over new orchestration.
## User voice
- > yeah use intercom not RPC
- > in the other one we 1) fork the main proc 2) compact. then the supervisor has a) a repeated loop stating it is the supervisor and the plan, and it also has the compacted session. we compact at >100k to keep it cheap
- > we should just be able to run the same pi profile but activate supervisor mode
- > yes but also it needs to be simple it's constantly breaking and the supervisor needs to actually offer advice and perspective not just fill forms
- All worker-Pi / supervisor-Pi communication must use pi-intercom. Direct controller calls are only ordinary module composition inside one process.
## Approved goals and current evidence
1. [/] goal: Start and recover supervision using one Pi profile
- scope: Ready forks the planning session, activates supervisor mode in a real Herdr pane, and compacts the fork. Reuse the same agent directory and resource choices. One Intercom registration per process, including profiles with Intercom already installed. Remove the generic plan-lifecycle RPC dispatcher; lifecycle stays directly in pi-goals.
- implemented: Removed `plan-api.ts`, `pi-supervise:plan:v1`, its request envelope and generic dispatcher. pi-goals owns a typed local controller; peer messages remain on Intercom. Herdr launch passes the current Pi agent directory, preserves explicit resource choices, and adds no companion extension. Recovery reuses the recorded session/pane rather than spawning a duplicate because a location is unavailable.
- evidence: `test/supervisor-integration.test.ts` checks native session-fork capture, Ready/model ordering, cancellation and two goals with the fresh judge. Real installed-Pi packed-artifact tests in `test/intercom-registration.test.ts` passed bundled-only, Intercom-before-goals and Intercom-after-goals, including native reload: one channel registration, one Intercom tool, unchanged tool sets.
- live evidence: The user disabled the host sandbox. The complete packed peer flow now passes. Ready created one real supervisor fork; reloading that same pane recovered the initial native-compaction no-op failure without another fork. Both panes subsequently reloaded and retained their relationship. Detailed observations and limitations are in the live-UAT log below.
2. [/] goal: Give visible, context-aware advice in the retained supervisor loop
- scope: Repeatedly supply the supervisor role and current canonical plan, preserve compacted planning context and incremental VCC views/judgments, compact again above 100k current-context tokens subject to the model limit. Show the actual assessment/advice. Keep the fresh evidence judge; plan-bound asynchronous checkpoint identity/freshness is code-owned, not a model-filled form.
- implemented: Each model call receives the role/current plan alongside retained context. Routine prompts request useful visible assessment/perspective; steering and no-intervention assessments display their actual content as durable messages. `review_goal` takes only decision/reason; its verdict must match the checkpoint actually presented to that model call. Transport messages carry plan binding; disconnection invalidates pending checkpoints and is shown rather than treated as approval/delivery.
- evidence: Supervisor regressions verify retained compaction context/judgments, canonical-plan refresh, visible actual advice, delayed verdict rejection after checkpoint replacement, changed-plan rejection, two-goal persistence, and >100k current context versus cumulative usage/exactly 100k, including compaction deferred until a busy supervisor settles. Existing small-fork behavior is retained: known context at most 20k skips unnecessary initial compaction; unknown usage does not skip it.
- cadence: One worker-owned cadence: 50 completed model turns OR 60 minutes OR genuinely settled with no tracked work. After explicit supervisor approval of a discovered baseline gap, absent optional tracking tools count as zero tracked work; installed trackers with failed/malformed status remain unknown. Tests cover absent, busy, malformed/mixed providers and coalesced settled reviews. Detached/unregistered work is still explicitly untracked.
- live evidence: The supervisor caught missing finite-result protection, an ineffective overflow test, and missing CLI overflow evidence. The worker corrected the function/tests but later acknowledged hand-written demo output. The stale checkpoint view, empty-response stall and false human-wait latch are fixed and reviewed with regressions; the final fixes have not been retried in the live pair. Final role usage is recorded below. This debugging trial has no worker-alone control and does not establish savings.
3. [/] goal: Keep supervisor mode inspection-only, including recovery
- scope: Block direct writes/bash and indirect process/subagent/scheduler/unknown mutation routes at both visibility and execution. Allow native inspection and narrowly owned supervisor steering/state only. Restore restrictions before model work and retain them in stopped plan forks.
- implemented: Replaced writer-name denylist with native read/grep/find/ls plus application-owned supervisor tools. Enforce `tool_call` and `user_bash` gates; restored supervisor state and bootstrap markers reapply restrictions on reload. No arbitrary filesystem-note writer was added.
- evidence: Tests cover hidden tools, re-enabled forbidden tools still blocked at execution, process/subagent/scheduler/Intercom/unknown routes, native read permitted, user bash rejected, and reload/stopped-fork restrictions. Registered tool provenance prevents a custom tool merely named `read` being treated as native inspection.
- live evidence: The supervisor's native `!printf EXECUTION_SHOULD_BE_BLOCKED_42` probe was rejected after reload: “Supervisor mode is inspection-only. Run commands in the worker pane.” A stopped-fork live check remains pending. This is a model-tool restriction, not an OS sandbox for malicious trusted extension code or a human deliberately invoking another extension's slash commands.
4. [ ] goal: Validate the installed artifact and real visible Herdr workflow honestly
- scope: Full tests, typecheck, lint, build, packed artifact checks and bounded real Herdr UAT: fork/compaction, visible assessment/steering, reload/recovery, two goals, stop, no duplicate tools. Use only dedicated test fixtures/panes; no active-profile replacement. RPC is a deterministic test harness only.
- evidence: Full `npm test` ran: 71 Vitest tests passed, one packed Intercom peer-flow test failed at broker connection. Separate `npm run test:supervisor` passed 128 tests. `npm run test:rpc`, typecheck, lint, build, diff checks passed. The explicit diagnostic run excluding the environment-blocked peer-flow file passed 71 tests; this does not make full `npm test` green. Packed content checks passed with Intercom/VCC present, removed `plan-api.ts` absent, core peers excluded, and one manifest extension entry. Real installed-Pi registration/reload tests passed without requiring a model call.
- blocker: `HERDR_ENV=1`, but `herdr workspace list` returned OS `PermissionDenied: Operation not permitted`. A fresh `/tmp` Unix-socket listen probe returned `listen EPERM`. Parent confirmed the same host-control restriction. Herdr control stopped before creating panes/workspaces; no auth/host workaround was attempted.
- current status: The host blocker above is historical and resolved for the parent. Final full validation passed 81 Vitest and 171 supervisor tests, plus typecheck/lint/build; targeted independent review approved the final fixes. The bounded Herdr trial is stopped and all three temporary role preferences restored. Goal one was accepted under the existing inconclusive-judge policy, not conclusively verified; goal two remains unsigned. The user requested commit/push of the latest changes with these live-UAT gaps retained.
## Preserved policies
- The fresh read-only evidence judge remains mandatory in the existing CompleteGoal path; existing inconclusive-judge semantics are unchanged.
- Sticky planning/worker/supervisor models and explicit auth recovery; three-question planning; ordinary-chat Discuss; Ready as sole human start approval; no-dash commands; SUPERVISOR.md precedence; plan/history retention.
- One goal's approval does not end supervision. Existing overall `done` / explicit stop behavior remains; stop leaves the pane and history available. A disconnected stop reports local success and unconfirmed peer delivery instead of claiming the remote session stopped.
- No new mailbox, orchestration framework, dependency upgrade, release or active-profile replacement. The user subsequently authorized commit/push after live testing. Only the three non-secret role preferences may be temporarily changed for the trial, with guarded restoration; no unrelated global settings edits.
## Log
### 2026-09-08 — Implementation and permitted validation
Exact worktree: `/home/ubuntu/.pi/agent/worktrees/pi-goals-simple-visible-supervision`.
Exact logs and packed artifact: `/tmp/pi-goals-simple-visible-evidence/`. Durable handoff copies are under the implementation output's sibling `validation/` directory. Primary logs:
- `final-command-status.log`
- `final-npm-test.log` — full suite, **not passed**
- `final-npm-run-test-supervisor.log` — 128 passed
- `final-npm-run-test-rpc.log` — deterministic conversational review passed
- `final-permitted-vitest.log` — 71 passed, explicit diagnostic exclusion only
- `final-npm-run-typecheck.log`, `final-npm-run-lint.log`, `final-npm-run-build.log`
- `registration-reload.log` — real installed-Pi packed registration and reload in three load orders
- `npm-pack.json`, `packed-artifact-check.log`
- `herdr-access.log`, `unix-socket-probe.log` — exact environment blockers
Read the installed Pi extension, session, session-format, compaction, package, TUI, environment and CLI-usage documentation, plus the relevant event-bus/compaction examples, public session-manager declarations and Intercom extension-channel contract. The implementation uses public extension/session APIs, not private runtime state.
The original plan's sign-off question is resolved: preserve the fresh judge. No authenticated model pair or spending was selected here because host UAT was blocked before that stage. Actual costs and token benefit remain unavailable, not zero or inferred from protocol tests.
### 2026-09-08 — Accepted review fixes and slow-supervisor robustness follow-up
The parent accepted two concrete review findings: retained non-plan peer reload recovery and missing
Pi `-ne` / `-ns` / `-np` aliases. Both now have saved red/green regressions. Recovery requires validated,
addressed traffic from the currently paired peer; arbitrary joins, wrong recipients and unrelated plan
bindings do not restore connectivity. Short/long resource options now produce equivalent production
launch arguments.
The user then explicitly required slow-supervisor fixes, not characterization-only gaps. The approved
minimal direction uses one active assessment, a pending dirty marker and one existing Intercom look/view
refresh in flight, without cursor acknowledgements or a new queue runtime. Busy-time incremental updates
do not replace the active view or enqueue model turns. After settling, the worker rebuilds a bounded VCC
overview from its latest compaction summary and current branch. Progress arriving during that refresh
remains pending. Explicit checkpoints and the canonical plan are separate from replaceable routine
status. Overview truncation is visible and includes a read-only source-session reference.
The arbitrary ten-minute supervisor model-review deadline has been removed. Finite transport startup
and attachment waits remain separate. Healthy long reviews can succeed; explicit cancellation, changed
plans, disconnects and genuine settled provider failures still fail safely without a replacement pair
or fork. A native automatic retry is not mistaken for a final provider failure. Routine review is paused
when waiting for a user decision, and stale active views cannot end supervision while newer work awaits
an overview.
Latest validation: `npm test` ran with **73 passed, one environment-blocked Intercom peer-flow failure**;
`npm run test:supervisor` passed **141 tests**. Typecheck, lint, build and deterministic RPC review passed.
The explicit permitted Vitest subset passed 73 tests. An earlier full run also hit the unchanged role-model
subprocess test's five-second timeout; its log is retained, and both the subsequent full run and isolated
role-model test passed without relaxing that timeout.
Exact follow-up logs, red/green evidence, final packed artifact and the lightweight usage extractor are
under the implementation output's `validation/review-fixes/` directory (latest suite logs in `final/`).
The extractor is `extract-trial-usage.mjs`: capture the worker boundary before Ready, then read both real
trial session files after settling. It separates input/output/cache-read/cache-write, excludes inherited
planning history and duplicate entries, includes recorded compaction usage and labels missing compaction
or fresh-judge usage as instrumentation gaps. Its validation used synthetic fixtures only; these are not
live token measurements. Current-context compaction thresholds are separate from cumulative recorded token totals.
Herdr control and Unix sockets still return PermissionDenied/EPERM. No actual Herdr trial, live per-role
usage, cost or savings evidence is claimed; authorized host UAT and reviewer recheck remain required.
### 2026-09-08 — Accepted liveness review corrections
The next read-only review identified two concrete refresh/compaction liveness races and a usage-report
label error. Scope stayed limited to those accepted findings. A worker now retains a full-overview
request and its evidence cursor until publication succeeds; failure does not recursively retry from
`finally`. Routine progress/cadence or explicit `/supervise look` can retry. View model/context display
uses native local context rather than a mandatory remote roster lookup. Existing addressed-peer,
plan-binding and stopped checks remain, with generation checks on suspended publication.
An obsolete advance awaiting compaction now releases its single-advance guard and re-drives only
actual current-generation pending work. Deferred old compaction success or rejection cannot strand
a new checkpoint after valid reconnect, fail that new checkpoint as obsolete, or restart stopped work.
No model timeout, queue runtime or additional pair/fork was added.
Saved red/green checks cover publication failure with routine and explicit recovery, nonfatal roster
unavailability, and deferred compaction success/rejection. Two further stop regressions cover both
completion orderings. Latest supervisor suite: **148/148 passed**. Full `npm test`: **73 passed,
one failed** at packed Intercom broker startup; the complete peer flow remains unpassed. Typecheck,
lint, build, deterministic RPC review, the explicit 73-test permitted subset, packed content checks and
all three real-Pi registration/reload load orders passed. Herdr still returns PermissionDenied, and a
fresh Unix socket probe returns EPERM. No live pane or authenticated usage proof is claimed.
The artifact-only usage extractor now labels counters `entries`, not provider calls: one persisted
compaction or nested-tool entry can aggregate multiple calls. Token components, missing usage/pricing
and judge instrumentation caveats remain separate. Its corrected assertions passed synthetic fixtures
only. Exact new logs, pre-edit snapshots, packed artifact and follow-up-only diff are in the
implementation output's `validation/review-fixes/liveness/` directory. Earlier logs remain historical;
independent reviewer recheck and authorized host acceptance are still pending.
### 2026-09-08 — Actual Herdr trial, in progress
The user disabled the sandbox and requested live testing before the authorized commit/push. Parent
host access and the full packed Intercom flow now work. The revived child retained its old sandbox;
no bypass was attempted. The parent operates the trial directly.
Fixture: `/tmp/pi-goals-herdr-uat.TGXaEL`. Worker `goals-uat-worker` (`w1:pE`) and Ready-created
supervisor `supervisor-9a775b1a` (`w1:pF`) use the extracted package and the normal agent directory,
with bundled-only extension loading. This is not a live test of every globally installed extension
combination. Planning/supervisor use `github-copilot/gpt-6-astra`; the worker requests
`github-copilot/claude-haiku-4.5` and reports `claude-haiku-4-5-20251001` in usage records.
Observed so far:
- Three alignment questions, ordinary-chat Discuss, final Ready, and the native fork were exercised.
- Initial compaction returned `Nothing to compact (session too small)` despite measured context above
20k. The exact initial no-op now retains the fork; genuine errors still fail. Reloading the same
supervisor completed startup. Later both panes reloaded without another fork.
- Supervisor advice caused finite-result protection and an effective overflow regression to be added.
It respected the required pause before the CLI goal. Native user bash was blocked after reload.
- Premature worker checkboxes and valid numbered judge checks exposed two completion defects. The
submitted goal is now reopened before review, and the checks parser accepts numbered/indented lists
only inside its checks section. These fixes passed independent review and red/green regressions.
- The [pre-checkpoint-fix full test log](../../reviews/evidence/2026-09-08-visible-supervision/pre-checkpoint-fix-tests.log)
records `Tests 81 passed (81)` and ` pass 150`; typecheck, lint and build also passed.
- Goal one's latest [actual receipt](../../reviews/evidence/2026-09-08-visible-supervision/completion-receipts-observed.json)
says `Judge returned no VERDICT line. Accepted inconclusive — logged.` It is not conclusive verification.
- Manual supervisor compaction persisted a [native record](../../reviews/evidence/2026-09-08-visible-supervision/observed-supervisor-actions.json)
with `tokensBefore: 43242` and 15,291 recorded tokens. The [Herdr observer](../../reviews/evidence/2026-09-08-visible-supervision/manual-compaction-result.json)
nevertheless returned `agent_prompt_stalled`. The native record, not that observer status, proves
compaction occurred. Its summary covers the older prefix; recent work remains in the retained suffix.
The automatic >100k threshold has unit coverage, not a real threshold-crossing trial.
- After explicit operator authorization, the worker built the CLI. The supervisor caught a missing
CLI overflow test/demo, which the worker added. However, its cached view still showed the old pause.
A subsequent empty final response left the completion request waiting. The operator cancelled the
request and paused the worker. A focused writer/reviewer round is correcting checkpoint freshness
and genuinely settled empty-response handling; no thinking deadline or new queue is being added.
[Interim usage](../../reviews/evidence/2026-09-08-visible-supervision/trial-usage-interim.json), captured before
manual compaction and the CLI work, records worker 1,369,570 tokens (19,409 output; 31 entries) and
supervisor 488,408 tokens (1,065 output; 15 entries). Inherited planning is excluded. Cache components
are separate in the artifact. Entries are not provider calls. Judge usage is unrecorded; supervisor
zero/missing pricing does not mean free use. This includes debugging, retries and reloads, has no
worker-alone control, and supports no savings claim. Final usage, stop checks, guarded role-preference
restoration, final validation and publication remain pending.
Recorded by Pi (OpenAI) from the observed trial artifacts; the section above records the trial before its publication checkpoint.
### 2026-09-08 — Publication checkpoint requested by the user
The user requested “commit and push latest”, then clarified “supervisor is waiting? it's job is not wait”.
The final correction removes the failure-to-human-wait latch: empty/incomplete assessments and provider
failures do not block later ordinary worker progress/cadence. Stale refresh flags are cleared to avoid
an immediate same-input retry loop. Explicit human dependencies remain separate. The two regression
cases failed [red](../../reviews/evidence/2026-09-08-visible-supervision/wait-latch-red.log) with
`ordinary worker progress must resume supervision without user input`, then passed
[green](../../reviews/evidence/2026-09-08-visible-supervision/wait-latch-green.log).
Fresh snapshots now travel on the existing Intercom checkpoint message, with the latest user direction
retained within the bounded overview. The snapshot is promoted only when its checkpoint becomes active;
replies carry identity/verdict, not a copy of the snapshot. Cancellation, changed plans, generation/session
identity and serialized 16 KiB limits are checked. The historical replay is not a live acceptance test.
The [independent targeted review](../../reviews/2026-09-08_checkpoint-review.md) reports “No issues found.”
Final parent-run evidence (committed log copies normalize trailing whitespace only; originals remain in the fixture):
- [npm test](../../reviews/evidence/2026-09-08-visible-supervision/publish-npm-test.log):
`Tests 81 passed (81)`, ` tests 171`, ` pass 171`, ` fail 0`.
- [Typecheck](../../reviews/evidence/2026-09-08-visible-supervision/publish-typecheck.log),
[lint](../../reviews/evidence/2026-09-08-visible-supervision/publish-lint.log) and
[build](../../reviews/evidence/2026-09-08-visible-supervision/publish-build.log) all exited successfully;
`git diff --check` passed.
- The [worker](../../reviews/evidence/2026-09-08-visible-supervision/worker-stopped.txt) shows
`Persistent plan steward disabled.` and 1/2 goals. The
[supervisor](../../reviews/evidence/2026-09-08-visible-supervision/supervisor-stopped.txt) shows
`Plan supervision stopped`. Auto-continue was disabled too. Panes/history are retained.
- [Guarded restoration](../../reviews/evidence/2026-09-08-visible-supervision/role-preferences-restored.log)
reports `Restored prior state` for all three role preference files. No active-installation replacement.
The worker's final acknowledgement says the CLI demo log was hand-written rather than captured from
actual runs, and reports temporary files outside the fixture. Those are trial failures, not accepted
evidence. Goal two remains unsigned; its demonstrations still need actual execution. The latest
checkpoint/latch fixes have not been reloaded and retried in the live pair. The stopped-fork execution
gate and automatic >100k threshold retain unit coverage, not a new live test here.
[Final recorded role usage](../../reviews/evidence/2026-09-08-visible-supervision/trial-usage-final.json),
after explicit stop, excludes inherited planning:
- Worker: 4,249,544 tokens across 68 entries: input 427, output 39,611, cache read 3,479,071,
cache write 730,435.
- Supervisor: 1,525,606 tokens across 42 entries: input 129, output 5,035, cache read 1,097,576,
cache write 422,866. This includes the one 15,291-token native compaction record.
The [extractor](../../reviews/evidence/2026-09-08-visible-supervision/extract-trial-usage.mjs) separates
assistant, compaction and nested usage; entries are not calls. Original session files remain local,
not committed. Fresh-judge usage and parent/development-agent overhead are not included. Supervisor
pricing is unrecorded/zero, and the worker's positive cost subtotal is incomplete. There is no reliable
total cost or savings claim. This is a debugging trial with retries/reloads and an unfinished second goal,
not a clean efficiency benchmark.
Recorded by Pi (OpenAI); publication does not signify complete live acceptance.
+1 -2
View File
@@ -54,8 +54,7 @@
},
"pi": {
"extensions": [
"./src/index.ts",
"./node_modules/pi-intercom/index.ts"
"./src/index.ts"
],
"image": "https://cdn.jsdelivr.net/gh/wassname/pi-goals@main/media/screenshot.png",
"skills": [
+30 -16
View File
@@ -57,7 +57,7 @@ import {
waivesAlignment,
} from "./prompts.js";
import { RoleModels } from "./role-models.js";
import { focusSupervisor, initializeSupervisor, planHash, type SupervisorBinding, type SupervisorDecision, startSupervisor, supervisorBootstrap, supervisorRequest } from "./supervisor.js";
import { focusSupervisor, initializeSupervisor, planHash, type SupervisorBinding, startSupervisor, supervisorBootstrap } from "./supervisor.js";
const STATE = "pi-goals-state";
const STATUS_KEY = "pi-goals";
@@ -202,7 +202,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
}
function workMessage(ctx: ExtensionContext): string {
return `Work the goals in ${planPath(ctx)}. Pick an open goal, mark it active ([/]), work its subtasks, and when its discriminator is satisfied fill its evidence: list, then call CompleteGoal with the goal's text. Keep the plan file current as you go.`;
return `Work the goals in ${planPath(ctx)}. Pick an open goal, mark it active ([/]), work its subtasks, and when its discriminator is satisfied fill its evidence: list, then call CompleteGoal with the goal's text. Do not mark a goal [x] before CompleteGoal accepts it. Keep the plan file current as you go.`;
}
function clearAutoTimer(): void {
@@ -301,7 +301,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
operation?.abort();
operation = null;
if (state.supervisor) {
try { await supervisorRequest(pi, "stop", { bindingId: state.supervisor.id }); }
try { await supervisor.stop(state.supervisor.id); }
catch (error) { ctx.ui.notify(`Could not reach the supervisor to stop it: ${String(error)}. Check its pane.`, "warning"); }
}
state = { ...state, supervisor: null };
@@ -325,12 +325,12 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
if (planHash(readPlan(ctx)) !== approvedDraft) throw new Error("The plan changed during model recovery; select Ready again");
if (!workerReady) { state = { ...state, phase: "planning" }; persist(); updateWidget(ctx); return; }
}
const binding = await startSupervisor(pi, ctx, planPath(ctx), state.supervisor, supervisor => {
const binding = await startSupervisor(pi, supervisor, ctx, planPath(ctx), state.supervisor, supervisor => {
if (signal.aborted) return;
state = { ...state, supervisor }; persist();
}, signal);
if (signal.aborted || state.planVersion !== version || !state.stewardEnabled) return;
if (planHash(readPlan(ctx)) !== approvedDraft) { await supervisorRequest(pi, "stop", { bindingId: binding.id }); state = { ...state, supervisor: null }; throw new Error("The plan changed during initialization; select Ready again"); }
if (planHash(readPlan(ctx)) !== approvedDraft) { await supervisor.stop(binding.id); state = { ...state, supervisor: null }; throw new Error("The plan changed during initialization; select Ready again"); }
state = { ...state, supervisor: binding };
persist(); updateWidget(ctx);
state = { ...state, modelRecovery: "worker" }; persist();
@@ -341,11 +341,11 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
return; // Keep the attached pairing inactive and the preference target on worker.
}
if (signal.aborted || state.planVersion !== version || state.supervisor?.id !== binding.id || !state.stewardEnabled) return;
if (planHash(readPlan(ctx)) !== approvedDraft) { await supervisorRequest(pi, "stop", { bindingId: binding.id }); state = { ...state, supervisor: null }; throw new Error("The plan changed during model restoration; select Ready again"); }
if (planHash(readPlan(ctx)) !== approvedDraft) { await supervisor.stop(binding.id); state = { ...state, supervisor: null }; throw new Error("The plan changed during model restoration; select Ready again"); }
state = { ...state, modelRecovery: null }; persist();
await supervisorRequest(pi, "activate", { bindingId: binding.id }, signal);
await supervisor.activate(binding.id, signal);
if (signal.aborted || state.planVersion !== version || state.supervisor?.id !== binding.id || !state.stewardEnabled) return;
if (planHash(readPlan(ctx)) !== approvedDraft) { await supervisorRequest(pi, "stop", { bindingId: binding.id }); state = { ...state, supervisor: null }; throw new Error("The plan changed during activation; select Ready again"); }
if (planHash(readPlan(ctx)) !== approvedDraft) { await supervisor.stop(binding.id); state = { ...state, supervisor: null }; throw new Error("The plan changed during activation; select Ready again"); }
state = { ...state, phase: "working" };
persist(); updateWidget(ctx);
pi.sendUserMessage(handoff, { deliverAs: "followUp" });
@@ -442,7 +442,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
const value = arg.slice("steward".length).trim() || "status";
if (value === "status") {
try {
const status = await supervisorRequest<{ connected: boolean }>(pi, "status");
const status = await supervisor.status();
ctx.ui.notify(`Plan supervisor: ${!state.stewardEnabled ? "disabled" : status.connected ? "connected" : "enabled, not connected; starts at Ready"}.`, "info");
} catch (error) { ctx.ui.notify(`Plan supervisor: ${state.stewardEnabled ? "enabled" : "disabled"}; ${String(error)}`, "warning"); }
return;
@@ -574,7 +574,6 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
return;
}
lastSeenWorkingSet = workingSet;
if (state.supervisor && state.phase === "working") void supervisorRequest(pi, "update", { bindingId: state.supervisor.id }).catch((error: Error) => ctx.ui.notify(error.message, "warning"));
turnsStale = 0;
updateWidget(ctx);
});
@@ -690,7 +689,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
if (bootstrap) {
supervisorOnly = true;
pi.setActiveTools(pi.getActiveTools().filter(tool => tool !== "CompleteGoal" && tool !== "RequestPlanReview"));
if (await models.enter("supervisor", ctx)) initializeSupervisor(pi, ctx, bootstrap, lifetime.signal);
if (await models.enter("supervisor", ctx)) initializeSupervisor(supervisor, ctx, bootstrap, lifetime.signal);
return;
}
const last = ctx.sessionManager
@@ -752,17 +751,28 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
}),
async execute(_id, params, signal, onUpdate, ctx) {
if (state.phase === "planning" || state.phase === "starting") return result("Planning is not approved. Wait for the steward or choose Ready before signing off a goal.", true);
const plan = readPlan(ctx);
let plan = readPlan(ctx);
if (!plan.trim()) return result(`No plan file at ${planRel(ctx)}. Run /goals to draft one.`, true);
if (supervisorOnly) throw new Error("Only the worker can complete its plan goals");
if (signal?.aborted || lifetime.signal.aborted) return result("Sign-off aborted.", true);
// A model may tick before calling this tool. The submitted goal is still under review;
// a rejection or cancellation must not leave that premature success visible.
const submitted = scanGoals(plan).filter(goal => goal.subject.toLowerCase() === params.goal.trim().toLowerCase());
if (submitted.length === 1 && submitted[0].status === "done") {
const lines = plan.split("\n");
lines[submitted[0].line] = lines[submitted[0].line].replace(/\[[xX]\]/, "[/]");
plan = lines.join("\n");
writePlan(ctx, plan);
updateWidget(ctx);
}
if (state.stewardEnabled) {
if (!state.supervisor) return result("No supervisor is paired. Retry Ready or use /goals steward off.", true);
const bindingId = state.supervisor.id;
const hash = planHash(plan);
try {
onUpdate?.({ content: [{ type: "text", text: "Supervisor checking trajectory and scope…" }], details: {} });
const decision = await supervisorRequest<SupervisorDecision>(pi, "review", { bindingId, goal: params.goal, planHash: hash }, AbortSignal.any([lifetime.signal, ...(signal ? [signal] : [])]));
const decision = await supervisor.review(bindingId, params.goal, hash, AbortSignal.any([lifetime.signal, ...(signal ? [signal] : [])]));
if (state.supervisor?.id !== bindingId || planHash(readPlan(ctx)) !== hash || decision.bindingId !== bindingId || decision.goal !== params.goal || decision.planHash !== hash) return result("Plan or pairing changed during goal review; retry.", true);
if (decision.decision !== "approve") return result(`Supervisor: ${decision.decision}. ${decision.reason}`, true);
} catch (error) { return result(`Supervisor review failed: ${String(error)}`, true); }
@@ -813,11 +823,11 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
},
});
// Registered after role restoration, so rejoin cannot start a supervisor turn on the worker model.
supervise(pi, () => models.ready);
const supervisor = supervise(pi, () => models.ready);
function modelRecovered(ctx: ExtensionContext): void {
if (supervisorOnly) {
const bootstrap = supervisorBootstrap(ctx);
if (bootstrap) initializeSupervisor(pi, ctx, bootstrap, lifetime.signal);
if (bootstrap) initializeSupervisor(supervisor, ctx, bootstrap, lifetime.signal);
} else if (state.modelRecovery && models.ready) {
// Do not await a UI dialog inside Pi's model_select dispatch.
setImmediate(() => { void reviewPlan(ctx).catch(error => ctx.ui.notify(String(error), "error")); });
@@ -906,7 +916,11 @@ export async function decideSignOff(
if (verdict === "accept") {
const beforeVerdict = judge.output.slice(0, judge.output.indexOf(verdictLine));
const checks = /^#{0,6}\s*(?:\*\*)?checks(?:\*\*)?:\s*$[\s\S]*^[-*]\s+.+$/im.test(beforeVerdict);
const heading = /^#{0,6}[ \t]*(?:\*\*)?checks(?:\*\*)?:[ \t]*$/im.exec(beforeVerdict);
const checksBody = heading
? beforeVerdict.slice(heading.index + heading[0].length).split(/^#{1,6}[ \t]+/m, 1)[0]
: "";
const checks = /^[ \t]*(?:[-*]|\d+[.)])[ \t]+\S.*$/m.test(checksBody);
if (!checks) {
return {
resultText: `Sign-off REJECTED. Missing:\nchecked-artifact list before VERDICT: accept\n\n--- judge ---\n${reasoning}`,
+3
View File
@@ -0,0 +1,3 @@
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
/** The bundled package exports an extension factory; its TypeScript source is loaded by Pi. */
export default function intercom(pi: ExtensionAPI): void;
+17
View File
@@ -0,0 +1,17 @@
import type { ExtensionAPI, ExtensionContext, SessionStartEvent } from "@earendil-works/pi-coding-agent";
/** Compose the bundled transport only after every installed extension has had a chance to register.
* An existing Intercom owns its own lifecycle. The fallback owns just its initial session_start;
* all subsequent hooks use Pi's public extension API normally (reload creates a fresh instance).
*/
export async function loadBundledIntercom(pi: ExtensionAPI, event: SessionStartEvent, ctx: ExtensionContext): Promise<void> {
if (pi.getAllTools().some(tool => tool.name === "intercom")) throw new Error("Installed Intercom has no extension channel. Enable/update that installation and reload; no second Intercom was loaded.");
const starts: Array<(event: SessionStartEvent, ctx: ExtensionContext) => unknown> = [];
const api = { ...pi, on(name: string, handler: (...args: any[]) => any) {
if (name === "session_start") starts.push(handler);
else pi.on(name as Parameters<ExtensionAPI["on"]>[0], handler);
} } as ExtensionAPI;
const { default: intercom } = await import("pi-intercom");
intercom(api);
for (const start of starts) await start(event, ctx);
}
+8 -4
View File
@@ -2,15 +2,19 @@ import { randomUUID } from "node:crypto";
export interface BackgroundState { quiet: boolean; description: string }
/** Public process-local protocols only. Missing owners remain explicitly unknown. */
/** Optional trackers are queried afresh. Absent tools mean no tracked work; an installed
* tracker that cannot report stays unknown. Detached/unregistered work is not OS-wide idleness. */
export async function backgroundState(pi: any): Promise<BackgroundState> {
const tools = pi.getAllTools();
const hasProcesses = tools.some((tool: any) => tool.name === "process");
const hasSubagents = tools.some((tool: any) => tool.name === "subagent");
let processes: unknown;
pi.events.emit("processes:request:list", { reply: (value: unknown) => { processes = value; } });
const rows = Array.isArray(processes) ? processes : null;
const rows = Array.isArray(processes) ? processes : !hasProcesses && processes === undefined ? [] : null;
const processKnown = rows?.every(p => p && ["running", "terminating", "terminate_timeout", "exited", "killed"].includes(p.status));
const activeProcesses = processKnown ? rows!.filter(p => !["exited", "killed"].includes(p.status)).length : null;
let activeSubagents: number | null = null;
if (pi.getAllTools?.().some((tool: any) => tool.name === "subagent")) {
let activeSubagents: number | null = hasSubagents ? null : 0;
if (hasSubagents) {
const requestId = randomUUID();
activeSubagents = await new Promise<number | null>(resolve => {
let unsubscribe: unknown;
File diff suppressed because it is too large Load Diff
-90
View File
@@ -1,90 +0,0 @@
import { createHash, randomUUID } from "node:crypto";
import { readFileSync } from "node:fs";
/** Process-local integration; Intercom carries the corresponding peer messages. */
export const PLAN_API_EVENT = "pi-supervise:plan:v1";
export interface PlanBinding {
id: string;
planPath: string;
workerSession: string;
workerPane: string;
supervisorPane?: string;
supervisorSession?: string;
active?: boolean;
stopped?: boolean;
everyTurns: number;
intervalMs: number;
compactTokens: number;
}
export interface GoalReview {
requestId: string;
bindingId: string;
goal: string;
planHash: string;
}
export interface GoalDecision extends GoalReview {
decision: "approve" | "needs_work" | "needs_user";
reason: string;
}
export type PlanWire =
| { t: "plan_hello" | "plan_hello_ack"; to: string; bindingId: string; role: "worker" | "supervisor"; sessionFile: string }
| ({ t: "goal_review"; to: string } & GoalReview)
| ({ t: "goal_decision"; to: string } & GoalDecision)
| { t: "goal_cancel"; to: string; requestId: string; bindingId: string }
| { t: "plan_update"; to: string; bindingId: string }
| { t: "plan_activate" | "plan_stop"; to: string; bindingId: string };
export interface PlanApiRequest {
version: 1;
method: "prepare" | "bootstrap" | "attached" | "activate" | "status" | "review" | "update" | "stop";
binding?: PlanBinding;
bindingId?: string;
workerId?: string;
goal?: string;
planHash?: string;
signal?: AbortSignal;
handled?: boolean;
resolve(value: unknown): void;
reject(error: Error): void;
}
export function validBinding(value: unknown): value is PlanBinding {
if (!value || typeof value !== "object") return false;
const b = value as PlanBinding;
return [b.id, b.planPath, b.workerSession, b.workerPane].every(v => typeof v === "string" && v.length > 0)
&& [b.everyTurns, b.intervalMs, b.compactTokens].every(v => Number.isSafeInteger(v) && v > 0);
}
export function planText(binding: PlanBinding): string {
return readFileSync(binding.planPath, "utf8");
}
export function planHash(text: string): string {
return createHash("sha256").update(text).digest("hex");
}
export function validPlanWire(value: any): value is PlanWire {
if (!value || typeof value.to !== "string" || typeof value.bindingId !== "string") return false;
if (value.t === "plan_hello" || value.t === "plan_hello_ack") return ["worker", "supervisor"].includes(value.role) && typeof value.sessionFile === "string";
if (value.t === "plan_update" || value.t === "plan_activate" || value.t === "plan_stop") return true;
if (typeof value.requestId !== "string") return false;
if (value.t === "goal_cancel") return true;
if (typeof value.goal !== "string" || typeof value.planHash !== "string") return false;
if (value.t === "goal_review") return true;
return value.t === "goal_decision" && ["approve", "needs_work", "needs_user"].includes(value.decision) && typeof value.reason === "string";
}
/** A wait owns its cancellation and deadline; a timeout never means approval. */
export function pendingReply<T>(signal: AbortSignal | undefined, cancel: () => void, timeoutMs = 600_000) {
let finish!: (value?: T, error?: Error) => void;
const promise = new Promise<T>((resolve, reject) => {
let settled = false;
const abort = () => finish(undefined, new Error("Supervisor request cancelled"));
const timer = setTimeout(() => finish(undefined, new Error("Supervisor request timed out; retry or turn the steward off")), timeoutMs);
finish = (value, error) => {
if (settled) return;
settled = true;
clearTimeout(timer);
signal?.removeEventListener("abort", abort);
if (error) { cancel(); reject(error); } else resolve(value as T);
};
signal?.addEventListener("abort", abort, { once: true });
if (signal?.aborted) queueMicrotask(abort);
});
return { requestId: randomUUID(), promise, finish };
}
+13 -10
View File
@@ -49,8 +49,9 @@ returns after a goal change, reload, compaction, and before every fifth review.
${goal || "not given, so infer it from the first view you receive and call set_goal"}
</goal>
Your verdict is a tool call, not text: let_it_run, steer or done. If the policy above tells you to reply
with JSON, ignore that part: it belongs to a different supervisor and nothing parses it here.
First visibly give a brief progress assessment and useful advice or perspective grounded in the view.
Then use let_it_run if on course, steer for a concrete correction, or done when finished.
Do not fill approval forms or merely report delivery. JSON from an older policy is not needed.
You see the worker twice: when it stops, and on a check in while it is still working. Each view
carries only what is new since your last look, so read it against what you already know rather
@@ -92,8 +93,8 @@ ${goal || "not set"}
${rounds} instructions so far.
A view of the worker follows. Answer it with one tool call: steer, done or let_it_run. The word on its
own does nothing; only the call reaches the worker.`;
A view of the worker follows. Give a brief visible assessment and perspective, then use steer, done
or let_it_run. Only steer sends an instruction; your visible assessment matters to the human too.`;
/** Sent when the human runs /supervise goal, so the supervisor does not judge against the old one. */
export const GOAL_CHANGED = (goal: string) =>
@@ -128,7 +129,7 @@ export const TOOL_LET_IT_RUN =
* 22 of 22 steers in the fifteen hours after.
*/
export const END_TURN =
`End the current supervisor response now: write one short line or no text, then make no further tool call.`;
`End the current supervisor response with a brief visible assessment of progress and useful perspective, unless you already gave it. Make no further tool call.`;
export const LET_IT_RUN_ACK = (reason: string, workerStopped = false) =>
`No supervisor instruction was sent for the current worker view. Supervisor-provided reason, not independently verified: ${reason}\n\nThe supervisor has completed its verdict for the current worker view. ${END_TURN}
@@ -194,14 +195,14 @@ export const REVIEW_NUDGE = (view: string, rounds: number, stopped: boolean) =>
${view}
${rounds} instructions so far. The status line says how long it has had no new turn. It will not start
again by itself, and the human being present does not count as somebody driving it. Answer with one
tool call: steer, done or let_it_run. The word on its own does nothing; only the call reaches the
worker.`
again by itself. You are the supervisor, not the worker. Give a brief visible progress assessment
and helpful perspective. Steer with a concrete continuation if work remains, or say what human
decision is needed. Use let_it_run when no intervention is useful, or done when complete.`
: `${VIEW_CHECKIN}
${view}
Call let_it_run unless the view gives concrete evidence that the worker needs an instruction.`;
You are the supervisor, not the worker. Briefly assess progress and the most useful next consideration in visible text. Use let_it_run when on course; steer only when the evidence calls for a concrete correction.`;
/** Refusal shown when done is called while the worker still has work running. */
export const DONE_BLOCKED = (what: string) =>
@@ -218,7 +219,9 @@ their phone.`;
* Unlike that extension there is no JSON verdict to parse, because the verdict is a tool call.
*/
export const DEFAULT_SUPERVISOR_PROMPT = `You supervise a coding agent from outside its session.
Your job is to make it reach the goal without the human stepping in.
Your job is to help it reach the agreed goal without unnecessary human intervention.
At each check visibly assess how the work is tracking and offer useful perspective in a few sentences.
Inspect, judge and steer; never execute work, delegate it, schedule it, or mutate the worker's files.
Judge from the view only. You cannot see the worker's files unless you read them yourself.
+64 -9
View File
@@ -5,7 +5,62 @@
* turn locally with pi.sendUserMessage after it receives one of these.
*/
import { type PlanBinding, type PlanWire, validBinding, validPlanWire } from "./plan-api.js";
import { type SupervisorBinding, validBinding } from "../../supervisor.js";
import { MAX_VIEW_BYTES } from "./view.js";
export interface GoalReview {
requestId: string;
bindingId: string;
goal: string;
planHash: string;
}
/** Frozen worker context travels only with the request, never with a decision. */
export interface GoalReviewRequest extends GoalReview {
view?: string;
}
export function reviewIdentity({ requestId, bindingId, goal, planHash }: GoalReview): GoalReview {
return { requestId, bindingId, goal, planHash };
}
/** Intercom measures serialized payload bytes, including JSON escapes and checkpoint identity. */
export function goalReviewWire(to: string, review: GoalReview, view: string): PlanWire {
const payload = { t: "goal_review" as const, to, ...reviewIdentity(review), view };
const fits = (value: string) => Buffer.byteLength(value, "utf8") <= MAX_VIEW_BYTES && Buffer.byteLength(JSON.stringify({ ...payload, view: value }), "utf8") <= 16 * 1024;
if (fits(view)) return payload;
const marker = "\n[checkpoint view cut to fit the channel; inspect the worker source session for omitted detail]\n";
if (!fits(marker)) throw new Error("Goal checkpoint identity is too large for the 16 KiB Intercom channel");
const chars = Array.from(view);
let low = 0; let high = chars.length;
while (low < high) {
const middle = Math.ceil((low + high) / 2);
if (fits(chars.slice(0, middle).join("") + marker)) low = middle;
else high = middle - 1;
}
return { ...payload, view: chars.slice(0, low).join("") + marker };
}
export interface GoalDecision extends GoalReview {
decision: "approve" | "needs_work" | "needs_user";
reason: string;
}
export type PlanWire =
| { t: "plan_hello" | "plan_hello_ack"; to: string; bindingId: string; role: "worker" | "supervisor"; sessionFile: string }
| ({ t: "goal_review"; to: string } & GoalReviewRequest)
| ({ t: "goal_decision"; to: string } & GoalDecision)
| { t: "goal_cancel"; to: string; requestId: string; bindingId: string }
| { t: "plan_activate" | "plan_stop"; to: string; bindingId: string };
export function validPlanWire(value: any): value is PlanWire {
if (!value || typeof value.to !== "string" || typeof value.bindingId !== "string") return false;
if (value.t === "plan_hello" || value.t === "plan_hello_ack") return ["worker", "supervisor"].includes(value.role) && typeof value.sessionFile === "string";
if (value.t === "plan_activate" || value.t === "plan_stop") return true;
if (typeof value.requestId !== "string") return false;
if (value.t === "goal_cancel") return true;
if (typeof value.goal !== "string" || typeof value.planHash !== "string") return false;
if (value.t === "goal_review") return value.view === undefined || (typeof value.view === "string" && Buffer.byteLength(value.view, "utf8") <= MAX_VIEW_BYTES);
return value.t === "goal_decision" && ["approve", "needs_work", "needs_user"].includes(value.decision) && typeof value.reason === "string";
}
export const NAMESPACE = "wassname/pi-intercom-supervisor/v1";
@@ -13,31 +68,31 @@ export const NAMESPACE = "wassname/pi-intercom-supervisor/v1";
// /supervise stop, because premature stopping is the failure this whole thing exists to prevent
// (wassname's SUPERVISOR.md, citing arXiv:2410.07095: 8.7% vs 0.8% on MLE-bench).
export type Wire = PlanWire
export type Wire = (PlanWire
/** Roll call, broadcast, so "to" is the wildcard rather than a session. Only /supervise sends it. */
| { t: "who"; to: "*" }
/** The answer to a roll call: I load this extension, I am free, and I am not a child run. */
| { t: "here"; to: string }
| { t: "pair"; to: string; goal: string; plan?: PlanBinding }
| { t: "paired"; to: string; plan?: PlanBinding }
| { t: "pair"; to: string; goal: string; plan?: SupervisorBinding }
| { t: "paired"; to: string; plan?: SupervisorBinding }
| { t: "goal"; to: string; goal: string }
/** stopped: the worker settled, so this is a decision point. false: a check in mid-turn. */
| { t: "view"; to: string; view: string; stopped: boolean }
| { t: "view"; to: string; view: string; stopped: boolean; refreshed?: boolean }
/** Supervisor asks for a view now. Its own turn cannot make one: the worker publishes them. */
| { t: "look"; to: string }
| { t: "directive"; to: string; text: string }
| { t: "done"; to: string; reason: string }
| { t: "unpair"; to: string };
| { t: "unpair"; to: string }) & { bindingId?: string };
/** Validates the field each kind carries, so a malformed peer cannot inject "[supervisor] undefined". */
export function isWire(payload: unknown): payload is Wire {
if (typeof payload !== "object" || payload === null) return false;
if (validPlanWire(payload)) return true;
const { t, to, goal, view, stopped, text, reason, plan } = payload as Record<string, unknown>;
const { t, to, goal, view, stopped, refreshed, text, reason, plan } = payload as Record<string, unknown>;
if ((t === "pair" || t === "paired") && plan !== undefined && !validBinding(plan)) return false;
if (typeof to !== "string") return false;
if (t === "pair" || t === "goal") return typeof goal === "string";
if (t === "view") return typeof view === "string" && typeof stopped === "boolean";
if (t === "view") return typeof view === "string" && typeof stopped === "boolean" && (refreshed === undefined || typeof refreshed === "boolean");
if (t === "directive") return typeof text === "string" && text.trim().length > 0;
if (t === "done") return typeof reason === "string";
return t === "unpair" || t === "paired" || t === "look" || t === "who" || t === "here";
@@ -86,7 +141,7 @@ export interface SuperviseState {
/** Recent steer texts, so the supervisor can see repetition after its own context is compacted. */
recentSteers: string[];
/** Optional pi-goals integration. Standalone supervision keeps its original policy. */
plan?: PlanBinding;
plan?: SupervisorBinding;
/** Supervisor bootstrap completed and the worker acknowledged this plan pairing. */
planInitialized?: boolean;
}
+16 -3
View File
@@ -224,6 +224,8 @@ function vccSections(fresh: AgentMsg[]): { headers: string; brief: string } {
export interface ViewInput {
goal: string;
status: string;
/** Read-only source for detail omitted by the bounded overview. */
sourceSession?: string;
entries: Entry[];
/**
* Turns the supervisor has already been sent, from turnsSince() after the last view.
@@ -245,10 +247,11 @@ export interface ViewInput {
* model, and a worker near the top of its context is about to compact and lose detail.
*/
model?: string;
background?: string;
}
/** Render the view, and cut it to MAX_VIEW_BYTES so the broker cannot reject it. */
export function buildView({ goal, status, entries, since = 0, stale = 0, subagents = [], model = "" }: ViewInput): string {
export function buildView({ goal, status, entries, since = 0, stale = 0, subagents = [], model = "", sourceSession, background }: ViewInput): string {
const messages = entries.filter((e) => e.type === "message" && e.message);
const pending = outstandingWork(messages);
const workerMessages = messagesSince(entries);
@@ -260,8 +263,16 @@ export function buildView({ goal, status, entries, since = 0, stale = 0, subagen
const fresh = workerMessages.slice(from);
const { headers, brief } = vccSections(fresh);
const earlier = compactionSummary(entries);
// VCC may omit an older user turn even when later work still depends on its authorization.
// Keep the latest actual user direction distinct from summaries and supervisor echoes.
const latestUser = [...entries].reverse().find(e => e.type === "message" && e.message?.role === "user" && textOf(e.message).trim() && !textOf(e.message).startsWith(SUPERVISOR_PREFIX));
const direction = latestUser?.message ? textOf(latestUser.message) : "";
const head = [
...(from === 0 && direction ? [
`# Latest user direction${latestUser?.timestamp ? ` (${latestUser.timestamp})` : ""}`,
direction.length > 2000 ? `${direction.slice(0, 2000)}\n[user direction truncated; inspect the worker source session for full text]` : direction,
"",
] : []),
// Short goals are the criterion on every review. A multi-line research rubric is reinserted
// into the supervisor context at its own cadence, so this view carries only its locator.
`<goal>`,
@@ -269,11 +280,13 @@ export function buildView({ goal, status, entries, since = 0, stale = 0, subagen
`</goal>`,
``,
`# Worker`,
...(sourceSession ? [`source session: ${sourceSession} (read-only history for omitted detail)`] : []),
...(model ? [`model: ${model}`] : []),
`status: ${status}`,
`turns: ${workerMessages.length}`,
`tool calls with no result: ${pending.length ? pending.join(", ") : "none"}`,
`child pi processes still running: ${subagents.length ? subagents.join(", ") : "none"}`,
...(background ? [`tracked background work: ${background}`] : []),
...(stale > 0 ? [`no new file or commit for ${stale} reviews in a row`] : []),
``,
// Sent when this view starts at the compaction boundary, which is the first view and every
@@ -281,7 +294,7 @@ export function buildView({ goal, status, entries, since = 0, stale = 0, subagen
...(from === 0 && earlier
? [
restarted ? `# The worker compacted, so this view restarts. Everything before it:` : `# Earlier work, from the worker's own compaction summary`,
earlier.slice(0, 6000),
earlier.length > 6000 ? `${earlier.slice(0, 6000)}\n[worker compaction summary truncated; inspect the worker session for full evidence]` : earlier,
``,
]
: []),
+68 -24
View File
@@ -1,9 +1,8 @@
import { createHash, randomUUID } from "node:crypto";
import { fileURLToPath } from "node:url";
import { type ExtensionAPI, type ExtensionContext, SessionManager } from "@earendil-works/pi-coding-agent";
import { readFileSync } from "node:fs";
import { type ExtensionAPI, type ExtensionContext, getAgentDir, SessionManager } from "@earendil-works/pi-coding-agent";
export const SUPERVISOR_ROLE = "pi-goals-supervisor";
const PLAN_API = "pi-supervise:plan:v1";
export interface SupervisorBinding {
id: string;
planPath: string;
@@ -12,25 +11,56 @@ export interface SupervisorBinding {
supervisorPane?: string;
supervisorSession?: string;
active?: boolean;
stopped?: boolean;
everyTurns: number;
intervalMs: number;
compactTokens: number;
}
interface Bootstrap { binding: SupervisorBinding; workerId: string }
interface SupervisorStatus { connected: boolean; binding?: SupervisorBinding; workerId: string; role?: string }
export interface Bootstrap { binding: SupervisorBinding; workerId: string }
export interface SupervisorStatus { connected: boolean; binding?: SupervisorBinding; workerId: string; role?: string }
export interface SupervisorDecision { bindingId: string; goal: string; planHash: string; decision: "approve" | "needs_work" | "needs_user"; reason: string }
export function planHash(text: string): string {
return createHash("sha256").update(text).digest("hex");
}
/** The owner claims synchronously; its promise includes peer acknowledgement or review. */
export function supervisorRequest<T>(pi: ExtensionAPI, method: string, params: Record<string, unknown> = {}, signal?: AbortSignal): Promise<T> {
return new Promise((resolve, reject) => {
const request = { version: 1, method, ...params, signal, handled: false, resolve, reject };
pi.events.emit(PLAN_API, request);
if (!request.handled) reject(new Error("The internal supervisor is not registered. Load the pi-goals package directory (not only src/index.ts), then reload Pi."));
});
export interface SupervisorController {
status(signal?: AbortSignal): Promise<SupervisorStatus>;
prepare(binding: SupervisorBinding, signal?: AbortSignal): Promise<void>;
bootstrap(bootstrap: Bootstrap, signal?: AbortSignal): Promise<SupervisorBinding>;
attached(bindingId: string, signal?: AbortSignal): Promise<SupervisorBinding>;
activate(bindingId: string, signal?: AbortSignal): Promise<void>;
review(bindingId: string, goal: string, hash: string, signal?: AbortSignal): Promise<SupervisorDecision>;
stop(bindingId: string): Promise<void>;
}
export function validBinding(value: unknown): value is SupervisorBinding {
if (!value || typeof value !== "object") return false;
const b = value as SupervisorBinding;
return [b.id, b.planPath, b.workerSession, b.workerPane].every(v => typeof v === "string" && v.length > 0)
&& [b.everyTurns, b.intervalMs, b.compactTokens].every(v => Number.isSafeInteger(v) && v > 0);
}
export function planText(binding: SupervisorBinding): string {
return readFileSync(binding.planPath, "utf8");
}
/** Startup waits may have a deadline. Model checkpoints pass null: elapsed thinking is not failure. */
export function pendingReply<T>(signal: AbortSignal | undefined, cancel: () => void, timeoutMs: number | null = 600_000) {
let finish!: (value?: T, error?: Error) => void;
const promise = new Promise<T>((resolve, reject) => {
let settled = false;
const abort = () => finish(undefined, new Error("Supervisor request cancelled"));
const timer = timeoutMs === null ? undefined : setTimeout(() => finish(undefined, new Error("Supervisor request timed out; retry or turn the steward off")), timeoutMs);
finish = (value, error) => {
if (settled) return;
settled = true;
clearTimeout(timer);
signal?.removeEventListener("abort", abort);
if (error) { cancel(); reject(error); } else resolve(value as T);
};
signal?.addEventListener("abort", abort, { once: true });
if (signal?.aborted) queueMicrotask(abort);
});
return { requestId: randomUUID(), promise, finish };
}
async function herdr(pi: ExtensionAPI, args: string[], signal?: AbortSignal): Promise<Record<string, any>> {
@@ -54,36 +84,51 @@ export function supervisorBootstrap(ctx: ExtensionContext): Bootstrap | undefine
return entry?.type === "custom" ? entry.data as Bootstrap : undefined;
}
export function initializeSupervisor(pi: ExtensionAPI, ctx: ExtensionContext, bootstrap: Bootstrap, signal?: AbortSignal): void {
// session_start handlers are ordered. Let all packages initialize before requesting their API.
export function initializeSupervisor(supervisor: SupervisorController, ctx: ExtensionContext, bootstrap: Bootstrap, signal?: AbortSignal): void {
// Let all packages initialize before bootstrapping the local supervisor role.
setImmediate(() => {
if (signal?.aborted) return;
void supervisorRequest(pi, "bootstrap", bootstrap as unknown as Record<string, unknown>, signal).catch((error: Error) => {
void supervisor.bootstrap(bootstrap, signal).catch((error: Error) => {
if (!signal?.aborted) ctx.ui.notify(`Supervisor initialization failed: ${error.message}`, "error");
});
});
}
/** Replay only the worker's explicit resource choices, never its prompt, mode, model or credentials.
* Configured packages come from the same agent directory. No companion extension is added. */
export function supervisorResourceArgs(argv: string[]): string[] {
const valued = new Set(["-e", "--extension", "--skill", "--prompt-template", "--theme"]);
const flags = new Set(["--no-extensions", "--no-skills", "--no-prompt-templates", "--no-themes", "--no-context-files", "-nc", "--approve", "-a", "--no-approve", "-na"]);
const aliases: Record<string, string> = { "-ne": "--no-extensions", "-ns": "--no-skills", "-np": "--no-prompt-templates" };
const result: string[] = [];
for (let i = 0; i < argv.length; i++) {
const arg = aliases[argv[i]] ?? argv[i];
if (arg === "--") break;
if (valued.has(arg) && argv[i + 1]) result.push(arg, argv[++i]);
else if (flags.has(arg)) result.push(arg);
}
return result;
}
export async function startSupervisor(
pi: ExtensionAPI, ctx: ExtensionContext, planPath: string, existing: SupervisorBinding | null,
pi: ExtensionAPI, supervisor: SupervisorController, ctx: ExtensionContext, planPath: string, existing: SupervisorBinding | null,
save: (binding: SupervisorBinding) => void, signal: AbortSignal,
): Promise<SupervisorBinding> {
if (process.env.HERDR_ENV !== "1" || !process.env.HERDR_PANE_ID) throw new Error("Start Pi inside Herdr before enabling the steward at Ready.");
const status = await supervisorRequest<SupervisorStatus>(pi, "status", {}, signal);
const status = await supervisor.status(signal);
signal.throwIfAborted();
if (existing && status.connected && status.binding?.id === existing.id) return status.binding;
if ((!existing && status.role && status.role !== "none") || (status.binding && status.binding.id !== existing?.id)) throw new Error("This session already has another supervision relationship. Stop it explicitly before Ready.");
const parent = ctx.sessionManager.getSessionFile();
const leaf = ctx.sessionManager.getLeafId();
if (!parent || !leaf) throw new Error("The planning session must be persisted before creating its supervisor fork.");
const packageRoot = fileURLToPath(new URL("../", import.meta.url));
let binding = existing ?? {
id: randomUUID(), planPath, workerSession: parent, workerPane: process.env.HERDR_PANE_ID,
everyTurns: 50, intervalMs: 60 * 60_000, compactTokens: 100_000,
};
if (!existing) {
save(binding);
await supervisorRequest(pi, "prepare", { binding }, signal);
await supervisor.prepare(binding, signal);
signal.throwIfAborted();
}
if (!binding.supervisorSession) {
@@ -99,9 +144,9 @@ export async function startSupervisor(
// An existing occupant is not permission to start another process on the same session file.
await focusSupervisor(pi, binding, "supervisor");
signal.throwIfAborted();
return await supervisorRequest<SupervisorBinding>(pi, "attached", { bindingId: binding.id }, signal);
return await supervisor.attached(binding.id, signal);
}
const split = await herdr(pi, ["pane", "split", "--current", "--direction", "right", "--cwd", ctx.cwd, "--no-focus"], signal);
const split = await herdr(pi, ["pane", "split", "--current", "--direction", "right", "--cwd", ctx.cwd, "--env", `PI_CODING_AGENT_DIR=${getAgentDir()}`, "--no-focus"], signal);
const pane = split.pane?.pane_id;
if (typeof pane !== "string") throw new Error("Herdr split did not return a pane ID");
binding = { ...binding, supervisorPane: pane };
@@ -109,11 +154,10 @@ export async function startSupervisor(
signal.throwIfAborted();
// Keep bootstrap and worker binding identical, including the returned pane identity.
SessionManager.open(binding.supervisorSession!).appendCustomEntry(SUPERVISOR_ROLE, { binding, workerId: status.workerId });
const waiting = supervisorRequest<SupervisorBinding>(pi, "attached", { bindingId: binding.id }, signal);
const waiting = supervisor.attached(binding.id, signal);
void waiting.catch(() => {});
try {
await herdr(pi, ["agent", "start", `supervisor-${binding.id.slice(0, 8)}`, "--kind", "pi", "--pane", pane, "--", "--session", binding.supervisorSession!,
"-e", packageRoot], signal);
await herdr(pi, ["agent", "start", `supervisor-${binding.id.slice(0, 8)}`, "--kind", "pi", "--pane", pane, "--", "--session", binding.supervisorSession!, ...supervisorResourceArgs(process.argv.slice(2))], signal);
return await waiting;
} catch (error) {
throw new Error(`Supervisor startup incomplete: ${String(error)}. Inspect the recorded pane, resolve startup, reload it, then retry Ready.`);
+20
View File
@@ -19,6 +19,26 @@ describe("decideSignOff (fail-forward invariant)", () => {
expect(out.resultText).toContain("evidence.txt: `PASS`");
});
it.each(["1. ", "1) ", " - "])("accepts a real checked-artifact list using Markdown marker %s", async marker => {
const output = `## checks:\n${marker}converter.mjs: \`export function convert\`; inspected the actual export.\n\nVERDICT: accept\nmissing:`;
const out = await decideSignOff(input, undefined, vi.fn().mockResolvedValue({ output }));
expect(out.isError).toBe(false);
expect(out.logEntry).toContain("judge accept");
});
it.each(["1. ", "- "])("does not borrow %s items from a later section when checks is empty", async marker => {
const output = `## checks:\n\n## Next steps\n${marker}Run the tests later\n\nVERDICT: accept\nmissing:`;
const out = await decideSignOff(input, undefined, vi.fn().mockResolvedValue({ output }));
expect(out.isError).toBe(true);
expect(out.logEntry).toContain("no checked-artifact list");
});
it("rejects an empty checks heading", async () => {
const out = await decideSignOff(input, undefined, vi.fn().mockResolvedValue({ output: "## checks:\n\nVERDICT: accept\nmissing:" }));
expect(out.isError).toBe(true);
expect(out.logEntry).toContain("no checked-artifact list");
});
it("rejects an accept verdict without a checked-artifact list", async () => {
const runJudge = vi.fn().mockResolvedValue({ output: "VERDICT: accept\nmissing:" });
const out = await decideSignOff(input, undefined, runJudge);
+3 -1
View File
@@ -12,7 +12,9 @@ export default function testHost(pi: ExtensionAPI): void {
if (args[0] === "pane") return { code: 0, stdout: JSON.stringify({ result: { pane: { pane_id: "test-supervisor-pane" } } }), stderr: "", killed: false };
if (args[1] === "start") {
const native = args.slice(args.indexOf("--") + 1);
const child = spawn(process.execPath, [process.argv[1], "--mode", "rpc", "--no-extensions", ...native, "--model", "offline/test"], {
// The worker's test-only -e host must not become the supervisor package. The fixture
// profile points at the untouched packed package; override only the UI mode for this test.
const child = spawn(process.execPath, [process.argv[1], "--mode", "rpc", "--session", native[native.indexOf("--session") + 1], "--model", "offline/test"], {
cwd: process.cwd(), env: process.env, stdio: ["pipe", "pipe", "pipe"],
});
children.push(child);
+13
View File
@@ -79,6 +79,19 @@ async function settleDraft(flow: ReturnType<typeof setup>) {
}
describe("/goals draft flow", () => {
it("reopens a prematurely ticked submitted goal before a failed sign-off", async () => {
const flow = setup([]);
try {
flow.entries.push({ type: "custom", customType: "pi-goals-state", data: { phase: "working", planVersion: 1, stewardEnabled: true, autoIntervalMs: null } });
await flow.hooks.get("session_start")({}, flow.ctx);
const file = join(flow.cwd, ".pi/plan/session-a-v1.md");
mkdirSync(join(flow.cwd, ".pi/plan"), { recursive: true });
writeFileSync(file, "# Plan\n1. [x] goal: first\n - [x] subtask\n2. [ ] goal: second\n");
const outcome = await flow.tools.get("CompleteGoal").execute("", { goal: "first" }, undefined, undefined, flow.ctx);
expect(outcome.isError).toBe(true);
expect(readFileSync(file, "utf8")).toBe("# Plan\n1. [/] goal: first\n - [x] subtask\n2. [ ] goal: second\n");
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
});
it("enables steward and hourly auto by default in new and cleared legacy sessions", async () => {
for (const legacy of [false, true]) {
const flow = setup([]);
+43
View File
@@ -0,0 +1,43 @@
import { execFileSync, spawn } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { describe, expect, it, vi } from "vitest";
describe("packed Intercom registration in real Pi (no model call)", () => {
it.each(["bundled", "installed before goals", "installed after goals"])("registers one transport and one tool: %s", async (order) => {
const cwd = mkdtempSync(join(tmpdir(), "goals-registration-"));
const packed = JSON.parse(execFileSync("npm", ["pack", "--json", "--ignore-scripts", "--cache", "/tmp/pi-goals-npm-cache", "--pack-destination", cwd], { cwd: resolve("."), encoding: "utf8" }))[0];
execFileSync("tar", ["-xzf", join(cwd, packed.filename), "-C", cwd]);
const root = join(cwd, "package");
const external = join(root, "node_modules/pi-intercom");
const agentDir = join(cwd, "profile"); mkdirSync(agentDir);
const packages = order === "bundled" ? [root] : order === "installed before goals" ? [external, root] : [root, external];
writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ packages }));
const probe = join(cwd, "probe.ts");
writeFileSync(probe, `import { writeFileSync } from "node:fs";
export default function(pi) {
pi.registerCommand("registrationreload", { handler: async (_args, ctx) => { await ctx.reload(); } });
pi.on("resources_discover", () => {
let registrations = 0;
pi.events.emit("intercom:extension-register", { namespace: "goals-registration-check", ownerEligible: false, onEvent() {}, onReady() { registrations++; } });
writeFileSync(${JSON.stringify(join(cwd, "registration.json"))}, JSON.stringify({ registrations, tools: pi.getAllTools().map(t => t.name) }));
});
}`);
const child = spawn(resolve("node_modules/.bin/pi"), ["--mode", "rpc", "--no-session", "-e", probe], { cwd, env: { ...process.env, PI_CODING_AGENT_DIR: agentDir }, stdio: ["pipe", "pipe", "pipe"] });
let stderr = ""; child.stderr.on("data", chunk => { stderr += chunk; }); child.stdout.resume();
try {
await vi.waitFor(() => expect(existsSync(join(cwd, "registration.json")), stderr).toBe(true), { timeout: 12_000, interval: 25 });
const report = JSON.parse(readFileSync(join(cwd, "registration.json"), "utf8"));
expect(report.registrations).toBe(1);
expect(report.tools.filter((name: string) => name === "intercom")).toHaveLength(1);
expect(new Set(report.tools).size).toBe(report.tools.length);
expect(stderr).not.toMatch(/conflicting tools|Multiple Intercom runtimes/);
rmSync(join(cwd, "registration.json"));
child.stdin.write(`${JSON.stringify({ type: "prompt", message: "/registrationreload" })}\n`);
await vi.waitFor(() => expect(existsSync(join(cwd, "registration.json")), stderr).toBe(true), { timeout: 12_000, interval: 25 });
const reloaded = JSON.parse(readFileSync(join(cwd, "registration.json"), "utf8"));
expect(reloaded).toEqual(report);
} finally { child.kill(); await new Promise(done => child.once("close", done)); rmSync(cwd, { recursive: true, force: true }); }
}, 20_000);
});
+25
View File
@@ -0,0 +1,25 @@
import type { ExtensionAPI, ExtensionContext, SessionStartEvent } from "@earendil-works/pi-coding-agent";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { loadBundledIntercom } from "../src/intercom.js";
const factory = vi.hoisted(() => vi.fn());
vi.mock("pi-intercom", () => ({ default: factory }));
beforeEach(() => { factory.mockReset(); });
describe("loadBundledIntercom", () => {
it("composes the fallback once and explicitly starts its public lifecycle", async () => {
const on = vi.fn(); const start = vi.fn(); const shutdown = vi.fn(); const registerTool = vi.fn();
factory.mockImplementation((pi: ExtensionAPI) => { pi.on("session_start", start); pi.on("session_shutdown", shutdown); pi.registerTool({ name: "intercom" } as any); });
const pi = { on, registerTool, getAllTools: () => [] } as unknown as ExtensionAPI;
const event = { type: "session_start", reason: "startup" } as SessionStartEvent;
const ctx = {} as ExtensionContext;
await loadBundledIntercom(pi, event, ctx);
expect(factory).toHaveBeenCalledOnce(); expect(start).toHaveBeenCalledExactlyOnceWith(event, ctx);
expect(on).toHaveBeenCalledExactlyOnceWith("session_shutdown", shutdown);
expect(registerTool).toHaveBeenCalledExactlyOnceWith({ name: "intercom" });
});
it("never loads a second copy when an installed Intercom cannot supply the required channel", async () => {
const pi = { getAllTools: () => [{ name: "intercom" }] } as unknown as ExtensionAPI;
await expect(loadBundledIntercom(pi, {} as SessionStartEvent, {} as ExtensionContext)).rejects.toThrow("no second Intercom was loaded");
expect(factory).not.toHaveBeenCalled();
});
});
+161 -35
View File
@@ -30,7 +30,7 @@ test("retries intercom registration when pi-intercom loads after pi-supervise",
sendUserMessage() {},
};
extension(pi);
bus.on(INTERCOM_EXTENSION_REGISTER_EVENT, (registration) => registrations.push(registration));
bus.on(INTERCOM_EXTENSION_REGISTER_EVENT, (registration) => { registrations.push(registration); registration.onReady({ snapshot: () => ({ connected: false }) }); });
bus.emit(INTERCOM_EXTENSION_REGISTRY_READY_EVENT, { version: 1 });
bus.emit(INTERCOM_EXTENSION_REGISTRY_READY_EVENT, { version: 1 });
assert.equal(registrations.length, 1);
@@ -91,8 +91,6 @@ function harness(
},
commitState: () => {},
listSessions: async () => [
// model and contextPct are on the real SessionInfo (pi-intercom/types.ts), pushed by presence.
// The view header reads them off our own record, so a session missing them fails a test here.
{ id: ownId, pid: process.pid, name: ownId === WORKER_ID ? "worker" : "supervisor", cwd: process.cwd(), model: "test/tiny", contextPct: 12 },
{
id: ownId === WORKER_ID ? SUPER_ID : WORKER_ID,
@@ -133,16 +131,19 @@ function harness(
// On the pi API, NOT on the command context. Putting them on the context here is what hid a
// real bug: the live command handler threw "context.getActiveTools is not a function", and
// before that the optional call returned undefined and the strip skipped in silence.
getAllTools: () => ["read", "grep", "find", "ls"].map(name => ({ name, sourceInfo: { source: "builtin" } })),
getActiveTools: () => activeTools,
setActiveTools: (names: string[]) => {
activeTools = names;
},
};
let activeTools = ["read", "grep", "list", "bash", "edit", "write"];
let activeTools = ["read", "grep", "ls", "bash", "edit", "write"];
const status = new Map<string, string | undefined>();
const ctx = {
cwd: process.cwd(),
model: { provider: "test", id: "tiny" },
getContextUsage: () => ({ percent: 12 }),
isIdle: () => isIdle,
hasUI: true,
// pi's own ExtensionContext.abort(): stops the agent loop before its next model call.
@@ -166,7 +167,9 @@ function harness(
},
};
let controller: ReturnType<typeof extension>;
return {
controllerStatus: () => controller.status(),
pi,
ctx,
tools,
@@ -179,9 +182,18 @@ function harness(
aborts,
status,
async start() {
extension(pi as any);
controller = extension(pi as any);
for (const fn of handlers.get("session_start") ?? []) await fn({}, ctx);
},
setIdle(value: boolean) { isIdle = value; },
async finishAssessment() {
await tools.get("let_it_run").execute("assessed", { reason: "Inspection complete" }, undefined, undefined, ctx);
for (const fn of handlers.get("agent_settled") ?? []) await fn({}, ctx);
},
async shutdown() {
for (const fn of handlers.get("session_shutdown") ?? []) await fn({}, ctx);
},
intercomEvent(event: any) { onEvent(event); },
async settle() {
for (const fn of handlers.get("agent_settled") ?? []) await fn({}, ctx);
},
@@ -324,11 +336,13 @@ test("a goal the supervisor inferred reaches the worker, which owns the view hea
worker.deliver(SUPER_ID, { t: "pair", to: WORKER_ID, goal: "" });
await new Promise((r) => setTimeout(r, 5));
await worker.settle();
await new Promise(resolve => setTimeout(resolve, 50));
assert.match(worker.published.find((p) => p.t === "view").view, /^<goal>\nnot set\n<\/goal>$/m, "no goal yet");
worker.deliver(SUPER_ID, { t: "goal", to: WORKER_ID, goal: "make the results table" });
await new Promise((r) => setTimeout(r, 5));
await worker.settle();
await new Promise(resolve => setTimeout(resolve, 50));
const views = worker.published.filter((p) => p.t === "view");
assert.match(views[views.length - 1].view, /^<goal>\nmake the results table\n<\/goal>$/m, "the header must follow set_goal");
});
@@ -344,10 +358,12 @@ test("the second view carries only what happened after the first", async () => {
await new Promise((r) => setTimeout(r, 300));
await worker.settle();
await new Promise(resolve => setTimeout(resolve, 50));
assert.match(worker.published.find((p) => p.t === "view").view, /THE FIRST INSTRUCTION/);
entries.push(message("assistant", "THE SECOND THING"));
await worker.settle();
await new Promise(resolve => setTimeout(resolve, 50));
const second = worker.published.filter((p) => p.t === "view").at(-1).view;
assert.match(second, /THE SECOND THING/);
assert.doesNotMatch(second, /THE FIRST INSTRUCTION/, "the supervisor already read this one");
@@ -371,6 +387,7 @@ test("on settle the worker publishes a view built from the live branch", async (
await new Promise((r) => setTimeout(r, 5));
await worker.settle();
await new Promise(resolve => setTimeout(resolve, 50));
const view = worker.published.find((p) => p.t === "view");
assert.ok(view, "expected a view publish");
assert.equal(view.to, SUPER_ID);
@@ -390,6 +407,7 @@ test("the view is built from the live branch, not from every entry in the sessio
await new Promise((r) => setTimeout(r, 5));
await worker.settle();
await new Promise(resolve => setTimeout(resolve, 50));
const view = worker.published.find((p) => p.t === "view");
assert.match(view.view, /kept on the live branch/);
assert.doesNotMatch(view.view, /ABANDONED after a rewind/);
@@ -435,19 +453,20 @@ test("goal, pairing and the steer count all survive a reload together", async ()
assert.deepEqual(restored.recentSteers, ["instruction 0", "instruction 1", "instruction 2"]);
});
test("a view that arrives while the supervisor is thinking is queued, not dropped", async () => {
// pi throws "Agent is already processing" when sendUserMessage gets no delivery option, and the
// catch upstream turns that into a dropped message. On a half hour look the supervisor would
// silently skip a whole look.
test("a view arriving during unrelated supervisor thinking waits for a fresh complete overview", async () => {
const sup = harness(SUPER_ID, { isIdle: false });
await sup.start();
await sup.run("supervise", "@worker g");
sup.userMessages.length = 0;
sup.deliver(WORKER_ID, { t: "view", to: SUPER_ID, view: "# Goal: g\n", stopped: true });
await new Promise((r) => setTimeout(r, 5));
assert.equal(sup.userMessages.length, 1, "the view must still reach the supervisor");
assert.deepEqual(sup.userMessages[0].options, { deliverAs: "followUp" });
try {
await sup.start(); await sup.run("supervise", "@worker g");
sup.deliver(WORKER_ID, { t: "view", to: SUPER_ID, view: "intervening evidence", stopped: true });
await new Promise(resolve => setTimeout(resolve, 5));
assert.equal(sup.userMessages.length, 0);
sup.setIdle(true); await sup.settle();
assert.equal(sup.published.filter(wire => wire.t === "look").length, 1);
sup.deliver(WORKER_ID, { t: "view", to: SUPER_ID, view: "complete intervening evidence", stopped: true, refreshed: true });
await new Promise(resolve => setTimeout(resolve, 5));
assert.equal(sup.userMessages.length, 1);
assert.match(sup.userMessages[0].content, /complete intervening evidence/);
} finally { await sup.shutdown(); }
});
test("the nudge repeats neither the instructions already sent nor the verdict rules", async () => {
@@ -468,7 +487,7 @@ test("the nudge repeats neither the instructions already sent nor the verdict ru
const nudge = sup.userMessages.at(-1)!.content;
assert.doesNotMatch(nudge, /instruction \d/, "the supervisor already has its own steer calls");
assert.match(nudge, new RegExp(`${STEER_MEMORY + 3} instructions so far`), "the count is the cheap part, so it stays");
assert.ok(nudge.length < 400, `the nudge is sent every look, so it stays short: ${nudge.length} chars`);
assert.ok(nudge.length < 600, `the nudge is sent every look, so it stays short: ${nudge.length} chars`);
});
test("a multi-line goal returns to supervisor context every fifth review and after compaction", async () => {
@@ -476,19 +495,22 @@ test("a multi-line goal returns to supervisor context every fifth review and aft
const sup = harness(SUPER_ID);
await sup.start();
await sup.run("supervise", `@worker ${goal}`);
const atPairing = sup.contextMessages.length;
const goalMessages = () => sup.contextMessages.filter(message => message.content.includes("<goal>"));
const atPairing = goalMessages().length;
assert.match(sup.contextMessages.at(-1)!.content, /<goal>\nBuild the causal evaluation\.\nThe full rubric stays here\.\n<\/goal>/);
for (let i = 0; i < 4; i++) {
sup.deliver(WORKER_ID, { t: "view", to: SUPER_ID, view: `view ${i}`, stopped: true });
await new Promise((r) => setTimeout(r, 5));
await sup.finishAssessment();
}
assert.equal(sup.contextMessages.length, atPairing, "the brief already gave the supervisor the full goal");
assert.equal(goalMessages().length, atPairing, "the brief already gave the supervisor the full goal");
sup.deliver(WORKER_ID, { t: "view", to: SUPER_ID, view: "view 4", stopped: true });
await new Promise((r) => setTimeout(r, 5));
assert.match(sup.contextMessages.at(-1)!.content, /<goal>\nBuild the causal evaluation\.\nThe full rubric stays here\.\n<\/goal>/);
await sup.finishAssessment();
const afterReview = sup.contextMessages.length;
await sup.compact();
assert.equal(sup.contextMessages.length, afterReview + 1);
@@ -506,13 +528,15 @@ test("a one-line goal is not redundantly reinserted", async () => {
const sup = harness(SUPER_ID);
await sup.start();
await sup.run("supervise", "@worker fix the parser");
const atPairing = sup.contextMessages.length;
const goalMessages = () => sup.contextMessages.filter(message => message.content.includes("<goal>"));
const atPairing = goalMessages().length;
for (let i = 0; i < 5; i++) {
sup.deliver(WORKER_ID, { t: "view", to: SUPER_ID, view: `view ${i}`, stopped: true });
await new Promise((r) => setTimeout(r, 5));
await sup.finishAssessment();
}
assert.equal(sup.contextMessages.length, atPairing);
assert.equal(goalMessages().length, atPairing);
});
test("a check in and a worker that stopped ask for different things", async () => {
@@ -524,8 +548,9 @@ test("a check in and a worker that stopped ask for different things", async () =
sup.userMessages.length = 0;
sup.deliver(WORKER_ID, { t: "view", to: SUPER_ID, view: "# Goal: g\n", stopped: false });
await new Promise((r) => setTimeout(r, 5));
assert.match(sup.userMessages.at(-1)!.content, /Call let_it_run unless the view gives concrete evidence/);
assert.match(sup.userMessages.at(-1)!.content, /Use let_it_run when on course; steer only when the evidence/);
await sup.finishAssessment();
sup.deliver(WORKER_ID, { t: "view", to: SUPER_ID, view: "# Goal: g\n", stopped: true });
await new Promise((r) => setTimeout(r, 5));
assert.match(sup.userMessages.at(-1)!.content, /The worker stopped/);
@@ -646,7 +671,7 @@ test("let_it_run says the turn is over, so it is not called four times running",
assert.match(result.content[0].text, /completed its verdict for the current worker view/);
// The result must name a way to end the turn. "Say nothing more" named none, and a model that
// may not write text can only call another tool, which is what session 019ffa73 did every look.
assert.match(result.content[0].text, /write one short line or no text/);
assert.match(result.content[0].text, /brief visible assessment/);
assert.doesNotMatch(result.content[0].text, /Say nothing more/);
});
@@ -746,11 +771,8 @@ test("a worker session never has its context rewritten", async () => {
assert.ok(sent.every((m: any) => m.content[0].text.includes("looks like a view")), "nothing was dropped");
});
test("a view that arrives mid-answer starts a fresh look", async () => {
// A view sent while the supervisor is busy is queued as a followUp, and a followUp runs inside
// the agent loop already going, so agent_start does not fire again. Counting per agent run would
// charge the second view for the first, and answer it "already recorded" for a view it has not
// seen. The view branch resets the count, so the new view gets a verdict of its own.
test("a newly presented view starts a fresh look", async () => {
// Counts belong to the presented view, not to how many worker updates arrived while busy.
const sup = harness(SUPER_ID);
await sup.start();
await sup.run("supervise", "@worker make the results table");
@@ -812,7 +834,7 @@ test("a resume onto a live worker keeps supervising, and takes the writers back
assert.match(anchor, /Supervising again/);
assert.match(anchor, /<goal>\ng\n<\/goal>/);
assert.match(anchor, /3 instructions so far/);
assert.match(anchor, /one tool call: steer, done or let_it_run/);
assert.match(anchor, /brief visible assessment and perspective/);
// The strip lives in the /supervise handler, which a resume never runs. Without this the
// supervisor comes back with bash and edit in a directory the worker is writing to.
const back = resumed.pi.getActiveTools();
@@ -837,12 +859,14 @@ test("done unpairs the worker, so it stops publishing views", async () => {
assert.equal(atPairing, 1, "pairing publishes a first view, so the supervisor has something to read");
await worker.settle();
await new Promise(resolve => setTimeout(resolve, 50));
assert.equal(worker.published.filter((p) => p.t === "view").length, 2, "paired worker publishes");
worker.deliver(SUPER_ID, { t: "done", to: WORKER_ID, reason: "results.md line 3" });
await new Promise((r) => setTimeout(r, 5));
await worker.settle();
await new Promise(resolve => setTimeout(resolve, 50));
assert.equal(
worker.published.filter((p) => p.t === "view").length,
2,
@@ -914,6 +938,7 @@ test("done is refused while the worker has an unanswered tool call", async () =>
assert.match(blocked.content[0].text, /still has work running \(subagent\)/);
// Same guard, the other half: a subagent running as its own process leaves no unanswered call.
await sup.finishAssessment();
const detached = buildView({ goal: "finish the sweep", status: "idle", entries: [message("assistant", "all done")], subagents: [4242] });
sup.deliver(WORKER_ID, { t: "view", to: SUPER_ID, view: detached, stopped: true });
await new Promise((r) => setTimeout(r, 5));
@@ -1112,7 +1137,7 @@ test("supervising takes the writing tools away, and stopping gives them back", a
const during = sup.pi.getActiveTools();
assert.deepEqual(
during,
["read", "grep", "list", "worker_view", "set_goal", "steer", "let_it_run", "done"],
["read", "grep", "ls", "worker_view", "set_goal", "steer", "let_it_run", "done"],
"no bash, no edit, no write, and the supervisor tools appear",
);
await sup.run("supervise", "stop");
@@ -1195,7 +1220,8 @@ test("/supervise goal changes the goal without breaking the pairing", async () =
], "the worker holds the copy every view header is built from");
assert.ok(sup.published.some((p) => p.t === "look"), "and a fresh view follows, so it judges now");
assert.match(sup.contextMessages.at(-1)!.content, /changed the goal[\s\S]*quote the evidence file instead/);
assert.match(sup.status.get("intercom-supervisor")!, /watching 1/, "the steer count survives");
assert.match(sup.status.get("intercom-supervisor")!, /waiting for worker overview/);
assert.equal(sup.appended.at(-1)!.data.steerRounds, 1, "the steer count survives");
});
test("the footer says which side of a pairing this session is, and clears when it ends", async () => {
@@ -1262,6 +1288,7 @@ test("the view names the worker's model and how full its context is", async () =
worker.deliver(SUPER_ID, { t: "pair", to: WORKER_ID, goal: "g" });
await new Promise((r) => setTimeout(r, 5));
await worker.settle();
await new Promise(resolve => setTimeout(resolve, 50));
const view = worker.published.find((p) => p.t === "view");
assert.match(view.view, /model: test\/tiny, 12% of its context used/);
@@ -1302,6 +1329,7 @@ test("the supervisor gets a look at a working worker every half hour, without be
assert.match(views[0].view, /^child pi processes still running: /m);
await worker.settle();
await new Promise(resolve => setTimeout(resolve, 50));
const after = worker.published.filter((p) => p.t === "view").length;
t.mock.timers.tick(600_000);
await new Promise((r) => setTimeout(r, 300)); // let any look that did start finish, so it counts
@@ -1323,7 +1351,7 @@ test("a human message in the worker session is not a reason to stand back", asyn
// And on the view that carries a stopped worker, where the excuse actually got used.
sup.deliver(WORKER_ID, { t: "view", to: SUPER_ID, view: "worker view", stopped: true });
await new Promise((r) => setTimeout(r, 5));
assert.match(sup.userMessages[0].content, /human being present does not count as somebody driving it/);
assert.match(sup.userMessages[0].content, /concrete continuation if work remains/);
});
test("letting a stopped worker run says plainly that the worker stays stopped", async () => {
@@ -1340,6 +1368,7 @@ test("letting a stopped worker run says plainly that the worker stays stopped",
const working = await letItRun.execute("id", { reason: "on track" }, undefined, undefined, sup.ctx);
assert.doesNotMatch(working.content[0].text, /does not start again by itself/, "a working worker needs no warning");
await sup.settle();
sup.deliver(WORKER_ID, { t: "view", to: SUPER_ID, view: "job 12 finished", stopped: true });
await new Promise((r) => setTimeout(r, 5));
const stopped = await letItRun.execute("id", { reason: "waiting for the worker to re-queue" }, undefined, undefined, sup.ctx);
@@ -1457,10 +1486,13 @@ test("the worker counts reviews in a row where nothing changed", async () => {
await new Promise((r) => setTimeout(r, 300)); // the pairing view runs ps
await worker.settle();
await new Promise(resolve => setTimeout(resolve, 50));
entries.push(message("assistant", "I will get to that shortly."));
await worker.settle();
await new Promise(resolve => setTimeout(resolve, 50));
entries.push(message("assistant", "Yes, I agree that is the right approach."));
await worker.settle();
await new Promise(resolve => setTimeout(resolve, 50));
// The pairing view is not a review, so it is dropped here and does not count.
const views = worker.published.filter((p) => p.t === "view").slice(1);
@@ -1472,6 +1504,7 @@ test("the worker counts reviews in a row where nothing changed", async () => {
message: { role: "assistant", content: [{ type: "toolCall", id: "w", name: "write", arguments: { path: "results.md" } }] },
});
await worker.settle();
await new Promise(resolve => setTimeout(resolve, 50));
const after = worker.published.filter((p) => p.t === "view").at(-1)!;
assert.doesNotMatch(after.view, /reviews in a row/, "real work must clear the count, not just pause it");
});
@@ -1512,6 +1545,99 @@ test("duplicate standalone Intercom registries are diagnosed and cannot bootstra
const h = harness(WORKER_ID);
await h.start();
h.pi.events.emit(INTERCOM_EXTENSION_REGISTRY_READY_EVENT, { version: 1 });
await assert.rejects(new Promise((resolve, reject) => h.pi.events.emit("pi-supervise:plan:v1", { version: 1, method: "status", resolve, reject })), /Duplicate standalone pi-intercom/);
assert.ok(h.notices.some(note => note.includes("Remove the old companion")));
await assert.rejects(h.controllerStatus(), /Multiple Intercom runtimes/);
assert.ok(h.notices.some(note => note.includes("Keep one Intercom installation")));
});
for (const reloading of ["worker", "supervisor"] as const) test(`retained non-plan supervision reconnects after the ${reloading} reloads, not on unrelated peer traffic`, async () => {
const worker = harness(WORKER_ID);
const supervisor = harness(SUPER_ID);
const sessions = [worker, supervisor];
try {
await worker.start(); await supervisor.start();
await supervisor.run("supervise", "@worker retain this goal");
worker.deliver(SUPER_ID, supervisor.published.find(wire => wire.t === "pair"));
await new Promise(resolve => setTimeout(resolve, 300));
supervisor.deliver(WORKER_ID, worker.published.find(wire => wire.t === "paired"));
await new Promise(resolve => setTimeout(resolve, 5));
const old = reloading === "worker" ? worker : supervisor;
const survivor = reloading === "worker" ? supervisor : worker;
const returningId = reloading === "worker" ? WORKER_ID : SUPER_ID;
const survivorId = reloading === "worker" ? SUPER_ID : WORKER_ID;
survivor.intercomEvent({ type: "session_left", sessionId: returningId });
await old.shutdown();
assert.equal(survivor.status.get("intercom-supervisor"), "supervision disconnected");
const returned = harness(returningId, { entries: old.appended.map(entry => ({ type: "custom", customType: entry.type, data: entry.data })) });
sessions.push(returned);
await returned.start();
await new Promise(resolve => setTimeout(resolve, 5));
const publishedBefore = survivor.published.length;
const messagesBefore = survivor.userMessages.length;
const fresh = reloading === "worker"
? { t: "view", to: survivorId, view: "Fresh evidence from the returning worker", stopped: false }
: { t: "look", to: survivorId };
survivor.intercomEvent({ type: "session_joined", session: { id: "unrelated-peer" } });
survivor.intercomEvent({ type: "session_joined", session: { id: returningId } });
survivor.deliver("unrelated-peer", fresh);
survivor.deliver(returningId, { ...fresh, to: "different-recipient" });
survivor.deliver(returningId, { ...fresh, bindingId: "unrelated-plan" });
await new Promise(resolve => setTimeout(resolve, 5));
assert.equal(survivor.status.get("intercom-supervisor"), "supervision disconnected", "presence and unrelated/misaddressed traffic must not restore the pairing");
assert.equal(survivor.published.length, publishedBefore);
assert.equal(survivor.userMessages.length, messagesBefore);
if (reloading === "worker") {
await assert.rejects(supervisor.tools.get("steer").execute("disconnected", { message: "Must not send yet" }, undefined, undefined, supervisor.ctx), /Worker disconnected/);
await returned.settle(); // The restored worker publishes through its retained relationship.
survivor.deliver(returningId, returned.published.findLast(wire => wire.t === "view"));
await new Promise(resolve => setTimeout(resolve, 5));
assert.match(supervisor.status.get("intercom-supervisor")!, /watching/);
await supervisor.tools.get("steer").execute("reconnected", { message: "Inspect the saved evidence" }, undefined, undefined, supervisor.ctx);
returned.deliver(SUPER_ID, supervisor.published.findLast(wire => wire.t === "directive"));
await new Promise(resolve => setTimeout(resolve, 5));
assert.equal(returned.userMessages.at(-1)?.content, "[supervisor] Inspect the saved evidence");
} else {
const look = returned.published.findLast(wire => wire.t === "look");
assert.ok(look, "the reloaded supervisor asks its retained worker for a view");
worker.deliver(SUPER_ID, look);
await new Promise(resolve => setTimeout(resolve, 300));
const views = worker.published.slice(publishedBefore).filter(wire => wire.t === "view");
assert.equal(views.length, 1, "the surviving worker must answer the validated look despite its previous disconnect flag");
assert.match(worker.status.get("intercom-supervisor")!, /watched/);
returned.deliver(WORKER_ID, views[0]);
await new Promise(resolve => setTimeout(resolve, 5));
assert.equal(returned.userMessages.length, 1);
await returned.tools.get("steer").execute("reconnected", { message: "Continue from the new view" }, undefined, undefined, returned.ctx);
worker.deliver(SUPER_ID, returned.published.findLast(wire => wire.t === "directive"));
await new Promise(resolve => setTimeout(resolve, 5));
assert.equal(worker.userMessages.at(-1)?.content, "[supervisor] Continue from the new view");
}
assert.equal(returned.published.filter(wire => wire.t === "pair" || wire.t === "paired").length, 0, "reload must not need a replacement pairing");
assert.equal((await survivor.controllerStatus()).role, reloading === "worker" ? "supervisor" : "worker");
} finally { for (const session of sessions) await session.shutdown(); }
});
test("busy supervisor retains its active view and requests one complete overview after settling", async () => {
const supervisor = harness(SUPER_ID);
try {
await supervisor.start(); await supervisor.run("supervise", "@worker examine the incremental evidence");
supervisor.deliver(WORKER_ID, { t: "paired", to: SUPER_ID });
supervisor.deliver(WORKER_ID, { t: "view", to: SUPER_ID, view: "active receipt", stopped: false });
await new Promise(resolve => setTimeout(resolve, 5));
for (let n = 1; n <= 5; n++) {
supervisor.deliver(WORKER_ID, { t: "view", to: SUPER_ID, view: `incremental receipt ${n}`, stopped: false });
await new Promise(resolve => setTimeout(resolve, 5));
}
assert.equal(supervisor.userMessages.length, 1, "routine status must not queue five model turns");
const latest = await supervisor.tools.get("worker_view").execute();
assert.equal(latest.content[0].text, "active receipt", "new arrivals cannot overwrite the active assessment");
await supervisor.tools.get("let_it_run").execute("assessed", { reason: "Active receipt is on course" }, undefined, undefined, supervisor.ctx);
const looks = supervisor.published.filter(wire => wire.t === "look").length;
await supervisor.settle();
assert.equal(supervisor.published.filter(wire => wire.t === "look").length, looks + 1);
await supervisor.settle();
assert.equal(supervisor.published.filter(wire => wire.t === "look").length, looks + 1, "only one refresh may be in flight");
} finally { await supervisor.shutdown(); }
});
+648 -19
View File
@@ -5,7 +5,7 @@ import { join } from "node:path";
import test from "node:test";
import { backgroundState } from "../../src/internal/supervisor/background.js";
import extension from "../../src/internal/supervisor/index.js";
import { PLAN_API_EVENT, type PlanBinding, planHash } from "../../src/internal/supervisor/plan-api.js";
import { type SupervisorBinding as PlanBinding, planHash } from "../../src/supervisor.js";
const tick = () => new Promise(resolve => setImmediate(resolve));
function pairHarness() {
@@ -19,13 +19,14 @@ function pairHarness() {
const hooks = new Map<string, any[]>(); const listeners = new Map<string, Set<any>>(); const tools = new Map<string, any>();
const messages: any[] = []; const contexts: any[] = []; const commands = new Map<string, any>();
let active: string[] = ["read", "grep", "bash", "edit", "write"];
const peer: any = { id, messages, contexts, entries, tools, commands, modelReady: true, activeProcesses: 0, activeSubagents: 0, idle: true, compactions: 0, tokens: 50_000, aborts: 0 };
const peer: any = { id, messages, contexts, entries, tools, commands, modelReady: true, activeProcesses: 0, activeSubagents: 0, idle: true, connected: true, compactions: 0, tokens: 50_000, aborts: 0 };
const bus = {
on(name: string, handler: any) { const set = listeners.get(name) ?? new Set(); set.add(handler); listeners.set(name, set); return () => set.delete(handler); },
emit(name: string, value: any) {
if (name === "intercom:extension-register") {
peer.receive = value.onEvent;
value.onReady({ snapshot: () => ({ connected: true, supported: true }), listSessions: async () => peers.map(p => ({ id: p.id, pid: p === peer ? process.pid : process.pid + 1, cwd, model: "test" })), publish(payload: any) {
value.onReady({ snapshot: () => ({ connected: peer.connected, supported: true }), listSessions: async () => { peer.beforeListSessions?.(); return peers.map(p => ({ id: p.id, pid: p === peer ? process.pid : process.pid + 1, cwd, model: "test" })); }, publish(payload: any) {
peer.beforePublish?.(payload);
wire.push({ from: peer.id, ...payload });
for (const p of peers) queueMicrotask(() => p.receive?.({ type: "message", fromSessionId: peer.id, payload }));
} }); return true;
@@ -35,17 +36,235 @@ function pairHarness() {
for (const handler of listeners.get(name) ?? []) handler(value);
},
};
const pi: any = { events: bus, on(name: string, fn: any) { hooks.set(name, [...(hooks.get(name) ?? []), fn]); }, registerTool(tool: any) { tools.set(tool.name, tool); active.push(tool.name); }, registerCommand(name: string, command: any) { commands.set(name, command); }, appendEntry(customType: string, data: any) { entries.push({ type: "custom", customType, data }); }, getActiveTools: () => active, setActiveTools: (names: string[]) => { active = names; }, getAllTools: () => [{ name: "subagent" }], sendMessage: (message: any) => contexts.push(message), sendUserMessage: (text: string, options: any) => messages.push({ text, options }) };
const ctx: any = { cwd, hasUI: true, model: { contextWindow: 200_000 }, isIdle: () => peer.idle, abort: () => { peer.aborts++; }, getContextUsage: () => ({ tokens: peer.tokens }), compact({ onComplete }: any) { peer.compactions++; peer.tokens = 10_000; onComplete({}); }, ui: { notify() {}, setStatus() {}, theme: { fg: (_: any, text: string) => text } }, sessionManager: { getEntries: () => entries, getBranch: () => entries, getSessionFile: () => sessionFile } };
const pi: any = { events: bus, on(name: string, fn: any) { hooks.set(name, [...(hooks.get(name) ?? []), fn]); }, registerTool(tool: any) { tools.set(tool.name, tool); active.push(tool.name); }, registerCommand(name: string, command: any) { commands.set(name, command); }, appendEntry(customType: string, data: any) { entries.push({ type: "custom", customType, data }); }, getActiveTools: () => active, setActiveTools: (names: string[]) => { active = names; }, getAllTools: () => [...["read", "grep", "find", "ls"].map(name => ({ name, sourceInfo: { source: "builtin" } })), { name: "subagent" }], sendMessage: (message: any) => contexts.push(message), sendUserMessage: (text: string, options: any) => messages.push({ text, options }) };
const ctx: any = { cwd, hasUI: true, model: { provider: "native", id: "test", contextWindow: 200_000 }, isIdle: () => peer.idle, abort: () => { peer.aborts++; }, getContextUsage: () => ({ tokens: peer.tokens }), compact({ onComplete }: any) { peer.compactions++; peer.tokens = 10_000; onComplete({}); }, ui: { notify() {}, setStatus() {}, theme: { fg: (_: any, text: string) => text } }, sessionManager: { getEntries: () => entries, getBranch: () => entries, getSessionFile: () => sessionFile } };
peer.pi = pi; peer.ctx = ctx;
peer.hook = async (name: string, event = {}) => { for (const fn of hooks.get(name) ?? []) await fn(event, ctx); };
peer.request = (method: string, params: any = {}) => new Promise<any>((resolve, reject) => bus.emit(PLAN_API_EVENT, { version: 1, method, ...params, resolve, reject }));
peers.push(peer); extension(pi, () => peer.modelReady); return peer;
peer.hook = async (name: string, event = {}) => { let result: any; for (const fn of hooks.get(name) ?? []) result = await fn(event, ctx) ?? result; return result; };
peers.push(peer); const controller = extension(pi, () => peer.modelReady);
peer.controller = controller;
peer.request = (method: string, p: any = {}) => {
switch (method) {
case "prepare": return controller.prepare(p.binding, p.signal);
case "bootstrap": return controller.bootstrap(p, p.signal);
case "status": return controller.status(p.signal);
case "review": return controller.review(p.bindingId, p.goal, p.planHash, p.signal);
case "stop": return controller.stop(p.bindingId);
case "activate": return controller.activate(p.bindingId, p.signal);
case "attached": return controller.attached(p.bindingId, p.signal);
default: throw new Error("Unknown test operation");
}
};
peer.finishAssessment = async () => {
await tools.get("let_it_run").execute("assessed", { reason: "Inspection complete" }, undefined, undefined, ctx);
await peer.hook("agent_settled");
};
peer.review = async (decision = "approve", reason = "Faithful to the plan") => {
await peer.hook("context", { messages: contexts });
const result = await tools.get("review_goal").execute("review", { decision, reason });
await peer.hook("agent_settled");
return result;
};
return peer;
}
const worker = make("worker"); const supervisor = make("supervisor");
return { worker, supervisor, binding, wire, planPath, async restart(peer: any, id: string) { await peer.hook("session_shutdown"); peers.splice(peers.indexOf(peer), 1); const replacement = make(id, structuredClone(peer.entries), peer.ctx.sessionManager.getSessionFile()); await replacement.hook("session_start"); await tick(); return replacement; }, async start() { await worker.hook("session_start"); await supervisor.hook("session_start"); await worker.request("prepare", { binding }); const attached = worker.request("attached", { bindingId: binding.id }); void attached.catch(() => {}); await supervisor.request("bootstrap", { binding, workerId: "worker" }); await attached; }, async close() { for (const peer of peers) await peer.hook("session_shutdown"); rmSync(cwd, { recursive: true, force: true }); } };
}
test("each checkpoint freezes fresh worker evidence and direction without replacing a busy assessment", async () => {
const h = pairHarness(); try {
await h.start();
h.worker.entries.push({ type: "message", message: { role: "user", content: "Pause CLI work until I authorize goal two." } });
await h.worker.controller.activate(h.binding.id); await tick();
const active = (await h.supervisor.tools.get("worker_view").execute()).content[0].text;
const hash = planHash(readFileSync(h.planPath, "utf8"));
for (const goal of ["first", "second"]) {
const direction = `You are now authorized to implement ${goal}; preserve the converter tests.`;
h.worker.entries.push({ type: "message", message: { role: "user", content: direction } });
h.worker.entries.push({ type: "message", message: { role: "assistant", content: `Saved ${goal} test evidence` } });
const pending = h.worker.controller.review(h.binding.id, goal, hash); void pending.catch(() => {}); await tick();
const request = h.wire.filter((w: any) => w.t === "goal_review").at(-1);
assert.equal(typeof request.view, "string");
assert.ok(request.view.includes(direction));
assert.match(request.view, new RegExp(`Saved ${goal} test evidence`));
if (goal === "first") {
assert.equal((await h.supervisor.tools.get("worker_view").execute()).content[0].text, active);
h.worker.entries.push({ type: "message", message: { role: "user", content: "Direction after the snapshot was frozen" } });
await h.supervisor.finishAssessment(); await tick();
}
assert.equal((await h.supervisor.tools.get("worker_view").execute()).content[0].text, request.view);
assert.ok(h.supervisor.contexts.at(-1).content.includes(request.view), "snapshot is presented with its checkpoint, not just hidden in a tool");
await h.supervisor.receive({ type: "message", fromSessionId: "worker", payload: { t: "view", to: "supervisor", bindingId: h.binding.id, view: "A later routine update", stopped: false } });
assert.equal((await h.supervisor.tools.get("worker_view").execute()).content[0].text, request.view);
const context = await h.supervisor.hook("context", { messages: h.supervisor.contexts });
assert.ok(context.messages[0].content.includes(readFileSync(h.planPath, "utf8")), "canonical plan remains whole and separate");
await h.supervisor.review(goal === "first" ? "needs_work" : "approve");
assert.equal((await pending).goal, goal);
assert.equal(Object.hasOwn(h.wire.filter((w: any) => w.t === "goal_decision").at(-1), "view"), false);
await tick();
// Drain any routine overview before the next explicit checkpoint.
await h.supervisor.finishAssessment(); await tick();
}
} finally { await h.close(); }
});
for (const cancellation of ["abort", "stop", "reload", "plan change"] as const) test(`checkpoint snapshot building cannot publish after ${cancellation}`, async () => {
const h = pairHarness(); try {
await h.start();
const emit = h.worker.pi.events.emit;
let release!: () => void;
h.worker.pi.events.emit = (name: string, value: any) => {
if (name === "subagents:rpc:v1:request") { release = () => emit(name, value); return; }
return emit(name, value);
};
const abort = new AbortController();
const pending = h.worker.controller.review(h.binding.id, "first", planHash(readFileSync(h.planPath, "utf8")), abort.signal);
const rejected = assert.rejects(pending, /cancelled|stopped|reloaded|ended|changed/i);
await tick(); assert.equal(typeof release, "function", "snapshot consults tracked work before publishing");
if (cancellation === "abort") abort.abort();
if (cancellation === "stop") await h.worker.controller.stop(h.binding.id);
if (cancellation === "reload") await h.worker.hook("session_shutdown");
if (cancellation === "plan change") writeFileSync(h.planPath, "Changed plan while snapshot was being built");
if (cancellation !== "plan change") await rejected; // cancellation is prompt, not blocked on a tracker
release(); await rejected; await tick();
assert.equal(h.wire.filter((w: any) => w.t === "goal_review").length, 0);
} finally { await h.close(); }
});
test("checkpoint capture includes user direction arriving while tracked work is queried", async () => {
const h = pairHarness(); try {
await h.start();
const emit = h.worker.pi.events.emit;
let release!: () => void;
h.worker.pi.events.emit = (name: string, value: any) => {
if (name === "subagents:rpc:v1:request") { release = () => emit(name, value); return; }
return emit(name, value);
};
const pending = h.worker.controller.review(h.binding.id, "second", planHash(readFileSync(h.planPath, "utf8"))); void pending.catch(() => {}); await tick();
h.worker.entries.push({ type: "message", message: { role: "user", content: "Latest authorization arrived during the tracker query." } });
release(); await tick();
const request = h.wire.find((w: any) => w.t === "goal_review");
assert.match(request.view, /Latest authorization arrived during the tracker query/);
await h.supervisor.review(); assert.equal((await pending).decision, "approve");
} finally { await h.close(); }
});
test("checkpoint payload fits the serialized channel limit without truncating its identity", async () => {
const h = pairHarness(); try {
await h.start();
h.worker.entries.push({ type: "message", message: { role: "user", content: "Latest authorization: implement goal two." } });
for (let n = 0; n < 100; n++) h.worker.entries.push({ type: "message", message: { role: "assistant", content: `Evidence ${n}: ${'\\"'.repeat(200)}` } });
const goal = `second ${"g".repeat(10_000)}`;
const pending = h.worker.controller.review(h.binding.id, goal, planHash(readFileSync(h.planPath, "utf8")));
void pending.catch(() => {}); await tick();
const request = h.wire.filter((w: any) => w.t === "goal_review").at(-1);
const { from: _from, ...payload } = request;
assert.equal(request.goal, goal);
assert.ok(Buffer.byteLength(JSON.stringify(payload)) <= 16 * 1024);
assert.match(request.view, /Latest authorization: implement goal two/);
assert.match(request.view, /cut|truncated/);
await h.supervisor.review(); assert.equal((await pending).decision, "approve");
await assert.rejects(h.worker.controller.review(h.binding.id, "x".repeat(17_000), request.planHash), /channel|16 KiB|too large/i);
assert.equal(h.wire.filter((w: any) => w.t === "goal_review").length, 1, "oversized identity fails locally, not a silent broker drop");
} finally { await h.close(); }
});
for (const content of [[], [{ type: "text", text: "" }], [{ type: "text", text: " \n " }], [{ type: "thinking", thinking: "Synthetic private block" }]]) test(`settled empty final response fails the checkpoint explicitly (${JSON.stringify(content.map(b => b.type))})`, async () => {
const h = pairHarness(); try {
await h.start(); const notices: string[] = [];
h.supervisor.ctx.ui.notify = (message: string) => notices.push(message);
const pending = h.worker.controller.review(h.binding.id, "first", planHash(readFileSync(h.planPath, "utf8"))); void pending.catch(() => {}); await tick();
await h.supervisor.hook("agent_end", { messages: [{ role: "assistant", stopReason: "stop", content }] });
assert.equal(h.wire.filter((w: any) => w.t === "goal_decision").length, 0, "agent_end alone cannot rule out native continuation");
await h.supervisor.hook("agent_settled"); await tick();
const decision = h.wire.find((w: any) => w.t === "goal_decision");
assert.equal(decision?.decision, "needs_work", "empty final cannot leave the worker pending");
assert.match((await pending).reason, /empty|incomplete/i);
assert.ok(notices.some(notice => /empty|incomplete/i.test(notice)));
assert.equal(Object.hasOwn(decision, "view"), false);
assert.equal(h.wire.filter((w: any) => w.t === "pair").length, 1);
} finally { await h.close(); }
});
for (const stopReason of ["stop", "error"]) test(`failed assessment resumes on later worker progress without a human poke (${stopReason})`, async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.controller.activate(h.binding.id); await tick();
const view = (text: string) => ({ type: "message", fromSessionId: "worker", payload: { t: "view", to: "supervisor", bindingId: h.binding.id, view: text, stopped: false } });
await h.supervisor.receive(view("Progress while the assessment is busy")); await tick();
const messages = h.supervisor.messages.length;
const looks = h.wire.filter((w: any) => w.t === "look").length;
await h.supervisor.hook("agent_end", { messages: [{ role: "assistant", stopReason, content: [] }] });
for (let n = 0; n < 3; n++) { await h.supervisor.hook("agent_settled"); await tick(); }
assert.equal(h.supervisor.messages.length, messages, "failure must not start a same-input retry loop");
assert.equal(h.wire.filter((w: any) => w.t === "look").length, looks);
await h.supervisor.receive(view("New worker progress after the failure")); await tick();
assert.equal(h.supervisor.messages.length, messages + 1, "ordinary worker progress must resume supervision without user input");
assert.match(h.supervisor.messages.at(-1)!.text, /New worker progress after the failure/);
} finally { await h.close(); }
});
test("empty low-level response may continue through compaction, ask a human, or finish with a tool verdict", async () => {
const h = pairHarness(); try {
await h.start();
const pending = h.worker.controller.review(h.binding.id, "first", planHash(readFileSync(h.planPath, "utf8"))); void pending.catch(() => {}); await tick();
const empty = { messages: [{ role: "assistant", stopReason: "stop", content: [{ type: "text", text: "" }] }] };
await h.supervisor.hook("agent_end", empty);
await h.supervisor.hook("session_compact", { willRetry: true });
assert.equal(h.wire.filter((w: any) => w.t === "goal_decision").length, 0);
await h.supervisor.hook("agent_end", { messages: [{ role: "assistant", stopReason: "stop", content: [{ type: "text", text: "Should we keep the output format?" }] }] });
await h.supervisor.hook("agent_settled"); await tick();
assert.equal(h.wire.filter((w: any) => w.t === "goal_decision").length, 0, "nonempty human question still waits");
await h.supervisor.hook("context", { messages: h.supervisor.contexts });
await h.supervisor.tools.get("review_goal").execute("approved", { decision: "approve", reason: "The user confirmed the format" });
await h.supervisor.hook("agent_end", empty);
await h.supervisor.hook("agent_settled"); await tick();
assert.equal((await pending).decision, "approve");
assert.equal(h.wire.filter((w: any) => w.t === "goal_decision").length, 1);
} finally { await h.close(); }
});
test("a successful goal tool verdict is not undone by an empty final response", async () => {
const h = pairHarness(); try {
await h.start(); const notices: string[] = [];
h.supervisor.ctx.ui.notify = (message: string) => notices.push(message);
const pending = h.worker.controller.review(h.binding.id, "first", planHash(readFileSync(h.planPath, "utf8"))); void pending.catch(() => {}); await tick();
await h.supervisor.hook("context", { messages: h.supervisor.contexts });
await h.supervisor.tools.get("review_goal").execute("approved", { decision: "approve", reason: "Evidence matches scope" });
await h.supervisor.hook("agent_end", { messages: [{ role: "assistant", stopReason: "stop", content: [] }] });
await h.supervisor.hook("agent_settled"); await tick();
assert.equal((await pending).decision, "approve");
assert.equal(h.wire.filter((w: any) => w.t === "goal_decision").length, 1);
assert.equal(notices.some(notice => /incomplete|waiting for user/i.test(notice)), false);
} finally { await h.close(); }
});
test("a duplicate checkpoint rejection never echoes its snapshot or changes the active view", async () => {
const h = pairHarness(); try {
await h.start();
const pending = h.worker.controller.review(h.binding.id, "first", planHash(readFileSync(h.planPath, "utf8"))); void pending.catch(() => {}); await tick();
const request = h.wire.find((w: any) => w.t === "goal_review");
const view = (await h.supervisor.tools.get("worker_view").execute()).content[0].text;
await h.supervisor.receive({ type: "message", fromSessionId: "worker", payload: { ...request, requestId: "duplicate", view: "Do not replace the active view" } });
await tick();
const rejected = h.wire.find((w: any) => w.t === "goal_decision");
assert.equal(rejected.requestId, "duplicate");
assert.equal(Object.hasOwn(rejected, "view"), false);
assert.equal((await h.supervisor.tools.get("worker_view").execute()).content[0].text, view);
await h.supervisor.review(); assert.equal((await pending).decision, "approve");
} finally { await h.close(); }
});
test("an empty routine assessment cannot fail a separately queued checkpoint", async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.controller.activate(h.binding.id); await tick();
const pending = h.worker.controller.review(h.binding.id, "first", planHash(readFileSync(h.planPath, "utf8"))); void pending.catch(() => {}); await tick();
await h.supervisor.hook("agent_end", { messages: [{ role: "assistant", stopReason: "stop", content: [] }] });
await h.supervisor.hook("agent_settled"); await tick();
assert.equal(h.wire.filter((w: any) => w.t === "goal_decision").length, 0);
assert.equal(h.supervisor.contexts.filter((m: any) => m.customType === "supervisor_checkpoint").length, 1);
await h.supervisor.review(); assert.equal((await pending).decision, "approve");
} finally { await h.close(); }
});
test("plan bootstrap compacts only the supervisor and pairing alone never starts a worker or a review", async () => {
const h = pairHarness(); try {
await h.start(); assert.equal(h.worker.compactions, 0); assert.equal(h.supervisor.compactions, 1);
@@ -62,10 +281,10 @@ test("goal decisions are correlated, preserve the pair across two goals, and can
await h.start(); const hash = planHash(readFileSync(h.planPath, "utf8"));
for (const goal of ["first", "second"]) {
const result = h.worker.request("review", { bindingId: h.binding.id, goal, planHash: hash }); await tick();
const req = h.wire.findLast((w: any) => w.t === "goal_review");
await assert.rejects(h.supervisor.tools.get("done").execute("", { reason: "done" }, undefined, undefined, h.supervisor.ctx), /Open plan goals/);
await assert.rejects(h.supervisor.tools.get("review_goal").execute("", { requestId: "stale", decision: "approve", reason: "ok" }), /No matching/);
await h.supervisor.tools.get("review_goal").execute("", { requestId: req.requestId, decision: "approve", reason: "Faithful to the plan" });
await assert.rejects(h.supervisor.tools.get("review_goal").execute("", { decision: "approve", reason: "Not presented yet" }), /No matching/);
await h.supervisor.review();
assert.equal((await result).goal, goal); assert.equal((await h.worker.request("status")).connected, true);
}
assert.equal(h.wire.filter((w: any) => w.t === "done").length, 0);
@@ -94,7 +313,7 @@ test("50 actual model turns trigger one view, independent of the number of messa
});
test("unknown background providers are not proof of quiescence", async () => {
const result = await backgroundState({ events: { emit() {} }, getAllTools: () => [] });
const result = await backgroundState({ events: { emit() {} }, getAllTools: () => [{ name: "process" }] });
assert.equal(result.quiet, false); assert.match(result.description, /unknown/);
});
@@ -102,9 +321,9 @@ test("stale plan content invalidates a pending goal review", async () => {
const h = pairHarness(); try {
await h.start(); const abort = new AbortController();
const pending = h.worker.request("review", { bindingId: h.binding.id, goal: "first", planHash: planHash(readFileSync(h.planPath, "utf8")), signal: abort.signal });
const rejected = assert.rejects(pending, /cancelled/); await tick();
const req = h.wire.findLast((w: any) => w.t === "goal_review"); writeFileSync(h.planPath, "Human changed the requirement");
await assert.rejects(h.supervisor.tools.get("review_goal").execute("", { requestId: req.requestId, decision: "approve", reason: "ok" }), /Plan changed/);
const rejected = assert.rejects(pending, /Plan changed/); await tick();
writeFileSync(h.planPath, "Human changed the requirement");
await assert.rejects(h.supervisor.review(), /Plan changed/);
abort.abort(); await rejected;
} finally { await h.close(); }
});
@@ -122,6 +341,25 @@ test("small forks skip compaction, but real compaction failure prevents pairing"
} finally { await failed.close(); }
});
for (const tokens of [50_000, null]) test(`native small-history result permits startup after an attempted compaction (${tokens})`, async () => {
const h = pairHarness(); const notices: string[] = [];
try {
h.supervisor.tokens = tokens;
h.supervisor.ctx.ui.notify = (message: string) => notices.push(message);
h.supervisor.ctx.compact = ({ onError }: any) => {
h.supervisor.compactions++;
onError(new Error("Nothing to compact (session too small)"));
};
await h.start();
assert.equal(h.supervisor.compactions, 1, "unknown or larger context must still attempt native compaction");
assert.equal(h.worker.compactions, 0);
assert.equal((await h.worker.request("status")).connected, true);
assert.equal(h.worker.messages.length, 0, "pairing does not start work before activation");
assert.ok(notices.some(message => /no older history eligible/i.test(message)));
assert.ok(h.supervisor.contexts.some((message: any) => message.content.includes('literal "[x]"')));
} finally { await h.close(); }
});
test("the hour timer and a coincident turn checkpoint produce a single view", async (t) => {
t.mock.timers.enable({ apis: ["Date", "setInterval", "setTimeout"] });
const h = pairHarness(); try {
@@ -171,9 +409,10 @@ test("a restarted worker reconnects by exact saved session identity without a ne
assert.equal((await reloaded.request("status")).connected, true);
assert.equal(h.wire.filter((w: any) => w.t === "pair").length, 1);
assert.equal(h.supervisor.compactions, 1);
await h.supervisor.finishAssessment();
const pending = reloaded.request("review", { bindingId: h.binding.id, goal: "first", planHash: planHash(readFileSync(h.planPath, "utf8")) }); await tick();
const req = h.wire.findLast((w: any) => w.t === "goal_review");
await h.supervisor.tools.get("review_goal").execute("", { requestId: req.requestId, decision: "approve", reason: "Same plan" });
await h.supervisor.review();
assert.equal((await pending).decision, "approve");
} finally { await h.close(); }
});
@@ -197,8 +436,9 @@ test("null post-compaction usage cannot raise the next configured 100k checkpoin
await h.start();
await h.worker.request("activate", { bindingId: h.binding.id }); await tick();
assert.equal(h.supervisor.compactions, 1);
await h.supervisor.finishAssessment();
h.supervisor.tokens = 100_001;
await h.supervisor.receive({ type: "message", fromSessionId: "worker", payload: { t: "view", to: "supervisor", view: "New work since compaction", stopped: false } });
await h.supervisor.receive({ type: "message", fromSessionId: "worker", payload: { t: "view", to: "supervisor", bindingId: h.binding.id, view: "New work since compaction", stopped: false } });
await tick();
assert.equal(h.supervisor.compactions, 2);
} finally { await h.close(); }
@@ -208,9 +448,10 @@ test("stopping a routine view during compaction invalidates its suspended contin
const h = pairHarness(); let finish!: () => void;
try {
await h.start(); await h.worker.request("activate", { bindingId: h.binding.id }); await tick();
await h.supervisor.finishAssessment();
h.supervisor.tokens = 150_000;
h.supervisor.ctx.compact = ({ onComplete }: any) => { finish = onComplete; };
h.supervisor.receive({ type: "message", fromSessionId: "worker", payload: { t: "view", to: "supervisor", view: "Must not restart the stopped supervisor", stopped: false } });
h.supervisor.receive({ type: "message", fromSessionId: "worker", payload: { t: "view", to: "supervisor", bindingId: h.binding.id, view: "Must not restart the stopped supervisor", stopped: false } });
await tick(); assert.equal(typeof finish, "function");
await h.worker.request("stop", { bindingId: h.binding.id }); await tick();
const messages = h.supervisor.messages.length;
@@ -326,3 +567,391 @@ test("model-unavailable stop validates binding, cancels pending reviews and igno
await tick(); assert.equal(h.worker.messages.length, before);
} finally { await h.close(); }
});
test("supervisor mode is a native inspection allowlist at visibility and execution, including reload and stopped forks", async () => {
const h = pairHarness(); try {
const mutations = ["bash", "write", "edit", "process", "subagent", "schedule", "intercom", "mcp_execute", "search_tools", "unknown_tool"];
h.supervisor.pi.setActiveTools([...h.supervisor.pi.getActiveTools(), ...mutations]);
await h.start();
for (const toolName of mutations) {
assert.ok(!h.supervisor.pi.getActiveTools().includes(toolName), toolName);
h.supervisor.pi.setActiveTools([...h.supervisor.pi.getActiveTools(), toolName]);
const blocked = await h.supervisor.hook("tool_call", { toolName, input: {} });
assert.equal(blocked?.block, true, `${toolName} must be blocked even if re-enabled by another extension`);
}
assert.equal((await h.supervisor.hook("tool_call", { toolName: "read", input: {} }))?.block, undefined);
const nativeTools = h.supervisor.pi.getAllTools;
h.supervisor.pi.getAllTools = () => [{ name: "read", sourceInfo: { source: "extension" } }];
assert.equal((await h.supervisor.hook("tool_call", { toolName: "read", input: {} }))?.block, true, "a custom override is not a native reader");
h.supervisor.pi.getAllTools = nativeTools;
const reloaded = await h.restart(h.supervisor, "supervisor-reloaded");
assert.equal((await reloaded.hook("tool_call", { toolName: "subagent", input: {} }))?.block, true);
assert.equal((await reloaded.hook("user_bash", { command: "touch never" }))?.result.exitCode, 1);
await h.worker.request("stop", { bindingId: h.binding.id }); await tick();
assert.equal((await reloaded.hook("tool_call", { toolName: "write", input: {} }))?.block, true);
} finally { await h.close(); }
});
test("a cancelled checkpoint's delayed verdict cannot approve the replacement checkpoint", async () => {
const h = pairHarness(); try {
await h.start();
const hash = planHash(readFileSync(h.planPath, "utf8"));
const abort = new AbortController();
const old = h.worker.controller.review(h.binding.id, "first", hash, abort.signal);
const cancelled = assert.rejects(old, /cancelled/); await tick();
await h.supervisor.hook("context", { messages: [...h.supervisor.contexts] });
abort.abort(); await cancelled; await tick();
await h.supervisor.hook("agent_settled");
const next = h.worker.controller.review(h.binding.id, "second", hash); await tick();
await assert.rejects(h.supervisor.tools.get("review_goal").execute("late", { decision: "approve", reason: "Old assessment" }), /No matching/);
await h.supervisor.review();
assert.equal((await next).goal, "second");
assert.equal(h.wire.filter((w: any) => w.t === "goal_decision").length, 1);
} finally { await h.close(); }
});
test("each model call reanchors the canonical plan and role without losing compacted planning context or judgments", async () => {
const h = pairHarness(); try {
await h.start();
const history = [{ role: "compactionSummary", summary: "User requires literal [x]; avoid global changes" }, { role: "assistant", content: [{ type: "text", text: "Earlier judgment: test boundary conditions" }] }];
writeFileSync(h.planPath, "# Current plan\n1. [/] goal: first\nEvidence: new receipt");
const result = await h.supervisor.hook("context", { messages: history });
assert.match(result.messages[0].content, /You are the supervisor, not the worker/);
assert.match(result.messages[0].content, /Evidence: new receipt/);
assert.deepEqual(result.messages.slice(1), history);
} finally { await h.close(); }
});
test("routine assessments and steering display the actual advice rather than only a receipt", async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.controller.activate(h.binding.id); await tick();
await h.supervisor.tools.get("let_it_run").execute("look", { reason: "Parser tests now pass; keep the remaining change focused on recovery." }, undefined, undefined, h.supervisor.ctx);
await h.supervisor.tools.get("steer").execute("steer", { message: "Check reload before claiming completion; the previous view has no restart evidence." }, undefined, undefined, h.supervisor.ctx);
assert.ok(h.supervisor.contexts.some((m: any) => m.display && m.content.includes("Parser tests now pass")));
assert.ok(h.supervisor.contexts.some((m: any) => m.display && m.content.includes("Check reload before claiming completion")));
await tick(); assert.match(h.worker.messages.at(-1).text, /Check reload before claiming completion/);
} finally { await h.close(); }
});
test("current context above 100k compacts; cumulative usage and exactly 100k do not", async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.controller.activate(h.binding.id); await tick();
await h.supervisor.finishAssessment();
h.supervisor.entries.push({ type: "message", message: { role: "assistant", usage: { totalTokens: 900_000 } } });
const view = () => h.supervisor.receive({ type: "message", fromSessionId: "worker", payload: { t: "view", to: "supervisor", bindingId: h.binding.id, view: "Current progress", stopped: false } });
h.supervisor.tokens = 100_000; view(); await tick(); assert.equal(h.supervisor.compactions, 1);
await h.supervisor.finishAssessment();
h.supervisor.tokens = 100_001; view(); await tick(); assert.equal(h.supervisor.compactions, 2);
} finally { await h.close(); }
});
test("absent optional trackers count as zero tracked work; installed failed or busy trackers remain non-quiet", async () => {
assert.equal((await backgroundState({ events: { emit() {} }, getAllTools: () => [] })).quiet, true);
for (const rows of [undefined, [{ status: "running" }], [{ status: "unexpected" }]]) {
const status = await backgroundState({ events: { emit(_name: string, request: any) { if (rows) request.reply(rows); } }, getAllTools: () => [{ name: "process" }] });
assert.equal(status.quiet, false);
}
});
test("a settled worker with no optional trackers sends one review, not repeated idle wakes", async (t) => {
t.mock.timers.enable({ apis: ["Date", "setInterval", "setTimeout"] });
const h = pairHarness(); try {
h.worker.pi.getAllTools = () => [];
await h.start(); await h.worker.controller.activate(h.binding.id); await tick();
const count = h.wire.filter((w: any) => w.t === "view").length;
t.mock.timers.tick(2_000); await h.worker.hook("agent_settled"); await tick();
assert.equal(h.wire.filter((w: any) => w.t === "view").length, count + 1);
t.mock.timers.tick(30_000); await tick();
assert.equal(h.wire.filter((w: any) => w.t === "view").length, count + 1);
} finally { await h.close(); t.mock.timers.reset(); }
});
test("disconnect cancels a checkpoint and blocks steering; local stop still clears ownership", async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.controller.activate(h.binding.id); await tick();
const pending = h.worker.controller.review(h.binding.id, "first", planHash(readFileSync(h.planPath, "utf8")));
const rejected = assert.rejects(pending, /disconnected/); await tick();
h.worker.connected = false;
h.worker.receive({ type: "connection", connected: false, supported: true }); await rejected;
h.supervisor.receive({ type: "session_left", sessionId: "worker" }); await tick();
await assert.rejects(h.supervisor.tools.get("steer").execute("steer", { message: "Do not claim delivery" }, undefined, undefined, h.supervisor.ctx), /Worker disconnected/);
await assert.rejects(h.worker.controller.stop(h.binding.id), /Stopped locally/);
assert.equal(h.worker.entries.at(-1).data.role, "none");
} finally { await h.close(); }
});
test("registered malformed subagent tracker stays unknown even when the process tracker is absent", async () => {
let reply: (value: any) => void = () => {};
const result = await backgroundState({
getAllTools: () => [{ name: "subagent" }],
events: { on(_event: string, fn: any) { reply = fn; return () => {}; }, emit(event: string, value: any) { if (event === "subagents:rpc:v1:request") reply({ requestId: value.requestId, success: true, data: { fleet: { version: 1, totalActive: "not a count" } } }); } },
});
assert.equal(result.quiet, false);
assert.match(result.description, /processes: 0; subagents: unknown/);
});
test("a busy supervisor defers the 100k compaction until its own run settles", async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.controller.activate(h.binding.id); await tick();
h.supervisor.tokens = 100_001; h.supervisor.idle = false;
h.supervisor.receive({ type: "message", fromSessionId: "worker", payload: { t: "view", to: "supervisor", bindingId: h.binding.id, view: "Incremental work", stopped: false } });
await tick(); assert.equal(h.supervisor.compactions, 1);
h.supervisor.idle = true;
await h.supervisor.hook("agent_settled");
assert.equal(h.supervisor.compactions, 2);
} finally { await h.close(); }
});
test("a healthy goal review can take longer than ten minutes without cancellation or another pairing", async (t) => {
t.mock.timers.enable({ apis: ["setTimeout", "Date"] });
const h = pairHarness(); try {
await h.start();
const hash = planHash(readFileSync(h.planPath, "utf8"));
const pending = h.worker.controller.review(h.binding.id, "first", hash);
let failure: unknown; void pending.catch(error => { failure = error; });
await tick(); await h.supervisor.hook("context", { messages: h.supervisor.contexts });
h.supervisor.idle = false;
t.mock.timers.tick(3_600_000); await tick();
assert.equal(failure, undefined);
assert.equal(h.supervisor.aborts, 0);
assert.equal(h.wire.filter((wire: any) => wire.t === "goal_cancel").length, 0);
await h.supervisor.review();
assert.equal((await pending).decision, "approve");
assert.equal(h.wire.filter((wire: any) => wire.t === "pair").length, 1);
assert.equal(h.supervisor.compactions, 1);
} finally { await h.close(); t.mock.timers.reset(); }
});
test("busy plan supervisor refreshes cumulative VCC evidence without an idle feedback loop", async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.controller.activate(h.binding.id); await tick();
const active = (await h.supervisor.tools.get("worker_view").execute()).content[0].text;
h.supervisor.idle = false;
for (const text of ["First intervening evidence", "Second intervening evidence"]) {
h.worker.entries.push({ type: "message", message: { role: "assistant", content: [{ type: "text", text }] } });
for (let n = 0; n < 50; n++) await h.worker.hook("turn_end");
await tick();
}
assert.equal(h.supervisor.messages.length, 1);
assert.equal((await h.supervisor.tools.get("worker_view").execute()).content[0].text, active);
await h.supervisor.tools.get("let_it_run").execute("finish", { reason: "Initial handoff is on course" }, undefined, undefined, h.supervisor.ctx);
h.supervisor.idle = true; await h.supervisor.hook("agent_settled"); await tick();
assert.equal(h.supervisor.messages.length, 2);
assert.match(h.supervisor.messages[1].text, /First intervening evidence/);
assert.match(h.supervisor.messages[1].text, /Second intervening evidence/);
const count = h.wire.filter((wire: any) => wire.t === "look").length;
await h.supervisor.tools.get("let_it_run").execute("finish-next", { reason: "Both receipts are consistent" }, undefined, undefined, h.supervisor.ctx);
await h.supervisor.hook("agent_settled"); await tick();
assert.equal(h.wire.filter((wire: any) => wire.t === "look").length, count);
assert.equal(h.supervisor.messages.length, 2);
} finally { await h.close(); }
});
test("progress during a refresh remains pending with only one look in flight", async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.controller.activate(h.binding.id); await tick();
const receive = h.worker.receive; const looks: any[] = [];
h.worker.receive = (event: any) => { if (event.payload?.t === "look") looks.push(event); else receive(event); };
const progress = async (text: string) => {
h.worker.entries.push({ type: "message", message: { role: "assistant", content: [{ type: "text", text }] } });
for (let n = 0; n < 50; n++) await h.worker.hook("turn_end"); await tick();
};
await progress("Before the refresh request");
await h.supervisor.finishAssessment(); await tick(); assert.equal(looks.length, 1);
await progress("While the complete overview is in flight");
await h.supervisor.hook("agent_settled"); await tick(); assert.equal(looks.length, 1);
receive(looks[0]); await tick();
assert.match(h.supervisor.messages.at(-1).text, /Before the refresh request/);
assert.match(h.supervisor.messages.at(-1).text, /While the complete overview is in flight/);
await h.supervisor.finishAssessment(); await tick(); assert.equal(looks.length, 2);
receive(looks[1]); await tick(); await h.supervisor.finishAssessment(); await tick();
assert.equal(looks.length, 2, "no subsequent progress means no refresh feedback loop");
} finally { await h.close(); }
});
for (const change of ["compaction", "rewind", "reload"] as const) test(`complete refresh uses the current worker branch after ${change}`, async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.controller.activate(h.binding.id); await tick();
h.worker.entries.push({ type: "message", message: { role: "assistant", content: [{ type: "text", text: "Pre-change evidence" }] } });
for (let n = 0; n < 50; n++) await h.worker.hook("turn_end"); await tick();
if (change === "compaction") h.worker.entries.push({ type: "compaction", summary: "Retained worker compaction evidence" });
if (change === "rewind") h.worker.entries.splice(0, h.worker.entries.length, ...h.worker.entries.filter((entry: any) => entry.type === "custom"));
h.worker.entries.push({ type: "message", message: { role: "assistant", content: [{ type: "text", text: "Current branch evidence" }] } });
if (change === "reload") {
h.supervisor.receive({ type: "session_left", sessionId: "worker" });
await h.restart(h.worker, "returned-worker"); await tick();
} else { await h.supervisor.finishAssessment(); await tick(); }
const view = h.supervisor.messages.at(-1).text;
assert.match(view, /Current branch evidence/);
if (change === "compaction") assert.match(view, /Retained worker compaction evidence/);
if (change === "rewind") assert.doesNotMatch(view, /Pre-change evidence/);
assert.equal(h.wire.filter((wire: any) => wire.t === "pair").length, 1);
} finally { await h.close(); }
});
test("explicit checkpoints wait separately from routine coalescing and do not replace an active assessment", async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.controller.activate(h.binding.id); await tick();
const active = (await h.supervisor.tools.get("worker_view").execute()).content[0].text;
const pending = h.worker.controller.review(h.binding.id, "first", planHash(readFileSync(h.planPath, "utf8")));
await tick();
assert.equal(h.supervisor.contexts.filter((message: any) => message.customType === "supervisor_checkpoint").length, 0);
assert.equal((await h.supervisor.tools.get("worker_view").execute()).content[0].text, active);
await assert.rejects(h.supervisor.tools.get("review_goal").execute("unpresented", { decision: "approve", reason: "Not yet shown" }), /No matching/);
await h.supervisor.finishAssessment(); await tick();
assert.equal(h.supervisor.contexts.filter((message: any) => message.customType === "supervisor_checkpoint").length, 1);
await h.supervisor.review("needs_user", "The user must choose the output format");
assert.equal((await pending).decision, "needs_user");
const looks = h.wire.filter((wire: any) => wire.t === "look").length;
h.supervisor.receive({ type: "message", fromSessionId: "worker", payload: { t: "view", to: "supervisor", bindingId: h.binding.id, view: "More routine status", stopped: false } });
await h.supervisor.hook("agent_settled"); await tick();
assert.equal(h.wire.filter((wire: any) => wire.t === "look").length, looks, "awaiting a user decision must not wake routine reviews");
assert.equal(h.supervisor.messages.length, 1);
} finally { await h.close(); }
});
test("actual supervisor provider failure is returned explicitly without another pair or an elapsed-time cancellation", async () => {
const h = pairHarness(); try {
await h.start();
const pending = h.worker.controller.review(h.binding.id, "first", planHash(readFileSync(h.planPath, "utf8"))); await tick();
await h.supervisor.hook("agent_end", { messages: [{ role: "assistant", stopReason: "error", errorMessage: "Provider unavailable" }] });
await h.supervisor.hook("agent_settled"); await tick();
const result = await pending;
assert.equal(result.decision, "needs_work"); assert.match(result.reason, /Provider unavailable/);
assert.equal(h.wire.filter((wire: any) => wire.t === "pair").length, 1);
assert.equal(h.wire.filter((wire: any) => wire.t === "goal_cancel").length, 0);
} finally { await h.close(); }
});
test("a provider error followed by native retry success does not cancel the supervisor checkpoint", async () => {
const h = pairHarness(); try {
await h.start();
const pending = h.worker.controller.review(h.binding.id, "first", planHash(readFileSync(h.planPath, "utf8"))); await tick();
await h.supervisor.hook("agent_end", { messages: [{ role: "assistant", stopReason: "error", errorMessage: "Retryable provider error" }] });
await tick();
assert.equal(h.wire.filter((wire: any) => wire.t === "goal_decision" || wire.t === "goal_cancel").length, 0);
await h.supervisor.hook("agent_end", { messages: [{ role: "assistant", stopReason: "toolUse" }] });
await h.supervisor.review();
assert.equal((await pending).decision, "approve");
assert.equal(h.supervisor.aborts, 0);
} finally { await h.close(); }
});
for (const retry of ["routine progress", "explicit look"] as const) test(`a failed full overview waits without spinning and recovers on ${retry}`, async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.controller.activate(h.binding.id); await tick();
h.worker.entries.push({ type: "message", message: { role: "assistant", content: [{ type: "text", text: "Evidence before publication failed" }] } });
for (let n = 0; n < 50; n++) await h.worker.hook("turn_end"); await tick();
let attempts = 0; let failing = true;
h.worker.beforePublish = (payload: any) => {
if (payload.t !== "view") return;
attempts++;
if (failing) throw new Error("One-shot Intercom publication failure");
};
await h.supervisor.finishAssessment(); await tick();
assert.equal(attempts, 1);
for (let n = 0; n < 3; n++) await tick();
assert.equal(attempts, 1, "a failure must wait for an event or explicit retry, not recurse");
assert.equal(h.supervisor.messages.length, 1, "publication failure is not assessment delivery");
for (const [from, to, bindingId] of [["unrelated", "worker", h.binding.id], ["supervisor", "elsewhere", h.binding.id], ["supervisor", "worker", "stale-binding"]]) {
h.worker.receive({ type: "message", fromSessionId: from, payload: { t: "look", to, bindingId } });
}
h.supervisor.receive({ type: "message", fromSessionId: "unrelated", payload: { t: "view", to: "supervisor", bindingId: h.binding.id, view: "Not the current worker", refreshed: true, stopped: true } });
await tick(); assert.equal(attempts, 1); assert.equal(h.supervisor.messages.length, 1);
failing = false;
if (retry === "routine progress") {
h.worker.entries.push({ type: "message", message: { role: "assistant", content: [{ type: "text", text: "Progress after transport restored" }] } });
for (let n = 0; n < 50; n++) await h.worker.hook("turn_end");
} else await h.supervisor.commands.get("supervise").handler("look", h.supervisor.ctx);
await tick();
assert.equal(attempts, 2);
assert.equal(h.supervisor.messages.length, 2);
const refreshed = h.wire.filter((wire: any) => wire.t === "view").at(-1);
assert.equal(refreshed.refreshed, true, "the full overview request survives failed publication");
assert.match(refreshed.view, /Evidence before publication failed/);
assert.ok(refreshed.view.includes(h.binding.workerSession));
if (retry === "routine progress") assert.match(refreshed.view, /Progress after transport restored/);
await h.supervisor.finishAssessment(); await tick();
assert.equal(attempts, 2, "success consumes the request without an idle feedback loop");
assert.equal(h.wire.filter((wire: any) => wire.t === "pair").length, 1);
assert.equal(h.wire.filter((wire: any) => wire.t === "goal_decision").length, 0);
} finally { await h.close(); }
});
test("overview display metadata uses native context without requiring a remote roster lookup", async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.controller.activate(h.binding.id); await tick();
await h.supervisor.finishAssessment();
let rosterRequests = 0;
h.worker.beforeListSessions = () => { rosterRequests++; throw new Error("Roster timeout without disconnect"); };
h.worker.ctx.getContextUsage = () => ({ tokens: 24_000, contextWindow: 200_000, percent: 12 });
await h.supervisor.commands.get("supervise").handler("look", h.supervisor.ctx); await tick();
assert.equal(rosterRequests, 0);
assert.equal(h.supervisor.messages.length, 2);
assert.match(h.supervisor.messages.at(-1).text, /native\/test, 12% of its context used/);
} finally { await h.close(); }
});
for (const outcome of ["success", "rejection"] as const) test(`a superseded advance re-drives the current checkpoint after deferred compaction ${outcome}`, async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.controller.activate(h.binding.id); await tick();
h.supervisor.receive({ type: "message", fromSessionId: "worker", payload: { t: "view", to: "supervisor", bindingId: h.binding.id, view: "Pending routine work", stopped: false } });
await tick();
h.supervisor.tokens = 150_000;
const compactions: any[] = [];
h.supervisor.ctx.compact = (options: any) => { compactions.push(options); };
const oldAdvance = h.supervisor.finishAssessment(); await tick();
assert.equal(compactions.length, 1);
h.supervisor.receive({ type: "session_left", sessionId: "worker" }); await tick();
const acks = h.wire.filter((wire: any) => wire.t === "plan_hello_ack").length;
h.supervisor.receive({ type: "message", fromSessionId: "returned-wrong-session", payload: { t: "plan_hello", to: "supervisor", bindingId: h.binding.id, role: "worker", sessionFile: "/not-the-worker" } });
await tick(); assert.equal(h.wire.filter((wire: any) => wire.t === "plan_hello_ack").length, acks);
await assert.rejects(h.supervisor.tools.get("steer").execute("wrong-peer", { message: "Not connected" }, undefined, undefined, h.supervisor.ctx), /Worker disconnected/);
h.supervisor.receive({ type: "message", fromSessionId: "worker", payload: { t: "plan_hello", to: "supervisor", bindingId: h.binding.id, role: "worker", sessionFile: h.binding.workerSession } }); await tick();
assert.equal(h.wire.filter((wire: any) => wire.t === "plan_hello_ack").length, acks + 1);
const pending = h.worker.controller.review(h.binding.id, "first", planHash(readFileSync(h.planPath, "utf8")));
void pending.catch(() => {}); await tick();
assert.equal(compactions.length, 1, "no concurrent advance/compaction while the old one is outstanding");
assert.equal(h.supervisor.contexts.filter((message: any) => message.customType === "supervisor_checkpoint").length, 0);
if (outcome === "success") { h.supervisor.tokens = 10_000; compactions[0].onComplete({}); }
else compactions[0].onError(new Error("Obsolete compaction failed"));
await oldAdvance; await tick();
if (outcome === "rejection") {
assert.equal(compactions.length, 2, "only current work may retry compaction after the old rejection");
h.supervisor.tokens = 10_000; compactions[1].onComplete({}); await tick();
}
assert.equal(h.supervisor.contexts.filter((message: any) => message.customType === "supervisor_checkpoint").length, 1);
assert.equal(h.wire.filter((wire: any) => wire.t === "goal_decision").length, 0, "an obsolete failure cannot reject or approve the new checkpoint");
await h.supervisor.review();
assert.equal((await pending).decision, "approve");
await tick(); await h.supervisor.finishAssessment(); await tick();
const looks = h.wire.filter((wire: any) => wire.t === "look").length;
await h.supervisor.hook("agent_settled"); await tick();
assert.equal(h.wire.filter((wire: any) => wire.t === "look").length, looks);
assert.equal(h.wire.filter((wire: any) => wire.t === "pair").length, 1);
assert.equal(h.supervisor.aborts, 1, "only the actual disconnect aborts model work");
} finally { await h.close(); }
});
for (const outcome of ["success", "rejection"] as const) test(`stopping current work during an obsolete advance prevents re-drive after compaction ${outcome}`, async () => {
const h = pairHarness(); try {
await h.start(); await h.worker.controller.activate(h.binding.id); await tick();
h.supervisor.tokens = 150_000;
let compact: any; let compactions = 0;
h.supervisor.ctx.compact = (options: any) => { compact = options; compactions++; };
const oldAdvance = h.supervisor.finishAssessment(); await tick();
h.supervisor.receive({ type: "session_left", sessionId: "worker" }); await tick();
h.supervisor.receive({ type: "message", fromSessionId: "worker", payload: { t: "plan_hello", to: "supervisor", bindingId: h.binding.id, role: "worker", sessionFile: h.binding.workerSession } }); await tick();
const pending = h.worker.controller.review(h.binding.id, "first", planHash(readFileSync(h.planPath, "utf8")));
const cancelled = assert.rejects(pending, /stopped/); await tick();
await h.worker.controller.stop(h.binding.id); await cancelled; await tick();
const wireCount = h.wire.length;
if (outcome === "success") compact.onComplete({});
else compact.onError(new Error("Obsolete compaction failed after stop"));
await oldAdvance; await tick();
assert.equal(compactions, 1);
assert.equal(h.wire.length, wireCount, "stopped work does not retry, publish or approve");
assert.equal(h.supervisor.contexts.filter((message: any) => message.customType === "supervisor_checkpoint").length, 0);
assert.equal(h.supervisor.entries.at(-1).data.plan.stopped, true);
} finally { await h.close(); }
});
+25
View File
@@ -0,0 +1,25 @@
import assert from "node:assert/strict";
import test from "node:test";
import { goalReviewWire, isWire } from "../../src/internal/supervisor/protocol.js";
import { MAX_VIEW_BYTES } from "../../src/internal/supervisor/view.js";
test("goal_review accepts an optional bounded snapshot but rejects malformed snapshots", () => {
const review = { t: "goal_review", to: "supervisor", requestId: "request", bindingId: "binding", goal: "first", planHash: "hash" };
assert.equal(isWire(review), true, "older peers can omit the optional snapshot");
assert.equal(isWire({ ...review, view: "Fresh worker evidence" }), true);
for (const view of [null, false, 1, {}, [], "x".repeat(MAX_VIEW_BYTES + 1)]) {
assert.equal(isWire({ ...review, view }), false);
}
});
test("checkpoint snapshot bounding counts JSON escapes and does not split Unicode characters", () => {
const review = { requestId: "request", bindingId: "binding", goal: "goal ".repeat(1800), planHash: "hash" };
const wire = goalReviewWire("supervisor", review, '😀\\"\n'.repeat(2000));
assert.equal(isWire(wire), true);
assert.ok(Buffer.byteLength(JSON.stringify(wire)) <= 16 * 1024);
assert.equal(wire.t, "goal_review");
if (wire.t !== "goal_review") assert.fail("Expected a checkpoint request");
assert.equal(wire.goal, review.goal);
assert.doesNotMatch(wire.view!, /|\\ud83d(?!\\ude00)|(?<!\\ud83d)\\ude00/u);
assert.match(wire.view!, /cut to fit/);
});
+30
View File
@@ -19,6 +19,30 @@ function toolResult(toolName: string, text: string, isError = false): Entry {
return { type: "message", message: { role: "toolResult", toolName, isError, content: [{ type: "text", text }] } };
}
test("latest user direction survives bounded summaries, compaction and later supervisor echoes", () => {
const direction = "You are now explicitly authorized to implement goal two. Keep converter requirements intact. Stop after goal two is signed off.";
const entries: Entry[] = [
{ type: "message", message: { role: "user", content: direction } },
{ type: "compaction", summary: "Older summary still says to pause the CLI. ".repeat(200) },
...Array.from({ length: 200 }, (_, n) => assistant(`Evidence ${n} ${"detail ".repeat(200)}`)),
{ type: "message", message: { role: "user", content: "[supervisor] An older supervisor instruction is not new human direction." } },
];
const view = buildView({ goal: "goal two", status: "checkpoint", entries, sourceSession: "/history/worker.jsonl" });
assert.ok(Buffer.byteLength(view) <= MAX_VIEW_BYTES);
assert.ok(view.includes(direction));
assert.ok(view.indexOf(direction) < view.indexOf("Older summary"), "latest direction is separate from older summarized context");
assert.match(view, /latest user direction/i);
});
test("oversized user direction is visibly bounded with a source reference", () => {
const view = buildView({ goal: "g", status: "checkpoint", sourceSession: "/history/worker.jsonl", entries: [
{ type: "message", message: { role: "user", content: "Long direction ".repeat(2000) } },
] });
assert.ok(Buffer.byteLength(view) <= MAX_VIEW_BYTES);
assert.match(view, /user direction truncated/);
assert.match(view, /source session: \/history\/worker.jsonl/);
});
test("a one-line goal stays whole while a multi-line goal has a locator", () => {
assert.equal(goalPreview("fix the parser"), "fix the parser");
assert.equal(goalPreview("Build the causal evaluation.\nThe full rubric follows."), "Build the causal evaluation. [...]");
@@ -312,3 +336,9 @@ test("a long goal cannot push the view past the broker limit", () => {
const view = buildView({ goal: "x".repeat(60000), status: "idle", entries: [assistant("hi")] });
assert.ok(Buffer.byteLength(view, "utf-8") <= MAX_VIEW_BYTES, `view was ${Buffer.byteLength(view)} bytes`);
});
test("a bounded complete overview explicitly labels a truncated worker compaction summary", () => {
const view = buildView({ goal: "Check the cumulative evidence", status: "stopped", sourceSession: "/fixture/worker.jsonl", entries: [{ type: "compaction", summary: "evidence ".repeat(1_000) }] });
assert.match(view, /source session: \/fixture\/worker.jsonl/);
assert.match(view, /worker compaction summary truncated; inspect the worker session for full evidence/);
});
+3 -2
View File
@@ -46,14 +46,15 @@ describe("role-models storage and public model_select", () => {
const dir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals");
const h = harness(dir);
await h.enter("planning"); h.select("cycle", { provider: "planner", id: "persisted" });
await Promise.all(["worker", "supervisor"].map(role => promisify(execFile)(process.execPath, ["--import", "tsx", resolve("test/fixtures/role-model-process.ts"), dir, role, `${role}-provider`, "persisted"])));
// Two cold Pi/tsx imports compete with the packed-session tests in the full suite.
await Promise.all(["worker", "supervisor"].map(role => promisify(execFile)(process.execPath, ["--import", "tsx", resolve("test/fixtures/role-model-process.ts"), dir, role, `${role}-provider`, "persisted"], { timeout: 20_000 })));
const fresh = harness(dir, { provider: "unrelated", id: "start" });
for (const role of ["planning", "worker", "supervisor"] as const) {
await fresh.enter(role);
expect(fresh.ctx.model).toEqual({ provider: role === "planning" ? "planner" : `${role}-provider`, id: "persisted" });
expect(Object.keys(stored(dir, role))).toEqual(["provider", "id"]);
}
});
}, 30_000);
it.each(["unavailable", "unauthenticated"])("visibly pauses a %s remembered model without replacing it", async failure => {
const dir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals");
+3 -2
View File
@@ -40,8 +40,8 @@ describe("two real Pi sessions with the actual Intercom broker (Herdr mocked)",
if (last.role === "tool") return stream(res, { content: "Check complete." });
if (names.includes("review_goal")) {
supervisorModels.push(body.model);
const request = text.match(/Goal sign-off request ([^ .]+)\./);
if (request) { reviewCalls++; return call("review_goal", { requestId: request[1], decision: "approve", reason: "This goal remains faithful to the plan." }); }
const request = text.includes("Goal sign-off:");
if (request) { reviewCalls++; return call("review_goal", { decision: "approve", reason: "This goal remains faithful to the plan." }); }
return call("let_it_run", { reason: "Ready selected; worker starting" });
}
if (text.includes("intercom status")) return call("intercom", { action: "status" });
@@ -56,6 +56,7 @@ describe("two real Pi sessions with the actual Intercom broker (Herdr mocked)",
await new Promise<void>(done => server.listen(0, "127.0.0.1", done));
const address = server.address(); if (!address || typeof address === "string") throw new Error("Offline HTTP server did not start");
const agentDir = join(cwd, ".agent"); mkdirSync(agentDir);
writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ packages: [packageRoot] }));
writeFileSync(join(agentDir, "models.json"), JSON.stringify({ providers: { offline: { baseUrl: `http://127.0.0.1:${address.port}`, apiKey: "test", api: "openai-completions", models: ["test", "planning", "worker", "supervisor", "judge"].map(id => ({ id, name: `Offline ${id}`, reasoning: false, input: ["text"], contextWindow: 200_000, maxTokens: 1000, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } })) } } }));
const modelDir = join(agentDir, "pi-goals"); mkdirSync(modelDir);
for (const role of ["worker", "supervisor"]) writeFileSync(join(modelDir, `${role}-model.json`), JSON.stringify({ provider: "offline", id: role }));
+16 -2
View File
@@ -67,7 +67,7 @@ describe("actual goals and supervisor package hooks (Herdr and judge mocked)", (
appendEntry: (name: string, data: any) => manager.appendCustomEntry(name, data),
getActiveTools: () => active, setActiveTools: (names: string[]) => { active = names; },
getCommands: () => [{ name: "supervise", sourceInfo: { path: "internal-supervisor" } }],
getAllTools: () => [{ name: "subagent" }, { name: "intercom", sourceInfo: { path: "intercom-test-only" } }],
getAllTools: () => [...["read", "grep", "find", "ls"].map(name => ({ name, sourceInfo: { source: "builtin" } })), { name: "subagent" }, { name: "intercom", sourceInfo: { path: "intercom-test-only" } }],
sendUserMessage: (text: string) => messages.push(text), sendMessage: (message: any) => contexts.push(message),
exec: async (command: string, args: string[]) => {
expect(command).toBe("herdr"); herdrCalls.push(args);
@@ -187,12 +187,26 @@ describe("actual goals and supervisor package hooks (Herdr and judge mocked)", (
expect(supervisor.compactions).toBe(1); expect(worker.compactions).toBe(0);
expect(worker.messages.filter((m: string) => m.startsWith("Work the goals"))).toHaveLength(1);
expect(supervisor.pi.getActiveTools()).not.toContain("CompleteGoal");
await supervisor.tools.get("let_it_run").execute("ready-assessed", { reason: "Worker is ready" }, undefined, undefined, supervisor.ctx);
await supervisor.hook("agent_settled");
await worker.commands.get("goals").handler("judge offline/judge", worker.ctx);
writeFileSync(path, readFileSync(path, "utf8").replace("[ ] goal: first", "[x] goal: first"));
const premature = worker.tools.get("CompleteGoal").execute("", { goal: "first" }, undefined, undefined, worker.ctx);
await tick();
expect(readFileSync(path, "utf8")).toContain("[/] goal: first");
await supervisor.hook("context", { messages: supervisor.contexts });
await supervisor.tools.get("review_goal").execute("", { decision: "needs_work", reason: "The overflow test does not exercise overflow" });
await supervisor.hook("agent_settled");
expect((await premature).isError).toBe(true);
expect(readFileSync(path, "utf8")).not.toContain("[x] goal: first");
expect(judge.calls).toHaveLength(0);
for (const goal of ["first", "second"]) {
const completion = worker.tools.get("CompleteGoal").execute("", { goal }, undefined, undefined, worker.ctx);
await tick(); const request = wires.findLast((w: any) => w.t === "goal_review");
expect(request.goal).toBe(goal);
await supervisor.tools.get("review_goal").execute("", { requestId: request.requestId, decision: "approve", reason: "Within the requested scope" });
await supervisor.hook("context", { messages: supervisor.contexts });
await supervisor.tools.get("review_goal").execute("", { decision: "approve", reason: "Within the requested scope" });
await supervisor.hook("agent_settled");
const completed = await completion;
expect(completed.isError, JSON.stringify(completed)).toBe(false);
expect(readFileSync(path, "utf8")).toContain(`[x] goal: ${goal}`);
+30 -7
View File
@@ -1,13 +1,36 @@
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import { type ExtensionAPI, type ExtensionContext, SessionManager } from "@earendil-works/pi-coding-agent";
import { afterEach, describe, expect, it, vi } from "vitest";
import { focusSupervisor, planHash, type SupervisorBinding, startSupervisor, supervisorRequest } from "../src/supervisor.js";
import { focusSupervisor, planHash, type SupervisorBinding, type SupervisorController, startSupervisor, supervisorResourceArgs } from "../src/supervisor.js";
const binding: SupervisorBinding = { id: "pair", workerSession: "/worker.jsonl", supervisorSession: "/supervisor.jsonl", planPath: "/plan.md", workerPane: "w1:p1", supervisorPane: "w1:p2", everyTurns: 50, intervalMs: 3_600_000, compactTokens: 100_000 };
afterEach(() => vi.unstubAllEnvs());
afterEach(() => { vi.unstubAllEnvs(); vi.restoreAllMocks(); });
describe("supervisor adapter", () => {
it("fails visibly when the supervisor API is absent", async () => {
const pi = { events: { emit() {} } } as unknown as ExtensionAPI;
await expect(supervisorRequest(pi, "status")).rejects.toThrow("Load the pi-goals package directory");
it("reuses explicit resource choices without adding companion packages or copying mode/model/auth", () => {
expect(supervisorResourceArgs(["--mode", "rpc", "--model", "expensive", "--api-key", "not-forwarded", "--no-extensions", "-e", "/package", "--skill", "/skill", "prompt"])).toEqual(["--no-extensions", "-e", "/package", "--skill", "/skill"]);
expect(supervisorResourceArgs([])).toEqual([]);
});
it.each([
["--no-extensions", "--no-skills", "--no-prompt-templates"],
["-ne", "-ns", "-np"],
])("preserves equivalent disabling flags in the generated launch: %s %s %s", async (extensions, skills, prompts) => {
const resources = [extensions, "-e", "/package", skills, prompts];
const expected = ["--no-extensions", "-e", "/package", "--no-skills", "--no-prompt-templates"];
expect(supervisorResourceArgs(resources)).toEqual(expected);
vi.stubEnv("HERDR_ENV", "1"); vi.stubEnv("HERDR_PANE_ID", "w1:p1");
const existing = { ...binding, supervisorPane: undefined };
const appendCustomEntry = vi.fn();
vi.spyOn(SessionManager, "open").mockReturnValue({ appendCustomEntry } as unknown as SessionManager);
const exec = vi.fn(async (_command: string, args: string[]) => ({ code: 0, stderr: "", stdout: args[0] === "pane" ? JSON.stringify({ result: { pane: { pane_id: "w1:p2" } } }) : "{}" }));
const pi = { exec } as unknown as ExtensionAPI;
const controller = { status: async () => ({ connected: false, binding: existing, workerId: "worker" }), attached: async () => binding } as unknown as SupervisorController;
const ctx = { cwd: "/project", sessionManager: { getSessionFile: () => binding.workerSession, getLeafId: () => "leaf" } } as unknown as ExtensionContext;
const originalArgv = process.argv;
try {
process.argv = [originalArgv[0], originalArgv[1], "--mode", "rpc", ...resources];
await startSupervisor(pi, controller, ctx, binding.planPath, existing, vi.fn(), new AbortController().signal);
} finally { process.argv = originalArgv; }
expect(exec.mock.calls[1]).toEqual(["herdr", ["agent", "start", "supervisor-pair", "--kind", "pi", "--pane", "w1:p2", "--", "--session", binding.supervisorSession, ...expected], expect.anything()]);
expect(appendCustomEntry).toHaveBeenCalledOnce();
});
it("does not operate on a live Herdr session from outside Herdr", async () => {
vi.stubEnv("HERDR_ENV", "");
@@ -20,7 +43,7 @@ describe("supervisor adapter", () => {
const exec = vi.fn(async () => ({ code: 1, stdout: "", stderr: '{"error":{"code":"pane_not_found","message":"Pane unavailable"}}' }));
const pi = { exec, events: { emit(_name: string, request: any) { request.handled = true; request.resolve({ connected: false, binding, workerId: "worker" }); } }, getCommands: () => [{ name: "supervise", sourceInfo: { path: "/supervise.ts" } }], getAllTools: () => [{ name: "intercom", sourceInfo: { path: "/intercom.ts" } }] } as unknown as ExtensionAPI;
const ctx = { cwd: "/project", sessionManager: { getSessionFile: () => "/worker.jsonl", getLeafId: () => "leaf" } } as unknown as ExtensionContext;
await expect(startSupervisor(pi, ctx, binding.planPath, binding, vi.fn(), new AbortController().signal)).rejects.toThrow("/supervisor.jsonl");
await expect(startSupervisor(pi, { status: async () => ({ connected: false, binding, workerId: "worker" }) } as SupervisorController, ctx, binding.planPath, binding, vi.fn(), new AbortController().signal)).rejects.toThrow("/supervisor.jsonl");
expect(exec.mock.calls).toHaveLength(1);
expect(exec.mock.calls[0]).toEqual(["herdr", ["agent", "focus", "w1:p2"], expect.anything()]);
});
+1
View File
@@ -2,6 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"paths": {
"pi-intercom": ["./src/intercom-package.d.ts"],
"@sting8k/pi-vcc/src/*": [
"./src/internal/supervisor/vcc.d.ts"
]