mirror of
https://github.com/wassname/pi-goals.git
synced 2026-09-11 12:43:57 +08:00
Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15dd7f0222 | ||
|
|
138bde57f4 | ||
|
|
1d5285721c | ||
|
|
34335752f7 | ||
|
|
565b272c71 | ||
|
|
a7385d4b76 | ||
|
|
e19028e330 | ||
|
|
039f4a4048 | ||
|
|
8953dceb46 | ||
|
|
5567c9d5c2 | ||
|
|
2b61440c73 | ||
|
|
cb4790a96c | ||
|
|
88bfcc1c42 | ||
|
|
1668c941aa | ||
|
|
325b93983f | ||
|
|
2824396a71 | ||
|
|
ddd552b1a5 | ||
|
|
47cc054582 | ||
|
|
489298d58b | ||
|
|
b13f001110 | ||
|
|
386305afd3 | ||
|
|
94102524b6 | ||
|
|
a4ed6cfbaa | ||
|
|
06794bfd44 | ||
|
|
4ebb4d127b | ||
|
|
6b641c7d17 | ||
|
|
1717dd6821 | ||
|
|
19fa8d7a7b |
@@ -15,7 +15,63 @@ Run `npm test` before a commit. It includes unit and flow tests plus the RPC rev
|
||||
Run `/goals <objective>` in that pane. Tmux checks the rendered menu, editor focus, widget, and keyboard handling. RPC does not render the terminal UI.
|
||||
- `pi -p` has no UI, so it cannot test `Ready`, `Refine`, `Edit`, or `Cancel`.
|
||||
|
||||
## Intended supervision workflow
|
||||
## Functional acceptance: real Herdr workflow
|
||||
|
||||
Pi/OpenAI procedure, requested by wassname. Automated tests do not replace this check.
|
||||
|
||||
1. Read `herdr --skill` and confirm `HERDR_ENV=1`. Create a separate test pane with `--no-focus` and an isolated temporary Git repo. Never operate the user's existing worker or supervisor panes. Record the code revision and any uncommitted changes being tested.
|
||||
2. Start real interactive Pi with this extension and an available real model. Use `/goals` with a trivial, bounded deliverable, for example `hello.txt` containing an exact line plus a saved byte-verification log. No GPU, dependencies or unrelated work.
|
||||
3. Read the rendered planning conversation. Check that ordinary implementation details do not cause needless confirmation questions. Inspect the drafted plan and select Ready through the actual UI.
|
||||
4. Confirm Ready opens a visible supervisor pane and the worker starts. Read both panes. Verify the supervisor's exact advice is visible, reaches the worker, and helps it progress toward the requested artifact. A delivery receipt alone is not proof.
|
||||
5. Let the pair produce the artifact, save verification evidence, and complete the real ApproveGoal -> CompleteGoal sequence. Do not perform the task for the worker. Record any manual nudge as intervention, not autonomous success.
|
||||
6. Inspect the artifact itself and its saved verification output. Check the final plan state and both sessions. Success means the requested result exists and the workflow completes, not merely that tests pass or messages were exchanged.
|
||||
7. Exercise reload and supervision recovery in these test panes, preserving the current plan. Check planning exit too. Record commands available in the tested revision; do not claim unimplemented commands work.
|
||||
8. When a stage fails, read both panes and the exact error before diagnosing it. Fix the cause, reload only the test instance, and retry the failed stage. After a prompt change, use a fresh task to verify changed behavior. Repeated status checks are not a repair.
|
||||
9. Save pane captures, session paths, artifact paths, code revision, interventions and remaining failures under `slop/reviews/`. A wait-output timeout or match is only a signal to inspect the pane, not a pass/fail verdict. Report the observed result and gaps, not a test-count substitute.
|
||||
|
||||
Keep this check small and goal-focused. Its purpose is to expose real startup, UI, steering and completion failures, not to create another review loop. Only close test panes that you created.
|
||||
|
||||
## 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 by Pi/OpenAI)
|
||||
|
||||
## Supervisor behavior preferences
|
||||
|
||||
Recorded by Pi/OpenAI from wassname's instructions.
|
||||
|
||||
The supervisor's job is to supervise autonomously until the agreed goal is achieved and it has inspected the actual result. Elicit high-level judgment and perspective, not compliance with a detailed procedure. It should want to diagnose and fix problems through the worker, keep useful work moving, and avoid making the human drive progress.
|
||||
|
||||
Treat claims of being blocked, waiting, unable to proceed, or already done skeptically. Inspect the evidence, question assumptions, and look for authorized ways forward. Do not accept an excuse at face value or repeat status checks that cannot resolve it. Respect real dependencies and permission limits; skepticism does not authorize bypassing them. Seek justified confidence, not certainty at any cost.
|
||||
|
||||
User-authorized full-profile supervision: preserve normal Pi extensions and tools, including bash/edit/write and custom actions. Inspection-only is a role instruction, not a tool denylist or enforced sandbox. Repeat the division of work in the existing short opening: inspect and diagnose directly, delegate changes through SteerWorker, and do not take over implementation or alter shared state. Do not add per-tool reminders. Worker planning restrictions and approval checks are separate and unchanged. Validate the full profile in isolated parent-owned Herdr panes; automated tests do not prove role adherence or lifecycle recovery.
|
||||
|
||||
Keep the prompt generic. Do not prescribe pueue, Modal, worktrees, or a particular research setup. Explain the job and what deserves attention; let the supervisor choose useful checks. Tool requirements belong in tool descriptions. Administrative approval must not replace the requested deliverable.
|
||||
|
||||
Use `@monotykamary/pi-supervisor` as a behavioral reference, not an implementation to copy wholesale. Its outcome focus, autonomous continuation, and instruction not to repeat ineffective steering are useful. Judge our behavior in real sessions, not by test counts alone.
|
||||
|
||||
Pi/OpenAI implementation: each review repeats the short supervisor opening and current plan outcome, preferences, goals and discriminators, excluding task/evidence detail. Startup and compaction repeat the longer role prompt and full active plan before appendices/history. The long prompt asks the supervisor to read applicable AGENTS.md instructions and relevant skills rather than assuming project-specific preferences. Both forms preserve plan wording. Prompt inspiration: Anthropic's constitution (intent and autonomy) and @monotykamary/pi-supervisor (outcome focus and effective steering). Repetition supports judgment; it does not establish success.
|
||||
|
||||
Keep brief visible recaps that add judgment rather than repeat unchanged status. Preserve useful reasoning and evidence checks; reduce redundant context and reviews before reducing judgment. Manual checkbox changes are claims, not proof of completion. Plan edits should reach the supervisor so it can judge drift and direct corrections.
|
||||
|
||||
## Earlier supervision workflow discussion
|
||||
|
||||
I already have pi-intercom-supervisor, but thought using pi-subagents could make it simpler. The idea is that the user makes a plan as in pi-goals, but on this branch, instead of a naive stateless subagent, we 1) fork, 2) compact, and 3) make it a supervisor with a prompt as in pi-intercom-supervisor. The supervisor is cheap because it sees only high-level material, which costs fewer tokens. It has good judgement because it sees a) compacted planning context, b) the plan, and c) summarised context (for example, my modified pi-vcc). This lets it operate read-only and steer the worker without losing track. It also compacts every 100k tokens to keep it cheap and high-level.
|
||||
|
||||
|
||||
@@ -8,22 +8,25 @@ Plan in one Pi session, then do the work there while a stronger visible Pi sessi
|
||||
2. Pi asks only material questions, writes the plan, and shows **Ready / Refine / Edit / Cancel**.
|
||||
3. **Ready** opens a second Herdr pane. The new Pi session explicitly forks the planning session and compacts that fork.
|
||||
4. The original session becomes the implementation worker. It keeps the full conversation and normal tools.
|
||||
5. The fork becomes a read-only supervisor. `pi-supervise` gives it compact worker views and carries its instructions to the worker through `pi-intercom`.
|
||||
6. The supervisor compacts again when its context reaches 100k tokens.
|
||||
7. The supervisor records a private approval only after it sees a stopped worker, no active work, a clean commit, evidence, and saved verification output. `CompleteGoal` checks that approval against the exact plan block and Git tree before it ticks `[x]`.
|
||||
5. The fork becomes an inspection-only supervisor by instruction, with normal Pi tools and extensions available. Worker views and supervisor instructions travel over pi-intercom's extension channel, scoped to this plan pairing.
|
||||
6. Ready waits for the supervisor's Intercom readiness message; the worker does not begin before the fork has compacted and started.
|
||||
7. The supervisor compacts again when its context reaches 100k tokens.
|
||||
8. The supervisor records a private approval only after it sees a stopped worker, no active work, a clean worktree (or an explicit inspected-state override), evidence, and saved verification output. `CompleteGoal` checks that approval against the exact plan block and Git tree before it ticks `[x]`.
|
||||
|
||||
The two Pi sessions are visible. You can switch to the supervisor pane and talk to it directly.
|
||||
The two Pi sessions are visible. You can switch to the supervisor pane and talk to it directly. Supervisor instructions are shown in full, including in collapsed tool rows; ordinary messages and emitted thinking use Pi's display settings. The supervisor is prompted to give brief progress assessments and use judgment about when to intervene.
|
||||
|
||||
On same-process reload, monitoring is restored without removing normal or custom tools. Views include the latest human direction, source-session path, worker model, and new messages since the last acknowledged view. They report Pi idleness and tracked process/subagent activity separately. Unavailable trackers stay unknown; unregistered detached jobs are not tracked. Approval is blocked while tracked work is active or unknown. Intercom disconnects are reported; unsent current views and unacknowledged instructions are retained in Pi session history for reconnect. Incoming review/instruction payloads are saved in a bounded inbox before presentation. A receipt is sent only when Pi emits the corresponding user `message_start`: this confirms session acceptance, not model judgment or tool execution. Pending payloads survive reload, and manual-compaction success, failure or cancellation resumes delivery after Pi is idle. Unpresented incremental views are kept separately, not overwritten. The inbox limit is 64 messages; overflow is reported without acknowledging the new message. Crashes around acceptance/persistence or extensions that transform/consume input can still require inspection and explicit reconnect; end-to-end exactly-once execution is not guaranteed. Reviews stop after all goals are completed or cancelled, and both panes remain available. These mechanics are tested; useful judgment and savings from a cheaper worker still require a representative two-model run. -- Pi/OpenAI
|
||||
|
||||
## Install
|
||||
|
||||
This branch requires Herdr 0.7.5 or newer and these Pi packages:
|
||||
This branch requires Pi 0.85.1 or newer (before 1.0) and Herdr 0.7.5 or newer. Pi 0.85.1 supplies the public compaction-failure event and compaction-aware idle state used for delivery recovery. It reuses installed pi-intercom or loads its pi-intercom dependency when none is registered:
|
||||
|
||||
```bash
|
||||
pi install npm:@wassname2/pi-goals
|
||||
pi install npm:@wassname2/pi-supervise
|
||||
pi install npm:pi-intercom
|
||||
```
|
||||
|
||||
The supervisor launcher uses the normal Pi profile: it inherits the agent directory/environment and discovers configured extensions, skills, prompt templates, themes and authentication. It explicitly loads this pi-goals source and forks the planning session with the supervisor role/model. Existing Intercom is reused when registered. The repeated role instruction says to inspect and diagnose directly, but delegate changes through `SteerWorker` rather than alter shared state. **This is not an enforced sandbox:** bash, edit, write and extension actions remain available; other extensions may have their own hooks or restrictions. Planning-mode restrictions and approval checks are unchanged. A complete supervisor role/binding is saved before startup compaction and restored before worker handlers can run, including fresh-shell `pi --session <saved-file>` and stopped supervisor forks. Older bootstrap markers are migrated only when their saved pairing is recoverable; incomplete identity fails visibly rather than selecting worker mode. Full-profile Herdr behavior still needs parent-owned functional acceptance.
|
||||
|
||||
For a local checkout:
|
||||
|
||||
```bash
|
||||
@@ -37,15 +40,38 @@ Run Pi from the Git repository that the plan will change. **Ready** fails if the
|
||||
```text
|
||||
/goals <objective> create a new plan
|
||||
/goals model <model> select the visible supervisor model
|
||||
/goals model use Pi's current default model
|
||||
/goals model use the remembered supervisor model
|
||||
/goals work reconnect the existing approved worker pairing
|
||||
/goals supervise reconnect from the saved supervisor session
|
||||
/goals noplan exit planning, preserving the draft without approving work
|
||||
/goals clear close the supervisor pane and disconnect the plan
|
||||
```
|
||||
|
||||
`/goals clear` keeps the plan file. Starting another plan also keeps older versions.
|
||||
`work` and `supervise` are role-aware recovery commands, not role conversion or new-pairing commands. Wrong or missing identities are rejected. `noplan` preserves the draft/history, leaves planning restrictions, and does not select Ready, start implementation or launch a supervisor. `/goals clear` closes the tracked pane and keeps the plan file. Starting another plan also keeps older versions.
|
||||
|
||||
If a required model or supervisor is unavailable, the widget says **goals paused** and implementation/sign-off tools are gated. Human input, read-only diagnosis, `/model`, and recovery commands remain available:
|
||||
|
||||
- `/goals reconnect` retries the remembered role model and existing supervisor binding. Worker readiness/reconnect waits allow five minutes, including an ordinary 60-second supervisor compaction, and never replace a slow or missing pane automatically. A returning peer clears the connection pause automatically; an established active worker pairing publishes a fresh current view so supervisor-only reload can resume review even when its previous view was already accepted.
|
||||
- `/goals restart` explicitly closes only the tracked supervisor pane and starts a replacement for a working plan, preserving its file/version but invalidating old approvals. During planning it clears the failed pane so Ready can launch again.
|
||||
- In the supervisor pane, use `/model` then `/goals supervise` (or `/goals reconnect`) to recover an unavailable supervisor model. Startup failure is reported to the waiting worker; it need not wait for the timeout to learn the cause.
|
||||
|
||||
Both sessions must load the updated transport for the request/reply reconnect fix; mixed-version peers are not a supported recovery configuration. Ready announces worker readiness only after its model is restored. Plan content is rechecked across startup/model-restore waits; changed content returns to review using the existing pane instead of starting different work. Clearing or leaving planning cancels its pending Ready attempt. `CompleteGoal` checks cancellation and the original binding/version after its asynchronous status lookup and before recording completion.
|
||||
|
||||
A new supervisor may still need up to five minutes for initial compaction. Recovery does not terminate background jobs. Planning/diagnostic command checks are guardrails, not an OS sandbox; loaded extensions and repository Git configuration must be trusted.
|
||||
|
||||
Model choices are remembered per project and role in `.pi/pi-goals/models/`. Use `/model` in planning, worker, or supervisor sessions to change that role's choice. Ready restores the worker choice after the planning fork is ready. An unavailable saved model stops the transition instead of substituting another. `/goals model <model>` explicitly overrides the supervisor choice for launch. -- Pi/OpenAI
|
||||
|
||||
## Inspected dirty-worktree approval
|
||||
|
||||
The supervisor can call `ApproveGoal` with `force: true` and a nonempty `reason` when preserved unrelated changes would otherwise prevent sign-off. It must inspect the changes first, not commit, reset or delete someone else's work. Force bypasses **only** cleanliness, never evidence, the current stopped view, active/unknown work, or exact goal/HEAD/tree checks.
|
||||
|
||||
The approval JSON stores the reason, NUL-delimited Git status, an index SHA-256 digest and per-dirty/untracked-file content SHA-256 digests (including modes, symlink targets and deletions). `CompleteGoal` requires the same state; even editing an already-dirty file without changing its status invalidates approval. Normal clean approvals behave as before. Git-ignored files and pi-goals' private plan/approval/model paths remain excluded. Dirty submodule/nested-repository directories or other unhashable paths fail closed; there is no recursive submodule override. Fingerprinting reads all included dirty/untracked bytes and can be expensive for large outputs; it does not lock concurrent writers.
|
||||
|
||||
A gate rejection is not automatically an experiment failure or a dependency of other authorized work. The supervisor should inspect the exact error and implementation, distinguish causes with a cheap check, and steer repairs plus safe independent progress instead of repeating an unproductive status check. -- Pi/OpenAI
|
||||
|
||||
## Plan format
|
||||
|
||||
A goal is a checkbox line whose text starts with `goal:`:
|
||||
Current goals belong above `## Log`; goal-shaped historical checklists below it are ignored by the widget, approval matching and sign-off. A goal is a checkbox line whose text starts with `goal:`:
|
||||
|
||||
```md
|
||||
1. [ ] goal: Produce the report
|
||||
@@ -57,6 +83,8 @@ A goal is a checkbox line whose text starts with `goal:`:
|
||||
|
||||
The worker saves verification output in a nonempty repository file, adds that path to evidence, and commits it. The supervisor calls `ApproveGoal` with the inspected path; the worker then calls `CompleteGoal` with the exact goal text.
|
||||
|
||||
If context usage is unavailable, the supervisor warns once that its custom 100k compaction trigger cannot be checked. Pi's normal post-compaction `tokens: null` sample does not produce that warning; default auto-compaction is unchanged.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
@@ -65,6 +93,6 @@ npm run typecheck
|
||||
npm run lint
|
||||
```
|
||||
|
||||
`test/rpc-review.test.ts` runs the planning review flow through Pi's real RPC protocol with a local deterministic model. The Herdr launcher and visible supervisor bootstrap have focused tests; use a real Herdr session for the final two-pane check.
|
||||
`test/intercom-broker.test.ts` checks readiness and exact message delivery through an isolated real Intercom broker. `test/rpc-review.test.ts` runs the planning review flow through Pi's real RPC protocol with a local deterministic model. The Herdr launcher and visible supervisor bootstrap have focused tests; use a real Herdr session for the final two-pane check.
|
||||
|
||||
-- PI[gpt-5.6-sol]
|
||||
|
||||
Generated
+2296
-159
File diff suppressed because it is too large
Load Diff
+8
-3
@@ -19,11 +19,11 @@
|
||||
"uat",
|
||||
"evidence",
|
||||
"supervisor",
|
||||
"pi-intercom",
|
||||
"herdr"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@earendil-works/pi-coding-agent": "*",
|
||||
"@earendil-works/pi-coding-agent": ">=0.85.1 <1.0.0",
|
||||
"@earendil-works/pi-tui": "*",
|
||||
"typebox": "*"
|
||||
},
|
||||
"files": [
|
||||
@@ -43,9 +43,14 @@
|
||||
"lint": "biome check src/ test/",
|
||||
"lint:fix": "biome check --fix src/ test/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sting8k/pi-vcc": "0.5.0",
|
||||
"pi-intercom": "^0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.8",
|
||||
"@earendil-works/pi-coding-agent": "^0.84.1",
|
||||
"@earendil-works/pi-coding-agent": "0.85.1",
|
||||
"@earendil-works/pi-tui": "^0.85.1",
|
||||
"@types/node": "^20.0.0",
|
||||
"typebox": "^1.3.7",
|
||||
"typescript": "^5.0.0",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# One-package visible supervision
|
||||
|
||||
## Goal
|
||||
|
||||
Replace the pi-goals → pi-supervise → pi-intercom runtime chain with one pi-goals extension in two Pi processes. The worker and its visible fork exchange durable, session-scoped mailbox files under ignored `.pi/`.
|
||||
|
||||
## Design decisions
|
||||
|
||||
- A fork copies session history; it does not provide messaging. The mailbox is the explicit local-process channel.
|
||||
- Ready waits for the supervisor's durable `ready.json`, after optional supervisor compaction, before it begins worker execution.
|
||||
- Worker views are written on Ready, settle, 50 turns, and 60 minutes. The supervisor polls views and writes one steer request. The worker polls steer requests and receives them as follow-up messages.
|
||||
- The canonical plan remains a direct path in the supervisor prompt. It is not a summary artifact.
|
||||
- Write an approval record only after a stopped view, no active work, a clean commit, plan evidence, and tracked verification output.
|
||||
- No external `pi-supervise` or `pi-intercom` runtime dependency remains.
|
||||
|
||||
## Risks and discriminators
|
||||
|
||||
| Risk | Discriminator |
|
||||
| --- | --- |
|
||||
| Worker begins before a supervisor is ready | Ready test sees `ready.json` before state changes to working or sends the execution prompt. |
|
||||
| Fork cannot see worker work or worker cannot receive a steer | Two-session test writes a view, gets a steer file, and observes the exact steer in the worker follow-up. |
|
||||
| Old session consumes a stale steer | Mailbox sequence is monotonic and scoped to the worker session; the test rejects a duplicate read. |
|
||||
| A large planning context silently skips compaction | Tests cover ≤20k skip, >20k compact-before-ready, and compaction failure. |
|
||||
|
||||
## Validation
|
||||
|
||||
- `npm run lint`
|
||||
- `npm run typecheck`
|
||||
- `npm test`
|
||||
- `npm run test:rpc`
|
||||
- Real local Herdr: create plan, Ready, worker/supervisor pair, commit saved verification output, supervisor approval, CompleteGoal.
|
||||
|
||||
-- PI[Kimi K3]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Dirty-worktree approval override — Pi/OpenAI
|
||||
|
||||
Baseline: cb4790a. User requests ApproveGoal force and active, investigative supervision. Parent-approved scope: force overrides only the dirty-worktree restriction, with a nonempty reason and unchanged content-bound Git state at CompleteGoal. Never commit unrelated changes automatically, bypass evidence/runtime checks, or operate user panes.
|
||||
|
||||
- [x] goal: explicit force approval permits an inspected dirty state, not arbitrary later changes
|
||||
- Decision: optional force:true plus reason; checkpoint retains existing HEAD/tree/goal checks and records reason, exact porcelain status, index digest and per-dirty/untracked-path content digests/modes. Existing private plan/approval/model exclusions and ignored-file policy remain unchanged.
|
||||
- Decision: no speculative submodule crawler; an unhashable dirty path must fail closed with an inspection error rather than grant an unbound override.
|
||||
- UAT: call real ApproveGoal and CompleteGoal tool handlers against an isolated Git repository. Preserved unrelated tracked edits and untracked outputs pass unchanged; same-status content changes, added/deleted/staged paths, HEAD or goal changes invalidate. Default dirty rejection and force-without-reason rejection remain. Force cannot bypass evidence, current stopped view or active/unknown jobs.
|
||||
- [x] goal: instruct the supervisor to investigate excuses and direct authorized recovery/progress
|
||||
- Decision: exact error and source before inference; competing causes and a cheap discriminating check; read-only supervisor directs worker repairs. Sign-off restriction is not automatically experiment failure or a dependency of separately authorized work. No new authority, spending or mutation tool.
|
||||
- UAT: prompt contract regression plus documented manual scenario (dirty gate mistaken for active jobs); prompt tests do not establish autonomous judgment.
|
||||
|
||||
## Validation/provenance
|
||||
|
||||
Read AGENTS.md and installed Pi extension custom-tool/schema documentation. Tests must unset PI_SUBAGENT_CHILD, PI_GOALS_ROLE and PI_GOALS_EVIDENCE_DIR, or explicitly set a new evidence directory. Two pre-existing dirty review-fixes-native logs must remain untouched. Save complete validation output; commit scoped changes locally only. Independent review is parent-owned and still required.
|
||||
|
||||
## Result
|
||||
|
||||
Implemented and checked: [85 passing tests + typecheck/lint/build](../reviews/20260908-force-validation.txt). Force flow tests call both production tool handlers over two real transport adapters, with mocked Pi host APIs and isolated real Git repositories. They do not touch Herdr panes. Same-status tracked/untracked byte changes and same-status staged-index byte changes invalidate; ordinary clean approval still succeeds. Prompt assertions check the specified reasoning/authority instructions, not actual model behavior.
|
||||
|
||||
Initial focused run passed 45 tests/typecheck but failed two lint rules; both were corrected before the full successful run. [Initial output](../reviews/20260908-force-initial-validation.txt) is retained, not counted as a pass. Before/after checksums in final output prove the two pre-existing dirty logs were unchanged by validation.
|
||||
|
||||
Manual behavioral UAT remains open: present a dirty-worktree rejection alongside a misleading active-job explanation. Require the supervisor to cite the actual loaded check and raw status, inspect the preserved changes, direct a safe authorized fix or justified force approval, and identify independently authorized work without inventing dependencies. No useful-judgment claim from prompt tests.
|
||||
|
||||
No push, no nested review loop, no /goals supervise or noplan work. Parent's w8:p4T functional pane at pinned cb4790a was not touched. Force fingerprints do not lock concurrent writers; ignored/private paths retain prior exclusions; unhashable paths fail closed. Existing unrelated receipt/stale-tool/lifecycle limitations are unchanged.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Intercom supervision and selected feature transfer
|
||||
|
||||
User priority: pi-intercom is the session-to-session transport. Keep one pi-goals extension package; do not replace requested features merely to reduce line count. Existing worker and supervisor panes are off-limits.
|
||||
|
||||
- [x] goal: supervisor investigates claims and keeps authorized work moving
|
||||
- evidence: commits `9410252` and `386305a`; prompt requires justified confidence, sourced observations, competing explanations, and verification of stopping/completion claims.
|
||||
- limitation: prompt checks do not prove behavioral improvement.
|
||||
- [/] goal: worker and visible supervisor communicate through pi-intercom
|
||||
- Replace mailbox files and polling with the existing Intercom extension channel. No separate RPC transport or mailbox fallback.
|
||||
- Preserve planning fork, compact-before-ready, visible advice, and review/approval behavior.
|
||||
- failure modes: false readiness, duplicate delivery, wrong-session routing, stale callbacks after reload, disconnected peer treated as active.
|
||||
- deliverable: isolated two-session message transcript with exact instructions received, reconnect/reload checks, and saved validation output. Do not operate user panes.
|
||||
- evidence: [37 passing tests, typecheck and lint](../reviews/20260908-intercom-validation.txt). Two client sessions exchange readiness, view, and exact advice through a real isolated Intercom broker. Lifecycle mocks cover resume/deduplication. Mailbox source and polling were removed.
|
||||
- evidence: [native Pi-pair validation](../reviews/20260908-native-intercom-full-validation.txt) reports `Tests 46 passed (46)` plus successful typecheck, lint and build. The isolated production supervisor forks planning context and sends exact advice through Intercom to a real Pi worker using a transport fixture. [Supervisor events](../reviews/native-intercom/supervisor-events.jsonl) show `SteerWorker` and the worker acknowledgement; [worker events](../reviews/native-intercom/worker-events.jsonl) show the received user message. Both saved stderr files are empty.
|
||||
- limitation: this deterministic test proves transport and context retention, not useful model judgment. The worker fixture does not exercise the full Ready path. Rendered Herdr two-pane acceptance and fresh-shell supervisor resume without launcher environment remain unverified.
|
||||
- [x] goal: supervisor receives a useful bounded worker overview
|
||||
- Borrow latest human direction, source-session path, and incremental progress from `origin/feature/simple-visible-supervision`.
|
||||
- failure modes: lost authorization, repeated summaries, truncated evidence treated as complete.
|
||||
- deliverable: saved before/after overview fixtures covering compaction and changed human direction.
|
||||
- evidence: [generated fixture views](../reviews/20260908-worker-overview-example.txt) retain the human direction while omitting acknowledged old detail. [43-test validation](../reviews/20260908-worker-overview-validation.txt) also checks compaction reset and serialized Unicode limits; fixture content is synthetic, not a model performance claim.
|
||||
- [x] goal: supervisor distinguishes agent idleness from tracked background work
|
||||
- Borrow existing process/subagent tracker queries; report unavailable trackers as unknown.
|
||||
- failure modes: approving while a tracked job runs, treating a local queue as a dependency of remote work.
|
||||
- deliverable: idle/running/unknown status cases and a blocker-diagnosis scenario.
|
||||
- evidence: the same validation log checks active and unavailable tracker reports and rejection of approval with unknown background state. Actual independent diagnosis of the queue mistake remains a behavioral acceptance task.
|
||||
- [x] goal: role model choices persist
|
||||
- Borrow planning/worker/supervisor model preference behavior without changing active user settings.
|
||||
- failure modes: automatic model changes overwrite user choices; a missing model silently substitutes another.
|
||||
- deliverable: isolated preference restoration and explicit unavailable-model errors.
|
||||
- evidence: [45-test validation](../reviews/20260908-role-model-validation.txt) restores three distinct role choices, ignores automatic restore events, and leaves an unavailable saved choice unchanged. Preferences are project-local; active user settings were not edited.
|
||||
|
||||
## Verification
|
||||
|
||||
Run project tests, typecheck, and lint before commits. Preserve full command output. Keep each feature in a separate commit where practical and push finished changes. Inspect the transferred code rather than equating tests or source size with quality. Keep the requested independent evidence judge when combining implementations; do not silently remove it.
|
||||
|
||||
The earlier asynchronous subagent failure (missing pi-client/unix) prevented the original review. A later parent retry completed independent review run `5c8c2017-a92f-4a5f-baf6-f441f9b50495`; its [findings are preserved with attribution](../reviews/20260908-independent-supervision-bug-review.md). Behavioral acceptance requires observed useful judgment, not merely matching prompt strings. Cost savings require a measured comparison and remain unproven.
|
||||
|
||||
## Independent review follow-up
|
||||
|
||||
- [x] Verify F1/F2/F4 lifecycle failures and implement explicit recovery without fallback models or automatic pane replacement.
|
||||
- [x] Address F3 inactive bindings and preserve synchronous handoff-before-ack ordering. Pending transport frames retry; end-to-end durable delivery is not guaranteed.
|
||||
- [ ] F3 deeper delivery confirmation: Pi's void adapter can ack before an asynchronous enqueue failure. Future UAT must inject that failure, avoid reporting confirmed model delivery, and keep the instruction recoverable. See the [SDK source-backed limitation](../reviews/20260908-review-fixes.md). Parent approved keeping this protocol expansion out of the current fix commit.
|
||||
- [x] Remove F5 general Intercom actuator, reject F6 nested placeholders, and correct F7 goal/log hashing boundary.
|
||||
- [x] Add focused regressions and inspect final full test/typecheck/lint/build output. [57-test evidence](../reviews/20260908-review-fixes-validation.txt); [initial child-environment failure and correction](../reviews/20260908-review-fixes-initial-validation.md).
|
||||
- [x] Parent independent post-change review of `1668c94` completed and found P1–P5; [attributed review](../reviews/20260908-independent-post-fix-review.md).
|
||||
- [x] Fix P1–P5: explicit hello request/reply, not-ready until worker-model success, peer-specific recovery guidance, one current-plan boundary and cancellation-safe Ready waits. [Disposition and provenance](../reviews/20260908-handshake-fixes.md); [67-test full validation](../reviews/20260908-handshake-validation.txt). Real two-adapter regressions replace reliance on automatic hello replies for handshake acceptance.
|
||||
- [x] F8 usage observability: warn once for an unavailable usage result, not for normal post-compaction null tokens. No estimator or changed auto-compaction policy.
|
||||
- [ ] Parent independent review of these handshake fixes before push. F3 durable enqueue confirmation stays open; F8 persistent unknown/null usage, F9 trusted-repo guardrails, and native/UI/behavioral gaps remain documented.
|
||||
|
||||
Recovery operations were exercised only with isolated mocks/native test processes; existing user panes were not operated. Implementation worker commits locally only; parent owns review and push.
|
||||
|
||||
-- Pi/OpenAI
|
||||
@@ -0,0 +1,105 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run --run test/force-approval-flow.test.ts test/approval.test.ts test/supervisor-session.test.ts test/goals-flow.test.ts --reporter=verbose
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
✓ test/approval.test.ts > hashes only the current goal, excluding the log, interview, and their historical goal text 6ms
|
||||
stdout | test/force-approval-flow.test.ts > force ApproveGoal -> CompleteGoal accepts only the reviewed dirty state without committing or modifying it
|
||||
Force UAT: paired real handlers accepted unchanged tracked + untracked dirty content; Git HEAD and user files stayed unchanged.
|
||||
|
||||
✓ test/force-approval-flow.test.ts > force ApproveGoal -> CompleteGoal accepts only the reviewed dirty state without committing or modifying it 108ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > reports actual idle state, invalidates stopped views on start, and stops completed plans 41ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > preserves drafts, records the interview, and keeps planning read-only 20ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > forks a visible supervisor on Ready and keeps the main session as worker 35ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed tracked content 99ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > starts work only after the supervisor launcher resolves 24ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > delivers an Intercom instruction to the worker 25ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > closes the supervisor on clear but keeps the plan file 23ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > accepts only an approval for the exact clean commit and goal block 64ms
|
||||
✓ test/goals-flow.test.ts > process role > keeps subagent children and visible supervisors out of the worker extension 0ms
|
||||
✓ test/goals-flow.test.ts > restores working linkage even when its remembered model is unavailable, and supports explicit recovery 24ms
|
||||
✓ test/goals-flow.test.ts > restores planning linkage even when its remembered model is unavailable, and supports explicit recovery 23ms
|
||||
✓ test/goals-flow.test.ts > shows a missing resumed supervisor, pauses writes, and automatically unpauses when that peer returns 25ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed untracked content 82ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed new untracked 83ms
|
||||
✓ test/goals-flow.test.ts > times out stale Ready retries in five seconds, without replacing the pane automatically 35ms
|
||||
✓ test/goals-flow.test.ts > explicitly restarts only the tracked pane, keeps the plan, and invalidates old approval binding 24ms
|
||||
✓ test/goals-flow.test.ts > does not persist startup results or launch work after session shutdown 21ms
|
||||
✓ test/goals-flow.test.ts > keeps a failed Ready model not-ready and recovers the same real supervisor binding 26ms
|
||||
✓ test/goals-flow.test.ts > points a present-but-paused peer recovery at the supervisor pane 17ms
|
||||
✓ test/goals-flow.test.ts > clear during the initial Ready wait cancels immediately and cannot resurrect the plan 21ms
|
||||
✓ test/goals-flow.test.ts > clear before the launcher resolves rejects late pane callbacks without restoring the binding 21ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed deleted untracked 62ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed index only 65ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed rename 74ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed HEAD 80ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed goal 79ms
|
||||
✓ test/force-approval-flow.test.ts > force does not bypass the evidence gate 44ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > restores monitoring and read-only tools without replaying persisted views 10ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > renders all advice in real Pi tool rows, including collapsed and restored rows 43ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > asks for judgment and useful recaps without inventing instructions 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes readiness only after removing writing tools 7ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > compacts a large planning fork before writing readiness 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > does not become ready when initial compaction fails 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes a durable worker instruction 2ms
|
||||
✓ test/force-approval-flow.test.ts > force does not bypass the verification gate 47ms
|
||||
✓ test/force-approval-flow.test.ts > force does not bypass the stopped view gate 29ms
|
||||
✓ test/force-approval-flow.test.ts > force does not bypass the tool call gate 21ms
|
||||
✓ test/force-approval-flow.test.ts > force does not bypass the unknown tracker gate 27ms
|
||||
✓ test/force-approval-flow.test.ts > force does not bypass the active tracker gate 28ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > records approval only from a stopped view with evidence and no active work 98ms
|
||||
✓ test/supervisor-session.test.ts > blocks the general intercom actuator even if enabled after startup 2ms
|
||||
✓ test/supervisor-session.test.ts > keeps a supervisor unready after model restoration failure, then recovers explicitly without substituting a model 2ms
|
||||
✓ test/supervisor-session.test.ts > warns once on unavailable usage but stays quiet for Pi's post-compaction null token sample 3ms
|
||||
|
||||
Test Files 4 passed (4)
|
||||
Tests 45 passed (45)
|
||||
Start at 06:14:52
|
||||
Duration 1.54s (transform 492ms, setup 0ms, import 2.27s, tests 1.58s, environment 0ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
src/approval.ts:42:7 lint/suspicious/noImplicitAnyLet ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
× This variable implicitly has the any type.
|
||||
|
||||
40 │ const files = paths.map((path): WorktreeSnapshot["files"][number] => {
|
||||
41 │ const fullPath = join(repoRoot, path);
|
||||
> 42 │ let stat;
|
||||
│ ^^^^
|
||||
43 │ try { stat = lstatSync(fullPath); }
|
||||
44 │ catch (error) {
|
||||
|
||||
i Variable declarations without type annotation and initialization implicitly have the any type. Declare a type or initialize the variable with some value.
|
||||
|
||||
|
||||
src/approval.ts:56:12 lint/suspicious/noAssignInExpressions ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
× The assignment should not be in an expression.
|
||||
|
||||
54 │ const buffer = Buffer.alloc(256 * 1024);
|
||||
55 │ let bytes: number;
|
||||
> 56 │ while ((bytes = readSync(fd, buffer, 0, buffer.length, null)) > 0) hash.update(buffer.subarray(0, bytes));
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
57 │ } finally { closeSync(fd); }
|
||||
58 │ return { path, kind: "file", mode, contentHash: hash.digest("hex") };
|
||||
|
||||
i The use of assignments in expressions is confusing.
|
||||
Expressions are often considered as side-effect free.
|
||||
|
||||
|
||||
Checked 33 files in 39ms. No fixes applied.
|
||||
Found 2 errors.
|
||||
check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
× Some errors were emitted while running checks.
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
Before validation: pre-existing dirty evidence checksums
|
||||
a8f1facd16fd6ff2938195a7adbdc3b4b1e1dea61533e90a39e5eb18d201e190 slop/reviews/review-fixes-native/supervisor-events.jsonl
|
||||
de5bb7171e71508c40e19f3929303a63f1fa0f2ea7789a2bcc8728ea038310d2 slop/reviews/review-fixes-native/worker-events.jsonl
|
||||
|
||||
Command: env -u PI_SUBAGENT_CHILD -u PI_GOALS_ROLE -u PI_GOALS_EVIDENCE_DIR npm test -- --reporter=verbose
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run --reporter=verbose
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
✓ test/package-agent.test.ts > package manifest > includes the extension without registering a packaged subagent 3ms
|
||||
✓ test/prompts.test.ts > planning prompt > requires fact finding or a focused question before a goal 2ms
|
||||
✓ test/prompts.test.ts > planning prompt > restores the same rule after compaction 1ms
|
||||
✓ test/prompts.test.ts > planning prompt > anchors work and sign-off to the user-visible result 0ms
|
||||
✓ test/worker-view.test.ts > keeps human direction and source location while sending only new messages 3ms
|
||||
✓ test/worker-view.test.ts > restarts after compaction and does not report historical tool calls as active 1ms
|
||||
✓ test/worker-view.test.ts > bounds serialized Unicode and quoted logs while marking omissions 3ms
|
||||
stdout | test/role-models.test.ts > remembers each role without automatic switching overwriting another role
|
||||
Role preferences restored: planning=planner, worker=small-worker, supervisor=astra; restore events did not overwrite the choice.
|
||||
|
||||
✓ test/role-models.test.ts > remembers each role without automatic switching overwriting another role 9ms
|
||||
✓ test/role-models.test.ts > fails on an unavailable remembered model without replacing the choice 4ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > receives exact advice once, acknowledges it and rejects unrelated peers 15ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > restores an unacknowledged steer on reconnect and stops replay after acknowledgment 5ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > advances the incremental overview only after acknowledgment 1ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > cancels a readiness wait on shutdown 2ms
|
||||
✓ test/intercom.test.ts > does not acknowledge a synchronous handoff failure, and retries the instruction 1ms
|
||||
✓ test/intercom.test.ts > detaches a completed binding and ignores its late advice without replay errors or false acceptance 1ms
|
||||
✓ test/background.test.ts > reports tracked running work, rather than equating idle agent with finished jobs 110ms
|
||||
✓ test/background.test.ts > distinguishes missing providers from an unavailable installed tracker 4ms
|
||||
✓ test/intercom-handshake.test.ts > re-handshakes unchanged peers in either direction without hello ping-pong or lost advice 11ms
|
||||
✓ test/intercom-handshake.test.ts > replays pending advice and views across either role's own readiness transition 1ms
|
||||
✓ test/intercom-handshake.test.ts > cancels pending waits immediately on detach or reconfiguration 2ms
|
||||
✓ test/herdr.test.ts > supervisor pane command > forks the planning session with pi-goals owning its Intercom dependency 8ms
|
||||
✓ test/herdr.test.ts > supervisor pane command > accepts Herdr's text version output and stale pane cleanup 55ms
|
||||
✓ test/approval.test.ts > fingerprints literal unusual paths, binary bytes, symlink targets, modes and deletions 177ms
|
||||
✓ test/approval.test.ts > hashes only the current goal, excluding the log, interview, and their historical goal text 1ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > keeps the title, user voice and goals 1ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > drops the log, the learnings and the unlimited appendix 0ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > returns the whole plan when there is no ## Log yet (a fresh draft) 0ms
|
||||
✓ test/fold.test.ts > openSubtasks (the widget shows the next action, so the plan IS the task list) > lists the active goal's open and in-progress subtasks, stopping at the next goal 1ms
|
||||
✓ test/fold.test.ts > openSubtasks (the widget shows the next action, so the plan IS the task list) > does not leak subtasks from the goal below 1ms
|
||||
✓ test/fold.test.ts > does not show historical Log subtasks under the last active goal 0ms
|
||||
✓ test/append-log.test.ts > appendLog (the extension's only plan-file write) > creates ## Log at EOF when absent 2ms
|
||||
✓ test/append-log.test.ts > appendLog (the extension's only plan-file write) > appends after the last existing log line, before any following header 1ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > ticks the exact-matching goal line, case-insensitive, leaving subtasks alone 2ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null on wording drift (fuzzy matching is the judge's job, not TypeScript's) 0ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null when the subject matches more than one goal line 0ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > ignores a historical duplicate below the Log and leaves it unchanged 0ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > reports actual idle state, invalidates stopped views on start, and stops completed plans 51ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > preserves drafts, records the interview, and keeps planning read-only 25ms
|
||||
stdout | test/intercom-broker.test.ts > exchanges readiness, views and exact advice over a real isolated pi-intercom broker
|
||||
Intercom broker: readiness confirmed; exact worker view and supervisor advice received.
|
||||
|
||||
stdout | test/force-approval-flow.test.ts > force ApproveGoal -> CompleteGoal accepts only the reviewed dirty state without committing or modifying it
|
||||
Force UAT: paired real handlers accepted unchanged tracked + untracked dirty content; Git HEAD and user files stayed unchanged.
|
||||
|
||||
✓ test/force-approval-flow.test.ts > force ApproveGoal -> CompleteGoal accepts only the reviewed dirty state without committing or modifying it 124ms
|
||||
✓ test/intercom-broker.test.ts > exchanges readiness, views and exact advice over a real isolated pi-intercom broker 425ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > forks a visible supervisor on Ready and keeps the main session as worker 27ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > starts work only after the supervisor launcher resolves 28ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > delivers an Intercom instruction to the worker 20ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > closes the supervisor on clear but keeps the plan file 22ms
|
||||
✓ test/force-approval-flow.test.ts > keeps ordinary clean approval unchanged and rechecks runtime state at forced completion 104ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > accepts only an approval for the exact clean commit and goal block 74ms
|
||||
✓ test/goals-flow.test.ts > process role > keeps subagent children and visible supervisors out of the worker extension 0ms
|
||||
✓ test/goals-flow.test.ts > restores working linkage even when its remembered model is unavailable, and supports explicit recovery 27ms
|
||||
✓ test/goals-flow.test.ts > restores planning linkage even when its remembered model is unavailable, and supports explicit recovery 26ms
|
||||
✓ test/goals-flow.test.ts > shows a missing resumed supervisor, pauses writes, and automatically unpauses when that peer returns 25ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed tracked content 78ms
|
||||
✓ test/goals-flow.test.ts > times out stale Ready retries in five seconds, without replacing the pane automatically 32ms
|
||||
✓ test/goals-flow.test.ts > explicitly restarts only the tracked pane, keeps the plan, and invalidates old approval binding 31ms
|
||||
✓ test/goals-flow.test.ts > does not persist startup results or launch work after session shutdown 27ms
|
||||
✓ test/goals-flow.test.ts > keeps a failed Ready model not-ready and recovers the same real supervisor binding 32ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed untracked content 76ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed new untracked 70ms
|
||||
✓ test/goals-flow.test.ts > points a present-but-paused peer recovery at the supervisor pane 21ms
|
||||
✓ test/goals-flow.test.ts > clear during the initial Ready wait cancels immediately and cannot resurrect the plan 27ms
|
||||
✓ test/goals-flow.test.ts > clear before the launcher resolves rejects late pane callbacks without restoring the binding 19ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed deleted untracked 61ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed index only 63ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed index contents 122ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed rename 73ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed HEAD 67ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > restores monitoring and read-only tools without replaying persisted views 9ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > renders all advice in real Pi tool rows, including collapsed and restored rows 26ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > asks for judgment and useful recaps without inventing instructions 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes readiness only after removing writing tools 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > compacts a large planning fork before writing readiness 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > does not become ready when initial compaction fails 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes a durable worker instruction 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > records approval only from a stopped view with evidence and no active work 77ms
|
||||
✓ test/supervisor-session.test.ts > blocks the general intercom actuator even if enabled after startup 2ms
|
||||
✓ test/supervisor-session.test.ts > keeps a supervisor unready after model restoration failure, then recovers explicitly without substituting a model 2ms
|
||||
✓ test/supervisor-session.test.ts > warns once on unavailable usage but stays quiet for Pi's post-compaction null token sample 3ms
|
||||
✓ test/force-approval-flow.test.ts > invalidates forced approval after changed goal 86ms
|
||||
✓ test/force-approval-flow.test.ts > force does not bypass the evidence gate 51ms
|
||||
✓ test/force-approval-flow.test.ts > force does not bypass the verification gate 42ms
|
||||
✓ test/force-approval-flow.test.ts > force does not bypass the stopped view gate 24ms
|
||||
✓ test/force-approval-flow.test.ts > force does not bypass the tool call gate 25ms
|
||||
✓ test/force-approval-flow.test.ts > force does not bypass the unknown tracker gate 25ms
|
||||
✓ test/force-approval-flow.test.ts > force does not bypass the active tracker gate 27ms
|
||||
✓ test/rpc-review.test.ts > RPC review flow > opens Refine's editor before it starts the revision turn 1856ms
|
||||
stdout | test/native-intercom.test.ts > runs a forked Pi supervisor and receives its exact instruction in another Pi session
|
||||
Native Pi pair: fork retained planning context; SteerWorker delivered exactly: Read the real outputs before declaring completion.
|
||||
|
||||
✓ test/native-intercom.test.ts > runs a forked Pi supervisor and receives its exact instruction in another Pi session 2941ms
|
||||
|
||||
Test Files 18 passed (18)
|
||||
Tests 85 passed (85)
|
||||
Start at 06:18:03
|
||||
Duration 3.48s (transform 3.77s, setup 0ms, import 7.89s, tests 7.45s, environment 2ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 33 files in 68ms. No fixes applied.
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 build
|
||||
> tsc
|
||||
|
||||
|
||||
After validation: pre-existing dirty evidence checksums
|
||||
a8f1facd16fd6ff2938195a7adbdc3b4b1e1dea61533e90a39e5eb18d201e190 slop/reviews/review-fixes-native/supervisor-events.jsonl
|
||||
de5bb7171e71508c40e19f3929303a63f1fa0f2ea7789a2bcc8728ea038310d2 slop/reviews/review-fixes-native/worker-events.jsonl
|
||||
@@ -0,0 +1,122 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run --reporter=verbose
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
✓ test/package-agent.test.ts > package manifest > includes the extension without registering a packaged subagent 3ms
|
||||
stdout | test/role-models.test.ts > remembers each role without automatic switching overwriting another role
|
||||
Role preferences restored: planning=planner, worker=small-worker, supervisor=astra; restore events did not overwrite the choice.
|
||||
|
||||
✓ test/intercom.test.ts > pi-intercom transport > receives exact advice once, acknowledges it and rejects unrelated peers 4ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > restores an unacknowledged steer on reconnect and stops replay after acknowledgment 2ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > advances the incremental overview only after acknowledgment 1ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > cancels a readiness wait on shutdown 1ms
|
||||
✓ test/intercom.test.ts > does not acknowledge a synchronous handoff failure, and retries the instruction 1ms
|
||||
✓ test/intercom.test.ts > detaches a completed binding and ignores its late advice without replay errors or false acceptance 1ms
|
||||
✓ test/role-models.test.ts > remembers each role without automatic switching overwriting another role 32ms
|
||||
✓ test/role-models.test.ts > fails on an unavailable remembered model without replacing the choice 5ms
|
||||
✓ test/intercom-handshake.test.ts > re-handshakes unchanged peers in either direction without hello ping-pong or lost advice 9ms
|
||||
✓ test/intercom-handshake.test.ts > replays pending advice and views across either role's own readiness transition 2ms
|
||||
✓ test/intercom-handshake.test.ts > cancels pending waits immediately on detach or reconfiguration 2ms
|
||||
✓ test/worker-view.test.ts > keeps human direction and source location while sending only new messages 3ms
|
||||
✓ test/worker-view.test.ts > restarts after compaction and does not report historical tool calls as active 1ms
|
||||
✓ test/worker-view.test.ts > bounds serialized Unicode and quoted logs while marking omissions 3ms
|
||||
✓ test/approval.test.ts > hashes only the current goal, excluding the log, interview, and their historical goal text 8ms
|
||||
✓ test/prompts.test.ts > planning prompt > requires fact finding or a focused question before a goal 2ms
|
||||
✓ test/prompts.test.ts > planning prompt > restores the same rule after compaction 2ms
|
||||
✓ test/prompts.test.ts > planning prompt > anchors work and sign-off to the user-visible result 0ms
|
||||
✓ test/herdr.test.ts > supervisor pane command > forks the planning session with pi-goals owning its Intercom dependency 3ms
|
||||
✓ test/herdr.test.ts > supervisor pane command > accepts Herdr's text version output and stale pane cleanup 51ms
|
||||
✓ test/background.test.ts > reports tracked running work, rather than equating idle agent with finished jobs 52ms
|
||||
✓ test/background.test.ts > distinguishes missing providers from an unavailable installed tracker 5ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > keeps the title, user voice and goals 2ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > drops the log, the learnings and the unlimited appendix 1ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > returns the whole plan when there is no ## Log yet (a fresh draft) 0ms
|
||||
✓ test/fold.test.ts > openSubtasks (the widget shows the next action, so the plan IS the task list) > lists the active goal's open and in-progress subtasks, stopping at the next goal 1ms
|
||||
✓ test/fold.test.ts > openSubtasks (the widget shows the next action, so the plan IS the task list) > does not leak subtasks from the goal below 1ms
|
||||
✓ test/fold.test.ts > does not show historical Log subtasks under the last active goal 0ms
|
||||
✓ test/append-log.test.ts > appendLog (the extension's only plan-file write) > creates ## Log at EOF when absent 2ms
|
||||
✓ test/append-log.test.ts > appendLog (the extension's only plan-file write) > appends after the last existing log line, before any following header 0ms
|
||||
stdout | test/intercom-broker.test.ts > exchanges readiness, views and exact advice over a real isolated pi-intercom broker
|
||||
Intercom broker: readiness confirmed; exact worker view and supervisor advice received.
|
||||
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > ticks the exact-matching goal line, case-insensitive, leaving subtasks alone 2ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null on wording drift (fuzzy matching is the judge's job, not TypeScript's) 0ms
|
||||
× test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null when the subject matches more than one goal line 5ms
|
||||
→ expected '# Plan\n\n## Goals\n\n1. [/] goal: Im…' to be null
|
||||
✓ test/intercom-broker.test.ts > exchanges readiness, views and exact advice over a real isolated pi-intercom broker 254ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > reports actual idle state, invalidates stopped views on start, and stops completed plans 59ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > preserves drafts, records the interview, and keeps planning read-only 33ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > forks a visible supervisor on Ready and keeps the main session as worker 27ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > starts work only after the supervisor launcher resolves 38ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > delivers an Intercom instruction to the worker 41ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > closes the supervisor on clear but keeps the plan file 27ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > accepts only an approval for the exact clean commit and goal block 61ms
|
||||
✓ test/goals-flow.test.ts > process role > keeps subagent children and visible supervisors out of the worker extension 0ms
|
||||
✓ test/goals-flow.test.ts > restores working linkage even when its remembered model is unavailable, and supports explicit recovery 21ms
|
||||
✓ test/goals-flow.test.ts > restores planning linkage even when its remembered model is unavailable, and supports explicit recovery 23ms
|
||||
✓ test/goals-flow.test.ts > shows a missing resumed supervisor, pauses writes, and automatically unpauses when that peer returns 26ms
|
||||
✓ test/goals-flow.test.ts > times out stale Ready retries in five seconds, without replacing the pane automatically 29ms
|
||||
✓ test/goals-flow.test.ts > explicitly restarts only the tracked pane, keeps the plan, and invalidates old approval binding 24ms
|
||||
✓ test/goals-flow.test.ts > does not persist startup results or launch work after session shutdown 23ms
|
||||
✓ test/goals-flow.test.ts > keeps a failed Ready model not-ready and recovers the same real supervisor binding 31ms
|
||||
✓ test/goals-flow.test.ts > points a present-but-paused peer recovery at the supervisor pane 21ms
|
||||
✓ test/goals-flow.test.ts > clear during the initial Ready wait cancels immediately and cannot resurrect the plan 25ms
|
||||
✓ test/goals-flow.test.ts > clear before the launcher resolves rejects late pane callbacks without restoring the binding 21ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > restores monitoring and read-only tools without replaying persisted views 8ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > renders all advice in real Pi tool rows, including collapsed and restored rows 21ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > asks for judgment and useful recaps without inventing instructions 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes readiness only after removing writing tools 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > compacts a large planning fork before writing readiness 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > does not become ready when initial compaction fails 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes a durable worker instruction 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > records approval only from a stopped view with evidence and no active work 96ms
|
||||
✓ test/supervisor-session.test.ts > blocks the general intercom actuator even if enabled after startup 3ms
|
||||
✓ test/supervisor-session.test.ts > keeps a supervisor unready after model restoration failure, then recovers explicitly without substituting a model 2ms
|
||||
✓ test/supervisor-session.test.ts > warns once on unavailable usage but stays quiet for Pi's post-compaction null token sample 3ms
|
||||
✓ test/rpc-review.test.ts > RPC review flow > opens Refine's editor before it starts the revision turn 1541ms
|
||||
stdout | test/native-intercom.test.ts > runs a forked Pi supervisor and receives its exact instruction in another Pi session
|
||||
Native Pi pair: fork retained planning context; SteerWorker delivered exactly: Read the real outputs before declaring completion.
|
||||
|
||||
✓ test/native-intercom.test.ts > runs a forked Pi supervisor and receives its exact instruction in another Pi session 2503ms
|
||||
|
||||
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
|
||||
|
||||
FAIL test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null when the subject matches more than one goal line
|
||||
AssertionError: expected '# Plan\n\n## Goals\n\n1. [/] goal: Im…' to be null
|
||||
|
||||
- Expected:
|
||||
null
|
||||
|
||||
+ Received:
|
||||
"# Plan
|
||||
|
||||
## Goals
|
||||
|
||||
1. [/] goal: Implement the cache layer
|
||||
- tasks:
|
||||
1. [x] wire client
|
||||
2. [x] goal: Ship the docs
|
||||
|
||||
## Log
|
||||
3. [ ] goal: Ship the docs
|
||||
"
|
||||
|
||||
❯ test/tick-goal.test.ts:30:42
|
||||
28| it("returns null when the subject matches more than one goal line", (…
|
||||
29| const dup = `${plan}3. [ ] goal: Ship the docs\n`;
|
||||
30| expect(tickGoal(dup, "Ship the docs")).toBeNull();
|
||||
| ^
|
||||
31| });
|
||||
32| });
|
||||
|
||||
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
|
||||
|
||||
|
||||
Test Files 1 failed | 16 passed (17)
|
||||
Tests 1 failed | 65 passed (66)
|
||||
Start at 19:22:13
|
||||
Duration 2.84s (transform 2.51s, setup 0ms, import 5.72s, tests 5.21s, environment 2ms)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Post-review P1–P5 fixes — Pi/OpenAI implementation worker
|
||||
|
||||
Baseline: `1668c94`. Independent review: [attributed, preserved source](20260908-independent-post-fix-review.md), reviewer run `bdb93a2e-52f4-4a6d-bac2-7c9eb48118b5`.
|
||||
|
||||
Read AGENTS.md and installed Pi extension docs for lifecycle teardown, commands, model selection and compaction before editing. No user pane/session was operated; tests use isolated mocks/brokers/native Pi processes. No nested delegation or push.
|
||||
|
||||
## Dispositions
|
||||
|
||||
- **P1:** Replaced change-gated hello replies with an explicit request/reply bit. Every request receives one reply even when the peer state is unchanged; a reply never elicits another hello. Each successful exchange retries only still-pending instructions/current view, including after either side's own readiness transition. Repeated wire frames are allowed and deduplicated at the receiving adapter; this is not a new durable-delivery claim. `test/intercom-handshake.test.ts` wires **two real GoalIntercom adapters**, not an auto-ready peer. It checks repeated worker and supervisor reconfiguration, concurrent reset (four hello frames), single-sided reset (two frames), exact advice handoff, own-ready pause/resume, pending advice/view replay, and reconnect deduplication. Both peers must load the updated transport; mixed-version reconnect is not claimed supported.
|
||||
- **P2:** The worker configures not-ready during startup/recovery. Startup can await peer readiness without claiming implementation readiness. It announces ready only after worker model restoration and the phase transition to working. Model failures remain not-ready. The Ready/retry flow test uses the same two real adapters and proves unavailable worker model → not-ready → `/model` + reconnect → Ready → working on the **same** binding/pane, followed by successful advice after healthy reconnect. Planning reconnect alone does not authorize implementation.
|
||||
- **P3:** A known peer with incomplete readiness is distinguished from an absent peer. Guidance points to the supervisor pane's compaction/model diagnostics and `/model` + `/goals reconnect`, rather than treating every pause as disconnection. Regression checks widget and prompt guidance.
|
||||
- **P4:** `src/plan.ts` now owns the single goal-line and Log-fold definitions. Widget scanning, subtasks, goal ticking and approval use the same current-plan boundary. Tick still rejects duplicate active-region matches; historical Log copies remain byte-for-byte unchanged. The existing approval/sign-off flow now includes duplicate/historical goal lines in the Log, succeeds and ends the active plan instead of reopening historical goals.
|
||||
- **P5:** Detach/reconfigure immediately reject old readiness waiters. A small Ready-attempt identity plus plan-version guard invalidates asynchronous startup results on clear/recovery/replacement; stale menu/editor responses are also ignored. Regressions clear while the five-minute initial wait is pending (no clock advance needed to settle), then advance five minutes and verify no resurrection; a second test clears before the launcher callback resolves and verifies no late binding/pane persistence or work launch. No automatic late-pane kill was added.
|
||||
- **F8 observability:** One warning per runtime if the entire usage result is unavailable at a settled check. No warning for Pi's ordinary post-compaction `tokens: null` sample. No speculative token estimator, new compaction policy, or change to Pi auto-compaction. Persistent null usage still cannot trigger the custom 100k check.
|
||||
|
||||
## Verification and changed old assertions
|
||||
|
||||
[Full successful commands/output](20260908-handshake-validation.txt): **67 tests passed in 17 files**, followed by successful typecheck, lint, build and `git diff --check`. The exact main-session test command unsets `PI_SUBAGENT_CHILD` and `PI_GOALS_ROLE` and sets a **fresh explicit** `PI_GOALS_EVIDENCE_DIR` to `slop/reviews/handshake-native`.
|
||||
|
||||
Two intermediate failures are preserved, not counted as passes:
|
||||
|
||||
1. [Handshake-focused run](20260908-handshake-initial-validation.txt): 26 passed/1 failed. The old assertion demanded exactly one outbound wire retry after configure+markReady. These now generate separate request/reply exchanges, which can retry the same still-unacked id more than once before its ack. Updated assertion requires at least one retry, every retry's exact id/text, and no further retries after ack. Two-real-adapter tests independently require exactly one user handoff/view callback after duplicate wire delivery.
|
||||
2. [First full boundary run](20260908-handshake-boundary-initial-validation.txt): 65 passed/1 failed. Its old duplicate-goal fixture appended the duplicate **below `## Log`**, precisely the P4 behavior being corrected. The duplicate-rejection test now inserts the duplicate above the fold and still requires null; an added test requires historical copies below the fold to be ignored and unchanged.
|
||||
|
||||
An intermediate typecheck passed; lint initially flagged import order and a nested assignment. Those were corrected; the full final lint passed with no fixes applied.
|
||||
|
||||
[Native log inspection](20260908-handshake-log-inspection.txt) reads the final worker/supervisor event files, verifies zero error records and empty stderr, and matches the outgoing instruction, worker incoming instruction and adapter ack id `22068654-6e7c-4328-890f-9382b25c8ea7`. The worker user message is exactly `[supervisor] Read the real outputs before declaring completion.` This deterministic native test proves context retention/routing/tool exposure, not autonomous judgment or durable enqueue guarantees.
|
||||
|
||||
## Evidence provenance caveat
|
||||
|
||||
At the first inspection, these two tracked files were **already dirty**:
|
||||
|
||||
- `slop/reviews/review-fixes-native/supervisor-events.jsonl`
|
||||
- `slop/reviews/review-fixes-native/worker-events.jsonl`
|
||||
|
||||
The inherited `PI_GOALS_EVIDENCE_DIR` pointed there. The first full test run accidentally refreshed them again. Their pre-task uncommitted bytes were not captured, so it is not established that all differences from HEAD were produced by this worker. They are left **unstaged and uncommitted**, not restored over unknown prior edits. The refreshed copies were separately preserved under this worker's output directory, `scratch-refreshed-prior-evidence/`. Final evidence uses only the fresh `handshake-native/` directory and is not mixed with these prior paths.
|
||||
|
||||
## Still open / acceptance limits
|
||||
|
||||
**F3 async enqueue confirmation remains open.** Pi's void `sendUserMessage` wrapper can return before a later async enqueue rejection. An adapter ack is not durable enqueue, model receipt, or execution confirmation. No correlated-receipt protocol was added; the future forced-async-rejection UAT remains required. A synchronous handoff failure still remains unacked for retry.
|
||||
|
||||
Parent/reviewer owns independent post-change review and push. Rendered Herdr acceptance, the complete two-native-session ApproveGoal → CompleteGoal chain, fresh-shell role recovery without launcher environment, useful independent judgment and cost savings remain unproven. Planning/pause shell gates remain trusted-repo guardrails, not a security sandbox. If a pane split finishes after cancellation, it can remain untracked for human inspection; this change prevents stale state resurrection rather than operating a late pane automatically.
|
||||
@@ -0,0 +1,77 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run --run test/intercom-handshake.test.ts test/intercom.test.ts test/goals-flow.test.ts --reporter=verbose
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
✓ test/intercom-handshake.test.ts > re-handshakes unchanged peers in either direction without hello ping-pong or lost advice 7ms
|
||||
✓ test/intercom-handshake.test.ts > replays pending advice and views across either role's own readiness transition 1ms
|
||||
✓ test/intercom-handshake.test.ts > cancels pending waits immediately on detach or reconfiguration 1ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > receives exact advice once, acknowledges it and rejects unrelated peers 4ms
|
||||
× test/intercom.test.ts > pi-intercom transport > restores an unacknowledged steer on reconnect and stops replay after acknowledgment 7ms
|
||||
→ expected [ { binding: 'binding', …(4) }, …(1) ] to match object [ { …(2) } ]
|
||||
✓ test/intercom.test.ts > pi-intercom transport > advances the incremental overview only after acknowledgment 1ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > cancels a readiness wait on shutdown 1ms
|
||||
✓ test/intercom.test.ts > does not acknowledge a synchronous handoff failure, and retries the instruction 1ms
|
||||
✓ test/intercom.test.ts > detaches a completed binding and ignores its late advice without replay errors or false acceptance 3ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > reports actual idle state, invalidates stopped views on start, and stops completed plans 34ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > preserves drafts, records the interview, and keeps planning read-only 18ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > forks a visible supervisor on Ready and keeps the main session as worker 24ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > starts work only after the supervisor launcher resolves 22ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > delivers an Intercom instruction to the worker 18ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > closes the supervisor on clear but keeps the plan file 23ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > accepts only an approval for the exact clean commit and goal block 47ms
|
||||
✓ test/goals-flow.test.ts > process role > keeps subagent children and visible supervisors out of the worker extension 0ms
|
||||
✓ test/goals-flow.test.ts > restores working linkage even when its remembered model is unavailable, and supports explicit recovery 18ms
|
||||
✓ test/goals-flow.test.ts > restores planning linkage even when its remembered model is unavailable, and supports explicit recovery 16ms
|
||||
✓ test/goals-flow.test.ts > shows a missing resumed supervisor, pauses writes, and automatically unpauses when that peer returns 17ms
|
||||
✓ test/goals-flow.test.ts > times out stale Ready retries in five seconds, without replacing the pane automatically 26ms
|
||||
✓ test/goals-flow.test.ts > explicitly restarts only the tracked pane, keeps the plan, and invalidates old approval binding 23ms
|
||||
✓ test/goals-flow.test.ts > does not persist startup results or launch work after session shutdown 23ms
|
||||
✓ test/goals-flow.test.ts > keeps a failed Ready model not-ready and recovers the same real supervisor binding 23ms
|
||||
✓ test/goals-flow.test.ts > points a present-but-paused peer recovery at the supervisor pane 18ms
|
||||
✓ test/goals-flow.test.ts > clear during the initial Ready wait cancels immediately and cannot resurrect the plan 21ms
|
||||
✓ test/goals-flow.test.ts > clear before the launcher resolves rejects late pane callbacks without restoring the binding 35ms
|
||||
|
||||
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
|
||||
|
||||
FAIL test/intercom.test.ts > pi-intercom transport > restores an unacknowledged steer on reconnect and stops replay after acknowledgment
|
||||
AssertionError: expected [ { binding: 'binding', …(4) }, …(1) ] to match object [ { …(2) } ]
|
||||
|
||||
- Expected
|
||||
+ Received
|
||||
|
||||
[
|
||||
{
|
||||
+ "binding": "binding",
|
||||
+ "id": "7f4566ea-f2ef-4a14-ad75-e138fc3a26a0",
|
||||
+ "kind": "steer",
|
||||
+ "role": "supervisor",
|
||||
+ "text": "Read the full output.",
|
||||
+ },
|
||||
+ {
|
||||
+ "binding": "binding",
|
||||
"id": "7f4566ea-f2ef-4a14-ad75-e138fc3a26a0",
|
||||
+ "kind": "steer",
|
||||
+ "role": "supervisor",
|
||||
"text": "Read the full output.",
|
||||
},
|
||||
]
|
||||
|
||||
❯ test/intercom.test.ts:45:76
|
||||
43| resumed.link.markReady();
|
||||
44| await resumed.link.waitReady();
|
||||
45| expect(resumed.fixture.sent.filter(message => message.kind === "stee…
|
||||
| ^
|
||||
46| resumed.fixture.receive({ binding: "binding", role: "worker", kind: …
|
||||
47| resumed.fixture.connect(false);
|
||||
|
||||
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
|
||||
|
||||
|
||||
Test Files 1 failed | 2 passed (3)
|
||||
Tests 1 failed | 26 passed (27)
|
||||
Start at 19:20:05
|
||||
Duration 861ms (transform 365ms, setup 0ms, import 583ms, tests 437ms, environment 0ms)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
slop/reviews/handshake-native/worker-events.jsonl: 22 events; 0 error records; stderr 0 bytes
|
||||
worker: ack received cf656aef-1404-4d5c-9a11-4fbd4ed5b5dc
|
||||
worker: ack received cf656aef-1404-4d5c-9a11-4fbd4ed5b5dc
|
||||
worker: in steer 22068654-6e7c-4328-890f-9382b25c8ea7
|
||||
worker: [{"type":"text","text":"[supervisor] Read the real outputs before declaring completion."}]
|
||||
slop/reviews/handshake-native/supervisor-events.jsonl: 21 events; 0 error records; stderr 0 bytes
|
||||
supervisor: out steer 22068654-6e7c-4328-890f-9382b25c8ea7
|
||||
supervisor: ack received 22068654-6e7c-4328-890f-9382b25c8ea7
|
||||
@@ -0,0 +1,107 @@
|
||||
+ env -u PI_SUBAGENT_CHILD -u PI_GOALS_ROLE PI_GOALS_EVIDENCE_DIR=/home/code/.pi/agent/git/github.com/wassname/pi-goals/slop/reviews/handshake-native npm test -- --reporter=verbose
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run --reporter=verbose
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
✓ test/background.test.ts > reports tracked running work, rather than equating idle agent with finished jobs 5ms
|
||||
✓ test/background.test.ts > distinguishes missing providers from an unavailable installed tracker 47ms
|
||||
✓ test/package-agent.test.ts > package manifest > includes the extension without registering a packaged subagent 3ms
|
||||
✓ test/herdr.test.ts > supervisor pane command > forks the planning session with pi-goals owning its Intercom dependency 3ms
|
||||
✓ test/herdr.test.ts > supervisor pane command > accepts Herdr's text version output and stale pane cleanup 26ms
|
||||
✓ test/prompts.test.ts > planning prompt > requires fact finding or a focused question before a goal 2ms
|
||||
✓ test/prompts.test.ts > planning prompt > restores the same rule after compaction 0ms
|
||||
✓ test/prompts.test.ts > planning prompt > anchors work and sign-off to the user-visible result 0ms
|
||||
✓ test/worker-view.test.ts > keeps human direction and source location while sending only new messages 3ms
|
||||
✓ test/worker-view.test.ts > restarts after compaction and does not report historical tool calls as active 1ms
|
||||
✓ test/worker-view.test.ts > bounds serialized Unicode and quoted logs while marking omissions 2ms
|
||||
stdout | test/role-models.test.ts > remembers each role without automatic switching overwriting another role
|
||||
Role preferences restored: planning=planner, worker=small-worker, supervisor=astra; restore events did not overwrite the choice.
|
||||
|
||||
✓ test/role-models.test.ts > remembers each role without automatic switching overwriting another role 14ms
|
||||
✓ test/role-models.test.ts > fails on an unavailable remembered model without replacing the choice 5ms
|
||||
✓ test/approval.test.ts > hashes only the current goal, excluding the log, interview, and their historical goal text 5ms
|
||||
✓ test/intercom-handshake.test.ts > re-handshakes unchanged peers in either direction without hello ping-pong or lost advice 9ms
|
||||
✓ test/intercom-handshake.test.ts > replays pending advice and views across either role's own readiness transition 1ms
|
||||
✓ test/intercom-handshake.test.ts > cancels pending waits immediately on detach or reconfiguration 2ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > receives exact advice once, acknowledges it and rejects unrelated peers 7ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > restores an unacknowledged steer on reconnect and stops replay after acknowledgment 4ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > advances the incremental overview only after acknowledgment 1ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > cancels a readiness wait on shutdown 1ms
|
||||
✓ test/intercom.test.ts > does not acknowledge a synchronous handoff failure, and retries the instruction 2ms
|
||||
✓ test/intercom.test.ts > detaches a completed binding and ignores its late advice without replay errors or false acceptance 1ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > ticks the exact-matching goal line, case-insensitive, leaving subtasks alone 2ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null on wording drift (fuzzy matching is the judge's job, not TypeScript's) 0ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null when the subject matches more than one goal line 0ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > ignores a historical duplicate below the Log and leaves it unchanged 0ms
|
||||
✓ test/append-log.test.ts > appendLog (the extension's only plan-file write) > creates ## Log at EOF when absent 4ms
|
||||
✓ test/append-log.test.ts > appendLog (the extension's only plan-file write) > appends after the last existing log line, before any following header 1ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > keeps the title, user voice and goals 2ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > drops the log, the learnings and the unlimited appendix 1ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > returns the whole plan when there is no ## Log yet (a fresh draft) 0ms
|
||||
✓ test/fold.test.ts > openSubtasks (the widget shows the next action, so the plan IS the task list) > lists the active goal's open and in-progress subtasks, stopping at the next goal 1ms
|
||||
✓ test/fold.test.ts > openSubtasks (the widget shows the next action, so the plan IS the task list) > does not leak subtasks from the goal below 1ms
|
||||
✓ test/fold.test.ts > does not show historical Log subtasks under the last active goal 0ms
|
||||
stdout | test/intercom-broker.test.ts > exchanges readiness, views and exact advice over a real isolated pi-intercom broker
|
||||
Intercom broker: readiness confirmed; exact worker view and supervisor advice received.
|
||||
|
||||
✓ test/goals-flow.test.ts > /goals flow > reports actual idle state, invalidates stopped views on start, and stops completed plans 47ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > preserves drafts, records the interview, and keeps planning read-only 24ms
|
||||
✓ test/intercom-broker.test.ts > exchanges readiness, views and exact advice over a real isolated pi-intercom broker 236ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > forks a visible supervisor on Ready and keeps the main session as worker 32ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > starts work only after the supervisor launcher resolves 29ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > delivers an Intercom instruction to the worker 26ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > closes the supervisor on clear but keeps the plan file 20ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > accepts only an approval for the exact clean commit and goal block 62ms
|
||||
✓ test/goals-flow.test.ts > process role > keeps subagent children and visible supervisors out of the worker extension 0ms
|
||||
✓ test/goals-flow.test.ts > restores working linkage even when its remembered model is unavailable, and supports explicit recovery 33ms
|
||||
✓ test/goals-flow.test.ts > restores planning linkage even when its remembered model is unavailable, and supports explicit recovery 24ms
|
||||
✓ test/goals-flow.test.ts > shows a missing resumed supervisor, pauses writes, and automatically unpauses when that peer returns 24ms
|
||||
✓ test/goals-flow.test.ts > times out stale Ready retries in five seconds, without replacing the pane automatically 25ms
|
||||
✓ test/goals-flow.test.ts > explicitly restarts only the tracked pane, keeps the plan, and invalidates old approval binding 42ms
|
||||
✓ test/goals-flow.test.ts > does not persist startup results or launch work after session shutdown 36ms
|
||||
✓ test/goals-flow.test.ts > keeps a failed Ready model not-ready and recovers the same real supervisor binding 31ms
|
||||
✓ test/goals-flow.test.ts > points a present-but-paused peer recovery at the supervisor pane 17ms
|
||||
✓ test/goals-flow.test.ts > clear during the initial Ready wait cancels immediately and cannot resurrect the plan 25ms
|
||||
✓ test/goals-flow.test.ts > clear before the launcher resolves rejects late pane callbacks without restoring the binding 19ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > restores monitoring and read-only tools without replaying persisted views 7ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > renders all advice in real Pi tool rows, including collapsed and restored rows 22ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > asks for judgment and useful recaps without inventing instructions 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes readiness only after removing writing tools 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > compacts a large planning fork before writing readiness 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > does not become ready when initial compaction fails 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes a durable worker instruction 1ms
|
||||
✓ test/rpc-review.test.ts > RPC review flow > opens Refine's editor before it starts the revision turn 1350ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > records approval only from a stopped view with evidence and no active work 87ms
|
||||
✓ test/supervisor-session.test.ts > blocks the general intercom actuator even if enabled after startup 2ms
|
||||
✓ test/supervisor-session.test.ts > keeps a supervisor unready after model restoration failure, then recovers explicitly without substituting a model 2ms
|
||||
✓ test/supervisor-session.test.ts > warns once on unavailable usage but stays quiet for Pi's post-compaction null token sample 2ms
|
||||
stdout | test/native-intercom.test.ts > runs a forked Pi supervisor and receives its exact instruction in another Pi session
|
||||
Native Pi pair: fork retained planning context; SteerWorker delivered exactly: Read the real outputs before declaring completion.
|
||||
|
||||
✓ test/native-intercom.test.ts > runs a forked Pi supervisor and receives its exact instruction in another Pi session 2530ms
|
||||
|
||||
Test Files 17 passed (17)
|
||||
Tests 67 passed (67)
|
||||
Start at 19:24:07
|
||||
Duration 2.84s (transform 2.36s, setup 0ms, import 6.39s, tests 4.95s, environment 3ms)
|
||||
|
||||
+ npm run typecheck
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
+ npm run lint
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 32 files in 91ms. No fixes applied.
|
||||
+ npm run build
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 build
|
||||
> tsc
|
||||
|
||||
+ git diff --check
|
||||
@@ -0,0 +1,100 @@
|
||||
# Post-fix independent bug review — pi-goals supervision recovery
|
||||
|
||||
Scope: changes `2824396..1668c94` (fix commit `325b939` + evidence commit `1668c94`).
|
||||
Inputs: AGENTS.md, `slop/reviews/20260908-review-fixes.md` (dispositions), `slop/reviews/20260908-independent-supervision-bug-review.md` (original F1–F9), full current sources of `src/{index,intercom,supervisor-session,approval,role-models,herdr}.ts`, changed tests, native evidence logs.
|
||||
Read-only: no repo edits, no live panes, no messaging. Repro artifacts lived in /tmp only.
|
||||
|
||||
Labels: **[TESTED]** = executed/compiled and observed. **[CODE]** = unambiguous from source. **[INFERENCE]** = depends on runtime behavior I could not observe here. Test passage is treated as evidence of the asserted path only, not as design proof.
|
||||
|
||||
Baseline check: `env -u PI_SUBAGENT_CHILD -u PI_GOALS_ROLE npx vitest run` → 57/57 pass in 16 files, matching the worker's validation claim. **[TESTED]**
|
||||
|
||||
---
|
||||
|
||||
## Fix verification F1–F7
|
||||
|
||||
**F1 (unavailable remembered model aborts restore) — verified fixed. [CODE + repo tests]**
|
||||
`session_start` now sets `modelError` first, configures the binding and timers independent of model selection, and only then attempts `restoreModel` in try/catch (`src/index.ts:557-580`). On failure: no `setModel`, no ready hello (`configure(..., false)` at :562, `markReady` gated on `!modelError` at :572-573), paused widget (`pauseReason`/`updateWidget` :149-153, 271-276), write/sign-off gating (:453-456, 592-597), human input and read-only diagnosis retained. `/model` saves over the failed role choice because `RoleModels.enter` sets `this.role` before throwing (`src/role-models.ts:29-30`), so `/goals reconnect` then picks up the user's replacement — a deliberate, workable recovery chain. No fallback model is substituted anywhere.
|
||||
|
||||
**F2 (resumed worker silently unsupervised) — verified fixed, with a residual in P3 below. [CODE + repo tests]**
|
||||
`connected` now requires own-ready + binding + channel + peerReady (`src/intercom.ts:87`); `onConnectionChange` refreshes the widget immediately (:139, :145, :164); a resumed working worker hellos not-ready until its model restores, then `waitReady(5000)` and warns on failure (`src/index.ts:572-577`). Returning peers clear the pause automatically via the hello/`changed` path. Explicit `/goals restart` preserves plan/version and replaces only the tracked pane with a fresh approval binding (test asserts old checkpoint removed, plan unchanged, exactly one close).
|
||||
|
||||
**F3 (inactive-plan steer throws/replays) — lifecycle part fixed; delivery gap honestly open. [CODE + repo tests]**
|
||||
`detach()` (`src/intercom.ts:72-81`) clears the binding on completion/clear/restart; late steers for a dead binding are dropped at the binding check (:151) without invoking `onSteer` or acking — covered by the new intercom tests. Ack ordering is still handoff-before-ack (:186-193), so a synchronous `sendUserMessage` throw leaves the steer unrecorded/unacked and it retries (test proves retry succeeds). The documented residual is real and correctly **not** claimed fixed: Pi 0.84.1's `sendUserMessage` is a void wrapper over an async enqueue, so an ack can precede an asynchronous enqueue failure, after which the instruction is gone from the supervisor's pending set and never reaches the model — silently. This is a genuine gap a future correlated-receipt protocol should close; the recorded UAT requirement (force an async enqueue rejection) is the right acceptance test. Acceptable as an explicitly-open item, not as a resolved one.
|
||||
|
||||
**F4 (stale pane → repeated 5-minute Ready waits) — verified fixed for the timeout itself. [CODE + repo tests]**
|
||||
Existing-pane reconnect waits are 5s (`src/index.ts:186`, `:323`); first launch keeps the 300s compaction allowance (:213); failed `pane run` retains and reports the pane ID (:204-208); no automatic pane kill. **However, see P1: in one important sub-scenario the 5s retry fails deterministically even when the supervisor is healthy, so the fix's "retry" guidance does not actually recover there.**
|
||||
|
||||
**F5 (supervisor gets general `intercom` tool) — verified fixed. [CODE + native test]**
|
||||
`intercom` is in `BLOCKED_TOOLS` (`src/supervisor-session.ts:13`), filtered from active tools at start and bootstrap (:108-110, :146), and blocked at tool-call time even if re-enabled (:170-173). The native pair test asserts the real supervisor model request's tool list excludes `intercom` and `bash`. Note the tool gate uses `terminate: true`, so a blocked call ends the whole supervisor turn — a deliberate-looking but behaviorally stronger choice than the planning gate's plain block.
|
||||
|
||||
**F6 (nested placeholder evidence) — verified fixed as far as claimed. [CODE + repo tests]**
|
||||
Both the inline and child-bullet paths reject exactly `(empty until sign-off)` case-insensitively (`src/supervisor-session.ts:46,52`); the string matches the template in `src/prompts.ts:97`. Residual (documented as a presence floor): the child scan still accepts *any* deeper-indented nonblank bullet as evidence, and quoted variants like `"(empty until sign-off)"` pass. That is a semantic-judging limit, accurately disclaimed; supervisor judgment remains the real gate. Acceptable.
|
||||
|
||||
**F7 (last-goal hash includes Log/Interview) — verified fixed. [CODE + TESTED via unit tests]**
|
||||
`goalBlock` now truncates the plan at `## Log` before scanning and ends the block at the next goal line or any `#`/`##` heading, with `trimEnd` (`src/approval.ts:43-58`). `approval.test.ts` covers duplicate goal text in the log, Interview section, next-goal boundary, and real block edits invalidating the hash; the flow test proves a manual log line between ApproveGoal and CompleteGoal no longer invalidates. Fail-closed on upgrade (old hashes need re-approval) is the right direction. One residual asymmetry (P4 below).
|
||||
|
||||
---
|
||||
|
||||
## New findings
|
||||
|
||||
### P1 — Reconnect/Ready-retry can wedge in a half-open state: peer hello is only re-sent on *perceived* state change (Medium) [TESTED]
|
||||
|
||||
`GoalIntercom.configure()` resets `peer`/`peerReady` (`src/intercom.ts:45-46`) and sends a hello, but the peer replies to a hello only when *its own* view changed (`src/intercom.ts:154-164`: `changed = !this.peer || this.peerReady !== message.ready; if (changed) this.hello()`). There is no periodic hello. If the peer's stored state already matches the incoming hello (same session id, same ready flag), it stays silent — so the side that reconfigured never learns the peer and `connected` stays false forever (until some unrelated broker `session_joined` at :148 happens to trigger a hello).
|
||||
|
||||
Reproduced in isolation (compiled real `src/intercom.ts` from HEAD, two links cross-wired as broker peers, script in /tmp, no repo changes):
|
||||
|
||||
```
|
||||
after initial link: worker.connected = true supervisor.connected = true
|
||||
after worker reconfigure: worker.connected = false supervisor.connected = true
|
||||
waitReady REJECTED after 201 ms: Supervisor did not become ready through pi-intercom; inspect its pane.
|
||||
supervisor steer() succeeded (reports sent)
|
||||
worker delivered steer: null
|
||||
```
|
||||
|
||||
Consequences, all reachable through new/changed code paths:
|
||||
|
||||
1. **`/goals reconnect` on a healthy link breaks it.** `src/index.ts:322-323` reconfigures the same binding; if nothing about readiness actually changed, every retry times out after 5s with "Goal recovery failed", and the only in-app escape is `/goals restart` — which closes the *healthy* pane and invalidates the approval binding (`beginReview` deletes checkpoints).
|
||||
2. **The F1-on-Ready retry fails deterministically.** First Ready: `startSupervisor` completes the hello exchange (worker announced `ready=true` at `beginReview`/`configure`), then `restoreModel("worker")` throws → back to planning. After the user fixes the model, the next Ready takes the existing-pane path (`src/index.ts:184-186`): `configure` resets the worker's peer, hellos `ready=true`, the supervisor sees no change, stays silent, and `waitReady(5000)` times out — even though the supervisor is up and healthy. Every Ready/reconnect retry repeats this. This undercuts the F1/F4 recovery story on exactly the path those fixes target.
|
||||
3. **Silent steer loss during the window.** While half-open, the supervisor's `connected` is true, so `SteerWorker` "succeeds", but the worker drops the message at `src/intercom.ts:166` (`event.fromSessionId !== this.peer`) with no notification; the steer sits in pending awaiting a changed hello that may never come. The SteerWorker result text ("Receipt and execution are not confirmed") softens but does not surface this.
|
||||
|
||||
Why the tests miss it: the fixture auto-replies to **every** hello with `ready: true` (`test/intercom-fixture.ts:21`), which is precisely the behavior the real peer logic does not have. The new peer-return and reconnect tests therefore cannot observe the change-gated reply.
|
||||
|
||||
Suggested direction (not implemented): reply to a hello whenever the sender may have lost state — e.g. have `configure` retain `peer`/`peerReady` when the binding is unchanged (re-handshake is only needed after a real transport/binding change), or include a monotonic hello generation and reply to any newer/unknown generation. An unconditional reply to every hello also works without ping-pong, because the *response* hello is still change-gated on the receiver's side.
|
||||
|
||||
### P2 — Failed Ready leaves the worker announcing `ready=true` while paused (Low) [CODE]
|
||||
|
||||
In the Ready flow, `beginReview` → `configure(approvalId, "worker", ctx)` defaults to `ready=true` (`src/intercom.ts:43`, `src/index.ts:171`). If `restoreModel("worker")` then throws (`src/index.ts:525` → catch at :537-544), the code rolls back `phase` to planning but never rolls back intercom readiness or detaches. The worker thereafter hellos `ready=true` on any broker event while `modelError` is set; the supervisor sees a ready, connected worker, `SteerWorker` sends successfully, and the worker's `onSteer` throws (`src/index.ts:113-116`) — unacked, error notification per replay. Self-consistent recovery exists (next Ready after `/model` works, modulo P1), so impact is confusing error noise and a misleading readiness signal, not loss. A `markNotReady`/detach in that catch would align the announced state with the pause.
|
||||
|
||||
### P3 — Worker pause message misattributes a *supervisor-side* pause (Low) [CODE]
|
||||
|
||||
`pauseReason()` (`src/index.ts:149-154`) collapses "peer absent" and "peer present but not ready" into one message: "Supervisor disconnected. Run /goals reconnect, or /goals restart…". When the supervisor pane is alive but paused on its own model restoration (`src/supervisor-session.ts:150-158` sets supervisor `modelError`, readiness stays false), the worker-side `/goals reconnect` will hello, get a `ready=false` reply, and time out after 5s — the advised action cannot work; the actual fix is `/model` + `/goals reconnect` in the supervisor pane. The supervisor pane does display its own error notification, and the design keeps both panes visible, so the user has the information — but the worker-side guidance points the wrong way. `peer` set + `peerReady` false is distinguishable from no peer; the message could be too.
|
||||
|
||||
### P4 — `goalBlock` and `tickGoal` now scan different regions (Low) [CODE]
|
||||
|
||||
Post-F7, `goalBlock` only sees the pre-`## Log` region (`src/approval.ts:44`), while `tickGoal` and `scanGoals` still scan the whole file (`src/index.ts:670-677`, :53-60). A goal-shaped line quoted inside the Log (e.g. a pasted checklist) yields: `goalBlock` finds a unique match → approval proceeds and hashes, but `CompleteGoal` → `tickGoal` finds two hits → returns null → sign-off blocked after a successful approval. Fail-closed, requires unusual plan content, and the same class of confusion pre-dates the fix (both sides failed before); noting it because the fix changed the boundary of only one of the two scanners.
|
||||
|
||||
### P5 — Ready catch can resurrect a cleared plan phase after a concurrent `/goals clear` (Low) [CODE/INFERENCE]
|
||||
|
||||
The Ready path awaits up to 300s inside `startSupervisor` (`src/index.ts:213`), and neither `detach()` nor `configure()` wakes `waitReady` waiters (only hellos/shutdown do; `detach`'s clearing of `binding` means its own not-ready hello is ignored by waiters, and the peer's reply is dropped by the now-empty binding check). If the user runs `/goals clear` during that wait, the plan state is cleared; when the wait later times out, the catch at `src/index.ts:537-544` unconditionally sets `phase: "planning"` and persists — yielding `phase: "planning"` with `planVersion: null`, a "drafting goals" widget over no plan, and "No active plan to disconnect" from `/goals clear`. Recoverable via a fresh `/goals <objective>`, and the interleaving requires issuing a command while the Ready select-loop is mid-wait, hence Low. I did not execute this interleaving; it follows from the unconditional catch and the waiter semantics. Guarding the catch on "state still belongs to this Ready attempt" (e.g. approvalId/planVersion unchanged) would close it.
|
||||
|
||||
---
|
||||
|
||||
## Assessment of documented residuals (not accepted on documentation alone)
|
||||
|
||||
- **F3 durable-delivery gap**: genuine and correctly scoped as open. Concretely, after an acked-but-async-failed enqueue, the supervisor waits indefinitely for a response to an instruction the worker model never saw, with no signal on either side; the recorded future UAT (inject an async enqueue rejection, assert no confirmed-delivery claim and recoverability) is the right bar. Fine to defer; not fine to call resolved — and it isn't.
|
||||
- **F8 (unknown usage disables 100k compaction)**: the `?? 0` fallback (`src/supervisor-session.ts:187`) silently disables the custom compaction wherever `getContextUsage()` is unavailable, contradicting the AGENTS.md cost design with no user-visible signal. Mitigating factor I verified: the supervisor system prompt (with `planPath`) is re-appended every `before_agent_start` (:175), and approvals/checkpoints live on disk, so a fallback default-compaction does not lose the plan pointer or approval state — the consequence is cost/context-rot drift, not correctness. Still, a one-time "usage unknown; custom compaction inactive" notification would close the observability gap cheaply. Acceptable as a documented limitation; the silence is the weakest part.
|
||||
- **F9 (planning/pause bash gate holes)**: accurately disclaimed as guardrail-not-sandbox. The paused-diagnostic gate (`src/index.ts:453-456`) intentionally inherits the same heuristic, including the `git diff --ext-diff` external-command hole that requires a pre-existing hostile `.git/config`. Given the threat model (trusted repo, trusted extensions), the README statement is sufficient; a hardened policy remains correctly out of scope.
|
||||
- **Test-quality caveat**: beyond the fixture issue in P1, the native pair test drives only `SteerWorker`; the two-real-session `ApproveGoal → CompleteGoal` chain is still unexecuted end to end, so the F7 hash boundary and the view-freshness gate are verified only per-side (unit/flow tests) plus one real supervisor tool-list inspection. The disposition states this; I confirm it remains true at 1668c94.
|
||||
|
||||
## What is solid
|
||||
|
||||
- Shutdown/late-startup guards (`src/index.ts:180, 199-203, 210`) and the supervisor's `bootstrapping` `finally` fix are correct; the test proves no late persistence after shutdown.
|
||||
- Queued pending steers when *own* readiness changes are handled correctly on both roles: republish is gated on `peerReady && this.ready` (`src/intercom.ts:158-162`), pause suppresses republication, and recovery replays exactly the unacked set from session entries. The only hole in this chain is P1's missing trigger.
|
||||
- Approval binding safety (per-approvalId, head/tree/clean-worktree/goal-block-hash equality, restart invalidating old approvals) is preserved and extended by the restart flow.
|
||||
- Human recovery availability while paused is real on both roles: input, read-only tools, `/model`, `/goals reconnect|restart|clear` all remain reachable; sign-off and writes fail closed.
|
||||
|
||||
## Summary
|
||||
|
||||
F1–F7 are fixed as claimed, with tests and native evidence matching the dispositions. The significant new finding is **P1**: the change-gated hello reply combined with `configure()`'s peer reset makes `/goals reconnect` and Ready-retry wedge half-open precisely when readiness did not change — including the F1-on-Ready retry scenario the fixes were built for — and steers are silently dropped in that window while `steer()` reports success. P2–P5 are low-severity consistency/guard gaps. Documented residuals F3/F8/F9 are honest; F3's delivery gap and F8's silent compaction-disable remain open items, not fixes.
|
||||
|
||||
Attribution: independent reviewer, run bdb93a2e-52f4-4a6d-bac2-7c9eb48118b5; preserved verbatim by Pi/OpenAI implementation worker.
|
||||
@@ -0,0 +1,151 @@
|
||||
# pi-goals supervision bug review — HEAD 2824396
|
||||
|
||||
> Attribution: independent delegated reviewer, run `5c8c2017-a92f-4a5f-baf6-f441f9b50495`, artifact `supervision-bug-review.md`. Findings below are preserved from that reviewer, not authored by the implementation worker. Implementation dispositions are in `20260908-review-fixes.md`.
|
||||
|
||||
Scope: AGENTS.md, src/{index,intercom,supervisor-session,background,role-models,approval,worker-view,herdr,prompts}.ts and tests.
|
||||
Priorities: lifecycle/reload, delivery/reconnect, approval safety, autonomy failures.
|
||||
Method: static review plus targeted checks against the installed `@earendil-works/pi-coding-agent` 0.84.1 and `pi-intercom` 0.13.0 sources, and one live reproduction (F1). Nothing in the repo was modified; repro scripts lived in /tmp.
|
||||
|
||||
Labels: **[TESTED]** = demonstrated by execution or verified against dependency source. **[CODE]** = read directly from pi-goals source; control flow unambiguous. **[INFERENCE]** = depends on behavior I could not observe.
|
||||
|
||||
---
|
||||
|
||||
## F1 — Working-phase session restore aborts halfway when the remembered role model is unavailable (Medium) [TESTED]
|
||||
|
||||
`src/index.ts:486-495` (`session_start`):
|
||||
|
||||
```ts
|
||||
if (state.phase) await models.enter(...); // line 488 — can throw
|
||||
planningContextPending = state.phase === "planning";
|
||||
resyncReason = state.phase === "working" ? "New session." : null;
|
||||
if (state.phase === "working") {
|
||||
intercom.configure(state.approvalId!, "worker", ctx); // line 492 — skipped on throw
|
||||
startWorkerTimers(ctx); // line 493 — skipped
|
||||
}
|
||||
updateWidget(ctx); // line 495 — skipped
|
||||
```
|
||||
|
||||
`RoleModels.enter` (`src/role-models.ts:30-40`) throws `worker model is unavailable...` when the saved `.pi/pi-goals/models/worker.json` names a model that `ctx.modelRegistry.find` can no longer resolve (provider removed, auth expired). That is a realistic state: the feature exists precisely to remember models across sessions, and model availability changes over time.
|
||||
|
||||
Reproduction (executed): mock Pi host, persisted state `{phase: "working", approvalId: "appr-1", planVersion: 1}`, saved worker model `gone/expired`, `modelRegistry.find → undefined`. Result:
|
||||
|
||||
```
|
||||
session_start handler threw: worker model is unavailable. Select an available model with /model, then retry. Saved choice was not replaced.
|
||||
intercom.configure called during working-phase restore: false
|
||||
```
|
||||
|
||||
Consequences after the throw:
|
||||
- Intercom binding is never restored: no hello, `peerReady` stays false, every subsequent `publishWorkerView` silently records views with an empty binding and never publishes them.
|
||||
- The hourly view timer never starts.
|
||||
- Widget is not updated.
|
||||
- On a planning-phase resume, `planningContextPending` is never set, so the planning snapshot is never re-injected.
|
||||
- Pi catches per-handler errors (`ExtensionRunner.emit`, runner.js:587-601) and routes them to `emitError`, so the user sees at most an extension-error diagnostic. The error text says "then retry", but no code path retries the restore — `session_start` does not re-run when the user picks a new model.
|
||||
|
||||
This overlaps F2: the session resumes looking normal while supervision is dead.
|
||||
|
||||
## F2 — No supervisor liveness check on worker resume; dead supervisor pane is invisible (Medium) [CODE]
|
||||
|
||||
`src/index.ts:486-495`: on resume with `phase === "working"`, the worker calls `intercom.configure` (which sends one hello) and starts timers. There is no `waitReady`, no Herdr pane probe, and no timeout. If the supervisor pane died while the worker session was closed:
|
||||
|
||||
- `GoalIntercom.view()` (`src/intercom.ts:82-88`) records the view and skips publishing because `connected` is false — silently. No notify anywhere on this path.
|
||||
- `updateWidget` still renders "· supervised" (`src/index.ts:249`).
|
||||
- The worker system prompt tells the model to "Stop when a goal appears complete so the supervisor can inspect a settled worker view" — it will stop and wait for an approval that can never arrive. `CompleteGoal` then fails with "no matching supervisor approval checkpoint" with no hint that the supervisor is gone.
|
||||
- The only reconnect wait (`waitReady`) lives in `startSupervisor`, which is unreachable from the working phase: the Ready menu only renders when `state.phase === "planning"` (`src/index.ts:417`). The only recovery is `/goals clear` (drops plan linkage) or `/goals <new objective>` (new plan version, old plan orphaned). There is no "restart supervisor" path that preserves the current plan.
|
||||
|
||||
[INFERENCE] Whether the broker notices the dead pane while the worker is offline is irrelevant here — the worker has no handler for "peer never came back after resume" in either case.
|
||||
|
||||
## F3 — `onSteer` rejection throws before ack: unacked steer replays forever, error notification each reconnect (Medium-low) [CODE]
|
||||
|
||||
`src/index.ts:113-116`:
|
||||
|
||||
```ts
|
||||
intercom.onSteer = (instruction) => {
|
||||
if (state.phase !== "working") throw new Error("Worker plan is not active; instruction rejected.");
|
||||
pi.sendUserMessage(`[supervisor] ${instruction}`, { deliverAs: "steer" });
|
||||
};
|
||||
```
|
||||
|
||||
`src/intercom.ts:167-174` (worker steer branch):
|
||||
|
||||
```ts
|
||||
this.onSteer(message.text!); // throws → everything below skipped
|
||||
this.received.add(message.id);
|
||||
this.record("in", message);
|
||||
this.publish({ ... kind: "received" ... });
|
||||
```
|
||||
|
||||
Because `onSteer` runs before dedupe/record/ack, a steer that arrives when the plan is not active (plan just completed — `publishWorkerView` sets `phase: null` at index.ts:213; or `/goals clear`; or a supervisor that ignores "stop issuing instructions") is:
|
||||
1. never acked — the supervisor keeps it in `pending` and republishes it on every `changed` hello (`src/intercom.ts:143-149`), so each supervisor reconnect re-fires the throw;
|
||||
2. never recorded — so the dedupe set can't suppress it;
|
||||
3. surfaced only as `Goal Intercom error: ... instruction rejected.` notifications in the worker pane; the supervisor's `SteerWorker` result says only "Receipt and execution are not confirmed", so the supervisor model cannot distinguish "rejected" from "lost" and may re-send, producing one error notification per attempt.
|
||||
|
||||
Related at-least-once window: a crash between `pi.sendUserMessage` (persisted) and `record("in", ...)` causes the same `[supervisor] ...` instruction to be delivered twice after resume. Narrow, but the fix is the same: record/ack before invoking `onSteer`, and add a rejection result back to the supervisor instead of throwing.
|
||||
|
||||
## F4 — Stale `supervisorPaneId` makes every Ready retry block for 5 minutes (Medium-low) [CODE]
|
||||
|
||||
`src/index.ts:163-166`:
|
||||
|
||||
```ts
|
||||
if (state.supervisorPaneId && state.approvalId) {
|
||||
intercom.configure(state.approvalId, "worker", ctx);
|
||||
await intercom.waitReady(); // default 300_000 ms, intercom.ts:73
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
This reconnect path is taken after a partial `startSupervisor` failure — e.g. `herdr pane split` succeeded (pane id persisted via the `onOpened` callback at index.ts:174-176) but `herdr pane run` failed (`src/herdr.ts:76-84`), or `models.enter("worker")` threw after the supervisor started. The pane is dead or the supervisor process exited, but the retry never asks Herdr whether the pane exists; it blocks the `agent_settled` handler (and therefore the planning menu) until the 5-minute `waitReady` timeout. Every subsequent Ready repeats the 5-minute hang. `/goals clear` recovers (`closeSupervisorPane` tolerates `NOT_FOUND`/`PANE_GONE`, herdr.ts:60-66), but the timeout error message ("inspect its pane") does not say so.
|
||||
|
||||
Also note the same 5-minute blocking wait applies to the supervisor's first-time initial compaction (`supervisor-session.ts:133-160`); a slow compaction of a large fork produces the same opaque worker-side failure, though that path self-heals on retry.
|
||||
|
||||
## F5 — The "read-only" supervisor gets pi-intercom's full `intercom` tool (Low-medium, approval/authority surface) [TESTED against pi-intercom source]
|
||||
|
||||
The supervisor runs with `--no-extensions -e src/index.ts` (`src/herdr.ts:48-58`), so pi-intercom is never an installed extension in the supervisor session, so `GoalIntercom.loadIntercom` (`src/intercom.ts:189-200`) always dynamically imports it. `intercom(api)` executes pi-intercom's full default export, which registers:
|
||||
|
||||
- the `intercom` tool: "Send a message to another pi session running on this machine" (pi-intercom/index.ts:2088),
|
||||
- `/intercom`, `/intercom-id`, `/alias` commands (pi-intercom/index.ts:2802-2812).
|
||||
|
||||
The supervisor's read-only enforcement filters only `WRITER_TOOLS` (`src/supervisor-session.ts:13`, applied at session_start line ~124 and in bootstrap), so `intercom` remains an active tool for the supervisor model. Effect: the supervisor — prompted as read-only with `SteerWorker`/`ApproveGoal` as its only actuators — can message arbitrary Pi sessions on the machine, including the user's other sessions, outside the auditable SteerWorker channel whose renders the tests assert are visible. Severity depends on how much you trust the supervisor model; the capability contradicts the stated design ("all supervisor thinking and messages should be visible", AGENTS.md).
|
||||
|
||||
Mitigation would be filtering `intercom` (and any other messaging tools) out of the supervisor's active set, or passing a `registerTool` denylist through the `loadIntercom` proxy (it currently only wraps `on`).
|
||||
|
||||
## F6 — `hasEvidenceEntry` accepts placeholder or unrelated nested bullets as evidence (Low) [CODE]
|
||||
|
||||
`src/supervisor-session.ts:41-60`. The inline placeholder `(empty until sign-off)` is rejected, but when the inline value is empty the child scan returns true for *any* deeper-indented bullet with nonblank text — including `- (empty until sign-off)` written as a child bullet, or any stray nested line. So this block passes the gate:
|
||||
|
||||
```
|
||||
1. [ ] goal: x
|
||||
- evidence:
|
||||
- (empty until sign-off)
|
||||
```
|
||||
|
||||
The supervisor model is instructed to actually read the evidence, so this is a heuristic floor rather than the real defense; still, the placeholder check should apply to child bullets too.
|
||||
|
||||
## F7 — Approval goal block for the last goal runs to EOF; tail edits spuriously invalidate approvals (Low) [CODE]
|
||||
|
||||
`goalBlock` (`src/approval.ts:36-53`) slices from the goal line to the next goal line *or EOF*. For the last goal, the block includes `## Log`, `## Interview`, and the Appendix. `hashGoalBlock` therefore changes if the worker appends a manual `## Log` line (which the prompts encourage — `stamp()` exists for that) between `ApproveGoal` and `CompleteGoal`, producing "no matching supervisor approval checkpoint" and forcing a fresh review. Fail-closed, so not a safety bug; it is an availability/UX trap in the normal approve → log → sign-off rhythm. Consider ending the block at the fold (`## Log`) like `foldPlan` does.
|
||||
|
||||
## F8 — Supervisor 100k compaction silently disabled when `getContextUsage` is unavailable (Low) [INFERENCE]
|
||||
|
||||
`src/supervisor-session.ts:141-143`: `if (compacting || (ctx.getContextUsage()?.tokens ?? 0) < COMPACT_AT_TOKENS) return;`. If `getContextUsage()` returns undefined (RPC/print modes or any runtime where it isn't wired), tokens coerce to 0 and the supervisor never self-compacts, contradicting the AGENTS.md cost design ("compacts every 100k"). Pi's own auto-compaction will eventually fire without the custom instructions that protect the plan pointer and approval state. I could not confirm whether `getContextUsage` is ever undefined in the Herdr-pane interactive mode; in the test mock it is explicitly set.
|
||||
|
||||
## F9 — Plan-mode bash gate: residual holes are narrow but worth noting (Low) [CODE/INFERENCE]
|
||||
|
||||
`isPlanningReadOnlyCommand` (`src/index.ts:566-574`) is otherwise tight (blocks pipes/redirects/backticks/`$`, splits on `&&`/`;`, whitelists verbs, special-cases `--output`, `find -delete/-exec...`, mutating `git branch`). Residual issues:
|
||||
|
||||
- The whitelist allows `git log/show/diff` with *arbitrary* flags. `git diff --ext-diff` / `git log --ext-diff` execute the command configured in `diff.external`/`GIT_EXTERNAL_DIFF`. Env-prefix assignments are blocked (the part must start with a whitelisted verb), so this requires a pre-existing malicious `.git/config` in the target repo. [INFERENCE] that any real repo would have this.
|
||||
- Plan mode blocks only `edit`/`write` by name (`PLAN_MODE_BLOCKED_TOOLS`, index.ts:34). Any other extension's mutating tool (e.g. a `process`/background-task launcher, `apply_patch`-style tools — the supervisor's own `WRITER_TOOLS` list at supervisor-session.ts:13 acknowledges several) is not blocked in plan mode. In the worker session all user extensions are loaded, so this depends on the user's setup. [INFERENCE]
|
||||
|
||||
## Non-findings (checked, working as intended)
|
||||
|
||||
- **Shutdown latch**: `GoalIntercom.stopped = true` on `session_shutdown` looked dangerous for in-process `/resume`, but Pi tears down and re-creates the ExtensionRunner (and therefore all extension instances) on resume/new/fork/reload (agent-session-runtime.js:102-112, agent-session.js:2053-2073). [TESTED against pi source]
|
||||
- **Handler ordering**: Pi runs handlers sequentially in registration order and awaits them (runner.js:579-604), so `GoalIntercom`'s constructor-registered `session_start` (channel load) completes before index.ts's `configure` → hello. Per-handler errors are swallowed into `emitError` (this is why F1 is silent). [TESTED against pi source]
|
||||
- **ApproveGoal freshness gates**: the `view === newest.text` equality check is sound — `sendUserMessage` stores the text verbatim (agent-session.js:1106-1133), `latestView` is set before `onView` fires, and any newer queued view makes the comparison fail closed. Worker "stopped"/`backgroundQuiet` are extension-computed (`ctx.isIdle()`, `backgroundState`), not model-controlled, so the worker model cannot spoof a stopped/quiet view. `backgroundState` fails closed ("unknown" → not quiet) when an installed tracker doesn't answer, and distinguishes missing providers (background.ts, covered by test/background.test.ts).
|
||||
- **Approval checkpoint binding**: head/tree/clean-worktree/goal-block-hash equality plus per-`approvalId` binding and `beginReview`'s deletion of prior approvals make approval replay across plans or commits infeasible; `.pi/plan`, approvals, and model prefs are correctly excluded from the dirtiness check relative to repo root (`approval.ts:20-34`).
|
||||
- **Steer/view redelivery after restart**: pending steers and `received` dedupe survive restart via session-entry replay (`intercom.ts:54-68`) and are covered by test/intercom.test.ts; ack cursors are monotonic.
|
||||
- **Two-peer guard**: the "Two peers claim this supervision binding" throw fires before any state mutation and repeats harmlessly; it can wedge only if the broker failed to emit `session_left` for the previous peer, which the broker does send on disconnect/reregister (broker.ts:327,540).
|
||||
|
||||
## Test-coverage gaps relevant to the above
|
||||
|
||||
- No test exercises F1 (resume with unavailable remembered model).
|
||||
- No test exercises a resumed worker whose supervisor never answers (F2) — the intercom fixture auto-replies to hellos, so `connected` is always true in tests.
|
||||
- The native end-to-end test (test/native-intercom.test.ts) drives `SteerWorker` only; the `ApproveGoal` → approval file → `CompleteGoal` chain has never run through two real Pi sessions, so F7-class friction and the `view === newest.text` gate are unverified end to end.
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run --reporter=verbose
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
✓ test/package-agent.test.ts > package manifest > includes the extension without registering a packaged subagent 6ms
|
||||
✓ test/herdr.test.ts > supervisor pane command > forks the planning session with pi-goals owning its Intercom dependency 3ms
|
||||
✓ test/herdr.test.ts > supervisor pane command > accepts Herdr's text version output and stale pane cleanup 40ms
|
||||
✓ test/prompts.test.ts > planning prompt > requires fact finding or a focused question before a goal 2ms
|
||||
✓ test/prompts.test.ts > planning prompt > restores the same rule after compaction 0ms
|
||||
✓ test/prompts.test.ts > planning prompt > anchors work and sign-off to the user-visible result 0ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > receives exact advice once, acknowledges it and rejects unrelated peers 14ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > restores an unacknowledged steer on reconnect and stops replay after acknowledgment 7ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > cancels a readiness wait on shutdown 4ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > keeps the title, user voice and goals 2ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > drops the log, the learnings and the unlimited appendix 1ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > returns the whole plan when there is no ## Log yet (a fresh draft) 0ms
|
||||
✓ test/fold.test.ts > openSubtasks (the widget shows the next action, so the plan IS the task list) > lists the active goal's open and in-progress subtasks, stopping at the next goal 1ms
|
||||
✓ test/fold.test.ts > openSubtasks (the widget shows the next action, so the plan IS the task list) > does not leak subtasks from the goal below 1ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > ticks the exact-matching goal line, case-insensitive, leaving subtasks alone 2ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null on wording drift (fuzzy matching is the judge's job, not TypeScript's) 0ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null when the subject matches more than one goal line 0ms
|
||||
stdout | test/intercom-broker.test.ts > exchanges readiness, views and exact advice over a real isolated pi-intercom broker
|
||||
Intercom broker: readiness confirmed; exact worker view and supervisor advice received.
|
||||
|
||||
✓ test/append-log.test.ts > appendLog (the extension's only plan-file write) > creates ## Log at EOF when absent 2ms
|
||||
✓ test/append-log.test.ts > appendLog (the extension's only plan-file write) > appends after the last existing log line, before any following header 1ms
|
||||
✓ test/intercom-broker.test.ts > exchanges readiness, views and exact advice over a real isolated pi-intercom broker 221ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > reports actual idle state, invalidates stopped views on start, and stops completed plans 38ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > preserves drafts, records the interview, and keeps planning read-only 21ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > forks a visible supervisor on Ready and keeps the main session as worker 24ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > starts work only after the supervisor launcher resolves 20ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > delivers an Intercom instruction to the worker 20ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > closes the supervisor on clear but keeps the plan file 24ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > accepts only an approval for the exact clean commit and goal block 49ms
|
||||
✓ test/goals-flow.test.ts > process role > keeps subagent children and visible supervisors out of the worker extension 0ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > restores monitoring and read-only tools without replaying persisted views 6ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > renders all advice in real Pi tool rows, including collapsed and restored rows 25ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > asks for judgment and useful recaps without inventing instructions 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes readiness only after removing writing tools 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > compacts a large planning fork before writing readiness 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > does not become ready when initial compaction fails 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes a durable worker instruction 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > records approval only from a stopped view with evidence and no active work 58ms
|
||||
✓ test/rpc-review.test.ts > RPC review flow > opens Refine's editor before it starts the revision turn 1384ms
|
||||
|
||||
Test Files 11 passed (11)
|
||||
Tests 37 passed (37)
|
||||
Start at 16:31:10
|
||||
Duration 1.67s (transform 1.31s, setup 0ms, import 3.59s, tests 2.00s, environment 1ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 21 files in 67ms. No fixes applied.
|
||||
@@ -0,0 +1,81 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run --reporter=verbose
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
✓ test/package-agent.test.ts > package manifest > includes the extension without registering a packaged subagent 2ms
|
||||
✓ test/prompts.test.ts > planning prompt > requires fact finding or a focused question before a goal 2ms
|
||||
✓ test/prompts.test.ts > planning prompt > restores the same rule after compaction 1ms
|
||||
✓ test/prompts.test.ts > planning prompt > anchors work and sign-off to the user-visible result 0ms
|
||||
✓ test/background.test.ts > reports tracked running work, rather than equating idle agent with finished jobs 3ms
|
||||
✓ test/background.test.ts > distinguishes missing providers from an unavailable installed tracker 4ms
|
||||
stdout | test/role-models.test.ts > remembers each role without automatic switching overwriting another role
|
||||
Role preferences restored: planning=planner, worker=small-worker, supervisor=astra; restore events did not overwrite the choice.
|
||||
|
||||
✓ test/role-models.test.ts > remembers each role without automatic switching overwriting another role 7ms
|
||||
✓ test/role-models.test.ts > fails on an unavailable remembered model without replacing the choice 4ms
|
||||
✓ test/worker-view.test.ts > keeps human direction and source location while sending only new messages 5ms
|
||||
✓ test/worker-view.test.ts > restarts after compaction and does not report historical tool calls as active 1ms
|
||||
✓ test/worker-view.test.ts > bounds serialized Unicode and quoted logs while marking omissions 3ms
|
||||
✓ test/herdr.test.ts > supervisor pane command > forks the planning session with pi-goals owning its Intercom dependency 3ms
|
||||
✓ test/herdr.test.ts > supervisor pane command > accepts Herdr's text version output and stale pane cleanup 30ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > receives exact advice once, acknowledges it and rejects unrelated peers 6ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > restores an unacknowledged steer on reconnect and stops replay after acknowledgment 6ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > advances the incremental overview only after acknowledgment 1ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > cancels a readiness wait on shutdown 1ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > keeps the title, user voice and goals 2ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > drops the log, the learnings and the unlimited appendix 1ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > returns the whole plan when there is no ## Log yet (a fresh draft) 0ms
|
||||
✓ test/fold.test.ts > openSubtasks (the widget shows the next action, so the plan IS the task list) > lists the active goal's open and in-progress subtasks, stopping at the next goal 1ms
|
||||
✓ test/fold.test.ts > openSubtasks (the widget shows the next action, so the plan IS the task list) > does not leak subtasks from the goal below 1ms
|
||||
✓ test/append-log.test.ts > appendLog (the extension's only plan-file write) > creates ## Log at EOF when absent 2ms
|
||||
✓ test/append-log.test.ts > appendLog (the extension's only plan-file write) > appends after the last existing log line, before any following header 0ms
|
||||
stdout | test/intercom-broker.test.ts > exchanges readiness, views and exact advice over a real isolated pi-intercom broker
|
||||
Intercom broker: readiness confirmed; exact worker view and supervisor advice received.
|
||||
|
||||
✓ test/intercom-broker.test.ts > exchanges readiness, views and exact advice over a real isolated pi-intercom broker 206ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > reports actual idle state, invalidates stopped views on start, and stops completed plans 34ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > preserves drafts, records the interview, and keeps planning read-only 21ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > forks a visible supervisor on Ready and keeps the main session as worker 25ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > ticks the exact-matching goal line, case-insensitive, leaving subtasks alone 2ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null on wording drift (fuzzy matching is the judge's job, not TypeScript's) 0ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null when the subject matches more than one goal line 0ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > starts work only after the supervisor launcher resolves 23ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > delivers an Intercom instruction to the worker 23ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > closes the supervisor on clear but keeps the plan file 27ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > accepts only an approval for the exact clean commit and goal block 163ms
|
||||
✓ test/goals-flow.test.ts > process role > keeps subagent children and visible supervisors out of the worker extension 0ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > restores monitoring and read-only tools without replaying persisted views 8ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > renders all advice in real Pi tool rows, including collapsed and restored rows 27ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > asks for judgment and useful recaps without inventing instructions 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes readiness only after removing writing tools 3ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > compacts a large planning fork before writing readiness 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > does not become ready when initial compaction fails 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes a durable worker instruction 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > records approval only from a stopped view with evidence and no active work 59ms
|
||||
✓ test/rpc-review.test.ts > RPC review flow > opens Refine's editor before it starts the revision turn 1226ms
|
||||
stdout | test/native-intercom.test.ts > runs a forked Pi supervisor and receives its exact instruction in another Pi session
|
||||
Native Pi pair: fork retained planning context; SteerWorker delivered exactly: Read the real outputs before declaring completion.
|
||||
|
||||
✓ test/native-intercom.test.ts > runs a forked Pi supervisor and receives its exact instruction in another Pi session 2314ms
|
||||
|
||||
Test Files 15 passed (15)
|
||||
Tests 46 passed (46)
|
||||
Start at 16:58:17
|
||||
Duration 2.73s (transform 2.47s, setup 0ms, import 5.82s, tests 4.28s, environment 2ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 28 files in 21ms. No fixes applied.
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 build
|
||||
> tsc
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run --run test/native-intercom.test.ts --reporter=verbose
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
stdout | test/native-intercom.test.ts > runs a forked Pi supervisor and receives its exact instruction in another Pi session
|
||||
Native Pi pair: fork retained planning context; SteerWorker delivered exactly: Read the real outputs before declaring completion.
|
||||
|
||||
✓ test/native-intercom.test.ts > runs a forked Pi supervisor and receives its exact instruction in another Pi session 2264ms
|
||||
|
||||
Test Files 1 passed (1)
|
||||
Tests 1 passed (1)
|
||||
Start at 16:52:40
|
||||
Duration 2.43s (transform 26ms, setup 0ms, import 56ms, tests 2.27s, environment 0ms)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Initial delegated-worker test failure — Pi/OpenAI
|
||||
|
||||
The first implementation check ran `npm run typecheck && npm test` inside the delegated worker's inherited environment (`PI_SUBAGENT_CHILD=1`). Typecheck passed. At that point the suite reported:
|
||||
|
||||
```
|
||||
Test Files 2 failed | 13 passed (15)
|
||||
Tests 8 failed | 38 passed (46)
|
||||
```
|
||||
|
||||
Representative actual output from that run (18:39:14):
|
||||
|
||||
```
|
||||
FAIL test/goals-flow.test.ts > /goals flow > preserves drafts, records the interview, and keeps planning read-only
|
||||
TypeError: Cannot read properties of undefined (reading 'handler')
|
||||
at flow.commands.get("goals").handler("first objective", flow.ctx)
|
||||
|
||||
FAIL test/rpc-review.test.ts > RPC review flow > opens Refine's editor before it starts the revision turn
|
||||
Error: Test timed out in 15000ms.
|
||||
```
|
||||
|
||||
Diagnosis: the production `isMainSession()` deliberately excludes subagent children. Consequently the mock host never registered `/goals`, and the real RPC test process inherited the child flag and did not register it either. This was not treated as a passing test and no product guard was removed to conceal it.
|
||||
|
||||
Exact corrected test command: `env -u PI_SUBAGENT_CHILD -u PI_GOALS_ROLE npm test`.
|
||||
That rerun passed all 46 then-existing tests. Subsequent added regressions also passed. The final complete command and unabridged final output are saved in `20260908-review-fixes-validation.txt`; it uses the same two-variable isolation. All role-specific tests still explicitly configure their intended role. No live user session's environment or settings were changed.
|
||||
@@ -0,0 +1,4 @@
|
||||
worker: 21 events parsed; 0 errors; stderr empty
|
||||
Worker user-message event contains exact instructed text.
|
||||
supervisor: 21 events parsed; 0 errors; stderr empty
|
||||
Supervisor outbound/ack ID matched: 780f7dac-e4e5-4351-b890-0684a42de681. This is adapter handling, not durable queue confirmation.
|
||||
@@ -0,0 +1,93 @@
|
||||
COMMAND: env -u PI_SUBAGENT_CHILD -u PI_GOALS_ROLE PI_GOALS_EVIDENCE_DIR="$PWD/slop/reviews/review-fixes-native" npm test -- --reporter=verbose && npm run typecheck && npm run lint && npm run build && git diff --check
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run --reporter=verbose
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
✓ test/package-agent.test.ts > package manifest > includes the extension without registering a packaged subagent 2ms
|
||||
✓ test/approval.test.ts > hashes only the current goal, excluding the log, interview, and their historical goal text 3ms
|
||||
✓ test/prompts.test.ts > planning prompt > requires fact finding or a focused question before a goal 3ms
|
||||
✓ test/prompts.test.ts > planning prompt > restores the same rule after compaction 0ms
|
||||
✓ test/prompts.test.ts > planning prompt > anchors work and sign-off to the user-visible result 1ms
|
||||
stdout | test/role-models.test.ts > remembers each role without automatic switching overwriting another role
|
||||
Role preferences restored: planning=planner, worker=small-worker, supervisor=astra; restore events did not overwrite the choice.
|
||||
|
||||
✓ test/background.test.ts > reports tracked running work, rather than equating idle agent with finished jobs 3ms
|
||||
✓ test/background.test.ts > distinguishes missing providers from an unavailable installed tracker 6ms
|
||||
✓ test/role-models.test.ts > remembers each role without automatic switching overwriting another role 7ms
|
||||
✓ test/role-models.test.ts > fails on an unavailable remembered model without replacing the choice 4ms
|
||||
✓ test/worker-view.test.ts > keeps human direction and source location while sending only new messages 3ms
|
||||
✓ test/worker-view.test.ts > restarts after compaction and does not report historical tool calls as active 1ms
|
||||
✓ test/worker-view.test.ts > bounds serialized Unicode and quoted logs while marking omissions 3ms
|
||||
✓ test/herdr.test.ts > supervisor pane command > forks the planning session with pi-goals owning its Intercom dependency 3ms
|
||||
✓ test/herdr.test.ts > supervisor pane command > accepts Herdr's text version output and stale pane cleanup 25ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > receives exact advice once, acknowledges it and rejects unrelated peers 5ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > restores an unacknowledged steer on reconnect and stops replay after acknowledgment 3ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > advances the incremental overview only after acknowledgment 1ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > cancels a readiness wait on shutdown 1ms
|
||||
✓ test/intercom.test.ts > does not acknowledge a synchronous handoff failure, and retries the instruction 1ms
|
||||
✓ test/intercom.test.ts > detaches a completed binding and ignores its late advice without replay errors or false acceptance 1ms
|
||||
✓ test/append-log.test.ts > appendLog (the extension's only plan-file write) > creates ## Log at EOF when absent 2ms
|
||||
✓ test/append-log.test.ts > appendLog (the extension's only plan-file write) > appends after the last existing log line, before any following header 0ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > ticks the exact-matching goal line, case-insensitive, leaving subtasks alone 2ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null on wording drift (fuzzy matching is the judge's job, not TypeScript's) 0ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null when the subject matches more than one goal line 0ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > keeps the title, user voice and goals 2ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > drops the log, the learnings and the unlimited appendix 1ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > returns the whole plan when there is no ## Log yet (a fresh draft) 0ms
|
||||
✓ test/fold.test.ts > openSubtasks (the widget shows the next action, so the plan IS the task list) > lists the active goal's open and in-progress subtasks, stopping at the next goal 1ms
|
||||
✓ test/fold.test.ts > openSubtasks (the widget shows the next action, so the plan IS the task list) > does not leak subtasks from the goal below 1ms
|
||||
stdout | test/intercom-broker.test.ts > exchanges readiness, views and exact advice over a real isolated pi-intercom broker
|
||||
Intercom broker: readiness confirmed; exact worker view and supervisor advice received.
|
||||
|
||||
✓ test/intercom-broker.test.ts > exchanges readiness, views and exact advice over a real isolated pi-intercom broker 226ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > reports actual idle state, invalidates stopped views on start, and stops completed plans 45ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > preserves drafts, records the interview, and keeps planning read-only 21ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > forks a visible supervisor on Ready and keeps the main session as worker 26ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > starts work only after the supervisor launcher resolves 19ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > delivers an Intercom instruction to the worker 22ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > closes the supervisor on clear but keeps the plan file 23ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > accepts only an approval for the exact clean commit and goal block 42ms
|
||||
✓ test/goals-flow.test.ts > process role > keeps subagent children and visible supervisors out of the worker extension 0ms
|
||||
✓ test/goals-flow.test.ts > restores working linkage even when its remembered model is unavailable, and supports explicit recovery 19ms
|
||||
✓ test/goals-flow.test.ts > restores planning linkage even when its remembered model is unavailable, and supports explicit recovery 18ms
|
||||
✓ test/goals-flow.test.ts > shows a missing resumed supervisor, pauses writes, and automatically unpauses when that peer returns 19ms
|
||||
✓ test/goals-flow.test.ts > times out stale Ready retries in five seconds, without replacing the pane automatically 27ms
|
||||
✓ test/goals-flow.test.ts > explicitly restarts only the tracked pane, keeps the plan, and invalidates old approval binding 22ms
|
||||
✓ test/goals-flow.test.ts > does not persist startup results or launch work after session shutdown 19ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > restores monitoring and read-only tools without replaying persisted views 8ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > renders all advice in real Pi tool rows, including collapsed and restored rows 28ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > asks for judgment and useful recaps without inventing instructions 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes readiness only after removing writing tools 19ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > compacts a large planning fork before writing readiness 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > does not become ready when initial compaction fails 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes a durable worker instruction 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > records approval only from a stopped view with evidence and no active work 89ms
|
||||
✓ test/supervisor-session.test.ts > blocks the general intercom actuator even if enabled after startup 2ms
|
||||
✓ test/supervisor-session.test.ts > keeps a supervisor unready after model restoration failure, then recovers explicitly without substituting a model 2ms
|
||||
✓ test/rpc-review.test.ts > RPC review flow > opens Refine's editor before it starts the revision turn 1303ms
|
||||
stdout | test/native-intercom.test.ts > runs a forked Pi supervisor and receives its exact instruction in another Pi session
|
||||
Native Pi pair: fork retained planning context; SteerWorker delivered exactly: Read the real outputs before declaring completion.
|
||||
|
||||
✓ test/native-intercom.test.ts > runs a forked Pi supervisor and receives its exact instruction in another Pi session 2338ms
|
||||
|
||||
Test Files 16 passed (16)
|
||||
Tests 57 passed (57)
|
||||
Start at 18:57:11
|
||||
Duration 2.57s (transform 1.87s, setup 0ms, import 4.26s, tests 4.46s, environment 2ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 29 files in 28ms. No fixes applied.
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 build
|
||||
> tsc
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Independent review fixes — Pi/OpenAI implementation worker
|
||||
|
||||
Baseline: `2824396a711ed56bd462742ab22620df44e5624f`.
|
||||
Original independent review: [preserved review and attribution](20260908-independent-supervision-bug-review.md), run `5c8c2017-a92f-4a5f-baf6-f441f9b50495`.
|
||||
|
||||
The parent approved explicit `/goals reconnect` and `/goals restart`, with human input and read-only diagnosis available while paused. No existing user pane/session was inspected, operated, restarted, or closed. All Herdr recovery operations in this work used mocks. No additional reviewer or subagent was launched by this worker.
|
||||
|
||||
Read AGENTS.md and installed Pi extension documentation covering session replacement/shutdown, sequential events, model selection, input, command context, tool gates, compaction and context usage before editing extension lifecycle code. The native tests use repository-local Pi **0.84.1**; the parent host's 0.85.1 version is not the test executable's version.
|
||||
|
||||
## Finding dispositions
|
||||
|
||||
| Finding | Disposition and concrete proof |
|
||||
| --- | --- |
|
||||
| F1: unavailable remembered model aborts restore | Confirmed and fixed. Planning injection and worker binding/timers initialize independently of model selection. A failure leaves an explicit paused widget and gates implementation/sign-off, not a partially initialized normal worker. No fallback model is selected. The worker does not announce readiness until model restoration succeeds. `/model` followed by `/goals reconnect` retries the same plan. Added flow reproductions for both working/planning restoration and supervisor model recovery. Tests assert no `setModel` on unavailable lookup, no ready hello, preserved preferences/binding/version, retained human input, allowed diagnostic reads, blocked writes/sign-off and successful explicit recovery. |
|
||||
| F2: resumed worker silently has no supervisor | Confirmed and fixed. Connection changes update the widget immediately; absence on restore warns after five seconds. Writes/sign-off are gated while disconnected; human prompts, diagnostic reads and recovery commands remain available. Returning peers clear the connection pause automatically. Explicit restart preserves the working plan and replaces only its tracked pane with a new approval binding. Tests exercise a never-answering peer, later hello, exact owned-pane close, unchanged plan/version, removed old checkpoint, and no automatic pane replacement. |
|
||||
| F3: inactive-plan steer throws/replays | Confirmed; lifecycle cause addressed without premature acceptance. Completed, cancelled or cleared plans detach their binding and announce not-ready; stale advice is ignored without invoking delivery or acknowledging it. Tests cover completion/clear not accepting advice or restarting, transport detach, and a *synchronous handoff* failure remaining unrecorded/unacked until retry succeeds. **Delivery gap remains:** Pi 0.84.1 `dist/core/agent-session.js:1855–1862` implements ExtensionAPI.sendUserMessage as a void wrapper calling async `this.sendUserMessage(...).catch(emitError)`; `:1106–1133` awaits `prompt` internally. The adapter therefore cannot observe durable enqueue success. An async enqueue error may occur after the ack and suppress replay even though the model never received the instruction. A crash between handoff and recording can instead duplicate it. Pending transport messages retain at-least-once retry semantics, but end-to-end durable/at-least-once/exactly-once model delivery is NOT guaranteed. Recording/acking before even the synchronous handoff would worsen loss and was deliberately NOT implemented. Parent explicitly accepted documenting this deeper limitation rather than expanding the protocol in this commit. README and the code comment distinguish adapter handling from persistence/model receipt/execution. |
|
||||
| F4: stale pane causes repeated five-minute Ready waits | Confirmed and fixed. Existing-pane Ready/reconnect waits are five seconds, with explicit reconnect/restart guidance. First-time launch retains the five-minute compaction allowance. Failed `pane run` retains the pane ID for inspection and reports it. No automatic probe-driven kill/replacement of a possibly healthy slow supervisor. Planning restart clears its failed tracked pane for the next Ready; working restart forks a replacement immediately. Test simulates split success/run failure and two Ready attempts, and proves one launch, zero automatic closes, and a five-second retry. Startup results arriving after shutdown are not persisted and do not launch worker work. |
|
||||
| F5: general intercom tool available to supervisor | Confirmed and fixed. `intercom` is removed alongside writing tools before readiness and blocked at tool-call time even if later re-enabled. `SteerWorker` remains available. Both mocked gate tests and the real native pair test assert this; the latter inspects the actual supervisor model request's tool list. |
|
||||
| F6: nested placeholder evidence accepted | Confirmed and fixed. The same exact `(empty until sign-off)` rejection applies to inline and child bullets. Tests exercise both through `ApproveGoal`, assert no approval file, then confirm actual nonblank evidence still passes. This is a presence floor, not a semantic evidence judge; unrelated nonblank prose still requires supervisor judgment. |
|
||||
| F7: last goal hash includes Log/Interview | Confirmed and fixed. The canonical block ignores the Log and stops at the next goal or top-level section. Trailing section-separator whitespace is excluded. Unit tests cover log history containing duplicate goal text, interview, next goal, and actual goal edits still invalidating the hash. The flow test appends a manual Log entry between approval and CompleteGoal and still succeeds. Previously recorded hashes may require a fresh approval after upgrade (fail closed). |
|
||||
| F8: unknown usage disables 100k compaction | Assessed; no speculative estimator added. Installed `agent-session.js:getContextUsage()` returns undefined without a usable model/window and explicitly returns `tokens: null` after compaction until a later assistant usage sample exists. Native RPC tests have valid usage; RPC mode itself is not evidence of permanent unavailability. Existing unknown handling avoids immediate repeat compaction. Persistent unavailable usage still prevents the custom 100k trigger; this remains a documented limitation, not claimed fixed. |
|
||||
| F9: planning read-only guard residuals | Confirmed as a guardrail limitation, not fixed by broad tool/shell policy changes in this task. Planning still explicitly gates edit/write and selected bash commands, not every installed extension actuator. Git read commands can invoke configured external diff/textconv programs. Paused diagnostic bash uses this same heuristic, so trusted repository configuration and extensions are required; it is NOT a security sandbox. README now states this. A hardened shell policy or comprehensive planning tool allowlist needs separately scoped review; no claim of airtight isolation is made. |
|
||||
|
||||
## Validation and inspected evidence
|
||||
|
||||
- [Full final command/output](20260908-review-fixes-validation.txt): **57 tests passed in 16 files**, typecheck, lint, build and `git diff --check` passed.
|
||||
- Exact test environment isolation: `env -u PI_SUBAGENT_CHILD -u PI_GOALS_ROLE PI_GOALS_EVIDENCE_DIR="$PWD/slop/reviews/review-fixes-native" npm test -- --reporter=verbose`. This runs main-session tests as a main session, while explicit role tests still select their role.
|
||||
- [Initial child-environment failure](20260908-review-fixes-initial-validation.md): recorded the original failure (8 failed/38 passed), cause and exact corrected command. It was not omitted or counted as a pass.
|
||||
- Inspected [worker native events](review-fixes-native/worker-events.jsonl) and [supervisor native events](review-fixes-native/supervisor-events.jsonl): the worker received `[supervisor] Read the real outputs before declaring completion.`; supervisor out and ack records share the same instruction ID in the saved run. Both stderr files are empty; neither event log contains an error/failed-response/error-notification record.
|
||||
- Tests preserve human recovery/input while paused, peer-return unpause, and clear/complete not receiving stale advice or reopening supervision. Shutdown guards prevent late startup results from writing the disposed runtime.
|
||||
|
||||
## Remaining acceptance limits
|
||||
|
||||
**Priority residual: F3 durable delivery confirmation is still open.** Required UAT for a future correlated-receipt design: force an asynchronous Pi enqueue rejection after the adapter returns; the supervisor must not report confirmed model delivery and the instruction must remain recoverable. The current receipt does not make that promise.
|
||||
|
||||
These fixes still need the parent's independent post-change review. The native pair uses a deterministic local model and a transport-only worker fixture; it proves routing/context retention and supervisor tool exposure, not good judgment, cost savings, rendered Herdr behavior, or the full two-native-session ApproveGoal → CompleteGoal chain. Full fresh-shell supervisor role restoration without launcher environment remains unverified. An already-created pane can remain for inspection if shutdown interrupts startup; the fix avoids stale context use rather than silently operating such a pane. Background work is not killed by recovery, and unregistered detached work remains outside tracker coverage.
|
||||
|
||||
No changes were pushed by this implementation worker.
|
||||
@@ -0,0 +1,73 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run --reporter=verbose
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
✓ test/package-agent.test.ts > package manifest > includes the extension without registering a packaged subagent 3ms
|
||||
stdout | test/role-models.test.ts > remembers each role without automatic switching overwriting another role
|
||||
Role preferences restored: planning=planner, worker=small-worker, supervisor=astra; restore events did not overwrite the choice.
|
||||
|
||||
✓ test/background.test.ts > reports tracked running work, rather than equating idle agent with finished jobs 2ms
|
||||
✓ test/background.test.ts > distinguishes missing providers from an unavailable installed tracker 6ms
|
||||
✓ test/role-models.test.ts > remembers each role without automatic switching overwriting another role 7ms
|
||||
✓ test/role-models.test.ts > fails on an unavailable remembered model without replacing the choice 6ms
|
||||
✓ test/prompts.test.ts > planning prompt > requires fact finding or a focused question before a goal 2ms
|
||||
✓ test/prompts.test.ts > planning prompt > restores the same rule after compaction 0ms
|
||||
✓ test/prompts.test.ts > planning prompt > anchors work and sign-off to the user-visible result 0ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > receives exact advice once, acknowledges it and rejects unrelated peers 5ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > restores an unacknowledged steer on reconnect and stops replay after acknowledgment 3ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > advances the incremental overview only after acknowledgment 1ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > cancels a readiness wait on shutdown 1ms
|
||||
✓ test/herdr.test.ts > supervisor pane command > forks the planning session with pi-goals owning its Intercom dependency 2ms
|
||||
✓ test/herdr.test.ts > supervisor pane command > accepts Herdr's text version output and stale pane cleanup 30ms
|
||||
✓ test/worker-view.test.ts > keeps human direction and source location while sending only new messages 2ms
|
||||
✓ test/worker-view.test.ts > restarts after compaction and does not report historical tool calls as active 0ms
|
||||
✓ test/worker-view.test.ts > bounds serialized Unicode and quoted logs while marking omissions 2ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > keeps the title, user voice and goals 2ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > drops the log, the learnings and the unlimited appendix 0ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > returns the whole plan when there is no ## Log yet (a fresh draft) 0ms
|
||||
✓ test/fold.test.ts > openSubtasks (the widget shows the next action, so the plan IS the task list) > lists the active goal's open and in-progress subtasks, stopping at the next goal 1ms
|
||||
✓ test/fold.test.ts > openSubtasks (the widget shows the next action, so the plan IS the task list) > does not leak subtasks from the goal below 1ms
|
||||
stdout | test/intercom-broker.test.ts > exchanges readiness, views and exact advice over a real isolated pi-intercom broker
|
||||
Intercom broker: readiness confirmed; exact worker view and supervisor advice received.
|
||||
|
||||
✓ test/append-log.test.ts > appendLog (the extension's only plan-file write) > creates ## Log at EOF when absent 1ms
|
||||
✓ test/append-log.test.ts > appendLog (the extension's only plan-file write) > appends after the last existing log line, before any following header 0ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > ticks the exact-matching goal line, case-insensitive, leaving subtasks alone 2ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null on wording drift (fuzzy matching is the judge's job, not TypeScript's) 0ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null when the subject matches more than one goal line 0ms
|
||||
✓ test/intercom-broker.test.ts > exchanges readiness, views and exact advice over a real isolated pi-intercom broker 222ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > reports actual idle state, invalidates stopped views on start, and stops completed plans 36ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > preserves drafts, records the interview, and keeps planning read-only 24ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > forks a visible supervisor on Ready and keeps the main session as worker 26ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > starts work only after the supervisor launcher resolves 21ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > delivers an Intercom instruction to the worker 21ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > closes the supervisor on clear but keeps the plan file 19ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > accepts only an approval for the exact clean commit and goal block 47ms
|
||||
✓ test/goals-flow.test.ts > process role > keeps subagent children and visible supervisors out of the worker extension 0ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > restores monitoring and read-only tools without replaying persisted views 7ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > renders all advice in real Pi tool rows, including collapsed and restored rows 22ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > asks for judgment and useful recaps without inventing instructions 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes readiness only after removing writing tools 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > compacts a large planning fork before writing readiness 2ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > does not become ready when initial compaction fails 3ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes a durable worker instruction 5ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > records approval only from a stopped view with evidence and no active work 49ms
|
||||
✓ test/rpc-review.test.ts > RPC review flow > opens Refine's editor before it starts the revision turn 1427ms
|
||||
|
||||
Test Files 14 passed (14)
|
||||
Tests 45 passed (45)
|
||||
Start at 16:49:24
|
||||
Duration 1.79s (transform 1.51s, setup 0ms, import 4.33s, tests 2.03s, environment 1ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 26 files in 62ms. No fixes applied.
|
||||
@@ -0,0 +1,27 @@
|
||||
INITIAL VIEW
|
||||
The worker stopped.
|
||||
|
||||
review trigger: settled
|
||||
source session: /isolated/worker.jsonl
|
||||
worker model: test/worker
|
||||
latest human direction:
|
||||
Modal does not use our GPU.
|
||||
tool calls with no result: none
|
||||
tracked background work: processes: 0; subagents: 0; unregistered work is not tracked
|
||||
|
||||
new worker transcript (initial or reset view):
|
||||
I am waiting for the local queue.
|
||||
|
||||
AFTER ACKNOWLEDGMENT
|
||||
The worker stopped.
|
||||
|
||||
review trigger: settled
|
||||
source session: /isolated/worker.jsonl
|
||||
worker model: test/worker
|
||||
latest human direction:
|
||||
Modal does not use our GPU.
|
||||
tool calls with no result: none
|
||||
tracked background work: processes: 0; subagents: 0; unregistered work is not tracked
|
||||
|
||||
new worker transcript since the last acknowledged view:
|
||||
The command launches a Modal remote GPU; local default stays paused.
|
||||
@@ -0,0 +1,68 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run --reporter=verbose
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
✓ test/worker-view.test.ts > keeps human direction and source location while sending only new messages 6ms
|
||||
✓ test/worker-view.test.ts > restarts after compaction and does not report historical tool calls as active 0ms
|
||||
✓ test/worker-view.test.ts > bounds serialized Unicode and quoted logs while marking omissions 2ms
|
||||
✓ test/background.test.ts > reports tracked running work, rather than equating idle agent with finished jobs 2ms
|
||||
✓ test/background.test.ts > distinguishes missing providers from an unavailable installed tracker 5ms
|
||||
✓ test/package-agent.test.ts > package manifest > includes the extension without registering a packaged subagent 2ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > receives exact advice once, acknowledges it and rejects unrelated peers 5ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > restores an unacknowledged steer on reconnect and stops replay after acknowledgment 3ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > advances the incremental overview only after acknowledgment 1ms
|
||||
✓ test/intercom.test.ts > pi-intercom transport > cancels a readiness wait on shutdown 1ms
|
||||
✓ test/herdr.test.ts > supervisor pane command > forks the planning session with pi-goals owning its Intercom dependency 2ms
|
||||
✓ test/herdr.test.ts > supervisor pane command > accepts Herdr's text version output and stale pane cleanup 26ms
|
||||
✓ test/prompts.test.ts > planning prompt > requires fact finding or a focused question before a goal 2ms
|
||||
✓ test/prompts.test.ts > planning prompt > restores the same rule after compaction 1ms
|
||||
✓ test/prompts.test.ts > planning prompt > anchors work and sign-off to the user-visible result 0ms
|
||||
✓ test/append-log.test.ts > appendLog (the extension's only plan-file write) > creates ## Log at EOF when absent 1ms
|
||||
✓ test/append-log.test.ts > appendLog (the extension's only plan-file write) > appends after the last existing log line, before any following header 0ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > keeps the title, user voice and goals 1ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > drops the log, the learnings and the unlimited appendix 1ms
|
||||
✓ test/fold.test.ts > foldPlan (current goals are above ## Log; durable memory is below it) > returns the whole plan when there is no ## Log yet (a fresh draft) 0ms
|
||||
✓ test/fold.test.ts > openSubtasks (the widget shows the next action, so the plan IS the task list) > lists the active goal's open and in-progress subtasks, stopping at the next goal 1ms
|
||||
✓ test/fold.test.ts > openSubtasks (the widget shows the next action, so the plan IS the task list) > does not leak subtasks from the goal below 1ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > ticks the exact-matching goal line, case-insensitive, leaving subtasks alone 2ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null on wording drift (fuzzy matching is the judge's job, not TypeScript's) 1ms
|
||||
✓ test/tick-goal.test.ts > tickGoal (sign-off ticks the goal; agent only ticks on wording drift) > returns null when the subject matches more than one goal line 0ms
|
||||
stdout | test/intercom-broker.test.ts > exchanges readiness, views and exact advice over a real isolated pi-intercom broker
|
||||
Intercom broker: readiness confirmed; exact worker view and supervisor advice received.
|
||||
|
||||
✓ test/intercom-broker.test.ts > exchanges readiness, views and exact advice over a real isolated pi-intercom broker 226ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > reports actual idle state, invalidates stopped views on start, and stops completed plans 45ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > preserves drafts, records the interview, and keeps planning read-only 17ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > forks a visible supervisor on Ready and keeps the main session as worker 20ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > starts work only after the supervisor launcher resolves 18ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > delivers an Intercom instruction to the worker 22ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > closes the supervisor on clear but keeps the plan file 20ms
|
||||
✓ test/goals-flow.test.ts > /goals flow > accepts only an approval for the exact clean commit and goal block 44ms
|
||||
✓ test/goals-flow.test.ts > process role > keeps subagent children and visible supervisors out of the worker extension 0ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > restores monitoring and read-only tools without replaying persisted views 5ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > renders all advice in real Pi tool rows, including collapsed and restored rows 20ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > asks for judgment and useful recaps without inventing instructions 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes readiness only after removing writing tools 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > compacts a large planning fork before writing readiness 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > does not become ready when initial compaction fails 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > writes a durable worker instruction 1ms
|
||||
✓ test/supervisor-session.test.ts > visible supervisor session > records approval only from a stopped view with evidence and no active work 49ms
|
||||
✓ test/rpc-review.test.ts > RPC review flow > opens Refine's editor before it starts the revision turn 1197ms
|
||||
|
||||
Test Files 13 passed (13)
|
||||
Tests 43 passed (43)
|
||||
Start at 16:41:18
|
||||
Duration 1.43s (transform 927ms, setup 0ms, import 3.00s, tests 1.77s, environment 1ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 24 files in 19ms. No fixes applied.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Review against user intent
|
||||
|
||||
## Follow-up: code fixes, full goal still unproven
|
||||
|
||||
Implemented directly after the subagent runner failed and the user authorized direct work. The tests now exercise full advice in real Pi tool components (collapsed, expanded, restored, streaming arguments), emitted thinking/text display, resume without replay of persisted views, latest-view coalescing, actual idle/busy status, stale-view approval rejection, and stopping completed-plan timers. The supervisor prompt now asks for a brief evidence-based progress assessment and useful judgment instead of instruction-only reviews. Background job status is explicitly unmeasured; approval still requires the supervisor to inspect job evidence when relevant.
|
||||
|
||||
[Saved validation output](20260908_supervision-fixes-validation.txt):
|
||||
|
||||
> Tests 34 passed (34)
|
||||
> resumed: deliveredViews=0, activeTools=read, readyReceipt=true
|
||||
> interval view without any work: The worker stopped.
|
||||
|
||||
Typecheck and lint also succeeded in that log. The reproduction script now asserts the corrected behavior; the original reproduction output below is retained as historical evidence. Readiness is cleared on startup and normal shutdown, but it is not a heartbeat or proof of worker receipt. The review here is my own source/diff review, not the independent review that failed to launch. Existing user panes and the separate pi-supervise worktree were not modified.
|
||||
|
||||
Remaining acceptance: a real isolated two-pane run with the intended model pair, observed useful advice and worker response, plus measured token/cost totals. Prompt assertions do not establish judgment quality. No full-goal completion is claimed.
|
||||
|
||||
-- Pi/OpenAI
|
||||
|
||||
## Original review
|
||||
|
||||
Verdict at `06794bf`: not achieved.
|
||||
|
||||
Reviewed `experiment/goals-owned-supervision` at `4ebb4d1` against [AGENTS.md](../../AGENTS.md#user-intent-for-this-branch). This is a source review and isolated runtime reproduction by Pi/OpenAI, not an independent model review or a real two-pane acceptance test. No existing session or pane was operated.
|
||||
|
||||
The overnight `goals-supervisor-01a040d0` transcript used the older pi-supervise/intercom implementation. It is not runtime evidence for this mailbox branch. The uncommitted display patch in `/tmp/pi-supervise-visible-advice` is also separate from this branch and was not counted as completed work.
|
||||
|
||||
## Findings
|
||||
|
||||
1. **P1: the supervisor's actual advice is still hidden by the default tool-call display.** `src/supervisor-session.ts:157-169` registers `SteerWorker` without a call renderer and returns only a receipt. This fails the user's explicit visibility requirement. Normal emitted assistant text/thinking uses Pi's own display; the local setting already has `hideThinkingBlock: false`. That does not reveal advice inside unrendered tool arguments.
|
||||
|
||||
Observed in the isolated harness:
|
||||
> steer: renderCall=undefined, result=Worker instruction 1 recorded.
|
||||
|
||||
2. **P1: supervisor resume loses monitoring and read-only tool selection.** `src/supervisor-session.ts:94-107` returns when it finds the persisted bootstrap marker, before starting the new process's polling timer or removing write tools. `ready.json` remains present, so the receipt does not identify this loss of supervision. The harness starts, shuts down, then registers a new extension instance using the saved entries and default tools:
|
||||
|
||||
> fresh: deliveredViews=1, activeTools=read, readyReceipt=true
|
||||
> resumed: deliveredViews=0, activeTools=read,write,bash, readyReceipt=true
|
||||
|
||||
This reproduces a branch bug; it is not a diagnosis of the different overnight implementation.
|
||||
|
||||
3. **P1: periodic views can tell the supervisor an idle worker is running.** `src/index.ts:201-203` publishes an interval view without checking idle state. `src/worker-view.ts:45` derives worker status from the review trigger, not actual execution. This undermines decisions about whether continuation is needed.
|
||||
|
||||
> interval view without any work: The worker is still working.
|
||||
|
||||
4. **The intended judgment and recap behavior is not established.** The current supervisor prompt (`src/supervisor-session.ts:70-73`) emphasizes:
|
||||
> Use SteerWorker to give one concrete instruction when work is incomplete.
|
||||
|
||||
Most of the remaining prompt concerns approval checks. It does not request the user's short assessment of progress, independent perspective, or explanation of a recommendation. This is a mismatch in emphasis, not proof that Astra cannot exercise judgment. Neither minimal thinking nor the requirement to use a steer tool establishes a cause of poor advice.
|
||||
|
||||
5. **Low-cost, useful supervision remains untested.** `src/worker-view.ts:41-46` repeats the last compaction summary and up to 12,000 characters from recent messages; it does not make incremental views or measure usefulness/cost. The RPC test (`test/rpc-review.test.ts:51-52`) tests Refine/editor ordering with a deterministic model, not a cheaper worker benefiting from an expensive supervisor. Passing it cannot establish the intended economic or behavioral outcome.
|
||||
|
||||
## What is present
|
||||
|
||||
- Herdr two-pane launch with an explicit planning-session fork: `src/herdr.ts:49-86`.
|
||||
- Separate supervisor model selection via `/goals model`; the worker keeps its model. The cheaper-worker/more-expensive-supervisor arrangement is possible but not established by default or validated on a task.
|
||||
- Initial context compaction above 20k and subsequent compaction at 100k: `src/supervisor-session.ts:8-10,113-155`. This broadly meets the request for compaction around 150k or similar; the exact threshold is not the main gap.
|
||||
- Direct canonical plan path in the supervisor prompt and worker resynchronization after compaction.
|
||||
- Settle, 50-turn, and hourly review triggers. Reliable continuation is incomplete because of the resume/status defects above.
|
||||
|
||||
## Acceptance still needed
|
||||
|
||||
First make the advice visible and correct resume/status behavior. Then run one bounded task in separate test panes with the intended model pair. Save the rendered advice, worker receipt, a useful progress assessment or correction, continuation after compaction/resume, and measured token/cost totals. Judge the content of the advice, not the number of messages or merely successful delivery. Do not use the user's working panes for this test.
|
||||
|
||||
The transport rewrite is an implementation choice, not the user's goal.
|
||||
|
||||
## Reproduction
|
||||
|
||||
The mailbox reproduction script is historical; retrieve it at commit `386305a`. The Intercom migration removes that implementation. Current transport checks are in `test/intercom.test.ts` and `test/intercom-broker.test.ts`.
|
||||
|
||||
[Saved output](20260908_supervisor-intent-reproduction.txt) records the exact observations quoted above. The harness uses only temporary mailbox files and mocked Pi lifecycle events; it neither launches Pi nor contacts another session. It asserts the currently observed failure, not desired behavior.
|
||||
|
||||
-- Pi/OpenAI
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
|
||||
Test Files 9 passed (9)
|
||||
Tests 34 passed (34)
|
||||
Start at 10:49:05
|
||||
Duration 1.34s (transform 639ms, setup 0ms, import 2.76s, tests 1.68s, environment 1ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 17 files in 20ms. No fixes applied.
|
||||
fresh: deliveredViews=1, activeTools=read, readyReceipt=true
|
||||
steer: renderCall=function, result=Worker instruction 1 recorded. Worker receipt and execution are not confirmed.
|
||||
resumed: deliveredViews=0, activeTools=read, readyReceipt=true
|
||||
interval view without any work: The worker stopped.
|
||||
@@ -0,0 +1,4 @@
|
||||
fresh: deliveredViews=1, activeTools=read, readyReceipt=true
|
||||
steer: renderCall=undefined, result=Worker instruction 1 recorded.
|
||||
resumed: deliveredViews=0, activeTools=read,write,bash, readyReceipt=true
|
||||
interval view without any work: The worker is still working.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Real Herdr functional check
|
||||
|
||||
Pi/OpenAI observed this interactive run. Code: HEAD `2b61440` plus uncommitted plan-watch/manual-claim, supervisor-prompt, planning-prompt and startup-compaction changes. These observations do not cover later changes.
|
||||
|
||||
## Task and result
|
||||
|
||||
Isolated repository: `/tmp/pi-goals-herdr-functional-task`. Worker pane `w8:p4V`; second supervisor pane `w8:p4Y`. Real model: openai-codex/gpt-5.6-terra. Parent selected Ready through the rendered menu. No research pane was operated during this check.
|
||||
|
||||
Task: create `hello-again.txt` containing exactly `Hello again.\n`, save byte verification, and commit only the two new files. Commit observed: `1732acccb798e6edd9626fbfde38e3aca941ce4a`.
|
||||
|
||||
Observed worker tool output:
|
||||
|
||||
> CompleteGoal
|
||||
> Sign-off accepted. Goal ticked [x] in
|
||||
> .pi/plan/01a0831d-38ca-76db-b91a-ef4dd0bfecdd-v2.md.
|
||||
|
||||
Observed final supervisor response:
|
||||
|
||||
> Complete. Supervisor approval and CompleteGoal sign-off are recorded.
|
||||
|
||||
Independent parent `od -An -tx1 hello-again.txt` output:
|
||||
|
||||
```text
|
||||
48 65 6c 6c 6f 20 61 67 61 69 6e 2e 0a
|
||||
```
|
||||
|
||||
This is the requested text and final newline. Verification artifact: `/tmp/pi-goals-herdr-functional-task/verify-hello-again.log`.
|
||||
|
||||
## Behavior observed
|
||||
|
||||
The worker manually ticked the goal before sign-off. The widget displayed `claimed complete; awaiting supervisor review` and supervision remained connected. The supervisor read the actual artifact, verification log and plan, then directed the worker to reopen the goal checkbox. The worker reopened it, preserving evidence. The supervisor recorded approval and instructed CompleteGoal. The worker called it successfully. Parent did not supply these corrective instructions or perform the artifact work in this second run.
|
||||
|
||||
## Earlier failures and interventions
|
||||
|
||||
The first task stopped for three routine confirmations because the planning prompt required three questions. Parent answered and removed that quota with subsequent user approval. Ready opened a supervisor, but the separate 20k startup compaction threshold caused `Nothing to compact (session too small)`. Parent removed that lower threshold and reloaded the test supervisor. The first run produced its artifact but manual ticking detached supervision before approval; it was NOT successful workflow acceptance.
|
||||
|
||||
The first run also showed approval attempts against older queued views. Error text now distinguishes a disconnected worker from a newer pending view and directs the supervisor to finish its response to receive the update rather than request another handoff. The second task reached Ready without routine confirmations and completed without parent repair after Ready.
|
||||
|
||||
## Source records
|
||||
|
||||
- Worker: `/home/code/.pi/agent/sessions/--tmp-pi-goals-herdr-functional-task--/2026-09-08T22-22-05-515Z_01a0831d-38ca-76db-b91a-ef4dd0bfecdd.jsonl`
|
||||
- Successful supervisor: `/home/code/.pi/agent/sessions/--tmp-pi-goals-herdr-functional-task--/2026-09-08T22-57-20-349Z_01a0833d-7ddd-7523-92b3-0560f61dbc64.jsonl`
|
||||
- Failed first supervisor: `/home/code/.pi/agent/sessions/--tmp-pi-goals-herdr-functional-task--/2026-09-08T22-26-54-633Z_01a08321-a229-70ad-9007-cba97aef7072.jsonl`
|
||||
|
||||
## Limits
|
||||
|
||||
This proves one real trivial workflow, including visible corrective supervision of a manual tick, artifact delivery and sign-off. It does not establish broad judgment quality or cost savings. Idle external-plan edits, active worker reload recovery, all-cancelled handling, and requested `/goals supervise` and `/goals noplan` still require acceptance. Those commands are not implemented yet. Test panes were left available for inspection. Two old dirty native-evidence files remain untouched and are unrelated to this evidence.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Full-profile supervisor: focused implementation
|
||||
|
||||
## Approved scope
|
||||
|
||||
The user explicitly chose normal Pi extensions and tools, including bash/edit/write and custom actions, with the division of work enforced by role instructions rather than a tool denylist. This change does not grant the supervisor implementation authority: the repeated short opening directs inspection/diagnosis and delegates changes through SteerWorker. The long prompt explicitly states that this is not an enforced sandbox.
|
||||
|
||||
## Changes
|
||||
|
||||
- `src/herdr.ts`: remove only `--no-extensions`; retain explicit source extension, fork, role/binding environment, name and selected model. The inherited environment and normal Pi discovery remain intact.
|
||||
- `src/supervisor-session.ts`: remove the supervisor BLOCKED_TOOLS constant, both active-tool filters, and tool_call denylist hook. No replacement hooks, per-tool reminders, approval changes or lifecycle repair.
|
||||
- `src/prompts.ts`: centralize the concise instruction in the already-repeated opening, and clarify the trust boundary in long orientation.
|
||||
- README/AGENTS: describe normal-profile discovery and instruction-only inspection; avoid claiming hard read-only enforcement or full lifecycle recovery.
|
||||
- Tests assert bash/edit/write/intercom and custom tools survive startup, simulated reload and reconnect without resetting extension selections. The launcher retains normal discovery. The real native Pi RPC test now enables normal discovery in an isolated agent directory, auto-loads a custom inspection tool without `-e`, verifies it reaches the supervisor's model tool schema, and still observes exact SteerWorker delivery. Its worker remains deliberately isolated with `--no-extensions`.
|
||||
|
||||
## Sources inspected
|
||||
|
||||
Installed Pi documentation: `docs/usage.md` extension/resource discovery flags, `docs/extensions.md` active-tool APIs and loading, `docs/packages.md` profile scope/deduplication. Read applicable local `recommending-pi-extensions` skill for the full-permission trust boundary. No packages installed or fetched. Existing Intercom reuse/fallback code is unchanged; broker/native tests pass.
|
||||
|
||||
## Validation
|
||||
|
||||
`validation.txt` records final successful run:
|
||||
|
||||
```
|
||||
env -u PI_GOALS_EVIDENCE_DIR -u PI_SUBAGENT_CHILD -u PI_GOALS_ROLE npm test
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run build
|
||||
git diff --check
|
||||
```
|
||||
|
||||
108/108 tests in 19 files, typecheck, lint (37 files), build and diff check passed. No changes to `src/index.ts` or `src/approval.ts`; worker planning restrictions and approval checks remain intact. No supervisor BLOCKED_TOOLS, setActiveTools or tool_call enforcement remains.
|
||||
|
||||
## Limits and remaining acceptance
|
||||
|
||||
This verifies normal discovery using a deterministic local model and an isolated custom extension. It does not prove a real user's complete profile respects the role instruction. Arbitrary extensions retain their own hooks/side effects/tool policies; tools can still write if the model disregards its task. The parent still needs to run real full-profile Herdr acceptance and obtain an independent review. No panes were opened, reloaded or operated.
|
||||
|
||||
Issue #6 cancellation, Ready content drift, compaction delivery, fresh-shell role restoration and other lifecycle bugs are intentionally not fixed in this scoped task. Existing running supervisors retain their already-loaded profile until appropriately restarted/reloaded by their owner.
|
||||
|
||||
Pre-existing dirty `slop/reviews/review-fixes-native/supervisor-events.jsonl`, `worker-events.jsonl` and untracked `docs/human_journal.md` were neither modified nor staged by this task. No commits include them.
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
|
||||
Test Files 19 passed (19)
|
||||
Tests 108 passed (108)
|
||||
Start at 12:24:25
|
||||
Duration 4.69s (transform 4.90s, setup 0ms, import 9.57s, tests 12.54s, environment 3ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 37 files in 109ms. No fixes applied.
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 build
|
||||
> tsc
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{"type":"extension_ui_request","id":"1b7b765d-82f4-4ac3-bd12-011f1f3227af","method":"notify","message":"supervisor model: offline/test","notifyType":"info"}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-visible-supervisor-v2","data":{"version":2,"workerSessionId":"01a080c2-dc16-705a-b544-e383580d9ac7","planPath":"/tmp/goals-native-pair-vfPu3e/plan.md"},"id":"e0a271f9","parentId":"d768c1fd","timestamp":"2026-09-08T11:24:10.287Z"}}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"in","message":{"binding":"native-pair-test","role":"worker","kind":"view","id":"cf656aef-1404-4d5c-9a11-4fbd4ed5b5dc","text":"The worker stopped.\n\nThe saved plan needs a check of the actual outputs.\n\nworker view id: cf656aef-1404-4d5c-9a11-4fbd4ed5b5dc","reason":"settled","backgroundQuiet":true}},"id":"848cb24d","parentId":"e0a271f9","timestamp":"2026-09-08T11:24:10.295Z"}}
|
||||
{"type":"agent_start"}
|
||||
{"type":"turn_start"}
|
||||
{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"The worker stopped.\n\nThe saved plan needs a check of the actual outputs.\n\nworker view id: cf656aef-1404-4d5c-9a11-4fbd4ed5b5dc"}],"timestamp":1788866650296}}
|
||||
{"type":"message_end","message":{"role":"user","content":[{"type":"text","text":"The worker stopped.\n\nThe saved plan needs a check of the actual outputs.\n\nworker view id: cf656aef-1404-4d5c-9a11-4fbd4ed5b5dc"}],"timestamp":1788866650296}}
|
||||
{"type":"message_start","message":{"role":"assistant","content":[{"type":"toolCall","id":"test-steer","name":"SteerWorker","arguments":{"instruction":"Read the real outputs before declaring completion."},"partialArgs":"{\"instruction\":\"Read the real outputs before declaring completion.\"}","streamIndex":0}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"pending","timestamp":1788866650324}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"toolcall_start","contentIndex":0}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"toolcall_delta","contentIndex":0,"delta":"{\"instruction\":\"Read the real outputs before declaring completion.\"}"}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"toolcall_end","contentIndex":0,"toolCall":{"type":"toolCall","id":"test-steer","name":"SteerWorker","arguments":{"instruction":"Read the real outputs before declaring completion."}}}}
|
||||
{"type":"message_end","message":{"role":"assistant","content":[{"type":"toolCall","id":"test-steer","name":"SteerWorker","arguments":{"instruction":"Read the real outputs before declaring completion."}}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":10,"output":10,"cacheRead":0,"cacheWrite":0,"reasoning":0,"totalTokens":20,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1788866650324,"rawStopReason":"tool_calls"}}
|
||||
{"type":"tool_execution_start","toolCallId":"test-steer","toolName":"SteerWorker","args":{"instruction":"Read the real outputs before declaring completion."}}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"out","message":{"binding":"native-pair-test","role":"supervisor","kind":"steer","id":"22068654-6e7c-4328-890f-9382b25c8ea7","text":"Read the real outputs before declaring completion."}},"id":"0c0166ea","parentId":"ed2e1ca2","timestamp":"2026-09-08T11:24:10.345Z"}}
|
||||
{"type":"tool_execution_end","toolCallId":"test-steer","toolName":"SteerWorker","result":{"content":[{"type":"text","text":"Worker instruction 22068654-6e7c-4328-890f-9382b25c8ea7 sent through pi-intercom. Receipt and execution are not confirmed by this result."}],"details":{},"isError":false},"isError":false}
|
||||
{"type":"message_start","message":{"role":"toolResult","toolCallId":"test-steer","toolName":"SteerWorker","content":[{"type":"text","text":"Worker instruction 22068654-6e7c-4328-890f-9382b25c8ea7 sent through pi-intercom. Receipt and execution are not confirmed by this result."}],"details":{},"isError":false,"timestamp":1788866650346}}
|
||||
{"type":"message_end","message":{"role":"toolResult","toolCallId":"test-steer","toolName":"SteerWorker","content":[{"type":"text","text":"Worker instruction 22068654-6e7c-4328-890f-9382b25c8ea7 sent through pi-intercom. Receipt and execution are not confirmed by this result."}],"details":{},"isError":false,"timestamp":1788866650346}}
|
||||
{"type":"turn_end","message":{"role":"assistant","content":[{"type":"toolCall","id":"test-steer","name":"SteerWorker","arguments":{"instruction":"Read the real outputs before declaring completion."}}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":10,"output":10,"cacheRead":0,"cacheWrite":0,"reasoning":0,"totalTokens":20,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1788866650324,"rawStopReason":"tool_calls"},"toolResults":[{"role":"toolResult","toolCallId":"test-steer","toolName":"SteerWorker","content":[{"type":"text","text":"Worker instruction 22068654-6e7c-4328-890f-9382b25c8ea7 sent through pi-intercom. Receipt and execution are not confirmed by this result."}],"details":{},"isError":false,"timestamp":1788866650346}]}
|
||||
{"type":"turn_start"}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"ack","message":{"binding":"native-pair-test","role":"worker","kind":"received","id":"22068654-6e7c-4328-890f-9382b25c8ea7"}},"id":"6b63b93d","parentId":"8ce5aa46","timestamp":"2026-09-08T11:24:10.348Z"}}
|
||||
{"id":"supervisor-state","type":"response","command":"get_state","success":true,"data":{"model":{"id":"test","name":"Offline test model","reasoning":false,"input":["text"],"contextWindow":16000,"maxTokens":1000,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0},"api":"openai-completions","provider":"offline","baseUrl":"http://127.0.0.1:37773"},"thinkingLevel":"off","isStreaming":true,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/tmp/goals-native-pair-vfPu3e/agent/sessions/--tmp-goals-native-pair-vfPu3e--/2026-09-08T11-24-10-111Z_01a080c2-dfff-7290-9118-66b87ab29080.jsonl","sessionId":"01a080c2-dfff-7290-9118-66b87ab29080","autoCompactionEnabled":true,"messageCount":5,"pendingMessageCount":0}}
|
||||
@@ -0,0 +1,22 @@
|
||||
{"id":"planning","type":"response","command":"prompt","success":true}
|
||||
{"type":"agent_start"}
|
||||
{"type":"turn_start"}
|
||||
{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"Retain this planning context for the supervisor fork."}],"timestamp":1788866649278}}
|
||||
{"type":"message_end","message":{"role":"user","content":[{"type":"text","text":"Retain this planning context for the supervisor fork."}],"timestamp":1788866649278}}
|
||||
{"type":"message_start","message":{"role":"assistant","content":[{"type":"text","text":"Test context retained. Actual outputs still need inspection."}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"pending","timestamp":1788866649355}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"text_start","contentIndex":0}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Test context retained. Actual outputs still need inspection."}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"text_end","contentIndex":0,"content":"Test context retained. Actual outputs still need inspection."}}
|
||||
{"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":"Test context retained. Actual outputs still need inspection."}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":10,"output":10,"cacheRead":0,"cacheWrite":0,"reasoning":0,"totalTokens":20,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1788866649355,"rawStopReason":"stop"}}
|
||||
{"type":"turn_end","message":{"role":"assistant","content":[{"type":"text","text":"Test context retained. Actual outputs still need inspection."}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":10,"output":10,"cacheRead":0,"cacheWrite":0,"reasoning":0,"totalTokens":20,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1788866649355,"rawStopReason":"stop"},"toolResults":[]}
|
||||
{"type":"agent_end","messages":[{"role":"user","content":[{"type":"text","text":"Retain this planning context for the supervisor fork."}],"timestamp":1788866649278},{"role":"assistant","content":[{"type":"text","text":"Test context retained. Actual outputs still need inspection."}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":10,"output":10,"cacheRead":0,"cacheWrite":0,"reasoning":0,"totalTokens":20,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1788866649355,"rawStopReason":"stop"}],"willRetry":false}
|
||||
{"type":"agent_settled"}
|
||||
{"id":"worker-state","type":"response","command":"get_state","success":true,"data":{"model":{"id":"test","name":"Offline test model","reasoning":false,"input":["text"],"contextWindow":16000,"maxTokens":1000,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0},"api":"openai-completions","provider":"offline","baseUrl":"http://127.0.0.1:37773"},"thinkingLevel":"off","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/tmp/goals-native-pair-vfPu3e/agent/sessions/--tmp-goals-native-pair-vfPu3e--/2026-09-08T11-24-09-110Z_01a080c2-dc16-705a-b544-e383580d9ac7.jsonl","sessionId":"01a080c2-dc16-705a-b544-e383580d9ac7","autoCompactionEnabled":true,"messageCount":2,"pendingMessageCount":0}}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"out","message":{"binding":"native-pair-test","role":"worker","kind":"view","id":"cf656aef-1404-4d5c-9a11-4fbd4ed5b5dc","text":"The worker stopped.\n\nThe saved plan needs a check of the actual outputs.\n\nworker view id: cf656aef-1404-4d5c-9a11-4fbd4ed5b5dc","reason":"settled","backgroundQuiet":true}},"id":"57ac2295","parentId":"21d6e68a","timestamp":"2026-09-08T11:24:10.294Z"}}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"ack","message":{"binding":"native-pair-test","role":"supervisor","kind":"received","id":"cf656aef-1404-4d5c-9a11-4fbd4ed5b5dc"}},"id":"7ffd87dc","parentId":"57ac2295","timestamp":"2026-09-08T11:24:10.295Z"}}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"ack","message":{"binding":"native-pair-test","role":"supervisor","kind":"received","id":"cf656aef-1404-4d5c-9a11-4fbd4ed5b5dc"}},"id":"b2ffeed1","parentId":"7ffd87dc","timestamp":"2026-09-08T11:24:10.335Z"}}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"in","message":{"binding":"native-pair-test","role":"supervisor","kind":"steer","id":"22068654-6e7c-4328-890f-9382b25c8ea7","text":"Read the real outputs before declaring completion."}},"id":"34521c62","parentId":"b2ffeed1","timestamp":"2026-09-08T11:24:10.346Z"}}
|
||||
{"type":"agent_start"}
|
||||
{"type":"turn_start"}
|
||||
{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"[supervisor] Read the real outputs before declaring completion."}],"timestamp":1788866650346}}
|
||||
{"type":"message_end","message":{"role":"user","content":[{"type":"text","text":"[supervisor] Read the real outputs before declaring completion."}],"timestamp":1788866650346}}
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
❯ test/goals-flow.test.ts (21 tests | 1 failed | 20 skipped) 71ms
|
||||
× accepts only an approval for the exact clean commit and goal block 70ms
|
||||
|
||||
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
|
||||
|
||||
FAIL test/goals-flow.test.ts > /goals flow > accepts only an approval for the exact clean commit and goal block
|
||||
AssertionError: expected false to be true // Object.is equality
|
||||
|
||||
- Expected
|
||||
+ Received
|
||||
|
||||
- true
|
||||
+ false
|
||||
|
||||
❯ test/goals-flow.test.ts:318:38
|
||||
316| const cancelled = flow.tools.get("CompleteGoal").execute("cancelled…
|
||||
317| controller.abort(); // Cancel while the background-state lookup yie…
|
||||
318| expect((await cancelled).isError).toBe(true);
|
||||
| ^
|
||||
319| expect(readFileSync(planPath, "utf8")).toBe(beforeCancel);
|
||||
320| expect((await flow.tools.get("CompleteGoal").execute("already-cance…
|
||||
|
||||
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
|
||||
|
||||
|
||||
Test Files 1 failed (1)
|
||||
Tests 1 failed | 20 skipped (21)
|
||||
Start at 12:28:54
|
||||
Duration 734ms (transform 273ms, setup 0ms, import 544ms, tests 71ms, environment 0ms)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
|
||||
Test Files 19 passed (19)
|
||||
Tests 110 passed (110)
|
||||
Start at 12:30:42
|
||||
Duration 3.60s (transform 4.23s, setup 0ms, import 8.38s, tests 8.31s, environment 2ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 37 files in 34ms. No fixes applied.
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 build
|
||||
> tsc
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
stdout | test/native-compaction-delivery.test.ts > real Pi preserves worker delivery through compaction success, failure and cancellation
|
||||
real Pi worker/success: retained message presented once and saved, no extension errors
|
||||
|
||||
stdout | test/native-compaction-delivery.test.ts > real Pi preserves worker delivery through compaction success, failure and cancellation
|
||||
real Pi worker/failure: retained message presented once and saved, no extension errors
|
||||
|
||||
stdout | test/native-intercom.test.ts > runs a forked Pi supervisor and receives its exact instruction in another Pi session
|
||||
Native Pi pair: fork retained planning context; SteerWorker delivered exactly: Read the real outputs before declaring completion.
|
||||
|
||||
✓ test/native-intercom.test.ts > runs a forked Pi supervisor and receives its exact instruction in another Pi session 2071ms
|
||||
stdout | test/native-compaction-delivery.test.ts > real Pi preserves worker delivery through compaction success, failure and cancellation
|
||||
real Pi worker/cancel: retained message presented once and saved, no extension errors
|
||||
|
||||
✓ test/native-compaction-delivery.test.ts > real Pi preserves worker delivery through compaction success, failure and cancellation 2239ms
|
||||
stdout | test/native-compaction-delivery.test.ts > real Pi preserves supervisor delivery through compaction success, failure and cancellation
|
||||
real Pi supervisor/success: retained message presented once and saved, no extension errors
|
||||
|
||||
stdout | test/native-compaction-delivery.test.ts > real Pi preserves supervisor delivery through compaction success, failure and cancellation
|
||||
real Pi supervisor/failure: retained message presented once and saved, no extension errors
|
||||
|
||||
stdout | test/native-compaction-delivery.test.ts > real Pi preserves supervisor delivery through compaction success, failure and cancellation
|
||||
real Pi supervisor/cancel: retained message presented once and saved, no extension errors
|
||||
|
||||
✓ test/native-compaction-delivery.test.ts > real Pi preserves supervisor delivery through compaction success, failure and cancellation 2176ms
|
||||
|
||||
Test Files 2 passed (2)
|
||||
Tests 3 passed (3)
|
||||
Start at 12:59:23
|
||||
Duration 4.60s (transform 52ms, setup 0ms, import 133ms, tests 6.49s, environment 0ms)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Lifecycle recovery and role-aware commands
|
||||
|
||||
## Scope and decisions
|
||||
|
||||
Continue issue #6 and the independent lifecycle review after full-profile commit `1d52857`. Keep normal extensions/tools; do not alter Git/evidence approval policy. The supervisor approved delayed role selection at session_start, a durable inbound-message record, and requiring Pi 0.85.1 instead of maintaining a 0.84 compatibility retry layer. The user additionally authorized `work`, `supervise`, and `noplan` recovery commands.
|
||||
|
||||
`138bde5` already committed/pushed the first increment: CompleteGoal cancellation and Ready content stability. Remaining changes build on it, not replace it.
|
||||
|
||||
## Fixes
|
||||
|
||||
- CompleteGoal checks its signal before/after the asynchronous background lookup and before persistence; stale binding/version/phase or a disposed runtime also rejects. Cancelling one call leaves the plan active. `cancellation-before.txt` records a regression failure against the earlier implementation (it returned success after abort); the updated test verifies no tick/sign-off marker is written and a later valid call still works.
|
||||
- Ready checks the displayed plan content before launch, after launch, after worker-model restoration and after the first view. A changed plan returns to planning using the existing pane; it does not start the changed implementation. Tests mutate content at launch and model restoration, then select Ready again without another pane.
|
||||
- Real supervisor and worker sessions now select their role when session entries are available, before registering either role's handlers. A complete supervisor marker (worker, owner, plan path, approval binding) is written before model restoration/compaction. Persisted identity wins over launch environment. Legacy bootstrap markers migrate only with an existing pairing; incomplete records throw visibly without enabling worker mode. Stopped supervisor forks retain their identity. No tool denylist was reintroduced.
|
||||
- Intercom saves each incoming non-started view/steer in a bounded inbox before calling Pi. It acknowledges only a matching user message_start. That is **session acceptance**, not model judgment or action execution. Unconfirmed payloads are restored from the active branch on reload; separate incremental views are retained rather than overwritten. A pending Pi queue is not re-enqueued while waiting for presentation. Completed/detached bindings and shutdown cancel delivery timers.
|
||||
- Pi 0.85.1 has a public session_compact_failed event and fixes isIdle to include manual compaction; repo 0.84.1 did not. The minimum peer and exact dev version are now 0.85.1. Installation used `--ignore-scripts --no-audit --no-fund`; no global Pi change. Success/failure/cancel events resume retained context. Since session_compact precedes final controller cleanup, delivery waits for compaction-aware idle state; it never probes by sending a prompt during compaction. Waiting is bounded to 300 one-second idle checks with retained payload and a visible reconnect instruction on exhaustion, not a deadline that interrupts the model.
|
||||
- Readiness/reconnect paths allow five minutes rather than five seconds. Reload while an inherited compaction is active waits for it rather than starting a second one. An existing last compaction or Pi's Already compacted/Nothing to compact result can proceed to bootstrap. Startup model/compaction failure is communicated through the existing hello so the worker sees the cause promptly. A later ready hello clears the failure.
|
||||
- An established worker pairing republishes one fresh current view when disconnected→connected, including after supervisor-only reload where the old stopped view had already been accepted. This reuses the connection callback; Ready retains its own initial publication and session_start/reconnect no longer separately publish duplicates. Cleared/completed sessions do not restart monitoring.
|
||||
- A started-worker view now reports that work is running instead of falsely claiming a newer review is queued for delivery.
|
||||
|
||||
## Command meanings
|
||||
|
||||
- `/goals work`: existing approved worker session reconnects its saved pairing/model; no new plan, pairing or model fallback. Missing/unapproved pairing is rejected.
|
||||
- `/goals supervise`: existing saved supervisor reconnects its role/model/pairing. Running it in a worker session is rejected rather than converting the role.
|
||||
- `/goals noplan`: leave planning restrictions and preserve the draft/history without Ready, implementation, supervisor launch or file deletion. In-flight Ready is invalidated. It does not claim the retained draft was approved.
|
||||
- `/goals reconnect` remains generic recovery; `/goals restart` explicitly replaces only the tracked pane and invalidates the prior binding; `/goals clear` closes/disconnects while retaining the plan file.
|
||||
|
||||
## Runtime validation actually observed
|
||||
|
||||
`native-validation.txt` is fresh verbose output from installed Pi 0.85.1 with a local deterministic HTTP model; no credentials or model credits used.
|
||||
|
||||
1. Real Pi worker and supervisor delivery during manual compaction: success, local model failure and cancellation, six cases total. Each retained payload is presented exactly once and saved in the session; no extension_error events. The transport in this fixture is deterministic, while Pi owns the real compaction and prompt lifecycle. Success uses an extension-provided summary; failure exercises Pi's HTTP summarization failure. These are not rendered Herdr sessions or 60-second real-model runs.
|
||||
2. Real native Pi/Intercom pair: full-profile discovery in an isolated agent directory, exact steering delivery, then supervisor termination and fresh-shell `--session` resume with role/binding launcher environment removed. The resumed model sees SteerWorker/ApproveGoal and the discovered profile tool, not CompleteGoal, and retains the supervisor opening. No second supervisor pairing is constructed.
|
||||
|
||||
Hook tests additionally cover a simulated 60-second inherited compaction without competing compaction, five-minute Ready/reconnect patience, immediate reported failure plus rejoin, retained distinct deltas through reload, delayed presentation without duplicate enqueue, role migration/incomplete identity, cancelled completion, Ready content drift, and command semantics. Existing paired tests cover symmetric reconnect/model restoration. The accepted-view reconnect regression checks one new view ID and unchanged-payload replay deduplication separately; cleared/completed pairings produce no new view. The tests model `/reload` with new extension instances or saved state; no real interactive `/reload` command was exercised in this task.
|
||||
|
||||
Final `validation.txt`: 123 tests in 22 files, typecheck, lint, build, and diff check pass. Native fixture initially could not compact a single retained turn; it now seeds two sufficiently sized turns. This corrected fixture setup is not counted as a product failure. No test processes from earlier runs remained when resuming after timeout; all processes started by these tests were shut down.
|
||||
|
||||
## Limits and remaining acceptance
|
||||
|
||||
The parent must still run full-profile Herdr acceptance: actual reloads in both orders, drafting/Ready/checkpoint interruption, stopped pair resume, real-model long compaction, and an unmet-outcome correction followed by both sign-offs. Automated session acceptance does not establish judgment quality or cheaper-worker success.
|
||||
|
||||
The inbox holds at most 64 messages; overflow is visible and unacknowledged. Arbitrary extensions that rewrite or consume injected user messages can defeat exact-text acceptance matching; crashes between message_start and message persistence can require review/replay. This is not an exactly-once execution guarantee or a general durable model queue. Role instructions remain the only prohibition on supervisor writes. Existing all-cancelled completion behavior and Git-tracked verification policy are unchanged.
|
||||
|
||||
No user or test Herdr panes, research sessions, human journal, or pre-existing dirty native evidence logs were operated/read/edited by this task. Changes to package-lock reflect the approved local Pi dependency upgrade. This report does not claim all issue #6 behavioral acceptance is complete.
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
|
||||
Test Files 22 passed (22)
|
||||
Tests 123 passed (123)
|
||||
Start at 13:08:28
|
||||
Duration 4.90s (transform 5.07s, setup 0ms, import 11.10s, tests 11.14s, environment 4ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 41 files in 77ms. No fixes applied.
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 build
|
||||
> tsc
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{"type":"extension_ui_request","id":"bd32f722-e23e-418f-9c7d-ad1c95622d41","method":"notify","message":"supervisor model: offline/test","notifyType":"info"}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-visible-supervisor-v2","data":{"version":2,"workerSessionId":"01a08038-30f7-77a0-943b-8a16928ce6f1","planPath":"/tmp/goals-native-pair-KSyOe8/plan.md"},"id":"6b40c31e","parentId":"a6375cb3","timestamp":"2026-09-08T08:52:42.516Z"}}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"in","message":{"binding":"native-pair-test","role":"worker","kind":"view","id":"508e947c-aff9-427b-a8ff-6f8158fada1f","text":"The worker stopped.\n\nThe saved plan needs a check of the actual outputs.\n\nworker view id: 508e947c-aff9-427b-a8ff-6f8158fada1f","reason":"settled","backgroundQuiet":true}},"id":"666c7a1b","parentId":"6b40c31e","timestamp":"2026-09-08T08:52:42.525Z"}}
|
||||
{"type":"agent_start"}
|
||||
{"type":"turn_start"}
|
||||
{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"The worker stopped.\n\nThe saved plan needs a check of the actual outputs.\n\nworker view id: 508e947c-aff9-427b-a8ff-6f8158fada1f"}],"timestamp":1788857562526}}
|
||||
{"type":"message_end","message":{"role":"user","content":[{"type":"text","text":"The worker stopped.\n\nThe saved plan needs a check of the actual outputs.\n\nworker view id: 508e947c-aff9-427b-a8ff-6f8158fada1f"}],"timestamp":1788857562526}}
|
||||
{"type":"message_start","message":{"role":"assistant","content":[{"type":"toolCall","id":"test-steer","name":"SteerWorker","arguments":{"instruction":"Read the real outputs before declaring completion."},"partialArgs":"{\"instruction\":\"Read the real outputs before declaring completion.\"}","streamIndex":0}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"pending","timestamp":1788857562553}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"toolcall_start","contentIndex":0}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"toolcall_delta","contentIndex":0,"delta":"{\"instruction\":\"Read the real outputs before declaring completion.\"}"}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"toolcall_end","contentIndex":0,"toolCall":{"type":"toolCall","id":"test-steer","name":"SteerWorker","arguments":{"instruction":"Read the real outputs before declaring completion."}}}}
|
||||
{"type":"message_end","message":{"role":"assistant","content":[{"type":"toolCall","id":"test-steer","name":"SteerWorker","arguments":{"instruction":"Read the real outputs before declaring completion."}}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":10,"output":10,"cacheRead":0,"cacheWrite":0,"reasoning":0,"totalTokens":20,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1788857562553,"rawStopReason":"tool_calls"}}
|
||||
{"type":"tool_execution_start","toolCallId":"test-steer","toolName":"SteerWorker","args":{"instruction":"Read the real outputs before declaring completion."}}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"out","message":{"binding":"native-pair-test","role":"supervisor","kind":"steer","id":"1508350f-7ffc-4b01-a976-da06813569f8","text":"Read the real outputs before declaring completion."}},"id":"066af95c","parentId":"1bcc9002","timestamp":"2026-09-08T08:52:42.575Z"}}
|
||||
{"type":"tool_execution_end","toolCallId":"test-steer","toolName":"SteerWorker","result":{"content":[{"type":"text","text":"Worker instruction 1508350f-7ffc-4b01-a976-da06813569f8 sent through pi-intercom. Receipt and execution are not confirmed by this result."}],"details":{},"isError":false},"isError":false}
|
||||
{"type":"message_start","message":{"role":"toolResult","toolCallId":"test-steer","toolName":"SteerWorker","content":[{"type":"text","text":"Worker instruction 1508350f-7ffc-4b01-a976-da06813569f8 sent through pi-intercom. Receipt and execution are not confirmed by this result."}],"details":{},"isError":false,"timestamp":1788857562575}}
|
||||
{"type":"message_end","message":{"role":"toolResult","toolCallId":"test-steer","toolName":"SteerWorker","content":[{"type":"text","text":"Worker instruction 1508350f-7ffc-4b01-a976-da06813569f8 sent through pi-intercom. Receipt and execution are not confirmed by this result."}],"details":{},"isError":false,"timestamp":1788857562575}}
|
||||
{"type":"turn_end","message":{"role":"assistant","content":[{"type":"toolCall","id":"test-steer","name":"SteerWorker","arguments":{"instruction":"Read the real outputs before declaring completion."}}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":10,"output":10,"cacheRead":0,"cacheWrite":0,"reasoning":0,"totalTokens":20,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1788857562553,"rawStopReason":"tool_calls"},"toolResults":[{"role":"toolResult","toolCallId":"test-steer","toolName":"SteerWorker","content":[{"type":"text","text":"Worker instruction 1508350f-7ffc-4b01-a976-da06813569f8 sent through pi-intercom. Receipt and execution are not confirmed by this result."}],"details":{},"isError":false,"timestamp":1788857562575}]}
|
||||
{"type":"turn_start"}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"ack","message":{"binding":"native-pair-test","role":"worker","kind":"received","id":"1508350f-7ffc-4b01-a976-da06813569f8"}},"id":"46f0a926","parentId":"7f0c0a7d","timestamp":"2026-09-08T08:52:42.578Z"}}
|
||||
{"id":"supervisor-state","type":"response","command":"get_state","success":true,"data":{"model":{"id":"test","name":"Offline test model","reasoning":false,"input":["text"],"contextWindow":16000,"maxTokens":1000,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0},"api":"openai-completions","provider":"offline","baseUrl":"http://127.0.0.1:38351"},"thinkingLevel":"off","isStreaming":true,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/tmp/goals-native-pair-KSyOe8/agent/sessions/--tmp-goals-native-pair-KSyOe8--/2026-09-08T08-52-42-353Z_01a08038-34f1-74a8-a4a6-0f10c436d961.jsonl","sessionId":"01a08038-34f1-74a8-a4a6-0f10c436d961","autoCompactionEnabled":true,"messageCount":5,"pendingMessageCount":0}}
|
||||
@@ -0,0 +1,21 @@
|
||||
{"id":"planning","type":"response","command":"prompt","success":true}
|
||||
{"type":"agent_start"}
|
||||
{"type":"turn_start"}
|
||||
{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"Retain this planning context for the supervisor fork."}],"timestamp":1788857561517}}
|
||||
{"type":"message_end","message":{"role":"user","content":[{"type":"text","text":"Retain this planning context for the supervisor fork."}],"timestamp":1788857561517}}
|
||||
{"type":"message_start","message":{"role":"assistant","content":[{"type":"text","text":"Test context retained. Actual outputs still need inspection."}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"pending","timestamp":1788857561554}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"text_start","contentIndex":0}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Test context retained. Actual outputs still need inspection."}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"text_end","contentIndex":0,"content":"Test context retained. Actual outputs still need inspection."}}
|
||||
{"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":"Test context retained. Actual outputs still need inspection."}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":10,"output":10,"cacheRead":0,"cacheWrite":0,"reasoning":0,"totalTokens":20,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1788857561554,"rawStopReason":"stop"}}
|
||||
{"type":"turn_end","message":{"role":"assistant","content":[{"type":"text","text":"Test context retained. Actual outputs still need inspection."}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":10,"output":10,"cacheRead":0,"cacheWrite":0,"reasoning":0,"totalTokens":20,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1788857561554,"rawStopReason":"stop"},"toolResults":[]}
|
||||
{"type":"agent_end","messages":[{"role":"user","content":[{"type":"text","text":"Retain this planning context for the supervisor fork."}],"timestamp":1788857561517},{"role":"assistant","content":[{"type":"text","text":"Test context retained. Actual outputs still need inspection."}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":10,"output":10,"cacheRead":0,"cacheWrite":0,"reasoning":0,"totalTokens":20,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1788857561554,"rawStopReason":"stop"}],"willRetry":false}
|
||||
{"type":"agent_settled"}
|
||||
{"id":"worker-state","type":"response","command":"get_state","success":true,"data":{"model":{"id":"test","name":"Offline test model","reasoning":false,"input":["text"],"contextWindow":16000,"maxTokens":1000,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0},"api":"openai-completions","provider":"offline","baseUrl":"http://127.0.0.1:38351"},"thinkingLevel":"off","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/tmp/goals-native-pair-KSyOe8/agent/sessions/--tmp-goals-native-pair-KSyOe8--/2026-09-08T08-52-41-336Z_01a08038-30f7-77a0-943b-8a16928ce6f1.jsonl","sessionId":"01a08038-30f7-77a0-943b-8a16928ce6f1","autoCompactionEnabled":true,"messageCount":2,"pendingMessageCount":0}}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"out","message":{"binding":"native-pair-test","role":"worker","kind":"view","id":"508e947c-aff9-427b-a8ff-6f8158fada1f","text":"The worker stopped.\n\nThe saved plan needs a check of the actual outputs.\n\nworker view id: 508e947c-aff9-427b-a8ff-6f8158fada1f","reason":"settled","backgroundQuiet":true}},"id":"ac6b2134","parentId":"741cdd0c","timestamp":"2026-09-08T08:52:42.523Z"}}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"ack","message":{"binding":"native-pair-test","role":"supervisor","kind":"received","id":"508e947c-aff9-427b-a8ff-6f8158fada1f"}},"id":"fd82ceb3","parentId":"ac6b2134","timestamp":"2026-09-08T08:52:42.525Z"}}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"in","message":{"binding":"native-pair-test","role":"supervisor","kind":"steer","id":"1508350f-7ffc-4b01-a976-da06813569f8","text":"Read the real outputs before declaring completion."}},"id":"495adb1a","parentId":"fd82ceb3","timestamp":"2026-09-08T08:52:42.575Z"}}
|
||||
{"type":"agent_start"}
|
||||
{"type":"turn_start"}
|
||||
{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"[supervisor] Read the real outputs before declaring completion."}],"timestamp":1788857562576}}
|
||||
{"type":"message_end","message":{"role":"user","content":[{"type":"text","text":"[supervisor] Read the real outputs before declaring completion."}],"timestamp":1788857562576}}
|
||||
@@ -0,0 +1,21 @@
|
||||
{"type":"extension_ui_request","id":"ec31db54-0eee-4cc4-bbdc-550e3297292e","method":"notify","message":"supervisor model: offline/test","notifyType":"info"}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-visible-supervisor-v2","data":{"version":2,"workerSessionId":"01a080aa-3018-770c-8926-822a3e1a2aa9","planPath":"/tmp/goals-native-pair-Mk0U2D/plan.md"},"id":"280d42da","parentId":"6c8e3540","timestamp":"2026-09-08T10:57:13.430Z"}}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"in","message":{"binding":"native-pair-test","role":"worker","kind":"view","id":"b483d8c1-8c16-4f2b-8d2f-2de98f190337","text":"The worker stopped.\n\nThe saved plan needs a check of the actual outputs.\n\nworker view id: b483d8c1-8c16-4f2b-8d2f-2de98f190337","reason":"settled","backgroundQuiet":true}},"id":"97ea169c","parentId":"280d42da","timestamp":"2026-09-08T10:57:13.439Z"}}
|
||||
{"type":"agent_start"}
|
||||
{"type":"turn_start"}
|
||||
{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"The worker stopped.\n\nThe saved plan needs a check of the actual outputs.\n\nworker view id: b483d8c1-8c16-4f2b-8d2f-2de98f190337"}],"timestamp":1788865033440}}
|
||||
{"type":"message_end","message":{"role":"user","content":[{"type":"text","text":"The worker stopped.\n\nThe saved plan needs a check of the actual outputs.\n\nworker view id: b483d8c1-8c16-4f2b-8d2f-2de98f190337"}],"timestamp":1788865033440}}
|
||||
{"type":"message_start","message":{"role":"assistant","content":[{"type":"toolCall","id":"test-steer","name":"SteerWorker","arguments":{"instruction":"Read the real outputs before declaring completion."},"partialArgs":"{\"instruction\":\"Read the real outputs before declaring completion.\"}","streamIndex":0}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"pending","timestamp":1788865033468}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"toolcall_start","contentIndex":0}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"toolcall_delta","contentIndex":0,"delta":"{\"instruction\":\"Read the real outputs before declaring completion.\"}"}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"toolcall_end","contentIndex":0,"toolCall":{"type":"toolCall","id":"test-steer","name":"SteerWorker","arguments":{"instruction":"Read the real outputs before declaring completion."}}}}
|
||||
{"type":"message_end","message":{"role":"assistant","content":[{"type":"toolCall","id":"test-steer","name":"SteerWorker","arguments":{"instruction":"Read the real outputs before declaring completion."}}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":10,"output":10,"cacheRead":0,"cacheWrite":0,"reasoning":0,"totalTokens":20,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1788865033468,"rawStopReason":"tool_calls"}}
|
||||
{"type":"tool_execution_start","toolCallId":"test-steer","toolName":"SteerWorker","args":{"instruction":"Read the real outputs before declaring completion."}}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"out","message":{"binding":"native-pair-test","role":"supervisor","kind":"steer","id":"780f7dac-e4e5-4351-b890-0684a42de681","text":"Read the real outputs before declaring completion."}},"id":"d35e2a1d","parentId":"d5c62400","timestamp":"2026-09-08T10:57:13.489Z"}}
|
||||
{"type":"tool_execution_end","toolCallId":"test-steer","toolName":"SteerWorker","result":{"content":[{"type":"text","text":"Worker instruction 780f7dac-e4e5-4351-b890-0684a42de681 sent through pi-intercom. Receipt and execution are not confirmed by this result."}],"details":{},"isError":false},"isError":false}
|
||||
{"type":"message_start","message":{"role":"toolResult","toolCallId":"test-steer","toolName":"SteerWorker","content":[{"type":"text","text":"Worker instruction 780f7dac-e4e5-4351-b890-0684a42de681 sent through pi-intercom. Receipt and execution are not confirmed by this result."}],"details":{},"isError":false,"timestamp":1788865033490}}
|
||||
{"type":"message_end","message":{"role":"toolResult","toolCallId":"test-steer","toolName":"SteerWorker","content":[{"type":"text","text":"Worker instruction 780f7dac-e4e5-4351-b890-0684a42de681 sent through pi-intercom. Receipt and execution are not confirmed by this result."}],"details":{},"isError":false,"timestamp":1788865033490}}
|
||||
{"type":"turn_end","message":{"role":"assistant","content":[{"type":"toolCall","id":"test-steer","name":"SteerWorker","arguments":{"instruction":"Read the real outputs before declaring completion."}}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":10,"output":10,"cacheRead":0,"cacheWrite":0,"reasoning":0,"totalTokens":20,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1788865033468,"rawStopReason":"tool_calls"},"toolResults":[{"role":"toolResult","toolCallId":"test-steer","toolName":"SteerWorker","content":[{"type":"text","text":"Worker instruction 780f7dac-e4e5-4351-b890-0684a42de681 sent through pi-intercom. Receipt and execution are not confirmed by this result."}],"details":{},"isError":false,"timestamp":1788865033490}]}
|
||||
{"type":"turn_start"}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"ack","message":{"binding":"native-pair-test","role":"worker","kind":"received","id":"780f7dac-e4e5-4351-b890-0684a42de681"}},"id":"13443ef1","parentId":"573b245e","timestamp":"2026-09-08T10:57:13.492Z"}}
|
||||
{"id":"supervisor-state","type":"response","command":"get_state","success":true,"data":{"model":{"id":"test","name":"Offline test model","reasoning":false,"input":["text"],"contextWindow":16000,"maxTokens":1000,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0},"api":"openai-completions","provider":"offline","baseUrl":"http://127.0.0.1:34037"},"thinkingLevel":"off","isStreaming":true,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/tmp/goals-native-pair-Mk0U2D/agent/sessions/--tmp-goals-native-pair-Mk0U2D--/2026-09-08T10-57-13-272Z_01a080aa-3438-70b0-8498-53cf3c7d68b3.jsonl","sessionId":"01a080aa-3438-70b0-8498-53cf3c7d68b3","autoCompactionEnabled":true,"messageCount":5,"pendingMessageCount":0}}
|
||||
@@ -0,0 +1,21 @@
|
||||
{"id":"planning","type":"response","command":"prompt","success":true}
|
||||
{"type":"agent_start"}
|
||||
{"type":"turn_start"}
|
||||
{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"Retain this planning context for the supervisor fork."}],"timestamp":1788865032458}}
|
||||
{"type":"message_end","message":{"role":"user","content":[{"type":"text","text":"Retain this planning context for the supervisor fork."}],"timestamp":1788865032458}}
|
||||
{"type":"message_start","message":{"role":"assistant","content":[{"type":"text","text":"Test context retained. Actual outputs still need inspection."}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"pending","timestamp":1788865032494}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"text_start","contentIndex":0}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Test context retained. Actual outputs still need inspection."}}
|
||||
{"type":"message_update","assistantMessageEvent":{"type":"text_end","contentIndex":0,"content":"Test context retained. Actual outputs still need inspection."}}
|
||||
{"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":"Test context retained. Actual outputs still need inspection."}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":10,"output":10,"cacheRead":0,"cacheWrite":0,"reasoning":0,"totalTokens":20,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1788865032494,"rawStopReason":"stop"}}
|
||||
{"type":"turn_end","message":{"role":"assistant","content":[{"type":"text","text":"Test context retained. Actual outputs still need inspection."}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":10,"output":10,"cacheRead":0,"cacheWrite":0,"reasoning":0,"totalTokens":20,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1788865032494,"rawStopReason":"stop"},"toolResults":[]}
|
||||
{"type":"agent_end","messages":[{"role":"user","content":[{"type":"text","text":"Retain this planning context for the supervisor fork."}],"timestamp":1788865032458},{"role":"assistant","content":[{"type":"text","text":"Test context retained. Actual outputs still need inspection."}],"api":"openai-completions","provider":"offline","model":"test","usage":{"input":10,"output":10,"cacheRead":0,"cacheWrite":0,"reasoning":0,"totalTokens":20,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1788865032494,"rawStopReason":"stop"}],"willRetry":false}
|
||||
{"type":"agent_settled"}
|
||||
{"id":"worker-state","type":"response","command":"get_state","success":true,"data":{"model":{"id":"test","name":"Offline test model","reasoning":false,"input":["text"],"contextWindow":16000,"maxTokens":1000,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0},"api":"openai-completions","provider":"offline","baseUrl":"http://127.0.0.1:34037"},"thinkingLevel":"off","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/tmp/goals-native-pair-Mk0U2D/agent/sessions/--tmp-goals-native-pair-Mk0U2D--/2026-09-08T10-57-12-216Z_01a080aa-3018-770c-8926-822a3e1a2aa9.jsonl","sessionId":"01a080aa-3018-770c-8926-822a3e1a2aa9","autoCompactionEnabled":true,"messageCount":2,"pendingMessageCount":0}}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"out","message":{"binding":"native-pair-test","role":"worker","kind":"view","id":"b483d8c1-8c16-4f2b-8d2f-2de98f190337","text":"The worker stopped.\n\nThe saved plan needs a check of the actual outputs.\n\nworker view id: b483d8c1-8c16-4f2b-8d2f-2de98f190337","reason":"settled","backgroundQuiet":true}},"id":"0615fd50","parentId":"0390c249","timestamp":"2026-09-08T10:57:13.437Z"}}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"ack","message":{"binding":"native-pair-test","role":"supervisor","kind":"received","id":"b483d8c1-8c16-4f2b-8d2f-2de98f190337"}},"id":"4df9ba42","parentId":"0615fd50","timestamp":"2026-09-08T10:57:13.439Z"}}
|
||||
{"type":"entry_appended","entry":{"type":"custom","customType":"pi-goals-intercom","data":{"direction":"in","message":{"binding":"native-pair-test","role":"supervisor","kind":"steer","id":"780f7dac-e4e5-4351-b890-0684a42de681","text":"Read the real outputs before declaring completion."}},"id":"814b241e","parentId":"4df9ba42","timestamp":"2026-09-08T10:57:13.490Z"}}
|
||||
{"type":"agent_start"}
|
||||
{"type":"turn_start"}
|
||||
{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"[supervisor] Read the real outputs before declaring completion."}],"timestamp":1788865033490}}
|
||||
{"type":"message_end","message":{"role":"user","content":[{"type":"text","text":"[supervisor] Read the real outputs before declaring completion."}],"timestamp":1788865033490}}
|
||||
@@ -0,0 +1,66 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
|
||||
Test Files 19 passed (19)
|
||||
Tests 108 passed (108)
|
||||
Start at 10:47:37
|
||||
Duration 3.83s (transform 2.36s, setup 0ms, import 6.55s, tests 9.35s, environment 2ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
src/worker-view.ts:2:1 assist/source/organizeImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
× Sort the imported names.
|
||||
|
||||
1 │ import { compile } from "@sting8k/pi-vcc/src/core/summarize";
|
||||
> 2 │ import { supervisorCheckIn, type SupervisorReviewReason } from "./prompts.js";
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
3 │
|
||||
4 │ export interface SessionBlock {
|
||||
|
||||
i Safe fix: Organize imports and exports (Biome)
|
||||
|
||||
1 1 │ import { compile } from "@sting8k/pi-vcc/src/core/summarize";
|
||||
2 │ - import·{·supervisorCheckIn,·type·SupervisorReviewReason·}·from·"./prompts.js";
|
||||
2 │ + import·{·type·SupervisorReviewReason,·supervisorCheckIn·}·from·"./prompts.js";
|
||||
3 3 │
|
||||
4 4 │ export interface SessionBlock {
|
||||
|
||||
|
||||
test/worker-view.test.ts:2:1 assist/source/organizeImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
× Sort the imported names.
|
||||
|
||||
1 │ import { expect, it } from "vitest";
|
||||
> 2 │ import { supervisorPeriodicReview, supervisorPlanChangeReview, supervisorReadyReview, type SupervisorReviewReason, supervisorStartedReview, supervisorStoppedReview } from "../src/prompts.js";
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
3 │ import { workerView } from "../src/worker-view.js";
|
||||
4 │
|
||||
|
||||
i Safe fix: Organize imports and exports (Biome)
|
||||
|
||||
1 1 │ import { expect, it } from "vitest";
|
||||
2 │ - import·{·supervisorPeriodicReview,·supervisorPlanChangeReview,·supervisorReadyReview,·type·SupervisorReviewReason,·supervisorStartedReview,·supervisorStoppedReview·}·from·"../src/prompts.js";
|
||||
2 │ + import·{·type·SupervisorReviewReason,·supervisorPeriodicReview,·supervisorPlanChangeReview,·supervisorReadyReview,·supervisorStartedReview,·supervisorStoppedReview·}·from·"../src/prompts.js";
|
||||
3 3 │ import { workerView } from "../src/worker-view.js";
|
||||
4 4 │
|
||||
|
||||
|
||||
Checked 36 files in 63ms. No fixes applied.
|
||||
Found 2 errors.
|
||||
check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
× Some errors were emitted while running checks.
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Supervisor prompt flow review
|
||||
|
||||
Pi/OpenAI implementation, based on `a7385d4`. Scope: centralize supervisor instructions in `src/prompts.ts` and make check-in tasks and tool descriptions ask for judgment followed by useful action. No transport, lifecycle, approval-gate, plan-selection, or planning-policy changes.
|
||||
|
||||
## Narrative order and wiring
|
||||
|
||||
1. Existing planning and worker resync prompts, unchanged.
|
||||
2. `supervisorOpening`, `supervisorPrompt`, `supervisorReviewContext`, `supervisorOrientation`, `supervisorCompaction`: role and plan context. The user-authored agency opening and constitution/pi-supervisor provenance are retained. Long role asks for applicable AGENTS.md/skills, remains generic, removes one duplicate autonomy paragraph, and makes SteerWorker—not a recap—the continuation action. The short review and startup/compaction cadence are unchanged.
|
||||
3. `supervisorCheckIn`: ready, started, active periodic, stopped/settled, plan-edit tasks. `src/worker-view.ts` invokes it outside truncatable activity content. Status prefixes remain exactly `The worker is ready to begin.`, `The worker is still working.`, and `The worker stopped.`. Observed idleness still governs the prefix; an idle interval gets stopped guidance, while a nominal settled event that is not idle gets active-work guidance.
|
||||
4. `supervisorPlanReview`: existing diff/claim data plus plan-change guidance, wired from `src/index.ts`. The guidance now precedes truncatable diff detail so long diffs do not evict it.
|
||||
5. SteerWorker description, parameter description and delivery result.
|
||||
6. ApproveGoal description, parameter descriptions and approval-success instruction. Acceptance is conditional on the supervisor judging the result achieved; mechanics are a separate paragraph. Gate errors stay at their checks, unchanged. The successful result tells the supervisor to use SteerWorker for CompleteGoal and continue remaining goals.
|
||||
7. Existing worker CompleteGoal description corrected to address its caller: the worker runs verification and seeks supervisor review first; the tool consumes recorded approval. It no longer tells the worker to "direct the worker" or implies the read-only supervisor can create evidence. Approval gates are unchanged.
|
||||
|
||||
Runtime data labels and view serialization remain near their producers, rather than turning this into a string registry. The dynamic mechanical errors remain in supervisor-session.ts as allowed by the task.
|
||||
|
||||
## Exact event tasks
|
||||
|
||||
Ready:
|
||||
> Check the agreed outcome and decide the next useful action. Use SteerWorker to send the worker a concrete starting instruction; do not repeat one already being acted on.
|
||||
|
||||
Started:
|
||||
> The worker has begun a turn. Check whether its direction fits the agreed goal; let productive work continue and use SteerWorker only if a correction is needed.
|
||||
|
||||
Active periodic:
|
||||
> Is the worker on track toward the user's intended outcome? Check for drift, mistaken assumptions, or wasted effort. Use SteerWorker to send a correction where useful; otherwise let productive work continue without interruption.
|
||||
|
||||
Stopped/settled:
|
||||
> Inspect the results and judge whether the agreed goal is actually achieved. If unfinished, investigate why the worker stopped and use SteerWorker to send the next useful instruction and resume work. If a verified dependency prevents progress, establish what will resume it and how that will be observed. Do not treat stopping as completion. Consider ApproveGoal only after the results satisfy the goal.
|
||||
|
||||
Plan edit/manual tick:
|
||||
> Assess plan changes against the user's intent and preferences. Manual checkbox edits are claims, not proof of completion. Inspect the actual result before accepting a claim; use SteerWorker to send corrections when the plan or work has drifted. Preserve authorized changes.
|
||||
|
||||
ApproveGoal decision paragraph:
|
||||
> Use only after judging that the actual result satisfies the user's intended outcome and the goal's discriminator. This tool records your acceptance; its mechanical checks cannot establish success. If the goal is unmet or evidence is insufficient, do not approve: use SteerWorker to request the next useful work or check.
|
||||
|
||||
## Validation and limits
|
||||
|
||||
- Read AGENTS.md, annoy-less skill and installed Pi extension docs: before_agent_start persistent custom messages/chained system prompt, and sendUserMessage behavior (an idle worker starts a turn; an active worker receives queued steering).
|
||||
- `validation.txt`: 108 tests pass in 19 files, including real installed Pi RPC and native fork/Intercom checks; typecheck, lint, build and diff check pass.
|
||||
- `initial-validation.txt`: same tests/typecheck passed, lint found only two import-order issues. Fixed those and reran the full command successfully.
|
||||
- Added 9 worker-view event/status combinations and 3 prompt-semantic tests. Updated flow tests assert manual ticks and external plan edits carry judgment/continuation instructions. Supervisor hook/tool tests verify centralized text is wired, including startup/compaction and approval success.
|
||||
- Native pair fixture now produces its view through the actual workerView. The real Pi provider request is asserted to contain the stopped task and actual registered SteerWorker/ApproveGoal descriptions, and its emitted instruction reaches the worker exactly. The local model is deterministic: this establishes wiring, not judgment quality.
|
||||
- Approval logic/transport/plan extraction are unchanged. `git diff --quiet HEAD -- src/approval.ts src/intercom.ts src/plan-view.ts src/plan.ts` passed before commit. Unicode envelope budget regression still passes with the added event tasks.
|
||||
- No user or test Herdr panes operated. No push. Unrelated dirty native logs were not changed; docs/human_journal.md was never read or written. Tests unset inherited PI_GOALS_EVIDENCE_DIR, PI_SUBAGENT_CHILD and PI_GOALS_ROLE.
|
||||
|
||||
## Parent acceptance still required
|
||||
|
||||
In a new isolated Herdr task, require an exact result (for example a specific byte sequence). Let the worker stop with a real artifact that fails that goal. Read both panes and the artifact: the supervisor must identify the mismatch, send a corrective SteerWorker instruction rather than approve, observe the resumed worker, and only approve after the corrected result satisfies the discriminator. Also confirm productive active work is left alone and an authorized plan edit is not mechanically rejected. Do not count deterministic test output or delivery receipts as autonomous outcome success. Independent reviewer gate remains parent-owned.
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
|
||||
Test Files 19 passed (19)
|
||||
Tests 108 passed (108)
|
||||
Start at 10:48:47
|
||||
Duration 3.88s (transform 4.98s, setup 0ms, import 9.43s, tests 8.78s, environment 2ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 36 files in 37ms. No fixes applied.
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 build
|
||||
> tsc
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Read-only replay of recorded research branches. Run from the pi-goals root after npm run build.
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { resolve } from "node:path";
|
||||
import ts from "typescript";
|
||||
|
||||
const output = "slop/reviews/vcc-view";
|
||||
const root = "/home/code/.pi/agent/sessions/--workspace-2026-mfv-manifold-steer--/";
|
||||
const workerPath = `${root}2026-09-08T10-41-19-145Z_01a0809b-a528-7724-a514-59f3c61116a6.jsonl`;
|
||||
const supervisorPath = `${root}2026-09-08T22-43-35-654Z_01a08330-e866-7004-9b7f-5efdceb2488e.jsonl`;
|
||||
const load = path => readFileSync(path, "utf8").trim().split("\n").map(line => JSON.parse(line));
|
||||
const entries = load(workerPath);
|
||||
const byId = new Map(entries.map(entry => [entry.id, entry]));
|
||||
const records = load(supervisorPath);
|
||||
const oldSource = execFileSync("git", ["show", "8953dce:src/worker-view.ts"], { encoding: "utf8" });
|
||||
const oldCode = ts.transpileModule(oldSource, { compilerOptions: { module: ts.ModuleKind.ES2022 } }).outputText;
|
||||
const { workerView: oldView } = await import(`data:text/javascript;base64,${Buffer.from(oldCode).toString("base64")}`);
|
||||
const piRequire = createRequire(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
||||
const { createJiti } = piRequire("jiti");
|
||||
const jiti = createJiti(import.meta.url, { moduleCache: false, fsCache: false });
|
||||
const { workerView: newView } = await jiti.import(resolve("src/worker-view.ts"));
|
||||
const { workerView: builtView } = await jiti.import(resolve("dist/worker-view.js"));
|
||||
const results = [];
|
||||
for (const [name, through] of [["pilot-preparation", "3cb9b26f"], ["flow-implementation", "69943231"], ["settled-checkpoint", "e266d41e"]]) {
|
||||
const record = records.find(entry => entry.customType === "pi-goals-intercom" && entry.data.message.kind === "view" && entry.data.message.through === through);
|
||||
assert(record, `recorded view ${through}`);
|
||||
const message = record.data.message;
|
||||
const branch = [];
|
||||
for (let entry = byId.get(through); entry; entry = byId.get(entry.parentId)) branch.unshift(entry);
|
||||
assert(branch.length, "nonempty live branch");
|
||||
const ack = branch.filter(entry => entry.customType === "pi-goals-intercom" && entry.data.direction === "ack" && entry.data.message.through).at(-1);
|
||||
const context = {
|
||||
sourceSession: workerPath,
|
||||
model: message.text.match(/^worker model: (.*)$/m)[1],
|
||||
latestDirection: message.text.match(/latest human direction:\n([\s\S]*?)\ntool calls with no result:/)[1],
|
||||
background: message.text.match(/^tracked background work: (.*)$/m)[1],
|
||||
since: ack?.data.message.through,
|
||||
};
|
||||
const args = [branch, message.reason, message.text.startsWith("The worker stopped."), context];
|
||||
const before = oldView(...args);
|
||||
const after = newView(...args);
|
||||
assert.equal(builtView(...args), after, "built/source real compiler parity");
|
||||
const envelope = { binding: message.binding, role: "worker", kind: "view", id: message.id, text: after, reason: message.reason, through, backgroundQuiet: message.backgroundQuiet };
|
||||
assert(Buffer.byteLength(JSON.stringify(envelope)) < 16_000, "serialized transport bound");
|
||||
// Normalize only the saved files' trailing blank lines; byte metrics use the exact rendered strings.
|
||||
writeFileSync(`${output}/${name}-old.md`, before.trimEnd() + "\n");
|
||||
writeFileSync(`${output}/${name}-vcc.md`, after.trimEnd() + "\n");
|
||||
results.push({ name, timestamp: record.timestamp, through, since: context.since, branchEntries: branch.length, branchSha256: createHash("sha256").update(JSON.stringify(branch)).digest("hex"), oldBytes: Buffer.byteLength(before), vccBytes: Buffer.byteLength(after), oldSerializedTextBytes: Buffer.byteLength(JSON.stringify(before)), vccSerializedTextBytes: Buffer.byteLength(JSON.stringify(after)), envelopeBytes: Buffer.byteLength(JSON.stringify(envelope)) });
|
||||
}
|
||||
const manifest = { baseline: "8953dce", workerPath, supervisorPath, compiler: "@sting8k/pi-vcc@0.5.0", results };
|
||||
writeFileSync(`${output}/comparison.json`, JSON.stringify(manifest, null, 2) + "\n");
|
||||
console.log(JSON.stringify(manifest, null, 2));
|
||||
console.log("PASS: three identical historical branch/ack windows, serialized bounds, source and built compiler execution agree.");
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"baseline": "8953dce",
|
||||
"workerPath": "/home/code/.pi/agent/sessions/--workspace-2026-mfv-manifold-steer--/2026-09-08T10-41-19-145Z_01a0809b-a528-7724-a514-59f3c61116a6.jsonl",
|
||||
"supervisorPath": "/home/code/.pi/agent/sessions/--workspace-2026-mfv-manifold-steer--/2026-09-08T22-43-35-654Z_01a08330-e866-7004-9b7f-5efdceb2488e.jsonl",
|
||||
"compiler": "@sting8k/pi-vcc@0.5.0",
|
||||
"results": [
|
||||
{
|
||||
"name": "pilot-preparation",
|
||||
"timestamp": "2026-09-08T22:59:31.347Z",
|
||||
"through": "3cb9b26f",
|
||||
"branchEntries": 597,
|
||||
"branchSha256": "f0da668db6266780aa3ba803e5260d4009732c74ab3276f4db55910b8d74af5a",
|
||||
"oldBytes": 4961,
|
||||
"vccBytes": 5356,
|
||||
"oldSerializedTextBytes": 5324,
|
||||
"vccSerializedTextBytes": 5462,
|
||||
"envelopeBytes": 5655
|
||||
},
|
||||
{
|
||||
"name": "flow-implementation",
|
||||
"timestamp": "2026-09-08T23:17:08.542Z",
|
||||
"through": "69943231",
|
||||
"since": "54648ab4",
|
||||
"branchEntries": 715,
|
||||
"branchSha256": "2293e2e7158c051c3571cd50befb14900cea39314640421a5735633775af427b",
|
||||
"oldBytes": 4897,
|
||||
"vccBytes": 2396,
|
||||
"oldSerializedTextBytes": 5109,
|
||||
"vccSerializedTextBytes": 2452,
|
||||
"envelopeBytes": 2646
|
||||
},
|
||||
{
|
||||
"name": "settled-checkpoint",
|
||||
"timestamp": "2026-09-09T00:11:07.924Z",
|
||||
"through": "e266d41e",
|
||||
"since": "18c6af89",
|
||||
"branchEntries": 981,
|
||||
"branchSha256": "3460caf8d1a093309ad2639decbc181151f27871c42ffa62ca98803aae726ce7",
|
||||
"oldBytes": 1676,
|
||||
"vccBytes": 1840,
|
||||
"oldSerializedTextBytes": 1721,
|
||||
"vccSerializedTextBytes": 1882,
|
||||
"envelopeBytes": 2078
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
The worker is still working.
|
||||
|
||||
review trigger: turns
|
||||
source session: /home/code/.pi/agent/sessions/--workspace-2026-mfv-manifold-steer--/2026-09-08T10-41-19-145Z_01a0809b-a528-7724-a514-59f3c61116a6.jsonl
|
||||
worker model: openai-codex/gpt-6-astra
|
||||
latest human direction:
|
||||
but also try the flow healing one as a next goal on the list
|
||||
tool calls with no result: none
|
||||
tracked background work: processes: 1 (e55-fine-job802-follower); subagents: 0; unregistered detached work is not tracked
|
||||
|
||||
new worker transcript since the last acknowledged view:
|
||||
[truncated; inspect source session]
|
||||
: answer_reached={} reason={} E={:.3f}", setting.name, task.name, r["scorable"], r["reason"], r["E"])
|
||||
if setting.method == "base":
|
||||
if r["scorable"]:
|
||||
base_scorable.add(task.name)
|
||||
if task.name in {"s_add", "l_moral_lie"}:
|
||||
ref_ids = ids[:, :n_prompt + cfg.kl_tokens]
|
||||
base_logp = model(ref_ids, use_cache=False).logits[:, n_prompt - 1:-1].float().log_softmax(-1)
|
||||
references.append((ref_ids, n_prompt - 1, base_logp))
|
||||
report = evaluate_with_vector(model, tok, vignettes=vignettes, max_think_tokens=cfg.think_tokens,
|
||||
batch_size=2, log_demo=False, verbose=0)
|
||||
kl = shared_prefix_kl(model, references)
|
||||
if setting.method == "logit_diff":
|
||||
dump(outdir / f"{setting.name}_adapter.json", stats)
|
||||
dump(outdir / f"{setting.name}_behavior.json", report["per_row"])
|
||||
if setting.method == "base":
|
||||
base_report = report
|
||||
delta = dclr_per_foundation(base_report, report)
|
||||
care, auth = delta["Care"]["mean"], delta["Authority"]["mean"]
|
||||
other = [delta[f]["mean"] for f in FOUNDATION_ORDER if f not in {"Care", "Authority"}]
|
||||
off = sum(abs(x) for x in other) / len(other)
|
||||
scorable = [r for r in generated if r["scorable"]]
|
||||
subset = [r for r in generated if r["task"] in base_scorable]
|
||||
direction = random_v if setting.random_direction else v0
|
||||
along = setting.c * float(direction @ v0)
|
||||
point = dict(name=setting.name, method=setting.method, internal_c=setting.c,
|
||||
logit_alpha=setting.alpha if setting.method == "logit_diff" else 1.0,
|
||||
frac_scorable=len(scorable) / len(tasks), n_base_scorable=len(subset),
|
||||
frac_on_base_scorable=sum(r["scorable"] for r in subset) / len(subset) if subset else float("nan"),
|
||||
mean_nonforced_E=sum(r["E"] for r in generated) / len(tasks),
|
||||
care=care, auth=auth, dlog_on_target=-auth, dlog_off_target=off,
|
||||
net_behavior_nats=care - auth, score=float("nan"),
|
||||
pmass_coherence=report["mean_pmass_allowed"], base_pmass=base_report["mean_pmass_allowed"],
|
||||
pmass_floor=0.99 * base_report["mean_pmass_allowed"],
|
||||
passes_pmass=report["mean_pmass_allowed"] >= 0.99 * base_report["mean_pmass_allowed"],
|
||||
steering_strength_v0=along, total_strength=setting.c,
|
||||
off_axis_strength=setting.c * float((direction - (direction @ v0) * v0).norm()),
|
||||
shared_prefix_kl_nats=kl, behavior_pairs=delta["Authority"]["n"],
|
||||
behavior_pairs_total=delta["Authority"]["n_total"], elapsed_s=time.monotonic() - stage_start)
|
||||
points.append(point)
|
||||
pl.DataFrame([{k: v for k, v in r.items() if k not in {"completion", "prefix", "generated_ids"}} for r in records]).write_csv(outdir / "tasks.csv")
|
||||
pl.DataFrame([{"name": r["name"], "prompt": r["task"], "task": r["task"],
|
||||
"scenario": choice_metadata[r["task"]]["scenario"] if r["task"] in choice_metadata else None,
|
||||
"rating_1_to_5": None, "coherent": None, "passes_demo_gate": None,
|
||||
"premise_preserved": None, "post_answer_repetition": None, "evidence_quote": None,
|
||||
"care_choice_verified": None, "failure_reason": "pending_manual"} for r in records]).write_csv(outdir / "demo_audit.tsv", separator="\t")
|
||||
table = write_report(outdir, points, records, cfg)
|
||||
logger.info("{}: {:.1f}s, ΔCare={:+.3f}, ΔAuth={:+.3f}, KL={:.4f}", setting.name, point["elapsed_s"], care, auth, kl)
|
||||
assert {(r["name"], r["task"]) for r in records} == {(s.name, t.name) for s in settings for t in tasks}
|
||||
assert len(records) == len(settings) * len(tasks), "duplicate or missing generation"
|
||||
logger.info("SHOULD every treatment/prompt cell retained: {} records PASS", len(records))
|
||||
logger.info("RESULT_DEMO: NO_RESULT (pending manual audit)\n{}\nreport={}\nelapsed_s={:.1f}", table, outdir / "report.md", time.monotonic() - start)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(tyro.cli(Cfg))
|
||||
@@ -0,0 +1,39 @@
|
||||
The worker is still working.
|
||||
|
||||
review trigger: turns
|
||||
source session: /home/code/.pi/agent/sessions/--workspace-2026-mfv-manifold-steer--/2026-09-08T10-41-19-145Z_01a0809b-a528-7724-a514-59f3c61116a6.jsonl
|
||||
worker model: openai-codex/gpt-6-astra
|
||||
latest human direction:
|
||||
but also try the flow healing one as a next goal on the list
|
||||
tool calls with no result: none
|
||||
tracked background work: processes: 1 (e55-fine-job802-follower); subagents: 0; unregistered detached work is not tracked
|
||||
|
||||
new worker overview since the last acknowledged view (VCC algorithmic compression; local # refs index new messages; tool-result bodies omitted; inspect source for evidence):
|
||||
[Files And Changes]
|
||||
- Modified: experiments/e56_flow_repair/flow.py, experiments/e56_flow_repair/test_flow.py,
|
||||
slop/reviews/e56_flow_matching_source.md, slop/reviews/e56_flow_discussion_brief.md,
|
||||
experiments/e56_flow_repair/intervention.py, src/manifold_steer/autoencoder.py
|
||||
- Read: slop/reviews/2026-09-09_glm-5.3-flash_e56_flow_scientist.md,
|
||||
/workspace/2026/lite/steering-lite/src/steering_lite/variants/mean_diff.py,
|
||||
slop/audits/steering_tradeoff/flow_synthetic.log, slop/reviews/2026-09-09_deepseek-v4-pro-0813_e56_flow_scientist.md,
|
||||
/home/code/.pi/agent/skills/arxiv/SKILL.md, /workspace/2026/lite/steering-lite/src/steering_lite/config.py,
|
||||
experiments/e55_logit_diff_amplification/logit_diff.py, /workspace/2026/lite/steering-lite/src/steering_lite/vector.py
|
||||
, slop/reviews/2026-09-09_deepseek-v4-pro-0813_e56_flow_discussion.md,
|
||||
slop/reviews/2026-09-09_glm-5.3-flash_e56_flow_discussion.md
|
||||
|
||||
[assistant]
|
||||
* (15 earlier tool-call entries omitted)
|
||||
* read "experiments/e55_logit_diff_amplification/logit_diff.py" (#30)
|
||||
* read "/workspace/2026/lite/steering-lite/src/steering_lite/vector.py" (#30)
|
||||
* write "experiments/e56_flow_repair/intervention.py" (#34)
|
||||
* edit "experiments/e56_flow_repair/test_flow.py" (#36)
|
||||
* read "slop/reviews/2026-09-09_deepseek-v4-pro-0813_e56_flow_discussion.md" (#38)
|
||||
* read "slop/reviews/2026-09-09_glm-5.3-flash_e56_flow_discussion.md" (#40)
|
||||
* edit "src/manifold_steer/autoencoder.py" (#42, #44, #46) x3
|
||||
(thinking) Thinking: **Preparing evaluation configuration**
|
||||
|
||||
**Refactoring evaluation context** (#48)
|
||||
(thinking) Thinking: **Preparing token geometry logging**
|
||||
|
||||
**Implementing token geometry logging** (#48)
|
||||
* bash "sed -n '255,370p' experiments/e55_logit_diff_amplification/run.py" (#48)
|
||||
@@ -0,0 +1,20 @@
|
||||
# VCC functional acceptance
|
||||
|
||||
Pi/OpenAI observed the isolated interactive worker in Herdr pane `w8:p54`, task repo `/tmp/pi-goals-vcc-functional-task`, using source commit `039f4a4`. Parent submitted the trivial exact-byte goal and selected Ready. The pair then completed without parent task execution or steering.
|
||||
|
||||
Observed rendered output:
|
||||
|
||||
> [supervisor] Supervisor approval is recorded. Call CompleteGoal for Deliver the committed hello file with byte proof and completion approval now. Do not change any files or run further work.
|
||||
|
||||
> CompleteGoal
|
||||
> Sign-off accepted. Goal ticked [x] in .pi/plan/01a083f1-296e-7594-8623-3e009c90a85d-v1.md.
|
||||
|
||||
Parent independently read artifact bytes with `od -An -tx1 hello.txt`:
|
||||
|
||||
```
|
||||
48 65 6c 6c 6f 20 66 72 6f 6d 20 56 43 43 2e 0a
|
||||
```
|
||||
|
||||
These encode `Hello from VCC.` plus one newline. Task commit: `a701105 Add verified VCC hello file`.
|
||||
|
||||
This establishes successful real planning/Ready/supervised completion with the VCC dependency loaded. It does not establish improved scientific judgment or cost savings. Replay comparisons are in `review.md`. The subsequent goal-repeat/reminder change was not loaded in this pair; its cadence, changed-checkbox and compaction behavior were checked separately in automated tests. Rejecting a valid artifact that fails a scientific goal remains a behavioral acceptance gap.
|
||||
@@ -0,0 +1,144 @@
|
||||
The worker is still working.
|
||||
|
||||
review trigger: turns
|
||||
source session: /home/code/.pi/agent/sessions/--workspace-2026-mfv-manifold-steer--/2026-09-08T10-41-19-145Z_01a0809b-a528-7724-a514-59f3c61116a6.jsonl
|
||||
worker model: openai-codex/gpt-6-astra
|
||||
latest human direction:
|
||||
but also try the flow healing one as a next goal on the list
|
||||
tool calls with no result: none
|
||||
tracked background work: processes: 0; subagents: 0; unregistered detached work is not tracked
|
||||
|
||||
compaction summary (worker account, not independent evidence):
|
||||
[OpenAI native compaction checkpoint]
|
||||
|
||||
new worker transcript (initial or reset view):
|
||||
[truncated; inspect source session]
|
||||
"enqueued_at": "2026-09-08T13:38:57.301057325+08:00"
|
||||
}
|
||||
},
|
||||
"priority": 0,
|
||||
"label": "why: validate common L22-24 C2 on dog legs; resolve: correct clean controls and consistent transfer on plain questions",
|
||||
"path": "/workspace/2026/suppressed-activations"
|
||||
},
|
||||
{
|
||||
"id": 769,
|
||||
"status": {
|
||||
"Queued": {
|
||||
"enqueued_at": "2026-09-08T13:38:57.518018760+08:00"
|
||||
}
|
||||
},
|
||||
"priority": 0,
|
||||
"label": "why: validate common L22-24 C2 on dog name; resolve: correct clean controls and consistent transfer on plain questions",
|
||||
"path": "/workspace/2026/suppressed-activations"
|
||||
},
|
||||
{
|
||||
"id": 770,
|
||||
"status": {
|
||||
"Queued": {
|
||||
"enqueued_at": "2026-09-08T13:38:57.740040876+08:00"
|
||||
}
|
||||
},
|
||||
"priority": 0,
|
||||
"label": "why: validate common L22-24 C2 on dog property; resolve: correct clean controls and consistent transfer on plain questions",
|
||||
"path": "/workspace/2026/suppressed-activations"
|
||||
},
|
||||
{
|
||||
"id": 771,
|
||||
"status": {
|
||||
"Queued": {
|
||||
"enqueued_at": "2026-09-08T13:38:57.987386109+08:00"
|
||||
}
|
||||
},
|
||||
"priority": 0,
|
||||
"label": "why: validate common L22-24 C2 on ant legs; resolve: correct clean controls and consistent transfer on plain questions",
|
||||
"path": "/workspace/2026/suppressed-activations"
|
||||
},
|
||||
{
|
||||
"id": 772,
|
||||
"status": {
|
||||
"Queued": {
|
||||
"enqueued_at": "2026-09-08T13:38:58.231236887+08:00"
|
||||
}
|
||||
},
|
||||
"priority": 0,
|
||||
"label": "why: validate common L22-24 C2 on ant name; resolve: correct clean controls and consistent transfer on plain questions",
|
||||
"path": "/workspace/2026/suppressed-activations"
|
||||
},
|
||||
{
|
||||
"id": 773,
|
||||
"status": {
|
||||
"Queued": {
|
||||
"enqueued_at": "2026-09-08T13:38:58.435527993+08:00"
|
||||
}
|
||||
},
|
||||
"priority": 0,
|
||||
"label": "why: validate common L22-24 C2 on ant property; resolve: correct clean controls and consistent transfer on plain questions",
|
||||
"path": "/workspace/2026/suppressed-activations"
|
||||
},
|
||||
{
|
||||
"id": 775,
|
||||
"status": {
|
||||
"Queued": {
|
||||
"enqueued_at": "2026-09-08T13:41:03.538541658+08:00"
|
||||
}
|
||||
},
|
||||
"priority": 0,
|
||||
"label": "why: selected dog band may just cause generic changes; resolve: eight random spans matched per-token edit magnitude",
|
||||
"path": "/workspace/2026/suppressed-activations"
|
||||
},
|
||||
{
|
||||
"id": 776,
|
||||
"status": {
|
||||
"Queued": {
|
||||
"enqueued_at": "2026-09-08T13:41:04.114346905+08:00"
|
||||
}
|
||||
},
|
||||
"priority": 0,
|
||||
"label": "why: selected ant band may just cause generic changes; resolve: eight random spans matched per-token edit magnitude",
|
||||
"path": "/workspace/2026/suppressed-activations"
|
||||
},
|
||||
{
|
||||
"id": 778,
|
||||
"status": {
|
||||
"Queued": {
|
||||
"enqueued_at": "2026-09-08T15:16:24.724372591+08:00"
|
||||
}
|
||||
},
|
||||
"priority": 0,
|
||||
"label": "why: dog clean controls may fail from assistant-prefilled questions; resolve: user-role and generation-boundary repair must give correct clean answers before assessing fixed L22-24 C2 transfer",
|
||||
"path": "/workspace/2026/suppressed-activations"
|
||||
},
|
||||
{
|
||||
"id": 779,
|
||||
"status": {
|
||||
"Queued": {
|
||||
"enqueued_at": "2026-09-08T15:16:24.923827832+08:00"
|
||||
}
|
||||
},
|
||||
"priority": 0,
|
||||
"label": "why: ant clean controls may fail from assistant-prefilled questions; resolve: user-role and generation-boundary repair must give correct clean answers before assessing fixed L22-24 C2 transfer",
|
||||
"path": "/workspace/2026/suppressed-activations"
|
||||
},
|
||||
{
|
||||
"id": 801,
|
||||
"status": {
|
||||
"Running": {
|
||||
"enqueued_at": "2026-09-09T06:50:35.856041965+08:00",
|
||||
"start": "2026-09-09T06:50:48.080797309+08:00"
|
||||
}
|
||||
},
|
||||
"priority": 0,
|
||||
"label": "why: larger paired MLP reader/writer needs a fitting check; resolve: batch-eight memory and complete local readouts before held-out training; args --steps 4 --eval-every 2",
|
||||
"path": "/workspace/2026/LUCID3_wikit"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
tool: edit
|
||||
|
||||
Successfully replaced 3 block(s) in experiments/e55_logit_diff_amplification/run_fine_s43/run_card.md.
|
||||
|
||||
tool: bash
|
||||
|
||||
(no output)
|
||||
@@ -0,0 +1,82 @@
|
||||
The worker is still working.
|
||||
|
||||
review trigger: turns
|
||||
source session: /home/code/.pi/agent/sessions/--workspace-2026-mfv-manifold-steer--/2026-09-08T10-41-19-145Z_01a0809b-a528-7724-a514-59f3c61116a6.jsonl
|
||||
worker model: openai-codex/gpt-6-astra
|
||||
latest human direction:
|
||||
but also try the flow healing one as a next goal on the list
|
||||
tool calls with no result: none
|
||||
tracked background work: processes: 0; subagents: 0; unregistered detached work is not tracked
|
||||
|
||||
compaction summary (worker account, not independent evidence):
|
||||
[OpenAI native compaction checkpoint]
|
||||
|
||||
new worker overview (initial or reset view) (VCC algorithmic compression; local # refs index new messages; tool-result bodies omitted; inspect source for evidence):
|
||||
[Session Goal]
|
||||
- was your job killed?
|
||||
- [Scope change]
|
||||
- maybe read it your self ml-debug
|
||||
|
||||
[Files And Changes]
|
||||
- Modified: slop/audits/e55/job798_parent_read.md, /workspace/2026/mfv/manifold-steer/.pi/plan/01a0809b-a528-7724-a514-5
|
||||
9f3c61116a6-v1.md, experiments/e55_logit_diff_amplification/LAB.md, experiments/ACTIVE.md,
|
||||
experiments/e55_logit_diff_amplification/choice_tasks.py, experiments/e55_logit_diff_amplification/run.py,
|
||||
experiments/e55_logit_diff_amplification/choice_summary.py,
|
||||
experiments/e55_logit_diff_amplification/test_choice_summary.py, justfile,
|
||||
experiments/e55_logit_diff_amplification/run_fine_s43/run_card.md (+2 more)
|
||||
- Read: experiments/e55_logit_diff_amplification/LAB.md, /home/code/.pi/agent/skills/ml-debug/SKILL.md,
|
||||
experiments/e55_logit_diff_amplification/run_s43/run.log, experiments/e55_logit_diff_amplification/run_s43/run_card.md
|
||||
, /home/code/.pi/agent/skills/plan-format/SKILL.md, experiments/e55_logit_diff_amplification/run.py,
|
||||
/home/code/.pi/agent/skills/pueue/SKILL.md, /home/code/.pi/agent/skills/machine-map/SKILL.md,
|
||||
/home/code/.pi/agent/skills/ponytailplus/SKILL.md, /workspace/2026/lite/steering-lite/src/steering_lite/eval/tinymfv.p
|
||||
y (+11 more)
|
||||
|
||||
[Commits]
|
||||
- dff95a9: Record E55 independent demo audit and parent caveats
|
||||
|
||||
[User Preferences]
|
||||
- Style: Make it easy for a busy and forgetfull user to review. Use ASD-STE100 Simplified Technical English. Use active
|
||||
voice, one idea per sentence, common words,
|
||||
|
||||
[truncated; inspect source session]
|
||||
te the stale E55 LAB/ACTIVE claims ('Not run'/'queued798') from existing evidence.
|
||||
Then implement and run the smallest finer-dose comparison resolving matched achieved behavior versus coherence, with a
|
||||
reviewed run card and saved just smoke output before queueing on the existing default group. Specify behavior
|
||||
measurement and uncertainty before collecting new results: forced 32-token CLR and answer-span E alone cannot establish
|
||||
the tradeoff; retain full 512-token generations, premise/repetition audits and missing completions rather than selecting
|
||||
survivors. Compare only overlapping measured effects and keep confirmation prompts separate. Diagnose unexpected results
|
||||
with ml-debug. Goal2 is authorized next: inspect existing AE repair and prepare independent-reviewed corruption-to-clean
|
||||
flow pseudocode during safe CPU work if goal1 is queued; no sign-off dependency forbids that preparation. Deliver the
|
||||
single linked report with both plots, full demos, independent reviews and saved verification/job receipts. Continue
|
||||
autonomously within scope; no approval claim until real evidence is ready.
|
||||
|
||||
[assistant]
|
||||
* (40 earlier tool-call entries omitted)
|
||||
* edit "experiments/e55_logit_diff_amplification/run.py" (#161, #163) x2
|
||||
* process (#165)
|
||||
* bash "cp /home/code/.pi/agent/sessions/--workspace-2026-mfv-manifold-steer--/subagent-artifacts/outputs/7a33c86b-5383-
|
||||
4752-8ecb-d5912c2583bd/slop/reviews/tradeoff_measurement.md slop/reviews/tradeoff_prequeue.md; cp
|
||||
/home/code/.pi/agent/sessio..." (#167)
|
||||
The review found an important measurement issue: moralmaps deliberately prevents `</think>` and EOS during its probe.
|
||||
Its `emitted_close=0` therefore was not evidence of natural nontermination.
|
||||
|
||||
The follow-up keeps that probe diagnostic only. It adds freely generated care-versus-authority choices, finer doses,
|
||||
paired option orders, and explicit uncertainty for missing answers. I’m fixing the review’s parser and analysis issues
|
||||
before queueing.
|
||||
|
||||
Flow preparation also confirms a distinct test: the previous online AE used one learned map; flow repair would integrate
|
||||
a time-conditioned recovery field. (#169)
|
||||
* edit ".pi/plan/01a0809b-a528-7724-a514-59f3c61116a6-v1.md" (#169)
|
||||
* bash "grep -E 'Ran |OK|PASS|elapsed_s=' slop/audits/steering_tradeoff/reviewed_smoke.log; cat
|
||||
slop/audits/steering_tradeoff/saved_vector_check.log; git diff --check; pueue add --help | head -65; pueue status
|
||||
--json | jq '[.tasks[] | select(.gr..." (#171)
|
||||
* bash "pueue status --json | jq '{groups:.groups,tasks:[.tasks[] | select(.group=="default" and (.status.Done == null))
|
||||
| {id,status,priority,label,path}]}'" (#173)
|
||||
(thinking) Thinking: **Refining run-card wording** (#175)
|
||||
* edit "experiments/e55_logit_diff_amplification/run_fine_s43/run_card.md" (#175)
|
||||
(thinking) Thinking: **Checking saved-vector comment**
|
||||
|
||||
**Extracting old-run report blocks** (#177)
|
||||
* bash "UV_NO_SYNC=1 uv run python -c 'from pathlib import Path;
|
||||
p=Path("experiments/e55_logit_diff_amplification/LAB.md"); report=Path("experiments/e55_logit_diff_amplification/run_s
|
||||
43/report.md").read_text(); blocks=report.split("\n## ")[1:]; ..." (#177)
|
||||
@@ -0,0 +1,7 @@
|
||||
# VCC worker-view change / UAT plan
|
||||
|
||||
Goal: improve supervisor judgment per context token by replacing raw transcript-tail extraction with the existing deterministic VCC compiler. Keep acknowledged-entry slicing, plan changes, tracked background work, and missing tool results. Preserve two recent thinking tails next to their actions. No transport or supervision lifecycle redesign.
|
||||
|
||||
Acceptance: pin and inspect the compiler dependency; test new-turn slicing, compaction/rewind resets, thinking/action ordering, tool arguments, extracted files/context, output omission notices and serialized byte bounds. Run full tests, typecheck, lint and build. Render old and new views from identical recorded maniworker windows, saving reproducible comparison and honest information-loss notes. The parent must perform real isolated Herdr acceptance after this handoff; no research pane interaction here.
|
||||
|
||||
Baseline: HEAD 8953dce, src/worker-view.ts 69 lines. Pre-existing dirty native worker/supervisor event logs and untracked docs/human_journal.md are outside scope and remain untouched/unstaged. No dependency lifecycle scripts will run.
|
||||
@@ -0,0 +1,62 @@
|
||||
# VCC worker overview: implementation and replay review
|
||||
|
||||
## Decision and scope
|
||||
|
||||
Use the deterministic compiler from `@sting8k/pi-vcc@0.5.0` inside the existing worker view. No model call, copied compiler, transport/lifecycle change, new monitoring framework, or change to approval rules. Keep acknowledged-entry boundaries, compaction reset, missing-result matching, plan diff/status claims and existing managed process/subagent tracking. Add context percentage from `ctx.getContextUsage().percent`; unknown remains omitted.
|
||||
|
||||
`src/worker-view.ts`: **69 -> 100 lines (+31)**. Compiler declaration: **10 lines**. Caller: **+1 line**. Not fewer lines than the previous raw-tail implementation, but much smaller than transplanting the 302-line pi-supervise view plus its lifecycle. VCC itself remains an external dependency, not free code complexity.
|
||||
|
||||
## Dependency provenance and security
|
||||
|
||||
- Inspected `../pi-supervise/src/view.ts`, package manifest/lock, installed compiler and normalization/brief/extractor path. Custom last-two-thinking support is in pi-supervise's adapter, not a patched installed VCC package.
|
||||
- Downloaded exact registry tarball using `npm pack @sting8k/pi-vcc@0.5.0 --ignore-scripts --pack-destination /tmp/pi-goals-vcc-package --json`.
|
||||
- `diff -qr /tmp/pi-goals-vcc-package/package ../pi-supervise/node_modules/@sting8k/pi-vcc` produced no differences. This includes all installed package files, not just version strings.
|
||||
- Pinned exact `0.5.0` in dependencies and lockfile. Registry: `https://registry.npmjs.org/@sting8k/pi-vcc/-/pi-vcc-0.5.0.tgz`; SHA512 integrity: `KJbOVUFbyghn6h+RD9bDXFNWkKNqpxaCpPQWceOuxMPe9ySpbEfaYnqO9CZUiCP3AFmQ5Ghnsg2B8pdKgY+0Hg==`.
|
||||
- Tarball SHA1: `090e5c7cacec00b1083bf423bc08aa2d3eb9cb3a`; size 16,206,703 bytes compressed, 16,712,402 unpacked. It ships more than just the compiler. Added one package; no new transitive packages beyond already installed peers.
|
||||
- Read cybersec-situational-awareness skill before fetching/installing. Used `npm install --save-exact @sting8k/pi-vcc@0.5.0 --ignore-scripts --no-audit --no-fund`. No lifecycle scripts run. Mise is installed but has no configured/installed Node version; used current project Node v22.23.2/npm rather than install another toolchain. This was not a sandboxed install.
|
||||
- Runtime imports only compiler source, not the VCC extension entrypoint. The compiler pipeline is algorithmic: no network, shell, or model call.
|
||||
- Local upstream clone is newer (09c4a74, 0.6.0 work); deliberately did not switch versions. This reproduces the installed reference dependency.
|
||||
|
||||
### Source-only package type boundary
|
||||
|
||||
Direct tsc traversal exposed three upstream 0.5.0 errors: `brief.ts:61,77` passes Intl SegmentData with optional `isWordLike` to a required-boolean shape, and `normalize.ts:21` compares Pi Message role with `bashExecution`, outside that union. Supervisor approved a narrow declaration for the exact compile input/output, with tsconfig path mapping just as this repo handles pi-intercom. No runtime fallback or node_modules patch. Source and built JS imports execute the real compiler in replay; tests likewise execute the real package. This declaration must be reviewed if the pinned compiler version changes.
|
||||
|
||||
## Replay method and result
|
||||
|
||||
`node slop/reviews/vcc-view/compare.mjs` loads the actual maniworker branch terminating at each recorded manisuper view's `through` ID, follows parent links (not dead branches), and recovers its last acknowledged view boundary. Both renderers receive identical entries, context, trigger and idle state. Old renderer comes from HEAD 8953dce; new source and built renderer outputs must agree. Paths, timestamps, IDs and branch SHA256 hashes are in `comparison.json`. No sessions were messaged, reloaded or edited. The source sessions remain local prerequisites to reproduce the replay; saved rendered views are reviewable without them.
|
||||
|
||||
| Recorded window | Old UTF-8 bytes | VCC UTF-8 bytes | Observation |
|
||||
|---|---:|---:|---|
|
||||
| Pilot preparation, 22:59:31 | 4,961 | 5,356 | Slightly larger, much better task signal |
|
||||
| Flow implementation, 23:17:08 | 4,897 | 2,396 | About half the bytes; restores action context |
|
||||
| Settled checkpoint, 00:11:07 | 1,676 | 1,840 | Slightly larger; direct test output lost |
|
||||
|
||||
These are bytes, **not measured token counts or realized supervisor cost savings**. Each complete serialized transport envelope is below 16KB (largest 5,655 bytes). The raw transcript and VCC brief keep the same total 5.5KB serialized content allowance; VCC splits extracted headers (1.5KB) from newest brief (4KB) when headers exist, so one cannot evict the other.
|
||||
|
||||
### What improves
|
||||
|
||||
- Pilot old view is dominated by unrelated queue-job JSON. VCC retains the decision that moralmaps deliberately suppresses think closure/EOS, so `emitted_close=0` is not evidence of natural nontermination; the narrower choice pilot/uncertainty approach; edited files/commit; recent thinking about run-card wording and saved-vector checks; actual shell commands rather than `tool: bash`.
|
||||
- Flow old view is almost entirely a raw Python source tail with no useful explanation of the current action. VCC gives changed file paths, reviewed source paths, edit/read/bash arguments and the two recent thinking tails about evaluation configuration and token geometry. Existing E55 follower tracking remains unchanged.
|
||||
- Checkpoint retains the claim `Test passed. Committed as c41e0f2`, extracted commit title, changed files, verification-log path, and thinking about byte identity/rubric migration. The old view had no tool arguments or thinking.
|
||||
|
||||
### Honest limits / evidence lost
|
||||
|
||||
- The checkpoint's actual `Ran 1 test ... OK` tool result is absent in VCC. Supervisor must read the retained log path before treating the worker's pass claim as evidence. The view explicitly says tool-result bodies are omitted and provides the source session path.
|
||||
- VCC file lists/commits are extracted activity, not independent proof that a write or commit succeeded. Some file classifications are heuristic (`write` is reported as Modified). Full source/artifact inspection still matters.
|
||||
- VCC's initial Session Goal extraction includes weak historical phrases such as `was your job killed?`, not the actual agreed research discriminator. It is an overview, not a replacement for the plan or latest human direction. Existing plan review remains separate.
|
||||
- Generic `process` calls appear by name only in VCC's installed compiler; their detailed command/state is not reconstructed here. Existing live background summary still names tracked processes/subagents. Unregistered detached work remains untracked, as before.
|
||||
- Older brief/tool entries can still be cut. Long paths/commands can wrap or truncate. Local `#` references index fresh messages, not session entry IDs; the label now explains this. The compiler's unavailable `vcc_recall` instruction is removed.
|
||||
- Two recent thinking tails are limited to 400 characters before the compiler's own shortening; hidden/redacted thinking cannot be recovered. Large views may still cut earlier retained thinking.
|
||||
- First/reset views can repeat older instructions. No new deduplication or lifecycle machinery was introduced in this scoped change.
|
||||
|
||||
## Validation
|
||||
|
||||
Final output: `validation.txt`. `npm test`: **94/94**, 18 files including RPC. Typecheck, lint, build and diff check pass. Six focused worker-view tests added to the previous three: thinking/action order and immutability; extracted paths/blockers/arguments with omitted output notices and metadata; partial pending calls across acknowledgements; rewind/compaction reset and unknown context; oversized headers/brief preserving newest activity; omitted-result versus empty-update distinction and commit extraction. Existing flow fixture supplies the new standard context-usage API.
|
||||
|
||||
During implementation, full tests caught a partial `edit` call with no arguments: the adapter now supplies an empty argument object for that incomplete call while preserving missing-result status. Two added assertions initially assumed VCC classified `write` as Created and could extract a commit from a result without its call; corrected tests to the inspected compiler semantics, not patched dependency behavior. Upstream type errors are isolated as described above. All final checks pass.
|
||||
|
||||
## Remaining acceptance
|
||||
|
||||
The replay supports a **better overview**, especially when raw output crowds out decisions, but not a claim that this produces better outcomes or lower total token cost. Parent-owned isolated real Herdr acceptance and independent reviewer gate remain required. Suggested UAT: trivial artifact and saved verification log; inspect the supervisor's new overview and actual artifact read; exercise a manual plan tick/edit and full ApproveGoal -> CompleteGoal sequence. Do not use the user's research panes.
|
||||
|
||||
No Herdr panes, GPU jobs, supervisor sessions, user research files, old native evidence logs or human journal were modified by this task. No push performed.
|
||||
@@ -0,0 +1,44 @@
|
||||
The worker stopped.
|
||||
|
||||
review trigger: settled
|
||||
source session: /home/code/.pi/agent/sessions/--workspace-2026-mfv-manifold-steer--/2026-09-08T10-41-19-145Z_01a0809b-a528-7724-a514-59f3c61116a6.jsonl
|
||||
worker model: openai-codex/gpt-6-astra
|
||||
latest human direction:
|
||||
but also try the flow healing one as a next goal on the list
|
||||
tool calls with no result: none
|
||||
tracked background work: processes: 2 (e55-fine-job802-follower, e56-recovery-job805-follower); subagents: 0; unregistered detached work is not tracked
|
||||
|
||||
new worker transcript since the last acknowledged view:
|
||||
tool: edit
|
||||
|
||||
Successfully replaced 1 block(s) in experiments/e55_logit_diff_amplification/audit_choices.py.
|
||||
|
||||
tool: edit
|
||||
|
||||
Successfully replaced 1 block(s) in experiments/e55_logit_diff_amplification/test_audit_choices.py.
|
||||
|
||||
tool: process
|
||||
|
||||
Started process audit-rubric-template-test (proc_43e4) with pid 3380015.
|
||||
|
||||
Moved the unchanged rubric to [audit_instructions.md](experiments/e55_logit_diff_amplification/audit_instructions.md). The file-roundtrip test is running; queued code is untouched.
|
||||
|
||||
tool: read
|
||||
|
||||
test_prepare_and_summary_file_paths (experiments.e55_logit_diff_amplification.test_audit_choices.AuditTests.test_prepare_and_summary_file_paths) ... ok
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Ran 1 test in 0.018s
|
||||
|
||||
OK
|
||||
|
||||
|
||||
tool: bash
|
||||
|
||||
[main c41e0f2] Move generated audit rubric out of Python source
|
||||
4 files changed, 19 insertions(+), 12 deletions(-)
|
||||
create mode 100644 experiments/e55_logit_diff_amplification/audit_instructions.md
|
||||
create mode 100644 slop/audits/steering_tradeoff/audit_rubric_template_test.log
|
||||
|
||||
|
||||
Test passed. Committed as `c41e0f2`; rubric wording is unchanged.
|
||||
@@ -0,0 +1,33 @@
|
||||
The worker stopped.
|
||||
|
||||
review trigger: settled
|
||||
source session: /home/code/.pi/agent/sessions/--workspace-2026-mfv-manifold-steer--/2026-09-08T10-41-19-145Z_01a0809b-a528-7724-a514-59f3c61116a6.jsonl
|
||||
worker model: openai-codex/gpt-6-astra
|
||||
latest human direction:
|
||||
but also try the flow healing one as a next goal on the list
|
||||
tool calls with no result: none
|
||||
tracked background work: processes: 2 (e55-fine-job802-follower, e56-recovery-job805-follower); subagents: 0; unregistered detached work is not tracked
|
||||
|
||||
new worker overview since the last acknowledged view (VCC algorithmic compression; local # refs index new messages; tool-result bodies omitted; inspect source for evidence):
|
||||
[Files And Changes]
|
||||
- Modified: experiments/e55_logit_diff_amplification/audit_choices.py,
|
||||
experiments/e55_logit_diff_amplification/test_audit_choices.py
|
||||
- Read: slop/audits/steering_tradeoff/audit_rubric_template_test.log
|
||||
|
||||
[Commits]
|
||||
- c41e0f2: Move generated audit rubric out of Python source
|
||||
|
||||
[assistant]
|
||||
* edit "experiments/e55_logit_diff_amplification/audit_choices.py" (#0)
|
||||
* edit "experiments/e55_logit_diff_amplification/test_audit_choices.py" (#2)
|
||||
* process (#4)
|
||||
(thinking) Thinking: **Checking baseline byte identity**
|
||||
|
||||
**Testing the rubric migration** (#6)
|
||||
Moved the unchanged rubric to [audit_instructions.md](experiments/e55_logit_diff_amplification/audit_instructions.md).
|
||||
The file-roundtrip test is running; queued code is untouched. (#6)
|
||||
* read "slop/audits/steering_tradeoff/audit_rubric_template_test.log" (#7)
|
||||
* bash "git add experiments/e55_logit_diff_amplification/audit_choices.py
|
||||
experiments/e55_logit_diff_amplification/audit_instructions.md
|
||||
experiments/e55_logit_diff_amplification/test_audit_choices.py && git add -f slop/audits/steering_tradeoff/a..." (#9)
|
||||
Test passed. Committed as `c41e0f2`; rubric wording is unchanged. (#11)
|
||||
@@ -0,0 +1,73 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run
|
||||
|
||||
|
||||
RUN v4.1.9 /home/code/.pi/agent/git/github.com/wassname/pi-goals
|
||||
|
||||
|
||||
Test Files 18 passed (18)
|
||||
Tests 94 passed (94)
|
||||
Start at 10:10:30
|
||||
Duration 3.11s (transform 3.98s, setup 0ms, import 8.30s, tests 7.14s, environment 2ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 34 files in 45ms. No fixes applied.
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 build
|
||||
> tsc
|
||||
|
||||
{
|
||||
"baseline": "8953dce",
|
||||
"workerPath": "/home/code/.pi/agent/sessions/--workspace-2026-mfv-manifold-steer--/2026-09-08T10-41-19-145Z_01a0809b-a528-7724-a514-59f3c61116a6.jsonl",
|
||||
"supervisorPath": "/home/code/.pi/agent/sessions/--workspace-2026-mfv-manifold-steer--/2026-09-08T22-43-35-654Z_01a08330-e866-7004-9b7f-5efdceb2488e.jsonl",
|
||||
"compiler": "@sting8k/pi-vcc@0.5.0",
|
||||
"results": [
|
||||
{
|
||||
"name": "pilot-preparation",
|
||||
"timestamp": "2026-09-08T22:59:31.347Z",
|
||||
"through": "3cb9b26f",
|
||||
"branchEntries": 597,
|
||||
"branchSha256": "f0da668db6266780aa3ba803e5260d4009732c74ab3276f4db55910b8d74af5a",
|
||||
"oldBytes": 4961,
|
||||
"vccBytes": 5356,
|
||||
"oldSerializedTextBytes": 5324,
|
||||
"vccSerializedTextBytes": 5462,
|
||||
"envelopeBytes": 5655
|
||||
},
|
||||
{
|
||||
"name": "flow-implementation",
|
||||
"timestamp": "2026-09-08T23:17:08.542Z",
|
||||
"through": "69943231",
|
||||
"since": "54648ab4",
|
||||
"branchEntries": 715,
|
||||
"branchSha256": "2293e2e7158c051c3571cd50befb14900cea39314640421a5735633775af427b",
|
||||
"oldBytes": 4897,
|
||||
"vccBytes": 2396,
|
||||
"oldSerializedTextBytes": 5109,
|
||||
"vccSerializedTextBytes": 2452,
|
||||
"envelopeBytes": 2646
|
||||
},
|
||||
{
|
||||
"name": "settled-checkpoint",
|
||||
"timestamp": "2026-09-09T00:11:07.924Z",
|
||||
"through": "e266d41e",
|
||||
"since": "18c6af89",
|
||||
"branchEntries": 981,
|
||||
"branchSha256": "3460caf8d1a093309ad2639decbc181151f27871c42ffa62ca98803aae726ce7",
|
||||
"oldBytes": 1676,
|
||||
"vccBytes": 1840,
|
||||
"oldSerializedTextBytes": 1721,
|
||||
"vccSerializedTextBytes": 1882,
|
||||
"envelopeBytes": 2078
|
||||
}
|
||||
]
|
||||
}
|
||||
PASS: three identical historical branch/ack windows, serialized bounds, source and built compiler execution agree.
|
||||
+58
-13
@@ -1,9 +1,15 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, readSync, renameSync, rmSync, type Stats, statSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, relative, resolve } from "node:path";
|
||||
|
||||
const GOAL_LINE = /^\s*(?:\d+\.|[-*])\s*\[([ xX/-])\]\s*goal:\s*(.*)$/i;
|
||||
import { foldPlan, GOAL_LINE } from "./plan.js";
|
||||
|
||||
export interface WorktreeSnapshot {
|
||||
status: string;
|
||||
indexHash: string;
|
||||
files: Array<{ path: string; kind: "file" | "symlink" | "missing"; mode?: number; contentHash?: string }>;
|
||||
}
|
||||
|
||||
export interface ApprovalRecord {
|
||||
version: 3;
|
||||
@@ -15,7 +21,8 @@ export interface ApprovalRecord {
|
||||
repoRoot: string;
|
||||
head: string;
|
||||
tree: string;
|
||||
cleanWorktree: true;
|
||||
cleanWorktree: boolean;
|
||||
force?: { reason: string; worktree: WorktreeSnapshot };
|
||||
inspected: { plan: true; repository: true; evidence: true; verifyOutput: true };
|
||||
verifyOutputPath: string;
|
||||
supervisor: { sessionId: string; runId: string | null };
|
||||
@@ -26,22 +33,56 @@ function command(repoRoot: string, args: string[]): string {
|
||||
return execFileSync("git", args, { cwd: repoRoot, encoding: "utf8" }).trim();
|
||||
}
|
||||
|
||||
export function repositoryState(cwd: string): { repoRoot: string; head: string; tree: string; cleanWorktree: boolean } {
|
||||
// Capture bytes, not status flags: an edited file can change again while Git still reports M or ??.
|
||||
function worktreeSnapshot(repoRoot: string, status: string, pathspec: string[]): WorktreeSnapshot {
|
||||
const index = execFileSync("git", ["ls-files", "--stage", "-z", "--", ...pathspec], { cwd: repoRoot });
|
||||
const paths = status.split("\0").filter(Boolean).map(entry => entry.slice(3)).sort();
|
||||
const files = paths.map((path): WorktreeSnapshot["files"][number] => {
|
||||
const fullPath = join(repoRoot, path);
|
||||
let stat: Stats;
|
||||
try { stat = lstatSync(fullPath); }
|
||||
catch (error) {
|
||||
if (["ENOENT", "ENOTDIR"].includes((error as NodeJS.ErrnoException).code ?? "")) return { path, kind: "missing" };
|
||||
throw error;
|
||||
}
|
||||
const mode = stat.mode & 0o777;
|
||||
const hash = createHash("sha256");
|
||||
if (stat.isSymbolicLink()) return { path, kind: "symlink", mode, contentHash: hash.update(readlinkSync(fullPath, { encoding: "buffer" })).digest("hex") };
|
||||
if (!stat.isFile()) throw new Error(`Cannot fingerprint dirty path ${path}: only regular files and symlinks are supported.`);
|
||||
const fd = openSync(fullPath, "r");
|
||||
try {
|
||||
const buffer = Buffer.alloc(256 * 1024);
|
||||
for (;;) {
|
||||
const bytes = readSync(fd, buffer, 0, buffer.length, null);
|
||||
if (!bytes) break;
|
||||
hash.update(buffer.subarray(0, bytes));
|
||||
}
|
||||
} finally { closeSync(fd); }
|
||||
return { path, kind: "file", mode, contentHash: hash.digest("hex") };
|
||||
});
|
||||
return { status, indexHash: createHash("sha256").update(index).digest("hex"), files };
|
||||
}
|
||||
|
||||
export function repositoryState(cwd: string, captureWorktree = false): { repoRoot: string; head: string; tree: string; cleanWorktree: boolean; worktree?: WorktreeSnapshot } {
|
||||
const repoRoot = command(cwd, ["rev-parse", "--show-toplevel"]);
|
||||
const head = command(repoRoot, ["rev-parse", "HEAD"]);
|
||||
const tree = command(repoRoot, ["rev-parse", "HEAD^{tree}"]);
|
||||
const prefix = relative(repoRoot, resolve(cwd)).replaceAll("\\", "/");
|
||||
const owned = prefix ? `${prefix}/.pi` : ".pi";
|
||||
const cleanWorktree = command(repoRoot, [
|
||||
"status", "--porcelain=v1", "--untracked-files=all", "--", ".",
|
||||
const pathspec = [".",
|
||||
`:(exclude,glob)${owned}/plan/*.md`,
|
||||
`:(exclude,glob)${owned}/pi-goals/approvals/*`,
|
||||
]) === "";
|
||||
return { repoRoot, head, tree, cleanWorktree };
|
||||
`:(exclude,glob)${owned}/pi-goals/models/*`,
|
||||
];
|
||||
// NUL delimiters and no rename folding preserve whitespace/newlines and both sides of renames.
|
||||
const raw = execFileSync("git", ["status", "--porcelain=v1", "-z", "--no-renames", ...(captureWorktree ? ["--ignore-submodules=none"] : []), "--untracked-files=all", "--", ...pathspec], { cwd: repoRoot });
|
||||
const status = raw.toString("utf8");
|
||||
if (captureWorktree && !raw.equals(Buffer.from(status))) throw new Error("Cannot fingerprint non-UTF-8 Git paths.");
|
||||
return { repoRoot, head, tree, cleanWorktree: status === "", ...(captureWorktree ? { worktree: worktreeSnapshot(repoRoot, status, pathspec) } : {}) };
|
||||
}
|
||||
|
||||
export function goalBlock(plan: string, goal: string): string | null {
|
||||
const lines = plan.split("\n");
|
||||
const lines = foldPlan(plan).split("\n");
|
||||
const wanted = goal.trim().toLowerCase();
|
||||
const hits = lines.flatMap((line, index) => {
|
||||
const match = GOAL_LINE.exec(line);
|
||||
@@ -51,12 +92,12 @@ export function goalBlock(plan: string, goal: string): string | null {
|
||||
const start = hits[0];
|
||||
let end = lines.length;
|
||||
for (let index = start + 1; index < lines.length; index++) {
|
||||
if (GOAL_LINE.test(lines[index])) {
|
||||
if (GOAL_LINE.test(lines[index]) || /^#{1,2}\s/.test(lines[index])) {
|
||||
end = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return lines.slice(start, end).join("\n");
|
||||
return lines.slice(start, end).join("\n").trimEnd();
|
||||
}
|
||||
|
||||
export function hashGoalBlock(block: string): string {
|
||||
@@ -111,6 +152,7 @@ export function approvalMatches(record: ApprovalRecord | null, input: {
|
||||
head: string;
|
||||
tree: string;
|
||||
cleanWorktree: boolean;
|
||||
worktree?: WorktreeSnapshot;
|
||||
}): boolean {
|
||||
return record?.version === 3
|
||||
&& record.verdict === "accept"
|
||||
@@ -121,8 +163,11 @@ export function approvalMatches(record: ApprovalRecord | null, input: {
|
||||
&& resolve(record.repoRoot) === resolve(input.repoRoot)
|
||||
&& record.head === input.head
|
||||
&& record.tree === input.tree
|
||||
&& record.cleanWorktree === true
|
||||
&& input.cleanWorktree
|
||||
&& (record.force
|
||||
? Boolean(record.force.reason?.trim()) && Boolean(record.force.worktree) && Boolean(input.worktree)
|
||||
&& record.cleanWorktree === input.cleanWorktree
|
||||
&& JSON.stringify(record.force.worktree) === JSON.stringify(input.worktree)
|
||||
: record.cleanWorktree === true && input.cleanWorktree)
|
||||
&& record.inspected.plan === true
|
||||
&& record.inspected.repository === true
|
||||
&& record.inspected.evidence === true
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
// Pi/OpenAI: Adapted from this repo's feature/simple-visible-supervision at cecb1e9.
|
||||
export async function backgroundState(pi: ExtensionAPI): Promise<{ quiet: boolean; description: string }> {
|
||||
const tools = pi.getAllTools();
|
||||
const hasProcesses = tools.some(tool => tool.name === "process");
|
||||
const hasSubagents = tools.some(tool => tool.name === "subagent");
|
||||
let processes: unknown;
|
||||
pi.events.emit("processes:request:list", { reply: (value: unknown) => { processes = value; } });
|
||||
const rows = Array.isArray(processes) ? processes : !hasProcesses && processes === undefined ? [] : null;
|
||||
const known = rows?.every(p => p && ["running", "terminating", "terminate_timeout", "exited", "killed"].includes(p.status));
|
||||
const activeProcesses = known ? rows!.filter(p => !["exited", "killed"].includes(p.status)) : null;
|
||||
let subagents: number | null = hasSubagents ? null : 0;
|
||||
if (hasSubagents) {
|
||||
const requestId = randomUUID();
|
||||
subagents = await new Promise<number | null>(resolve => {
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
const finish = (value: number | null) => { clearTimeout(timer); unsubscribe?.(); resolve(value); };
|
||||
const timer = setTimeout(() => finish(null), 2000);
|
||||
unsubscribe = pi.events.on(`subagents:rpc:v1:reply:${requestId}`, (reply: any) => {
|
||||
if (reply?.requestId !== requestId) return;
|
||||
const count = reply?.success && reply?.data?.fleet?.version === 1 ? reply.data.fleet.totalActive : undefined;
|
||||
finish(Number.isSafeInteger(count) && count >= 0 ? count : null);
|
||||
});
|
||||
pi.events.emit("subagents:rpc:v1:request", { version: 1, requestId, method: "status", params: {}, source: { extension: "pi-goals" } });
|
||||
});
|
||||
}
|
||||
return {
|
||||
quiet: activeProcesses?.length === 0 && subagents === 0,
|
||||
description: `processes: ${activeProcesses?.length ?? "unknown"}${activeProcesses?.length ? ` (${activeProcesses.map(p => p.name || p.id).join(", ")})` : ""}; subagents: ${subagents ?? "unknown"}; unregistered detached work is not tracked`,
|
||||
};
|
||||
}
|
||||
+5
-14
@@ -7,11 +7,9 @@ interface LaunchSupervisorInput {
|
||||
cwd: string;
|
||||
sourceSessionFile: string;
|
||||
workerSessionId: string;
|
||||
workerIntercomId: string;
|
||||
planPath: string;
|
||||
approvalId: string;
|
||||
extensionPath: string;
|
||||
superviseExtensionPath: string | null;
|
||||
model: string | null;
|
||||
}
|
||||
|
||||
@@ -48,17 +46,14 @@ function stalePaneError(error: unknown): boolean {
|
||||
export function supervisorCommand(input: LaunchSupervisorInput): string {
|
||||
const env = [
|
||||
"PI_GOALS_ROLE=supervisor",
|
||||
`PI_GOALS_MODEL_EXPLICIT=${input.model ? "1" : "0"}`,
|
||||
`PI_GOALS_WORKER_ID=${input.workerSessionId}`,
|
||||
`PI_GOALS_WORKER_INTERCOM_ID=${input.workerIntercomId}`,
|
||||
`PI_GOALS_PLAN_PATH=${input.planPath}`,
|
||||
`PI_GOALS_APPROVAL_ID=${input.approvalId}`,
|
||||
`PI_GOALS_OWNER_SESSION_ID=${input.workerSessionId}`,
|
||||
];
|
||||
const args = [
|
||||
"pi",
|
||||
"--no-extensions",
|
||||
"-e", "npm:pi-intercom",
|
||||
"-e", process.env.PI_GOALS_SUPERVISE_EXTENSION ?? input.superviseExtensionPath ?? "npm:@wassname2/pi-supervise@0.0.4",
|
||||
"-e", input.extensionPath,
|
||||
"--fork", input.sourceSessionFile,
|
||||
"--name", `goals-supervisor-${input.workerSessionId.slice(0, 8)}`,
|
||||
@@ -67,19 +62,15 @@ export function supervisorCommand(input: LaunchSupervisorInput): string {
|
||||
return `env ${[...env, ...args].map(shellQuote).join(" ")}`;
|
||||
}
|
||||
|
||||
export async function openSupervisorPane(input: LaunchSupervisorInput): Promise<string> {
|
||||
export async function openSupervisorPane(input: LaunchSupervisorInput, onOpened: (paneId: string) => void): Promise<string> {
|
||||
if (process.env.HERDR_ENV !== "1") throw new Error("Ready needs a Herdr session so pi-goals can open the supervisor session.");
|
||||
await herdr(["--version"], false);
|
||||
const split = await herdr(["pane", "split", "--current", "--direction", "right", "--cwd", input.cwd, "--no-focus"]);
|
||||
const paneId = findPaneId(split);
|
||||
if (!paneId) throw new Error("Herdr did not return the new supervisor pane ID.");
|
||||
try {
|
||||
await herdr(["pane", "run", paneId, supervisorCommand(input)]);
|
||||
return paneId;
|
||||
} catch (error) {
|
||||
await closeSupervisorPane(paneId);
|
||||
throw error;
|
||||
}
|
||||
onOpened(paneId);
|
||||
await herdr(["pane", "run", paneId, supervisorCommand(input)]);
|
||||
return paneId;
|
||||
}
|
||||
|
||||
export async function closeSupervisorPane(paneId: string): Promise<void> {
|
||||
|
||||
+378
-55
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* PI: pi-goals owns one versioned plan per session. After Ready, the main session implements the
|
||||
* plan while a compacted, visible fork supervises it through pi-supervise.
|
||||
* plan while a compacted, visible fork supervises it through pi-intercom.
|
||||
*
|
||||
* Each /goals call makes `.pi/plan/<session_id>-vN.md`. The selected version survives resume and
|
||||
* compaction. Old plans stay on disk but inactive. A session with no selected plan has no widget,
|
||||
@@ -15,16 +15,22 @@
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, type FSWatcher, mkdirSync, readdirSync, readFileSync, rmSync, watch, writeFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
import { Type } from "typebox";
|
||||
import { approvalMatches, approvalPath, goalBlock, hashGoalBlock, readApproval, repositoryState } from "./approval.js";
|
||||
import { backgroundState } from "./background.js";
|
||||
import { closeSupervisorPane, openSupervisorPane } from "./herdr.js";
|
||||
import { completeGoalDescription, completeGoalParamDescription, planDrafting, planningState, resync } from "./prompts.js";
|
||||
import { SUPERVISOR_STARTUP_TIMEOUT_MS, workerPiSupervise } from "./supervise.js";
|
||||
import { isVisibleSupervisor, registerVisibleSupervisor } from "./supervisor-session.js";
|
||||
import { GoalIntercom } from "./intercom.js";
|
||||
import { FOLD_LINE, foldPlan, GOAL_LINE } from "./plan.js";
|
||||
import { completeGoalDescription, completeGoalParamDescription, planDrafting, planningState, resync, supervisorPlanReview } from "./prompts.js";
|
||||
import { RoleModels } from "./role-models.js";
|
||||
import { isVisibleSupervisor, registerVisibleSupervisor, restoredSupervisor } from "./supervisor-session.js";
|
||||
import { workerView } from "./worker-view.js";
|
||||
|
||||
export { foldPlan } from "./plan.js";
|
||||
|
||||
const STATE = "pi-goals-state";
|
||||
const STATUS_KEY = "pi-goals";
|
||||
@@ -36,35 +42,26 @@ const PLAN_SHAPE = `${PLAN_DIR}/<session_id>-vN.md`;
|
||||
// Plan mode blocks edit/write except for its plan file. bash remains available for read-only inspection. -- Pi/Codex
|
||||
const PLAN_MODE_BLOCKED_TOOLS = ["edit", "write"];
|
||||
|
||||
// A checkbox line beginning "goal:", used by the widget and supervisor scheduling.
|
||||
// Everything else reads the file as prose.
|
||||
const GOAL_LINE = /^\s*(?:\d+\.|[-*])\s*\[([ xX/-])\]\s*goal:\s*(.*)$/i;
|
||||
// An indented checkbox line that isn't a goal: a subtask. Only the widget reads these, so the human
|
||||
// sees the next action and not just the goal -- this file IS the task list.
|
||||
const SUBTASK_LINE = /^\s+(?:\d+\.|[-*])\s*\[([ xX/-])\]\s*(.*)$/;
|
||||
// The fold separates current goals from the longer research record.
|
||||
const FOLD_LINE = /^##\s+Log\s*$/im;
|
||||
type GoalStatus = "open" | "active" | "done" | "cancelled";
|
||||
const CHAR_TO_STATUS: Record<string, GoalStatus> = { " ": "open", "/": "active", x: "done", "-": "cancelled" };
|
||||
const STATUS_TO_CHAR: Record<GoalStatus, string> = { open: " ", active: "/", done: "x", cancelled: "-" };
|
||||
const goalKey = (subject: string) => subject.trim().toLowerCase();
|
||||
|
||||
function scanGoals(plan: string): Array<{ status: GoalStatus; subject: string; line: number }> {
|
||||
const goals: Array<{ status: GoalStatus; subject: string; line: number }> = [];
|
||||
plan.split("\n").forEach((line, i) => {
|
||||
foldPlan(plan).split("\n").forEach((line, i) => {
|
||||
const m = GOAL_LINE.exec(line);
|
||||
if (m) goals.push({ status: CHAR_TO_STATUS[m[1].toLowerCase()] ?? "open", subject: m[2].trim(), line: i });
|
||||
});
|
||||
return goals;
|
||||
}
|
||||
|
||||
/** Return the short current-goal section above "## Log". Exported for the unit test. */
|
||||
export function foldPlan(plan: string): string {
|
||||
const m = FOLD_LINE.exec(plan);
|
||||
return (m ? plan.slice(0, m.index) : plan).trimEnd();
|
||||
}
|
||||
|
||||
/** Open subtasks under the goal on line `goalLine`, up to the next goal line. */
|
||||
export function openSubtasks(plan: string, goalLine: number): string[] {
|
||||
const lines = plan.split("\n");
|
||||
const lines = foldPlan(plan).split("\n");
|
||||
const out: string[] = [];
|
||||
for (let i = goalLine + 1; i < lines.length; i++) {
|
||||
if (GOAL_LINE.test(lines[i])) break;
|
||||
@@ -96,20 +93,52 @@ interface PlanState {
|
||||
supervisorPaneId: string | null;
|
||||
approvalId: string | null;
|
||||
planVersion: number | null;
|
||||
latestDirection: string;
|
||||
signedOffGoals: string[];
|
||||
previousPlan: string | null;
|
||||
}
|
||||
|
||||
export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
if (isVisibleSupervisor()) {
|
||||
registerVisibleSupervisor(pi);
|
||||
return;
|
||||
}
|
||||
if (!isMainSession()) return;
|
||||
let initialized = false;
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
const saved = restoredSupervisor(ctx.sessionManager.getEntries());
|
||||
if (saved || isVisibleSupervisor()) registerVisibleSupervisor(pi, saved);
|
||||
else if (isMainSession()) registerWorker(pi);
|
||||
// Pi's dispatcher iterates its live handler list, including those just registered.
|
||||
});
|
||||
}
|
||||
|
||||
export function registerWorker(pi: ExtensionAPI): void {
|
||||
const intercom = new GoalIntercom(pi);
|
||||
const models = new RoleModels(pi);
|
||||
intercom.onSteer = (instruction) => {
|
||||
if (state.phase !== "working" || modelError) throw new Error("Worker is paused or its plan is not active; instruction not delivered. Use /goals reconnect after selecting an available model.");
|
||||
pi.sendUserMessage(`[supervisor] ${instruction}`, { deliverAs: "steer" });
|
||||
};
|
||||
let state: PlanState = {
|
||||
phase: null,
|
||||
supervisorModel: null,
|
||||
supervisorPaneId: null,
|
||||
approvalId: null,
|
||||
planVersion: null,
|
||||
latestDirection: "",
|
||||
signedOffGoals: [],
|
||||
previousPlan: null,
|
||||
};
|
||||
let modelError: string | null = null;
|
||||
let readyAttempt: object | undefined;
|
||||
let wasConnected = false;
|
||||
intercom.onConnectionChange = (ctx) => {
|
||||
const connected = intercom.connected;
|
||||
const rejoined = connected && !wasConnected;
|
||||
wasConnected = connected;
|
||||
updateWidget(ctx);
|
||||
// Ready publishes its own first view. Subsequent rejoins need a new ID even if the old view was accepted.
|
||||
if (rejoined && !readyAttempt && state.phase === "working" && !modelError) {
|
||||
void publishWorkerView(ctx, "settled").catch(error => { if (!intercom.ended) ctx.ui.notify(`Recovery view failed: ${String(error)}`, "error"); });
|
||||
}
|
||||
};
|
||||
let planningContextPending = false;
|
||||
let resyncReason: string | null = "New session.";
|
||||
@@ -131,55 +160,185 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
pi.appendEntry<PlanState>(STATE, state);
|
||||
}
|
||||
|
||||
// Only CompleteGoal adds sign-off; direct edits remain claims for supervisor judgment.
|
||||
function refreshSignoffs(ctx: ExtensionContext): void {
|
||||
if (state.phase !== "working") return;
|
||||
const goals = scanGoals(readPlan(ctx));
|
||||
const signedOffGoals = state.signedOffGoals.filter(subject => {
|
||||
const matches = goals.filter(goal => goalKey(goal.subject) === subject);
|
||||
return matches.length === 1 && matches[0].status === "done";
|
||||
});
|
||||
if (signedOffGoals.length !== state.signedOffGoals.length) {
|
||||
state = { ...state, signedOffGoals };
|
||||
persist();
|
||||
}
|
||||
}
|
||||
|
||||
function planReview(plan: string): string {
|
||||
const goals = scanGoals(plan);
|
||||
const previous = scanGoals(state.previousPlan ?? "");
|
||||
const changes = goals.flatMap(goal => {
|
||||
const old = previous.find(prior => goalKey(prior.subject) === goalKey(goal.subject));
|
||||
return old?.status === goal.status ? [] : [`${goal.subject}: ${old ? `[${STATUS_TO_CHAR[old.status]}]` : "not previously observed"} -> [${STATUS_TO_CHAR[goal.status]}]${goal.status === "done" ? state.signedOffGoals.includes(goalKey(goal.subject)) ? "; CompleteGoal sign-off recorded" : "; manual completion claim, no CompleteGoal sign-off recorded" : ""}`];
|
||||
});
|
||||
const claims = goals.filter(goal => goal.status === "done" && !state.signedOffGoals.includes(goalKey(goal.subject)));
|
||||
return supervisorPlanReview(claims.map(goal => goal.subject), changes, planDiff(state.previousPlan ?? "", plan));
|
||||
}
|
||||
|
||||
function pauseReason(): string | null {
|
||||
if (!state.phase) return null;
|
||||
if (modelError) return `${modelError} Select /model, then run /goals reconnect.`;
|
||||
if (state.phase === "working" && !intercom.connected) return intercom.peerPresent
|
||||
? "Supervisor is present but not ready. Inspect its pane for startup/compaction or model errors; recover with /model then /goals reconnect in the supervisor pane if needed."
|
||||
: "Supervisor disconnected. Run /goals reconnect, or /goals restart to replace its tracked pane without discarding the plan.";
|
||||
return null;
|
||||
}
|
||||
|
||||
async function restoreModel(role: "planning" | "worker", ctx: ExtensionContext): Promise<void> {
|
||||
modelError = `${role} model restoration is pending.`;
|
||||
intercom.markNotReady();
|
||||
try {
|
||||
await models.enter(role, ctx);
|
||||
modelError = null;
|
||||
} catch (error) {
|
||||
modelError = error instanceof Error ? error.message : String(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function beginReview(ctx: ExtensionContext): void {
|
||||
for (const goal of scanGoals(readPlan(ctx))) {
|
||||
rmSync(approvalPath(ctx.cwd, ctx.sessionManager.getSessionId(), goal.subject), { force: true });
|
||||
}
|
||||
state = { ...state, approvalId: randomUUID() };
|
||||
const approvalId = randomUUID();
|
||||
state = { ...state, approvalId };
|
||||
intercom.configure(approvalId, "worker", ctx, false);
|
||||
persist();
|
||||
}
|
||||
|
||||
function loadedPiSuperviseExtensionPath(): string | null {
|
||||
const tool = pi.getAllTools().find((candidate) => candidate.name === "worker_view") as { sourceInfo?: { path?: unknown } } | undefined;
|
||||
return typeof tool?.sourceInfo?.path === "string" ? tool.sourceInfo.path : null;
|
||||
}
|
||||
|
||||
function repositoryRoot(cwd: string): string {
|
||||
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" }).trim();
|
||||
}
|
||||
|
||||
async function startSupervisor(ctx: ExtensionContext): Promise<void> {
|
||||
async function startSupervisor(ctx: ExtensionContext, isCurrent = () => !intercom.ended): Promise<void> {
|
||||
if (intercom.ended) throw new Error("Session ended before supervisor startup.");
|
||||
repositoryRoot(ctx.cwd);
|
||||
const sourceSessionFile = ctx.sessionManager.getSessionFile();
|
||||
if (!sourceSessionFile) throw new Error("The current session is not persisted, so it cannot be forked.");
|
||||
const worker = await workerPiSupervise(pi);
|
||||
if (state.supervisorPaneId && state.approvalId) {
|
||||
intercom.configure(state.approvalId, "worker", ctx, false);
|
||||
await intercom.waitReady(undefined, { peerOnly: true });
|
||||
return;
|
||||
}
|
||||
beginReview(ctx);
|
||||
const binding = state.approvalId;
|
||||
const current = () => isCurrent() && !intercom.ended && state.approvalId === binding;
|
||||
let paneId: string | null = null;
|
||||
try {
|
||||
paneId = await openSupervisorPane({
|
||||
cwd: ctx.cwd,
|
||||
sourceSessionFile,
|
||||
workerSessionId: ctx.sessionManager.getSessionId(),
|
||||
workerIntercomId: worker.intercomId,
|
||||
planPath: planPath(ctx),
|
||||
approvalId: state.approvalId!,
|
||||
extensionPath: fileURLToPath(import.meta.url),
|
||||
superviseExtensionPath: loadedPiSuperviseExtensionPath(),
|
||||
model: state.supervisorModel,
|
||||
}, (opened) => {
|
||||
if (!current()) throw new Error("Supervisor startup was cancelled.");
|
||||
paneId = opened;
|
||||
state = { ...state, supervisorPaneId: opened };
|
||||
persist();
|
||||
});
|
||||
await worker.waitForPair(SUPERVISOR_STARTUP_TIMEOUT_MS);
|
||||
} catch (error) {
|
||||
if (paneId) throw new Error(`Supervisor startup failed in Herdr pane ${paneId}; it remains open for inspection. ${error instanceof Error ? error.message : String(error)}`);
|
||||
throw error;
|
||||
}
|
||||
if (!current()) throw new Error("Supervisor startup was cancelled.");
|
||||
state = { ...state, supervisorPaneId: paneId };
|
||||
persist();
|
||||
await intercom.waitReady(undefined, { peerOnly: true });
|
||||
}
|
||||
|
||||
let workerTurns = 0;
|
||||
let viewGeneration = 0;
|
||||
let viewTimer: ReturnType<typeof setInterval> | undefined;
|
||||
let planWatcher: FSWatcher | undefined;
|
||||
let planEditTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
async function publishWorkerView(ctx: ExtensionContext, reason: "ready" | "settled" | "turns" | "interval" | "started" | "plan"): Promise<void> {
|
||||
if (state.phase !== "working" || modelError || !intercom.bound) return;
|
||||
const generation = ++viewGeneration;
|
||||
const binding = state.approvalId;
|
||||
const background = reason === "started" ? { quiet: false, description: "agent starting; background state not queried" } : await backgroundState(pi);
|
||||
if (!intercom.bound || modelError || generation !== viewGeneration || binding !== state.approvalId || state.phase !== "working") return;
|
||||
refreshSignoffs(ctx);
|
||||
const plan = readPlan(ctx);
|
||||
const entries = ctx.sessionManager.getBranch();
|
||||
const view = workerView(entries, reason, reason !== "started" && ctx.isIdle(), {
|
||||
sourceSession: ctx.sessionManager.getSessionFile()!, latestDirection: state.latestDirection,
|
||||
model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "not selected",
|
||||
contextPercent: ctx.getContextUsage()?.percent,
|
||||
since: intercom.acknowledgedEntry, background: background.description,
|
||||
planReview: `Plan: ${planRel(ctx)}\n${planReview(plan)}`,
|
||||
});
|
||||
intercom.view(view, reason, entries.at(-1)?.id, background.quiet);
|
||||
if (reason !== "started" && intercom.connected && state.previousPlan !== plan) {
|
||||
state = { ...state, previousPlan: plan };
|
||||
persist();
|
||||
}
|
||||
const goals = scanGoals(plan);
|
||||
if (goals.length > 0 && goals.every((goal) => (goal.status === "done" && state.signedOffGoals.includes(goalKey(goal.subject))) || goal.status === "cancelled")) {
|
||||
stopWorkerTimers();
|
||||
state = { ...state, phase: null };
|
||||
models.leave();
|
||||
persist();
|
||||
intercom.detach();
|
||||
}
|
||||
}
|
||||
|
||||
function startWorkerTimers(ctx: ExtensionContext): void {
|
||||
if (!planWatcher) {
|
||||
const activePath = planPath(ctx);
|
||||
try {
|
||||
// Watch the containing directory so atomic replacement does not lose the file watch.
|
||||
planWatcher = watch(join(ctx.cwd, PLAN_DIR), (_event, filename) => {
|
||||
if (intercom.ended || state.phase !== "working") return;
|
||||
if (filename && join(ctx.cwd, PLAN_DIR, filename.toString()) !== activePath) return;
|
||||
if (planEditTimer) clearTimeout(planEditTimer);
|
||||
planEditTimer = setTimeout(() => {
|
||||
planEditTimer = undefined;
|
||||
if (intercom.ended || state.phase !== "working" || planPath(ctx) !== activePath) return;
|
||||
updateWidget(ctx);
|
||||
// Working edits coalesce into the existing settled view; idle edits wake review now.
|
||||
if (ctx.isIdle() && readPlan(ctx) !== state.previousPlan) {
|
||||
void publishWorkerView(ctx, "plan").catch(error => { if (!intercom.ended) ctx.ui.notify(`Plan review failed: ${String(error)}`, "error"); });
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
planWatcher.on("error", error => { if (!intercom.ended) ctx.ui.notify(`Plan watch failed: ${error.message}`, "error"); });
|
||||
} catch (error) { ctx.ui.notify(`Could not watch active plan: ${String(error)}`, "warning"); }
|
||||
}
|
||||
if (!viewTimer) viewTimer = setInterval(() => {
|
||||
void publishWorkerView(ctx, "interval").catch(error => { if (!intercom.ended) ctx.ui.notify(`Worker view failed: ${String(error)}`, "error"); });
|
||||
}, 60 * 60_000);
|
||||
}
|
||||
|
||||
function stopWorkerTimers(): void {
|
||||
planWatcher?.close();
|
||||
planWatcher = undefined;
|
||||
if (planEditTimer) clearTimeout(planEditTimer);
|
||||
planEditTimer = undefined;
|
||||
if (viewTimer) clearInterval(viewTimer);
|
||||
viewTimer = undefined;
|
||||
}
|
||||
|
||||
async function stopSupervisor(): Promise<boolean> {
|
||||
if (!state.supervisorPaneId) return true;
|
||||
readyAttempt = undefined;
|
||||
if (!state.supervisorPaneId) { stopWorkerTimers(); intercom.detach(); return true; }
|
||||
try {
|
||||
await closeSupervisorPane(state.supervisorPaneId);
|
||||
stopWorkerTimers();
|
||||
intercom.detach();
|
||||
state = { ...state, supervisorPaneId: null };
|
||||
persist();
|
||||
return true;
|
||||
@@ -189,6 +348,13 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
}
|
||||
|
||||
function updateWidget(ctx: ExtensionContext): void {
|
||||
refreshSignoffs(ctx);
|
||||
const paused = pauseReason();
|
||||
if (paused) {
|
||||
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("warning", "goals paused"));
|
||||
ctx.ui.setWidget(WIDGET_KEY, [`pi-goals paused: ${paused}`]);
|
||||
return;
|
||||
}
|
||||
if (state.phase === "planning") {
|
||||
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("warning", "planning"));
|
||||
ctx.ui.setWidget(WIDGET_KEY, ["pi-goals: drafting goals"]);
|
||||
@@ -200,16 +366,19 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
||||
return;
|
||||
}
|
||||
const done = goals.filter((g) => g.status === "done").length;
|
||||
const liveGoals = goals.filter((g) => g.status === "active" || g.status === "open");
|
||||
const stateLabel = liveGoals.length > 0 ? " · supervised" : " · complete";
|
||||
const isSignedOff = (subject: string) => state.signedOffGoals.includes(goalKey(subject));
|
||||
const done = goals.filter(g => g.status === "done" && isSignedOff(g.subject)).length;
|
||||
const claimed = goals.filter(g => g.status === "done" && !isSignedOff(g.subject));
|
||||
const liveGoals = goals.filter(g => g.status === "active" || g.status === "open");
|
||||
const stateLabel = claimed.length ? ` · ${claimed.length} claimed, awaiting review` : liveGoals.length > 0 ? " · supervised" : " · complete";
|
||||
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("accent", `◷ ${done}/${goals.length} goals${stateLabel}`));
|
||||
const mark: Record<GoalStatus, string> = { done: "✔", active: "▸", open: "◻", cancelled: "✗" };
|
||||
// Only live goals get lines so finished work never pushes current work off screen. The active
|
||||
// goal also shows its open subtasks: this file is the task list, so the widget is the task list.
|
||||
// No path line: the session id makes it too long to be useful in the widget.
|
||||
const plan = readPlan(ctx);
|
||||
const lines: string[] = liveGoals.length === 0 ? ["✔ complete"] : [];
|
||||
const lines: string[] = claimed.map(g => `? claimed complete; awaiting supervisor review: ${g.subject}`);
|
||||
if (liveGoals.length === 0 && claimed.length === 0) lines.push("✔ complete");
|
||||
for (const g of liveGoals) {
|
||||
lines.push(`${mark[g.status]} ${g.status === "active" ? "supervising… " : ""}${g.subject}`);
|
||||
if (g.status === "active") lines.push(...openSubtasks(plan, g.line).slice(0, 3).map((s) => ctx.ui.theme.fg("muted", ` ◦ ${s}`)));
|
||||
@@ -220,9 +389,59 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
// --- /goals: enter plan mode or configure supervision -- Pi/Codex -----------------------------
|
||||
|
||||
pi.registerCommand("goals", {
|
||||
description: `Plan goals, then open a visible supervisor session. /goals <objective> | clear | model <supervisor>`,
|
||||
description: `Plan goals, then open a visible supervisor session. /goals <objective> | work | supervise | noplan | reconnect | restart | clear | model <supervisor>`,
|
||||
handler: async (args, ctx) => {
|
||||
const arg = args.trim();
|
||||
let arg = args.trim();
|
||||
if (arg === "supervise") { ctx.ui.notify("This is the worker session. Run /goals supervise in the saved supervisor session; no new pairing was created.", "warning"); return; }
|
||||
if (arg === "work") {
|
||||
if (state.phase !== "working" || !state.approvalId || !state.supervisorPaneId) { ctx.ui.notify("No approved worker pairing to reconnect. A retained draft still needs Ready.", "warning"); return; }
|
||||
arg = "reconnect";
|
||||
}
|
||||
if (arg === "noplan") {
|
||||
if (state.phase !== "planning") { ctx.ui.notify("Not in planning mode; the current plan is unchanged.", "info"); return; }
|
||||
readyAttempt = undefined;
|
||||
planningContextPending = false;
|
||||
resyncReason = null;
|
||||
stopWorkerTimers();
|
||||
intercom.detach();
|
||||
models.leave();
|
||||
state = { ...state, phase: null };
|
||||
persist();
|
||||
updateWidget(ctx);
|
||||
ctx.ui.notify(`Planning exited. Draft preserved at ${planRel(ctx)}; no implementation was approved or started.`, "info");
|
||||
return;
|
||||
}
|
||||
if (arg === "reconnect" || arg === "restart") {
|
||||
if (!state.phase) { ctx.ui.notify("No active plan to recover.", "info"); return; }
|
||||
if (!ctx.isIdle()) { ctx.ui.notify("Stop the current turn before recovering goal supervision.", "warning"); return; }
|
||||
readyAttempt = undefined;
|
||||
try {
|
||||
await restoreModel(state.phase === "planning" ? "planning" : "worker", ctx);
|
||||
if (arg === "restart") {
|
||||
if (!(await stopSupervisor())) throw new Error("Could not close the tracked supervisor pane; no replacement was opened.");
|
||||
state = { ...state, supervisorPaneId: null, approvalId: null };
|
||||
persist();
|
||||
}
|
||||
if (state.phase === "working" || state.supervisorPaneId) {
|
||||
if (arg === "reconnect") {
|
||||
if (!state.approvalId) throw new Error("No saved supervision binding. Use /goals restart.");
|
||||
intercom.configure(state.approvalId, "worker", ctx, false);
|
||||
await intercom.waitReady(undefined, { peerOnly: true });
|
||||
} else await startSupervisor(ctx);
|
||||
}
|
||||
if (intercom.ended) return;
|
||||
if (state.phase === "working") {
|
||||
intercom.markReady();
|
||||
startWorkerTimers(ctx);
|
||||
}
|
||||
ctx.ui.notify(state.phase === "planning" ? "Planning model restored. Choose Ready when the plan is agreed." : "Goal supervision reconnected; the current plan is unchanged.", "info");
|
||||
} catch (error) {
|
||||
if (intercom.ended) return;
|
||||
ctx.ui.notify(`Goal recovery failed: ${String(error)} Use /goals reconnect to retry, or /goals restart to explicitly replace the tracked pane.`, "warning");
|
||||
}
|
||||
updateWidget(ctx);
|
||||
return;
|
||||
}
|
||||
if (arg === "clear") {
|
||||
if (state.planVersion === null) {
|
||||
ctx.ui.notify("No active plan to disconnect.", "info");
|
||||
@@ -234,6 +453,8 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
return;
|
||||
}
|
||||
state = { ...state, phase: null, supervisorPaneId: null, approvalId: null, planVersion: null };
|
||||
models.leave();
|
||||
modelError = null;
|
||||
persist();
|
||||
updateWidget(ctx);
|
||||
ctx.ui.notify(`Disconnected from ${currentPlan}; the file remains on disk.`, "info");
|
||||
@@ -251,14 +472,15 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
const ref = arg.slice("model".length).trim();
|
||||
state = { ...state, supervisorModel: ref || null, supervisorPaneId: null, approvalId: null };
|
||||
persist();
|
||||
ctx.ui.notify(`Goal-supervisor model ${ref ? `set to ${ref}` : "reset to the current Pi default"}.`, "info");
|
||||
ctx.ui.notify(`Goal-supervisor model ${ref ? `set to ${ref}` : "reset to the remembered supervisor model"}.`, "info");
|
||||
return;
|
||||
}
|
||||
if (!(await stopSupervisor())) {
|
||||
ctx.ui.notify("Could not close the visible supervisor; no new plan was started.", "warning");
|
||||
return;
|
||||
}
|
||||
state = { ...state, phase: "planning", supervisorPaneId: null, approvalId: null, planVersion: nextVersion(ctx) };
|
||||
await restoreModel("planning", ctx);
|
||||
state = { ...state, phase: "planning", supervisorPaneId: null, approvalId: null, planVersion: nextVersion(ctx), latestDirection: arg, signedOffGoals: [], previousPlan: null };
|
||||
planningContextPending = true;
|
||||
resyncReason = null;
|
||||
writePlan(ctx, "");
|
||||
@@ -286,9 +508,11 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
|
||||
// The phase snapshot enters context only when planning starts or context was lost.
|
||||
pi.on("before_agent_start", async (_event, ctx) => {
|
||||
const paused = pauseReason();
|
||||
if (paused) return { systemPrompt: `${ctx.getSystemPrompt()}\n\nGoal work is paused: ${paused} Do not implement or sign off goals. Human input and read-only diagnosis remain available; wait for recovery before resuming autonomous work.` };
|
||||
if (state.phase === "working") {
|
||||
return {
|
||||
systemPrompt: `${ctx.getSystemPrompt()}\n\nYou are the implementation worker for ${planRel(ctx)}. Keep the full conversation and do the work directly. A stronger read-only supervisor watches this session through pi-supervise and can steer you. Commit clean evidence before asking for sign-off. Stop when a goal appears complete so the supervisor can inspect a settled worker view. Call CompleteGoal only after the supervisor says it recorded approval. -- Pi/Codex`,
|
||||
systemPrompt: `${ctx.getSystemPrompt()}\n\nYou are the implementation worker for ${planRel(ctx)}. Keep the full conversation and do the work directly. A stronger read-only supervisor watches this session through pi-intercom and can steer you. Commit your evidence before asking for sign-off; never commit or discard unrelated changes to satisfy the clean-worktree gate. The supervisor can explicitly accept an inspected unchanged dirty state with ApproveGoal force and a reason. Stop when a goal appears complete so the supervisor can inspect a settled worker view. Call CompleteGoal only after the supervisor says it recorded approval. -- PI[Kimi K3]`,
|
||||
};
|
||||
}
|
||||
if (!planningContextPending) return;
|
||||
@@ -312,14 +536,30 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
|
||||
// PI: Human plan-mode replies are durable evidence of the interview, not model summaries.
|
||||
pi.on("input", async (event, ctx) => {
|
||||
if (state.phase === "planning" && event.source !== "extension") writePlan(ctx, appendInterview(readPlan(ctx), event.text));
|
||||
if (event.source === "extension") return;
|
||||
state = { ...state, latestDirection: event.text };
|
||||
persist();
|
||||
if (state.phase === "planning") writePlan(ctx, appendInterview(readPlan(ctx), event.text));
|
||||
});
|
||||
|
||||
pi.on("agent_start", async (_event, ctx) => {
|
||||
await publishWorkerView(ctx, "started");
|
||||
});
|
||||
|
||||
pi.on("turn_end", async (_event, ctx) => {
|
||||
updateWidget(ctx);
|
||||
if (state.phase !== "working") return;
|
||||
workerTurns++;
|
||||
if (workerTurns < 50) return;
|
||||
workerTurns = 0;
|
||||
await publishWorkerView(ctx, "turns");
|
||||
});
|
||||
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
const paused = pauseReason();
|
||||
if (paused && !(["read", "grep", "find", "ls"].includes(event.toolName) || (event.toolName === "bash" && isPlanningReadOnlyCommand(String((event.input as { command?: string }).command))))) {
|
||||
return { block: true, terminate: true, reason: `Goal work is paused: ${paused} Only read-only diagnosis is available.` };
|
||||
}
|
||||
if (state.phase === "planning") {
|
||||
if (PLAN_MODE_BLOCKED_TOOLS.includes(event.toolName)) {
|
||||
const target = (event.input as { path?: string }).path;
|
||||
@@ -341,9 +581,16 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
|
||||
// PI: Print after Pi settles. agent_end is still streaming, so its message queues behind the menu.
|
||||
pi.on("agent_settled", async (_event, ctx) => {
|
||||
if (state.phase !== "planning" || !ctx.hasUI) return;
|
||||
if (state.phase === "working") {
|
||||
await publishWorkerView(ctx, "settled");
|
||||
return;
|
||||
}
|
||||
if (state.phase !== "planning" || modelError || !ctx.hasUI) return;
|
||||
const version = state.planVersion;
|
||||
const planning = () => !intercom.ended && state.phase === "planning" && state.planVersion === version;
|
||||
let printed = "";
|
||||
while (true) {
|
||||
if (!planning()) return;
|
||||
const plan = readPlan(ctx);
|
||||
if (scanGoals(plan).length === 0) {
|
||||
if (plan.trim()) ctx.ui.notify(`The plan has no goal line. Revise ${planRel(ctx)} to add one.`, "warning");
|
||||
@@ -354,9 +601,13 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
pi.sendMessage({ customType: "plan", content: plan, display: true });
|
||||
}
|
||||
const choice = await ctx.ui.select(`Plan drafted in ${planRel(ctx)}.`, ["Ready", "Refine", "Edit", "Cancel"]);
|
||||
if (!planning()) return;
|
||||
if (choice === "Refine") {
|
||||
const notes = await ctx.ui.editor("What should change about the plan?", "");
|
||||
if (!planning()) return;
|
||||
if (!notes?.trim()) continue;
|
||||
state = { ...state, latestDirection: notes };
|
||||
persist();
|
||||
writePlan(ctx, appendInterview(plan, notes));
|
||||
planningContextPending = true;
|
||||
pi.sendUserMessage(`Revise the plan at ${planPath(ctx)} using these human notes:\n\n${notes}\n\nKeep the same goal structure.`, { deliverAs: "followUp" });
|
||||
@@ -364,11 +615,14 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
}
|
||||
if (choice === "Edit") {
|
||||
const edited = await ctx.ui.editor("Edit the plan", plan);
|
||||
if (!planning()) return;
|
||||
if (edited !== undefined && edited !== plan) writePlan(ctx, edited);
|
||||
continue;
|
||||
}
|
||||
if (choice === "Cancel") {
|
||||
if (!(await stopSupervisor())) { ctx.ui.notify("Could not close the tracked supervisor; plan was not discarded.", "warning"); return; }
|
||||
rmSync(planPath(ctx), { force: true });
|
||||
models.leave();
|
||||
state = { ...state, phase: null, supervisorPaneId: null, approvalId: null, planVersion: null };
|
||||
persist();
|
||||
updateWidget(ctx);
|
||||
@@ -376,17 +630,38 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
return;
|
||||
}
|
||||
if (choice !== "Ready") return;
|
||||
const attempt = {};
|
||||
readyAttempt = attempt;
|
||||
const current = () => !intercom.ended && readyAttempt === attempt && state.planVersion === version;
|
||||
const checkApprovedPlan = () => {
|
||||
if (readPlan(ctx) !== plan) throw new Error("The plan changed after Ready was selected. Review the changed plan and select Ready again; the existing supervisor pane is retained.");
|
||||
};
|
||||
try {
|
||||
await startSupervisor(ctx);
|
||||
checkApprovedPlan();
|
||||
await startSupervisor(ctx, current);
|
||||
if (!current()) return;
|
||||
checkApprovedPlan();
|
||||
await restoreModel("worker", ctx);
|
||||
if (!current()) return;
|
||||
checkApprovedPlan();
|
||||
state = { ...state, phase: "working" };
|
||||
resyncReason = "The plan was approved.";
|
||||
persist();
|
||||
intercom.markReady();
|
||||
startWorkerTimers(ctx);
|
||||
await publishWorkerView(ctx, "ready");
|
||||
if (!current()) return;
|
||||
checkApprovedPlan();
|
||||
updateWidget(ctx);
|
||||
ctx.ui.notify(`Visible supervisor opened in Herdr pane ${state.supervisorPaneId}.`, "info");
|
||||
pi.sendUserMessage("The plan is approved. Begin implementation as the worker.");
|
||||
readyAttempt = undefined;
|
||||
} catch (error) {
|
||||
ctx.ui.notify(`Goal supervisor could not start: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
||||
state = { ...state, phase: "planning", supervisorPaneId: null, approvalId: null };
|
||||
if (!current()) return;
|
||||
intercom.markNotReady();
|
||||
stopWorkerTimers();
|
||||
ctx.ui.notify(`Goal supervisor could not start: ${error instanceof Error ? error.message : String(error)} Use /goals reconnect to retry, or /goals restart to replace the tracked pane.`, "warning");
|
||||
state = { ...state, phase: "planning" };
|
||||
persist();
|
||||
updateWidget(ctx);
|
||||
}
|
||||
@@ -405,12 +680,36 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
supervisorPaneId: last?.data?.supervisorPaneId ?? null,
|
||||
approvalId: last?.data?.approvalId ?? null,
|
||||
planVersion: last?.data?.planVersion ?? null,
|
||||
latestDirection: last?.data?.latestDirection ?? "",
|
||||
signedOffGoals: last?.data?.signedOffGoals ?? [],
|
||||
previousPlan: last?.data?.previousPlan ?? null,
|
||||
};
|
||||
modelError = state.phase ? "Role model restoration is pending." : null;
|
||||
planningContextPending = state.phase === "planning";
|
||||
resyncReason = state.phase === "working" ? "New session." : null;
|
||||
if (state.phase === "working" && state.approvalId) {
|
||||
intercom.configure(state.approvalId, "worker", ctx, false);
|
||||
startWorkerTimers(ctx);
|
||||
}
|
||||
try {
|
||||
if (state.phase) await restoreModel(state.phase === "planning" ? "planning" : "worker", ctx);
|
||||
} catch (error) {
|
||||
if (!intercom.ended) ctx.ui.notify(`Goal work paused: ${String(error)} Use /model, then /goals reconnect.`, "warning");
|
||||
}
|
||||
if (intercom.ended) return;
|
||||
if (state.phase === "working" && state.approvalId && !modelError) {
|
||||
intercom.markReady();
|
||||
void intercom.waitReady().catch(error => {
|
||||
if (!intercom.ended && state.phase === "working") ctx.ui.notify(`Goal work paused: ${String(error)} Use /goals reconnect or /goals restart.`, "warning");
|
||||
});
|
||||
}
|
||||
updateWidget(ctx);
|
||||
});
|
||||
|
||||
pi.on("session_shutdown", async () => {
|
||||
stopWorkerTimers();
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "CompleteGoal",
|
||||
label: "Goal signoff",
|
||||
@@ -418,21 +717,28 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
parameters: Type.Object({
|
||||
goal: Type.String({ description: completeGoalParamDescription }),
|
||||
}),
|
||||
async execute(_id, params, _signal, _onUpdate, ctx) {
|
||||
async execute(_id, params, signal, _onUpdate, ctx) {
|
||||
if (signal?.aborted) return result("Goal sign-off cancelled; no completion recorded.", true);
|
||||
const binding = state.approvalId;
|
||||
const version = state.planVersion;
|
||||
if (state.phase !== "working") return result("Planning is not approved. Choose Ready before signing off a goal.", true);
|
||||
if (pauseReason()) return result(`Goal sign-off blocked: ${pauseReason()}`, true);
|
||||
if (!state.approvalId) return result("Goal sign-off blocked: no current supervisor review.", true);
|
||||
const background = await backgroundState(pi);
|
||||
if (signal?.aborted || intercom.ended || state.approvalId !== binding || state.planVersion !== version || state.phase !== "working") return result("Goal sign-off cancelled or superseded; no completion recorded.", true);
|
||||
if (intercom.ended || !background.quiet || pauseReason()) return result(`Goal sign-off blocked: ${pauseReason() ?? background.description}`, true);
|
||||
const plan = readPlan(ctx);
|
||||
if (!plan.trim()) return result(`No plan file at ${planRel(ctx)}. Run /goals to draft one.`, true);
|
||||
const block = goalBlock(plan, params.goal);
|
||||
if (!block) return result(`No unique open goal line matched "${params.goal}" in ${planRel(ctx)}.`, true);
|
||||
const approval = readApproval(approvalPath(ctx.cwd, ctx.sessionManager.getSessionId(), params.goal));
|
||||
let repository: ReturnType<typeof repositoryState>;
|
||||
try {
|
||||
repository = repositoryState(ctx.cwd);
|
||||
repository = repositoryState(ctx.cwd, Boolean(approval?.force));
|
||||
} catch (error) {
|
||||
return result(`Goal sign-off could not inspect the repository: ${error instanceof Error ? error.message : String(error)}`, true);
|
||||
}
|
||||
if (!repository.cleanWorktree) return result("Goal sign-off blocked: worktree is dirty.", true);
|
||||
const approval = readApproval(approvalPath(ctx.cwd, ctx.sessionManager.getSessionId(), params.goal));
|
||||
if (!repository.cleanWorktree && !approval?.force) return result("Goal sign-off blocked: worktree is dirty. Request supervisor inspection, not an unrelated cleanup commit.", true);
|
||||
if (!approvalMatches(approval, {
|
||||
approvalId: state.approvalId,
|
||||
goal: params.goal,
|
||||
@@ -442,10 +748,14 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
head: repository.head,
|
||||
tree: repository.tree,
|
||||
cleanWorktree: repository.cleanWorktree,
|
||||
worktree: repository.worktree,
|
||||
})) return result("Goal sign-off blocked: no matching supervisor approval checkpoint. Request a fresh supervisor review.", true);
|
||||
const ticked = tickGoal(plan, params.goal);
|
||||
if (!ticked) return result(`No unique exact goal line matched "${params.goal}" in ${planRel(ctx)}.`, true);
|
||||
if (signal?.aborted) return result("Goal sign-off cancelled; no completion recorded.", true);
|
||||
writePlan(ctx, appendLog(ticked, `${stamp()} mechanically signed off "${params.goal}" after matching supervisor approval`));
|
||||
state = { ...state, signedOffGoals: [...state.signedOffGoals.filter(goal => goal !== goalKey(params.goal)), goalKey(params.goal)] };
|
||||
persist();
|
||||
updateWidget(ctx);
|
||||
return result(`Sign-off accepted. Goal ticked [x] in ${planRel(ctx)}.`);
|
||||
},
|
||||
@@ -454,6 +764,19 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
|
||||
// --- helpers (module scope) --------------------------------------------------------------------
|
||||
|
||||
// A compact changed span, not a second plan parser. Worker views bound its serialized size.
|
||||
function planDiff(before: string, after: string): string {
|
||||
if (before === after) return "none";
|
||||
const old = before.split("\n");
|
||||
const next = after.split("\n");
|
||||
let start = 0;
|
||||
while (start < old.length && start < next.length && old[start] === next[start]) start++;
|
||||
let oldEnd = old.length;
|
||||
let nextEnd = next.length;
|
||||
while (oldEnd > start && nextEnd > start && old[oldEnd - 1] === next[nextEnd - 1]) { oldEnd--; nextEnd--; }
|
||||
return [`@@ from line ${start + 1} @@`, ...old.slice(start, oldEnd).map(line => `- ${line}`), ...next.slice(start, nextEnd).map(line => `+ ${line}`)].join("\n");
|
||||
}
|
||||
|
||||
function result(text: string, isError = false) {
|
||||
return { content: [{ type: "text" as const, text }], details: {}, isError };
|
||||
}
|
||||
@@ -485,7 +808,7 @@ function stamp(): string {
|
||||
export function tickGoal(plan: string, goal: string): string | null {
|
||||
const lines = plan.split("\n");
|
||||
const want = goal.trim().toLowerCase();
|
||||
const hits = lines.flatMap((l, i) => (GOAL_LINE.exec(l)?.[2].trim().toLowerCase() === want ? [i] : []));
|
||||
const hits = scanGoals(plan).filter(g => g.subject.toLowerCase() === want).map(g => g.line);
|
||||
if (hits.length !== 1) return null;
|
||||
lines[hits[0]] = lines[hits[0]].replace(/\[[ xX/-]\]/, "[x]");
|
||||
return lines.join("\n");
|
||||
@@ -495,7 +818,7 @@ export function tickGoal(plan: string, goal: string): string | null {
|
||||
export function appendLog(text: string, entry: string): string {
|
||||
const lines = text.split("\n");
|
||||
const line = `- ${entry}`;
|
||||
const header = lines.findIndex((l) => /^##\s+Log\s*$/i.test(l));
|
||||
const header = lines.findIndex((l) => FOLD_LINE.test(l));
|
||||
if (header === -1) return `${text.replace(/\n+$/, "")}\n\n## Log\n${line}\n`;
|
||||
let insertAt = header + 1;
|
||||
for (let i = header + 1; i < lines.length; i++) {
|
||||
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module "pi-intercom" {
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
export default function intercom(pi: ExtensionAPI): void;
|
||||
}
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { ExtensionAPI, ExtensionContext, SessionStartEvent } from "@earendil-works/pi-coding-agent";
|
||||
import type { IntercomExtensionChannel, IntercomExtensionEvent } from "pi-intercom/extension-api.ts";
|
||||
|
||||
export type Role = "worker" | "supervisor";
|
||||
export interface View { id: string; text: string; reason: string; through?: string; backgroundQuiet: boolean }
|
||||
interface Message { binding: string; role: Role; kind: "hello" | "view" | "steer" | "received"; id: string; text?: string; reason?: string; failure?: string; ready?: boolean; reply?: boolean; through?: string; backgroundQuiet?: boolean }
|
||||
const STATE = "pi-goals-intercom";
|
||||
|
||||
export class GoalIntercom {
|
||||
private channel?: IntercomExtensionChannel;
|
||||
private ctx?: ExtensionContext;
|
||||
private stopped = false;
|
||||
private registered = false;
|
||||
private binding = "";
|
||||
private role: Role = "worker";
|
||||
private ready = false;
|
||||
private failure?: string;
|
||||
private peer?: string;
|
||||
private peerReady = false;
|
||||
private peerFailure?: string;
|
||||
private pending = new Map<string, Message>();
|
||||
private received = new Set<string>();
|
||||
private inbox = new Map<string, Message>();
|
||||
private deliveryTimer?: ReturnType<typeof setTimeout>;
|
||||
private idleChecks = 0;
|
||||
private delivering?: string;
|
||||
private compacting = false;
|
||||
private waiters = new Set<(error?: Error) => void>();
|
||||
latestView?: View;
|
||||
acknowledgedEntry?: string;
|
||||
onView: (view: View) => void = () => {};
|
||||
onSteer: (text: string) => void = () => {};
|
||||
onConnectionChange: (ctx: ExtensionContext) => void = () => {};
|
||||
|
||||
constructor(private pi: ExtensionAPI) {
|
||||
pi.events.on("intercom:extension-registry-ready", () => this.register());
|
||||
this.register();
|
||||
pi.on("session_start", async (event, ctx) => {
|
||||
this.ctx = ctx;
|
||||
if (!this.channel) await this.loadIntercom(event, ctx);
|
||||
});
|
||||
pi.on("message_start", async event => {
|
||||
if (event.message.role !== "user") return;
|
||||
const content = event.message.content;
|
||||
const text = typeof content === "string" ? content : content.filter(part => part.type === "text").map(part => part.text).join("\n");
|
||||
for (const message of this.inbox.values()) {
|
||||
if (text !== this.deliveryText(message)) continue;
|
||||
this.inbox.delete(message.id);
|
||||
this.received.add(message.id);
|
||||
this.record("in", message);
|
||||
if (this.connected) this.publish({ binding: this.binding, role: this.role, kind: "received", id: message.id });
|
||||
this.delivering = undefined;
|
||||
this.idleChecks = 0;
|
||||
this.scheduleDelivery(0);
|
||||
break;
|
||||
}
|
||||
});
|
||||
pi.on("session_before_compact", async () => { this.compacting = true; });
|
||||
pi.on("session_compact", async () => { this.compacting = true; this.resumeDelivery(); });
|
||||
pi.on("session_compact_failed", async () => { this.compacting = true; this.resumeDelivery(); });
|
||||
pi.on("agent_settled", async () => this.resumeDelivery());
|
||||
pi.on("session_shutdown", async () => {
|
||||
if (this.deliveryTimer) clearTimeout(this.deliveryTimer);
|
||||
this.stopped = true;
|
||||
this.peerReady = false;
|
||||
for (const wake of this.waiters) wake();
|
||||
});
|
||||
}
|
||||
|
||||
configure(binding: string, role: Role, ctx: ExtensionContext, ready = role === "worker"): void {
|
||||
for (const wake of this.waiters) wake(new Error("Supervision readiness wait cancelled by reconfiguration."));
|
||||
this.binding = binding;
|
||||
this.role = role;
|
||||
this.ctx = ctx;
|
||||
this.ready = ready;
|
||||
this.failure = undefined;
|
||||
this.peer = undefined;
|
||||
this.peerReady = false;
|
||||
this.peerFailure = undefined;
|
||||
this.pending.clear();
|
||||
this.received.clear();
|
||||
this.inbox.clear();
|
||||
this.delivering = undefined;
|
||||
this.idleChecks = 0;
|
||||
if (this.deliveryTimer) clearTimeout(this.deliveryTimer);
|
||||
this.deliveryTimer = undefined;
|
||||
this.latestView = undefined;
|
||||
this.acknowledgedEntry = undefined;
|
||||
for (const entry of ctx.sessionManager.getBranch?.() ?? ctx.sessionManager.getEntries()) {
|
||||
if (entry.type !== "custom" || entry.customType !== STATE) continue;
|
||||
const record = entry.data as { direction: string; message: Message };
|
||||
const message = record.message;
|
||||
if (message.binding !== binding) continue;
|
||||
if (record.direction === "out" && message.kind === "steer") this.pending.set(message.id, message);
|
||||
if (record.direction === "ack") {
|
||||
this.pending.delete(message.id);
|
||||
if (message.through) this.acknowledgedEntry = message.through;
|
||||
}
|
||||
if (record.direction === "queued") this.inbox.set(message.id, message);
|
||||
if (record.direction === "in") { this.received.add(message.id); this.inbox.delete(message.id); }
|
||||
if (message.kind === "view") this.latestView = { id: message.id, text: message.text!, reason: message.reason!, through: message.through, backgroundQuiet: message.backgroundQuiet === true };
|
||||
}
|
||||
this.hello();
|
||||
this.scheduleDelivery(0);
|
||||
}
|
||||
|
||||
// End this plan's binding without disposing the session's transport.
|
||||
detach(): void {
|
||||
if (this.deliveryTimer) clearTimeout(this.deliveryTimer);
|
||||
this.deliveryTimer = undefined;
|
||||
this.inbox.clear();
|
||||
this.delivering = undefined;
|
||||
this.ready = false;
|
||||
this.hello();
|
||||
this.binding = "";
|
||||
this.peer = undefined;
|
||||
this.peerReady = false;
|
||||
this.latestView = undefined;
|
||||
this.pending.clear();
|
||||
for (const wake of this.waiters) wake(new Error("Supervision readiness wait cancelled: plan detached."));
|
||||
if (this.ctx) this.onConnectionChange(this.ctx);
|
||||
}
|
||||
|
||||
failReady(reason: string): void { this.failure = reason; this.setReady(false); }
|
||||
markReady(): void { this.failure = undefined; this.setReady(true); this.resumeDelivery(); }
|
||||
markNotReady(): void { this.setReady(false); }
|
||||
private setReady(ready: boolean): void {
|
||||
if (this.stopped) return;
|
||||
this.ready = ready;
|
||||
this.hello();
|
||||
if (this.ctx) this.onConnectionChange(this.ctx);
|
||||
}
|
||||
get ended(): boolean { return this.stopped; }
|
||||
get bound(): boolean { return !this.stopped && Boolean(this.binding); }
|
||||
get peerPresent(): boolean { return Boolean(this.bound && this.peer && this.channel?.snapshot().connected); }
|
||||
get connected(): boolean { return this.ready && this.peerPresent && this.peerReady; }
|
||||
|
||||
// Startup can wait for the supervisor while the worker is still in planning/model recovery.
|
||||
async waitReady(timeoutMs = 300_000, { peerOnly = false } = {}): Promise<void> {
|
||||
const ready = () => this.connected || (peerOnly && this.peerPresent && this.peerReady);
|
||||
if (this.peerFailure) throw new Error(this.peerFailure);
|
||||
if (ready()) return;
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const finish = (error?: Error) => {
|
||||
if (!error && !ready() && !this.stopped) return;
|
||||
clearTimeout(timer); this.waiters.delete(finish);
|
||||
if (error) reject(error);
|
||||
else if (this.stopped) reject(new Error("Session ended while waiting for Intercom readiness."));
|
||||
else resolve();
|
||||
};
|
||||
const timer = setTimeout(() => { this.waiters.delete(finish); reject(new Error("Supervisor did not become ready through pi-intercom; inspect its pane.")); }, timeoutMs);
|
||||
this.waiters.add(finish);
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
view(text: string, reason: string, through?: string, backgroundQuiet = false): View {
|
||||
if (!this.bound) throw new Error("No active supervision binding for a worker view.");
|
||||
const id = randomUUID();
|
||||
const message: Message = { binding: this.binding, role: this.role, kind: "view", id, text: `${text}\n\nworker view id: ${id}`, reason, through, backgroundQuiet };
|
||||
this.record("out", message);
|
||||
this.latestView = { id, text: message.text!, reason, through, backgroundQuiet };
|
||||
if (this.connected) this.publish(message);
|
||||
return this.latestView;
|
||||
}
|
||||
|
||||
steer(text: string): string {
|
||||
if (!this.connected) throw new Error("Worker is disconnected; no instruction was sent.");
|
||||
const message: Message = { binding: this.binding, role: this.role, kind: "steer", id: randomUUID(), text };
|
||||
this.record("out", message);
|
||||
this.pending.set(message.id, message);
|
||||
this.publish(message);
|
||||
return message.id;
|
||||
}
|
||||
|
||||
// The inbox is persisted before handoff. Receipt means Pi started the user message, not model judgment or execution.
|
||||
private deliveryText(message: Message): string { return message.kind === "view" ? message.text! : `[supervisor] ${message.text!}`; }
|
||||
resumeDelivery(): void {
|
||||
if (!this.ctx?.hasPendingMessages?.()) this.delivering = undefined;
|
||||
this.idleChecks = 0;
|
||||
this.scheduleDelivery(0); // Pi 0.85.1 isIdle includes compaction; check it after the success/failure hook.
|
||||
}
|
||||
private scheduleDelivery(delay: number): void {
|
||||
if (this.deliveryTimer) clearTimeout(this.deliveryTimer);
|
||||
this.deliveryTimer = undefined;
|
||||
if (!this.bound || !this.inbox.size) return;
|
||||
this.deliveryTimer = setTimeout(() => { this.deliveryTimer = undefined; this.deliverNext(); }, delay);
|
||||
}
|
||||
private deliverNext(): void {
|
||||
if (!this.bound || !this.ready || !this.inbox.size) return;
|
||||
// Accepted-but-not-yet-presented messages must not be submitted again behind a running turn.
|
||||
if (this.ctx?.hasPendingMessages?.()) return;
|
||||
if (this.compacting && !this.ctx?.isIdle?.()) {
|
||||
if (++this.idleChecks <= 300) this.scheduleDelivery(1000);
|
||||
else this.ctx?.ui.notify("Supervision message retained while Pi is busy. Use /goals reconnect when ready to retry delivery.", "warning");
|
||||
return;
|
||||
}
|
||||
if (this.delivering && !this.compacting) return;
|
||||
this.compacting = false;
|
||||
const message = this.inbox.values().next().value!;
|
||||
this.delivering = message.id;
|
||||
try {
|
||||
if (message.kind === "view") this.onView({ id: message.id, text: message.text!, reason: message.reason!, through: message.through, backgroundQuiet: message.backgroundQuiet === true });
|
||||
else this.onSteer(message.text!);
|
||||
} catch (error) { this.delivering = undefined; this.ctx?.ui.notify(`Supervision message retained: ${String(error)} Use /goals reconnect to retry.`, "warning"); }
|
||||
}
|
||||
|
||||
private record(direction: string, message: Message): void { this.pi.appendEntry(STATE, { direction, message }); }
|
||||
private publish(message: Message): void {
|
||||
if (this.stopped) throw new Error("Intercom session ended.");
|
||||
if (Buffer.byteLength(JSON.stringify(message)) > 16_000) throw new Error("Supervisor message exceeds the Intercom payload limit.");
|
||||
if (!this.channel?.snapshot().supported) throw new Error("pi-intercom broker does not support extension channels.");
|
||||
this.channel.publish(message, { audience: "capable" });
|
||||
}
|
||||
private hello(reply = false): void {
|
||||
if (!this.stopped && this.binding && this.channel?.snapshot().connected) this.publish({ binding: this.binding, role: this.role, kind: "hello", id: "hello", ready: this.ready, failure: this.failure, reply });
|
||||
}
|
||||
private receive(event: IntercomExtensionEvent): void {
|
||||
if (this.stopped) return;
|
||||
if (event.type === "connection") {
|
||||
if (!event.connected) {
|
||||
if (this.peerReady) this.ctx?.ui.notify("Goal supervision disconnected from pi-intercom.", "warning");
|
||||
this.peer = undefined; this.peerReady = false;
|
||||
}
|
||||
else this.hello();
|
||||
if (this.ctx) this.onConnectionChange(this.ctx);
|
||||
return;
|
||||
}
|
||||
if (event.type === "session_left" && event.sessionId === this.peer) {
|
||||
this.peer = undefined; this.peerReady = false;
|
||||
this.ctx?.ui.notify("Goal supervision peer disconnected; reconnect the existing session.", "warning");
|
||||
if (this.ctx) this.onConnectionChange(this.ctx);
|
||||
return;
|
||||
}
|
||||
if (event.type === "session_joined") { this.hello(); return; }
|
||||
if (event.type !== "message") return;
|
||||
const message = event.payload as Message;
|
||||
if (!message || message.binding !== this.binding || message.role !== (this.role === "worker" ? "supervisor" : "worker")) return;
|
||||
if (message.kind === "hello") {
|
||||
if (this.peer && this.peer !== event.fromSessionId) throw new Error("Two peers claim this supervision binding. Stop the duplicate session.");
|
||||
const changed = !this.peer || this.peerReady !== Boolean(message.ready);
|
||||
this.peer = event.fromSessionId;
|
||||
this.peerReady = Boolean(message.ready);
|
||||
this.peerFailure = message.failure;
|
||||
if (message.failure) {
|
||||
this.ctx?.ui.notify(message.failure, "error");
|
||||
for (const wake of this.waiters) wake(new Error(message.failure));
|
||||
}
|
||||
// Every request gets one reply, even if only the sender forgot its peer.
|
||||
// Replies never elicit hellos; own-ready transitions also trigger replay here.
|
||||
if (!message.reply) this.hello(true);
|
||||
if (this.peerReady && this.ready) {
|
||||
if (this.role === "worker" && this.latestView) this.publish({ binding: this.binding, role: this.role, kind: "view", ...this.latestView });
|
||||
for (const pending of this.pending.values()) this.publish(pending);
|
||||
}
|
||||
if (this.connected && this.inbox.size && !this.deliveryTimer) this.scheduleDelivery(0);
|
||||
if (changed && this.ctx) this.onConnectionChange(this.ctx);
|
||||
for (const wake of this.waiters) wake();
|
||||
return;
|
||||
}
|
||||
if (event.fromSessionId !== this.peer || !this.ready) return;
|
||||
if (message.kind === "received") {
|
||||
this.pending.delete(message.id);
|
||||
const through = message.id === this.latestView?.id ? this.latestView.through : undefined;
|
||||
if (through) this.acknowledgedEntry = through;
|
||||
this.record("ack", { ...message, through });
|
||||
return;
|
||||
}
|
||||
if (this.received.has(message.id)) {
|
||||
if (message.kind === "steer" || (message.kind === "view" && message.reason !== "started")) this.publish({ binding: this.binding, role: this.role, kind: "received", id: message.id });
|
||||
return;
|
||||
}
|
||||
if (message.kind === "view" && this.role === "supervisor") {
|
||||
this.latestView = { id: message.id, text: message.text!, reason: message.reason!, through: message.through, backgroundQuiet: message.backgroundQuiet === true };
|
||||
if (message.reason === "started") { this.record("in", message); return; }
|
||||
} else if (message.kind !== "steer" || this.role !== "worker") return;
|
||||
if (this.inbox.has(message.id)) return;
|
||||
if (this.inbox.size >= 64) { this.ctx?.ui.notify("Supervision inbox is full; message was not acknowledged. Use /goals reconnect after pending review finishes.", "error"); return; }
|
||||
this.record("queued", message);
|
||||
this.inbox.set(message.id, message);
|
||||
if (this.compacting) this.ctx?.ui.notify("Supervision message retained during compaction; delivery will retry automatically.", "info");
|
||||
if (!this.delivering) this.deliverNext();
|
||||
}
|
||||
private register(): void {
|
||||
if (this.stopped || this.registered) return;
|
||||
this.pi.events.emit("intercom:extension-register", {
|
||||
namespace: "pi-goals", ownerEligible: false,
|
||||
onReady: (channel: IntercomExtensionChannel) => { if (this.stopped) return; this.registered = true; this.channel = channel; this.hello(); },
|
||||
onEvent: (event: IntercomExtensionEvent) => {
|
||||
try { this.receive(event); }
|
||||
catch (error) { if (!this.stopped) this.ctx?.ui.notify(`Goal Intercom error: ${String(error)}`, "error"); }
|
||||
},
|
||||
});
|
||||
}
|
||||
private async loadIntercom(event: SessionStartEvent, ctx: ExtensionContext): Promise<void> {
|
||||
if (this.pi.getAllTools().some(tool => tool.name === "intercom")) throw new Error("Installed pi-intercom has no extension channel; update it before continuing.");
|
||||
const starts: Array<(event: SessionStartEvent, ctx: ExtensionContext) => unknown> = [];
|
||||
const api = { ...this.pi, on: (name: string, handler: (...args: any[]) => any) => {
|
||||
if (name === "session_start") starts.push(handler);
|
||||
else this.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);
|
||||
this.register();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Pi/OpenAI: Preserve plan wording; omit history and, in the short view, task/evidence details.
|
||||
export function planViews(plan: string): { short: string; long: string } {
|
||||
const long = plan.split(/^#{1,6}\s+(?:Log|Appendix|Appendices|Appendixes|Interview|Learnings|Papercuts)\b.*$/mi)[0].trim();
|
||||
const kept: string[] = [];
|
||||
let omittedIndent: number | null = null;
|
||||
let omittedHeading: number | null = null;
|
||||
for (const line of long.split("\n")) {
|
||||
const heading = /^(#{1,6})\s+(.+)$/.exec(line);
|
||||
if (heading) {
|
||||
if (omittedHeading !== null && heading[1].length <= omittedHeading) omittedHeading = null;
|
||||
if (/^(?:Tasks?|Task list|Subtasks?|Evidence)\b/i.test(heading[2])) omittedHeading = heading[1].length;
|
||||
}
|
||||
if (omittedHeading !== null) continue;
|
||||
const indent = line.match(/^\s*/)?.[0].length ?? 0;
|
||||
if (omittedIndent !== null) {
|
||||
if (!line.trim() || indent > omittedIndent) continue;
|
||||
omittedIndent = null;
|
||||
}
|
||||
if (/^\s*[-*]\s+(?:tasks?|subtasks?|evidence):/i.test(line) || /^\s*(?:\d+[.)]|[-*])\s+\[[ x/~-]\]\s+(?!goal:)/i.test(line)) {
|
||||
omittedIndent = indent;
|
||||
continue;
|
||||
}
|
||||
kept.push(line);
|
||||
}
|
||||
return { short: kept.join("\n").trim(), long };
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Shared plan syntax: only the section above the Log contains current goals.
|
||||
export const GOAL_LINE = /^\s*(?:\d+\.|[-*])\s*\[([ xX/-])\]\s*goal:\s*(.*)$/i;
|
||||
export const FOLD_LINE = /^##\s+Log\s*$/im;
|
||||
|
||||
export function foldPlan(plan: string): string {
|
||||
const match = FOLD_LINE.exec(plan);
|
||||
return (match ? plan.slice(0, match.index) : plan).trimEnd();
|
||||
}
|
||||
+119
-28
@@ -3,17 +3,13 @@
|
||||
*
|
||||
* Design: the plan file is for LLMs and the human, not for TypeScript. No parser and no schema;
|
||||
* the skeleton below is a convention the drafting prompt teaches. The main session implements it,
|
||||
* while a visible forked Pi session supervises through pi-supervise.
|
||||
* while a visible forked Pi session supervises through pi-intercom.
|
||||
*
|
||||
* THE FOLD: everything above "## Log" is the short current-goal section. Everything below it
|
||||
* (Log, Learnings, Appendix) is durable memory: unlimited, read on demand, and sent in full at
|
||||
* session start and after compaction.
|
||||
* The worker resync receives the whole plan. Supervisor reviews receive outcome/preferences/goals;
|
||||
* startup and compaction add the full active plan before appendices/history (see plan-view.ts).
|
||||
*
|
||||
* Flow:
|
||||
* SETUP (plan mode) 1. planDrafting — draft goals into the plan file (read-only), sent once
|
||||
* EXEC, after compact 2. resync — the WHOLE file back, once
|
||||
* SIGN-OFF, worker-side 3. completeGoal* — the one blessed tool's description
|
||||
* SUPERVISION supervisor-session.ts — visible read-only supervisor
|
||||
* Flow: planning → worker resync → supervisor orientation → check-ins → steering → approval →
|
||||
* worker sign-off. Dynamic gate errors stay beside their checks; these prompts ask for judgment.
|
||||
*
|
||||
* The goal's test is the DISCRIMINATOR: the concrete observation that tells real success from the
|
||||
* named subtle failure mode. Evidence is empty at planning and filled at sign-off.
|
||||
@@ -23,23 +19,30 @@
|
||||
* 1. planDrafting — SETUP, plan mode (read-only: edit/write blocked except the plan file)
|
||||
* ──────────────────────────────────────────────────────────────────────── */
|
||||
export const planDrafting = `\
|
||||
You are in plan mode. You are making a short judgeable plan that captures the user's real goals, then tests it in conversation.
|
||||
You are in plan mode. Help the user express what they want this project to achieve in a short judgeable plan. Seek to understand their underlying goals, infer ordinary details, and use their applicable AGENTS.md instructions, relevant skills, and project context to interpret the request correctly. Do not silently substitute your own goals or expand the agreed scope.
|
||||
|
||||
1. Reduce technical uncertainty first. Use read-only repository tools or web search when either can
|
||||
resolve a fact. Do not write or run code in this phase (edit/write are blocked except for the plan
|
||||
file; don't mutate state via bash either).
|
||||
2. Before you draft a goal, identify its object, observable result, scope, and any decision that the
|
||||
human would need to approve later. Ask at least three short, concrete questions that test whether you
|
||||
understand the requested outcome, boundary, and how success will be judged. Inspect files or search the
|
||||
web before asking when either can answer a fact. If the human does not answer a question, record that
|
||||
point as unknown; do not silently replace it with an inference. Do not present the review menu with a
|
||||
placeholder goal such as "work out the thing", "improve it", or "investigate".
|
||||
3. For independent high-impact questions, build a decision tree and ask the whole frontier in one
|
||||
round. Ask only questions worth the human's time, where the answer materially reduces uncertainty
|
||||
human would need to approve later. Briefly reframe the request in your own words to check comprehension
|
||||
and make your understanding visible: the intended outcome, boundary, and success check. Invite correction,
|
||||
but do not require confirmation when these are already clear. Ask questions that expose differences
|
||||
between your understanding and the user's that would otherwise stay hidden. Probe consequential
|
||||
assumptions, challenge inconsistencies, and follow up where an answer exposes a gap. Do not use a question quota or ask the human
|
||||
to approve ordinary implementation details. Inspect files or search the web before asking when either
|
||||
can answer a fact. If the human does not answer a question, record that
|
||||
point as unknown; do not silently replace it with an inference or turn it into a new blocking decision.
|
||||
Do not present the review menu with a placeholder goal such as "work out the thing", "improve it", or
|
||||
"investigate".
|
||||
3. Use questions to clarify and narrow the goal, test your assumptions, and bring your understanding
|
||||
into agreement with the user's. Respect their limited time: batch independent high-impact questions
|
||||
in one short round, where the answer materially reduces uncertainty
|
||||
while discovering the right plan. Each question must be short and self-contained: state the relevant
|
||||
context, use the human's language and ASD-STE100
|
||||
Simple Technical English, and give a recommended answer. Record each answer, or the unanswered
|
||||
unknown, in ## Interview. Do not make the plan final while material user decisions remain open.
|
||||
unknown, in ## Interview. Draft goals and present Ready when the requested work is otherwise executable.
|
||||
Only withhold Ready for an unanswered choice that changes scope, spending, or the user-visible result.
|
||||
4. State the user-visible result before the goals: one concrete sentence naming what the human will
|
||||
inspect when this plan is done. Take it from the original request, not from your implementation plan.
|
||||
Every requested artifact and action must survive into this sentence. An agent-inferred constraint may
|
||||
@@ -145,8 +148,9 @@ export function planningState(planPath: string): string {
|
||||
The plan at ${planPath} is the only file you may change. Use read-only repository tools or web search
|
||||
when either can resolve a fact. Ask the human to confirm unresolved interpretation, outcome, task,
|
||||
scope, or a choice that needs their approval. Batch independent high-impact questions in one short,
|
||||
self-contained round with relevant context and a recommendation. Do not draft a placeholder goal
|
||||
without a concrete object, observable result, settled scope, and required approval. Do not execute
|
||||
self-contained round with relevant context and a recommendation. Record unanswered questions as
|
||||
unknown and still present Ready when the requested work is otherwise executable. Do not draft a
|
||||
placeholder goal without a concrete object, observable result, settled scope, and required approval. Do not execute
|
||||
work, mark a goal [/] or [x], or sign off a goal. The plan is not approved until the human selects
|
||||
Ready.`;
|
||||
}
|
||||
@@ -161,7 +165,7 @@ export function resync(plan: string, planRel: string, why: string): string {
|
||||
<system-reminder>
|
||||
${why} This is the whole plan file (${planRel}), appendix included. You are the implementation worker.
|
||||
Keep the high-level goal and human intent stable and do the work directly. A visible read-only Pi
|
||||
session supervises you through pi-supervise. The human's latest message outranks the plan: if it
|
||||
session supervises you through pi-intercom. The human's latest message outranks the plan: if it
|
||||
changes scope, amend the plan rather than preserving an obsolete decision.
|
||||
|
||||
${plan}
|
||||
@@ -169,22 +173,109 @@ ${plan}
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* 3. completeGoal — SIGN-OFF, agent-side: the one blessed tool
|
||||
* 3. Supervisor orientation: short each review, full at startup/after compaction.
|
||||
* ──────────────────────────────────────────────────────────────────────── */
|
||||
export function supervisorOpening(planPath: string): string {
|
||||
return `Your job is to be a diligent supervisor, autonomously extending the user's agency by correctly understanding their goals and preferences. Supervise the worker according to ${planPath}, which the user helped write. Inspect and diagnose directly. Delegate changes to the worker through SteerWorker; do not take over implementation or alter shared state.`;
|
||||
}
|
||||
|
||||
// Pi/OpenAI: User intent/autonomy adapted from https://www.anthropic.com/constitution; outcome focus from @monotykamary/pi-supervisor.
|
||||
export function supervisorPrompt(planPath: string): string {
|
||||
return `${supervisorOpening(planPath)}
|
||||
|
||||
At startup and after compaction, read the applicable AGENTS.md instructions and relevant skills to understand the user's goals, preferences, and working standards. Do not assume a particular project or workflow. Read the plan's appendices when needed.
|
||||
|
||||
Understand the user's immediate request without interpreting it too literally or too liberally. Consider their final goals and the background standards and preferences the work should meet. Use good planning, taste, context, and high-level perspective. Infer ordinary implementation details, but do not silently replace the agreed outcome or invent restrictions.
|
||||
|
||||
Protect the user's epistemic autonomy and rational agency. Make consequential uncertainty and disagreement visible. Respect their authorized decisions without requiring them to justify reasonable preferences; voice concerns without substituting your preferences for theirs.
|
||||
|
||||
You are the visible pi-goals supervisor for ${planPath}. You are a stronger reviewer with normal Pi tools and extensions. Your inspection-only role is an instruction, not an enforced sandbox: tool availability does not authorize taking over the worker's changes. The other Pi session is the implementation worker and keeps the full conversation. You keep the high-level intent from the compacted planning conversation and worker views. The complete plan at ${planPath} is the source of truth; read it directly after every compaction.
|
||||
|
||||
Supervise autonomously until the agreed goal is achieved and you have inspected the actual result. Use judgment: identify the missing user-visible result, decide the next useful action, and supervise it through to delivery. Approval records support this work; they are not the outcome. Seek justified confidence, not certainty at any cost. Investigate uncertainty with the cheapest useful check, then decide. Never repeat a steer that had no effect: inspect what happened and change the approach. Do not prolong completed work for optional polish.
|
||||
|
||||
The worker stopping is not a reason for you to stop. Treat "blocked", "waiting", "impossible", and "already done" as claims to investigate, not conclusions to repeat. Check the evidence and whether the claimed dependency is real. Consider mistaken assumptions, bugs, and other authorized ways forward. If progress stalls, diagnose why and use SteerWorker to send a useful next instruction instead of repeating status checks. Keep independent work moving when it does not depend on the blocker. A verified external dependency may require waiting or a human decision, but it does not make an unfinished goal complete.
|
||||
|
||||
Keep authorized work moving. Resolve technical choices within the agreed scope yourself. If idle with unfinished goals, use SteerWorker to resume useful work; a recap alone does not restart the worker. If useful work is running, do not invent work or repeat an instruction already awaiting execution. Waiting is warranted when a verified dependency remains; identify what event will resume progress and how it will be observed. Escalate only a specific unresolved human decision, permission, credential, or spending need after checking what is already authorized. Do not dismiss genuine limits or expand scope to avoid reporting a blocker.
|
||||
|
||||
At each review, give a brief visible recap of how work is tracking against the goal: what the evidence shows and your judgment about the next step. Add perspective rather than repeating status. Distinguish observations from guesses. Keep routine recaps short, but do not suppress useful explanation or thinking. Do not edit files or execute the worker's work.
|
||||
|
||||
Ground consequential judgments in verbatim evidence with a source path or link and enough surrounding context to check the interpretation. Keep the observation separate from your inference. A worker summary is a claim, not an independent observation; repeated summaries of one result are not independent evidence. Say what evidence would change your mind. Missing evidence stays unknown until you inspect where it should be.
|
||||
|
||||
Check the actual deliverable against the user's goal. Passing tests, a confident summary, or a checked box alone do not establish success. Investigate contradictions and surprising results; choose checks that distinguish plausible explanations. Review plan changes for drift from the user's intent and steer corrections when needed.
|
||||
|
||||
Only if the evidence establishes completion, use ApproveGoal and direct the worker to CompleteGoal. Otherwise send the next useful instruction with SteerWorker, or explain the verified dependency preventing progress. Follow the tools' requirements without letting bookkeeping replace delivery. Once the agreed work is complete, give a short assessment and stop. -- Pi/OpenAI`;
|
||||
}
|
||||
|
||||
export function supervisorReviewContext(planPath: string, shortPlan: string): string {
|
||||
return `${supervisorOpening(planPath)}\n\nCurrent agreed plan (reread for every review):\n${shortPlan}\n\nJudge progress against this outcome and its discriminators. A completed artifact or task is not completion unless it satisfies the agreed goal.`;
|
||||
}
|
||||
|
||||
export function supervisorOrientation(planPath: string, fullPlan: string): string {
|
||||
return `${supervisorPrompt(planPath)}\n\nFull active plan:\n${fullPlan}`;
|
||||
}
|
||||
|
||||
export function supervisorCompaction(planPath: string, initial: boolean): string {
|
||||
return initial
|
||||
? `Preserve the user's high-level intent, decisions, unresolved risks, and the supervisor's remit. The canonical plan is ${planPath}; it remains available directly and must not be replaced by this summary.`
|
||||
: `Keep the user's high-level intent, current plan state, unresolved risks, approval decisions, and the supervisor's own concise findings. Remove old worker views and implementation detail. The canonical plan remains ${planPath}.`;
|
||||
}
|
||||
|
||||
/* 4. Check-ins: decide whether work is on track, then act when needed. */
|
||||
export type SupervisorReviewReason = "ready" | "settled" | "turns" | "interval" | "started" | "plan";
|
||||
|
||||
export const supervisorReadyReview = "Check the agreed outcome and decide the next useful action. Use SteerWorker to send the worker a concrete starting instruction; do not repeat one already being acted on.";
|
||||
export const supervisorStartedReview = "The worker has begun a turn. Check whether its direction fits the agreed goal; let productive work continue and use SteerWorker only if a correction is needed.";
|
||||
export const supervisorPeriodicReview = "Is the worker on track toward the user's intended outcome? Check for drift, mistaken assumptions, or wasted effort. Use SteerWorker to send a correction where useful; otherwise let productive work continue without interruption.";
|
||||
export const supervisorStoppedReview = "Inspect the results and judge whether the agreed goal is actually achieved. If unfinished, investigate why the worker stopped and use SteerWorker to send the next useful instruction and resume work. If a verified dependency prevents progress, establish what will resume it and how that will be observed. Do not treat stopping as completion. Consider ApproveGoal only after the results satisfy the goal.";
|
||||
export const supervisorPlanChangeReview = "Assess plan changes against the user's intent and preferences. Manual checkbox edits are claims, not proof of completion. Inspect the actual result before accepting a claim; use SteerWorker to send corrections when the plan or work has drifted. Preserve authorized changes.";
|
||||
|
||||
export function supervisorCheckIn(reason: SupervisorReviewReason, idle: boolean): string {
|
||||
// These status prefixes are also read by approval checks; keep them unchanged.
|
||||
const state = reason === "ready" ? "is ready to begin" : idle ? "stopped" : "is still working";
|
||||
const task = reason === "ready" ? supervisorReadyReview : idle ? supervisorStoppedReview : reason === "started" ? supervisorStartedReview : supervisorPeriodicReview;
|
||||
return `The worker ${state}.\n\n${reason === "plan" ? `${supervisorPlanChangeReview}\n\n` : ""}${task}`;
|
||||
}
|
||||
|
||||
export function supervisorPlanReview(claims: string[], changes: string[], diff: string): string {
|
||||
return `${supervisorPlanChangeReview}\nClaims awaiting supervisor judgment: ${claims.join(", ") || "none"}\nGoal-state changes:\n${changes.join("\n") || "none"}\nPlan diff since the previous published view:\n${diff}`;
|
||||
}
|
||||
|
||||
/* 5. Steering: a visible message is an assessment; this tool sends an actionable instruction. */
|
||||
export const steerWorkerDescription = "Send one concrete instruction to the implementation worker. Use it to resume useful work after a stop, request a needed check, or correct drift toward the agreed goal. A recap alone does not send an instruction. Do not interrupt productive work or repeat ineffective steering without changing the approach.";
|
||||
export const steerWorkerInstructionDescription = "The next useful action and its purpose toward the agreed goal; include the check or result needed to assess progress.";
|
||||
export function workerInstructionSent(id: string): string {
|
||||
return `Worker instruction ${id} sent through pi-intercom. Receipt and execution are not confirmed by this result.`;
|
||||
}
|
||||
|
||||
/* 6. Approval: the supervisor's acceptance action AFTER judgment, not a request to judge. */
|
||||
export const approveGoalDescription = "Use only after judging that the actual result satisfies the user's intended outcome and the goal's discriminator. This tool records your acceptance; its mechanical checks cannot establish success. If the goal is unmet or evidence is insufficient, do not approve: use SteerWorker to request the next useful work or check.\n\nRequirements: inspect the current goal, repository, evidence, and a saved nonempty verification-output file, with a current stopped worker view and no active work. force overrides only dirty-worktree rejection and requires a reason; later Git/content changes invalidate approval.";
|
||||
export const approveGoalParameters = {
|
||||
goal: "Exact text after goal: in the plan, whose intended outcome you have judged achieved.",
|
||||
verifyOutputPath: "Nonempty repository-relative file containing the verification output you inspected against the goal's discriminator.",
|
||||
force: "Accept this exact inspected dirty worktree, without bypassing any other approval gate.",
|
||||
reason: "Required with force:true. Why accepting this inspected worktree state is justified.",
|
||||
};
|
||||
export function goalApprovalRecorded(goal: string, forced?: { reason: string; path: string }): string {
|
||||
return `Approval recorded for "${goal}".${forced ? ` Forced worktree acceptance: ${forced.reason}. Exact status and content fingerprints saved in ${forced.path}; changes require fresh review.` : ""} Use SteerWorker to tell the worker to call CompleteGoal with this exact goal text. Continue supervising any remaining goals.`;
|
||||
}
|
||||
|
||||
/* 7. Worker sign-off: consume the supervisor's recorded approval. */
|
||||
export const completeGoalDescription =
|
||||
"Sign off a goal once its discriminator is satisfied. First fill the goal's evidence: list in the " +
|
||||
"Worker-only sign-off after the visible supervisor has judged the goal achieved and recorded approval. " +
|
||||
"If approval is absent, provide the result and evidence for review rather than calling this tool. " +
|
||||
"First fill the goal's evidence: list in the " +
|
||||
"plan file: each item pairs a durable artifact with a short read of it (a quoted+linked log, a " +
|
||||
"table plus how to read it, a metric plus what it shows -- not a bare claim). Quote verbatim from " +
|
||||
"output you actually observed; never reconstruct numbers from memory. If you couldn't see an " +
|
||||
"output, rerun it or write that you couldn't -- an honest gap beats a plausible fabrication. If " +
|
||||
"the goal names a verify: command, direct the worker to run it and save its output to a file cited " +
|
||||
"in the evidence. The supervisor may run an allowed read-only verification command, but must not " +
|
||||
"create the evidence file itself. The visible supervisor must reject a claimed pass with no saved " +
|
||||
"output. The read must show success POSITIVELY happened, not just that failures were avoided. The " +
|
||||
"the goal names a verify: command, run it and save its output to a file cited in the evidence. " +
|
||||
"The visible supervisor reads the actual result and saved output to judge whether the discriminator " +
|
||||
"is satisfied, not merely whether tasks finished or files exist. The read must show success " +
|
||||
"POSITIVELY happened, not just that failures were avoided. The " +
|
||||
"supervisor records an approval checkpoint only after it inspected the current plan, repository, " +
|
||||
"evidence, verify output, and a stopped worker view with no active work. Then the worker calls this " +
|
||||
"tool with the exact goal text. This tool independently checks that checkpoint " +
|
||||
"against the exact current goal block, HEAD/tree, and clean worktree before it appends the sign-off to " +
|
||||
"against the exact current goal block, HEAD/tree, and approved repository state before it appends the sign-off to " +
|
||||
"## Log and ticks the goal [x]. If any check differs, it fails closed and requires a fresh supervisor review.";
|
||||
|
||||
export const completeGoalParamDescription = "The goal's text: the line after 'goal:' in the plan file.";
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
export type ModelRole = "planning" | "worker" | "supervisor";
|
||||
interface Choice { provider: string; id: string }
|
||||
|
||||
// Pi/OpenAI: Per-role model memory, adapted from cecb1e9; preferences stay in the project.
|
||||
export class RoleModels {
|
||||
private role?: ModelRole;
|
||||
private ctx?: ExtensionContext;
|
||||
private switching = false;
|
||||
private stopped = false;
|
||||
|
||||
constructor(private pi: ExtensionAPI) {
|
||||
pi.on("session_shutdown", async () => { this.stopped = true; this.leave(); });
|
||||
pi.on("model_select", async (event) => {
|
||||
if (this.switching || event.source === "restore" || !this.role || !this.ctx) return;
|
||||
this.save(this.role, this.ctx, event.model);
|
||||
});
|
||||
}
|
||||
|
||||
leave(): void { this.role = undefined; }
|
||||
|
||||
async enter(role: ModelRole, ctx: ExtensionContext, useCurrent = false): Promise<void> {
|
||||
if (this.stopped) throw new Error("Role model session ended.");
|
||||
this.role = role;
|
||||
this.ctx = ctx;
|
||||
let choice: Choice | undefined;
|
||||
if (!useCurrent) {
|
||||
try { choice = JSON.parse(readFileSync(this.path(role, ctx), "utf8")); }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
|
||||
}
|
||||
if (choice && (!choice.provider || !choice.id)) throw new Error(`Invalid saved ${role} model.`);
|
||||
const model = choice ? ctx.modelRegistry.find(choice.provider, choice.id) : ctx.model;
|
||||
if (!model) throw new Error(`${role} model is unavailable. Select an available model with /model, then retry. Saved choice was not replaced.`);
|
||||
this.switching = true;
|
||||
try {
|
||||
if (!await this.pi.setModel(model)) throw new Error(`${role} model ${model.provider}/${model.id} is unavailable or unauthenticated. Saved choice was not replaced.`);
|
||||
if (this.stopped) throw new Error("Role model session ended.");
|
||||
this.save(role, ctx, model);
|
||||
ctx.ui.notify(`${role} model: ${model.provider}/${model.id}`, "info");
|
||||
} finally { this.switching = false; }
|
||||
}
|
||||
|
||||
private path(role: ModelRole, ctx: ExtensionContext): string {
|
||||
return join(ctx.cwd, ".pi", "pi-goals", "models", `${role}.json`);
|
||||
}
|
||||
private save(role: ModelRole, ctx: ExtensionContext, model: Choice): void {
|
||||
const path = this.path(role, ctx);
|
||||
mkdirSync(join(ctx.cwd, ".pi", "pi-goals", "models"), { recursive: true });
|
||||
const temporary = `${path}.${process.pid}.tmp`;
|
||||
writeFileSync(temporary, `${JSON.stringify({ provider: model.provider, id: model.id })}\n`);
|
||||
renameSync(temporary, path);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const PAIR_EVENT = "pi-supervise:pair:v1";
|
||||
const WORKER_STATE_EVENT = "pi-supervise:worker-state:v1";
|
||||
const WORKER_PAIRED_EVENT = "pi-supervise:worker-paired:v1";
|
||||
const API_READY_EVENT = "pi-supervise:api-ready:v1";
|
||||
const TIMEOUT_MS = 15_000;
|
||||
export const SUPERVISOR_STARTUP_TIMEOUT_MS = 5 * 60_000;
|
||||
|
||||
type Events = { emit(name: string, value: unknown): boolean; on(name: string, handler: (value: any) => void): void };
|
||||
|
||||
function wait<T>(start: (resolve: (value: T) => void, reject: (error: Error) => void) => void, message: string, timeoutMs = TIMEOUT_MS): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
||||
start((value) => { clearTimeout(timer); resolve(value); }, (error) => { clearTimeout(timer); reject(error); });
|
||||
});
|
||||
}
|
||||
|
||||
export function pairWithPiSupervise(pi: ExtensionAPI, workerIntercomId: string, goal: string): Promise<void> {
|
||||
const events = (pi as unknown as { events: Events }).events;
|
||||
return wait((resolve, reject) => events.emit(PAIR_EVENT, { version: 1, workerIntercomId, goal, resolve, reject }), "pi-supervise did not accept the visible-supervisor pairing request.");
|
||||
}
|
||||
|
||||
export interface WorkerPiSupervise {
|
||||
intercomId: string;
|
||||
waitForPair(timeoutMs?: number): Promise<void>;
|
||||
}
|
||||
|
||||
export function workerPiSupervise(pi: ExtensionAPI, timeoutMs = TIMEOUT_MS): Promise<WorkerPiSupervise> {
|
||||
const events = (pi as unknown as { events: Events }).events;
|
||||
let paired = false;
|
||||
let resolvePair: (() => void) | undefined;
|
||||
events.on(WORKER_PAIRED_EVENT, () => {
|
||||
paired = true;
|
||||
resolvePair?.();
|
||||
});
|
||||
return wait((resolve, reject) => {
|
||||
let resolved = false;
|
||||
const request = () => events.emit(WORKER_STATE_EVENT, (state: { intercomId?: string; paired?: boolean }) => {
|
||||
if (resolved) return;
|
||||
if (!state.intercomId) return reject(new Error("pi-supervise returned no worker intercom ID."));
|
||||
if (state.paired) return reject(new Error("This worker is already paired with a supervisor. Stop that supervision before selecting Ready."));
|
||||
resolved = true;
|
||||
resolve({
|
||||
intercomId: state.intercomId,
|
||||
waitForPair: (pairTimeoutMs = timeoutMs) => paired ? Promise.resolve() : wait((pairResolve) => { resolvePair = pairResolve; }, "The visible supervisor did not pair with this worker.", pairTimeoutMs),
|
||||
});
|
||||
});
|
||||
events.on(API_READY_EVENT, request);
|
||||
request();
|
||||
}, "pi-supervise did not publish this worker's intercom state.", timeoutMs);
|
||||
}
|
||||
+150
-50
@@ -1,17 +1,21 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
import { Text } from "@earendil-works/pi-tui";
|
||||
import { Type } from "typebox";
|
||||
import { approvalPath, goalBlock, hashGoalBlock, repositoryState, verifyOutputPath, writeApproval } from "./approval.js";
|
||||
import { pairWithPiSupervise } from "./supervise.js";
|
||||
import { GoalIntercom } from "./intercom.js";
|
||||
import { planViews } from "./plan-view.js";
|
||||
import { approveGoalDescription, approveGoalParameters, goalApprovalRecorded, steerWorkerDescription, steerWorkerInstructionDescription, supervisorCompaction, supervisorOrientation, supervisorReviewContext, workerInstructionSent } from "./prompts.js";
|
||||
import { RoleModels } from "./role-models.js";
|
||||
|
||||
const BOOTSTRAPPED = "pi-goals-visible-supervisor-v1";
|
||||
const INITIAL_COMPACT_AT_TOKENS = 20_000;
|
||||
const BOOTSTRAPPED = "pi-goals-visible-supervisor-v2";
|
||||
const COMPACT_AT_TOKENS = 100_000;
|
||||
|
||||
interface SupervisorConfig {
|
||||
const ROLE_STATE = "pi-goals-supervisor-binding";
|
||||
|
||||
export interface SupervisorConfig {
|
||||
workerSessionId: string;
|
||||
workerIntercomId: string;
|
||||
ownerSessionId: string;
|
||||
planPath: string;
|
||||
approvalId: string;
|
||||
@@ -30,7 +34,6 @@ function requiredEnv(name: string): string {
|
||||
function config(): SupervisorConfig {
|
||||
return {
|
||||
workerSessionId: requiredEnv("PI_GOALS_WORKER_ID"),
|
||||
workerIntercomId: requiredEnv("PI_GOALS_WORKER_INTERCOM_ID"),
|
||||
ownerSessionId: requiredEnv("PI_GOALS_OWNER_SESSION_ID"),
|
||||
planPath: resolve(requiredEnv("PI_GOALS_PLAN_PATH")),
|
||||
approvalId: requiredEnv("PI_GOALS_APPROVAL_ID"),
|
||||
@@ -48,7 +51,7 @@ function hasEvidenceEntry(block: string): boolean {
|
||||
const childIndent = lines[child].match(/^\s*/)?.[0].length ?? 0;
|
||||
if (lines[child].trim() && childIndent <= indent) break;
|
||||
const entry = /^\s+[-*]\s+(.+?)\s*$/.exec(lines[child]);
|
||||
if (entry?.[1].trim()) return true;
|
||||
if (entry?.[1].trim() && !/^\(empty until sign-off\)$/i.test(entry[1].trim())) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -66,92 +69,199 @@ function latestWorkerView(ctx: ExtensionContext): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function supervisorPrompt(settings: SupervisorConfig): string {
|
||||
return `You are the visible pi-goals supervisor for ${settings.planPath}. You are a stronger, read-only reviewer. The other Pi session is the implementation worker and keeps the full conversation. You keep the high-level intent from the compacted planning conversation and pi-supervise worker views. The complete plan at ${settings.planPath} is the source of truth; read it directly after every compaction.
|
||||
|
||||
Use pi-supervise to inspect and steer the worker. Give one concrete instruction when work is incomplete. Do not edit files. For each open goal, inspect its exact plan block, repository state, cited evidence, and a saved nonempty verification-output file. When its discriminator is positively satisfied and the worker view says no work is active, call ApproveGoal with that repository-relative path. Then call steer and tell the worker to call CompleteGoal with the exact goal text. Do not call done until every plan goal is [x]. -- PI[gpt-5.6-sol]`;
|
||||
}
|
||||
|
||||
export function isVisibleSupervisor(): boolean {
|
||||
return process.env.PI_GOALS_ROLE === "supervisor";
|
||||
}
|
||||
|
||||
export function registerVisibleSupervisor(pi: ExtensionAPI): void {
|
||||
const settings = config();
|
||||
export function restoredSupervisor(entries: readonly { type: string; customType?: string; data?: unknown }[]): SupervisorConfig | undefined {
|
||||
const savedEntry = entries.filter(entry => entry.type === "custom" && entry.customType === ROLE_STATE).at(-1);
|
||||
if (savedEntry) {
|
||||
const saved = savedEntry.data as SupervisorConfig | undefined;
|
||||
if (!saved || ![saved.workerSessionId, saved.ownerSessionId, saved.planPath, saved.approvalId].every(value => typeof value === "string" && value.trim())) throw new Error("Saved supervisor binding is incomplete. Restore its original session metadata before resuming; worker mode was not enabled.");
|
||||
return saved;
|
||||
}
|
||||
const legacyEntry = entries.filter(entry => entry.type === "custom" && entry.customType === BOOTSTRAPPED).at(-1);
|
||||
if (!legacyEntry) return;
|
||||
const legacy = legacyEntry.data as Partial<SupervisorConfig> | undefined;
|
||||
const binding = entries.filter(entry => entry.type === "custom" && entry.customType === "pi-goals-state").map(entry => entry.data as { approvalId?: string }).filter(state => state?.approvalId).at(-1)?.approvalId;
|
||||
if (!legacy || ![legacy.workerSessionId, legacy.planPath, binding].every(value => typeof value === "string" && value.trim())) throw new Error("Saved supervisor role has no complete binding. Restore its original session metadata before resuming. Worker mode was not enabled.");
|
||||
return { workerSessionId: legacy.workerSessionId!, ownerSessionId: legacy.workerSessionId!, planPath: legacy.planPath!, approvalId: binding! };
|
||||
}
|
||||
|
||||
export function registerVisibleSupervisor(pi: ExtensionAPI, restored?: SupervisorConfig): void {
|
||||
const settings = restored ?? config();
|
||||
let compacting = false;
|
||||
let startupTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let startupChecks = 0;
|
||||
pi.on("session_shutdown", async () => { if (startupTimer) clearTimeout(startupTimer); });
|
||||
let repeatFullPrompt = true;
|
||||
pi.on("session_compact", async () => { repeatFullPrompt = true; });
|
||||
let bootstrapping = false;
|
||||
let warnedUnknownUsage = false;
|
||||
let modelError: string | null = null;
|
||||
const intercom = new GoalIntercom(pi);
|
||||
const models = new RoleModels(pi);
|
||||
intercom.onView = (view) => pi.sendUserMessage(view.text, { deliverAs: "followUp" });
|
||||
pi.on("session_compact_failed", async event => {
|
||||
if (!startupTimer) return;
|
||||
clearTimeout(startupTimer);
|
||||
startupTimer = undefined;
|
||||
intercom.failReady(`${event.aborted ? "Supervisor compaction was cancelled" : event.errorMessage ?? "Supervisor compaction failed"}. Use /goals supervise in its session to retry.`);
|
||||
});
|
||||
|
||||
const bootstrap = async (ctx: ExtensionContext): Promise<void> => {
|
||||
if (bootstrapping) return;
|
||||
if (bootstrapping || intercom.ended) return;
|
||||
const entries = ctx.sessionManager.getEntries();
|
||||
if (entries.some((entry: { type?: string; customType?: string }) => entry.type === "custom" && entry.customType === BOOTSTRAPPED)) return;
|
||||
bootstrapping = true;
|
||||
try {
|
||||
await pairWithPiSupervise(pi, settings.workerIntercomId, settings.planPath);
|
||||
pi.appendEntry(BOOTSTRAPPED, { version: 1, workerSessionId: settings.workerSessionId, planPath: settings.planPath });
|
||||
pi.sendUserMessage("Supervision is paired. Inspect the worker and give its next concrete instruction.");
|
||||
if (!entries.some((entry: { type?: string; customType?: string }) => entry.type === "custom" && entry.customType === BOOTSTRAPPED)) {
|
||||
pi.appendEntry(BOOTSTRAPPED, { version: 2, workerSessionId: settings.workerSessionId, planPath: settings.planPath });
|
||||
}
|
||||
intercom.markReady();
|
||||
} catch (error) {
|
||||
ctx.ui.notify(`Supervisor startup failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
||||
}
|
||||
} finally { bootstrapping = false; }
|
||||
};
|
||||
|
||||
const bootstrapAfterInitialCompaction = (ctx: ExtensionContext): void => {
|
||||
if (intercom.ended) return;
|
||||
// A reload does not terminate Pi's in-flight compaction. Never start a competing one.
|
||||
if (!ctx.isIdle()) {
|
||||
if (++startupChecks > 300) { intercom.failReady("Supervisor startup is still busy. Inspect its session, then use /goals supervise to retry."); return; }
|
||||
startupTimer = setTimeout(() => bootstrapAfterInitialCompaction(ctx), 1000);
|
||||
return;
|
||||
}
|
||||
startupTimer = undefined;
|
||||
const tokens = ctx.getContextUsage()?.tokens;
|
||||
if (typeof tokens === "number" && tokens <= INITIAL_COMPACT_AT_TOKENS) {
|
||||
const resumed = ctx.sessionManager.getEntries().some((entry: { type?: string; customType?: string }) => entry.type === "custom" && entry.customType === BOOTSTRAPPED);
|
||||
const latestContext = ctx.sessionManager.getEntries().filter(entry => entry.type === "message" || entry.type === "compaction").at(-1);
|
||||
if (resumed || latestContext?.type === "compaction" || (typeof tokens === "number" && tokens < COMPACT_AT_TOKENS)) {
|
||||
void bootstrap(ctx);
|
||||
return;
|
||||
}
|
||||
compacting = true;
|
||||
ctx.compact({
|
||||
customInstructions: `Preserve the user's high-level intent, decisions, unresolved risks, and the supervisor's remit. The canonical plan is ${settings.planPath}; it remains available directly and must not be replaced by this summary.`,
|
||||
customInstructions: supervisorCompaction(settings.planPath, true),
|
||||
onComplete: () => {
|
||||
compacting = false;
|
||||
if (intercom.ended) return;
|
||||
ctx.ui.notify("Supervisor planning context compacted before work started.", "info");
|
||||
void bootstrap(ctx);
|
||||
},
|
||||
onError: (error) => {
|
||||
compacting = false;
|
||||
if (intercom.ended) return;
|
||||
if (/^(Already compacted|Nothing to compact)/.test(error.message)) { void bootstrap(ctx); return; }
|
||||
intercom.failReady(`Supervisor startup compaction failed: ${error.message}. Use /goals reconnect in the supervisor pane.`);
|
||||
ctx.ui.notify(`Supervisor startup compaction failed: ${error.message}`, "error");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
setImmediate(() => { bootstrapAfterInitialCompaction(ctx); });
|
||||
const start = async (ctx: ExtensionContext): Promise<void> => {
|
||||
if (startupTimer) clearTimeout(startupTimer);
|
||||
startupTimer = undefined;
|
||||
startupChecks = 0;
|
||||
modelError = "Supervisor model restoration is pending.";
|
||||
if (!ctx.sessionManager.getEntries().some(entry => entry.type === "custom" && entry.customType === ROLE_STATE)) pi.appendEntry(ROLE_STATE, settings);
|
||||
intercom.configure(settings.approvalId, "supervisor", ctx);
|
||||
try {
|
||||
await models.enter("supervisor", ctx, process.env.PI_GOALS_MODEL_EXPLICIT === "1");
|
||||
modelError = null;
|
||||
setImmediate(() => { if (!intercom.ended) bootstrapAfterInitialCompaction(ctx); });
|
||||
} catch (error) {
|
||||
modelError = String(error);
|
||||
if (!intercom.ended) {
|
||||
intercom.failReady(`Supervisor paused: ${modelError}. Select /model in its session, then /goals supervise.`);
|
||||
ctx.ui.notify(`Supervisor paused: ${modelError} Select /model, then /goals reconnect.`, "error");
|
||||
}
|
||||
}
|
||||
};
|
||||
pi.on("session_start", async (_event, ctx) => start(ctx));
|
||||
pi.registerCommand("goals", {
|
||||
description: "Reconnect this saved supervisor role and pairing: /goals supervise (or reconnect)",
|
||||
handler: async (args, ctx) => {
|
||||
if (!["reconnect", "supervise"].includes(args.trim())) { ctx.ui.notify("This is the supervisor session. Use /goals supervise here; /goals work and /goals noplan belong to the worker. No role or plan was changed.", "info"); return; }
|
||||
if (!ctx.isIdle() || compacting) { ctx.ui.notify("Wait for the supervisor to settle before reconnecting.", "warning"); return; }
|
||||
await start(ctx);
|
||||
},
|
||||
});
|
||||
|
||||
pi.on("before_agent_start", async (_event, ctx) => {
|
||||
return { systemPrompt: `${ctx.getSystemPrompt()}\n\n${supervisorPrompt(settings)}` };
|
||||
const plan = planViews(readFileSync(settings.planPath, "utf8"));
|
||||
const message = repeatFullPrompt ? { customType: "pi-goals-supervisor-role", content: supervisorOrientation(settings.planPath, plan.long), display: true } : undefined;
|
||||
repeatFullPrompt = false;
|
||||
return {
|
||||
systemPrompt: `${ctx.getSystemPrompt()}\n\n${supervisorReviewContext(settings.planPath, plan.short)}`,
|
||||
...(message ? { message } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
pi.on("agent_settled", async (_event, ctx) => {
|
||||
if (compacting || (ctx.getContextUsage()?.tokens ?? 0) < COMPACT_AT_TOKENS) return;
|
||||
if (compacting) return;
|
||||
const usage = ctx.getContextUsage();
|
||||
if (!usage && !warnedUnknownUsage) {
|
||||
warnedUnknownUsage = true;
|
||||
ctx.ui.notify("Supervisor context usage unavailable; the custom 100k compaction trigger cannot be checked. Pi's default auto-compaction is unchanged.", "warning");
|
||||
}
|
||||
// Pi reports tokens:null after compaction until a fresh assistant usage sample.
|
||||
if (typeof usage?.tokens !== "number" || usage.tokens < COMPACT_AT_TOKENS) return;
|
||||
compacting = true;
|
||||
ctx.compact({
|
||||
customInstructions: `Keep the user's high-level intent, current plan state, unresolved risks, approval decisions, and the supervisor's own concise findings. Remove old worker views and implementation detail.`,
|
||||
customInstructions: supervisorCompaction(settings.planPath, false),
|
||||
onComplete: () => {
|
||||
compacting = false;
|
||||
if (intercom.ended) return;
|
||||
ctx.ui.notify("Supervisor context compacted at 100k tokens.", "info");
|
||||
},
|
||||
onError: (error) => {
|
||||
compacting = false;
|
||||
if (intercom.ended) return;
|
||||
ctx.ui.notify(`Supervisor compaction failed: ${error.message}`, "error");
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "SteerWorker",
|
||||
label: "Steer worker",
|
||||
executionMode: "sequential",
|
||||
description: steerWorkerDescription,
|
||||
parameters: Type.Object({ instruction: Type.String({ description: steerWorkerInstructionDescription }) }),
|
||||
renderCall(args, theme) {
|
||||
return new Text(`${theme.fg("toolTitle", "Supervisor → worker")}\n${args.instruction ?? ""}`, 0, 0);
|
||||
},
|
||||
async execute(_id, params) {
|
||||
if (modelError) return result(`Supervisor paused: ${modelError} Use /model, then /goals reconnect.`, true);
|
||||
const instruction = params.instruction.trim();
|
||||
if (!instruction) return result("A worker instruction cannot be empty.", true);
|
||||
const id = intercom.steer(instruction);
|
||||
return result(workerInstructionSent(id));
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "ApproveGoal",
|
||||
label: "Approve goal",
|
||||
executionMode: "sequential",
|
||||
description: "Record approval after inspecting the current goal, repository, evidence, and a saved nonempty verification-output file, with a stopped worker view and no active work.",
|
||||
description: approveGoalDescription,
|
||||
parameters: Type.Object({
|
||||
goal: Type.String({ description: "Exact text after goal: in the plan." }),
|
||||
verifyOutputPath: Type.String({ description: "Nonempty repository-relative file containing the verification output you inspected." }),
|
||||
goal: Type.String({ description: approveGoalParameters.goal }),
|
||||
verifyOutputPath: Type.String({ description: approveGoalParameters.verifyOutputPath }),
|
||||
force: Type.Optional(Type.Boolean({ description: approveGoalParameters.force })),
|
||||
reason: Type.Optional(Type.String({ description: approveGoalParameters.reason })),
|
||||
}),
|
||||
async execute(_id, params, _signal, _onUpdate, ctx) {
|
||||
if (modelError) return result(`Supervisor paused: ${modelError} Use /model, then /goals reconnect.`, true);
|
||||
const force = params.force === true;
|
||||
const reason = params.reason?.trim();
|
||||
if (force && !reason) return result("Cannot force approval without an explicit nonempty reason for accepting this worktree state.", true);
|
||||
const view = latestWorkerView(ctx);
|
||||
const newest = intercom.latestView;
|
||||
if (!intercom.connected) return result("Cannot approve: worker supervision is disconnected or not ready. Restore the existing connection before review.", true);
|
||||
if (!newest) return result("Cannot approve: no worker view has arrived.", true);
|
||||
if (newest.reason === "started") return result("Cannot approve while the worker is starting or running. Wait for its stopped-worker view.", true);
|
||||
if (view !== newest.text) return result("Cannot approve this older worker view. A newer view is queued for you; finish this response to receive it. Do not ask the worker to generate another handoff merely to refresh this review.", true);
|
||||
if (!view?.startsWith("The worker stopped.")) return result("Cannot approve without a current stopped-worker view.", true);
|
||||
if (!newest.backgroundQuiet) return result("Cannot approve while tracked background work is active or unknown.", true);
|
||||
const pendingTool = view.match(/^tool calls with no result: (?!none$)(.+)$/m);
|
||||
const pendingChild = view.match(/^child pi processes still running: (?!none$)(.+)$/m);
|
||||
if (pendingTool || pendingChild) return result(`Cannot approve while work is active: ${(pendingTool ?? pendingChild)![1]}`, true);
|
||||
@@ -159,11 +269,11 @@ export function registerVisibleSupervisor(pi: ExtensionAPI): void {
|
||||
let repository: ReturnType<typeof repositoryState>;
|
||||
try {
|
||||
plan = readFileSync(settings.planPath, "utf8");
|
||||
repository = repositoryState(ctx.cwd);
|
||||
repository = repositoryState(ctx.cwd, force);
|
||||
} catch (error) {
|
||||
return result(`Cannot inspect approval inputs: ${error instanceof Error ? error.message : String(error)}`, true);
|
||||
}
|
||||
if (!repository.cleanWorktree) return result("Cannot approve with a dirty worktree. Commit the worker changes first.", true);
|
||||
if (!repository.cleanWorktree && !force) return result("Cannot approve with a dirty worktree. Commit only the worker's changes, or inspect preserved changes and use force:true with a reason. Do not commit unrelated changes to satisfy this gate.", true);
|
||||
const block = goalBlock(plan, params.goal);
|
||||
if (!block) return result(`Cannot approve: no unique open goal matches "${params.goal}".`, true);
|
||||
if (!hasEvidenceEntry(block)) return result("Cannot approve without a nonblank evidence entry in the goal block.", true);
|
||||
@@ -171,22 +281,12 @@ export function registerVisibleSupervisor(pi: ExtensionAPI): void {
|
||||
if (!verifiedOutput) return result("Cannot approve without a nonempty repository-relative verification-output file.", true);
|
||||
const path = approvalPath(ctx.cwd, settings.ownerSessionId, params.goal);
|
||||
writeApproval(path, {
|
||||
version: 3,
|
||||
verdict: "accept",
|
||||
approvalId: settings.approvalId,
|
||||
goal: params.goal,
|
||||
planPath: settings.planPath,
|
||||
goalBlockHash: hashGoalBlock(block),
|
||||
repoRoot: repository.repoRoot,
|
||||
head: repository.head,
|
||||
tree: repository.tree,
|
||||
cleanWorktree: true,
|
||||
inspected: { plan: true, repository: true, evidence: true, verifyOutput: true },
|
||||
verifyOutputPath: verifiedOutput,
|
||||
supervisor: { sessionId: ctx.sessionManager.getSessionId(), runId: null },
|
||||
timestamp: new Date().toISOString(),
|
||||
version: 3, verdict: "accept", approvalId: settings.approvalId, goal: params.goal, planPath: settings.planPath,
|
||||
goalBlockHash: hashGoalBlock(block), repoRoot: repository.repoRoot, head: repository.head, tree: repository.tree,
|
||||
cleanWorktree: repository.cleanWorktree, ...(force ? { force: { reason: reason!, worktree: repository.worktree! } } : {}), inspected: { plan: true, repository: true, evidence: true, verifyOutput: true }, verifyOutputPath: verifiedOutput,
|
||||
supervisor: { sessionId: ctx.sessionManager.getSessionId(), runId: null }, timestamp: new Date().toISOString(),
|
||||
});
|
||||
return result(`Approval recorded for "${params.goal}". Now steer the worker to call CompleteGoal.`);
|
||||
return result(goalApprovalRecorded(params.goal, force ? { reason: reason!, path } : undefined));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
// VCC 0.5.0 ships source only; describe its compiler boundary without typechecking upstream internals.
|
||||
declare module "@sting8k/pi-vcc/src/core/summarize" {
|
||||
import type { Message } from "@earendil-works/pi-ai";
|
||||
export interface CompileInput {
|
||||
messages: Message[];
|
||||
previousSummary?: string;
|
||||
fileOps?: { readFiles?: string[]; modifiedFiles?: string[]; createdFiles?: string[] };
|
||||
}
|
||||
export function compile(input: CompileInput): string;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { compile } from "@sting8k/pi-vcc/src/core/summarize";
|
||||
import { type SupervisorReviewReason, supervisorCheckIn } from "./prompts.js";
|
||||
|
||||
export interface SessionBlock {
|
||||
type?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
text?: string;
|
||||
thinking?: string;
|
||||
arguments?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SessionMessage {
|
||||
role?: string;
|
||||
content?: string | SessionBlock[];
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
}
|
||||
|
||||
export interface SessionEntry {
|
||||
id?: string;
|
||||
type?: string;
|
||||
summary?: string;
|
||||
message?: SessionMessage;
|
||||
}
|
||||
|
||||
// VCC drops reasoning. Preserve only two recent tails, in place beside the actions they inform.
|
||||
function recentThinking(messages: SessionMessage[]): SessionMessage[] {
|
||||
let remaining = 2;
|
||||
return messages.map(message => ({
|
||||
...message,
|
||||
// Unanswered/partial calls may have no arguments yet; VCC expects an argument object.
|
||||
content: Array.isArray(message.content) ? message.content.map(block => block.type === "toolCall" ? { ...block, arguments: block.arguments ?? {} } : { ...block }) : message.content,
|
||||
})).reverse().map(message => {
|
||||
if (Array.isArray(message.content)) {
|
||||
for (const block of [...message.content].reverse()) {
|
||||
if (block.type === "thinking" && block.thinking && remaining > 0) {
|
||||
block.type = "text";
|
||||
block.text = `(thinking) ${block.thinking.slice(-400)}`;
|
||||
remaining--;
|
||||
}
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}).reverse();
|
||||
}
|
||||
|
||||
function compiledView(messages: SessionMessage[]): string {
|
||||
// Only role/content/tool fields are read by normalize; Pi usage/provider metadata is irrelevant.
|
||||
const compiled = compile({ messages: recentThinking(messages) as Parameters<typeof compile>[0]["messages"] })
|
||||
.replace(/\n*-*\n*Use `vcc_recall`[\s\S]*$/, "").trim();
|
||||
const separator = compiled.indexOf("\n\n---\n\n");
|
||||
// Keep extracted context and newest actions separately: a long brief must not evict all headers.
|
||||
if (/^\[(Session Goal|Files And Changes|Commits|Outstanding Context|User Preferences)\]/.test(compiled)) {
|
||||
if (separator < 0) return bounded(compiled, 1500);
|
||||
return `${bounded(compiled.slice(0, separator), 1500)}\n\n${bounded(compiled.slice(separator + 7), 4000, true)}`;
|
||||
}
|
||||
return bounded(compiled || (messages.length ? "No overview text retained from these messages." : "No new messages."), 5500, true);
|
||||
}
|
||||
|
||||
function outstandingTools(entries: SessionEntry[]): string[] {
|
||||
const calls = new Map<string, string>();
|
||||
const results = new Set<string>();
|
||||
for (const entry of entries) {
|
||||
for (const block of Array.isArray(entry.message?.content) ? entry.message.content : []) {
|
||||
if (block.type === "toolCall" && block.id) calls.set(block.id, block.name ?? "unknown");
|
||||
}
|
||||
if (entry.message?.role === "toolResult" && entry.message.toolCallId) results.add(entry.message.toolCallId);
|
||||
}
|
||||
return [...calls].filter(([id]) => !results.has(id)).map(([, name]) => name);
|
||||
}
|
||||
|
||||
function bounded(value: string, bytes: number, tail = false): string {
|
||||
if (Buffer.byteLength(JSON.stringify(value)) <= bytes) return value;
|
||||
let size = Math.min(value.length, bytes - 100);
|
||||
while (Buffer.byteLength(JSON.stringify(tail ? value.slice(-size) : value.slice(0, size))) > bytes - 100) size = Math.floor(size * 0.8);
|
||||
const notice = "[truncated; inspect source session]";
|
||||
return tail ? `${notice}\n${value.slice(-size)}` : `${value.slice(0, size)}\n${notice}`;
|
||||
}
|
||||
|
||||
export interface ViewContext {
|
||||
sourceSession: string;
|
||||
latestDirection: string;
|
||||
model: string;
|
||||
contextPercent?: number | null;
|
||||
since?: string;
|
||||
background: string;
|
||||
planReview?: string;
|
||||
}
|
||||
|
||||
export function workerView(entries: SessionEntry[], reason: SupervisorReviewReason, idle: boolean, context: ViewContext): string {
|
||||
const compactAt = entries.map(entry => entry.type).lastIndexOf("compaction");
|
||||
const since = context.since ? entries.findIndex(entry => entry.id === context.since) : -1;
|
||||
const from = since >= compactAt ? since + 1 : compactAt + 1;
|
||||
const fresh = entries.slice(from);
|
||||
const recent = compiledView(fresh.flatMap(entry => entry.type === "message" && entry.message ? [entry.message] : []));
|
||||
const summary = since < compactAt ? entries[compactAt]?.summary : undefined;
|
||||
const outstanding = outstandingTools(entries.slice(compactAt + 1));
|
||||
return `${supervisorCheckIn(reason, idle)}\n\nreview trigger: ${reason}\nsource session: ${bounded(context.sourceSession, 800)}\nworker model: ${bounded(context.model, 300)}${context.contextPercent == null ? "" : `; context used: ${context.contextPercent}%`}\nlatest human direction:\n${bounded(context.latestDirection || "not recorded", 1800)}\ntool calls with no result: ${bounded(outstanding.join(", ") || "none", 500)}\ntracked background work: ${bounded(context.background, 800)}\n\n${context.planReview ? `Plan review:\n${bounded(context.planReview, 1800)}\n\n` : ""}${summary ? `compaction summary (worker account, not independent evidence):\n${bounded(summary, 2500)}\n\n` : ""}new worker overview${since === -1 ? " (initial or reset view)" : " since the last acknowledged view"} (VCC algorithmic compression; local # refs index new messages; tool-result bodies omitted; inspect source for evidence):\n${recent}`;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { chmodSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { goalBlock, hashGoalBlock, repositoryState } from "../src/approval.js";
|
||||
|
||||
it("fingerprints literal unusual paths, binary bytes, symlink targets, modes and deletions", () => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "goals-fingerprint-"));
|
||||
const git = (...args: string[]) => execFileSync("git", args, { cwd });
|
||||
try {
|
||||
git("init", "-q");
|
||||
writeFileSync(join(cwd, "tracked"), "original"); git("add", ".");
|
||||
git("-c", "user.name=test", "-c", "user.email=test@example.com", "commit", "-qm", "base");
|
||||
const path = " white space\nname ";
|
||||
writeFileSync(join(cwd, path), Buffer.from([0, 255, 42]));
|
||||
symlinkSync("absent target", join(cwd, "link"));
|
||||
const first = repositoryState(cwd, true).worktree!;
|
||||
expect(first.files.map(file => file.path)).toContain(path);
|
||||
expect(first.files.find(file => file.path === "link")?.kind).toBe("symlink");
|
||||
writeFileSync(join(cwd, path), Buffer.from([0, 254, 42]));
|
||||
const changed = repositoryState(cwd, true).worktree!;
|
||||
expect(changed.status).toBe(first.status);
|
||||
expect(changed.files).not.toEqual(first.files);
|
||||
chmodSync(join(cwd, path), 0o700);
|
||||
expect(repositoryState(cwd, true).worktree?.files).not.toEqual(changed.files);
|
||||
rmSync(join(cwd, "link")); symlinkSync("other target", join(cwd, "link"));
|
||||
expect(repositoryState(cwd, true).worktree?.files.find(file => file.path === "link")?.contentHash).not.toBe(first.files.find(file => file.path === "link")?.contentHash);
|
||||
rmSync(join(cwd, "tracked"));
|
||||
expect(repositoryState(cwd, true).worktree?.files).toContainEqual({ path: "tracked", kind: "missing" });
|
||||
} finally { rmSync(cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("hashes only the current goal, excluding the log, interview, and their historical goal text", () => {
|
||||
const goal = "1. [ ] goal: output\n - evidence: output.txt\n";
|
||||
const before = `${goal}\n## Log\n- first entry\n`;
|
||||
const after = `${goal}\n## Log\n- later entry\n\n${goal}\n## Interview\n> new notes\n`;
|
||||
expect(goalBlock(before, "output")).toBe(goal.trimEnd());
|
||||
expect(hashGoalBlock(goalBlock(before, "output")!)).toBe(hashGoalBlock(goalBlock(after, "output")!));
|
||||
expect(goalBlock(`${goal}\n## Interview\n> notes`, "output")).toBe(goal.trimEnd());
|
||||
expect(goalBlock(`${goal}2. [ ] goal: second\n - evidence: second.txt`, "output")).toBe(goal.trimEnd());
|
||||
expect(hashGoalBlock(goalBlock(before.replace("output.txt", "changed.txt"), "output")!)).not.toBe(hashGoalBlock(goalBlock(before, "output")!));
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { backgroundState } from "../src/background.js";
|
||||
|
||||
function api(tools: string[], processes?: unknown, subagents?: number) {
|
||||
const bus = new EventEmitter();
|
||||
if (processes !== undefined) bus.on("processes:request:list", request => request.reply(processes));
|
||||
if (subagents !== undefined) bus.on("subagents:rpc:v1:request", request => bus.emit(`subagents:rpc:v1:reply:${request.requestId}`, { requestId: request.requestId, success: true, data: { fleet: { version: 1, totalActive: subagents } } }));
|
||||
return { getAllTools: () => tools.map(name => ({ name })), events: { emit: (name: string, value: unknown) => bus.emit(name, value), on: (name: string, fn: (...args: any[]) => void) => { bus.on(name, fn); return () => { bus.off(name, fn); }; } } } as unknown as ExtensionAPI;
|
||||
}
|
||||
|
||||
it("reports tracked running work, rather than equating idle agent with finished jobs", async () => {
|
||||
const active = await backgroundState(api(["process", "subagent"], [{ name: "generation", status: "running" }], 1));
|
||||
expect(active.quiet).toBe(false);
|
||||
expect(active.description).toContain("processes: 1 (generation)");
|
||||
expect(active.description).toContain("subagents: 1");
|
||||
const finished = await backgroundState(api(["process", "subagent"], [{ status: "exited" }], 0));
|
||||
expect(finished.quiet).toBe(true);
|
||||
});
|
||||
|
||||
it("distinguishes missing providers from an unavailable installed tracker", async () => {
|
||||
expect((await backgroundState(api([]))).quiet).toBe(true);
|
||||
expect(await backgroundState(api(["process"]))).toMatchObject({ quiet: false, description: expect.stringContaining("processes: unknown") });
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const unavailable = backgroundState(api(["subagent"]));
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
expect(await unavailable).toMatchObject({ quiet: false, description: expect.stringContaining("subagents: unknown") });
|
||||
} finally { vi.useRealTimers(); }
|
||||
});
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { GoalIntercom } from "../../src/intercom.js";
|
||||
import { intercomFixture } from "../intercom-fixture.js";
|
||||
|
||||
// Real Pi owns compaction; only the external transport is deterministic in this fixture.
|
||||
export default function compactionDelivery(pi: ExtensionAPI) {
|
||||
const wire = intercomFixture();
|
||||
const link = new GoalIntercom({ ...pi, events: wire.events } as ExtensionAPI);
|
||||
const role = process.env.PI_GOALS_TEST_DELIVERY_ROLE === "worker" ? "worker" : "supervisor";
|
||||
link.onSteer = text => pi.sendUserMessage(`[supervisor] ${text}`, { deliverAs: "steer" });
|
||||
link.onView = view => pi.sendUserMessage(view.text, { deliverAs: "followUp" });
|
||||
pi.on("session_start", async (_event, ctx) => link.configure("compaction-pair", role, ctx, true));
|
||||
pi.on("session_before_compact", async event => {
|
||||
wire.receive({ binding: "compaction-pair", role: role === "worker" ? "supervisor" : "worker", kind: role === "worker" ? "steer" : "view", id: "retained-evidence", text: "Retained evidence arrived during manual compaction.", reason: "settled" });
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
if (process.env.PI_GOALS_TEST_COMPACTION === "cancel") return { cancel: true };
|
||||
if (process.env.PI_GOALS_TEST_COMPACTION === "failure") return; // Local model fails only its summarization request.
|
||||
return { compaction: { summary: "Previous task and result preserved.", firstKeptEntryId: event.preparation.firstKeptEntryId, tokensBefore: event.preparation.tokensBefore } };
|
||||
});
|
||||
}
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { GoalIntercom } from "../../src/intercom.js";
|
||||
import { workerView } from "../../src/worker-view.js";
|
||||
|
||||
export default function worker(pi: ExtensionAPI): void {
|
||||
const link = new GoalIntercom(pi);
|
||||
link.onSteer = instruction => pi.sendUserMessage(`[supervisor] ${instruction}`, { deliverAs: "steer" });
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
link.configure("native-pair-test", "worker", ctx);
|
||||
void link.waitReady(8000).then(() => {
|
||||
link.view(workerView(ctx.sessionManager.getBranch(), "settled", true, {
|
||||
sourceSession: ctx.sessionManager.getSessionFile()!, latestDirection: "Inspect actual outputs.",
|
||||
model: "offline/test", background: "No tracked work in this fixture.",
|
||||
}), "settled", undefined, true);
|
||||
}).catch(error => { if (!link.ended) ctx.ui.notify(String(error), "error"); });
|
||||
});
|
||||
}
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { Type } from "typebox";
|
||||
|
||||
// Auto-discovered by the isolated native supervisor profile, not passed with -e.
|
||||
export default function profileTools(pi: ExtensionAPI) {
|
||||
pi.registerTool({
|
||||
name: "profile_inspection",
|
||||
label: "Profile inspection",
|
||||
description: "Inspect the test profile marker.",
|
||||
parameters: Type.Object({}),
|
||||
async execute() {
|
||||
return { content: [{ type: "text", text: "Profile loaded." }], details: {} };
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -61,3 +61,8 @@ describe("openSubtasks (the widget shows the next action, so the plan IS the tas
|
||||
expect(openSubtasks(plan, active)).not.toContain("write the readme");
|
||||
});
|
||||
});
|
||||
|
||||
it("does not show historical Log subtasks under the last active goal", () => {
|
||||
const plan = "1. [/] goal: current\n - [ ] current task\n\n## Log\n - [ ] historical task\n";
|
||||
expect(openSubtasks(plan, 0)).toEqual(["current task"]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { approvalPath, readApproval } from "../src/approval.js";
|
||||
import { registerWorker as goals } from "../src/index.js";
|
||||
import { registerVisibleSupervisor } from "../src/supervisor-session.js";
|
||||
import { pairedIntercomFixture } from "./paired-intercom-fixture.js";
|
||||
|
||||
const goal = "inspect outputs";
|
||||
const reason = "Inspected preserved notebook edits and historical outputs; unrelated to this goal.";
|
||||
const settle = () => new Promise(resolve => setImmediate(resolve));
|
||||
|
||||
async function setup() {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "goals-force-approval-"));
|
||||
const git = (...args: string[]) => execFileSync("git", args, { cwd, encoding: "utf8" }).trim();
|
||||
writeFileSync(join(cwd, "verify.txt"), "PASS: actual output checked\n");
|
||||
writeFileSync(join(cwd, "notebook.py"), "original\n");
|
||||
git("init", "-q"); git("add", ".");
|
||||
git("-c", "user.name=test", "-c", "user.email=test@example.com", "commit", "-qm", "verified output");
|
||||
const head = git("rev-parse", "HEAD");
|
||||
const planPath = join(cwd, ".pi/plan/worker-v1.md");
|
||||
mkdirSync(join(cwd, ".pi/plan"), { recursive: true });
|
||||
const plan = `1. [ ] goal: ${goal}\n - evidence: verify.txt\n\n## Log\n`;
|
||||
writeFileSync(planPath, plan);
|
||||
const wire = pairedIntercomFixture();
|
||||
function runtime(role: "worker" | "supervisor") {
|
||||
const entries: any[] = role === "worker" ? [{ type: "custom", customType: "pi-goals-state", data: { phase: "working", approvalId: "force-binding", planVersion: 1, supervisorPaneId: "fixture-only" } }] : [];
|
||||
const branch: any[] = [];
|
||||
const hooks = new Map<string, any>();
|
||||
const tools = new Map<string, any>();
|
||||
const ctx = {
|
||||
cwd, hasUI: true, isIdle: vi.fn(() => true), model: { provider: "test", id: "model" },
|
||||
modelRegistry: { find: (provider: string, id: string) => ({ provider, id }) },
|
||||
getSystemPrompt: () => "base", getContextUsage: () => ({ tokens: 10 }),
|
||||
sessionManager: { getSessionId: () => role, getSessionFile: () => join(cwd, `${role}.jsonl`), getEntries: () => entries, getBranch: () => [...entries, ...branch] },
|
||||
ui: { notify: vi.fn(), setWidget: vi.fn(), setStatus: vi.fn(), theme: { fg: (_kind: string, text: string) => text } },
|
||||
};
|
||||
const pi = {
|
||||
events: wire[role].events, getAllTools: vi.fn((): any[] => []), getActiveTools: () => ["read", "ApproveGoal", "SteerWorker"], setActiveTools: () => {},
|
||||
on: (name: string, fn: any) => { const prior = hooks.get(name); hooks.set(name, async (...args: any[]) => { await prior?.(...args); return fn(...args); }); },
|
||||
registerTool: (tool: any) => tools.set(tool.name, tool), registerCommand: () => {}, setModel: async () => true,
|
||||
appendEntry: (customType: string, data: unknown) => entries.push({ type: "custom", customType, data }),
|
||||
sendUserMessage: (text: string) => {
|
||||
const message = { role: "user", content: [{ type: "text", text }] };
|
||||
branch.push({ type: "message", message });
|
||||
void hooks.get("message_start")?.({ message });
|
||||
},
|
||||
};
|
||||
(role === "worker" ? goals : registerVisibleSupervisor)(pi as unknown as ExtensionAPI);
|
||||
return { pi, ctx, hooks, branch, tools };
|
||||
}
|
||||
const worker = runtime("worker");
|
||||
vi.stubEnv("PI_GOALS_WORKER_ID", "worker"); vi.stubEnv("PI_GOALS_OWNER_SESSION_ID", "worker");
|
||||
vi.stubEnv("PI_GOALS_APPROVAL_ID", "force-binding"); vi.stubEnv("PI_GOALS_PLAN_PATH", planPath);
|
||||
const supervisor = runtime("supervisor");
|
||||
await worker.hooks.get("session_start")({}, worker.ctx);
|
||||
await supervisor.hooks.get("session_start")({}, supervisor.ctx);
|
||||
await settle(); await settle();
|
||||
writeFileSync(join(cwd, "notebook.py"), "preserved user edit\n");
|
||||
writeFileSync(join(cwd, "historical output.txt"), "historical result\n");
|
||||
const approve = (params: object = { force: true, reason }) => supervisor.tools.get("ApproveGoal").execute("approve", { goal, verifyOutputPath: "verify.txt", ...params }, undefined, undefined, supervisor.ctx);
|
||||
const complete = () => worker.tools.get("CompleteGoal").execute("complete", { goal }, undefined, undefined, worker.ctx);
|
||||
return {
|
||||
cwd, git, head, planPath, plan, worker, supervisor, approve, complete,
|
||||
checkpoint: approvalPath(cwd, "worker", goal),
|
||||
close: async () => { await worker.hooks.get("session_shutdown")(); await supervisor.hooks.get("session_shutdown")(); vi.unstubAllEnvs(); rmSync(cwd, { recursive: true, force: true }); },
|
||||
};
|
||||
}
|
||||
|
||||
it("force ApproveGoal -> CompleteGoal accepts only the reviewed dirty state without committing or modifying it", async () => {
|
||||
const flow = await setup();
|
||||
try {
|
||||
expect((await flow.approve({})).isError).toBe(true);
|
||||
for (const reason of [undefined, "", " "]) expect((await flow.approve({ force: true, reason })).content[0].text).toContain("nonempty reason");
|
||||
expect(existsSync(flow.checkpoint)).toBe(false);
|
||||
const status = flow.git("status", "--porcelain=v1");
|
||||
expect((await flow.approve()).isError).toBe(false);
|
||||
const record = readApproval(flow.checkpoint)!;
|
||||
expect(record.cleanWorktree).toBe(false);
|
||||
expect(record.force?.reason).toBe(reason);
|
||||
expect(record.force?.worktree.status).toContain("?? historical output.txt\0");
|
||||
expect(record.force?.worktree.files).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ path: "notebook.py", kind: "file", contentHash: expect.stringMatching(/^[a-f0-9]{64}$/) }),
|
||||
expect.objectContaining({ path: "historical output.txt", kind: "file", contentHash: expect.stringMatching(/^[a-f0-9]{64}$/) }),
|
||||
]));
|
||||
expect((await flow.complete()).isError).toBe(false);
|
||||
expect(readFileSync(flow.planPath, "utf8")).toContain(`[x] goal: ${goal}`);
|
||||
expect(readFileSync(join(flow.cwd, "notebook.py"), "utf8")).toBe("preserved user edit\n");
|
||||
expect(readFileSync(join(flow.cwd, "historical output.txt"), "utf8")).toBe("historical result\n");
|
||||
expect(flow.git("rev-parse", "HEAD")).toBe(flow.head);
|
||||
expect(flow.git("status", "--porcelain=v1")).toBe(status);
|
||||
console.log("Force UAT: paired real handlers accepted unchanged tracked + untracked dirty content; Git HEAD and user files stayed unchanged.");
|
||||
} finally { await flow.close(); }
|
||||
});
|
||||
|
||||
it("keeps ordinary clean approval unchanged and rechecks runtime state at forced completion", async () => {
|
||||
const flow = await setup();
|
||||
try {
|
||||
expect((await flow.approve()).isError).toBe(false);
|
||||
flow.worker.pi.getAllTools.mockReturnValue([{ name: "process" }]);
|
||||
expect((await flow.complete()).isError).toBe(true); // Installed tracker is now unavailable.
|
||||
flow.worker.pi.getAllTools.mockReturnValue([]);
|
||||
writeFileSync(join(flow.cwd, "notebook.py"), "original\n");
|
||||
rmSync(join(flow.cwd, "historical output.txt"));
|
||||
expect((await flow.complete()).isError).toBe(true); // Cleaning up also changes the accepted state.
|
||||
expect((await flow.approve({})).isError).toBe(false);
|
||||
expect(readApproval(flow.checkpoint)?.force).toBeUndefined();
|
||||
expect((await flow.complete()).isError).toBe(false);
|
||||
} finally { await flow.close(); }
|
||||
});
|
||||
|
||||
it.each(["tracked content", "untracked content", "new untracked", "deleted untracked", "index only", "index contents", "rename", "HEAD", "goal"])("invalidates forced approval after changed %s", async change => {
|
||||
const flow = await setup();
|
||||
try {
|
||||
expect((await flow.approve()).isError).toBe(false);
|
||||
if (change === "index contents") {
|
||||
flow.git("add", "notebook.py");
|
||||
writeFileSync(join(flow.cwd, "notebook.py"), "separate worktree\n");
|
||||
expect((await flow.approve()).isError).toBe(false);
|
||||
}
|
||||
const status = flow.git("status", "--porcelain=v1");
|
||||
if (change === "tracked content") writeFileSync(join(flow.cwd, "notebook.py"), "changed user edits\n");
|
||||
if (change === "untracked content") writeFileSync(join(flow.cwd, "historical output.txt"), "changed old output\n");
|
||||
if (change === "new untracked") writeFileSync(join(flow.cwd, "another.txt"), "new");
|
||||
if (change === "deleted untracked") rmSync(join(flow.cwd, "historical output.txt"));
|
||||
if (change === "index only") flow.git("add", "notebook.py");
|
||||
if (change === "index contents") {
|
||||
writeFileSync(join(flow.cwd, "notebook.py"), "different staged bytes\n");
|
||||
flow.git("add", "notebook.py");
|
||||
writeFileSync(join(flow.cwd, "notebook.py"), "separate worktree\n");
|
||||
expect(flow.git("status", "--porcelain=v1")).toBe(status);
|
||||
}
|
||||
if (change === "rename") renameSync(join(flow.cwd, "notebook.py"), join(flow.cwd, "renamed.py"));
|
||||
if (change === "HEAD") flow.git("-c", "user.name=test", "-c", "user.email=test@example.com", "commit", "--allow-empty", "-qm", "new head");
|
||||
if (change === "goal") writeFileSync(flow.planPath, flow.plan.replace("evidence: verify.txt", "evidence: different.txt"));
|
||||
if (change.endsWith(" content")) expect(flow.git("status", "--porcelain=v1")).toBe(status);
|
||||
expect((await flow.complete()).isError).toBe(true);
|
||||
expect(readFileSync(flow.planPath, "utf8")).toContain(`[ ] goal: ${goal}`);
|
||||
} finally { await flow.close(); }
|
||||
});
|
||||
|
||||
it.each(["evidence", "verification", "stopped view", "tool call", "unknown tracker", "active tracker"])("force does not bypass the %s gate", async gate => {
|
||||
const flow = await setup();
|
||||
try {
|
||||
if (gate === "evidence") writeFileSync(flow.planPath, flow.plan.replace("evidence: verify.txt", "evidence: (empty until sign-off)"));
|
||||
if (gate === "verification") writeFileSync(join(flow.cwd, "verify.txt"), "");
|
||||
if (gate === "stopped view") { flow.worker.ctx.isIdle.mockReturnValue(false); await flow.worker.hooks.get("agent_start")({}, flow.worker.ctx); }
|
||||
if (gate === "tool call") flow.worker.branch.push({ type: "message", message: { role: "assistant", content: [{ type: "toolCall", id: "pending", name: "edit" }] } });
|
||||
if (gate.endsWith("tracker")) {
|
||||
flow.worker.pi.getAllTools.mockReturnValue([{ name: "process" }]);
|
||||
if (gate === "active tracker") {
|
||||
const emit = flow.worker.pi.events.emit;
|
||||
flow.worker.pi.events.emit = (name, request) => { if (name === "processes:request:list") { request.reply([{ status: "running", name: "existing-job" }]); return true; } return emit(name, request); };
|
||||
}
|
||||
}
|
||||
await flow.worker.hooks.get("agent_settled")({}, flow.worker.ctx); await settle();
|
||||
const response = await flow.approve();
|
||||
expect(response.isError).toBe(true);
|
||||
expect(existsSync(flow.checkpoint)).toBe(false);
|
||||
if (gate.endsWith("tracker")) expect(response.content[0].text).toContain("background work");
|
||||
if (gate === "tool call") expect(response.content[0].text).toContain("work is active: edit");
|
||||
} finally { await flow.close(); }
|
||||
});
|
||||
+467
-33
@@ -1,24 +1,28 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { approvalPath, goalBlock, hashGoalBlock, repositoryState, writeApproval } from "../src/approval.js";
|
||||
import { GoalIntercom } from "../src/intercom.js";
|
||||
import { intercomFixture } from "./intercom-fixture.js";
|
||||
import { pairedIntercomFixture } from "./paired-intercom-fixture.js";
|
||||
|
||||
const openSupervisorPane = vi.fn(async () => "pane-2");
|
||||
const closeSupervisorPane = vi.fn(async () => undefined);
|
||||
const shutdowns: Array<() => Promise<void>> = [];
|
||||
vi.mock("../src/herdr.js", () => ({ openSupervisorPane, closeSupervisorPane }));
|
||||
const { default: piGoalsExtension, isMainSession } = await import("../src/index.js");
|
||||
const { registerWorker: piGoalsExtension, isMainSession } = await import("../src/index.js");
|
||||
|
||||
function setup(selectChoices: Array<string | undefined>, editorChoices: Array<string | undefined> = []) {
|
||||
function setup(selectChoices: Array<string | undefined>, editorChoices: Array<string | undefined> = [], events?: ExtensionAPI["events"]) {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-flow-"));
|
||||
writeFileSync(join(cwd, ".gitignore"), ".pi/\n");
|
||||
writeFileSync(join(cwd, "verify.txt"), "PASS\n");
|
||||
execFileSync("git", ["init", "-q"], { cwd });
|
||||
execFileSync("git", ["add", ".gitignore", "verify.txt"], { cwd });
|
||||
execFileSync("git", ["-c", "user.name=test", "-c", "user.email=test@example.com", "commit", "-qm", "initial"], { cwd });
|
||||
const transport = intercomFixture();
|
||||
const commands = new Map<string, any>();
|
||||
const hooks = new Map<string, any>();
|
||||
const tools = new Map<string, any>();
|
||||
@@ -28,11 +32,16 @@ function setup(selectChoices: Array<string | undefined>, editorChoices: Array<st
|
||||
const ctx = {
|
||||
cwd,
|
||||
hasUI: true,
|
||||
isIdle: vi.fn(() => true),
|
||||
getSystemPrompt: () => "base prompt",
|
||||
getContextUsage: () => ({ percent: 25 }),
|
||||
model: { provider: "test", id: "tiny" },
|
||||
modelRegistry: { find: (provider: string, id: string) => ({ provider, id }) },
|
||||
sessionManager: {
|
||||
getSessionId: () => "session-a",
|
||||
getSessionFile: () => join(cwd, "session.jsonl"),
|
||||
getEntries: () => entries,
|
||||
getBranch: () => [],
|
||||
},
|
||||
ui: {
|
||||
theme: { fg: (_kind: string, text: string) => text },
|
||||
@@ -43,24 +52,24 @@ function setup(selectChoices: Array<string | undefined>, editorChoices: Array<st
|
||||
editor: async () => editorChoices.shift(),
|
||||
},
|
||||
};
|
||||
const events = new EventEmitter();
|
||||
events.on("pi-supervise:worker-state:v1", (reply) => reply({ intercomId: "worker-intercom" }));
|
||||
openSupervisorPane.mockImplementation(async () => {
|
||||
queueMicrotask(() => events.emit("pi-supervise:worker-paired:v1", { supervisorIntercomId: "supervisor-intercom" }));
|
||||
return "pane-2";
|
||||
});
|
||||
openSupervisorPane.mockImplementation(async () => "pane-2");
|
||||
const pi = {
|
||||
events,
|
||||
events: events ?? transport.events,
|
||||
registerCommand: (name: string, command: any) => commands.set(name, command),
|
||||
on: (name: string, handler: any) => hooks.set(name, handler),
|
||||
appendEntry: (customType: string, data: unknown) => entries.push({ type: "custom", customType, data }),
|
||||
on: (name: string, handler: any) => {
|
||||
const prior = hooks.get(name);
|
||||
hooks.set(name, async (...args: any[]) => { await prior?.(...args); return handler(...args); });
|
||||
},
|
||||
appendEntry: (customType: string, data: unknown) => { if (customType === "pi-goals-state") entries.push({ type: "custom", customType, data }); },
|
||||
registerTool: (tool: any) => tools.set(tool.name, tool),
|
||||
getAllTools: () => [],
|
||||
setModel: vi.fn(async () => true),
|
||||
sendMessage: (message: { content: string; display?: boolean }) => messages.push(message),
|
||||
sendUserMessage: (content: string) => messages.push({ content }),
|
||||
};
|
||||
piGoalsExtension(pi as unknown as ExtensionAPI);
|
||||
return { commands, ctx, cwd, entries, events, hooks, messages, notifications, tools };
|
||||
shutdowns.push(() => hooks.get("session_shutdown")());
|
||||
return { pi, commands, ctx, cwd, entries, hooks, messages, notifications, tools, transport };
|
||||
}
|
||||
|
||||
function writePlan(cwd: string, content: string): string {
|
||||
@@ -74,12 +83,117 @@ function approvedPlan(cwd: string): string {
|
||||
return writePlan(cwd, "# Plan\n\n## Goals\n\n1. [ ] goal: make the file\n - discriminator: output exists\n - evidence:\n - `result.txt`: contains ok\n\n## Log\n");
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
for (const shutdown of shutdowns.splice(0)) await shutdown();
|
||||
vi.useRealTimers();
|
||||
openSupervisorPane.mockClear();
|
||||
closeSupervisorPane.mockClear();
|
||||
});
|
||||
|
||||
describe("/goals flow", () => {
|
||||
it("reports idleness and keeps manual completion claims supervised without reverting edits", async () => {
|
||||
vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] });
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("make the file", flow.ctx);
|
||||
const path = approvedPlan(flow.cwd);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
const views = () => flow.transport.sent.filter(message => message.kind === "view");
|
||||
await vi.advanceTimersByTimeAsync(60 * 60_000);
|
||||
expect(views().at(-1)?.text).toMatch(/^The worker stopped\./);
|
||||
writeFileSync(path, readFileSync(path, "utf8").replace("[ ] goal:", "[/] goal:"));
|
||||
flow.ctx.isIdle.mockReturnValue(false);
|
||||
await flow.hooks.get("agent_start")({}, flow.ctx);
|
||||
expect(views().at(-1)?.text).toMatch(/^The worker is still working\./);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
expect(views().at(-1)?.text).toMatch(/^The worker is still working\./);
|
||||
flow.ctx.isIdle.mockReturnValue(true);
|
||||
writeFileSync(path, readFileSync(path, "utf8").replace("[/] goal:", "[x] goal:"));
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
const count = views().length;
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "working", signedOffGoals: [] });
|
||||
expect(views().at(-1)?.text).toContain("make the file: [/] -> [x]; manual completion claim, no CompleteGoal sign-off recorded");
|
||||
expect(views().at(-1)?.text).toContain("use SteerWorker to send the next useful instruction and resume work");
|
||||
expect(views().at(-1)?.text).toContain("Manual checkbox edits are claims, not proof of completion");
|
||||
expect(readFileSync(path, "utf8")).toContain("[x] goal:");
|
||||
expect(flow.ctx.ui.setStatus).toHaveBeenLastCalledWith("pi-goals", expect.stringContaining("0/1 goals · 1 claimed, awaiting review"));
|
||||
expect(flow.ctx.ui.setWidget).toHaveBeenLastCalledWith("pi-goals-widget", [expect.stringContaining("claimed complete; awaiting supervisor review")]);
|
||||
const binding = (flow.entries.at(-1)?.data as any).approvalId;
|
||||
flow.transport.receive({ binding, role: "supervisor", kind: "steer", id: "review-claim", text: "Reopen the goal; verify the missing output first." });
|
||||
expect(flow.messages.at(-1)?.content).toBe("[supervisor] Reopen the goal; verify the missing output first.");
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "working", signedOffGoals: [] });
|
||||
await vi.advanceTimersByTimeAsync(60 * 60_000);
|
||||
expect(views().length).toBeGreaterThan(count);
|
||||
} finally {
|
||||
await flow.hooks.get("session_shutdown")();
|
||||
vi.useRealTimers();
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
it("wakes review for an external non-checkbox plan edit, including atomic replacement", async () => {
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("make the file", flow.ctx);
|
||||
const path = approvedPlan(flow.cwd);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
const before = readFileSync(path, "utf8");
|
||||
writeFileSync(`${path}.tmp`, before.replace("output exists", "output contains exact required bytes"));
|
||||
renameSync(`${path}.tmp`, path);
|
||||
await vi.waitFor(() => {
|
||||
const view = flow.transport.sent.filter(message => message.kind === "view").at(-1);
|
||||
expect(view?.reason).toBe("plan");
|
||||
expect(view?.text).toContain("Assess plan changes against the user's intent and preferences");
|
||||
expect(view?.text).toContain("- - discriminator: output exists");
|
||||
expect(view?.text).toContain("+ - discriminator: output contains exact required bytes");
|
||||
});
|
||||
const count = flow.transport.sent.length;
|
||||
await flow.hooks.get("session_shutdown")();
|
||||
writeFileSync(path, before);
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
expect(flow.transport.sent).toHaveLength(count);
|
||||
} finally { await flow.hooks.get("session_shutdown")(); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("coalesces active-worker plan edits into its settled review", async () => {
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("make the file", flow.ctx);
|
||||
const path = approvedPlan(flow.cwd);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
flow.ctx.isIdle.mockReturnValue(false);
|
||||
const before = readFileSync(path, "utf8");
|
||||
writeFileSync(path, before.replace("output exists", "intermediate discriminator"));
|
||||
writeFileSync(path, before.replace("output exists", "final discriminator"));
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
expect(flow.transport.sent.filter(message => message.kind === "view" && message.reason === "plan")).toHaveLength(0);
|
||||
flow.ctx.isIdle.mockReturnValue(true);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
const view = flow.transport.sent.filter(message => message.kind === "view").at(-1);
|
||||
expect(view?.text).toContain("- - discriminator: output exists");
|
||||
expect(view?.text).toContain("+ - discriminator: final discriminator");
|
||||
expect(view?.text).not.toContain("intermediate discriminator");
|
||||
} finally { await flow.hooks.get("session_shutdown")(); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("restores sign-off markers but clears one when a goal is reopened", async () => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
const path = writePlan(flow.cwd, "1. [x] goal: first\n2. [ ] goal: second\n");
|
||||
flow.entries.push({ type: "custom", customType: "pi-goals-state", data: { phase: "working", approvalId: "binding", planVersion: 1, signedOffGoals: ["first"], previousPlan: readFileSync(path, "utf8") } });
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
expect(flow.ctx.ui.setStatus).toHaveBeenLastCalledWith("pi-goals", expect.stringContaining("1/2 goals"));
|
||||
writeFileSync(path, "1. [/] goal: first\n2. [ ] goal: second\n");
|
||||
await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ signedOffGoals: [] });
|
||||
writeFileSync(path, "1. [x] goal: first\n2. [ ] goal: second\n");
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
expect(flow.ctx.ui.setStatus).toHaveBeenLastCalledWith("pi-goals", expect.stringContaining("0/2 goals · 1 claimed, awaiting review"));
|
||||
} finally { await flow.hooks.get("session_shutdown")(); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("preserves drafts, records the interview, and keeps planning read-only", async () => {
|
||||
const flow = setup(["Refine"], ["Keep two columns."]);
|
||||
try {
|
||||
@@ -110,9 +224,8 @@ describe("/goals flow", () => {
|
||||
cwd: flow.cwd,
|
||||
sourceSessionFile: join(flow.cwd, "session.jsonl"),
|
||||
workerSessionId: "session-a",
|
||||
workerIntercomId: "worker-intercom",
|
||||
planPath,
|
||||
}));
|
||||
}), expect.any(Function));
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "working", supervisorPaneId: "pane-2" });
|
||||
expect(flow.messages.at(-1)?.content).toBe("The plan is approved. Begin implementation as the worker.");
|
||||
const prompt = await flow.hooks.get("before_agent_start")({}, flow.ctx);
|
||||
@@ -123,35 +236,34 @@ describe("/goals flow", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns to planning when the worker is already paired", async () => {
|
||||
it("starts work only after the supervisor launcher resolves", async () => {
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
flow.events.removeAllListeners("pi-supervise:worker-state:v1");
|
||||
flow.events.on("pi-supervise:worker-state:v1", (reply) => reply({ intercomId: "worker-intercom", paired: true }));
|
||||
let ready: (() => void) | undefined;
|
||||
openSupervisorPane.mockImplementationOnce(() => new Promise((resolve) => { ready = () => resolve("pane-2"); }));
|
||||
await flow.commands.get("goals").handler("make the file", flow.ctx);
|
||||
approvedPlan(flow.cwd);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
expect(openSupervisorPane).not.toHaveBeenCalled();
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning", supervisorPaneId: null });
|
||||
expect(flow.notifications.at(-1)).toContain("already paired");
|
||||
const starting = flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning" });
|
||||
ready!();
|
||||
await starting;
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "working", supervisorPaneId: "pane-2" });
|
||||
} finally {
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("waits for the worker's real paired acknowledgement before beginning work", async () => {
|
||||
it("delivers an Intercom instruction to the worker", async () => {
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
openSupervisorPane.mockImplementationOnce(async () => "pane-2");
|
||||
await flow.commands.get("goals").handler("make the file", flow.ctx);
|
||||
approvedPlan(flow.cwd);
|
||||
const ready = flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning" });
|
||||
expect(flow.messages.some((message) => message.content === "The plan is approved. Begin implementation as the worker.")).toBe(false);
|
||||
flow.events.emit("pi-supervise:worker-paired:v1", { supervisorIntercomId: "supervisor-intercom" });
|
||||
await ready;
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "working", supervisorPaneId: "pane-2" });
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
const binding = (flow.entries.at(-1)?.data as { approvalId: string }).approvalId;
|
||||
flow.transport.receive({ binding, role: "supervisor", kind: "steer", id: "steer-1", text: "Run the focused test." });
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
expect(flow.messages.some((message) => message.content === "[supervisor] Run the focused test.")).toBe(true);
|
||||
} finally {
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
@@ -163,7 +275,14 @@ describe("/goals flow", () => {
|
||||
await flow.commands.get("goals").handler("make the file", flow.ctx);
|
||||
const planPath = approvedPlan(flow.cwd);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
const binding = (flow.entries.at(-1)?.data as any).approvalId;
|
||||
await flow.commands.get("goals").handler("clear", flow.ctx);
|
||||
const messageCount = flow.messages.length;
|
||||
flow.transport.receive({ binding, role: "supervisor", kind: "steer", id: "late-cleared", text: "Obsolete instruction." });
|
||||
await flow.commands.get("goals").handler("restart", flow.ctx);
|
||||
await flow.commands.get("goals").handler("reconnect", flow.ctx);
|
||||
expect(flow.messages).toHaveLength(messageCount);
|
||||
expect(openSupervisorPane).toHaveBeenCalledTimes(1);
|
||||
expect(closeSupervisorPane).toHaveBeenCalledWith("pane-2");
|
||||
expect(readFileSync(planPath, "utf8")).toContain("make the file");
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: null, supervisorPaneId: null, planVersion: null });
|
||||
@@ -191,9 +310,28 @@ describe("/goals flow", () => {
|
||||
verifyOutputPath: "verify.txt",
|
||||
supervisor: { sessionId: "supervisor", runId: null }, timestamp: new Date().toISOString(),
|
||||
});
|
||||
writeFileSync(planPath, `${plan}- Appended manual log after approval.\n1. [ ] goal: make the file\n2. [ ] goal: historical only\n`);
|
||||
const controller = new AbortController();
|
||||
const beforeCancel = readFileSync(planPath, "utf8");
|
||||
const cancelled = flow.tools.get("CompleteGoal").execute("cancelled", { goal }, controller.signal, undefined, flow.ctx);
|
||||
controller.abort(); // Cancel while the background-state lookup yields.
|
||||
expect((await cancelled).isError).toBe(true);
|
||||
expect(readFileSync(planPath, "utf8")).toBe(beforeCancel);
|
||||
expect((await flow.tools.get("CompleteGoal").execute("already-cancelled", { goal }, controller.signal, undefined, flow.ctx)).isError).toBe(true);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "working", signedOffGoals: [] });
|
||||
const signed = await flow.tools.get("CompleteGoal").execute("id", { goal }, undefined, undefined, flow.ctx);
|
||||
expect(signed.isError).toBe(false);
|
||||
expect(readFileSync(planPath, "utf8")).toContain("1. [x] goal: make the file");
|
||||
expect(readFileSync(planPath, "utf8")).toContain("1. [ ] goal: make the file");
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: null, signedOffGoals: [goal] });
|
||||
expect(flow.ctx.ui.setWidget).toHaveBeenLastCalledWith("pi-goals-widget", ["✔ complete"]);
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
expect(flow.ctx.ui.setWidget).toHaveBeenLastCalledWith("pi-goals-widget", ["✔ complete"]);
|
||||
const completedViews = flow.transport.sent.filter(message => message.kind === "view").length;
|
||||
flow.transport.receive({ binding: approvalId, role: "supervisor", kind: "hello", id: "hello", ready: true });
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
expect(flow.transport.sent.filter(message => message.kind === "view")).toHaveLength(completedViews);
|
||||
} finally {
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
@@ -206,3 +344,299 @@ describe("process role", () => {
|
||||
expect(isMainSession(true)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function restoredPlan(flow: ReturnType<typeof setup>, phase: "working" | "planning" = "working") {
|
||||
const path = approvedPlan(flow.cwd);
|
||||
flow.entries.push({ type: "custom", customType: "pi-goals-state", data: { phase, approvalId: "restored-binding", supervisorPaneId: "owned-pane", planVersion: 1 } });
|
||||
return path;
|
||||
}
|
||||
|
||||
it.each(["working", "planning"] as const)("restores %s linkage even when its remembered model is unavailable, and supports explicit recovery", async phase => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
const path = restoredPlan(flow, phase);
|
||||
const plan = readFileSync(path, "utf8");
|
||||
const role = phase === "working" ? "worker" : "planning";
|
||||
mkdirSync(join(flow.cwd, ".pi/pi-goals/models"), { recursive: true });
|
||||
const modelPath = join(flow.cwd, `.pi/pi-goals/models/${role}.json`);
|
||||
writeFileSync(modelPath, JSON.stringify({ provider: "gone", id: "expired" }));
|
||||
flow.ctx.modelRegistry.find = vi.fn().mockReturnValue(undefined);
|
||||
await expect(flow.hooks.get("session_start")({}, flow.ctx)).resolves.toBeUndefined();
|
||||
expect(flow.pi.setModel).not.toHaveBeenCalled();
|
||||
expect(readFileSync(modelPath, "utf8")).toContain("expired");
|
||||
expect(flow.transport.sent.filter(message => message.kind === "hello" && message.ready)).toHaveLength(0);
|
||||
expect(flow.ctx.ui.setStatus).toHaveBeenLastCalledWith("pi-goals", "goals paused");
|
||||
if (phase === "working") expect(flow.transport.sent).toContainEqual(expect.objectContaining({ kind: "hello", binding: "restored-binding" }));
|
||||
expect((await flow.hooks.get("tool_call")({ toolName: "edit", input: { path: "code.ts" } }, flow.ctx)).block).toBe(true);
|
||||
expect(await flow.hooks.get("tool_call")({ toolName: "read", input: { path: "code.ts" } }, flow.ctx)).toBeUndefined();
|
||||
expect(await flow.hooks.get("input")({ source: "interactive", text: "Why are we paused?" }, flow.ctx)).toBeUndefined();
|
||||
const signoff = await flow.tools.get("CompleteGoal").execute("id", { goal: "make the file" }, undefined, undefined, flow.ctx);
|
||||
expect(signoff.isError).toBe(true);
|
||||
flow.ctx.modelRegistry.find = (provider, id) => ({ provider, id });
|
||||
await flow.hooks.get("model_select")({ source: "set", model: { provider: "test", id: "chosen" } }, flow.ctx);
|
||||
await flow.commands.get("goals").handler("reconnect", flow.ctx);
|
||||
expect(flow.pi.setModel).toHaveBeenLastCalledWith({ provider: "test", id: "chosen" });
|
||||
expect(openSupervisorPane).not.toHaveBeenCalled();
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase, approvalId: "restored-binding", planVersion: 1 });
|
||||
const injection = await flow.hooks.get("before_agent_start")({}, flow.ctx);
|
||||
if (phase === "planning") expect(injection.message.content).toContain(path);
|
||||
else expect(injection.systemPrompt).toContain("implementation worker");
|
||||
// Human diagnostic input is retained in the planning interview, never discarded by recovery.
|
||||
expect(readFileSync(path, "utf8")).toContain(plan.trim());
|
||||
} finally { rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("shows a missing resumed supervisor, pauses writes, and automatically unpauses when that peer returns", async () => {
|
||||
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval"] });
|
||||
const flow = setup([]);
|
||||
try {
|
||||
restoredPlan(flow);
|
||||
flow.transport.replyToHello(false);
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
expect(flow.ctx.ui.setStatus).toHaveBeenLastCalledWith("pi-goals", "goals paused");
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(flow.notifications.some(text => text.includes("/goals restart"))).toBe(false);
|
||||
await vi.advanceTimersByTimeAsync(240_000);
|
||||
expect(flow.notifications.some(text => text.includes("/goals restart"))).toBe(true);
|
||||
expect((await flow.hooks.get("tool_call")({ toolName: "write", input: { path: "code.ts" } }, flow.ctx)).terminate).toBe(true);
|
||||
expect(await flow.hooks.get("tool_call")({ toolName: "bash", input: { command: "git status" } }, flow.ctx)).toBeUndefined();
|
||||
flow.transport.receive({ binding: "restored-binding", role: "supervisor", kind: "hello", id: "hello", ready: true });
|
||||
expect(flow.ctx.ui.setStatus).toHaveBeenLastCalledWith("pi-goals", expect.stringContaining("supervised"));
|
||||
expect(await flow.hooks.get("tool_call")({ toolName: "write", input: { path: "code.ts" } }, flow.ctx)).toBeUndefined();
|
||||
expect(openSupervisorPane).not.toHaveBeenCalled();
|
||||
} finally { rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("exits planning without deleting the draft or approving implementation", async () => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("draft", flow.ctx);
|
||||
const path = approvedPlan(flow.cwd);
|
||||
const before = readFileSync(path, "utf8");
|
||||
await flow.commands.get("goals").handler("noplan", flow.ctx);
|
||||
expect(readFileSync(path, "utf8")).toBe(before);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: null, planVersion: 1 });
|
||||
expect(openSupervisorPane).not.toHaveBeenCalled();
|
||||
expect(flow.messages.some(message => message.content.includes("Begin implementation"))).toBe(false);
|
||||
expect(await flow.hooks.get("tool_call")({ toolName: "write", input: { path: "arbitrary.txt" } }, flow.ctx)).toBeUndefined();
|
||||
await flow.commands.get("goals").handler("work", flow.ctx);
|
||||
expect(flow.notifications.at(-1)).toContain("No approved worker pairing");
|
||||
await flow.commands.get("goals").handler("supervise", flow.ctx);
|
||||
expect(flow.notifications.at(-1)).toContain("worker session");
|
||||
expect(openSupervisorPane).not.toHaveBeenCalled();
|
||||
} finally { rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("publishes one fresh view after an accepted-view peer reload, but never after clear", async () => {
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("draft", flow.ctx);
|
||||
const path = approvedPlan(flow.cwd);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
const views = () => flow.transport.sent.filter(message => message.kind === "view");
|
||||
const original = views().at(-1)!;
|
||||
flow.transport.receive({ binding: original.binding, role: "supervisor", kind: "received", id: original.id });
|
||||
flow.transport.event({ type: "session_left", sessionId: "peer" });
|
||||
writeFileSync(path, readFileSync(path, "utf8").replace("output exists", "current result must exist"));
|
||||
const previousIds = new Set(views().map(view => view.id));
|
||||
flow.transport.receive({ binding: original.binding, role: "supervisor", kind: "hello", id: "hello", ready: true, reply: true });
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
expect(views().filter(view => !previousIds.has(view.id))).toHaveLength(1);
|
||||
expect(views().at(-1)!.id).not.toBe(original.id);
|
||||
expect(views().at(-1)!.text).toContain("The worker stopped.");
|
||||
expect(views().at(-1)!.text).toContain("current result must exist");
|
||||
await flow.commands.get("goals").handler("clear", flow.ctx);
|
||||
const afterClear = views().length;
|
||||
flow.transport.receive({ binding: original.binding, role: "supervisor", kind: "hello", id: "hello", ready: true });
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
expect(views()).toHaveLength(afterClear);
|
||||
} finally { rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("reconnects an approved worker with work without making a new pairing", async () => {
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("draft", flow.ctx);
|
||||
approvedPlan(flow.cwd);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
const state = flow.entries.at(-1)?.data as any;
|
||||
await flow.commands.get("goals").handler("work", flow.ctx);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ approvalId: state.approvalId, phase: "working" });
|
||||
expect(openSupervisorPane).toHaveBeenCalledTimes(1);
|
||||
} finally { rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it.each(["launch", "model"])("rejects plan content changes during Ready %s without replacing its pane", async (stage) => {
|
||||
const flow = setup(["Ready", "Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("make the file", flow.ctx);
|
||||
const path = approvedPlan(flow.cwd);
|
||||
const mutate = () => writeFileSync(path, readFileSync(path, "utf8").replace("make the file", "make a different report"));
|
||||
if (stage === "launch") openSupervisorPane.mockImplementationOnce(async () => { mutate(); return "pane-2"; });
|
||||
else flow.pi.setModel.mockImplementationOnce(async () => { mutate(); return true; });
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning", supervisorPaneId: "pane-2" });
|
||||
expect(flow.messages.some(message => message.content.includes("Begin implementation"))).toBe(false);
|
||||
expect(flow.notifications.join("\n")).toContain("plan changed after Ready");
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
expect(openSupervisorPane).toHaveBeenCalledTimes(1);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "working" });
|
||||
} finally { rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("allows five minutes for stale Ready retries, without replacing the pane automatically", async () => {
|
||||
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval"] });
|
||||
const flow = setup(["Ready", "Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("make the file", flow.ctx);
|
||||
approvedPlan(flow.cwd);
|
||||
flow.transport.replyToHello(false);
|
||||
openSupervisorPane.mockImplementationOnce(async (_input: any, opened: any) => { opened("failed-pane"); throw new Error("pane run failed"); });
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
expect(flow.notifications.at(-1)).toContain("failed-pane");
|
||||
const retry = flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning" });
|
||||
await vi.advanceTimersByTimeAsync(240_000);
|
||||
await retry;
|
||||
expect(openSupervisorPane).toHaveBeenCalledTimes(1);
|
||||
expect(closeSupervisorPane).not.toHaveBeenCalled();
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning", supervisorPaneId: "failed-pane" });
|
||||
} finally { rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("explicitly restarts only the tracked pane, keeps the plan, and invalidates old approval binding", async () => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
const path = restoredPlan(flow);
|
||||
const before = readFileSync(path, "utf8");
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
const checkpoint = approvalPath(flow.cwd, "session-a", "make the file");
|
||||
mkdirSync(join(flow.cwd, ".pi/pi-goals/approvals"), { recursive: true });
|
||||
writeFileSync(checkpoint, "old checkpoint");
|
||||
await flow.commands.get("goals").handler("restart", flow.ctx);
|
||||
expect(closeSupervisorPane).toHaveBeenCalledExactlyOnceWith("owned-pane");
|
||||
expect(openSupervisorPane).toHaveBeenCalledTimes(1);
|
||||
expect(readFileSync(path, "utf8")).toBe(before);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "working", planVersion: 1 });
|
||||
expect((flow.entries.at(-1)?.data as any).approvalId).not.toBe("restored-binding");
|
||||
expect(() => readFileSync(checkpoint)).toThrow();
|
||||
await flow.commands.get("goals").handler("clear", flow.ctx);
|
||||
expect(await flow.hooks.get("tool_call")({ toolName: "write", input: { path: "unrelated.ts" } }, flow.ctx)).toBeUndefined();
|
||||
} finally { rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("does not persist startup results or launch work after session shutdown", async () => {
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
let finish: (() => void) | undefined;
|
||||
openSupervisorPane.mockImplementationOnce(() => new Promise(resolve => { finish = () => resolve("late-pane"); }));
|
||||
await flow.commands.get("goals").handler("make the file", flow.ctx);
|
||||
approvedPlan(flow.cwd);
|
||||
const starting = flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
await flow.hooks.get("session_shutdown")();
|
||||
const entries = flow.entries.length;
|
||||
const messages = flow.messages.length;
|
||||
finish!();
|
||||
await starting;
|
||||
expect(flow.entries).toHaveLength(entries);
|
||||
expect(flow.messages).toHaveLength(messages);
|
||||
} finally { rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("keeps a failed Ready model not-ready and recovers the same real supervisor binding", async () => {
|
||||
const wire = pairedIntercomFixture();
|
||||
const flow = setup(["Ready", "Ready"], [], wire.worker.events as ExtensionAPI["events"]);
|
||||
const supervisorEntries: any[] = [];
|
||||
const supervisor = new GoalIntercom({ events: wire.supervisor.events, on: () => {}, appendEntry: (customType: string, data: unknown) => supervisorEntries.push({ type: "custom", customType, data }) } as unknown as ExtensionAPI);
|
||||
const supervisorCtx = { sessionManager: { getEntries: () => supervisorEntries }, ui: { notify: vi.fn() } };
|
||||
try {
|
||||
await flow.commands.get("goals").handler("make the file", flow.ctx);
|
||||
approvedPlan(flow.cwd);
|
||||
mkdirSync(join(flow.cwd, ".pi/pi-goals/models"), { recursive: true });
|
||||
writeFileSync(join(flow.cwd, ".pi/pi-goals/models/worker.json"), JSON.stringify({ provider: "gone", id: "expired" }));
|
||||
flow.ctx.modelRegistry.find = () => undefined as any;
|
||||
openSupervisorPane.mockImplementationOnce(async (input: any) => {
|
||||
supervisor.configure(input.approvalId, "supervisor", supervisorCtx as any, true);
|
||||
return "pane-2";
|
||||
});
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
const binding = (flow.entries.at(-1)?.data as any).approvalId;
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning", planVersion: 1 });
|
||||
expect(wire.worker.sent.filter(message => message.kind === "hello" && message.ready)).toHaveLength(0);
|
||||
expect(supervisor.connected).toBe(false);
|
||||
expect(() => supervisor.steer("Must wait.")).toThrow("disconnected");
|
||||
await flow.hooks.get("model_select")({ source: "set", model: { provider: "test", id: "chosen" } }, flow.ctx);
|
||||
flow.ctx.modelRegistry.find = (provider, id) => ({ provider, id });
|
||||
await flow.commands.get("goals").handler("reconnect", flow.ctx);
|
||||
expect(supervisor.connected).toBe(false); // Planning is not implementation readiness.
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
expect(supervisor.connected).toBe(true);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "working", approvalId: binding, planVersion: 1 });
|
||||
expect(openSupervisorPane).toHaveBeenCalledTimes(1);
|
||||
expect(closeSupervisorPane).not.toHaveBeenCalled();
|
||||
await flow.commands.get("goals").handler("reconnect", flow.ctx);
|
||||
supervisor.steer("Recovered instruction.");
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
expect(flow.messages.filter(message => message.content === "[supervisor] Recovered instruction.")).toHaveLength(1);
|
||||
expect(supervisorCtx.ui.notify).not.toHaveBeenCalled();
|
||||
} finally { rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("points a present-but-paused peer recovery at the supervisor pane", async () => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
restoredPlan(flow);
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
flow.transport.replyToHello(false);
|
||||
flow.transport.receive({ binding: "restored-binding", role: "supervisor", kind: "hello", id: "hello", ready: false });
|
||||
expect(flow.ctx.ui.setWidget).toHaveBeenLastCalledWith("pi-goals-widget", [expect.stringContaining("Supervisor is present but not ready")]);
|
||||
const prompt = await flow.hooks.get("before_agent_start")({}, flow.ctx);
|
||||
expect(prompt.systemPrompt).toContain("/goals reconnect in the supervisor pane");
|
||||
} finally { rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("clear during the initial Ready wait cancels immediately and cannot resurrect the plan", async () => {
|
||||
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval"] });
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("make the file", flow.ctx);
|
||||
const path = approvedPlan(flow.cwd);
|
||||
flow.transport.replyToHello(false);
|
||||
const starting = flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ supervisorPaneId: "pane-2" });
|
||||
await flow.commands.get("goals").handler("clear", flow.ctx);
|
||||
await starting; // No timer advancement: detach must cancel the five-minute wait.
|
||||
const entryCount = flow.entries.length;
|
||||
await vi.advanceTimersByTimeAsync(300_000);
|
||||
expect(flow.entries).toHaveLength(entryCount);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: null, planVersion: null, approvalId: null });
|
||||
expect(flow.messages.some(message => message.content.includes("Begin implementation"))).toBe(false);
|
||||
expect(flow.ctx.ui.setWidget).toHaveBeenLastCalledWith("pi-goals-widget", undefined);
|
||||
expect(readFileSync(path, "utf8")).toContain("make the file");
|
||||
} finally { rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("clear before the launcher resolves rejects late pane callbacks without restoring the binding", async () => {
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("make the file", flow.ctx);
|
||||
approvedPlan(flow.cwd);
|
||||
let finish!: () => void;
|
||||
openSupervisorPane.mockImplementationOnce((_input: any, opened: any) => new Promise((resolve, reject) => {
|
||||
finish = () => { try { opened("late-pane"); resolve("late-pane"); } catch (error) { reject(error); } };
|
||||
}));
|
||||
const starting = flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
await flow.commands.get("goals").handler("clear", flow.ctx);
|
||||
const entryCount = flow.entries.length;
|
||||
finish(); await starting;
|
||||
expect(flow.entries).toHaveLength(entryCount);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: null, planVersion: null, approvalId: null });
|
||||
expect(flow.messages.some(message => message.content.includes("Begin implementation"))).toBe(false);
|
||||
} finally { rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
+10
-26
@@ -6,37 +6,23 @@ import { closeSupervisorPane, openSupervisorPane, supervisorCommand } from "../s
|
||||
|
||||
function input() {
|
||||
return {
|
||||
cwd: "/repo",
|
||||
sourceSessionFile: "/sessions/worker.jsonl",
|
||||
workerSessionId: "worker-12345678",
|
||||
workerIntercomId: "intercom-12345678",
|
||||
planPath: "/repo/.pi/plan/worker-v1.md",
|
||||
approvalId: "approval-1",
|
||||
extensionPath: "/repo/src/index.ts",
|
||||
superviseExtensionPath: null,
|
||||
model: "provider/supervisor",
|
||||
cwd: "/repo", sourceSessionFile: "/sessions/worker.jsonl", workerSessionId: "worker-12345678",
|
||||
planPath: "/repo/.pi/plan/worker-v1.md", approvalId: "approval-1", extensionPath: "/repo/src/index.ts", model: "provider/supervisor",
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => vi.unstubAllEnvs());
|
||||
|
||||
describe("supervisor pane command", () => {
|
||||
it("forks the planning session with an explicit supervisor role and model", () => {
|
||||
it("forks the planning session with pi-goals owning its Intercom dependency", () => {
|
||||
const command = supervisorCommand(input());
|
||||
expect(command).toContain("'PI_GOALS_ROLE=supervisor'");
|
||||
expect(command).toContain("'PI_GOALS_WORKER_INTERCOM_ID=intercom-12345678'");
|
||||
expect(command).toContain("'pi' '--no-extensions' '-e' 'npm:pi-intercom' '-e' 'npm:@wassname2/pi-supervise@0.0.4' '-e' '/repo/src/index.ts'");
|
||||
expect(command).toContain("'PI_GOALS_APPROVAL_ID=approval-1'");
|
||||
expect(command).toContain("'pi' '-e' '/repo/src/index.ts'");
|
||||
expect(command).not.toMatch(/--no-(?:extensions|skills|prompt-templates|themes|context-files)/);
|
||||
expect(command).toContain("'--fork' '/sessions/worker.jsonl'");
|
||||
expect(command).toContain("'--model' 'provider/supervisor'");
|
||||
expect(command).not.toContain("Initialize supervision startup.");
|
||||
expect(command).not.toContain("pi-subagents");
|
||||
});
|
||||
|
||||
it("uses the loaded pi-supervise extension before the npm fallback", () => {
|
||||
const loaded = { ...input(), superviseExtensionPath: "/repo/vendor/pi-supervise/src/index.ts" };
|
||||
expect(supervisorCommand(loaded)).toContain("'-e' '/repo/vendor/pi-supervise/src/index.ts'");
|
||||
vi.stubEnv("PI_GOALS_SUPERVISE_EXTENSION", "/repo/override/pi-supervise/src/index.ts");
|
||||
expect(supervisorCommand(loaded)).toContain("'-e' '/repo/override/pi-supervise/src/index.ts'");
|
||||
expect(command).not.toContain("pi-supervise");
|
||||
});
|
||||
|
||||
it("accepts Herdr's text version output and stale pane cleanup", async () => {
|
||||
@@ -53,12 +39,10 @@ exit 2
|
||||
vi.stubEnv("HERDR_ENV", "1");
|
||||
vi.stubEnv("HERDR_BIN_PATH", bin);
|
||||
try {
|
||||
await expect(openSupervisorPane(input())).resolves.toBe("new-pane");
|
||||
await expect(openSupervisorPane({ ...input(), cwd }, vi.fn())).resolves.toBe("new-pane");
|
||||
await expect(closeSupervisorPane("new-pane")).resolves.toBeUndefined();
|
||||
vi.stubEnv("HERDR_SMOKE_RUN_FAIL", "1");
|
||||
await expect(openSupervisorPane(input())).rejects.toThrow("run failed");
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
await expect(openSupervisorPane({ ...input(), cwd }, vi.fn())).rejects.toThrow("run failed");
|
||||
} finally { rmSync(cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { once } from "node:events";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { IntercomClient } from "pi-intercom/broker/client.ts";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { GoalIntercom } from "../src/intercom.js";
|
||||
|
||||
it("exchanges readiness, views and exact advice over a real isolated pi-intercom broker", async () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "goals-intercom-"));
|
||||
vi.stubEnv("PI_CODING_AGENT_DIR", directory);
|
||||
const broker = spawn(process.execPath, [resolve("node_modules/tsx/dist/cli.mjs"), resolve("node_modules/pi-intercom/broker/broker.ts")], { env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] });
|
||||
const clients: IntercomClient[] = [];
|
||||
const shutdowns: Array<() => void> = [];
|
||||
try {
|
||||
await new Promise<void>((resolveReady, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error("Isolated broker did not start.")), 5000);
|
||||
broker.stdout.on("data", chunk => { if (String(chunk).includes("Intercom broker started")) { clearTimeout(timer); resolveReady(); } });
|
||||
broker.once("exit", code => { clearTimeout(timer); reject(new Error(`Broker exited: ${code}`)); });
|
||||
});
|
||||
async function endpoint(role: "worker" | "supervisor") {
|
||||
const client = new IntercomClient();
|
||||
clients.push(client);
|
||||
await client.connect({ name: role, cwd: directory, model: "test", pid: process.pid, startedAt: Date.now(), lastActivity: Date.now(), extensions: [{ namespace: "pi-goals", ownerEligible: false }] });
|
||||
const entries: any[] = [];
|
||||
const api = {
|
||||
on: (name: string, callback: () => void) => { if (name === "session_shutdown") shutdowns.push(callback); },
|
||||
appendEntry: (customType: string, data: unknown) => entries.push({ type: "custom", customType, data }),
|
||||
events: {
|
||||
on() {},
|
||||
emit: (_name: string, registration: any) => {
|
||||
client.on("extension_message", message => registration.onEvent({ type: "message", fromSessionId: message.fromSessionId, payload: message.payload }));
|
||||
client.on("disconnected", () => registration.onEvent({ type: "connection", connected: false, supported: true }));
|
||||
client.onBrokerMessage(message => { if (message.type === "session_left") registration.onEvent(message); });
|
||||
registration.onReady({
|
||||
snapshot: () => ({ connected: client.isConnected(), supported: client.supportsFeature("extension-bus-v1") }),
|
||||
publish: (payload: unknown) => client.sendExtensionMessage({ type: "extension_publish", namespace: "pi-goals", audience: "capable", payload }),
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
const link = new GoalIntercom(api as unknown as ExtensionAPI);
|
||||
link.configure("isolated-binding", role, { sessionManager: { getEntries: () => entries }, ui: { notify() {} } } as any);
|
||||
return { link, client };
|
||||
}
|
||||
const worker = await endpoint("worker");
|
||||
const supervisor = await endpoint("supervisor");
|
||||
expect(worker.link.connected).toBe(false);
|
||||
supervisor.link.markReady();
|
||||
await Promise.all([worker.link.waitReady(3000), supervisor.link.waitReady(3000)]);
|
||||
const viewed = new Promise<string>(resolveView => { supervisor.link.onView = view => resolveView(view.text); });
|
||||
const view = worker.link.view("The worker stopped.\nModal uses a remote GPU.", "settled");
|
||||
expect(await viewed).toBe(view.text);
|
||||
const advice = "Check the Modal dependency. Keep the local GPU queue paused.";
|
||||
const received = new Promise<string>(resolveAdvice => { worker.link.onSteer = resolveAdvice; });
|
||||
supervisor.link.steer(advice);
|
||||
expect(await received).toBe(advice);
|
||||
console.log("Intercom broker: readiness confirmed; exact worker view and supervisor advice received.");
|
||||
} finally {
|
||||
for (const shutdown of shutdowns) await shutdown();
|
||||
for (const client of clients) await client.disconnect();
|
||||
if (broker.exitCode === null) { broker.kill("SIGTERM"); await once(broker, "exit"); }
|
||||
vi.unstubAllEnvs();
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
}, 15_000);
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { GoalIntercom } from "../src/intercom.js";
|
||||
import { intercomFixture } from "./intercom-fixture.js";
|
||||
|
||||
const shutdowns: Array<() => void> = [];
|
||||
function setup(role: "worker" | "supervisor", entries: any[] = []) {
|
||||
const fixture = intercomFixture();
|
||||
const hooks = new Map<string, any>();
|
||||
const ctx = { isIdle: vi.fn(() => true), hasPendingMessages: vi.fn(() => false), sessionManager: { getEntries: () => entries }, ui: { notify: vi.fn() } };
|
||||
const pi = { events: fixture.events, on: (name: string, hook: any) => hooks.set(name, hook), appendEntry: (customType: string, data: unknown) => entries.push({ type: "custom", customType, data }) };
|
||||
const link = new GoalIntercom(pi as unknown as ExtensionAPI);
|
||||
link.configure("binding", role, ctx as any, true);
|
||||
const delivered = vi.fn();
|
||||
link.onSteer = delivered;
|
||||
link.onView = view => delivered(view.text);
|
||||
shutdowns.push(() => hooks.get("session_shutdown")());
|
||||
const receive = (id: string, text: string) => fixture.receive({ binding: "binding", role: role === "worker" ? "supervisor" : "worker", kind: role === "worker" ? "steer" : "view", reason: "settled", id, text });
|
||||
const accept = (text: string) => hooks.get("message_start")({ message: { role: "user", content: role === "worker" ? `[supervisor] ${text}` : text } });
|
||||
return { link, hooks, ctx, entries, fixture, delivered, receive, accept };
|
||||
}
|
||||
afterEach(() => { for (const stop of shutdowns.splice(0)) stop(); vi.useRealTimers(); });
|
||||
|
||||
it.each(["worker", "supervisor"] as const)("retains %s messages across successful, failed and cancelled manual compaction", async role => {
|
||||
vi.useFakeTimers();
|
||||
const r = setup(role);
|
||||
for (const [id, event] of [["success", "session_compact"], ["failure", "session_compact_failed"], ["cancelled", "session_compact_failed"]]) {
|
||||
r.ctx.isIdle.mockReturnValue(false);
|
||||
await r.hooks.get("session_before_compact")({});
|
||||
r.receive(id, `evidence-${id}`);
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(r.delivered).not.toHaveBeenCalledWith(`evidence-${id}`);
|
||||
expect(r.fixture.sent.some(m => m.kind === "received" && m.id === id)).toBe(false);
|
||||
await r.hooks.get(event)({ aborted: id === "cancelled" });
|
||||
await vi.advanceTimersByTimeAsync(0); // Another extension may still be handling session_compact.
|
||||
expect(r.delivered).not.toHaveBeenCalledWith(`evidence-${id}`);
|
||||
r.ctx.isIdle.mockReturnValue(true);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
expect(r.delivered).toHaveBeenCalledWith(`evidence-${id}`);
|
||||
await r.accept(`evidence-${id}`);
|
||||
expect(r.fixture.sent.some(m => m.kind === "received" && m.id === id)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("retains distinct deltas through reload and does not duplicate delayed presentation", async () => {
|
||||
vi.useFakeTimers();
|
||||
const first = setup("supervisor");
|
||||
first.ctx.isIdle.mockReturnValue(false);
|
||||
await first.hooks.get("session_before_compact")({});
|
||||
first.receive("one", "first independent evidence");
|
||||
first.receive("two", "second independent evidence");
|
||||
await first.hooks.get("session_shutdown")();
|
||||
const resumed = setup("supervisor", [...first.entries]);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(resumed.delivered.mock.calls).toEqual([["first independent evidence"]]);
|
||||
resumed.ctx.hasPendingMessages.mockReturnValue(true);
|
||||
resumed.link.resumeDelivery();
|
||||
resumed.receive("one", "first independent evidence");
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(resumed.delivered).toHaveBeenCalledTimes(1);
|
||||
resumed.ctx.hasPendingMessages.mockReturnValue(false);
|
||||
await resumed.accept("first independent evidence");
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(resumed.delivered.mock.calls).toEqual([["first independent evidence"], ["second independent evidence"]]);
|
||||
await resumed.accept("second independent evidence");
|
||||
resumed.receive("two", "second independent evidence");
|
||||
expect(resumed.delivered).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
export function intercomFixture() {
|
||||
let autoHello = true;
|
||||
let registration: any;
|
||||
const sent: any[] = [];
|
||||
let connected = true;
|
||||
const receive = (payload: any, fromSessionId = "peer") => registration.onEvent({ type: "message", fromSessionId, payload });
|
||||
return {
|
||||
sent, receive,
|
||||
replyToHello: (value: boolean) => { autoHello = value; },
|
||||
event: (event: any) => registration.onEvent(event),
|
||||
connect: (value: boolean) => { connected = value; registration.onEvent({ type: "connection", connected: value, supported: true }); },
|
||||
events: {
|
||||
on: () => () => {},
|
||||
emit: (name: string, value: any) => {
|
||||
if (name !== "intercom:extension-register") return false;
|
||||
registration = value;
|
||||
value.onReady({
|
||||
snapshot: () => ({ connected, supported: true }),
|
||||
publish: (message: any) => {
|
||||
sent.push(message);
|
||||
if (message.kind === "hello" && !message.reply && autoHello) queueMicrotask(() => receive({ ...message, role: message.role === "worker" ? "supervisor" : "worker", ready: true, reply: true }));
|
||||
},
|
||||
});
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { GoalIntercom } from "../src/intercom.js";
|
||||
import { pairedIntercomFixture } from "./paired-intercom-fixture.js";
|
||||
|
||||
function endpoint(transport: ReturnType<typeof pairedIntercomFixture>["worker"]) {
|
||||
const entries: any[] = [];
|
||||
const ctx = { sessionManager: { getEntries: () => entries }, ui: { notify: vi.fn() } };
|
||||
const hooks = new Map<string, any>();
|
||||
const link = new GoalIntercom({ events: transport.events, on: (name: string, hook: any) => hooks.set(name, hook), appendEntry: (customType: string, data: unknown) => entries.push({ type: "custom", customType, data }) } as unknown as ExtensionAPI);
|
||||
return { link, ctx, entries, accept: (text: string) => hooks.get("message_start")({ message: { role: "user", content: text } }) };
|
||||
}
|
||||
const settle = () => new Promise(resolve => setImmediate(resolve));
|
||||
|
||||
it("re-handshakes unchanged peers in either direction without hello ping-pong or lost advice", async () => {
|
||||
const wire = pairedIntercomFixture();
|
||||
const worker = endpoint(wire.worker);
|
||||
const supervisor = endpoint(wire.supervisor);
|
||||
worker.link.configure("binding", "worker", worker.ctx as any);
|
||||
supervisor.link.configure("binding", "supervisor", supervisor.ctx as any);
|
||||
supervisor.link.markReady();
|
||||
await settle();
|
||||
expect(worker.link.connected && supervisor.link.connected).toBe(true);
|
||||
const deliver = vi.fn(); worker.link.onSteer = deliver;
|
||||
for (const side of [worker, supervisor, worker]) {
|
||||
const before = wire.worker.sent.length + wire.supervisor.sent.length;
|
||||
side.link.configure("binding", side === worker ? "worker" : "supervisor", side.ctx as any, true);
|
||||
await side.link.waitReady(100);
|
||||
await settle();
|
||||
expect(worker.link.connected && supervisor.link.connected).toBe(true);
|
||||
expect(wire.worker.sent.length + wire.supervisor.sent.length - before).toBe(2);
|
||||
}
|
||||
const before = wire.worker.sent.length + wire.supervisor.sent.length;
|
||||
worker.link.configure("binding", "worker", worker.ctx as any, true);
|
||||
supervisor.link.configure("binding", "supervisor", supervisor.ctx as any, true);
|
||||
await settle();
|
||||
expect(worker.link.connected && supervisor.link.connected).toBe(true);
|
||||
expect(wire.worker.sent.length + wire.supervisor.sent.length - before).toBe(4);
|
||||
supervisor.link.steer("Read actual output.");
|
||||
await settle();
|
||||
expect(deliver).toHaveBeenCalledExactlyOnceWith("Read actual output.");
|
||||
expect(worker.ctx.ui.notify).not.toHaveBeenCalled();
|
||||
expect(supervisor.ctx.ui.notify).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("replays pending advice and views across either role's own readiness transition", async () => {
|
||||
const wire = pairedIntercomFixture();
|
||||
const worker = endpoint(wire.worker), supervisor = endpoint(wire.supervisor);
|
||||
worker.link.configure("binding", "worker", worker.ctx as any);
|
||||
supervisor.link.configure("binding", "supervisor", supervisor.ctx as any, true);
|
||||
await settle();
|
||||
wire.supervisor.drop = message => message.kind === "steer";
|
||||
supervisor.link.steer("Pending advice.");
|
||||
wire.supervisor.drop = () => false;
|
||||
const deliver = vi.fn(); worker.link.onSteer = deliver;
|
||||
supervisor.link.markNotReady(); await settle();
|
||||
expect(worker.link.connected).toBe(false);
|
||||
supervisor.link.markReady(); await settle();
|
||||
expect(deliver).toHaveBeenCalledExactlyOnceWith("Pending advice.");
|
||||
await worker.accept("[supervisor] Pending advice.");
|
||||
worker.link.markNotReady(); await settle();
|
||||
const onView = vi.fn(); supervisor.link.onView = onView;
|
||||
worker.link.view("Fresh view.", "settled");
|
||||
expect(onView).not.toHaveBeenCalled();
|
||||
worker.link.markReady(); await settle();
|
||||
expect(onView).toHaveBeenCalledTimes(1);
|
||||
await supervisor.accept(onView.mock.calls[0][0].text);
|
||||
expect(worker.link.connected && supervisor.link.connected).toBe(true);
|
||||
wire.worker.connect(false); wire.worker.connect(true); await settle();
|
||||
expect(worker.link.connected && supervisor.link.connected).toBe(true);
|
||||
expect(deliver).toHaveBeenCalledTimes(1);
|
||||
expect(onView).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("cancels pending waits immediately on detach or reconfiguration", async () => {
|
||||
const wire = pairedIntercomFixture();
|
||||
const worker = endpoint(wire.worker);
|
||||
worker.link.configure("binding", "worker", worker.ctx as any);
|
||||
const cancelled = expect(worker.link.waitReady()).rejects.toThrow("plan detached");
|
||||
worker.link.detach(); await cancelled;
|
||||
worker.link.configure("next", "worker", worker.ctx as any);
|
||||
const replaced = expect(worker.link.waitReady()).rejects.toThrow("reconfiguration");
|
||||
worker.link.configure("third", "worker", worker.ctx as any); await replaced;
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { GoalIntercom } from "../src/intercom.js";
|
||||
import { intercomFixture } from "./intercom-fixture.js";
|
||||
|
||||
function setup(role: "worker" | "supervisor", entries: any[] = []) {
|
||||
const fixture = intercomFixture();
|
||||
const hooks = new Map<string, any>();
|
||||
const ctx = { isIdle: vi.fn(() => true), hasPendingMessages: vi.fn(() => false), sessionManager: { getEntries: () => entries }, ui: { notify: vi.fn() } };
|
||||
const api = { events: fixture.events, on: (name: string, hook: any) => hooks.set(name, hook), appendEntry: (customType: string, data: unknown) => entries.push({ type: "custom", customType, data }) };
|
||||
const link = new GoalIntercom(api as unknown as ExtensionAPI);
|
||||
link.configure("binding", role, ctx as any);
|
||||
return { link, fixture, entries, ctx, hooks };
|
||||
}
|
||||
|
||||
describe("pi-intercom transport", () => {
|
||||
it("receives exact advice once, acknowledges it and rejects unrelated peers", async () => {
|
||||
const runtime = setup("worker");
|
||||
await runtime.link.waitReady();
|
||||
const instruction = "Check the Modal dependency.\nKeep the local queue paused.";
|
||||
const delivered = vi.fn();
|
||||
runtime.link.onSteer = delivered;
|
||||
const message = { binding: "binding", role: "supervisor", kind: "steer", id: "instruction", text: instruction };
|
||||
runtime.fixture.receive({ ...message, binding: "other" });
|
||||
runtime.fixture.receive(message, "wrong-peer");
|
||||
expect(delivered).not.toHaveBeenCalled();
|
||||
runtime.fixture.receive(message);
|
||||
expect(runtime.fixture.sent.filter(message => message.kind === "received")).toHaveLength(0);
|
||||
await runtime.hooks.get("message_start")({ message: { role: "user", content: `[supervisor] ${instruction}` } });
|
||||
runtime.fixture.receive(message);
|
||||
expect(delivered).toHaveBeenCalledExactlyOnceWith(instruction);
|
||||
expect(runtime.fixture.sent.filter(message => message.kind === "received")).toHaveLength(2);
|
||||
await runtime.hooks.get("session_shutdown")();
|
||||
runtime.fixture.receive({ ...message, id: "late" });
|
||||
expect(delivered).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("restores an unacknowledged steer on reconnect and stops replay after acknowledgment", async () => {
|
||||
const first = setup("supervisor");
|
||||
first.link.markReady();
|
||||
await first.link.waitReady();
|
||||
const id = first.link.steer("Read the full output.");
|
||||
await first.hooks.get("session_shutdown")();
|
||||
const resumed = setup("supervisor", [...first.entries]);
|
||||
resumed.link.markReady();
|
||||
await resumed.link.waitReady();
|
||||
const retries = resumed.fixture.sent.filter(message => message.kind === "steer");
|
||||
expect(retries.length).toBeGreaterThan(0);
|
||||
for (const retry of retries) expect(retry).toMatchObject({ id, text: "Read the full output." });
|
||||
resumed.fixture.receive({ binding: "binding", role: "worker", kind: "received", id });
|
||||
resumed.fixture.connect(false);
|
||||
expect(resumed.link.connected).toBe(false);
|
||||
expect(() => resumed.link.steer("Must not send.")).toThrow("disconnected");
|
||||
resumed.fixture.connect(true);
|
||||
await resumed.link.waitReady();
|
||||
expect(resumed.fixture.sent.filter(message => message.kind === "steer")).toHaveLength(retries.length);
|
||||
});
|
||||
|
||||
it("advances the incremental overview only after acknowledgment", async () => {
|
||||
const runtime = setup("worker");
|
||||
await runtime.link.waitReady();
|
||||
const view = runtime.link.view("The worker stopped.", "settled", "entry-1", true);
|
||||
expect(runtime.link.acknowledgedEntry).toBeUndefined();
|
||||
runtime.fixture.receive({ binding: "binding", role: "supervisor", kind: "received", id: view.id });
|
||||
expect(runtime.link.acknowledgedEntry).toBe("entry-1");
|
||||
const resumed = setup("worker", [...runtime.entries]);
|
||||
expect(resumed.link.acknowledgedEntry).toBe("entry-1");
|
||||
});
|
||||
|
||||
it("cancels a readiness wait on shutdown", async () => {
|
||||
const runtime = setup("worker");
|
||||
await runtime.link.waitReady();
|
||||
runtime.fixture.connect(false);
|
||||
const wait = runtime.link.waitReady();
|
||||
const rejection = expect(wait).rejects.toThrow("Session ended");
|
||||
await runtime.hooks.get("session_shutdown")();
|
||||
await rejection;
|
||||
});
|
||||
});
|
||||
|
||||
it("does not acknowledge a synchronous handoff failure, and retries the instruction", async () => {
|
||||
vi.useFakeTimers();
|
||||
const runtime = setup("worker");
|
||||
await runtime.link.waitReady();
|
||||
const delivery = vi.fn().mockImplementationOnce(() => { throw new Error("Delivery unavailable"); });
|
||||
runtime.link.onSteer = delivery;
|
||||
const message = { binding: "binding", role: "supervisor", kind: "steer", id: "retry", text: "Inspect evidence." };
|
||||
runtime.fixture.receive(message);
|
||||
expect(runtime.fixture.sent.filter(m => m.kind === "received")).toHaveLength(0);
|
||||
expect(runtime.entries.filter(e => e.data.direction === "in")).toHaveLength(0);
|
||||
runtime.link.resumeDelivery();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(delivery).toHaveBeenCalledTimes(2);
|
||||
await runtime.hooks.get("message_start")({ message: { role: "user", content: "[supervisor] Inspect evidence." } });
|
||||
expect(runtime.fixture.sent.filter(m => m.kind === "received")).toHaveLength(1);
|
||||
await runtime.hooks.get("session_shutdown")();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("reports a peer startup failure immediately and recovers on its next ready hello", async () => {
|
||||
const runtime = setup("worker");
|
||||
await runtime.link.waitReady();
|
||||
runtime.fixture.receive({ binding: "binding", role: "supervisor", kind: "hello", id: "hello", reply: true, ready: false, failure: "Compaction cancelled; use /goals supervise." });
|
||||
await expect(runtime.link.waitReady()).rejects.toThrow("Compaction cancelled");
|
||||
runtime.fixture.receive({ binding: "binding", role: "supervisor", kind: "hello", id: "hello", reply: true, ready: true });
|
||||
await runtime.link.waitReady();
|
||||
expect(runtime.link.connected).toBe(true);
|
||||
await runtime.hooks.get("session_shutdown")();
|
||||
});
|
||||
|
||||
it("detaches a completed binding and ignores its late advice without replay errors or false acceptance", async () => {
|
||||
const runtime = setup("worker");
|
||||
await runtime.link.waitReady();
|
||||
const delivery = vi.fn();
|
||||
runtime.link.onSteer = delivery;
|
||||
runtime.link.detach();
|
||||
runtime.fixture.receive({ binding: "binding", role: "supervisor", kind: "steer", id: "late", text: "Obsolete advice." });
|
||||
expect(runtime.link.connected).toBe(false);
|
||||
expect(delivery).not.toHaveBeenCalled();
|
||||
expect(runtime.ctx.ui.notify).not.toHaveBeenCalled();
|
||||
expect(runtime.fixture.sent.filter(m => m.kind === "received")).toHaveLength(0);
|
||||
expect(runtime.fixture.sent.at(-1)).toMatchObject({ kind: "hello", ready: false });
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { once } from "node:events";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { createServer } from "node:http";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
it.each(["worker", "supervisor"] as const)("real Pi preserves %s delivery through compaction success, failure and cancellation", async role => {
|
||||
for (const outcome of ["success", "failure", "cancel"]) {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "goals-native-compaction-"));
|
||||
const agentDir = join(cwd, "agent");
|
||||
mkdirSync(agentDir);
|
||||
writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ compaction: { enabled: false, keepRecentTokens: 1000, reserveTokens: 1000 }, retry: { enabled: false } }));
|
||||
let requests = 0;
|
||||
const server = createServer(async (req, res) => {
|
||||
for await (const _chunk of req) { /* consume local request */ }
|
||||
requests++;
|
||||
if (outcome === "failure" && requests === 3) { res.writeHead(400); res.end(JSON.stringify({ error: { message: "deterministic compaction failure" } })); return; }
|
||||
res.writeHead(200, { "content-type": "text/event-stream" });
|
||||
res.end(`data: ${JSON.stringify({ choices: [{ index: 0, delta: { content: "Inspected the supplied context." }, finish_reason: null }] })}\n\ndata: ${JSON.stringify({ choices: [{ index: 0, delta: {}, finish_reason: "stop" }], usage: { prompt_tokens: 8000, completion_tokens: 10, total_tokens: 8010 } })}\n\ndata: [DONE]\n\n`);
|
||||
});
|
||||
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("No local model port");
|
||||
const child = spawn(resolve("node_modules/.bin/pi"), ["--mode", "rpc", "--no-extensions", "--model", "offline/test", "-e", resolve("test/fixtures/offline-model.ts"), "-e", resolve("test/fixtures/compaction-delivery.ts")], { cwd, env: { ...process.env, PI_CODING_AGENT_DIR: agentDir, PI_GOALS_OFFLINE_MODEL_URL: `http://127.0.0.1:${address.port}`, PI_GOALS_TEST_DELIVERY_ROLE: role, PI_GOALS_TEST_COMPACTION: outcome } });
|
||||
const messages: any[] = [];
|
||||
let buffer = "", stderr = "";
|
||||
child.stdout.on("data", data => {
|
||||
buffer += data;
|
||||
while (buffer.includes("\n")) { const n = buffer.indexOf("\n"); const line = buffer.slice(0, n); buffer = buffer.slice(n + 1); if (line.trim()) messages.push(JSON.parse(line)); }
|
||||
});
|
||||
child.stderr.on("data", data => { stderr += data; });
|
||||
const send = (message: object) => child.stdin.write(`${JSON.stringify(message)}\n`);
|
||||
const wait = async (predicate: (message: any) => boolean) => {
|
||||
for (let i = 0; i < 500; i++) { const found = messages.find(predicate); if (found) return found; await new Promise(done => setTimeout(done, 20)); }
|
||||
throw new Error(`Timed out ${role}/${outcome}: ${stderr}\n${JSON.stringify(messages.slice(-8))}`);
|
||||
};
|
||||
try {
|
||||
send({ type: "prompt", id: "seed", message: "Inspect this bounded test context. " + "Saved observation. ".repeat(2000) });
|
||||
await wait(m => m.type === "agent_settled");
|
||||
messages.length = 0;
|
||||
send({ type: "prompt", id: "second-turn", message: "Keep this most recent turn for continued work. " + "Recent context. ".repeat(500) });
|
||||
await wait(m => m.type === "agent_settled");
|
||||
send({ type: "compact", id: "compact" });
|
||||
const completion = await wait(m => m.type === "response" && m.id === "compact");
|
||||
expect(completion.success, JSON.stringify(completion)).toBe(outcome === "success");
|
||||
await wait(m => m.type === "message_start" && m.message?.role === "user" && JSON.stringify(m.message.content).includes("Retained evidence arrived"));
|
||||
await wait(m => m.type === "message_end" && m.message?.role === "user" && JSON.stringify(m.message.content).includes("Retained evidence arrived"));
|
||||
send({ type: "get_state", id: "state" });
|
||||
const state = await wait(m => m.type === "response" && m.id === "state");
|
||||
const transcript = readFileSync(state.data.sessionFile, "utf8");
|
||||
expect(transcript).toContain('"direction":"queued"');
|
||||
expect(transcript).toContain('"direction":"in"');
|
||||
expect(messages.filter(m => m.type === "message_start" && m.message?.role === "user" && JSON.stringify(m.message.content).includes("Retained evidence arrived"))).toHaveLength(1);
|
||||
expect(messages.filter(m => m.type === "extension_error")).toEqual([]);
|
||||
console.log(`real Pi ${role}/${outcome}: retained message presented once and saved, no extension errors`);
|
||||
} finally {
|
||||
child.kill("SIGTERM"); await once(child, "exit");
|
||||
await new Promise<void>(done => server.close(() => done()));
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}, 45_000);
|
||||
@@ -0,0 +1,150 @@
|
||||
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
||||
import { once } from "node:events";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { createServer } from "node:http";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { approveGoalDescription, steerWorkerDescription, supervisorStoppedReview } from "../src/prompts.js";
|
||||
|
||||
// Pi/OpenAI: RPC drives test inputs only; the two sessions communicate exclusively through Intercom.
|
||||
class Driver {
|
||||
messages: any[] = [];
|
||||
stderr = "";
|
||||
private waiters: Array<{ predicate: (value: any) => boolean; resolve: (value: any) => void }> = [];
|
||||
constructor(readonly process: ChildProcessWithoutNullStreams) {
|
||||
let buffer = "";
|
||||
process.stdout.setEncoding("utf8");
|
||||
process.stdout.on("data", chunk => {
|
||||
buffer += chunk;
|
||||
while (buffer.includes("\n")) {
|
||||
const newline = buffer.indexOf("\n");
|
||||
const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1);
|
||||
if (!line.trim()) continue;
|
||||
const message = JSON.parse(line);
|
||||
this.messages.push(message);
|
||||
for (const waiter of [...this.waiters]) if (waiter.predicate(message)) { this.waiters.splice(this.waiters.indexOf(waiter), 1); waiter.resolve(message); }
|
||||
}
|
||||
});
|
||||
process.stderr.on("data", chunk => { this.stderr += String(chunk); });
|
||||
}
|
||||
send(value: object): void { this.process.stdin.write(`${JSON.stringify(value)}\n`); }
|
||||
wait(predicate: (value: any) => boolean): Promise<any> {
|
||||
const found = this.messages.find(predicate);
|
||||
if (found) return Promise.resolve(found);
|
||||
return new Promise((resolveWait, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error(`Pi response timed out. stderr=${this.stderr}\nLast messages=${JSON.stringify(this.messages.slice(-5))}`)), 8000);
|
||||
this.waiters.push({ predicate, resolve: value => { clearTimeout(timer); resolveWait(value); } });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
it("runs a forked Pi supervisor and receives its exact instruction in another Pi session", async () => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "goals-native-pair-"));
|
||||
const agentDir = join(cwd, "agent");
|
||||
const advice = "Read the real outputs before declaring completion.";
|
||||
const children: ChildProcessWithoutNullStreams[] = [];
|
||||
let worker: Driver | undefined;
|
||||
let supervisor: Driver | undefined;
|
||||
let workerFile: string | undefined;
|
||||
let supervisorFile: string | undefined;
|
||||
let supervisorTools: string[] = [];
|
||||
let supervisorRequest: any;
|
||||
let latestRequest: any;
|
||||
const server = createServer(async (request, response) => {
|
||||
let body = "";
|
||||
for await (const chunk of request) body += chunk;
|
||||
const input = JSON.parse(body);
|
||||
latestRequest = input;
|
||||
const latest = input.messages.filter((message: any) => !JSON.stringify(message.content).includes("Full active plan:")).at(-1);
|
||||
const steer = latest.role === "user" && JSON.stringify(latest.content).includes("The worker stopped.");
|
||||
if (steer) {
|
||||
supervisorRequest = input;
|
||||
supervisorTools = input.tools.map((tool: any) => tool.function.name);
|
||||
}
|
||||
response.writeHead(200, { "content-type": "text/event-stream" });
|
||||
const delta = steer ? { tool_calls: [{ index: 0, id: "test-steer", type: "function", function: { name: "SteerWorker", arguments: JSON.stringify({ instruction: advice }) } }] } : { content: "Test context retained. Actual outputs still need inspection." };
|
||||
response.write(`data: ${JSON.stringify({ choices: [{ index: 0, delta, finish_reason: null }] })}\n\n`);
|
||||
response.write(`data: ${JSON.stringify({ choices: [{ index: 0, delta: {}, finish_reason: steer ? "tool_calls" : "stop" }], usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20 } })}\n\n`);
|
||||
response.end("data: [DONE]\n\n");
|
||||
});
|
||||
await new Promise<void>(resolveListen => server.listen(0, "127.0.0.1", resolveListen));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("Offline server did not bind.");
|
||||
const env = { ...process.env, PI_CODING_AGENT_DIR: agentDir, PI_GOALS_OFFLINE_MODEL_URL: `http://127.0.0.1:${address.port}` };
|
||||
const broker = spawn(process.execPath, [resolve("node_modules/tsx/dist/cli.mjs"), resolve("node_modules/pi-intercom/broker/broker.ts")], { env });
|
||||
children.push(broker);
|
||||
try {
|
||||
await new Promise<void>((resolveReady, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error("Broker did not start.")), 5000);
|
||||
broker.stdout.on("data", chunk => { if (String(chunk).includes("Intercom broker started")) { clearTimeout(timer); resolveReady(); } });
|
||||
broker.once("exit", code => { clearTimeout(timer); reject(new Error(`Broker exited ${code}`)); });
|
||||
});
|
||||
const common = ["--mode", "rpc", "--no-extensions", "--model", "offline/test", "-e", resolve("test/fixtures/offline-model.ts")];
|
||||
const workerProcess = spawn(resolve("node_modules/.bin/pi"), [...common, "-e", resolve("test/fixtures/intercom-worker.ts")], { cwd, env });
|
||||
children.push(workerProcess);
|
||||
worker = new Driver(workerProcess);
|
||||
worker.send({ type: "prompt", id: "planning", message: "Retain this planning context for the supervisor fork." });
|
||||
await worker.wait(message => message.type === "agent_end");
|
||||
worker.send({ type: "get_state", id: "worker-state" });
|
||||
const state = await worker.wait(message => message.type === "response" && message.id === "worker-state");
|
||||
workerFile = state.data.sessionFile;
|
||||
expect(workerFile).toBeTruthy();
|
||||
writeFileSync(join(cwd, "plan.md"), "1. [ ] goal: inspect actual outputs\n - discriminator: raw output inspected\n");
|
||||
mkdirSync(join(agentDir, "extensions"), { recursive: true });
|
||||
writeFileSync(join(agentDir, "extensions/profile-tools.ts"), `export { default } from ${JSON.stringify(resolve("test/fixtures/profile-tools.ts"))};`);
|
||||
// Exercise normal discovery in an isolated profile; never load the user's extensions in this test.
|
||||
const supervisorProcess = spawn(resolve("node_modules/.bin/pi"), [...common.filter(arg => arg !== "--no-extensions"), "-e", resolve("src/index.ts"), "--fork", workerFile!], { cwd, env: {
|
||||
...env, PI_GOALS_ROLE: "supervisor", PI_GOALS_WORKER_ID: state.data.sessionId, PI_GOALS_OWNER_SESSION_ID: state.data.sessionId,
|
||||
PI_GOALS_PLAN_PATH: join(cwd, "plan.md"), PI_GOALS_APPROVAL_ID: "native-pair-test", PI_GOALS_MODEL_EXPLICIT: "0",
|
||||
} });
|
||||
children.push(supervisorProcess);
|
||||
supervisor = new Driver(supervisorProcess);
|
||||
const received = await worker.wait(message => message.type === "message_start" && message.message?.role === "user" && JSON.stringify(message.message.content).includes(`[supervisor] ${advice}`));
|
||||
expect(JSON.stringify(received)).toContain(advice);
|
||||
const result = await supervisor.wait(message => message.type === "tool_execution_end" && message.toolName === "SteerWorker");
|
||||
expect(result.isError).toBe(false);
|
||||
expect(supervisorTools).toContain("SteerWorker");
|
||||
expect(supervisorTools).toContain("intercom");
|
||||
expect(supervisorTools).toContain("bash");
|
||||
expect(supervisorTools).toContain("edit");
|
||||
expect(supervisorTools).toContain("write");
|
||||
expect(supervisorTools).toContain("profile_inspection");
|
||||
expect(JSON.stringify(supervisorRequest.messages)).toContain(supervisorStoppedReview);
|
||||
const description = (name: string) => supervisorRequest.tools.find((tool: any) => tool.function.name === name).function.description;
|
||||
expect(description("SteerWorker")).toBe(steerWorkerDescription);
|
||||
expect(description("ApproveGoal")).toBe(approveGoalDescription);
|
||||
supervisor.send({ type: "get_state", id: "supervisor-state" });
|
||||
const supervisorState = await supervisor.wait(message => message.type === "response" && message.id === "supervisor-state");
|
||||
supervisorFile = supervisorState.data.sessionFile;
|
||||
expect(supervisorFile).not.toBe(workerFile);
|
||||
expect(readFileSync(supervisorFile!, "utf8")).toContain("Retain this planning context");
|
||||
await supervisor.wait(message => message.type === "agent_settled");
|
||||
supervisorProcess.kill("SIGTERM");
|
||||
await once(supervisorProcess, "exit");
|
||||
const freshEnv = Object.fromEntries(Object.entries(env).filter(([name]) => !/^PI_GOALS_(ROLE|WORKER_ID|OWNER_SESSION_ID|PLAN_PATH|APPROVAL_ID|MODEL_EXPLICIT)$/.test(name)));
|
||||
const resumedProcess = spawn(resolve("node_modules/.bin/pi"), [...common.filter(arg => arg !== "--no-extensions"), "-e", resolve("src/index.ts"), "--session", supervisorFile!], { cwd, env: freshEnv });
|
||||
children.push(resumedProcess);
|
||||
supervisor = new Driver(resumedProcess);
|
||||
supervisor.send({ type: "prompt", id: "resumed-review", message: "Confirm resumed identity without sending another instruction." });
|
||||
await supervisor.wait(message => message.type === "agent_settled");
|
||||
const resumedTools = latestRequest.tools.map((tool: any) => tool.function.name);
|
||||
expect(resumedTools).toContain("ApproveGoal");
|
||||
expect(resumedTools).toContain("SteerWorker");
|
||||
expect(resumedTools).toContain("profile_inspection");
|
||||
expect(resumedTools).not.toContain("CompleteGoal");
|
||||
expect(JSON.stringify(latestRequest.messages)).toContain("autonomously extending the user's agency");
|
||||
console.log(`Native Pi pair: fork retained planning context; SteerWorker delivered exactly: ${advice}`);
|
||||
} finally {
|
||||
if (process.env.PI_GOALS_EVIDENCE_DIR) {
|
||||
mkdirSync(process.env.PI_GOALS_EVIDENCE_DIR, { recursive: true });
|
||||
for (const [name, driver] of [["worker", worker], ["supervisor", supervisor]] as const) if (driver) {
|
||||
writeFileSync(join(process.env.PI_GOALS_EVIDENCE_DIR, `${name}-events.jsonl`), driver.messages.map(message => JSON.stringify(message)).join("\n"));
|
||||
writeFileSync(join(process.env.PI_GOALS_EVIDENCE_DIR, `${name}-stderr.txt`), driver.stderr);
|
||||
}
|
||||
}
|
||||
for (const child of children.reverse()) if (child.exitCode === null) { child.kill("SIGTERM"); await once(child, "exit"); }
|
||||
server.closeAllConnections(); server.close();
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
}, 25_000);
|
||||
@@ -0,0 +1,31 @@
|
||||
// Wire two real GoalIntercom adapters. This router never invents hello replies.
|
||||
export function pairedIntercomFixture() {
|
||||
const receivers = new Map<string, (event: any) => void>();
|
||||
const endpoint = (id: string, peer: string) => {
|
||||
const sent: any[] = [];
|
||||
let connected = true;
|
||||
const transport = {
|
||||
sent,
|
||||
drop: (_message: any) => false,
|
||||
connect: (value: boolean) => { connected = value; receivers.get(id)?.({ type: "connection", connected: value, supported: true }); },
|
||||
events: {
|
||||
on: () => () => {},
|
||||
emit: (name: string, registration: any) => {
|
||||
if (name !== "intercom:extension-register") return false;
|
||||
receivers.set(id, registration.onEvent);
|
||||
registration.onReady({
|
||||
snapshot: () => ({ connected, supported: true }),
|
||||
publish: (message: any) => {
|
||||
sent.push(message);
|
||||
if (sent.length > 200) throw new Error("Handshake did not settle; possible hello loop.");
|
||||
if (connected && !transport.drop(message)) queueMicrotask(() => receivers.get(peer)?.({ type: "message", fromSessionId: id, payload: message }));
|
||||
},
|
||||
});
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
return transport;
|
||||
};
|
||||
return { worker: endpoint("worker", "supervisor"), supervisor: endpoint("supervisor", "worker") };
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { planViews } from "../src/plan-view.js";
|
||||
|
||||
it("keeps outcome, preferences and discriminators without tasks or history", () => {
|
||||
const plan = "# Outcome\nBeat random, not just plot it.\n## User preferences\nKeep costs low.\n## Goals\n1. [ ] goal: repair\n - discriminator: beats random\n - subtle failure mode: plot exists but result fails\n - tasks:\n 1. [x] draw plot\n - evidence:\n - old output\n2. [ ] goal: confirm\n## Task list\n- [ ] run it\n## Appendix\nunapproved idea";
|
||||
const views = planViews(plan);
|
||||
for (const text of ["Beat random", "Keep costs low", "goal: repair", "discriminator: beats random", "subtle failure mode", "goal: confirm"]) expect(views.short).toContain(text);
|
||||
for (const text of ["draw plot", "old output", "run it", "unapproved idea"]) expect(views.short).not.toContain(text);
|
||||
expect(views.long).toContain("draw plot");
|
||||
expect(views.long).toContain("old output");
|
||||
expect(views.long).not.toContain("unapproved idea");
|
||||
});
|
||||
|
||||
it("stops at history and preserves a manual goal tick", () => {
|
||||
const view = planViews("# Plan\n1. [x] goal: result\n## Log\n1. [ ] goal: historical");
|
||||
expect(view.short).toContain("[x] goal: result");
|
||||
expect(view.long).not.toContain("historical");
|
||||
});
|
||||
+35
-5
@@ -1,21 +1,51 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { completeGoalDescription, planDrafting, planningState, resync } from "../src/prompts.js";
|
||||
import { approveGoalDescription, completeGoalDescription, goalApprovalRecorded, planDrafting, planningState, resync, steerWorkerDescription, supervisorPeriodicReview, supervisorPlanChangeReview, supervisorReadyReview, supervisorStartedReview, supervisorStoppedReview } from "../src/prompts.js";
|
||||
|
||||
describe("supervisor event tasks", () => {
|
||||
it("asks a ready or stopped worker to resume through SteerWorker rather than only review", () => {
|
||||
expect(supervisorReadyReview).toContain("Use SteerWorker");
|
||||
expect(supervisorStoppedReview).toContain("judge whether the agreed goal is actually achieved");
|
||||
expect(supervisorStoppedReview).toContain("use SteerWorker to send the next useful instruction and resume work");
|
||||
expect(supervisorStoppedReview).toContain("verified dependency");
|
||||
expect(supervisorStoppedReview).toContain("only after the results satisfy the goal");
|
||||
expect(steerWorkerDescription).toContain("A recap alone does not send an instruction");
|
||||
});
|
||||
|
||||
it("asks if active work is on track and keeps productive work uninterrupted", () => {
|
||||
expect(supervisorPeriodicReview).toContain("Is the worker on track");
|
||||
expect(supervisorPeriodicReview).toContain("let productive work continue without interruption");
|
||||
expect(supervisorStartedReview).toContain("only if a correction is needed");
|
||||
expect(supervisorPlanChangeReview).toContain("claims, not proof of completion");
|
||||
expect(supervisorPlanChangeReview).toContain("Preserve authorized changes");
|
||||
});
|
||||
|
||||
it("defines approval as acceptance after outcome judgment, with mechanics separate", () => {
|
||||
expect(approveGoalDescription).toMatch(/^Use only after judging that the actual result satisfies/);
|
||||
expect(approveGoalDescription).toContain("mechanical checks cannot establish success");
|
||||
expect(approveGoalDescription).toContain("If the goal is unmet or evidence is insufficient, do not approve");
|
||||
expect(approveGoalDescription).toContain("\n\nRequirements:");
|
||||
expect(goalApprovalRecorded("repair")).toContain("Use SteerWorker to tell the worker to call CompleteGoal with this exact goal text");
|
||||
expect(completeGoalDescription).toContain("Worker-only sign-off");
|
||||
expect(completeGoalDescription).not.toContain("direct the worker to run it");
|
||||
});
|
||||
});
|
||||
|
||||
describe("planning prompt", () => {
|
||||
it("requires fact finding or a focused question before a goal", () => {
|
||||
expect(planDrafting).toContain("Use read-only repository tools or web search when either can\nresolve a fact.");
|
||||
expect(planDrafting).toContain("Ask at least three short, concrete questions");
|
||||
expect(planDrafting).toContain("understand the requested outcome, boundary, and how success will be judged");
|
||||
expect(planDrafting).toContain("record that\npoint as unknown; do not silently replace it with an inference");
|
||||
expect(planDrafting).toContain("Do not use a question quota");
|
||||
expect(planDrafting).toContain("Briefly reframe the request in your own words to check comprehension");
|
||||
expect(planDrafting).toContain("point as unknown; do not silently replace it with an inference or turn it into a new blocking decision");
|
||||
expect(planDrafting).toContain("answer materially reduces uncertainty\nwhile discovering the right plan");
|
||||
expect(planDrafting).toContain("self-contained: state the relevant\ncontext, use the human's language and ASD-STE100");
|
||||
expect(planDrafting).toContain("placeholder goal such as \"work out the thing\"");
|
||||
expect(planDrafting).toContain("material user decisions remain open");
|
||||
expect(planDrafting).toContain("Only withhold Ready for an unanswered choice that changes scope, spending, or the user-visible result");
|
||||
});
|
||||
|
||||
it("restores the same rule after compaction", () => {
|
||||
expect(planningState(".pi/plan/test.md")).toContain("web search\nwhen either can resolve a fact.");
|
||||
expect(planningState(".pi/plan/test.md")).toContain("choice that needs their approval");
|
||||
expect(planningState(".pi/plan/test.md")).toContain("Record unanswered questions as\nunknown and still present Ready");
|
||||
expect(planningState(".pi/plan/test.md")).toContain("self-contained round with relevant context and a recommendation");
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { RoleModels } from "../src/role-models.js";
|
||||
|
||||
function runtime(cwd: string) {
|
||||
const hooks = new Map<string, any>();
|
||||
const ctx = { cwd, model: { provider: "test", id: "planner" }, modelRegistry: { find: vi.fn((provider: string, id: string): any => ({ provider, id })) }, ui: { notify: vi.fn() } };
|
||||
const pi = {
|
||||
on: (name: string, fn: any) => hooks.set(name, fn),
|
||||
setModel: vi.fn(async (model: any) => { ctx.model = model; await hooks.get("model_select")({ source: "set", model }); return true; }),
|
||||
};
|
||||
const models = new RoleModels(pi as unknown as ExtensionAPI);
|
||||
return { models, ctx, pi, hooks, select: async (id: string, source = "set") => { ctx.model = { provider: "test", id }; await hooks.get("model_select")({ source, model: ctx.model }); } };
|
||||
}
|
||||
|
||||
it("remembers each role without automatic switching overwriting another role", async () => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "goals-role-models-"));
|
||||
try {
|
||||
const first = runtime(cwd);
|
||||
await first.models.enter("planning", first.ctx as any);
|
||||
await first.models.enter("worker", first.ctx as any);
|
||||
await first.select("small-worker");
|
||||
await first.models.enter("supervisor", first.ctx as any);
|
||||
await first.select("astra");
|
||||
await first.models.enter("planning", first.ctx as any);
|
||||
expect(first.ctx.model.id).toBe("planner");
|
||||
const resumed = runtime(cwd);
|
||||
await resumed.models.enter("worker", resumed.ctx as any);
|
||||
expect(resumed.ctx.model.id).toBe("small-worker");
|
||||
await resumed.models.enter("supervisor", resumed.ctx as any);
|
||||
expect(resumed.ctx.model.id).toBe("astra");
|
||||
await resumed.select("restored-default", "restore");
|
||||
expect(JSON.parse(readFileSync(join(cwd, ".pi/pi-goals/models/supervisor.json"), "utf8")).id).toBe("astra");
|
||||
console.log("Role preferences restored: planning=planner, worker=small-worker, supervisor=astra; restore events did not overwrite the choice.");
|
||||
} finally { rmSync(cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("fails on an unavailable remembered model without replacing the choice", async () => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "goals-role-unavailable-"));
|
||||
try {
|
||||
const first = runtime(cwd);
|
||||
await first.models.enter("supervisor", first.ctx as any);
|
||||
await first.select("astra");
|
||||
const path = join(cwd, ".pi/pi-goals/models/supervisor.json");
|
||||
const saved = readFileSync(path, "utf8");
|
||||
const resumed = runtime(cwd);
|
||||
resumed.ctx.modelRegistry.find.mockReturnValue(undefined);
|
||||
await expect(resumed.models.enter("supervisor", resumed.ctx as any)).rejects.toThrow("unavailable");
|
||||
expect(resumed.pi.setModel).not.toHaveBeenCalled();
|
||||
expect(readFileSync(path, "utf8")).toBe(saved);
|
||||
await resumed.models.enter("supervisor", resumed.ctx as any, true);
|
||||
expect(JSON.parse(readFileSync(path, "utf8")).id).toBe("planner");
|
||||
resumed.models.leave();
|
||||
await resumed.select("unrelated-model");
|
||||
expect(JSON.parse(readFileSync(path, "utf8")).id).toBe("planner");
|
||||
} finally { rmSync(cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { workerPiSupervise } from "../src/supervise.js";
|
||||
|
||||
const API_READY = "pi-supervise:api-ready:v1";
|
||||
const WORKER_STATE = "pi-supervise:worker-state:v1";
|
||||
const WORKER_PAIRED = "pi-supervise:worker-paired:v1";
|
||||
|
||||
function pi(events: EventEmitter): ExtensionAPI {
|
||||
return { events } as unknown as ExtensionAPI;
|
||||
}
|
||||
|
||||
describe("pi-supervise worker API", () => {
|
||||
it("discovers pi-supervise when it loads after pi-goals", async () => {
|
||||
const events = new EventEmitter();
|
||||
const worker = workerPiSupervise(pi(events));
|
||||
events.on(WORKER_STATE, (reply) => reply({ intercomId: "worker-id", paired: false }));
|
||||
events.emit(API_READY);
|
||||
expect((await worker).intercomId).toBe("worker-id");
|
||||
});
|
||||
|
||||
it("discovers an already-loaded pi-supervise and accepts duplicate paired events once", async () => {
|
||||
const events = new EventEmitter();
|
||||
events.on(WORKER_STATE, (reply) => reply({ intercomId: "worker-id", paired: false }));
|
||||
const worker = await workerPiSupervise(pi(events));
|
||||
let acknowledgements = 0;
|
||||
const paired = worker.waitForPair().then(() => { acknowledgements += 1; });
|
||||
events.emit(WORKER_PAIRED, { supervisorIntercomId: "supervisor-id" });
|
||||
events.emit(WORKER_PAIRED, { supervisorIntercomId: "supervisor-id" });
|
||||
await paired;
|
||||
expect(acknowledgements).toBe(1);
|
||||
});
|
||||
|
||||
it("rejects Ready when another supervisor already owns the worker", async () => {
|
||||
const events = new EventEmitter();
|
||||
events.on(WORKER_STATE, (reply) => reply({ intercomId: "worker-id", paired: true }));
|
||||
await expect(workerPiSupervise(pi(events))).rejects.toThrow("already paired");
|
||||
});
|
||||
|
||||
it("times out when the visible supervisor never pairs", async () => {
|
||||
const events = new EventEmitter();
|
||||
events.on(WORKER_STATE, (reply) => reply({ intercomId: "worker-id", paired: false }));
|
||||
const worker = await workerPiSupervise(pi(events), 1);
|
||||
await expect(worker.waitForPair()).rejects.toThrow("did not pair");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { restoredSupervisor } from "../src/supervisor-session.js";
|
||||
|
||||
const binding = { workerSessionId: "worker", ownerSessionId: "worker", planPath: "/repo/plan.md", approvalId: "pairing" };
|
||||
it("restores complete supervisor identity even for a stopped fork without worker state", () => {
|
||||
expect(restoredSupervisor([{ type: "custom", customType: "pi-goals-supervisor-binding", data: binding }])).toEqual(binding);
|
||||
});
|
||||
it("migrates an older supervisor marker without inventing a pairing", () => {
|
||||
expect(restoredSupervisor([
|
||||
{ type: "custom", customType: "pi-goals-state", data: { approvalId: "pairing", phase: null } },
|
||||
{ type: "custom", customType: "pi-goals-visible-supervisor-v2", data: { workerSessionId: "worker", planPath: "/repo/plan.md" } },
|
||||
])).toEqual(binding);
|
||||
});
|
||||
it("refuses incomplete supervisor identity instead of returning worker mode", () => {
|
||||
expect(() => restoredSupervisor([{ type: "custom", customType: "pi-goals-supervisor-binding", data: { workerSessionId: "worker" } }])).toThrow("incomplete");
|
||||
expect(() => restoredSupervisor([{ type: "custom", customType: "pi-goals-visible-supervisor-v2", data: {} }])).toThrow("Worker mode was not enabled");
|
||||
for (const customType of ["pi-goals-supervisor-binding", "pi-goals-visible-supervisor-v2"]) expect(() => restoredSupervisor([{ type: "custom", customType, data: null }])).toThrow(/worker mode was not enabled/i);
|
||||
expect(restoredSupervisor([{ type: "custom", customType: "pi-goals-state", data: { phase: "working" } }])).toBeUndefined();
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user