mirror of
https://github.com/wassname/pi-goals.git
synced 2026-09-11 12:43:57 +08:00
Compare commits
44
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43fc97aebd | ||
|
|
097349632c | ||
|
|
b0759fe016 | ||
|
|
cecb1e9119 | ||
|
|
d5729ac106 | ||
|
|
8e44738773 | ||
|
|
1c10a282ac | ||
|
|
3f0aadfffa | ||
|
|
f09d443d88 | ||
|
|
4fce680f2d | ||
|
|
a45cc7d9c6 | ||
|
|
d0070d18b8 | ||
|
|
7f274a691c | ||
|
|
17a3b25a82 | ||
|
|
2362073d04 | ||
|
|
c1bf91f3db | ||
|
|
3dd0668963 | ||
|
|
389af540d1 | ||
|
|
fa7195eafb | ||
|
|
0d972e81c3 | ||
|
|
1426877817 | ||
|
|
6bb34f18cf | ||
|
|
36b0d98c2a | ||
|
|
b173d145db | ||
|
|
8de5c35248 | ||
|
|
a778480fec | ||
|
|
45d59e1edc | ||
|
|
d026e06b41 | ||
|
|
9ebcd3f4a6 | ||
|
|
4850b5195e | ||
|
|
e8bcba0fa7 | ||
|
|
0a349b056e | ||
|
|
3a1afd4c8e | ||
|
|
d5766c1a34 | ||
|
|
310ab730cc | ||
|
|
6892801fcc | ||
|
|
35713760ad | ||
|
|
144f4b95b4 | ||
|
|
87cf14a28c | ||
|
|
2adba3da45 | ||
|
|
842f1b85c7 | ||
|
|
e6af6db3d9 | ||
|
|
7c36c119c6 | ||
|
|
7e427d2ca6 |
@@ -0,0 +1,81 @@
|
||||
# pi-goals contributor notes
|
||||
|
||||
## User intent for supervision
|
||||
|
||||
The following preferences are the user's words, recorded on
|
||||
[`experiment/goals-owned-supervision`](https://github.com/wassname/pi-goals/blob/06794bf/AGENTS.md#user-intent-for-this-branch):
|
||||
|
||||
> To be clear, the hope is we can have a smart supervisor like you, with judgment and context. But it doesn't use many tokens as it checks in and sees an overview.
|
||||
>
|
||||
> It steers a smaller model, adding perspective and judgment.
|
||||
>
|
||||
> It compacts every 150k or similar to avoid cost and context rot.
|
||||
>
|
||||
> It has a goal / plan on a Ralph-loop-type repeat.
|
||||
>
|
||||
> That lets the worker be a cheaper model, and the supervisor more expensive, and still get a good outcome.
|
||||
>
|
||||
> Oh, and since it's two panes, the user can review both!
|
||||
>
|
||||
> Well, I want to see what the supervisor is thinking and saying. That's the whole point: all supervisor thinking and messages should be visible.
|
||||
>
|
||||
> So that should make it obvious that I need to see the messages, and the supervisor needs to use judgment. For example, it could say how we are tracking or whatever every time, and it would be useful, like in the recap.
|
||||
>
|
||||
> And it would only be a few output tokens.
|
||||
|
||||
— wassname; spelling and punctuation corrected in the source by Pi/OpenAI.
|
||||
|
||||
## Confirmed product preferences
|
||||
|
||||
- One installable pi-goals package, not separately configured supervisor packages.
|
||||
- Run the full normal Pi profile and package set in two real interactive sessions, visible beside each other in Herdr. Keep extensions, skills, prompt templates, themes, configuration and authentication. Do not silently launch a reduced profile with `--no-extensions`; honor deliberate worker resource choices. Supervisor mode changes the role/model and enforces its inspection-only policy, not a separately assembled installation. Fork the main planning session, activate supervisor mode and compact the fork.
|
||||
- The supervisor retains the compacted planning session. Its repeated review loop reminds it that it is the supervisor and supplies the current canonical plan. Keep those directly available rather than relying on the compaction summary alone.
|
||||
- Load Intercom once per Pi process. Do not add a second copy or extra standalone supervisor package when activating supervisor mode.
|
||||
- The worker carries implementation detail. The supervisor gets incremental high-level views and retains its judgments, user intent and decisions.
|
||||
- Remember the last model selected separately for planning, working and supervising.
|
||||
- Ask material unresolved alignment questions, not a fixed quota. Inspect technical facts yourself; do not ask for confirmation of ordinary implementation details or repeat answered questions. Batch high-impact questions with context and a recommendation. An explicit current-plan request to skip optional questions applies only to that plan; it does not grant missing permission.
|
||||
- Discuss returns the review menu to normal chat, preserving the draft. Do not immediately reopen the menu while the conversation is unfinished. Ready is the human's approval to start work.
|
||||
- Live supervision must not use headless Pi RPC mode or a bespoke plan-lifecycle RPC layer. Use the real sessions and Intercom messages for views, steering and plan-bound approval checkpoints.
|
||||
- Keep the design simple and reliable. The user reports that it is constantly breaking; adding more orchestration or approval forms is not progress. Preserve working components and remove unnecessary layers.
|
||||
- Show useful supervisor assessments, advice and perspective, not only hidden tool arguments or delivery receipts. Keep the assessment brief. The supervisor's job is judgment and helping the worker stay on course, not filling forms; transport and approval bookkeeping are supporting details.
|
||||
- After the initial fork compaction, compact the supervisor again above 100k current-context tokens (not cumulative usage), respecting the model's context limit. This is the latest user clarification of the earlier approximate 150k preference. Token/cost savings and the usefulness of advice need a real task trial; passing protocol tests alone does not establish either.
|
||||
|
||||
- Supervise autonomously until the agreed result is achieved and inspected. Investigate claims of being blocked, waiting, unable to proceed, or done; change ineffective steering, and keep authorized independent work moving. Respect genuine dependencies, explicit human pauses, scope and permission limits. Do not make the human drive routine progress.
|
||||
- Keep supervision instructions generic and outcome-focused. Approval bookkeeping supports delivery; it is not the deliverable. Inspect actual artifacts and execution evidence, not just summaries, checked boxes, or test counts. Manual completion checkboxes are claims until CompleteGoal records sign-off. Accepted inconclusive remains explicitly uncertain under fail-forward policy.
|
||||
- Git status is a review guideline, not a hard acceptance gate. Unrelated dirty files and ignored output directories can be legitimate. Do not force cleanup, commits, or a dirty-state fingerprint framework.
|
||||
|
||||
These are user preferences, not a claim that the current implementation satisfies every point.
|
||||
Validate them in real panes as well as automated tests; record remaining gaps and actual per-role usage.
|
||||
|
||||
## Tests
|
||||
|
||||
Run `npm test` before a commit. It includes unit and flow tests plus the RPC review test.
|
||||
|
||||
For an explicitly approved trusted-package update, a command-scoped npm release-age exception is allowed. Keep the default policy intact; do not turn a targeted update into a general package upgrade. The user approved pi-subagents 0.66.0 on 2026-09-09.
|
||||
|
||||
- `test/*.test.ts` unit and flow tests use a small Pi API mock. They check plan state, tool gates, and plan-file updates.
|
||||
- `npm run test:rpc` runs `test/rpc-review.test.ts`. It starts the installed Pi executable in RPC mode, uses Pi's real `select` protocol and conversational Discuss flow, and uses a local deterministic HTTP model. It does not need a credential or spend API credits. This is the closest automated session test.
|
||||
- Use tmux for visual TUI debugging when the RPC test fails or a terminal-only problem is reported:
|
||||
|
||||
```bash
|
||||
tmux new-session -s pi-goals-debug 'cd /path/to/pi-goals && pi -e .'
|
||||
```
|
||||
|
||||
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`, `Discuss`, `Edit`, or `Cancel`.
|
||||
|
||||
- `npm run test:supervisor` runs the inherited `node:test` supervisor regressions. `npm test` also includes the always-enabled packed-artifact Intercom flow; Linux requires Unix-socket support.
|
||||
|
||||
## Functional acceptance: real isolated Herdr workflow
|
||||
|
||||
Pi/OpenAI procedure, requested by wassname; adapted from `8953dce`. 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 uncommitted changes being tested. Use a packed test package without replacing the active installation.
|
||||
2. Start real interactive Pi with that package and available, different worker and supervisor models. Test the full normal profile on both sides, not two equally stripped profiles. Isolate only the candidate pi-goals package selection; do not load both old and candidate copies. Keep global settings untouched; if temporary non-secret role preferences must change, save and restore all three with guarded cleanup.
|
||||
3. Use `/goals` for a trivial, bounded two-goal task: two exact-content files plus saved byte-verification output, with an ignored output directory and an unrelated dirty file to preserve. No GPU, dependencies or unrelated work. Read the planning conversation, verify that questions are material, inspect the draft, exercise ordinary-chat Discuss, and select Ready through the actual UI.
|
||||
4. Confirm Ready opens a visible supervisor pane and the worker starts. Read both panes. Verify exact supervisor advice is visible, reaches the worker, and helps progress. Delivery receipts alone are not proof. Let the same pair stay active between both goals.
|
||||
5. Let the worker produce artifacts and real verification output, then complete both CompleteGoal calls (retained supervisor `review_goal`, followed by the fresh evidence judge). Do not perform the worker's task. Record each manual nudge or repair as an intervention, not autonomous success. Conclusive and accepted-inconclusive results are not equivalent.
|
||||
6. Inspect the actual artifacts and saved execution evidence, final plan, and both sessions. Handwritten output or a manual tick does not prove execution. Success means the requested results and both observed sign-offs, not tests passing or messages exchanged. Verify no commit/cleanup was forced for ignored outputs or unrelated dirt.
|
||||
7. Exercise worker-only, supervisor-only and both-side reloads; fresh-shell resume without special launcher environment; drafting/Discuss; Ready/startup compaction; pending completion; and a stopped pairing. Preserve the plan, role, restrictions and peer identity. Interrupted decisions must fail visibly and allow retry, not approve stale work or create duplicate panes. Planning reload must not become approval or get permanently stuck. Record any recovery action needed and commands actually available.
|
||||
8. If a stage fails, read both panes and the exact error before diagnosing it. Fix the cause, reload only the test instance, and retry that stage. After a prompt change use a fresh task. Repeated status checks are not a repair; wait-output timeouts/matches only signal that the pane needs inspection.
|
||||
9. Save pane captures, session/artifact/log paths, revision, interventions, remaining failures, and separate role usage under `docs/slop/reviews/`. Assess usefulness and actual token/cost use, not a test-count substitute. Only close panes you created. Do not release, merge, or replace the user's installation as part of acceptance.
|
||||
@@ -0,0 +1,3 @@
|
||||
# ARCHIVED
|
||||
|
||||
Superseded by [pi-goals](https://github.com/wassname/pi-goals).
|
||||
@@ -1,24 +1,45 @@
|
||||
# pi-goals
|
||||
|
||||
Plan mode for agreeing on goals before any code gets written. Each goal names the subtle failure
|
||||
mode that could fake a "done" and the discriminator that tells real success from it. Everything
|
||||
lives in one markdown file, `.pi/plan.md`, which the agent edits with its normal edit tool and which
|
||||
doubles as the task list. A goal is signed off only after a fresh read-only judge checks its
|
||||
evidence against the repo.
|
||||
Make a short list of goals in one Markdown plan file. This is easy to review, and a subagent can check whether each goal is complete.
|
||||
|
||||

|
||||
The plan file looks like this:
|
||||
|
||||
The file has a fold at `## Log`. Above it is the working set: title, the human's own words, goals,
|
||||
discriminators. Below it is durable memory: the log, the learnings, and an unlimited unverified
|
||||
appendix. The working set is re-sent when the plan goes stale for a couple of turns; the whole file
|
||||
comes back at session start and after a compaction, which is when the settled context is gone.
|
||||
```md
|
||||
## <short plan title>
|
||||
|
||||
Design bet (v2): the plan file is for LLMs and the human, not for TypeScript. There is no parser and
|
||||
no schema. The format is a convention taught by a prompt; the judge, being a model, reads the file
|
||||
natively, finds the claimed goal itself (wording drift is fine), and validates the evidence in
|
||||
words. It cannot execute anything, so it never re-runs your `verify` command: you run it and save
|
||||
the output as evidence. This deleted ~800 lines of v1 (parser, exact-string goal matching, verify
|
||||
runner, JSON-stream judge transport, review menus) and with them the footguns they caused.
|
||||
<context: one short paragraph. What the human wants and why.>
|
||||
|
||||
### User-visible result
|
||||
|
||||
<one concrete sentence naming the final artifact or behavior the human will inspect>
|
||||
|
||||
### User voice
|
||||
|
||||
- │ "<the human's requirement, quoted in full word for word (with spelling fixes)>"
|
||||
|
||||
### Goals
|
||||
|
||||
1. [ ] goal: <one short judgeable imperative outcome>
|
||||
- subtle failure mode: <a way this could look done but isn't>
|
||||
- discriminator: <the concrete observation that tells real success from that failure>
|
||||
- tasks:
|
||||
1. [ ] <subtask>
|
||||
- evidence: (empty until sign-off)
|
||||
|
||||
### Future work / out of scope
|
||||
|
||||
### Log
|
||||
|
||||
### Interview
|
||||
|
||||
### Learnings
|
||||
|
||||
### Papercuts - problems, gotchas, suggestions
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Related work
|
||||
|
||||
Like [pi-milestones](https://github.com/Neuron-Mr-White/UniPi/tree/main/packages/milestone) and
|
||||
[burneikis/pi-plan](https://github.com/burneikis/pi-plan), it guides rather than guards. The
|
||||
@@ -35,7 +56,7 @@ Or for development:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/wassname/pi-goals && cd pi-goals && npm install
|
||||
pi -e ./src/index.ts
|
||||
pi -e .
|
||||
```
|
||||
|
||||
## Use
|
||||
@@ -46,107 +67,226 @@ pi -e ./src/index.ts
|
||||
|
||||
`/goals` enters plan mode and starts a conversation; the objective is an optional seed. From there:
|
||||
|
||||
1. Plan. The agent explores read-only (edit/write are blocked except on the plan file itself), asks
|
||||
about anything unclear, and drafts the goals into `.pi/plan.md`. The drafting rules are sent
|
||||
once, with your objective, not re-sent every turn.
|
||||
2. Review. Read the file; a menu asks Ready, open in `$EDITOR`, or keep planning. To revise, just
|
||||
reply. Plan mode ends when you pick Ready.
|
||||
3. Work. The agent ticks subtasks, appends to `## Log` and `## Learnings`, fills `evidence:`, and
|
||||
calls `CompleteGoal` when a discriminator is satisfied. If it leaves the plan untouched for two
|
||||
turns, the working set is sent back with a short upkeep reminder.
|
||||
1. Align. The agent inspects technical facts read-only, then asks only material unresolved
|
||||
questions about outcome, scope, constraints, or success criteria. There is no fixed quota or
|
||||
confirmation ritual for ordinary implementation details. It waits for required answers before
|
||||
proposing the final plan. An explicit “no questions” or “skip questions” clause waives optional
|
||||
questions for that plan only, not missing permissions.
|
||||
“No q's” and “skip q's” are also supported. Negated instructions (“do not skip questions”) and
|
||||
quoted feature references (“add a 'skip questions' button”) do not waive alignment.
|
||||
2. Review. When alignment is complete, the agent requests review and the full draft is printed.
|
||||
Check that User-visible result names the artifact or behavior you expect. The menu offers
|
||||
**Ready / Discuss / Edit / Cancel**. Discuss returns to normal chat and asks useful alignment
|
||||
questions, not a refinement-notes editor. Keep talking for as many turns as needed; the old draft
|
||||
alone cannot reopen the menu. When discussion is finished, the agent calls `RequestPlanReview`,
|
||||
even if the draft is unchanged. Discussion state survives reload. Edit opens the full plan directly.
|
||||
Escape also returns to chat and preserves the draft; explicit Cancel discards the current draft.
|
||||
3. Work. Ready is the only review action that starts work. The agent ticks subtasks, appends to
|
||||
`## Log` and `## Learnings`, fills `evidence:`, and calls `CompleteGoal` when a discriminator is
|
||||
satisfied. Every human reply in plan mode is saved verbatim under `## Interview`.
|
||||
After eight turns without a change above `## Log`, the next natural prompt includes a saved
|
||||
extension message with the working set and a short upkeep reminder.
|
||||
|
||||
Other commands: `/goals clear` empties the plan file; `/goals judge <model-ref>` picks a specific
|
||||
model for the sign-off judge (default: your current session model, else pi's default).
|
||||
Plan reminders are saved in session history before they reach the model, not appended only to an
|
||||
outgoing request. In working mode, after startup or compaction, the next natural prompt refreshes
|
||||
the whole plan, including its appendix, from disk. In planning mode, it refreshes the planning-policy
|
||||
snapshot and plan path instead. Automatic compaction and tool-loop continuations do not start
|
||||
an extra turn for a reminder: the refresh waits until the next `before_agent_start` (normally the
|
||||
next user prompt). There is no fresh plan reminder during that ongoing automatic continuation.
|
||||
|
||||
Coming from v1: a leftover `.pi/goals.md` is renamed to `.pi/plan.md` on session start.
|
||||
## Plan supervisor and auto-continue
|
||||
|
||||
## The plan.md format (a convention, not a schema)
|
||||
Steward supervision and 60-minute auto-continue are enabled by default. A real supervisor starts at
|
||||
Ready. Auto-continue is the fallback when stewardship is off; it does not run a competing timer while
|
||||
supervision is enabled. Use `/goals steward off` or `/goals auto off` to opt out. Explicit preferences
|
||||
survive clear and reload. Cleared legacy sessions adopt the new defaults on reload; active legacy
|
||||
plans retain their settings so supervision is not attached midway through work.
|
||||
|
||||
```markdown
|
||||
# ship the cache layer
|
||||
Install/load **only pi-goals**. Its internal modules contain the supervisor; the package bundles
|
||||
`pi-intercom` 0.10.0 and `@sting8k/pi-vcc` 0.5.0 as locked runtime dependencies. An already installed
|
||||
Intercom is reused; otherwise pi-goals initializes its bundled copy after installed extensions load.
|
||||
There is one Intercom registration per process, not an extra supervisor companion. VCC is used as
|
||||
a compiler, not loaded as another extension. Pi core stays a peer dependency. **Herdr remains the
|
||||
supported terminal host**. Both panes use the same Pi agent directory and configured package set;
|
||||
explicit CLI resource choices are preserved without adding `-e` companions.
|
||||
|
||||
Latency target came from the SLO review; keep the existing client API.
|
||||
Ready is the human's plan approval. Pi-goals forks the planning session, initializes the supervisor
|
||||
with the original plan and supervisor policy, and waits for acknowledged pairing before starting the
|
||||
worker. The initial supervisor view can steer; it is not another mandatory plan-approval gate.
|
||||
The supervisor fork is compacted unless its known context is already at most 20k tokens. If native Pi
|
||||
reports that no older history is eligible for compaction, startup retains the fork unchanged and continues;
|
||||
other compaction failures still block initialization. Pi-goals requests compaction only for the
|
||||
supervisor. Later reviews compact above 100k **current-context** tokens (not cumulative
|
||||
usage), or earlier for a smaller model context limit. Each model call explicitly supplies the
|
||||
supervisor role and current canonical plan while retaining the compacted planning context and judgments.
|
||||
|
||||
## User voice
|
||||
The internal supervisor provides incremental VCC views and retains its decisions. While an assessment
|
||||
is active, later routine updates coalesce into one pending marker instead of queuing model turns or
|
||||
replacing the active view. Once settled, it requests one fresh VCC overview from the worker's latest
|
||||
compaction summary and current branch. This is a bounded high-level summary, not a lossless transcript;
|
||||
truncation is labeled and the overview names the read-only source session for omitted detail.
|
||||
Each explicit goal checkpoint carries a fresh bounded worker snapshot, including the latest user direction;
|
||||
it becomes visible only when that checkpoint is assessed. The canonical plan and checkpoint identity remain
|
||||
separate from replaceable routine status.
|
||||
The plan-aware policy checks every 50 model turns or 60 minutes, or when the worker settles with no tracked
|
||||
background work. Absent optional trackers contribute zero tracked work. Installed process/subagent
|
||||
providers that cannot answer remain unknown and do not prove the worker is finished. Unregistered
|
||||
detached work is not tracked. The `SUPERVISOR.md` policy lookup is unchanged: project `.pi/SUPERVISOR.md`, then the Pi agent directory, then the built-in policy. Auto-continue is
|
||||
suspended while the steward is enabled so there is only one continuation policy.
|
||||
|
||||
- > "keep the client API, I don't want to touch every call site"
|
||||
At each review the supervisor is prompted to visibly give a brief progress assessment and useful
|
||||
advice, not just a delivery receipt. Its advice and no-intervention assessment are also displayed
|
||||
as durable messages. Supervisor mode allows native read/grep/find/ls and the narrow supervisor
|
||||
tools only. Writes, bash (including `!`), process/subagent/scheduler and unknown extension tools
|
||||
are blocked at execution as well as hidden, including on reload and in a stopped plan fork.
|
||||
This is a model-tool policy, not an OS sandbox for arbitrary trusted extension code.
|
||||
|
||||
## Goals
|
||||
All worker/supervisor traffic uses Intercom. Local lifecycle calls are ordinary pi-goals module
|
||||
calls, with no plan-lifecycle RPC dispatcher or headless live Pi process. Disconnection invalidates
|
||||
pending approval and is shown explicitly; a send does not prove receipt or execution.
|
||||
|
||||
1. [/] goal: Implement cache layer
|
||||
- subtle failure mode: cache silently bypassed, latency ok by luck
|
||||
- discriminator: hit-rate > 0.8 in load-test.log (a bypass reads ~0)
|
||||
- verify: pytest tests/cache -q && python bench/p95.py --max-ms 50
|
||||
- tasks:
|
||||
1. [x] wire cache client
|
||||
2. [/] eviction policy
|
||||
- evidence:
|
||||
- > load-test.log: p95=41ms, hit-rate 0.93 (not bypassed)
|
||||
One `CompleteGoal` call asks this supervisor about direction and scope, then runs the normal fresh
|
||||
read-only evidence judge. Use one unique exact goal subject (case and surrounding whitespace do not
|
||||
matter); ambiguous or drifted wording gets an actionable retry, not a manual-tick fallback.
|
||||
Manual `[x]` marks are visible completion claims, not sign-off, even before this tool is called or
|
||||
after reload. A prematurely checked submitted goal is reopened before review. Only accepted sign-off
|
||||
checks it again and persists a per-goal record; observed reopening invalidates that record. The widget
|
||||
and supervisor distinguish conclusive acceptance from **accepted inconclusive** (judge failure or no
|
||||
verdict). Inconclusive still permits fail-forward, but is not verified completion. Git status is context,
|
||||
not a gate: the judge can inspect cited uncommitted and ignored files directly. No commit or clean
|
||||
worktree is required unless the goal itself requires it.
|
||||
|
||||
## Future work / out of scope
|
||||
Older sessions have no trusted per-goal records. Their existing checkboxes/evidence/logs are preserved
|
||||
as “legacy completion — sign-off not recorded,” not rejected or automatically reimplemented. Use normal
|
||||
CompleteGoal re-review if needed; editable historical log text is not imported as trusted sign-off.
|
||||
Stopped pairings remain stopped. New worker views include current completion claims and whether the
|
||||
canonical plan changed; a manual tick cannot end supervision. Ordinary supervisor prose and genuine
|
||||
questions no longer suppress later worker direction. Explicit human pauses remain instructions to
|
||||
respect, not a reason to discard new views; idle responses do not immediately retry themselves.
|
||||
The judge's checks section accepts ordinary numbered and indented Markdown lists,
|
||||
but an empty section cannot borrow a list from a later heading. Approving one goal does not finish supervision. Cancelled, stale or
|
||||
mismatched replies do not sign off goals. Goal/revision identity is bound in code to the checkpoint
|
||||
actually presented to the supervisor, not copied into a form by the model. Supervisor model checkpoints
|
||||
have no arbitrary thinking deadline: slow healthy reviews may finish. Explicit cancellation, replaced
|
||||
plans, disconnects and actual settled provider failures still fail safely; startup/attachment deadlines
|
||||
are separate. A genuinely settled empty response returns an incomplete assessment, not an invented
|
||||
human-input dependency. Later worker progress/cadence can resume supervision without a human poke,
|
||||
and failure does not immediately retry the same view. A required completion checkpoint may wait, but routine supervision does not block worker
|
||||
work. `/goals steward off` ends this plan's supervision and
|
||||
cancels pending goal requests; it does not close the human's terminal pane.
|
||||
|
||||
## Log
|
||||
- 2026-06-15 14:02 cache client wired; eviction next
|
||||
Navigation: `/goals supervisor` focuses the supervisor, `/goals worker` returns to the worker, and
|
||||
`/goals zoom` toggles supervisor zoom. These use the real Pi panes, not a Fleet inspector. If the
|
||||
recorded pane is unavailable, its location/liveness is unknown. Locate the existing session first;
|
||||
only after confirming it is no longer running, reopen the saved `pi --session` path shown in the
|
||||
error. Pi-goals never starts a duplicate merely because a pane ID is missing.
|
||||
|
||||
## Learnings
|
||||
- the client retries on 503, so a cache miss storm looks like latency, not errors
|
||||
After completion, keep the plan as a record. Ordinary auto-continue stops when no open goals remain.
|
||||
The supervisor's `done` ends the pairing and its watch timer; it leaves the terminal and saved session
|
||||
available for inspection. `/goals clear` is the manual way to disconnect. A later `/goals plan …`
|
||||
creates a new plan version and starts a new supervisor fork at Ready rather than reusing the completed
|
||||
plan's pairing. You can close an old supervisor pane after supervision has ended.
|
||||
|
||||
## Appendix (context, not approved)
|
||||
Other commands: `/goals clear` disconnects this session, preserving its plan file;
|
||||
`/goals auto [minutes|off]` controls ordinary auto-continue; `/goals judge <model-ref>` overrides the
|
||||
fresh judge's model; `/goals steward status` reports supervision. Use `/goals plan <objective>` for
|
||||
objectives beginning with reserved command words, such as `/goals plan judge the vendor options`.
|
||||
The old `--clear`, `--auto`, and `--judge` forms remain compatibility aliases.
|
||||
|
||||
For a local trial, start inside Herdr with just this checkout:
|
||||
|
||||
```bash
|
||||
pi -e /path/to/pi-goals
|
||||
```
|
||||
|
||||
- A goal is a checkbox line beginning `goal:` (`[ ]` open, `[/]` active, `[x]` done, `[-]`
|
||||
cancelled). Indented checkbox lines under a goal are its subtasks. Those two patterns plus the
|
||||
`## Log` fold are the only things the extension itself reads; everything else is prose to it.
|
||||
- The `discriminator` is the success test, written while planning: the positive observation that the
|
||||
goal succeeded and that none of the `subtle failure mode`s could fake. `evidence` is the proof,
|
||||
filled at sign-off: each item pairs a durable artifact with a short read of it. Prefer committed
|
||||
artifacts (files, tests, diffs); `.pi/` is usually gitignored, so evidence there is judge-time
|
||||
proof only and won't survive in history.
|
||||
- `## User voice` holds the human's requirements word for word. A paraphrase drifts, and then the
|
||||
goals churn on the next reply.
|
||||
- `## Learnings` is what you now know, one line per gotcha, deduped. `## Log` is what happened, one
|
||||
line a turn. `## Appendix` is unlimited and unverified: alternatives, links, dead ends, and
|
||||
settled detail that is not part of the approved goals.
|
||||
- Small format deviations are fine; the file is read by the human and the judge, not a parser.
|
||||
- The agent prunes finished goals itself when the working set gets long (evidence survives in git
|
||||
history and `## Log`).
|
||||
Then draft a plan and select Ready; no enable command is needed. Initialization failure stays in
|
||||
planning and names the unavailable component; resolve it in the supervisor pane, or turn the steward
|
||||
off and retry Ready. Sessions saved with the older checkpoint-only steward need a new Ready handoff;
|
||||
old pi-subagents reviewer runs are not reused as supervisor sessions.
|
||||
|
||||
## Signing off a goal (`CompleteGoal`)
|
||||
### Migrating an already-running installation
|
||||
|
||||
`CompleteGoal(goal)` is the one blessed tool. It spawns a strictly read-only `pi` subprocess (`-p
|
||||
--no-session --no-extensions`, tools `read,grep,find,ls` -- no bash, no edit/write) with the whole
|
||||
plan file and the claimed goal. The judge cannot execute anything, so it never re-runs your verify
|
||||
command (which may be a 10-hour training job); the agent runs `verify` itself and saves the output
|
||||
as evidence. The judge reviews evidence discipline in order -- anything here at all? each item
|
||||
quoted and attributed? provenance visible (how was this produced)? do the quotes match the cited
|
||||
files on disk? -- and only then the substance, returning `VERDICT: accept | reject` plus what's
|
||||
missing. It reads the live working tree, not HEAD: uncommitted work counts, and committing before
|
||||
sign-off is for durable evidence, not for the judge's visibility.
|
||||
After validating this package, remove any old standalone supervisor entry. A compatible standalone
|
||||
Intercom may remain: pi-goals reuses it rather than registering a second copy. **Reload existing workers before selecting Ready again**:
|
||||
an old worker still has old launch arguments in memory and can launch both old and internal copies.
|
||||
Reload both sides of a retained pairing. Do not add extra `-e` supervisor/Intercom arguments.
|
||||
Duplicate Intercom registries are diagnosed and plan bootstrap is refused; Pi also reports conflicting
|
||||
tools from duplicate packages. Diagnose/remove the duplicate rather than starting more panes. No
|
||||
settings or live panes are changed by this extension's migration.
|
||||
|
||||
- accept: a sign-off line is appended to `## Log` and the goal is ticked `[x]` in the same write
|
||||
(exact goal-line match only; on wording drift the result asks the agent to tick it). The
|
||||
tool-written log line is the audit trail; a hand-tick without one shows in the diff.
|
||||
- every run saves the judge's full transcript to `.pi/judge/<stamp>.md`, referenced from the log
|
||||
line, so "what did the judge actually check?" stays answerable after the fact.
|
||||
- reject: the goal stays open and the agent gets the missing list.
|
||||
- judge ran but failed/errored/timed out, or returned no VERDICT line: accepted inconclusive,
|
||||
logged as such. There is no pre-emptive "no model" path -- a null judgeModel just omits
|
||||
`--model` so pi's configured default runs the judge, so inconclusive always means "ran but
|
||||
failed", never "couldn't start". The working agent is never blocked on judge infra.
|
||||
### Remembered role models
|
||||
|
||||
Choose with `/model` or Pi's model-cycle shortcut in planning, the worker, or the supervisor. Each
|
||||
pi-goals role remembers its own last explicit provider/model. On first use it inherits the current
|
||||
model; no provider is hardcoded. Planning is restored on `/goals`, worker at Ready **after** the
|
||||
planning fork is captured and before pairing activation/the work handoff, and supervisor before its initial compaction
|
||||
or first turn. Reload/resume and later plans restore those choices. The supervisor does not inherit
|
||||
the worker's goal tools or auto-continue policy.
|
||||
|
||||
Storage is under `getAgentDir()/pi-goals/` (normally `~/.pi/agent/pi-goals/`):
|
||||
`planning-model.json`, `worker-model.json`, and `supervisor-model.json`. Each atomic file contains
|
||||
only `{"provider":"…","id":"…"}`. Different role processes never rewrite each other's file; competing
|
||||
explicit choices in the *same* role are last-write-wins. No credentials or thinking-level preferences
|
||||
are stored. Automatic `setModel` and Pi's restore events do not replace role preferences.
|
||||
|
||||
If a remembered model is missing or unauthenticated, the role pauses with an error instead of
|
||||
silently using a different provider. Configure the saved model and reload, or explicitly select a
|
||||
different available model with `/model`. Pi does not emit a selection event when you choose the
|
||||
already-current model; use **`/goals model current`** to explicitly save that current model for the
|
||||
paused role instead. This command verifies authentication before replacing the saved choice.
|
||||
|
||||
A worker-model failure at Ready keeps the attached supervisor inactive and persists **worker** as
|
||||
the recovery target across reload. Recovery updates the worker preference, not the planner's, and
|
||||
reoffers the existing Ready menu; Ready retries the same fork without another approval stage.
|
||||
There is no activation, supervisor review turn or work handoff before the worker model is usable.
|
||||
`/goals clear` and `/goals steward off` still stop/cancel supervision while a model is unavailable;
|
||||
old supervisor directives cannot restart the stopped work. A failed restore or recovery never
|
||||
silently replaces a saved model choice.
|
||||
`/goals judge <model-ref>` remains a separate override for the fresh evidence judge; it never changes
|
||||
these role files.
|
||||
|
||||
## Prompts
|
||||
|
||||
All model-facing text lives in [`src/prompts.ts`](src/prompts.ts), in flow order.
|
||||
Planning/judge text lives in [`src/prompts.ts`](src/prompts.ts); supervisor policy/text lives in [`src/internal/supervisor/prompts.ts`](src/internal/supervisor/prompts.ts).
|
||||
|
||||
## Develop
|
||||
|
||||
```bash
|
||||
pi -e ./src/index.ts # load locally
|
||||
npm test # vitest: judge argv invariants, appendLog, decideSignOff fail-forward
|
||||
pi -e . # one package; reuses installed or bundled Intercom
|
||||
npm test # unit/flow/RPC + inherited node:test supervisor regressions
|
||||
npm run test:rpc # real-Pi conversational review, local offline model
|
||||
npm run test:supervisor # inherited lifecycle/VCC/correlation/recovery regressions
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
No supervisor checkout or opt-in environment variable is needed. `test/rpc-supervisor.test.ts`
|
||||
performs `npm pack`, extracts the tarball outside the checkout, and runs real Pi sessions plus the
|
||||
actual bundled Intercom broker and a fresh offline evidence judge. Only Herdr is mocked: the worker's
|
||||
exec adapter imports the extracted goals entry, and the supervisor loads the untouched extracted
|
||||
package manifest. The test checks bundled production resources and excludes bundled Pi core peers.
|
||||
It requires Unix sockets on Linux and spends no API credits. The hook integration additionally
|
||||
checks initial supervisor compaction/model order, native planning-fork capture, cancellation, two
|
||||
goal reviews, and judge isolation without relying on a live terminal.
|
||||
|
||||
The moved VCC dependency is source-only and has upstream type incompatibilities with current Pi/Intl
|
||||
unions. `tsconfig.build.json` maps just its four imported API surfaces to narrow local declarations;
|
||||
the actual pinned VCC source still runs in tests and production. The Intercom extension factory
|
||||
has the same narrow declaration boundary; no dependency code is rewritten. All pi-goals source is typechecked
|
||||
and linted; the node:test suite is run separately, not silently collected/skipped by Vitest.
|
||||
|
||||
Baseline validation (2026-09-07, before the current supervision changes): **67 Vitest tests and 118 internal supervisor tests passed, with no skips**,
|
||||
including the packed real-Pi/Intercom flow. Typecheck, lint, build and diff checks passed. Independent
|
||||
review and targeted recheck are complete. See the [saved validation and review disposition](docs/reviews/2026-09-07_single-package-role-models.md).
|
||||
|
||||
The current isolated-worktree validation is recorded in [the approved supervision plan](docs/slop/plans/20260908_simple-visible-supervision.md). Packed registration/reload is also tested with Intercom loaded before or after pi-goals. The full Intercom peer flow and Herdr UAT need host Unix-socket/control access; do not treat registration alone as a successful supervision trial.
|
||||
|
||||
Neither automated test proves visual Herdr rendering/navigation or measured token savings. The
|
||||
previous live trial contained a historical tool call without a saved result, which can still block
|
||||
supervisor `done`. Use `/goals clear` to explicitly disconnect; genuine outstanding-work checks have
|
||||
not been weakened. See [the prior validation record](docs/reviews/2026-09-07_supervisor-validation.md).
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
MIT. See [third-party provenance and notices](THIRD_PARTY_NOTICES.md).
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Third-party provenance and notices
|
||||
|
||||
## Internal supervisor
|
||||
|
||||
`src/internal/supervisor/` and `test/internal-supervisor/` were moved from
|
||||
[wassname/pi-supervise](https://github.com/wassname/pi-supervise) (formerly
|
||||
pi-intercom-supervisor), commit `145c2cb081f85c08b0244c4a2c8a2d9aef8debda`.
|
||||
The source package is `@wassname2/pi-supervise` 0.0.4, author wassname, declared
|
||||
license MIT. The source comments and attribution are retained. Local changes
|
||||
integrate package loading, role-model readiness, duplicate-registration diagnostics,
|
||||
and strict build/lint compatibility. Its synthetic fork fixture is retained; it
|
||||
contains no user transcript.
|
||||
|
||||
The supervisor's `subagents.ts` retains its attribution to
|
||||
`@monotykamary/pi-supervisor` (MIT), `src/subagent-detector.ts`. Its policy precedence
|
||||
also follows that project. No separate supervisor package is required at runtime.
|
||||
|
||||
### MIT license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
## Bundled dependencies
|
||||
|
||||
- `pi-intercom` 0.10.0: existing Intercom transport and broker; MIT. Its upstream
|
||||
LICENSE is included under `node_modules/pi-intercom/LICENSE` in the package.
|
||||
- `@sting8k/pi-vcc` 0.5.0: existing algorithmic worker-view compiler. Its README's
|
||||
License section declares MIT and is included with the bundled source.
|
||||
- Intercom's runtime dependencies, including `tsx` and `esbuild`, retain their
|
||||
upstream package notices in the tarball. Pi core and typebox are peers, not bundled.
|
||||
|
||||
No replacement IPC runtime or VCC implementation was written for this move.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Single-package and role-model validation
|
||||
|
||||
2026-09-07. The parent accepted the implementation after independent review and a targeted recheck. Changes are in the local pi-goals feature worktree; this is not an npm release.
|
||||
|
||||
## Delivered
|
||||
|
||||
- One pi-goals package: internal supervisor, bundled Intercom/VCC, and one package-root supervisor launch. Herdr remains the terminal host.
|
||||
- Separate remembered planning, worker and supervisor provider/model choices. Files are under `getAgentDir()/pi-goals/`; the fresh evidence-judge override stays separate.
|
||||
- At least three task-specific alignment questions by default. An explicit affirmative current-plan waiver skips them; negations and quoted feature names do not.
|
||||
- Ready / Discuss / Edit / Cancel. Discuss and Escape preserve the draft and return to chat. `RequestPlanReview` reopens review when discussion is finished, including an unchanged draft. Only Ready starts work.
|
||||
|
||||
## Observed validation
|
||||
|
||||
[Saved full output](evidence/2026-09-07_single-package-final-validation.log) contains:
|
||||
|
||||
```text
|
||||
Test Files 12 passed (12)
|
||||
Tests 67 passed (67)
|
||||
...
|
||||
ℹ tests 118
|
||||
ℹ pass 118
|
||||
ℹ fail 0
|
||||
ℹ skipped 0
|
||||
...
|
||||
SINGLE_PACKAGE_FINAL_VALIDATION_PASSED
|
||||
```
|
||||
|
||||
`npm test` includes the packed/extracted production artifact running two real Pi sessions, the actual bundled Intercom broker and an offline fresh judge. It checks distinct actual role models and does not load a companion source checkout. The real-Pi conversational test covers Discuss and same-current-model recovery. Herdr is mocked in automated tests. Typecheck, lint, build and diff checks passed. Comparing common entries in the old/new lockfiles found no changed versions of existing locked packages.
|
||||
|
||||
The first review found four defects: stop blocked by model unavailability; activation before worker restoration and wrong recovery role; negated waiver matching; and same-model selection not triggering recovery. All were fixed with regressions. Parent inspection also caught a recovery await before cancellation ownership was captured; two more regressions cover clear/replacement during that await. The targeted review read current source and tests and returned `No issues found.` and `Merge verdict: OK`.
|
||||
|
||||
## Migration and remaining limits
|
||||
|
||||
The parent removed the two old companion entries from the user's Pi package list after the packed path passed. Only pi-goals remains registered for this workflow. Existing workers must reload before Ready: their old launch arguments still name the standalone extensions. The failed supervisor pane was observed at a shell with no Pi process; no duplicate recovery process was started during implementation.
|
||||
|
||||
Live Herdr recovery/navigation, the quality of questions from the user's chosen model, and token savings still need a human trial. The old historical tool-call-without-result issue can still block whole-plan `done`; `/goals clear` explicitly disconnects supervision and preserves history. This change did not weaken outstanding-work checks or alter old session transcripts.
|
||||
|
||||
<!-- Parent synthesis from observed commands, source inspection and review output, by Pi. -->
|
||||
@@ -0,0 +1,69 @@
|
||||
# Supervisor integration options
|
||||
|
||||
2026-09-07. Source-based recommendation, not implemented or tested as a live two-pane session. User intent: [Plan-aware persistent supervisor](../spec/2026-09-07_plan-supervisor.md).
|
||||
|
||||
## Recommendation
|
||||
|
||||
Compose `pi-goals` with `pi-intercom-supervisor` and its existing Intercom channel. Run the supervisor as a second real Pi session in a Herdr split. Use native Pi fork/resume and Herdr launch/focus facilities; keep a small adapter in pi-goals for the plan handoff and navigation.
|
||||
|
||||
This reuses the supervisor's policy, incremental views, decisions, and steering. It avoids both copying that runtime into pi-goals and placing a headless subagent behind a second UI. Both sessions remain ordinary Pi sessions that the human can inspect and address.
|
||||
|
||||
Herdr owns the terminals. Pi owns each session's history and compaction. The supervisor package owns the supervision relationship and observation cadence. Pi-goals owns the plan and individual goal-completion requests. Intercom transports messages. Pi-subagents can continue running the worker's delegated jobs without owning the supervisor.
|
||||
|
||||
## Existing examples and their fit
|
||||
|
||||
| Pattern | Fit for this task | Boundary |
|
||||
|---|---|---|
|
||||
| Intercom planner/worker + existing supervisor + native Herdr split | Recommended | New fork/bootstrap, plan-signoff, and cadence hooks still needed |
|
||||
| Intercom `openProjectPaneIfMissing` | Useful launch/discovery example | Starts plain Pi; no explicit fork/session argument; cwd selection is insufficient to identify this supervisor |
|
||||
| Pi-subagents `project.open` | Real visible Pi session | One binding per canonical cwd; options do not select a session file or supervisor role |
|
||||
| Pi-subagents retained child + Fleet/Herdr inspector | Retains child context and managed lifecycle | Inspector reads artifacts and offers control; it is not the child's interactive Pi session |
|
||||
| Messenger Crew/lobby + overlay | Useful for multi-worker task orchestration | Own task state and headless `--mode json --no-session -p` workers; overlay is chat/progress, not two real Pi terminals |
|
||||
|
||||
Intercom's README describes its planner/worker example as “one holds the big picture, the other does the hands-on work.” Its extension-channel example is the right transport pattern for non-conversational views and correlated goal requests: channel traffic alone starts no model turn. The supervisor already uses this channel and starts its own review when a view arrives.
|
||||
|
||||
The supervisor's `pi-supervise:pair:v1` event accepts an explicit worker Intercom identity and goal, and resolves on worker acknowledgement. It is emitted inside the supervisor process, not across the event bus from the worker. Its companion worker-state and paired events expose the identities needed by a launcher.
|
||||
|
||||
## Human interaction
|
||||
|
||||
Default to a named worker/supervisor split so both real sessions are visible. Focus either pane to type there; zoom temporarily when more width is needed. Offer direct pi-goals actions for opening/focusing the supervisor and returning to the worker. Exact slash spelling is still a proposal.
|
||||
|
||||
Installed help confirms this launch shape. These commands were NOT run:
|
||||
|
||||
```bash
|
||||
herdr pane split --current --direction right --cwd "$PROJECT" --no-focus
|
||||
# Take SUPERVISOR_PANE from the returned pane.pane_id.
|
||||
herdr agent start goals-supervisor --kind pi --pane "$SUPERVISOR_PANE" -- --session "$FORK_SESSION_FILE"
|
||||
herdr agent focus "$SUPERVISOR_PANE"
|
||||
herdr agent focus "$WORKER_PANE"
|
||||
herdr pane zoom --pane "$SUPERVISOR_PANE" --toggle
|
||||
```
|
||||
|
||||
`--session` here opens an already-created distinct fork, never the worker's live file. Pi also supports `--fork <path|id>` to create the separate session at launch, so a native CLI fork followed by supervisor-side initialization may eliminate custom fork-file construction. Herdr `agent start` waits for the expected interactive agent to be detected and ready; its pane must initially be at a shell prompt. Use a unique name per pairing and retain the returned pane ID for control.
|
||||
|
||||
A separate Herdr tab is an alternative for switching; the split satisfies seeing both at once. Outside Herdr, an ordinary second terminal remains usable, but automatic terminal creation is a separate portability question. Do not build an additional fallback process manager before that need is established.
|
||||
|
||||
## Minimal integration work still required
|
||||
|
||||
1. **Bootstrap:** fork at the plan handoff, establish a supervisor-only role, compact the fork, provide the worker's explicit plan path and full plan, then pair. Do not restore the fork's inherited pi-goals activation as another worker, and do not truncate the plan into the routine 15 KB view. No supervisor judgement should run before initialization finishes.
|
||||
2. **One review policy:** expose the proposed 50-model-turn / 60-minute / settled-and-no-background-work triggers in the supervisor package. Replace its stock 30-minute policy rather than running a second timer beside it. Count model turns, not transcript messages. Track the worker's registered processes/subagents and react when they finish; arbitrary unregistered detached work remains a visibility limitation.
|
||||
3. **Per-goal request/reply:** add correlated goal-signoff and plan-update hooks. Stock `done` ends the entire pairing. A goal decision should return to pi-goals, which owns checkbox updates and the existing fresh evidence judge. A stale or duplicate reply must not approve another goal or plan version. The current two-call CompleteGoal protocol is not a user requirement.
|
||||
4. **Recovery and navigation:** retain session/plan/pane identity; reconcile live Intercom identity and pairing on restart. Focus or resume the same supervisor instead of creating another. Do not open one session file in two live Pi processes. Preserve the original supervisor's decisions and reestablish policy/plan after compaction.
|
||||
|
||||
Keep VCC's incremental views and the supervisor's recent-view pruning initially. Use native compaction for the fork and later supervisor history. Measure token totals, cache reads, compaction cost, and whether useful interventions occur. The supervisor README's reported cost saving is not proof of an equivalent token reduction.
|
||||
|
||||
## Evidence and limits
|
||||
|
||||
Inspected Pi 0.84.4 and Herdr `0.8.2-preview.2026-09-06-9e9bc8a14466` CLI help. Herdr is installed, but this chat is not a Herdr-managed pane. No panes, agents, packages, or settings were started/changed for the research. Only documentation changed in this branch.
|
||||
|
||||
Source anchors:
|
||||
|
||||
- [Supervisor pairing and runtime](https://github.com/wassname/pi-intercom-supervisor/blob/409233cd4fb89e9b8c4a027affdedac5c9a8fddc/src/index.ts): pairing at 749–794; recent-view pruning at 535–556; settled reviews at 710–742. [Policy](https://github.com/wassname/pi-intercom-supervisor/blob/409233cd4fb89e9b8c4a027affdedac5c9a8fddc/src/prompts.ts) and [wire protocol](https://github.com/wassname/pi-intercom-supervisor/blob/409233cd4fb89e9b8c4a027affdedac5c9a8fddc/src/protocol.ts) show the reusable prompt and missing per-goal request.
|
||||
- [Intercom 0.13.0 README](https://github.com/nicobailon/pi-intercom/blob/199279ae861bf53ce014809fb2a03337538ae13e/README.md): Planner-Worker Coordination, Extension channels. [Project launcher](https://github.com/nicobailon/pi-intercom/blob/199279ae861bf53ce014809fb2a03337538ae13e/project-agent.ts) accepts cwd/focus but not a selected session. Inspected the published npm tarball as well. The supervisor README's old `tintinweb/pi-intercom` link returned 404; current Intercom source is under `nicobailon`.
|
||||
- [Pi-subagents current integration docs](https://github.com/nicobailon/pi-subagents/blob/54df154d1891db6b1a3e755539683335b9f9dbd3/docs/extension-api.md): Inspector panes, Project panes, Background-work provider API. Also inspected installed 0.60.0 source: `project-panes.ts:102–107,177–179,409–412,546–564` and `focus.ts:32–55`. Registry latest was 0.66.0; no upgrade was performed. Current source still distinguishes project sessions from headless-run inspectors.
|
||||
- [Messenger 0.15.2 lobby launch](https://github.com/nicobailon/pi-messenger/blob/09937ed647a1b07a3b595bf75943feacb80ff123/crew/lobby.ts): `spawnLobbyWorker` uses print/JSON/no-session mode. [Overlay actions](https://github.com/nicobailon/pi-messenger/blob/09937ed647a1b07a3b595bf75943feacb80ff123/overlay-actions.ts) send messages and manipulate Crew tasks.
|
||||
- Installed Pi SDK `docs/sdk.md`, `docs/session-format.md`, and `examples/sdk/11-sessions.ts`: session persistence, native branching, compaction, and CLI resume. Installed `herdr pane split --help`, `herdr agent start --help`, `herdr agent focus --help`, and `herdr pane zoom --help`: real interactive launch and navigation.
|
||||
|
||||
This is a fit comparison of the requested packages and their actual examples, not a package security audit or a measured reliability ranking. End-to-end fork/compact/pair, reload, background completion, sign-off, and visible switching remain to be tested.
|
||||
|
||||
<!-- Research and synthesis by Pi; recommendation pending user approval. -->
|
||||
@@ -0,0 +1,80 @@
|
||||
# Plan supervisor: implementation and trial status
|
||||
|
||||
2026-09-07. Feature-branch implementation, reviewed fixes and local trial. One live supervised goal completed successfully. Full navigation/reload UAT and token-saving measurements remain open. The user authorized registering the two companion packages in Pi settings and committing/pushing both feature branches. No npm release is part of this change.
|
||||
|
||||
## What changed
|
||||
|
||||
Pi-goals now forks a real supervisor session and starts it through native Herdr commands. The existing supervisor package supplies its policy, incremental VCC worker views and retained judgement. Ready waits for initialization and acknowledged pairing, then starts work; it is not a second plan-approval gate.
|
||||
|
||||
Routine checks use the 50-model-turn / 60-minute / settled-with-no-tracked-work policy. One CompleteGoal call requests a correlated supervisor decision before running the separate fresh evidence judge. Direct supervisor/worker focus and zoom commands replace Fleet navigation for this workflow. Small measured forks can skip compaction; larger/unknown forks use native compaction. Missing-pane recovery is deliberately manual.
|
||||
|
||||
## Parent-observed automated validation
|
||||
|
||||
[Saved default-on validation](evidence/2026-09-07_default-on-validation.log) records `Tests 48 passed (48)` across 11 files, plus typecheck, lint, build and diff checks. Both the actual-package hook test and real-Pi/Intercom test now initialize supervision without an explicit enable command. New regressions cover default-on migration, explicit-off persistence and preserving active legacy plans.
|
||||
|
||||
[Earlier full saved output](evidence/2026-09-07_supervisor-validation.log) covers all seven lifecycle review fixes, before the default-on change. Relevant excerpts:
|
||||
|
||||
```text
|
||||
GOALS: enabled suite after R6/R7
|
||||
Test Files 11 passed (11)
|
||||
Tests 45 passed (45)
|
||||
...
|
||||
Checked 16 files in 56ms. No fixes applied.
|
||||
...
|
||||
ℹ tests 116
|
||||
ℹ pass 116
|
||||
ℹ fail 0
|
||||
ℹ skipped 0
|
||||
...
|
||||
POST-R6-R7 PARENT VALIDATION PASSED
|
||||
```
|
||||
|
||||
The goals suite ran with `PI_GOALS_SUPERVISOR_SOURCE` pointing to the matching supervisor branch. It included the actual two-Pi RPC / Intercom-broker / fresh-offline-judge test, with Herdr mocked. The hook integration additionally exercises two goals with actual package code and a persisted native fork, but mocks transport, Herdr and the judge. Neither is visual TUI proof. Typecheck, lint, build and both diff checks also passed. The supervisor suite ran through its literal `npm test`, not only the worker's alternate runner.
|
||||
|
||||
Reproduce from the goals worktree:
|
||||
|
||||
```bash
|
||||
PI_GOALS_SUPERVISOR_SOURCE=/home/ubuntu/.pi/agent/worktrees/pi-intercom-supervisor-goals-integration/src/index.ts npm test
|
||||
npm run typecheck && npm run lint && npm run build && git diff --check
|
||||
cd /home/ubuntu/.pi/agent/worktrees/pi-intercom-supervisor-goals-integration
|
||||
npm test && git diff --check
|
||||
```
|
||||
|
||||
## Review disposition
|
||||
|
||||
All seven implementation findings were accepted and fixed:
|
||||
|
||||
- R1: preserve unknown context usage and remove the stale pre-compaction token floor.
|
||||
- R2: cancel stale Ready handoffs after awaited activation and plan replacement.
|
||||
- R3: invalidate suspended view/compaction continuations after stop or shutdown.
|
||||
- R4: persist acknowledged initialization separately from provisional bootstrap state.
|
||||
- R5: preserve explicit-stop state across cleanup and reload.
|
||||
- R6: preserve and acknowledge worker activation during same-binding bootstrap replay.
|
||||
- R7: keep the plan in starting until activation succeeds; steward-off returns it to planning.
|
||||
|
||||
The independent final review verified the original R1–R5 scenarios and found R6/R7. Its verdict was BLOCK for R6. The parent then authorized the two narrow fixes, inspected their source and regression tests, and ran the final suites above. No fourth independent review was launched: the three-round cap was reached. R6/R7 therefore have parent review and regression evidence, not a subsequent independent approval.
|
||||
|
||||
The orchestration script failed after the first fix worker because its progress object included an undefined optional output reference. Completed code/results were retained; only the unlaunched final reviewer was recovered. This did not constitute a code/test failure.
|
||||
|
||||
## Local trial
|
||||
|
||||
The user has registered all three local packages in Pi settings, so ordinary Pi startup now loads them. For a temporary trial elsewhere, start inside Herdr with the matching goals extension already loaded and pass the companions explicitly:
|
||||
|
||||
```bash
|
||||
base="$HOME/.pi/agent/worktrees/pi-intercom-supervisor-goals-integration"
|
||||
pi -e "$base/src/index.ts" -e "$base/node_modules/pi-intercom/index.ts"
|
||||
```
|
||||
|
||||
Then use `/goals plan <objective>` and Ready. Steward and 60-minute fallback auto-continue now default to on; explicit off preferences persist. `/goals supervisor`, `/goals worker`, and `/goals zoom` operate on the recorded real panes. `/goals steward off` stops the relationship, not the terminal pane.
|
||||
|
||||
Observe both panes, switch and zoom, reload/compact the supervisor, then complete two goals. If a pane ID is lost, locate the existing supervisor before reopening its saved session; a missing pane ID is not evidence that its process exited. Unknown background providers are not evidence that all work finished. Automatic phase-model switching was not implemented.
|
||||
|
||||
## Live trial and remaining cleanup issue
|
||||
|
||||
The user completed one real Herdr-supervised file-table goal. The supervisor delivered a direction message, and CompleteGoal ultimately accepted the evidence after corrections to the saved command/transcript attribution. This establishes a live pairing, steering and goal-sign-off path; it does not establish two-goal persistence or focus/zoom/reload behavior.
|
||||
|
||||
After completion, the supervisor reported that its `done` call was blocked by `Cannot finish: the worker still has work running (write).` Its worker view still reported an unresolved write while the worker was settled and tracked processes/subagents were zero. The origin of this apparently stale tool state has not been diagnosed. Automatic whole-plan cleanup is therefore not verified. `/goals clear` explicitly disconnects the pairing and stops its watch timer while preserving the plan; the supervisor pane/session can remain as history. A new plan creates a new version and pairing.
|
||||
|
||||
The managed pi-goals checkout's pre-existing `package-lock.json` modification was left untouched. Development dependencies were installed only in the new supervisor feature checkout. The later user-approved settings change registers the existing local supervisor and Intercom copies; it does not upgrade or download packages.
|
||||
|
||||
<!-- Final implementation synthesis and observed validation by Pi. -->
|
||||
@@ -0,0 +1,23 @@
|
||||
## Review
|
||||
|
||||
Reviewed the worker report, follow-up-only diffs under `/tmp/pi-goals-checkpoint-fix/`, corresponding current source/tests, and the parent’s final latch correction. The saved `before/` files separate this work from the previously reviewed implementation; this is not a re-review of the broader uncommitted changes.
|
||||
|
||||
- **Correct — fresh, immutable checkpoints:** `src/internal/supervisor/index.ts:362–396` captures a complete worker snapshot for each explicit review and rechecks request identity, cancellation, pairing generation, session identity and canonical-plan hash after capture. The branch is read after tracker queries in `captureWorkerView`. The snapshot replaces `worker_view` only when its checkpoint becomes active; routine updates cannot overwrite an active assessment. Covered by `test/internal-supervisor/plan.test.ts:73–168`.
|
||||
|
||||
- **Correct — current direction and bounded transport:** `src/internal/supervisor/view.ts:267–288` pins the latest non-supervisor user direction separately from older summaries, with explicit truncation. `src/internal/supervisor/protocol.ts:19–39` projects checkpoint identity into replies and bounds the actual serialized request, including JSON escaping and Unicode. Its size calculation matches the installed Intercom broker’s payload measurement. Success, duplicate rejection and failure responses omit the snapshot.
|
||||
|
||||
- **Correct — genuine settlement required:** `src/internal/supervisor/index.ts:899–908,1198–1215` records empty final output at `agent_end` but acts only at `agent_settled`, and only for an unresolved assessment. Successful verdicts and separately queued checkpoints are excluded. The installed Pi implementation emits settlement after retry, compaction and queued-continuation processing (`node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js:744–781`). Nonempty human-question handling remains distinct.
|
||||
|
||||
- **Fixed by parent; verified read-only — failure incorrectly latched a human dependency:** The earlier `failAssessment` set `awaitingUser=true`, suppressing later ordinary worker views indefinitely. Current `src/internal/supervisor/index.ts:634–647` clears that latch and stale routine-refresh flags, then refreshes the footer. This permits later progress/cadence without immediately retrying the same dirty view, while retaining separately queued checkpoints. The two regressions at `test/internal-supervisor/plan.test.ts:188–203` fail in `wait-latch-red.log` and pass in `wait-latch-green.log`. Explicit `needs_user` and nonempty-question pause paths remain present.
|
||||
|
||||
**No issues found.**
|
||||
|
||||
**Merge verdict: OK with notes.** The scoped changes are approved. Parent-owned final aggregate validation and live-pane acceptance remain outstanding evidence, not established by this read-only review.
|
||||
|
||||
### Validation and residual risks
|
||||
|
||||
- Inspected saved worker results: **89 focused tests**, **169 supervisor tests**, **51 integration tests**, plus clean typecheck/lint. These precede the parent’s latch correction.
|
||||
- Inspected parent’s post-correction regression log: **2 passed**, covering empty-final and provider-error recovery, no immediate retry loop, and subsequent ordinary progress.
|
||||
- No commands, edits, Herdr operations, settings changes, staging or push were performed by this reviewer.
|
||||
- Parent’s final `npm test`, typecheck, lint and build were starting; their results were not available for this verdict. Complete `npm test` before committing, as required by `AGENTS.md`.
|
||||
- Live current-pane behavior and supervision quality/cost still require parent validation. Bounded snapshots can explicitly truncate; historical replay is not live acceptance.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Autonomous supervision: implementation checked, live acceptance pending
|
||||
|
||||
Base: `cecb1e9`, branch `feature/simple-visible-supervision`. Follow-up changes are uncommitted.
|
||||
Scope: [approved plan](../slop/plans/20260909_autonomous-supervision-acceptance.md).
|
||||
|
||||
## Implemented
|
||||
|
||||
- Outcome-focused supervisor instructions: investigate blockers, change ineffective steering, inspect actual results, and keep authorized work moving. VCC and existing lifecycle protections remain.
|
||||
- Ordinary prose, empty responses and genuine questions do not discard later worker views or direction. Monitoring does not authorize restarting human-paused work. No immediate idle retry loop.
|
||||
- Manual checkmarks are claims; CompleteGoal records conclusive or inconclusive sign-off separately. Exact goal identity, cancellation and fresh-judge behavior remain. Legacy completion is labelled without inventing approval or restarting old work.
|
||||
- Dirty Git state is context, not an acceptance gate. Cited ignored output files are valid inspection targets; no forced cleanup or commit.
|
||||
- Material planning questions replace the quota; Discuss remains ordinary chat. AGENTS.md includes the other branch's relevant user preferences and real-Herdr testing procedure.
|
||||
|
||||
## Parent validation
|
||||
|
||||
- [Permitted Vitest subset](evidence/2026-09-09-autonomy/parent-permitted-vitest.log): `Tests 94 passed (94)`, across 13 files. Explicitly excludes `test/rpc-supervisor.test.ts`; this is not a passing full suite.
|
||||
- [Supervisor regressions](evidence/2026-09-09-autonomy/parent-supervisor.log): `ℹ tests 176`, `ℹ pass 176`, `ℹ fail 0`.
|
||||
- [Typecheck](evidence/2026-09-09-autonomy/parent-typecheck.log), [lint](evidence/2026-09-09-autonomy/parent-lint.log), and [build](evidence/2026-09-09-autonomy/parent-build.log) exited successfully. `git diff --check` passed on source changes.
|
||||
- Review found conflicting advice to prune completed goal lines and stale fuzzy-match descriptions. Parent corrected the instructions and added regressions. [Red](evidence/2026-09-09-autonomy/housekeeping-red.log) shows the two prompt failures; [green](evidence/2026-09-09-autonomy/housekeeping-green.log) records 43 passing tests, including preserved conclusive/inconclusive records after moving detail into the appendix.
|
||||
- [Read-only recheck](evidence/2026-09-09-autonomy/housekeeping-recheck.md): “Both previous findings are resolved; the narrow fixes are approved.”
|
||||
|
||||
## Still required
|
||||
|
||||
Full `npm test` did not pass: [broker diagnostics](evidence/2026-09-09-autonomy/broker-diagnostic.log) show Unix-socket `listen EPERM` in the tsx launcher. Parent Herdr control independently returned `PermissionDenied: Operation not permitted`. No TMPDIR/IPC workaround was authorized or used to bypass the restriction.
|
||||
|
||||
The fresh two-goal Herdr trial has not run. It must show both actual artifacts and verification output, both CompleteGoal results, useful visible supervision, same-pair continuity, explicit-pause/reload behavior, ignored output files and preserved unrelated dirty work. Record every operator intervention and separate worker/supervisor usage. Do not treat deterministic tests as evidence of live judgment or savings.
|
||||
|
||||
No role preferences, active installation, existing panes, or unrelated root-worktree files were changed in this follow-up. No commit, push, merge or release yet.
|
||||
|
||||
Recorded by Pi (OpenAI) from observed command output and the independent source review.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Recovery commands
|
||||
|
||||
Implemented directly by Pi/OpenAI at the user's request, on `feature/simple-visible-supervision`, base `cecb1e9` plus the existing uncommitted autonomy changes. The active global installation was not replaced.
|
||||
|
||||
## Commands
|
||||
|
||||
- `/goals help`: available commands and limits.
|
||||
- `/goals status`: phase, peer connectivity, recorded panes/session files and last runtime failure.
|
||||
- `/goals stop`: stop goal continuation and supervision; retain the plan and pair.
|
||||
- `/goals exit`: stop and return to ordinary chat, retaining files. Supervisor sessions remain inspection-only.
|
||||
- `/goals reconnect`: send the existing pair's identity handshake. Does not fork or authorize work.
|
||||
- `/goals resume`: in the worker, resume previously authorized work after peer acknowledgement. A stopped draft or changed plan returns to planning and still needs Ready. In the supervisor, directs the human to the worker for authorization.
|
||||
- `/goals supervisor`, `/goals worker`, `/goals zoom`: existing pane navigation.
|
||||
|
||||
Stop/exit cancel startup, an outstanding Ready selection and sign-off. They persist across reload. They do not kill independently running processes. Peer notification is best-effort and explicitly unconfirmed; use the other pane's stop command if it is disconnected. A pause identity prevents an old resume request from undoing a newer stop. Permanently ended pairings are not revived by reconnect.
|
||||
|
||||
## Observed interactive behaviour
|
||||
|
||||
Used real Pi 0.85.1 in a dedicated Herdr pane, with the normal global extensions, skills, prompts and themes. Project settings replaced only the old pi-goals package selection with the candidate. No global settings or package installation changed. The pane was closed after the check.
|
||||
|
||||
This was an **operator-seeded unapproved draft**, not an autonomous task or a model-produced plan. There were no model responses beyond the explicitly labelled fixture marker. The purpose was to exercise the public commands and persisted state in real Pi.
|
||||
|
||||
Saved [verification output](evidence/2026-09-09-recovery/verification.log) reports:
|
||||
|
||||
> PASS: stop persisted paused draft.
|
||||
> PASS: resume restored planning without Ready authorization.
|
||||
> PASS: exit persisted ordinary-chat state.
|
||||
> PASS: no assistant turn beyond operator fixture marker.
|
||||
> PASS: draft bytes unchanged.
|
||||
> PASS: actual reload rendered; stopped status retained.
|
||||
> PASS: reconnect without a pair reports failure instead of launching one.
|
||||
> PASS: fresh-shell --session retained exit; status reports ordinary chat.
|
||||
|
||||
The [reload capture](evidence/2026-09-09-recovery/reload-pane.txt) shows the actual reload notice and stopped widget. The [resume capture](evidence/2026-09-09-recovery/resume-pane.txt) says “Draft restored; no work started.” The [fresh-shell capture](evidence/2026-09-09-recovery/fresh-status-pane.txt) says “Goals: ordinary chat (goals exited).” These establish command behaviour in the interactive runtime, not supervisor judgment.
|
||||
|
||||
Local fixture and detailed command receipts: `/tmp/pi-goals-recovery-functional/`. Source diff: `/tmp/pi-goals-recovery-implemented.diff`.
|
||||
|
||||
## Automated checks
|
||||
|
||||
The final `npm test`, typecheck, lint, build and `git diff --check` completed successfully. Saved [npm test output](evidence/2026-09-09-recovery/npm-test.log), [typecheck](evidence/2026-09-09-recovery/typecheck.log), [lint](evidence/2026-09-09-recovery/lint.log) and [build](evidence/2026-09-09-recovery/build.log) are supporting checks, not substitutes for paired functional acceptance.
|
||||
|
||||
## Remaining acceptance
|
||||
|
||||
Real paired worker/supervisor recovery during bootstrap compaction and sign-off, and the autonomous two-goal trial, remain pending. Paired transport regressions use an in-process broker harness; they do not replace those checks. The encrypted-compaction replay mismatch is a separate unresolved issue; this change does not disable its guard or claim to fix it.
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run
|
||||
|
||||
|
||||
RUN v4.1.9 /home/ubuntu/.pi/agent/worktrees/pi-goals-persistent-steward
|
||||
|
||||
|
||||
Test Files 11 passed (11)
|
||||
Tests 48 passed (48)
|
||||
Start at 14:06:36
|
||||
Duration 6.78s (transform 1.61s, setup 0ms, import 16.58s, tests 11.71s, environment 2ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 16 files in 36ms. No fixes applied.
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 build
|
||||
> tsc
|
||||
|
||||
DEFAULT_ON_VALIDATION_PASSED
|
||||
@@ -0,0 +1,157 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run && npm run test:supervisor
|
||||
|
||||
|
||||
RUN v4.1.9 /home/ubuntu/.pi/agent/worktrees/pi-goals-persistent-steward
|
||||
|
||||
|
||||
Test Files 12 passed (12)
|
||||
Tests 67 passed (67)
|
||||
Start at 16:14:44
|
||||
Duration 15.63s (transform 1.67s, setup 307ms, import 22.12s, tests 28.12s, environment 3ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test:supervisor
|
||||
> node --import tsx --test test/internal-supervisor/*.test.ts
|
||||
|
||||
✔ retries intercom registration when pi-intercom loads after pi-supervise (3.540888ms)
|
||||
✔ a directive with no text is rejected, so the worker never sees undefined (0.288987ms)
|
||||
✔ a directive from the paired supervisor becomes a real user message (27.396147ms)
|
||||
✔ a directive to a busy worker interrupts, instead of waiting for the whole task (13.079025ms)
|
||||
✔ a directive from an unpaired session is dropped (11.977008ms)
|
||||
✔ a second pair takes over, and the first supervisor is told it lost the worker (21.508059ms)
|
||||
✔ only the paired worker can end a run (6.314542ms)
|
||||
✔ the programmatic pairing API waits for the worker acknowledgement (1.485485ms)
|
||||
✔ the worker acknowledges a pair, so the supervisor knows it was heard (5.359587ms)
|
||||
✔ a goal the supervisor inferred reaches the worker, which owns the view header (37.477099ms)
|
||||
✔ the second view carries only what happened after the first (328.068703ms)
|
||||
✔ a message addressed to a different session is ignored (11.834377ms)
|
||||
✔ on settle the worker publishes a view built from the live branch (17.888259ms)
|
||||
✔ the view is built from the live branch, not from every entry in the session (22.049192ms)
|
||||
✔ an unpaired session publishes nothing on settle (0.498005ms)
|
||||
✔ supervision never stops itself: no round limit at all (9.442441ms)
|
||||
✔ goal, pairing and the steer count all survive a reload together (0.791695ms)
|
||||
✔ a view that arrives while the supervisor is thinking is queued, not dropped (5.643173ms)
|
||||
✔ the nudge repeats neither the instructions already sent nor the verdict rules (5.269154ms)
|
||||
✔ a multi-line goal returns to supervisor context every fifth review and after compaction (31.103788ms)
|
||||
✔ a one-line goal is not redundantly reinserted (26.640178ms)
|
||||
✔ a check in and a worker that stopped ask for different things (12.287139ms)
|
||||
✔ a loop still gets named after the supervisor compacts, from restored state (1.450856ms)
|
||||
✔ a session that does not answer the roll call is not offered as a worker (501.077007ms)
|
||||
✔ a child run stays out of the roll call, so it can never be picked (5.337167ms)
|
||||
✔ a session already paired stays out of the roll call, and a free one answers (15.899876ms)
|
||||
✔ /supervise look asks the worker for a fresh view, rather than the supervisor guessing (306.105484ms)
|
||||
✔ let_it_run says the turn is over, so it is not called four times running (0.835308ms)
|
||||
✔ a sign-off verdict is answered, not aborted, and a runaway is still cut (0.609635ms)
|
||||
✔ every verdict result names the way to end the turn, steer included (0.449781ms)
|
||||
✔ an old view is dropped from context once its verdict is in, and the verdict is kept (1.046937ms)
|
||||
✔ a worker session never has its context rewritten (0.289351ms)
|
||||
✔ a view that arrives mid-answer starts a fresh look (5.824464ms)
|
||||
✔ a tool a worker cannot use never aborts its turn (0.339873ms)
|
||||
✔ a resume onto a session that is gone drops the pairing and says so (6.297894ms)
|
||||
✔ a resume onto a live worker keeps supervising, and takes the writers back off (5.29537ms)
|
||||
✔ state written before recentSteers existed still loads (0.171283ms)
|
||||
✔ done unpairs the worker, so it stops publishing views (320.167743ms)
|
||||
✔ with no goal the supervisor cannot steer, it must ask the human (0.573651ms)
|
||||
✔ set_goal binds an inferred goal, and steering then works (501.94885ms)
|
||||
✔ a goal given at pair time still allows steering (0.665903ms)
|
||||
✔ done is refused while the worker has an unanswered tool call (12.424782ms)
|
||||
✔ done is allowed once nothing is outstanding (6.182792ms)
|
||||
✔ steer refuses when the session is not supervising (0.325609ms)
|
||||
✔ a reworded repeat of an earlier instruction is sent, and named back to the supervisor (0.510602ms)
|
||||
✔ overlap scores rewording high and a different instruction low (0.136533ms)
|
||||
✔ the view of the old worker cannot be used to judge the new one (6.045704ms)
|
||||
✔ with one other session here, /supervise needs no target and the whole line is the goal (501.381162ms)
|
||||
✔ naming the worker still works, and the rest of the line is the goal (0.554981ms)
|
||||
✔ with two free sessions here, /supervise asks which one, and pairs with the choice (501.08053ms)
|
||||
✔ a goal that is a path is read from the file, so it is not pasted every run (2.297652ms)
|
||||
✔ a long goal is one short line above the picker, and reaches the worker whole (501.854523ms)
|
||||
✔ a session that stayed quiet is still on the list, because 0 free is a dead end (501.687356ms)
|
||||
✔ a cancelled picker pairs with nothing (500.579211ms)
|
||||
✔ supervising takes the writing tools away, and stopping gives them back (501.255208ms)
|
||||
✔ stopping gives back the writers without undoing another extension's tools (500.601415ms)
|
||||
✔ a first word that names no session is refused, rather than folded into the goal (0.611801ms)
|
||||
✔ a goal with spaces needs no target, and @name takes the rest of the line as the goal (501.893405ms)
|
||||
✔ the brief starts no turn, so there is no answer before the first view (5.995751ms)
|
||||
✔ /supervise goal changes the goal without breaking the pairing (0.70475ms)
|
||||
✔ the footer says which side of a pairing this session is, and clears when it ends (506.432358ms)
|
||||
✔ a session that is not supervising never sees the supervisor tools (5.830284ms)
|
||||
✔ worker_view refuses when there is no worker, rather than implying a pairing (0.513982ms)
|
||||
✔ the view names the worker's model and how full its context is (21.061667ms)
|
||||
✔ supervising a second session is refused while the first is still paired (0.548942ms)
|
||||
✔ the supervisor gets a look at a working worker every half hour, without being asked (926.124762ms)
|
||||
✔ a human message in the worker session is not a reason to stand back (6.127037ms)
|
||||
✔ letting a stopped worker run says plainly that the worker stays stopped (11.377477ms)
|
||||
✔ a stopped worker is looked at again, so let_it_run cannot silence the pairing (922.993821ms)
|
||||
✔ a worker that pairs at the prompt and never takes a turn is still watched (604.616558ms)
|
||||
✔ a worker that reloads at the prompt starts watching itself again (604.845926ms)
|
||||
✔ a timer look at a worker that has not moved is not sent, until it has been skipped three times (2425.474247ms)
|
||||
✔ the worker counts reviews in a row where nothing changed (356.432776ms)
|
||||
✔ an unacknowledged pair gives up, and a takeover cancels that timer (3.893969ms)
|
||||
✔ duplicate standalone Intercom registries are diagnosed and cannot bootstrap a plan (1.232918ms)
|
||||
✔ plan bootstrap compacts only the supervisor and pairing alone never starts a worker or a review (18.935035ms)
|
||||
✔ goal decisions are correlated, preserve the pair across two goals, and cannot call overall done (5.389797ms)
|
||||
✔ abort and stop cancel pending requests; late decisions cannot approve a replacement (3.265783ms)
|
||||
✔ 50 actual model turns trigger one view, independent of the number of messages (3.457965ms)
|
||||
✔ unknown background providers are not proof of quiescence (0.294658ms)
|
||||
✔ stale plan content invalidates a pending goal review (3.01992ms)
|
||||
✔ small forks skip compaction, but real compaction failure prevents pairing (2.471879ms)
|
||||
✔ the hour timer and a coincident turn checkpoint produce a single view (5.559542ms)
|
||||
✔ settled checks wait for tracked processes and subagents to finish (2.833087ms)
|
||||
✔ bootstrap stop cannot resurrect a supervisor after compaction completes (2.15529ms)
|
||||
✔ a restarted worker reconnects by exact saved session identity without a new supervisor (2.812666ms)
|
||||
✔ unknown initial context must compact instead of taking the known-small shortcut (1.119486ms)
|
||||
✔ null post-compaction usage cannot raise the next configured 100k checkpoint (2.180167ms)
|
||||
✔ stopping a routine view during compaction invalidates its suspended continuation (1.977452ms)
|
||||
✔ restart of a provisional bootstrap resumes compaction and pairing in the same saved session (2.18079ms)
|
||||
✔ command preserves a stopped supervisor across reload (2.007763ms)
|
||||
✔ done preserves a stopped supervisor across reload (2.456918ms)
|
||||
✔ same-binding replay retains activation when the supervisor lost its acknowledgement (2.599438ms)
|
||||
✔ model-unavailable stop validates binding, cancels pending reviews and ignores old directives (2.091306ms)
|
||||
✔ a child process named pi is found by ps, and stops being found when it exits (367.036839ms)
|
||||
✔ the check is a snapshot, so it cannot hold up the worker's settle (321.13146ms)
|
||||
✔ a one-line goal stays whole while a multi-line goal has a locator (6.226689ms)
|
||||
✔ a view carries only the turns the supervisor has not been sent (2.16041ms)
|
||||
✔ the last two reasoning blocks stay in the narrative, and older ones drop out (1.078426ms)
|
||||
✔ a compaction restarts the view, so no turn falls into the gap (0.455488ms)
|
||||
✔ pi-vcc reports the files the worker wrote, and separates them from the ones it read (1.361829ms)
|
||||
✔ progressKey is unchanged when a review produced no new file or commit (0.506944ms)
|
||||
✔ progressKey still sees a new file past pi-vcc's ten path display cap (0.575315ms)
|
||||
✔ a commit counts as progress, even when no file was written since (0.960805ms)
|
||||
✔ outstandingWork finds tool calls that never got a result (1.43676ms)
|
||||
✔ buildView reports a tool call with no result, so done can be refused (0.87924ms)
|
||||
✔ the view says how many reviews in a row changed nothing, and says nothing at zero (0.692566ms)
|
||||
✔ the view merges the worker's compaction summary with the turns after it (0.470565ms)
|
||||
✔ a turn the compaction summary already covers is not sent twice (0.336722ms)
|
||||
✔ pi-vcc's sections and its transcript land on the right sides of the split (2.943ms)
|
||||
✔ the view does not tell the supervisor to use vcc_recall, a tool it does not have (0.291333ms)
|
||||
✔ supervisor directives are not sent back as worker evidence (0.488463ms)
|
||||
✔ bookkeeping tool calls are kept out of the transcript (0.390618ms)
|
||||
✔ buildView reports the goal, status, and files without historical failures (0.42015ms)
|
||||
✔ how long the worker has been quiet, measured from its own last entry (0.593754ms)
|
||||
✔ buildView keeps the newest turns when it has to cut for the channel limit (26.316809ms)
|
||||
✔ pi's own branch logic drops the abandoned fork, on a session file (1793.809954ms)
|
||||
✔ a long goal cannot push the view past the broker limit (0.55492ms)
|
||||
ℹ tests 118
|
||||
ℹ suites 0
|
||||
ℹ pass 118
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 20178.623102
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc -p tsconfig.build.json --noEmit
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 32 files in 93ms. No fixes applied.
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 build
|
||||
> tsc -p tsconfig.build.json
|
||||
|
||||
SINGLE_PACKAGE_FINAL_VALIDATION_PASSED
|
||||
@@ -0,0 +1,167 @@
|
||||
GOALS: enabled suite after R6/R7
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run
|
||||
|
||||
|
||||
RUN v4.1.9 /home/ubuntu/.pi/agent/worktrees/pi-goals-persistent-steward
|
||||
|
||||
|
||||
Test Files 11 passed (11)
|
||||
Tests 45 passed (45)
|
||||
Start at 12:09:35
|
||||
Duration 6.14s (transform 1.20s, setup 0ms, import 14.85s, tests 10.77s, environment 2ms)
|
||||
|
||||
GOALS: typecheck
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
GOALS: lint
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 16 files in 56ms. No fixes applied.
|
||||
GOALS: build
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 build
|
||||
> tsc
|
||||
|
||||
SUPERVISOR: literal npm test after R6/R7
|
||||
|
||||
> @wassname2/pi-supervise@0.0.4 pretest
|
||||
> tsx scripts/make-fixture.ts
|
||||
|
||||
wrote /home/ubuntu/.pi/agent/worktrees/pi-intercom-supervisor-goals-integration/test/forked-session.jsonl
|
||||
entries (excluding the session header): 410
|
||||
abandoned entries: 22
|
||||
|
||||
> @wassname2/pi-supervise@0.0.4 test
|
||||
> tsx --test src/*.test.ts
|
||||
|
||||
✔ retries intercom registration when pi-intercom loads after pi-supervise (3.429966ms)
|
||||
✔ a directive with no text is rejected, so the worker never sees undefined (0.247018ms)
|
||||
✔ a directive from the paired supervisor becomes a real user message (29.622902ms)
|
||||
✔ a directive to a busy worker interrupts, instead of waiting for the whole task (13.977563ms)
|
||||
✔ a directive from an unpaired session is dropped (13.157035ms)
|
||||
✔ a second pair takes over, and the first supervisor is told it lost the worker (27.141375ms)
|
||||
✔ only the paired worker can end a run (7.283707ms)
|
||||
✔ the programmatic pairing API waits for the worker acknowledgement (1.436818ms)
|
||||
✔ the worker acknowledges a pair, so the supervisor knows it was heard (5.54306ms)
|
||||
✔ a goal the supervisor inferred reaches the worker, which owns the view header (44.194636ms)
|
||||
✔ the second view carries only what happened after the first (330.585574ms)
|
||||
✔ a message addressed to a different session is ignored (10.170908ms)
|
||||
✔ on settle the worker publishes a view built from the live branch (21.893806ms)
|
||||
✔ the view is built from the live branch, not from every entry in the session (17.260824ms)
|
||||
✔ an unpaired session publishes nothing on settle (0.379273ms)
|
||||
✔ supervision never stops itself: no round limit at all (4.651789ms)
|
||||
✔ goal, pairing and the steer count all survive a reload together (0.521505ms)
|
||||
✔ a view that arrives while the supervisor is thinking is queued, not dropped (5.970369ms)
|
||||
✔ the nudge repeats neither the instructions already sent nor the verdict rules (6.195585ms)
|
||||
✔ a multi-line goal returns to supervisor context every fifth review and after compaction (32.097966ms)
|
||||
✔ a one-line goal is not redundantly reinserted (26.327091ms)
|
||||
✔ a check in and a worker that stopped ask for different things (10.092757ms)
|
||||
✔ a loop still gets named after the supervisor compacts, from restored state (0.953582ms)
|
||||
✔ a session that does not answer the roll call is not offered as a worker (502.245612ms)
|
||||
✔ a child run stays out of the roll call, so it can never be picked (6.095977ms)
|
||||
✔ a session already paired stays out of the roll call, and a free one answers (16.179354ms)
|
||||
✔ /supervise look asks the worker for a fresh view, rather than the supervisor guessing (308.118194ms)
|
||||
✔ let_it_run says the turn is over, so it is not called four times running (0.768638ms)
|
||||
✔ a sign-off verdict is answered, not aborted, and a runaway is still cut (0.57058ms)
|
||||
✔ every verdict result names the way to end the turn, steer included (0.39166ms)
|
||||
✔ an old view is dropped from context once its verdict is in, and the verdict is kept (1.05293ms)
|
||||
✔ a worker session never has its context rewritten (0.295565ms)
|
||||
✔ a view that arrives mid-answer starts a fresh look (5.950038ms)
|
||||
✔ a tool a worker cannot use never aborts its turn (0.408612ms)
|
||||
✔ a resume onto a session that is gone drops the pairing and says so (5.502016ms)
|
||||
✔ a resume onto a live worker keeps supervising, and takes the writers back off (6.297883ms)
|
||||
✔ state written before recentSteers existed still loads (0.188312ms)
|
||||
✔ done unpairs the worker, so it stops publishing views (321.527785ms)
|
||||
✔ with no goal the supervisor cannot steer, it must ask the human (0.563499ms)
|
||||
✔ set_goal binds an inferred goal, and steering then works (501.01427ms)
|
||||
✔ a goal given at pair time still allows steering (0.523019ms)
|
||||
✔ done is refused while the worker has an unanswered tool call (11.351204ms)
|
||||
✔ done is allowed once nothing is outstanding (5.223176ms)
|
||||
✔ steer refuses when the session is not supervising (0.364627ms)
|
||||
✔ a reworded repeat of an earlier instruction is sent, and named back to the supervisor (0.483156ms)
|
||||
✔ overlap scores rewording high and a different instruction low (0.114745ms)
|
||||
✔ the view of the old worker cannot be used to judge the new one (6.06669ms)
|
||||
✔ with one other session here, /supervise needs no target and the whole line is the goal (501.412838ms)
|
||||
✔ naming the worker still works, and the rest of the line is the goal (0.519157ms)
|
||||
✔ with two free sessions here, /supervise asks which one, and pairs with the choice (501.863353ms)
|
||||
✔ a goal that is a path is read from the file, so it is not pasted every run (2.227961ms)
|
||||
✔ a long goal is one short line above the picker, and reaches the worker whole (501.010774ms)
|
||||
✔ a session that stayed quiet is still on the list, because 0 free is a dead end (501.565666ms)
|
||||
✔ a cancelled picker pairs with nothing (501.796819ms)
|
||||
✔ supervising takes the writing tools away, and stopping gives them back (501.55056ms)
|
||||
✔ stopping gives back the writers without undoing another extension's tools (501.583537ms)
|
||||
✔ a first word that names no session is refused, rather than folded into the goal (0.653782ms)
|
||||
✔ a goal with spaces needs no target, and @name takes the rest of the line as the goal (501.035374ms)
|
||||
✔ the brief starts no turn, so there is no answer before the first view (6.002338ms)
|
||||
✔ /supervise goal changes the goal without breaking the pairing (0.74172ms)
|
||||
✔ the footer says which side of a pairing this session is, and clears when it ends (505.756191ms)
|
||||
✔ a session that is not supervising never sees the supervisor tools (5.764996ms)
|
||||
✔ worker_view refuses when there is no worker, rather than implying a pairing (0.508317ms)
|
||||
✔ the view names the worker's model and how full its context is (18.254298ms)
|
||||
✔ supervising a second session is refused while the first is still paired (0.498834ms)
|
||||
✔ the supervisor gets a look at a working worker every half hour, without being asked (920.025754ms)
|
||||
✔ a human message in the worker session is not a reason to stand back (6.229147ms)
|
||||
✔ letting a stopped worker run says plainly that the worker stays stopped (10.401535ms)
|
||||
✔ a stopped worker is looked at again, so let_it_run cannot silence the pairing (924.087031ms)
|
||||
✔ a worker that pairs at the prompt and never takes a turn is still watched (604.883737ms)
|
||||
✔ a worker that reloads at the prompt starts watching itself again (604.981832ms)
|
||||
✔ a timer look at a worker that has not moved is not sent, until it has been skipped three times (2426.519261ms)
|
||||
✔ the worker counts reviews in a row where nothing changed (356.420327ms)
|
||||
✔ an unacknowledged pair gives up, and a takeover cancels that timer (4.220555ms)
|
||||
✔ plan bootstrap compacts only the supervisor and pairing alone never starts a worker or a review (23.339027ms)
|
||||
✔ goal decisions are correlated, preserve the pair across two goals, and cannot call overall done (6.488073ms)
|
||||
✔ abort and stop cancel pending requests; late decisions cannot approve a replacement (4.950084ms)
|
||||
✔ 50 actual model turns trigger one view, independent of the number of messages (4.336451ms)
|
||||
✔ unknown background providers are not proof of quiescence (0.39308ms)
|
||||
✔ stale plan content invalidates a pending goal review (3.928135ms)
|
||||
✔ small forks skip compaction, but real compaction failure prevents pairing (2.247287ms)
|
||||
✔ the hour timer and a coincident turn checkpoint produce a single view (3.504665ms)
|
||||
✔ settled checks wait for tracked processes and subagents to finish (2.813837ms)
|
||||
✔ bootstrap stop cannot resurrect a supervisor after compaction completes (1.747705ms)
|
||||
✔ a restarted worker reconnects by exact saved session identity without a new supervisor (2.135761ms)
|
||||
✔ unknown initial context must compact instead of taking the known-small shortcut (0.924763ms)
|
||||
✔ null post-compaction usage cannot raise the next configured 100k checkpoint (1.994091ms)
|
||||
✔ stopping a routine view during compaction invalidates its suspended continuation (1.671618ms)
|
||||
✔ restart of a provisional bootstrap resumes compaction and pairing in the same saved session (2.221047ms)
|
||||
✔ command preserves a stopped supervisor across reload (1.905196ms)
|
||||
✔ done preserves a stopped supervisor across reload (2.421426ms)
|
||||
✔ same-binding replay retains activation when the supervisor lost its acknowledgement (2.604633ms)
|
||||
✔ a child process named pi is found by ps, and stops being found when it exits (374.81872ms)
|
||||
✔ the check is a snapshot, so it cannot hold up the worker's settle (319.675518ms)
|
||||
✔ a one-line goal stays whole while a multi-line goal has a locator (8.167075ms)
|
||||
✔ a view carries only the turns the supervisor has not been sent (2.090138ms)
|
||||
✔ the last two reasoning blocks stay in the narrative, and older ones drop out (1.13405ms)
|
||||
✔ a compaction restarts the view, so no turn falls into the gap (0.563394ms)
|
||||
✔ pi-vcc reports the files the worker wrote, and separates them from the ones it read (1.400011ms)
|
||||
✔ progressKey is unchanged when a review produced no new file or commit (0.525086ms)
|
||||
✔ progressKey still sees a new file past pi-vcc's ten path display cap (0.725928ms)
|
||||
✔ a commit counts as progress, even when no file was written since (0.874443ms)
|
||||
✔ outstandingWork finds tool calls that never got a result (1.469005ms)
|
||||
✔ buildView reports a tool call with no result, so done can be refused (0.885647ms)
|
||||
✔ the view says how many reviews in a row changed nothing, and says nothing at zero (0.644311ms)
|
||||
✔ the view merges the worker's compaction summary with the turns after it (0.44949ms)
|
||||
✔ a turn the compaction summary already covers is not sent twice (0.364824ms)
|
||||
✔ pi-vcc's sections and its transcript land on the right sides of the split (2.422505ms)
|
||||
✔ the view does not tell the supervisor to use vcc_recall, a tool it does not have (0.315271ms)
|
||||
✔ supervisor directives are not sent back as worker evidence (0.494375ms)
|
||||
✔ bookkeeping tool calls are kept out of the transcript (0.412053ms)
|
||||
✔ buildView reports the goal, status, and files without historical failures (0.403255ms)
|
||||
✔ how long the worker has been quiet, measured from its own last entry (0.460999ms)
|
||||
✔ buildView keeps the newest turns when it has to cut for the channel limit (33.622661ms)
|
||||
✔ pi's own branch logic drops the abandoned fork, on a session file (1712.237778ms)
|
||||
✔ a long goal cannot push the view past the broker limit (0.489445ms)
|
||||
ℹ tests 116
|
||||
ℹ suites 0
|
||||
ℹ pass 116
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 19908.802286
|
||||
POST-R6-R7 PARENT VALIDATION PASSED
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"timestamp": "2026-09-08T05:41:10.346Z",
|
||||
"content": [
|
||||
"Sign-off REJECTED. Missing:\nchecked-artifact list before VERDICT: accept\n\n--- judge ---\n...\nEL/converter.mjs`, lines 63-65, 74-75\n - Quote: `if (!Number.isFinite(numericValue)) { throw new Error('Value must be a finite number'); }` and `if (!Number.isFinite(result)) { throw new Error('Conversion resulted in a non-finite value'); }`\n - Confirms rejection of NaN, Infinity, and overflow-to-Infinity.\n\n6. **58 tests in converter.test.mjs using node:test**:\n - Path: `/tmp/pi-goals-herdr-uat.TGXaEL/converter.test.mjs`, lines 1-8\n - Quote: `import test from 'node:test'; import assert from 'node:assert'; import { convert } from './converter.mjs';`\n - Confirms real module import and node:test framework; grep found exactly 58 test cases.\n\n7. **Test output log saved and all 58 tests pass**:\n - Path: `/tmp/pi-goals-herdr-uat.TGXaEL/evidence/function-tests.log`, bottom\n - Quote: `ℹ tests 58` `ℹ pass 58` `ℹ fail 0`\n - Confirms all 58 tests passed in a real node:test run.\n\n8. **Specific discriminator test cases all passing**:\n - Path: `/tmp/pi-goals-herdr-uat.TGXaEL/evidence/function-tests.log`\n - Quote: \n - `✔ 0 C = 32 F` (correct known conversion both directions)\n - `✔ Accept exact -273.15 C` (exact absolute-zero acceptance)\n - `✔ Reject -273.16 C (below absolute zero)` (below-limit rejection)\n - `✔ Reject trailing junk \"12junk\"` (regression test: rejects partial parseFloat)\n - `✔ Reject hexadecimal \"0x10\"` (invalid syntax rejection)\n - `✔ Reject NaN input` and `✔ Reject Infinity input` (nonfinite input rejection)\n - `✔ Reject conversion overflow to Infinity` (nonfinite result rejection)\n - `✔ All valid inputs produce finite outputs` (output finiteness assertion)\n - Confirms all subtle failure modes explicitly tested and passing.\n\n9. **Goal 2 (CLI) explicitly NOT implemented** (per pause gate):\n - Paths: find results show no cli.mjs, cli.test.mjs, cli-demo.log, or cli-tests.log in fixture root\n - Confirms the pause gate after goal 1 was respected; goal 2 awaits explicit user instruction.\n\nVERDICT: accept\nmissing:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-09-08T06:00:40.739Z",
|
||||
"content": [
|
||||
"Judge returned no VERDICT line. Accepted inconclusive — logged.\n\n--- judge ---\n(no output)\n\nGoal ticked [x] in .pi/plan/01a07f74-f42d-7eee-85b0-10df065920a0-v1.md."
|
||||
]
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-09-08T06:09:00.490Z",
|
||||
"content": [
|
||||
"Supervisor: needs_work. Inspected cli.mjs, evidence/cli-tests.log and evidence/cli-demo.log. Core CLI behavior and junk regression are evidenced, but neither log includes the required nonfinite conversion-result case (a finite Number.MAX_VALUE string in C). Add a real child-process overflow rejection test and demo recording arguments, empty stdout, stderr and nonzero exit, then rerun and save actual logs. Also reconcile the stale worker view, which still says CLI work is paused, with the explicit user authorization before claiming this checkpoint complete."
|
||||
]
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-09-08T06:25:30.356Z",
|
||||
"content": [
|
||||
"Supervisor review failed: Error: Supervisor request cancelled"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env node
|
||||
// Bounded trial accounting only. Reads explicitly named Pi session JSONL files, not RPC event logs.
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
function session(path) {
|
||||
const rows = readFileSync(path, "utf8").trim().split("\n").filter(Boolean).map(line => JSON.parse(line));
|
||||
if (rows[0]?.type !== "session") throw new Error(`Not a Pi session JSONL file: ${path}`);
|
||||
const byId = new Map();
|
||||
for (const row of rows.slice(1)) if (typeof row.id === "string") byId.set(row.id, row);
|
||||
return { header: rows[0], entries: [...byId.values()] }; // Replayed/replaced records count once by entry ID.
|
||||
}
|
||||
|
||||
function snapshot(workerFile, manifestFile) {
|
||||
const path = resolve(workerFile);
|
||||
const worker = session(path);
|
||||
const manifest = { workerFile: path, workerSessionId: worker.header.id, startMs: Date.now(), baselineIds: worker.entries.map(entry => entry.id) };
|
||||
writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
||||
return { manifestFile: resolve(manifestFile), workerSessionId: worker.header.id, startMs: manifest.startMs, note: "Snapshot taken before Ready. Keep worker idle until Ready; do not overwrite this boundary." };
|
||||
}
|
||||
|
||||
const fields = ["input", "output", "cacheRead", "cacheWrite", "totalTokens"];
|
||||
const empty = () => ({ entries: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, missingUsageEntries: [], missingFields: {}, recordedPositiveCostSubtotal: 0, zeroOrMissingCostEntries: [] });
|
||||
function add(totals, entry, usage) {
|
||||
totals.entries++; // Persisted accounting entries, including listed missing-usage entries; not provider calls.
|
||||
if (!usage) { totals.missingUsageEntries.push(entry.id); return; }
|
||||
for (const field of fields) {
|
||||
if (Number.isFinite(usage[field]) && usage[field] >= 0) totals[field] += usage[field];
|
||||
else totals.missingFields[field] = (totals.missingFields[field] ?? 0) + 1;
|
||||
}
|
||||
if (Number.isFinite(usage.cost?.total) && usage.cost.total > 0) totals.recordedPositiveCostSubtotal += usage.cost.total;
|
||||
else totals.zeroOrMissingCostEntries.push(entry.id);
|
||||
}
|
||||
|
||||
function roleUsage(path, baseline, endMs, role) {
|
||||
const data = session(path);
|
||||
const inherited = new Set(baseline.baselineIds);
|
||||
const marker = role === "supervisor" ? data.entries.findIndex(entry => entry.type === "custom" && entry.customType === "pi-goals-supervisor" && resolve(entry.data?.binding?.workerSession ?? "/missing") === baseline.workerFile) : -1;
|
||||
if (role === "supervisor" && marker < 0) throw new Error("No matching native supervisor bootstrap marker: refusing to count inherited planning usage as supervisor work");
|
||||
const total = empty(); const assistant = empty(); const compaction = empty(); const nestedTools = empty();
|
||||
const models = new Set(); const compactions = []; const entriesCounted = []; const missingTimestamps = [];
|
||||
let selectedModel = null; let completionToolResults = 0;
|
||||
data.entries.forEach((entry, index) => {
|
||||
if (entry.type === "model_change") selectedModel = `${entry.provider}/${entry.modelId}`;
|
||||
if (entry.type === "message" && entry.message?.role === "assistant" && entry.message.provider && entry.message.model) selectedModel = `${entry.message.provider}/${entry.message.model}`;
|
||||
if (index <= marker || inherited.has(entry.id)) return;
|
||||
const time = Date.parse(entry.timestamp);
|
||||
if (!Number.isFinite(time)) { missingTimestamps.push(entry.id); return; }
|
||||
if (time < baseline.startMs || time > endMs) return;
|
||||
let category; let usage;
|
||||
if (entry.type === "message" && entry.message?.role === "assistant" && entry.message.stopReason !== "pending") {
|
||||
category = assistant; usage = entry.message.usage;
|
||||
models.add(`${entry.message.provider ?? "unknown"}/${entry.message.model ?? "unknown"}`);
|
||||
} else if (entry.type === "compaction" || entry.type === "branch_summary") {
|
||||
category = compaction; usage = entry.usage;
|
||||
compactions.push({ entryId: entry.id, type: entry.type, timestamp: entry.timestamp, selectedModel, modelAttribution: "selected model inferred from preceding session entries; a custom summarizer may use another model", tokensBefore: entry.tokensBefore ?? null, hasUsage: Boolean(usage) });
|
||||
} else if (entry.type === "message" && entry.message?.role === "toolResult") {
|
||||
if (entry.message.toolName === "CompleteGoal") completionToolResults++;
|
||||
if (entry.message.usage) { category = nestedTools; usage = entry.message.usage; }
|
||||
}
|
||||
if (!category) return;
|
||||
add(category, entry, usage); add(total, entry, usage); entriesCounted.push(entry.id);
|
||||
});
|
||||
return { role, sessionFile: resolve(path), sessionId: data.header.id, reportedAssistantModels: [...models], totalRecordedUsage: total, assistant, compaction, nestedTools, compactions, entriesCounted, excludedUnknownTimestampEntries: missingTimestamps, completionToolResults,
|
||||
cost: total.zeroOrMissingCostEntries.length || total.missingUsageEntries.length ? "unavailable/incomplete: zero or missing pricing is not proof of zero cost" : "recorded positive costs only; not independently verified pricing",
|
||||
compactionCoverage: "All post-boundary compaction/branch-summary usage is included when persisted. Missing entries are listed, not assumed free. Session records do not explicitly label initial versus later compaction; inspect the timestamp/marker sequence." };
|
||||
}
|
||||
|
||||
const [command, first, second, out] = process.argv.slice(2);
|
||||
let result;
|
||||
if (command === "start" && first && second) result = snapshot(first, second);
|
||||
else if (command === "finish" && first && second) {
|
||||
const baseline = JSON.parse(readFileSync(first, "utf8"));
|
||||
const worker = session(baseline.workerFile);
|
||||
if (worker.header.id !== baseline.workerSessionId) throw new Error("Worker session identity changed since the boundary");
|
||||
const endMs = Date.now();
|
||||
result = { startMs: baseline.startMs, endMs,
|
||||
worker: roleUsage(baseline.workerFile, baseline, endMs, "worker"), supervisor: roleUsage(resolve(second), baseline, endMs, "supervisor"),
|
||||
currentContext: "These are cumulative recorded token metrics, NOT current context. The >100k compaction trigger uses ctx.getContextUsage().tokens. Native compaction tokensBefore is reported separately above.",
|
||||
freshEvidenceJudge: { status: "instrumentation gap", explanation: "Current CompleteGoal runs a fresh pi -p --no-session judge and saves its text receipt, not provider Usage. Completion tool result counts do not establish judge invocation counts or tokens. Report judge tokens/cost unavailable unless separately instrumented in an authorized trial; do not hide this overhead in worker/supervisor totals." },
|
||||
limitations: ["Wait for both trial sessions to settle/stop before finish so no partial JSONL/provider usage is mistaken for completed work.", "Inherited pre-fork history is excluded using both the worker baseline ID set and the supervisor bootstrap marker.", "No RPC message_update/message_end events are counted; duplicate persisted entry IDs are counted once.", "The entries counters are persisted accounting-entry counts, not provider-call counts. Native split-turn compaction may combine multiple calls in one entry, as may aggregated nested-tool usage. Entries with missing usage are counted and listed separately.", "Tool-result nested usage is separate. Do not add it again from another session without proving it is disjoint.", "Long complete VCC refreshes are ordinary supervisor calls and therefore included in supervisor usage, not assumed free."] };
|
||||
} else throw new Error("Usage: node extract-trial-usage.mjs start WORKER_SESSION.jsonl BOUNDARY.json | finish BOUNDARY.json SUPERVISOR_SESSION.jsonl [USAGE_OUTPUT.json]");
|
||||
const text = `${JSON.stringify(result, null, 2)}\n`;
|
||||
if (command === "finish" && out) writeFileSync(out, text, { flag: "wx", mode: 0o600 });
|
||||
process.stdout.write(text);
|
||||
@@ -0,0 +1 @@
|
||||
{"error":{"code":"agent_prompt_stalled","message":"agent prompt produced no observed working or blocked state within 5000 ms; current status is done"},"id":"cli:agent:prompt"}
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"sourceSession": "/home/ubuntu/.pi/agent/sessions/--tmp-pi-goals-herdr-uat.TGXaEL--/2026-09-08T05-27-39-268Z_01a07f7c-7a04-7015-bfca-8af0a2fc2089.jsonl",
|
||||
"compactions": [
|
||||
{
|
||||
"id": "4c93819c",
|
||||
"timestamp": "2026-09-08T06:03:51.958Z",
|
||||
"tokensBefore": 43242,
|
||||
"firstKeptEntryId": "19aff06f",
|
||||
"usage": {
|
||||
"input": 6,
|
||||
"output": 1811,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 13474,
|
||||
"reasoning": 12,
|
||||
"totalTokens": 15291,
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0,
|
||||
"total": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"advice": [
|
||||
{
|
||||
"timestamp": "2026-09-08T05:35:15.748Z",
|
||||
"tool": "steer",
|
||||
"arguments": {
|
||||
"message": "Ready is selected: implement goal one only, with converter.mjs and converter.test.mjs, then run node --test converter.test.mjs and save actual output to evidence/function-tests.log. Report the artifact paths and quote the saved test results for sign-off; do not start any CLI work before the user's explicit instruction."
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-09-08T05:37:00.558Z",
|
||||
"tool": "review_goal",
|
||||
"arguments": {
|
||||
"decision": "needs_work",
|
||||
"reason": "Read converter.mjs: it directly returns `(numericValue * 9 / 5) + 32` and `(numericValue - 32) * 5 / 9` without a finite-result check. Goal one requires rejecting nonfinite conversion results. Add the result guard and an explicit overflow rejection test, rerun the function suite and save its actual log. Keep CLI work paused."
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-09-08T05:38:59.617Z",
|
||||
"tool": "review_goal",
|
||||
"arguments": {
|
||||
"decision": "needs_work",
|
||||
"reason": "converter.mjs now guards both results, but converter.test.mjs's 'Verify conversion output validation is enforced' only checks 1e10, 0 and 32 produce finite outputs. Add assert.throws(() => convert(Number.MAX_VALUE, 'C'), /non-finite/) to exercise actual result overflow, rerun node --test converter.test.mjs and save the actual log. The current test cannot distinguish the earlier broken implementation. Keep goal two paused."
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-09-08T05:39:48.084Z",
|
||||
"tool": "review_goal",
|
||||
"arguments": {
|
||||
"decision": "approve",
|
||||
"reason": "Goal one remains in scope. The inspected implementation has finite-result guards, and converter.test.mjs now asserts actual overflow rejection with Number.MAX_VALUE. evidence/function-tests.log records '✔ Reject conversion overflow to Infinity', 'ℹ tests 58', 'ℹ pass 58', and 'ℹ fail 0'. Approve this checkpoint only; preserve the user-required pause before any goal-two work."
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-09-08T05:41:29.463Z",
|
||||
"tool": "let_it_run",
|
||||
"arguments": {
|
||||
"reason": "The worker states: \"Per the execution gate in the plan, I am now PAUSED and waiting for your explicit instruction before implementing Goal 2 (CLI).\" This matches the required user-controlled pause."
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-09-08T05:58:40.850Z",
|
||||
"tool": "let_it_run",
|
||||
"arguments": {
|
||||
"reason": "The worker states: \"Per the execution gate in the plan, I am now PAUSED and waiting for your explicit instruction before implementing Goal 2 (CLI).\" No subsequent user authorization for goal two appears in the view."
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-09-08T06:00:19.055Z",
|
||||
"tool": "review_goal",
|
||||
"arguments": {
|
||||
"decision": "approve",
|
||||
"reason": "Previously inspected converter.mjs finite-result guards and converter.test.mjs assertion that convert(Number.MAX_VALUE, 'C') throws /non-finite/. The latest worker view names evidence/function-tests.log and quotes 'ℹ tests 58', 'ℹ pass 58', 'ℹ fail 0', plus '✔ Reject conversion overflow to Infinity (0.299328ms)'. Direction and scope match goal one; independent artifact verification should confirm sign-off. Goal two remains paused pending explicit user instruction."
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-09-08T06:00:53.932Z",
|
||||
"tool": "let_it_run",
|
||||
"arguments": {
|
||||
"reason": "The user instructed: \"Keep all CLI work paused until I explicitly authorize goal two.\" The worker reports: \"I am now paused and waiting for your explicit instruction before implementing Goal 2 (the CLI). No CLI files have been created.\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-09-08T06:09:00.488Z",
|
||||
"tool": "review_goal",
|
||||
"arguments": {
|
||||
"decision": "needs_work",
|
||||
"reason": "Inspected cli.mjs, evidence/cli-tests.log and evidence/cli-demo.log. Core CLI behavior and junk regression are evidenced, but neither log includes the required nonfinite conversion-result case (a finite Number.MAX_VALUE string in C). Add a real child-process overflow rejection test and demo recording arguments, empty stdout, stderr and nonzero exit, then rerun and save actual logs. Also reconcile the stale worker view, which still says CLI work is paused, with the explicit user authorization before claiming this checkpoint complete."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run && npm run test:supervisor
|
||||
|
||||
|
||||
RUN v4.1.9 /home/ubuntu/.pi/agent/worktrees/pi-goals-simple-visible-supervision
|
||||
|
||||
|
||||
Test Files 14 passed (14)
|
||||
Tests 81 passed (81)
|
||||
Start at 14:01:25
|
||||
Duration 24.12s (transform 2.35s, setup 503ms, import 23.03s, tests 50.95s, environment 6ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test:supervisor
|
||||
> node --import tsx --test test/internal-supervisor/*.test.ts
|
||||
|
||||
✔ retries intercom registration when pi-intercom loads after pi-supervise (4.227298ms)
|
||||
✔ a directive with no text is rejected, so the worker never sees undefined (0.426221ms)
|
||||
✔ a directive from the paired supervisor becomes a real user message (37.225884ms)
|
||||
✔ a directive to a busy worker interrupts, instead of waiting for the whole task (15.784109ms)
|
||||
✔ a directive from an unpaired session is dropped (12.29568ms)
|
||||
✔ a second pair takes over, and the first supervisor is told it lost the worker (26.396215ms)
|
||||
✔ only the paired worker can end a run (8.158348ms)
|
||||
✔ the programmatic pairing API waits for the worker acknowledgement (2.239757ms)
|
||||
✔ the worker acknowledges a pair, so the supervisor knows it was heard (8.486998ms)
|
||||
✔ a goal the supervisor inferred reaches the worker, which owns the view header (142.929505ms)
|
||||
✔ the second view carries only what happened after the first (442.433685ms)
|
||||
✔ a message addressed to a different session is ignored (10.742462ms)
|
||||
✔ on settle the worker publishes a view built from the live branch (56.519214ms)
|
||||
✔ the view is built from the live branch, not from every entry in the session (56.418381ms)
|
||||
✔ an unpaired session publishes nothing on settle (0.741332ms)
|
||||
✔ supervision never stops itself: no round limit at all (5.534566ms)
|
||||
✔ goal, pairing and the steer count all survive a reload together (0.550053ms)
|
||||
✔ a view arriving during unrelated supervisor thinking waits for a fresh complete overview (11.333382ms)
|
||||
✔ the nudge repeats neither the instructions already sent nor the verdict rules (5.152421ms)
|
||||
✔ a multi-line goal returns to supervisor context every fifth review and after compaction (32.247189ms)
|
||||
✔ a one-line goal is not redundantly reinserted (27.177279ms)
|
||||
✔ a check in and a worker that stopped ask for different things (10.584784ms)
|
||||
✔ a loop still gets named after the supervisor compacts, from restored state (1.050267ms)
|
||||
✔ a session that does not answer the roll call is not offered as a worker (501.888517ms)
|
||||
✔ a child run stays out of the roll call, so it can never be picked (5.986158ms)
|
||||
✔ a session already paired stays out of the roll call, and a free one answers (16.582061ms)
|
||||
✔ /supervise look asks the worker for a fresh view, rather than the supervisor guessing (307.790991ms)
|
||||
✔ let_it_run says the turn is over, so it is not called four times running (0.647696ms)
|
||||
✔ a sign-off verdict is answered, not aborted, and a runaway is still cut (0.842584ms)
|
||||
✔ every verdict result names the way to end the turn, steer included (0.574206ms)
|
||||
✔ an old view is dropped from context once its verdict is in, and the verdict is kept (1.074702ms)
|
||||
✔ a worker session never has its context rewritten (0.294353ms)
|
||||
✔ a newly presented view starts a fresh look (5.78599ms)
|
||||
✔ a tool a worker cannot use never aborts its turn (0.418385ms)
|
||||
✔ a resume onto a session that is gone drops the pairing and says so (5.830953ms)
|
||||
✔ a resume onto a live worker keeps supervising, and takes the writers back off (6.00274ms)
|
||||
✔ state written before recentSteers existed still loads (0.169851ms)
|
||||
✔ done unpairs the worker, so it stops publishing views (421.186666ms)
|
||||
✔ with no goal the supervisor cannot steer, it must ask the human (0.899218ms)
|
||||
✔ set_goal binds an inferred goal, and steering then works (500.8593ms)
|
||||
✔ a goal given at pair time still allows steering (0.585863ms)
|
||||
✔ done is refused while the worker has an unanswered tool call (12.548547ms)
|
||||
✔ done is allowed once nothing is outstanding (6.609937ms)
|
||||
✔ steer refuses when the session is not supervising (0.480846ms)
|
||||
✔ a reworded repeat of an earlier instruction is sent, and named back to the supervisor (0.801901ms)
|
||||
✔ overlap scores rewording high and a different instruction low (0.177702ms)
|
||||
✔ the view of the old worker cannot be used to judge the new one (6.401726ms)
|
||||
✔ with one other session here, /supervise needs no target and the whole line is the goal (500.788638ms)
|
||||
✔ naming the worker still works, and the rest of the line is the goal (0.601582ms)
|
||||
✔ with two free sessions here, /supervise asks which one, and pairs with the choice (501.732414ms)
|
||||
✔ a goal that is a path is read from the file, so it is not pasted every run (2.041028ms)
|
||||
✔ a long goal is one short line above the picker, and reaches the worker whole (501.665702ms)
|
||||
✔ a session that stayed quiet is still on the list, because 0 free is a dead end (500.678627ms)
|
||||
✔ a cancelled picker pairs with nothing (500.632721ms)
|
||||
✔ supervising takes the writing tools away, and stopping gives them back (501.535796ms)
|
||||
✔ stopping gives back the writers without undoing another extension's tools (501.829786ms)
|
||||
✔ a first word that names no session is refused, rather than folded into the goal (0.789825ms)
|
||||
✔ a goal with spaces needs no target, and @name takes the rest of the line as the goal (501.348506ms)
|
||||
✔ the brief starts no turn, so there is no answer before the first view (6.442694ms)
|
||||
✔ /supervise goal changes the goal without breaking the pairing (1.012968ms)
|
||||
✔ the footer says which side of a pairing this session is, and clears when it ends (506.27294ms)
|
||||
✔ a session that is not supervising never sees the supervisor tools (6.599768ms)
|
||||
✔ worker_view refuses when there is no worker, rather than implying a pairing (0.619204ms)
|
||||
✔ the view names the worker's model and how full its context is (58.171961ms)
|
||||
✔ supervising a second session is refused while the first is still paired (0.831384ms)
|
||||
✔ the supervisor gets a look at a working worker every half hour, without being asked (975.124652ms)
|
||||
✔ a human message in the worker session is not a reason to stand back (6.169812ms)
|
||||
✔ letting a stopped worker run says plainly that the worker stays stopped (11.286563ms)
|
||||
✔ a stopped worker is looked at again, so let_it_run cannot silence the pairing (926.086028ms)
|
||||
✔ a worker that pairs at the prompt and never takes a turn is still watched (605.707779ms)
|
||||
✔ a worker that reloads at the prompt starts watching itself again (605.044296ms)
|
||||
✔ a timer look at a worker that has not moved is not sent, until it has been skipped three times (2428.612949ms)
|
||||
✔ the worker counts reviews in a row where nothing changed (579.699233ms)
|
||||
✔ an unacknowledged pair gives up, and a takeover cancels that timer (4.893877ms)
|
||||
✔ duplicate standalone Intercom registries are diagnosed and cannot bootstrap a plan (0.862724ms)
|
||||
✔ retained non-plan supervision reconnects after the worker reloads, not on unrelated peer traffic (345.917589ms)
|
||||
✔ retained non-plan supervision reconnects after the supervisor reloads, not on unrelated peer traffic (628.225316ms)
|
||||
✔ busy supervisor retains its active view and requests one complete overview after settling (31.243343ms)
|
||||
✔ plan bootstrap compacts only the supervisor and pairing alone never starts a worker or a review (32.264425ms)
|
||||
✔ goal decisions are correlated, preserve the pair across two goals, and cannot call overall done (4.70302ms)
|
||||
✔ abort and stop cancel pending requests; late decisions cannot approve a replacement (3.9249ms)
|
||||
✔ 50 actual model turns trigger one view, independent of the number of messages (5.510548ms)
|
||||
✔ unknown background providers are not proof of quiescence (0.401943ms)
|
||||
✔ stale plan content invalidates a pending goal review (6.905509ms)
|
||||
✔ small forks skip compaction, but real compaction failure prevents pairing (3.703867ms)
|
||||
✔ native small-history result permits startup after an attempted compaction (50000) (1.967591ms)
|
||||
✔ native small-history result permits startup after an attempted compaction (null) (2.078297ms)
|
||||
✔ the hour timer and a coincident turn checkpoint produce a single view (6.466486ms)
|
||||
✔ settled checks wait for tracked processes and subagents to finish (2.789561ms)
|
||||
✔ bootstrap stop cannot resurrect a supervisor after compaction completes (2.36892ms)
|
||||
✔ a restarted worker reconnects by exact saved session identity without a new supervisor (5.448449ms)
|
||||
✔ unknown initial context must compact instead of taking the known-small shortcut (1.249941ms)
|
||||
✔ null post-compaction usage cannot raise the next configured 100k checkpoint (2.734318ms)
|
||||
✔ stopping a routine view during compaction invalidates its suspended continuation (3.144067ms)
|
||||
✔ restart of a provisional bootstrap resumes compaction and pairing in the same saved session (3.524611ms)
|
||||
✔ command preserves a stopped supervisor across reload (2.800035ms)
|
||||
✔ done preserves a stopped supervisor across reload (2.91728ms)
|
||||
✔ same-binding replay retains activation when the supervisor lost its acknowledgement (3.51941ms)
|
||||
✔ model-unavailable stop validates binding, cancels pending reviews and ignores old directives (2.44535ms)
|
||||
✔ supervisor mode is a native inspection allowlist at visibility and execution, including reload and stopped forks (2.534243ms)
|
||||
✔ a cancelled checkpoint's delayed verdict cannot approve the replacement checkpoint (3.005501ms)
|
||||
✔ each model call reanchors the canonical plan and role without losing compacted planning context or judgments (2.28053ms)
|
||||
✔ routine assessments and steering display the actual advice rather than only a receipt (2.069783ms)
|
||||
✔ current context above 100k compacts; cumulative usage and exactly 100k do not (1.465216ms)
|
||||
✔ absent optional trackers count as zero tracked work; installed failed or busy trackers remain non-quiet (0.298529ms)
|
||||
✔ a settled worker with no optional trackers sends one review, not repeated idle wakes (2.344359ms)
|
||||
✔ disconnect cancels a checkpoint and blocks steering; local stop still clears ownership (1.802362ms)
|
||||
✔ registered malformed subagent tracker stays unknown even when the process tracker is absent (0.320226ms)
|
||||
✔ a busy supervisor defers the 100k compaction until its own run settles (1.549437ms)
|
||||
✔ a healthy goal review can take longer than ten minutes without cancellation or another pairing (1.953315ms)
|
||||
✔ busy plan supervisor refreshes cumulative VCC evidence without an idle feedback loop (5.894195ms)
|
||||
✔ progress during a refresh remains pending with only one look in flight (4.562161ms)
|
||||
✔ complete refresh uses the current worker branch after compaction (3.866041ms)
|
||||
✔ complete refresh uses the current worker branch after rewind (2.442543ms)
|
||||
✔ complete refresh uses the current worker branch after reload (3.280382ms)
|
||||
✔ explicit checkpoints wait separately from routine coalescing and do not replace an active assessment (3.029511ms)
|
||||
✔ actual supervisor provider failure is returned explicitly without another pair or an elapsed-time cancellation (3.241667ms)
|
||||
✔ a provider error followed by native retry success does not cancel the supervisor checkpoint (5.838725ms)
|
||||
✔ a failed full overview waits without spinning and recovers on routine progress (7.869651ms)
|
||||
✔ a failed full overview waits without spinning and recovers on explicit look (3.50097ms)
|
||||
✔ overview display metadata uses native context without requiring a remote roster lookup (4.49743ms)
|
||||
✔ a superseded advance re-drives the current checkpoint after deferred compaction success (4.541734ms)
|
||||
✔ a superseded advance re-drives the current checkpoint after deferred compaction rejection (3.364931ms)
|
||||
✔ stopping current work during an obsolete advance prevents re-drive after compaction success (2.761416ms)
|
||||
✔ stopping current work during an obsolete advance prevents re-drive after compaction rejection (9.510342ms)
|
||||
✔ a child process named pi is found by ps, and stops being found when it exits (400.670699ms)
|
||||
✔ the check is a snapshot, so it cannot hold up the worker's settle (325.855943ms)
|
||||
✔ a one-line goal stays whole while a multi-line goal has a locator (7.139299ms)
|
||||
✔ a view carries only the turns the supervisor has not been sent (2.42323ms)
|
||||
✔ the last two reasoning blocks stay in the narrative, and older ones drop out (1.126327ms)
|
||||
✔ a compaction restarts the view, so no turn falls into the gap (0.636179ms)
|
||||
✔ pi-vcc reports the files the worker wrote, and separates them from the ones it read (1.636892ms)
|
||||
✔ progressKey is unchanged when a review produced no new file or commit (0.641537ms)
|
||||
✔ progressKey still sees a new file past pi-vcc's ten path display cap (0.746451ms)
|
||||
✔ a commit counts as progress, even when no file was written since (0.993827ms)
|
||||
✔ outstandingWork finds tool calls that never got a result (1.512703ms)
|
||||
✔ buildView reports a tool call with no result, so done can be refused (0.974291ms)
|
||||
✔ the view says how many reviews in a row changed nothing, and says nothing at zero (2.051843ms)
|
||||
✔ the view merges the worker's compaction summary with the turns after it (0.46901ms)
|
||||
✔ a turn the compaction summary already covers is not sent twice (0.347676ms)
|
||||
✔ pi-vcc's sections and its transcript land on the right sides of the split (2.622939ms)
|
||||
✔ the view does not tell the supervisor to use vcc_recall, a tool it does not have (0.426445ms)
|
||||
✔ supervisor directives are not sent back as worker evidence (0.553966ms)
|
||||
✔ bookkeeping tool calls are kept out of the transcript (0.416196ms)
|
||||
✔ buildView reports the goal, status, and files without historical failures (0.446159ms)
|
||||
✔ how long the worker has been quiet, measured from its own last entry (0.547843ms)
|
||||
✔ buildView keeps the newest turns when it has to cut for the channel limit (42.609016ms)
|
||||
✔ pi's own branch logic drops the abandoned fork, on a session file (2208.621704ms)
|
||||
✔ a long goal cannot push the view past the broker limit (0.558345ms)
|
||||
✔ a bounded complete overview explicitly labels a truncated worker compaction summary (0.355821ms)
|
||||
ℹ tests 150
|
||||
ℹ suites 0
|
||||
ℹ pass 150
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 20896.672059
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 build
|
||||
> tsc -p tsconfig.build.json
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 36 files in 92ms. No fixes applied.
|
||||
@@ -0,0 +1,196 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run && npm run test:supervisor
|
||||
|
||||
|
||||
RUN v4.1.9 /home/ubuntu/.pi/agent/worktrees/pi-goals-simple-visible-supervision
|
||||
|
||||
|
||||
Test Files 14 passed (14)
|
||||
Tests 81 passed (81)
|
||||
Start at 14:39:34
|
||||
Duration 23.08s (transform 1.86s, setup 383ms, import 20.70s, tests 48.44s, environment 3ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test:supervisor
|
||||
> node --import tsx --test test/internal-supervisor/*.test.ts
|
||||
|
||||
✔ retries intercom registration when pi-intercom loads after pi-supervise (3.902174ms)
|
||||
✔ a directive with no text is rejected, so the worker never sees undefined (0.328116ms)
|
||||
✔ a directive from the paired supervisor becomes a real user message (21.426997ms)
|
||||
✔ a directive to a busy worker interrupts, instead of waiting for the whole task (14.219762ms)
|
||||
✔ a directive from an unpaired session is dropped (12.533882ms)
|
||||
✔ a second pair takes over, and the first supervisor is told it lost the worker (27.9299ms)
|
||||
✔ only the paired worker can end a run (7.03982ms)
|
||||
✔ the programmatic pairing API waits for the worker acknowledgement (1.53026ms)
|
||||
✔ the worker acknowledges a pair, so the supervisor knows it was heard (5.487175ms)
|
||||
✔ a goal the supervisor inferred reaches the worker, which owns the view header (137.577579ms)
|
||||
✔ the second view carries only what happened after the first (435.179753ms)
|
||||
✔ a message addressed to a different session is ignored (10.696368ms)
|
||||
✔ on settle the worker publishes a view built from the live branch (57.306992ms)
|
||||
✔ the view is built from the live branch, not from every entry in the session (56.68447ms)
|
||||
✔ an unpaired session publishes nothing on settle (0.581989ms)
|
||||
✔ supervision never stops itself: no round limit at all (5.758318ms)
|
||||
✔ goal, pairing and the steer count all survive a reload together (0.566147ms)
|
||||
✔ a view arriving during unrelated supervisor thinking waits for a fresh complete overview (11.73855ms)
|
||||
✔ the nudge repeats neither the instructions already sent nor the verdict rules (6.185005ms)
|
||||
✔ a multi-line goal returns to supervisor context every fifth review and after compaction (31.516221ms)
|
||||
✔ a one-line goal is not redundantly reinserted (28.008114ms)
|
||||
✔ a check in and a worker that stopped ask for different things (11.439857ms)
|
||||
✔ a loop still gets named after the supervisor compacts, from restored state (1.117781ms)
|
||||
✔ a session that does not answer the roll call is not offered as a worker (501.3539ms)
|
||||
✔ a child run stays out of the roll call, so it can never be picked (5.030483ms)
|
||||
✔ a session already paired stays out of the roll call, and a free one answers (15.089392ms)
|
||||
✔ /supervise look asks the worker for a fresh view, rather than the supervisor guessing (307.193495ms)
|
||||
✔ let_it_run says the turn is over, so it is not called four times running (0.676302ms)
|
||||
✔ a sign-off verdict is answered, not aborted, and a runaway is still cut (0.656312ms)
|
||||
✔ every verdict result names the way to end the turn, steer included (0.473839ms)
|
||||
✔ an old view is dropped from context once its verdict is in, and the verdict is kept (1.053468ms)
|
||||
✔ a worker session never has its context rewritten (0.333986ms)
|
||||
✔ a newly presented view starts a fresh look (5.745135ms)
|
||||
✔ a tool a worker cannot use never aborts its turn (0.405935ms)
|
||||
✔ a resume onto a session that is gone drops the pairing and says so (6.018145ms)
|
||||
✔ a resume onto a live worker keeps supervising, and takes the writers back off (6.026155ms)
|
||||
✔ state written before recentSteers existed still loads (0.179773ms)
|
||||
✔ done unpairs the worker, so it stops publishing views (430.643048ms)
|
||||
✔ with no goal the supervisor cannot steer, it must ask the human (0.730352ms)
|
||||
✔ set_goal binds an inferred goal, and steering then works (501.012999ms)
|
||||
✔ a goal given at pair time still allows steering (0.570176ms)
|
||||
✔ done is refused while the worker has an unanswered tool call (12.627739ms)
|
||||
✔ done is allowed once nothing is outstanding (6.242551ms)
|
||||
✔ steer refuses when the session is not supervising (0.340749ms)
|
||||
✔ a reworded repeat of an earlier instruction is sent, and named back to the supervisor (0.807133ms)
|
||||
✔ overlap scores rewording high and a different instruction low (0.131635ms)
|
||||
✔ the view of the old worker cannot be used to judge the new one (4.978392ms)
|
||||
✔ with one other session here, /supervise needs no target and the whole line is the goal (500.193542ms)
|
||||
✔ naming the worker still works, and the rest of the line is the goal (0.567943ms)
|
||||
✔ with two free sessions here, /supervise asks which one, and pairs with the choice (501.080782ms)
|
||||
✔ a goal that is a path is read from the file, so it is not pasted every run (1.947811ms)
|
||||
✔ a long goal is one short line above the picker, and reaches the worker whole (501.036423ms)
|
||||
✔ a session that stayed quiet is still on the list, because 0 free is a dead end (501.634751ms)
|
||||
✔ a cancelled picker pairs with nothing (500.580837ms)
|
||||
✔ supervising takes the writing tools away, and stopping gives them back (501.518809ms)
|
||||
✔ stopping gives back the writers without undoing another extension's tools (500.660152ms)
|
||||
✔ a first word that names no session is refused, rather than folded into the goal (0.57234ms)
|
||||
✔ a goal with spaces needs no target, and @name takes the rest of the line as the goal (501.882273ms)
|
||||
✔ the brief starts no turn, so there is no answer before the first view (6.132116ms)
|
||||
✔ /supervise goal changes the goal without breaking the pairing (0.710704ms)
|
||||
✔ the footer says which side of a pairing this session is, and clears when it ends (506.538204ms)
|
||||
✔ a session that is not supervising never sees the supervisor tools (4.968927ms)
|
||||
✔ worker_view refuses when there is no worker, rather than implying a pairing (0.621227ms)
|
||||
✔ the view names the worker's model and how full its context is (56.12934ms)
|
||||
✔ supervising a second session is refused while the first is still paired (0.813791ms)
|
||||
✔ the supervisor gets a look at a working worker every half hour, without being asked (972.724402ms)
|
||||
✔ a human message in the worker session is not a reason to stand back (5.144485ms)
|
||||
✔ letting a stopped worker run says plainly that the worker stays stopped (10.435518ms)
|
||||
✔ a stopped worker is looked at again, so let_it_run cannot silence the pairing (932.562879ms)
|
||||
✔ a worker that pairs at the prompt and never takes a turn is still watched (604.177279ms)
|
||||
✔ a worker that reloads at the prompt starts watching itself again (603.890441ms)
|
||||
✔ a timer look at a worker that has not moved is not sent, until it has been skipped three times (2426.054837ms)
|
||||
✔ the worker counts reviews in a row where nothing changed (587.040645ms)
|
||||
✔ an unacknowledged pair gives up, and a takeover cancels that timer (4.99076ms)
|
||||
✔ duplicate standalone Intercom registries are diagnosed and cannot bootstrap a plan (0.572811ms)
|
||||
✔ retained non-plan supervision reconnects after the worker reloads, not on unrelated peer traffic (343.931651ms)
|
||||
✔ retained non-plan supervision reconnects after the supervisor reloads, not on unrelated peer traffic (626.873765ms)
|
||||
✔ busy supervisor retains its active view and requests one complete overview after settling (32.087166ms)
|
||||
✔ each checkpoint freezes fresh worker evidence and direction without replacing a busy assessment (31.899373ms)
|
||||
✔ checkpoint snapshot building cannot publish after abort (2.880596ms)
|
||||
✔ checkpoint snapshot building cannot publish after stop (8.956519ms)
|
||||
✔ checkpoint snapshot building cannot publish after reload (2.667688ms)
|
||||
✔ checkpoint snapshot building cannot publish after plan change (2.877615ms)
|
||||
✔ checkpoint capture includes user direction arriving while tracked work is queried (2.511844ms)
|
||||
✔ checkpoint payload fits the serialized channel limit without truncating its identity (72.81385ms)
|
||||
✔ settled empty final response fails the checkpoint explicitly ([]) (9.685596ms)
|
||||
✔ settled empty final response fails the checkpoint explicitly (["text"]) (2.899287ms)
|
||||
✔ settled empty final response fails the checkpoint explicitly (["text"]) (2.850772ms)
|
||||
✔ settled empty final response fails the checkpoint explicitly (["thinking"]) (2.388046ms)
|
||||
✔ failed assessment resumes on later worker progress without a human poke (stop) (2.318919ms)
|
||||
✔ failed assessment resumes on later worker progress without a human poke (error) (1.930327ms)
|
||||
✔ empty low-level response may continue through compaction, ask a human, or finish with a tool verdict (2.482665ms)
|
||||
✔ a successful goal tool verdict is not undone by an empty final response (2.214831ms)
|
||||
✔ a duplicate checkpoint rejection never echoes its snapshot or changes the active view (2.275401ms)
|
||||
✔ an empty routine assessment cannot fail a separately queued checkpoint (2.55659ms)
|
||||
✔ plan bootstrap compacts only the supervisor and pairing alone never starts a worker or a review (2.075322ms)
|
||||
✔ goal decisions are correlated, preserve the pair across two goals, and cannot call overall done (3.23601ms)
|
||||
✔ abort and stop cancel pending requests; late decisions cannot approve a replacement (2.16195ms)
|
||||
✔ 50 actual model turns trigger one view, independent of the number of messages (2.424652ms)
|
||||
✔ unknown background providers are not proof of quiescence (0.207091ms)
|
||||
✔ stale plan content invalidates a pending goal review (2.674516ms)
|
||||
✔ small forks skip compaction, but real compaction failure prevents pairing (2.267117ms)
|
||||
✔ native small-history result permits startup after an attempted compaction (50000) (1.378746ms)
|
||||
✔ native small-history result permits startup after an attempted compaction (null) (1.096433ms)
|
||||
✔ the hour timer and a coincident turn checkpoint produce a single view (6.533516ms)
|
||||
✔ settled checks wait for tracked processes and subagents to finish (3.623399ms)
|
||||
✔ bootstrap stop cannot resurrect a supervisor after compaction completes (5.414626ms)
|
||||
✔ a restarted worker reconnects by exact saved session identity without a new supervisor (2.799798ms)
|
||||
✔ unknown initial context must compact instead of taking the known-small shortcut (1.042567ms)
|
||||
✔ null post-compaction usage cannot raise the next configured 100k checkpoint (2.177116ms)
|
||||
✔ stopping a routine view during compaction invalidates its suspended continuation (1.83495ms)
|
||||
✔ restart of a provisional bootstrap resumes compaction and pairing in the same saved session (2.098918ms)
|
||||
✔ command preserves a stopped supervisor across reload (1.88657ms)
|
||||
✔ done preserves a stopped supervisor across reload (2.164052ms)
|
||||
✔ same-binding replay retains activation when the supervisor lost its acknowledgement (2.445582ms)
|
||||
✔ model-unavailable stop validates binding, cancels pending reviews and ignores old directives (1.882713ms)
|
||||
✔ supervisor mode is a native inspection allowlist at visibility and execution, including reload and stopped forks (2.055621ms)
|
||||
✔ a cancelled checkpoint's delayed verdict cannot approve the replacement checkpoint (2.097359ms)
|
||||
✔ each model call reanchors the canonical plan and role without losing compacted planning context or judgments (1.652662ms)
|
||||
✔ routine assessments and steering display the actual advice rather than only a receipt (1.941714ms)
|
||||
✔ current context above 100k compacts; cumulative usage and exactly 100k do not (1.494223ms)
|
||||
✔ absent optional trackers count as zero tracked work; installed failed or busy trackers remain non-quiet (0.277273ms)
|
||||
✔ a settled worker with no optional trackers sends one review, not repeated idle wakes (1.891433ms)
|
||||
✔ disconnect cancels a checkpoint and blocks steering; local stop still clears ownership (1.836482ms)
|
||||
✔ registered malformed subagent tracker stays unknown even when the process tracker is absent (0.301608ms)
|
||||
✔ a busy supervisor defers the 100k compaction until its own run settles (1.994581ms)
|
||||
✔ a healthy goal review can take longer than ten minutes without cancellation or another pairing (1.921063ms)
|
||||
✔ busy plan supervisor refreshes cumulative VCC evidence without an idle feedback loop (2.61882ms)
|
||||
✔ progress during a refresh remains pending with only one look in flight (2.679516ms)
|
||||
✔ complete refresh uses the current worker branch after compaction (2.362564ms)
|
||||
✔ complete refresh uses the current worker branch after rewind (1.881952ms)
|
||||
✔ complete refresh uses the current worker branch after reload (1.942139ms)
|
||||
✔ explicit checkpoints wait separately from routine coalescing and do not replace an active assessment (2.066604ms)
|
||||
✔ actual supervisor provider failure is returned explicitly without another pair or an elapsed-time cancellation (1.521288ms)
|
||||
✔ a provider error followed by native retry success does not cancel the supervisor checkpoint (1.446067ms)
|
||||
✔ a failed full overview waits without spinning and recovers on routine progress (3.125641ms)
|
||||
✔ a failed full overview waits without spinning and recovers on explicit look (2.105449ms)
|
||||
✔ overview display metadata uses native context without requiring a remote roster lookup (1.67603ms)
|
||||
✔ a superseded advance re-drives the current checkpoint after deferred compaction success (2.494889ms)
|
||||
✔ a superseded advance re-drives the current checkpoint after deferred compaction rejection (2.082903ms)
|
||||
✔ stopping current work during an obsolete advance prevents re-drive after compaction success (1.93112ms)
|
||||
✔ stopping current work during an obsolete advance prevents re-drive after compaction rejection (1.735847ms)
|
||||
✔ goal_review accepts an optional bounded snapshot but rejects malformed snapshots (1.554983ms)
|
||||
✔ checkpoint snapshot bounding counts JSON escapes and does not split Unicode characters (4.67607ms)
|
||||
✔ a child process named pi is found by ps, and stops being found when it exits (400.386374ms)
|
||||
✔ the check is a snapshot, so it cannot hold up the worker's settle (351.256976ms)
|
||||
✔ latest user direction survives bounded summaries, compaction and later supervisor echoes (107.306515ms)
|
||||
✔ oversized user direction is visibly bounded with a source reference (2.769796ms)
|
||||
✔ a one-line goal stays whole while a multi-line goal has a locator (1.048328ms)
|
||||
✔ a view carries only the turns the supervisor has not been sent (1.039401ms)
|
||||
✔ the last two reasoning blocks stay in the narrative, and older ones drop out (0.805118ms)
|
||||
✔ a compaction restarts the view, so no turn falls into the gap (0.380018ms)
|
||||
✔ pi-vcc reports the files the worker wrote, and separates them from the ones it read (1.083438ms)
|
||||
✔ progressKey is unchanged when a review produced no new file or commit (0.507079ms)
|
||||
✔ progressKey still sees a new file past pi-vcc's ten path display cap (0.530678ms)
|
||||
✔ a commit counts as progress, even when no file was written since (1.701632ms)
|
||||
✔ outstandingWork finds tool calls that never got a result (1.014392ms)
|
||||
✔ buildView reports a tool call with no result, so done can be refused (0.70119ms)
|
||||
✔ the view says how many reviews in a row changed nothing, and says nothing at zero (0.40831ms)
|
||||
✔ the view merges the worker's compaction summary with the turns after it (0.34633ms)
|
||||
✔ a turn the compaction summary already covers is not sent twice (0.281564ms)
|
||||
✔ pi-vcc's sections and its transcript land on the right sides of the split (1.499473ms)
|
||||
✔ the view does not tell the supervisor to use vcc_recall, a tool it does not have (0.20563ms)
|
||||
✔ supervisor directives are not sent back as worker evidence (0.354027ms)
|
||||
✔ bookkeeping tool calls are kept out of the transcript (0.318495ms)
|
||||
✔ buildView reports the goal, status, and files without historical failures (0.286487ms)
|
||||
✔ how long the worker has been quiet, measured from its own last entry (0.341991ms)
|
||||
✔ buildView keeps the newest turns when it has to cut for the channel limit (39.367034ms)
|
||||
✔ pi's own branch logic drops the abandoned fork, on a session file (2082.572485ms)
|
||||
✔ a long goal cannot push the view past the broker limit (0.59147ms)
|
||||
✔ a bounded complete overview explicitly labels a truncated worker compaction summary (0.371831ms)
|
||||
ℹ tests 171
|
||||
ℹ suites 0
|
||||
ℹ pass 171
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 21133.203043
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc -p tsconfig.build.json --noEmit
|
||||
@@ -0,0 +1,3 @@
|
||||
Restored prior state: /home/ubuntu/.pi/agent/pi-goals/planning-model.json
|
||||
Restored prior state: /home/ubuntu/.pi/agent/pi-goals/worker-model.json
|
||||
Restored prior state: /home/ubuntu/.pi/agent/pi-goals/supervisor-model.json
|
||||
@@ -0,0 +1,37 @@
|
||||
does not end supervision. A
|
||||
later worker view will
|
||||
report the worker state.
|
||||
|
||||
|
||||
|
||||
[supervisor_brief]
|
||||
|
||||
Progress assessment: The
|
||||
worker states: "Per the
|
||||
execution gate in the plan,
|
||||
I am now PAUSED and waiting
|
||||
for your explicit
|
||||
instruction before
|
||||
implementing Goal 2 (CLI)."
|
||||
This matches the required
|
||||
user-controlled pause.
|
||||
|
||||
|
||||
──────────────────────────────
|
||||
$ printf
|
||||
EXECUTION_SHOULD_BE_BLOCKED_
|
||||
42
|
||||
|
||||
Supervisor mode is
|
||||
inspection-only. Run
|
||||
commands in the worker pane.
|
||||
|
||||
(exit 1)
|
||||
──────────────────────────────
|
||||
|
||||
──────────────────────────────
|
||||
|
||||
──────────────────────────────
|
||||
/tmp/pi-goals-herdr-uat.TGX...
|
||||
↑84 ↓4.0k R534k W153k CH99....
|
||||
👁 watching 1
|
||||
@@ -0,0 +1,35 @@
|
||||
supervisor message. If the
|
||||
goal remains unmet, send a
|
||||
concrete continuation
|
||||
instruction. A human message
|
||||
does not end supervision. A
|
||||
later worker view will
|
||||
report the worker state.
|
||||
|
||||
|
||||
|
||||
[supervisor_brief]
|
||||
|
||||
Progress assessment: The
|
||||
worker states: "cli-demo.log
|
||||
was hand-written (heredoc)
|
||||
instead of captured from
|
||||
actual CLI runs" and
|
||||
"Waiting for explicit
|
||||
operator resumption." The
|
||||
outstanding operator
|
||||
instruction was "Do not call
|
||||
CompleteGoal or do further
|
||||
work until the test operator
|
||||
explicitly resumes you after
|
||||
reloading the corrected
|
||||
package."
|
||||
|
||||
|
||||
Plan supervision stopped
|
||||
|
||||
──────────────────────────────
|
||||
|
||||
──────────────────────────────
|
||||
/tmp/pi-goals-herdr-uat.TGX...
|
||||
↑168 ↓8.0k R1.3M W442k CH99...
|
||||
@@ -0,0 +1,355 @@
|
||||
{
|
||||
"startMs": 1788845259248,
|
||||
"endMs": 1788849703205,
|
||||
"worker": {
|
||||
"role": "worker",
|
||||
"sessionFile": "/home/ubuntu/.pi/agent/sessions/--tmp-pi-goals-herdr-uat.TGXaEL--/2026-09-08T05-19-26-254Z_01a07f74-f42d-7eee-85b0-10df065920a0.jsonl",
|
||||
"sessionId": "01a07f74-f42d-7eee-85b0-10df065920a0",
|
||||
"reportedAssistantModels": [
|
||||
"github-copilot/claude-haiku-4-5-20251001",
|
||||
"github-copilot/claude-haiku-4.5"
|
||||
],
|
||||
"totalRecordedUsage": {
|
||||
"entries": 68,
|
||||
"input": 427,
|
||||
"output": 39611,
|
||||
"cacheRead": 3479071,
|
||||
"cacheWrite": 730435,
|
||||
"totalTokens": 4249544,
|
||||
"missingUsageEntries": [],
|
||||
"missingFields": {},
|
||||
"recordedPositiveCostSubtotal": 1.45943285,
|
||||
"zeroOrMissingCostEntries": [
|
||||
"6aeb938d"
|
||||
]
|
||||
},
|
||||
"assistant": {
|
||||
"entries": 68,
|
||||
"input": 427,
|
||||
"output": 39611,
|
||||
"cacheRead": 3479071,
|
||||
"cacheWrite": 730435,
|
||||
"totalTokens": 4249544,
|
||||
"missingUsageEntries": [],
|
||||
"missingFields": {},
|
||||
"recordedPositiveCostSubtotal": 1.45943285,
|
||||
"zeroOrMissingCostEntries": [
|
||||
"6aeb938d"
|
||||
]
|
||||
},
|
||||
"compaction": {
|
||||
"entries": 0,
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0,
|
||||
"totalTokens": 0,
|
||||
"missingUsageEntries": [],
|
||||
"missingFields": {},
|
||||
"recordedPositiveCostSubtotal": 0,
|
||||
"zeroOrMissingCostEntries": []
|
||||
},
|
||||
"nestedTools": {
|
||||
"entries": 0,
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0,
|
||||
"totalTokens": 0,
|
||||
"missingUsageEntries": [],
|
||||
"missingFields": {},
|
||||
"recordedPositiveCostSubtotal": 0,
|
||||
"zeroOrMissingCostEntries": []
|
||||
},
|
||||
"compactions": [],
|
||||
"entriesCounted": [
|
||||
"6a6e04a7",
|
||||
"b4a2f4f2",
|
||||
"febe0d96",
|
||||
"bf13d8c3",
|
||||
"2b51e152",
|
||||
"2adb0ad3",
|
||||
"2dab558e",
|
||||
"3d12be91",
|
||||
"f8b5837f",
|
||||
"341448f4",
|
||||
"c2b896e6",
|
||||
"e9d164a6",
|
||||
"e93ba0f2",
|
||||
"c38dc00e",
|
||||
"934af358",
|
||||
"837660f0",
|
||||
"d3a4614c",
|
||||
"843d9263",
|
||||
"76712952",
|
||||
"71541d12",
|
||||
"140245f3",
|
||||
"65409dac",
|
||||
"d9245939",
|
||||
"b1613a95",
|
||||
"df541386",
|
||||
"11f0c5ee",
|
||||
"50ca611e",
|
||||
"1c90f42a",
|
||||
"bb8b64ac",
|
||||
"7df8162f",
|
||||
"4f48db60",
|
||||
"8dfec4a3",
|
||||
"7e694984",
|
||||
"b708c0a6",
|
||||
"fca206ff",
|
||||
"cab1d0e9",
|
||||
"9369d60e",
|
||||
"4853512b",
|
||||
"936ea424",
|
||||
"9e5711e3",
|
||||
"c6db50c8",
|
||||
"42667d9f",
|
||||
"760e0677",
|
||||
"a56881eb",
|
||||
"dfe7af6f",
|
||||
"e4ad17b3",
|
||||
"6c771286",
|
||||
"de4ee95c",
|
||||
"c67a026c",
|
||||
"b385af99",
|
||||
"6aeb938d",
|
||||
"21facf63",
|
||||
"0f468d8d",
|
||||
"a22beee4",
|
||||
"85e01443",
|
||||
"ff92478f",
|
||||
"bac91d19",
|
||||
"e5dee53f",
|
||||
"9e5d823a",
|
||||
"21c9c61e",
|
||||
"1e3eca90",
|
||||
"4ac37cf2",
|
||||
"b4d5514f",
|
||||
"29b074e3",
|
||||
"5d8f3026",
|
||||
"beecb616",
|
||||
"01d61a64",
|
||||
"a5db8329"
|
||||
],
|
||||
"excludedUnknownTimestampEntries": [],
|
||||
"completionToolResults": 8,
|
||||
"cost": "unavailable/incomplete: zero or missing pricing is not proof of zero cost",
|
||||
"compactionCoverage": "All post-boundary compaction/branch-summary usage is included when persisted. Missing entries are listed, not assumed free. Session records do not explicitly label initial versus later compaction; inspect the timestamp/marker sequence."
|
||||
},
|
||||
"supervisor": {
|
||||
"role": "supervisor",
|
||||
"sessionFile": "/home/ubuntu/.pi/agent/sessions/--tmp-pi-goals-herdr-uat.TGXaEL--/2026-09-08T05-27-39-268Z_01a07f7c-7a04-7015-bfca-8af0a2fc2089.jsonl",
|
||||
"sessionId": "01a07f7c-7a04-7015-bfca-8af0a2fc2089",
|
||||
"reportedAssistantModels": [
|
||||
"github-copilot/gpt-6-astra"
|
||||
],
|
||||
"totalRecordedUsage": {
|
||||
"entries": 42,
|
||||
"input": 129,
|
||||
"output": 5035,
|
||||
"cacheRead": 1097576,
|
||||
"cacheWrite": 422866,
|
||||
"totalTokens": 1525606,
|
||||
"missingUsageEntries": [],
|
||||
"missingFields": {},
|
||||
"recordedPositiveCostSubtotal": 0,
|
||||
"zeroOrMissingCostEntries": [
|
||||
"4eef3d84",
|
||||
"e5bae9bb",
|
||||
"63ff341f",
|
||||
"efbc771f",
|
||||
"139ffb13",
|
||||
"c5e54d92",
|
||||
"59b3a2c8",
|
||||
"99860239",
|
||||
"efddcca1",
|
||||
"c7b02a2d",
|
||||
"e389821d",
|
||||
"fd1cd048",
|
||||
"cc59835c",
|
||||
"1189c2d8",
|
||||
"3d700727",
|
||||
"60e98242",
|
||||
"f0ecb5f7",
|
||||
"2591e9c8",
|
||||
"14a00f13",
|
||||
"2961318a",
|
||||
"4c93819c",
|
||||
"e02b8597",
|
||||
"b5f9e2f5",
|
||||
"7e91736c",
|
||||
"06e83709",
|
||||
"b4ecb5c3",
|
||||
"3409e629",
|
||||
"74ec901a",
|
||||
"09f9fce1",
|
||||
"b6482bac",
|
||||
"1638dffa",
|
||||
"cdb6286f",
|
||||
"5b6914e0",
|
||||
"43f6c1dc",
|
||||
"a4f89f95",
|
||||
"b4dfad7e",
|
||||
"f506115f",
|
||||
"91ac16f1",
|
||||
"c3e8f1de",
|
||||
"311303ce",
|
||||
"55083f25",
|
||||
"b989cb43"
|
||||
]
|
||||
},
|
||||
"assistant": {
|
||||
"entries": 41,
|
||||
"input": 123,
|
||||
"output": 3224,
|
||||
"cacheRead": 1097576,
|
||||
"cacheWrite": 409392,
|
||||
"totalTokens": 1510315,
|
||||
"missingUsageEntries": [],
|
||||
"missingFields": {},
|
||||
"recordedPositiveCostSubtotal": 0,
|
||||
"zeroOrMissingCostEntries": [
|
||||
"4eef3d84",
|
||||
"e5bae9bb",
|
||||
"63ff341f",
|
||||
"efbc771f",
|
||||
"139ffb13",
|
||||
"c5e54d92",
|
||||
"59b3a2c8",
|
||||
"99860239",
|
||||
"efddcca1",
|
||||
"c7b02a2d",
|
||||
"e389821d",
|
||||
"fd1cd048",
|
||||
"cc59835c",
|
||||
"1189c2d8",
|
||||
"3d700727",
|
||||
"60e98242",
|
||||
"f0ecb5f7",
|
||||
"2591e9c8",
|
||||
"14a00f13",
|
||||
"2961318a",
|
||||
"e02b8597",
|
||||
"b5f9e2f5",
|
||||
"7e91736c",
|
||||
"06e83709",
|
||||
"b4ecb5c3",
|
||||
"3409e629",
|
||||
"74ec901a",
|
||||
"09f9fce1",
|
||||
"b6482bac",
|
||||
"1638dffa",
|
||||
"cdb6286f",
|
||||
"5b6914e0",
|
||||
"43f6c1dc",
|
||||
"a4f89f95",
|
||||
"b4dfad7e",
|
||||
"f506115f",
|
||||
"91ac16f1",
|
||||
"c3e8f1de",
|
||||
"311303ce",
|
||||
"55083f25",
|
||||
"b989cb43"
|
||||
]
|
||||
},
|
||||
"compaction": {
|
||||
"entries": 1,
|
||||
"input": 6,
|
||||
"output": 1811,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 13474,
|
||||
"totalTokens": 15291,
|
||||
"missingUsageEntries": [],
|
||||
"missingFields": {},
|
||||
"recordedPositiveCostSubtotal": 0,
|
||||
"zeroOrMissingCostEntries": [
|
||||
"4c93819c"
|
||||
]
|
||||
},
|
||||
"nestedTools": {
|
||||
"entries": 0,
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0,
|
||||
"totalTokens": 0,
|
||||
"missingUsageEntries": [],
|
||||
"missingFields": {},
|
||||
"recordedPositiveCostSubtotal": 0,
|
||||
"zeroOrMissingCostEntries": []
|
||||
},
|
||||
"compactions": [
|
||||
{
|
||||
"entryId": "4c93819c",
|
||||
"type": "compaction",
|
||||
"timestamp": "2026-09-08T06:03:51.958Z",
|
||||
"selectedModel": "github-copilot/gpt-6-astra",
|
||||
"modelAttribution": "selected model inferred from preceding session entries; a custom summarizer may use another model",
|
||||
"tokensBefore": 43242,
|
||||
"hasUsage": true
|
||||
}
|
||||
],
|
||||
"entriesCounted": [
|
||||
"4eef3d84",
|
||||
"e5bae9bb",
|
||||
"63ff341f",
|
||||
"efbc771f",
|
||||
"139ffb13",
|
||||
"c5e54d92",
|
||||
"59b3a2c8",
|
||||
"99860239",
|
||||
"efddcca1",
|
||||
"c7b02a2d",
|
||||
"e389821d",
|
||||
"fd1cd048",
|
||||
"cc59835c",
|
||||
"1189c2d8",
|
||||
"3d700727",
|
||||
"60e98242",
|
||||
"f0ecb5f7",
|
||||
"2591e9c8",
|
||||
"14a00f13",
|
||||
"2961318a",
|
||||
"4c93819c",
|
||||
"e02b8597",
|
||||
"b5f9e2f5",
|
||||
"7e91736c",
|
||||
"06e83709",
|
||||
"b4ecb5c3",
|
||||
"3409e629",
|
||||
"74ec901a",
|
||||
"09f9fce1",
|
||||
"b6482bac",
|
||||
"1638dffa",
|
||||
"cdb6286f",
|
||||
"5b6914e0",
|
||||
"43f6c1dc",
|
||||
"a4f89f95",
|
||||
"b4dfad7e",
|
||||
"f506115f",
|
||||
"91ac16f1",
|
||||
"c3e8f1de",
|
||||
"311303ce",
|
||||
"55083f25",
|
||||
"b989cb43"
|
||||
],
|
||||
"excludedUnknownTimestampEntries": [],
|
||||
"completionToolResults": 0,
|
||||
"cost": "unavailable/incomplete: zero or missing pricing is not proof of zero cost",
|
||||
"compactionCoverage": "All post-boundary compaction/branch-summary usage is included when persisted. Missing entries are listed, not assumed free. Session records do not explicitly label initial versus later compaction; inspect the timestamp/marker sequence."
|
||||
},
|
||||
"currentContext": "These are cumulative recorded token metrics, NOT current context. The >100k compaction trigger uses ctx.getContextUsage().tokens. Native compaction tokensBefore is reported separately above.",
|
||||
"freshEvidenceJudge": {
|
||||
"status": "instrumentation gap",
|
||||
"explanation": "Current CompleteGoal runs a fresh pi -p --no-session judge and saves its text receipt, not provider Usage. Completion tool result counts do not establish judge invocation counts or tokens. Report judge tokens/cost unavailable unless separately instrumented in an authorized trial; do not hide this overhead in worker/supervisor totals."
|
||||
},
|
||||
"limitations": [
|
||||
"Wait for both trial sessions to settle/stop before finish so no partial JSONL/provider usage is mistaken for completed work.",
|
||||
"Inherited pre-fork history is excluded using both the worker baseline ID set and the supervisor bootstrap marker.",
|
||||
"No RPC message_update/message_end events are counted; duplicate persisted entry IDs are counted once.",
|
||||
"The entries counters are persisted accounting-entry counts, not provider-call counts. Native split-turn compaction may combine multiple calls in one entry, as may aggregated nested-tool usage. Entries with missing usage are counted and listed separately.",
|
||||
"Tool-result nested usage is separate. Do not add it again from another session without proving it is disjoint.",
|
||||
"Long complete VCC refreshes are ordinary supervisor calls and therefore included in supervisor usage, not assumed free."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"startMs": 1788845259248,
|
||||
"endMs": 1788846961538,
|
||||
"worker": {
|
||||
"role": "worker",
|
||||
"sessionFile": "/home/ubuntu/.pi/agent/sessions/--tmp-pi-goals-herdr-uat.TGXaEL--/2026-09-08T05-19-26-254Z_01a07f74-f42d-7eee-85b0-10df065920a0.jsonl",
|
||||
"sessionId": "01a07f74-f42d-7eee-85b0-10df065920a0",
|
||||
"reportedAssistantModels": [
|
||||
"github-copilot/claude-haiku-4-5-20251001"
|
||||
],
|
||||
"totalRecordedUsage": {
|
||||
"entries": 31,
|
||||
"input": 192,
|
||||
"output": 19409,
|
||||
"cacheRead": 1208000,
|
||||
"cacheWrite": 141969,
|
||||
"totalTokens": 1369570,
|
||||
"missingUsageEntries": [],
|
||||
"missingFields": {},
|
||||
"recordedPositiveCostSubtotal": 0.39549825,
|
||||
"zeroOrMissingCostEntries": []
|
||||
},
|
||||
"assistant": {
|
||||
"entries": 31,
|
||||
"input": 192,
|
||||
"output": 19409,
|
||||
"cacheRead": 1208000,
|
||||
"cacheWrite": 141969,
|
||||
"totalTokens": 1369570,
|
||||
"missingUsageEntries": [],
|
||||
"missingFields": {},
|
||||
"recordedPositiveCostSubtotal": 0.39549825,
|
||||
"zeroOrMissingCostEntries": []
|
||||
},
|
||||
"compaction": {
|
||||
"entries": 0,
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0,
|
||||
"totalTokens": 0,
|
||||
"missingUsageEntries": [],
|
||||
"missingFields": {},
|
||||
"recordedPositiveCostSubtotal": 0,
|
||||
"zeroOrMissingCostEntries": []
|
||||
},
|
||||
"nestedTools": {
|
||||
"entries": 0,
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0,
|
||||
"totalTokens": 0,
|
||||
"missingUsageEntries": [],
|
||||
"missingFields": {},
|
||||
"recordedPositiveCostSubtotal": 0,
|
||||
"zeroOrMissingCostEntries": []
|
||||
},
|
||||
"compactions": [],
|
||||
"entriesCounted": [
|
||||
"6a6e04a7",
|
||||
"b4a2f4f2",
|
||||
"febe0d96",
|
||||
"bf13d8c3",
|
||||
"2b51e152",
|
||||
"2adb0ad3",
|
||||
"2dab558e",
|
||||
"3d12be91",
|
||||
"f8b5837f",
|
||||
"341448f4",
|
||||
"c2b896e6",
|
||||
"e9d164a6",
|
||||
"e93ba0f2",
|
||||
"c38dc00e",
|
||||
"934af358",
|
||||
"837660f0",
|
||||
"d3a4614c",
|
||||
"843d9263",
|
||||
"76712952",
|
||||
"71541d12",
|
||||
"140245f3",
|
||||
"65409dac",
|
||||
"d9245939",
|
||||
"b1613a95",
|
||||
"df541386",
|
||||
"11f0c5ee",
|
||||
"50ca611e",
|
||||
"1c90f42a",
|
||||
"bb8b64ac",
|
||||
"7df8162f",
|
||||
"4f48db60"
|
||||
],
|
||||
"excludedUnknownTimestampEntries": [],
|
||||
"completionToolResults": 3,
|
||||
"cost": "recorded positive costs only; not independently verified pricing",
|
||||
"compactionCoverage": "All post-boundary compaction/branch-summary usage is included when persisted. Missing entries are listed, not assumed free. Session records do not explicitly label initial versus later compaction; inspect the timestamp/marker sequence."
|
||||
},
|
||||
"supervisor": {
|
||||
"role": "supervisor",
|
||||
"sessionFile": "/home/ubuntu/.pi/agent/sessions/--tmp-pi-goals-herdr-uat.TGXaEL--/2026-09-08T05-27-39-268Z_01a07f7c-7a04-7015-bfca-8af0a2fc2089.jsonl",
|
||||
"sessionId": "01a07f7c-7a04-7015-bfca-8af0a2fc2089",
|
||||
"reportedAssistantModels": [
|
||||
"github-copilot/gpt-6-astra"
|
||||
],
|
||||
"totalRecordedUsage": {
|
||||
"entries": 15,
|
||||
"input": 45,
|
||||
"output": 1065,
|
||||
"cacheRead": 353752,
|
||||
"cacheWrite": 133546,
|
||||
"totalTokens": 488408,
|
||||
"missingUsageEntries": [],
|
||||
"missingFields": {},
|
||||
"recordedPositiveCostSubtotal": 0,
|
||||
"zeroOrMissingCostEntries": [
|
||||
"4eef3d84",
|
||||
"e5bae9bb",
|
||||
"63ff341f",
|
||||
"efbc771f",
|
||||
"139ffb13",
|
||||
"c5e54d92",
|
||||
"59b3a2c8",
|
||||
"99860239",
|
||||
"efddcca1",
|
||||
"c7b02a2d",
|
||||
"e389821d",
|
||||
"fd1cd048",
|
||||
"cc59835c",
|
||||
"1189c2d8",
|
||||
"3d700727"
|
||||
]
|
||||
},
|
||||
"assistant": {
|
||||
"entries": 15,
|
||||
"input": 45,
|
||||
"output": 1065,
|
||||
"cacheRead": 353752,
|
||||
"cacheWrite": 133546,
|
||||
"totalTokens": 488408,
|
||||
"missingUsageEntries": [],
|
||||
"missingFields": {},
|
||||
"recordedPositiveCostSubtotal": 0,
|
||||
"zeroOrMissingCostEntries": [
|
||||
"4eef3d84",
|
||||
"e5bae9bb",
|
||||
"63ff341f",
|
||||
"efbc771f",
|
||||
"139ffb13",
|
||||
"c5e54d92",
|
||||
"59b3a2c8",
|
||||
"99860239",
|
||||
"efddcca1",
|
||||
"c7b02a2d",
|
||||
"e389821d",
|
||||
"fd1cd048",
|
||||
"cc59835c",
|
||||
"1189c2d8",
|
||||
"3d700727"
|
||||
]
|
||||
},
|
||||
"compaction": {
|
||||
"entries": 0,
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0,
|
||||
"totalTokens": 0,
|
||||
"missingUsageEntries": [],
|
||||
"missingFields": {},
|
||||
"recordedPositiveCostSubtotal": 0,
|
||||
"zeroOrMissingCostEntries": []
|
||||
},
|
||||
"nestedTools": {
|
||||
"entries": 0,
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0,
|
||||
"totalTokens": 0,
|
||||
"missingUsageEntries": [],
|
||||
"missingFields": {},
|
||||
"recordedPositiveCostSubtotal": 0,
|
||||
"zeroOrMissingCostEntries": []
|
||||
},
|
||||
"compactions": [],
|
||||
"entriesCounted": [
|
||||
"4eef3d84",
|
||||
"e5bae9bb",
|
||||
"63ff341f",
|
||||
"efbc771f",
|
||||
"139ffb13",
|
||||
"c5e54d92",
|
||||
"59b3a2c8",
|
||||
"99860239",
|
||||
"efddcca1",
|
||||
"c7b02a2d",
|
||||
"e389821d",
|
||||
"fd1cd048",
|
||||
"cc59835c",
|
||||
"1189c2d8",
|
||||
"3d700727"
|
||||
],
|
||||
"excludedUnknownTimestampEntries": [],
|
||||
"completionToolResults": 0,
|
||||
"cost": "unavailable/incomplete: zero or missing pricing is not proof of zero cost",
|
||||
"compactionCoverage": "All post-boundary compaction/branch-summary usage is included when persisted. Missing entries are listed, not assumed free. Session records do not explicitly label initial versus later compaction; inspect the timestamp/marker sequence."
|
||||
},
|
||||
"currentContext": "These are cumulative recorded token metrics, NOT current context. The >100k compaction trigger uses ctx.getContextUsage().tokens. Native compaction tokensBefore is reported separately above.",
|
||||
"freshEvidenceJudge": {
|
||||
"status": "instrumentation gap",
|
||||
"explanation": "Current CompleteGoal runs a fresh pi -p --no-session judge and saves its text receipt, not provider Usage. Completion tool result counts do not establish judge invocation counts or tokens. Report judge tokens/cost unavailable unless separately instrumented in an authorized trial; do not hide this overhead in worker/supervisor totals."
|
||||
},
|
||||
"limitations": [
|
||||
"Wait for both trial sessions to settle/stop before finish so no partial JSONL/provider usage is mistaken for completed work.",
|
||||
"Inherited pre-fork history is excluded using both the worker baseline ID set and the supervisor bootstrap marker.",
|
||||
"No RPC message_update/message_end events are counted; duplicate persisted entry IDs are counted once.",
|
||||
"The entries counters are persisted accounting-entry counts, not provider-call counts. Native split-turn compaction may combine multiple calls in one entry, as may aggregated nested-tool usage. Entries with missing usage are counted and listed separately.",
|
||||
"Tool-result nested usage is separate. Do not add it again from another session without proving it is disjoint.",
|
||||
"Long complete VCC refreshes are ordinary supervisor calls and therefore included in supervisor usage, not assumed free."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
✔ failed assessment resumes on later worker progress without a human poke (stop) (22.112625ms)
|
||||
✔ failed assessment resumes on later worker progress without a human poke (error) (3.617584ms)
|
||||
ℹ tests 2
|
||||
ℹ suites 0
|
||||
ℹ pass 2
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1889.732341
|
||||
@@ -0,0 +1,46 @@
|
||||
✖ failed assessment resumes on later worker progress without a human poke (stop) (29.280507ms)
|
||||
✖ failed assessment resumes on later worker progress without a human poke (error) (5.669199ms)
|
||||
ℹ tests 2
|
||||
ℹ suites 0
|
||||
ℹ pass 0
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1895.833929
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at test/internal-supervisor/plan.test.ts:1:12636
|
||||
✖ failed assessment resumes on later worker progress without a human poke (stop) (29.280507ms)
|
||||
AssertionError [ERR_ASSERTION]: ordinary worker progress must resume supervision without user input
|
||||
|
||||
1 !== 2
|
||||
|
||||
at TestContext.<anonymous> (/home/ubuntu/.pi/agent/worktrees/pi-goals-simple-visible-supervision/test/internal-supervisor/plan.test.ts:200:12)
|
||||
at async Test.run (node:internal/test_runner/test:1208:7)
|
||||
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:385:3) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 1,
|
||||
expected: 2,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at test/internal-supervisor/plan.test.ts:1:12636
|
||||
✖ failed assessment resumes on later worker progress without a human poke (error) (5.669199ms)
|
||||
AssertionError [ERR_ASSERTION]: ordinary worker progress must resume supervision without user input
|
||||
|
||||
1 !== 2
|
||||
|
||||
at TestContext.<anonymous> (/home/ubuntu/.pi/agent/worktrees/pi-goals-simple-visible-supervision/test/internal-supervisor/plan.test.ts:200:12)
|
||||
at async Test.run (node:internal/test_runner/test:1208:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:831:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 1,
|
||||
expected: 2,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
hand-written (heredoc)
|
||||
instead of captured from
|
||||
actual CLI runs
|
||||
2. Temporary files were
|
||||
written outside the
|
||||
fixture
|
||||
3. Need to capture real
|
||||
child-process demos from
|
||||
within the fixture only
|
||||
|
||||
Files preserved:
|
||||
- converter.mjs ✓
|
||||
- converter.test.mjs ✓
|
||||
- cli.mjs ✓
|
||||
- cli.test.mjs ✓
|
||||
- evidence/function-tests.lo
|
||||
g ✓
|
||||
- evidence/cli-tests.log ✓
|
||||
- evidence/cli-demo.log
|
||||
(needs regeneration from
|
||||
real runs)
|
||||
|
||||
Waiting for explicit
|
||||
operator resumption.
|
||||
|
||||
Persistent plan steward
|
||||
disabled.
|
||||
|
||||
▸ Provide a CLI with
|
||||
demonstrated valid and
|
||||
invalid inputs.
|
||||
──────────────────────────────
|
||||
|
||||
──────────────────────────────
|
||||
/tmp/pi-goals-herdr-uat.TGX...
|
||||
↑466 ↓43k R3.7M W749k CH99....
|
||||
◷ 1/2 goals
|
||||
@@ -0,0 +1,20 @@
|
||||
node:net:1986
|
||||
const error = new UVExceptionWithHostPort(rval, 'listen', address, port);
|
||||
^
|
||||
|
||||
Error: listen EPERM: operation not permitted /tmp/claude/tsx-1000/28.pipe
|
||||
at Server.setupListenHandle [as _listen2] (node:net:1986:21)
|
||||
at listenInCluster (node:net:2065:12)
|
||||
at Server.listen (node:net:2187:5)
|
||||
at file:///tmp/pi-goals-broker-diagnostic-1T7ENA/package/node_modules/tsx/dist/cli.mjs:53:31472
|
||||
at new Promise (<anonymous>)
|
||||
at createIpcServer (file:///tmp/pi-goals-broker-diagnostic-1T7ENA/package/node_modules/tsx/dist/cli.mjs:53:31450)
|
||||
at async file:///tmp/pi-goals-broker-diagnostic-1T7ENA/package/node_modules/tsx/dist/cli.mjs:55:542 {
|
||||
code: 'EPERM',
|
||||
errno: -1,
|
||||
syscall: 'listen',
|
||||
address: '/tmp/claude/tsx-1000/28.pipe',
|
||||
port: -1
|
||||
}
|
||||
|
||||
Node.js v25.8.1
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
RUN v4.1.9 /home/ubuntu/.pi/agent/worktrees/pi-goals-simple-visible-supervision
|
||||
|
||||
|
||||
Test Files 3 passed (3)
|
||||
Tests 43 passed (43)
|
||||
Start at 09:06:46
|
||||
Duration 2.31s (transform 729ms, setup 86ms, import 2.98s, tests 844ms, environment 0ms)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
## Review
|
||||
|
||||
Read-only recheck limited to the parent’s fixes for the previously reported P1/P2.
|
||||
|
||||
- **Fixed — P1 resolved:** `src/prompts.ts:199–201` now requires retaining every goal line, completion status, and evidence references above `## Log`; only verbose settled detail moves into the Appendix. This matches the unchanged identity/status filtering in `src/index.ts:220–228`. The pruning instruction and Git-history assumption are gone.
|
||||
- `test/prompts.test.ts:32–38` guards the corrected instructions.
|
||||
- `test/goals-flow.test.ts:183–201` moves supporting detail below the fold, exercises the reload hook, and confirms both accept/inconclusive records, the `2/2` count, and inconclusive disclosure remain intact without emitting new work.
|
||||
|
||||
- **Fixed — P2 resolved:** `src/prompts.ts:291–294` now directs the judge to review the unique exact subject and reject missing or ambiguous identity without substituting another goal. The overview in `src/index.ts:21–30` and descriptions in `test/tick-goal.test.ts:16–25` agree with that contract. `test/prompts.test.ts:40–44` guards against restoring fuzzy-match wording.
|
||||
|
||||
- **Correct:** These are bounded prompt/documentation and regression changes. They resolve the conflicts without changing sign-off invalidation semantics, inferring historical approval, or introducing a Git gate.
|
||||
|
||||
**No issues found.**
|
||||
|
||||
### Validation
|
||||
|
||||
Inspected the parent’s saved logs:
|
||||
|
||||
- `housekeeping-red.log`: the two new prompt assertions failed against the former wording.
|
||||
- `housekeeping-green.log`: **3 test files / 43 tests passed**.
|
||||
|
||||
No commands were run or files edited by this reviewer.
|
||||
|
||||
### Merge verdict: OK with notes
|
||||
|
||||
Both previous findings are resolved; the narrow fixes are approved. This supersedes the previous source-review block.
|
||||
|
||||
Full `npm test` and real isolated Herdr acceptance remain environment-blocked/pending. The targeted tests establish prompt and lifecycle behavior, not real-model judgment or completed two-goal acceptance. Broader parent validation was not attested by this recheck.
|
||||
@@ -0,0 +1,81 @@
|
||||
|
||||
RUN v4.1.9 /home/ubuntu/.pi/agent/worktrees/pi-goals-simple-visible-supervision
|
||||
|
||||
❯ test/prompts.test.ts (6 tests | 2 failed | 4 skipped) 23ms
|
||||
× keeps signed-off goal identities during plan housekeeping 18ms
|
||||
× gives the judge an exact subject rather than a fuzzy-match fallback 3ms
|
||||
|
||||
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
|
||||
|
||||
FAIL test/prompts.test.ts > planning prompt > keeps signed-off goal identities during plan housekeeping
|
||||
AssertionError: expected '<system-reminder>\nYour plan (.pi/pla…' to contain 'keep every goal line and its completi…'
|
||||
|
||||
- Expected
|
||||
+ Received
|
||||
|
||||
- keep every goal line and its completion status above ## Log
|
||||
+ <system-reminder>
|
||||
+ Your plan (.pi/plan/test.md, above the fold; the log, learnings and appendix are in the file):
|
||||
+
|
||||
+ plan
|
||||
+
|
||||
+ Keep it current as you work, with your normal edit tool:
|
||||
+ - tick finished subtasks ([/] in progress), add discovered ones
|
||||
+ - append ONE short line to ## Log, and a line to ## Learnings for a gotcha worth keeping
|
||||
+ - when the active goal's discriminator is satisfied, fill its evidence: list (each item = a durable
|
||||
+ artifact + a verbatim quote you actually observed + a short read of it), then call CompleteGoal.
|
||||
+ Don't tick a goal [x] before CompleteGoal accepts; the sign-off log line is the audit trail.
|
||||
+ - if the working set has grown long, prune finished goals (their evidence lives in git history and
|
||||
+ ## Log) and move settled detail down to ## Appendix, which is unlimited
|
||||
+ - the human's latest message outranks this plan. If it corrects the deliverable or scope, amend the
|
||||
+ user-visible result, user voice, and affected goals before continuing; don't defend the old plan
|
||||
+ - otherwise keep working toward the active goal; don't stop to ask unless genuinely blocked
|
||||
+ </system-reminder>
|
||||
|
||||
❯ test/prompts.test.ts:34:16
|
||||
32| it("keeps signed-off goal identities during plan housekeeping", () =>…
|
||||
33| const text = reminder("plan", ".pi/plan/test.md");
|
||||
34| expect(text).toContain("keep every goal line and its completion stat…
|
||||
| ^
|
||||
35| expect(text).toContain("evidence references beside each goal");
|
||||
36| expect(text).not.toContain("prune finished goals");
|
||||
|
||||
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯
|
||||
|
||||
FAIL test/prompts.test.ts > planning prompt > gives the judge an exact subject rather than a fuzzy-match fallback
|
||||
AssertionError: expected 'The working agent claims this goal is…' to contain 'unique exact goal subject'
|
||||
|
||||
- Expected
|
||||
+ Received
|
||||
|
||||
- unique exact goal subject
|
||||
+ The working agent claims this goal is complete:
|
||||
+
|
||||
+ goal: first
|
||||
+
|
||||
+ Below is the full plan file (plan.md). Find that goal in it (tolerate small wording drift; if
|
||||
+ you cannot find a matching goal at all, reject and say so). Read User-visible result and User voice
|
||||
+ first, then its discriminator, subtle failure modes, verify command, and evidence list.
|
||||
+
|
||||
+ --- plan file ---
|
||||
+ 1. [ ] goal: first
|
||||
+ --- end plan file ---
|
||||
+
|
||||
+ Read the cited artifacts (you cannot execute anything), then give your VERDICT.
|
||||
|
||||
❯ test/prompts.test.ts:42:16
|
||||
40| it("gives the judge an exact subject rather than a fuzzy-match fallba…
|
||||
41| const text = judgeUser({ goal: "first", plan: "1. [ ] goal: first", …
|
||||
42| expect(text).toContain("unique exact goal subject");
|
||||
| ^
|
||||
43| expect(text).not.toContain("tolerate small wording drift");
|
||||
44| });
|
||||
|
||||
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯
|
||||
|
||||
|
||||
Test Files 1 failed | 1 passed (2)
|
||||
Tests 2 failed | 1 passed | 37 skipped (40)
|
||||
Start at 09:06:07
|
||||
Duration 1.42s (transform 314ms, setup 63ms, import 1.30s, tests 38ms, environment 0ms)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 build
|
||||
> tsc -p tsconfig.build.json
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 36 files in 91ms. No fixes applied.
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
RUN v4.1.9 /home/ubuntu/.pi/agent/worktrees/pi-goals-simple-visible-supervision
|
||||
|
||||
|
||||
Test Files 13 passed (13)
|
||||
Tests 94 passed (94)
|
||||
Start at 09:07:25
|
||||
Duration 21.60s (transform 1.68s, setup 395ms, import 20.36s, tests 34.15s, environment 2ms)
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test:supervisor
|
||||
> node --import tsx --test test/internal-supervisor/*.test.ts
|
||||
|
||||
✔ retries intercom registration when pi-intercom loads after pi-supervise (3.974028ms)
|
||||
✔ a directive with no text is rejected, so the worker never sees undefined (0.293578ms)
|
||||
✔ a directive from the paired supervisor becomes a real user message (29.040859ms)
|
||||
✔ a directive to a busy worker interrupts, instead of waiting for the whole task (11.791265ms)
|
||||
✔ a directive from an unpaired session is dropped (19.421131ms)
|
||||
✔ a second pair takes over, and the first supervisor is told it lost the worker (20.715922ms)
|
||||
✔ only the paired worker can end a run (6.353483ms)
|
||||
✔ the programmatic pairing API waits for the worker acknowledgement (1.387398ms)
|
||||
✔ the worker acknowledges a pair, so the supervisor knows it was heard (5.41818ms)
|
||||
✔ a goal the supervisor inferred reaches the worker, which owns the view header (132.824229ms)
|
||||
✔ the second view carries only what happened after the first (435.544291ms)
|
||||
✔ a message addressed to a different session is ignored (10.263168ms)
|
||||
✔ on settle the worker publishes a view built from the live branch (55.816252ms)
|
||||
✔ the view is built from the live branch, not from every entry in the session (55.656998ms)
|
||||
✔ an unpaired session publishes nothing on settle (0.544096ms)
|
||||
✔ supervision never stops itself: no round limit at all (5.166058ms)
|
||||
✔ goal, pairing and the steer count all survive a reload together (0.627375ms)
|
||||
✔ a view arriving during unrelated supervisor thinking waits for a fresh complete overview (12.193906ms)
|
||||
✔ the nudge repeats neither the instructions already sent nor the verdict rules (6.136686ms)
|
||||
✔ a multi-line goal returns to supervisor context every fifth review and after compaction (32.391613ms)
|
||||
✔ a one-line goal is not redundantly reinserted (27.14758ms)
|
||||
✔ a check in and a worker that stopped ask for different things (11.315795ms)
|
||||
✔ a loop still gets named after the supervisor compacts, from restored state (0.916959ms)
|
||||
✔ a session that does not answer the roll call is not offered as a worker (501.50719ms)
|
||||
✔ a child run stays out of the roll call, so it can never be picked (4.989808ms)
|
||||
✔ a session already paired stays out of the roll call, and a free one answers (15.007393ms)
|
||||
✔ /supervise look asks the worker for a fresh view, rather than the supervisor guessing (306.09392ms)
|
||||
✔ let_it_run says the turn is over, so it is not called four times running (0.665336ms)
|
||||
✔ a sign-off verdict is answered, not aborted, and a runaway is still cut (0.661296ms)
|
||||
✔ every verdict result names the way to end the turn, steer included (0.41022ms)
|
||||
✔ an old view is dropped from context once its verdict is in, and the verdict is kept (1.049552ms)
|
||||
✔ a worker session never has its context rewritten (0.312445ms)
|
||||
✔ a newly presented view starts a fresh look (5.462915ms)
|
||||
✔ a tool a worker cannot use never aborts its turn (0.423344ms)
|
||||
✔ a resume onto a session that is gone drops the pairing and says so (5.570522ms)
|
||||
✔ a resume onto a live worker keeps supervising, and takes the writers back off (6.022216ms)
|
||||
✔ state written before recentSteers existed still loads (0.180461ms)
|
||||
✔ done unpairs the worker, so it stops publishing views (428.081704ms)
|
||||
✔ with no goal the supervisor cannot steer, it must ask the human (0.830044ms)
|
||||
✔ set_goal binds an inferred goal, and steering then works (501.633016ms)
|
||||
✔ a goal given at pair time still allows steering (0.554833ms)
|
||||
✔ done is refused while the worker has an unanswered tool call (12.761991ms)
|
||||
✔ done is allowed once nothing is outstanding (6.532494ms)
|
||||
✔ steer refuses when the session is not supervising (0.399461ms)
|
||||
✔ a reworded repeat of an earlier instruction is sent, and named back to the supervisor (0.593568ms)
|
||||
✔ overlap scores rewording high and a different instruction low (0.128624ms)
|
||||
✔ the view of the old worker cannot be used to judge the new one (6.184772ms)
|
||||
✔ with one other session here, /supervise needs no target and the whole line is the goal (501.541112ms)
|
||||
✔ naming the worker still works, and the rest of the line is the goal (0.679012ms)
|
||||
✔ with two free sessions here, /supervise asks which one, and pairs with the choice (500.988416ms)
|
||||
✔ a goal that is a path is read from the file, so it is not pasted every run (2.052271ms)
|
||||
✔ a long goal is one short line above the picker, and reaches the worker whole (501.890317ms)
|
||||
✔ a session that stayed quiet is still on the list, because 0 free is a dead end (501.763389ms)
|
||||
✔ a cancelled picker pairs with nothing (501.758475ms)
|
||||
✔ supervising takes the writing tools away, and stopping gives them back (500.746419ms)
|
||||
✔ stopping gives back the writers without undoing another extension's tools (501.771845ms)
|
||||
✔ a first word that names no session is refused, rather than folded into the goal (0.654949ms)
|
||||
✔ a goal with spaces needs no target, and @name takes the rest of the line as the goal (502.193535ms)
|
||||
✔ the brief starts no turn, so there is no answer before the first view (6.184913ms)
|
||||
✔ /supervise goal changes the goal without breaking the pairing (0.754771ms)
|
||||
✔ the footer says which side of a pairing this session is, and clears when it ends (508.144262ms)
|
||||
✔ a session that is not supervising never sees the supervisor tools (5.351772ms)
|
||||
✔ worker_view refuses when there is no worker, rather than implying a pairing (1.56818ms)
|
||||
✔ the view names the worker's model and how full its context is (55.815989ms)
|
||||
✔ supervising a second session is refused while the first is still paired (0.644348ms)
|
||||
✔ the supervisor gets a look at a working worker every half hour, without being asked (973.448527ms)
|
||||
✔ a human message in the worker session is not a reason to stand back (6.268048ms)
|
||||
✔ letting a stopped worker run says plainly that the worker stays stopped (10.365848ms)
|
||||
✔ a stopped worker is looked at again, so let_it_run cannot silence the pairing (925.887224ms)
|
||||
✔ a worker that pairs at the prompt and never takes a turn is still watched (605.51902ms)
|
||||
✔ a worker that reloads at the prompt starts watching itself again (604.743394ms)
|
||||
✔ a timer look at a worker that has not moved is not sent, until it has been skipped three times (2429.468038ms)
|
||||
✔ the worker counts reviews in a row where nothing changed (577.985914ms)
|
||||
✔ an unacknowledged pair gives up, and a takeover cancels that timer (5.178547ms)
|
||||
✔ duplicate standalone Intercom registries are diagnosed and cannot bootstrap a plan (0.832525ms)
|
||||
✔ retained non-plan supervision reconnects after the worker reloads, not on unrelated peer traffic (344.975935ms)
|
||||
✔ retained non-plan supervision reconnects after the supervisor reloads, not on unrelated peer traffic (627.895644ms)
|
||||
✔ busy supervisor retains its active view and requests one complete overview after settling (31.331632ms)
|
||||
✔ ordinary prose leaves future reviews live: On course; the saved check is the next useful evidence. (31.299092ms)
|
||||
✔ ordinary prose leaves future reviews live: Which output format do you want? (7.054922ms)
|
||||
✔ manual last-goal ticks cannot end plan supervision before CompleteGoal (3.367731ms)
|
||||
✔ completion counts are plan-bound and distinguish inconclusive sign-off when ending supervision (4.579827ms)
|
||||
✔ each checkpoint freezes fresh worker evidence and direction without replacing a busy assessment (9.345858ms)
|
||||
✔ checkpoint snapshot building cannot publish after abort (2.874272ms)
|
||||
✔ checkpoint snapshot building cannot publish after stop (21.699085ms)
|
||||
✔ checkpoint snapshot building cannot publish after reload (2.94349ms)
|
||||
✔ checkpoint snapshot building cannot publish after plan change (3.193298ms)
|
||||
✔ checkpoint capture includes user direction arriving while tracked work is queried (2.828249ms)
|
||||
✔ checkpoint payload fits the serialized channel limit without truncating its identity (77.119836ms)
|
||||
✔ settled empty final response fails the checkpoint explicitly ([]) (5.826315ms)
|
||||
✔ settled empty final response fails the checkpoint explicitly (["text"]) (2.399616ms)
|
||||
✔ settled empty final response fails the checkpoint explicitly (["text"]) (1.983875ms)
|
||||
✔ settled empty final response fails the checkpoint explicitly (["thinking"]) (1.55404ms)
|
||||
✔ failed assessment resumes on later worker progress without a human poke (stop) (1.767669ms)
|
||||
✔ failed assessment resumes on later worker progress without a human poke (error) (1.397948ms)
|
||||
✔ empty low-level response may continue through compaction, ask a human, or finish with a tool verdict (2.553043ms)
|
||||
✔ a successful goal tool verdict is not undone by an empty final response (1.959099ms)
|
||||
✔ a duplicate checkpoint rejection never echoes its snapshot or changes the active view (2.140537ms)
|
||||
✔ an empty routine assessment cannot fail a separately queued checkpoint (2.391144ms)
|
||||
✔ plan bootstrap compacts only the supervisor and pairing alone never starts a worker or a review (1.738156ms)
|
||||
✔ goal decisions are correlated, preserve the pair across two goals, and cannot call overall done (2.61962ms)
|
||||
✔ abort and stop cancel pending requests; late decisions cannot approve a replacement (1.943861ms)
|
||||
✔ 50 actual model turns trigger one view, independent of the number of messages (2.077155ms)
|
||||
✔ unknown background providers are not proof of quiescence (0.224206ms)
|
||||
✔ stale plan content invalidates a pending goal review (2.190617ms)
|
||||
✔ small forks skip compaction, but real compaction failure prevents pairing (1.770236ms)
|
||||
✔ native small-history result permits startup after an attempted compaction (50000) (1.149776ms)
|
||||
✔ native small-history result permits startup after an attempted compaction (null) (0.989274ms)
|
||||
✔ the hour timer and a coincident turn checkpoint produce a single view (4.810231ms)
|
||||
✔ settled checks wait for tracked processes and subagents to finish (2.321824ms)
|
||||
✔ bootstrap stop cannot resurrect a supervisor after compaction completes (1.887956ms)
|
||||
✔ a restarted worker reconnects by exact saved session identity without a new supervisor (2.651845ms)
|
||||
✔ unknown initial context must compact instead of taking the known-small shortcut (0.884878ms)
|
||||
✔ null post-compaction usage cannot raise the next configured 100k checkpoint (1.897635ms)
|
||||
✔ stopping a routine view during compaction invalidates its suspended continuation (1.841943ms)
|
||||
✔ restart of a provisional bootstrap resumes compaction and pairing in the same saved session (2.414359ms)
|
||||
✔ command preserves a stopped supervisor across reload (1.739858ms)
|
||||
✔ done preserves a stopped supervisor across reload (2.194354ms)
|
||||
✔ same-binding replay retains activation when the supervisor lost its acknowledgement (2.993195ms)
|
||||
✔ model-unavailable stop validates binding, cancels pending reviews and ignores old directives (2.274799ms)
|
||||
✔ supervisor mode is a native inspection allowlist at visibility and execution, including reload and stopped forks (2.183069ms)
|
||||
✔ a cancelled checkpoint's delayed verdict cannot approve the replacement checkpoint (2.297678ms)
|
||||
✔ each model call reanchors the canonical plan and role without losing compacted planning context or judgments (1.91378ms)
|
||||
✔ routine assessments and steering display the actual advice rather than only a receipt (1.950418ms)
|
||||
✔ current context above 100k compacts; cumulative usage and exactly 100k do not (1.806256ms)
|
||||
✔ absent optional trackers count as zero tracked work; installed failed or busy trackers remain non-quiet (0.346121ms)
|
||||
✔ a settled worker with no optional trackers sends one review, not repeated idle wakes (2.802864ms)
|
||||
✔ disconnect cancels a checkpoint and blocks steering; local stop still clears ownership (2.023057ms)
|
||||
✔ registered malformed subagent tracker stays unknown even when the process tracker is absent (0.352469ms)
|
||||
✔ a busy supervisor defers the 100k compaction until its own run settles (1.919556ms)
|
||||
✔ a healthy goal review can take longer than ten minutes without cancellation or another pairing (2.175237ms)
|
||||
✔ busy plan supervisor refreshes cumulative VCC evidence without an idle feedback loop (4.336383ms)
|
||||
✔ progress during a refresh remains pending with only one look in flight (8.493816ms)
|
||||
✔ complete refresh uses the current worker branch after compaction (4.751519ms)
|
||||
✔ complete refresh uses the current worker branch after rewind (3.59423ms)
|
||||
✔ complete refresh uses the current worker branch after reload (3.022736ms)
|
||||
✔ explicit checkpoints wait separately from routine coalescing and do not replace an active assessment (3.084625ms)
|
||||
✔ actual supervisor provider failure is returned explicitly without another pair or an elapsed-time cancellation (1.926238ms)
|
||||
✔ a provider error followed by native retry success does not cancel the supervisor checkpoint (1.872509ms)
|
||||
✔ a failed full overview waits without spinning and recovers on routine progress (4.173687ms)
|
||||
✔ a failed full overview waits without spinning and recovers on explicit look (2.924851ms)
|
||||
✔ overview display metadata uses native context without requiring a remote roster lookup (1.976883ms)
|
||||
✔ a superseded advance re-drives the current checkpoint after deferred compaction success (3.037736ms)
|
||||
✔ a superseded advance re-drives the current checkpoint after deferred compaction rejection (2.425301ms)
|
||||
✔ stopping current work during an obsolete advance prevents re-drive after compaction success (2.481801ms)
|
||||
✔ stopping current work during an obsolete advance prevents re-drive after compaction rejection (2.130017ms)
|
||||
✔ goal_review accepts an optional bounded snapshot but rejects malformed snapshots (1.89367ms)
|
||||
✔ worker completion metadata is optional but cannot claim malformed counts (0.451557ms)
|
||||
✔ checkpoint snapshot bounding counts JSON escapes and does not split Unicode characters (5.01807ms)
|
||||
✔ a child process named pi is found by ps, and stops being found when it exits (413.232236ms)
|
||||
✔ the check is a snapshot, so it cannot hold up the worker's settle (324.687926ms)
|
||||
✔ latest user direction survives bounded summaries, compaction and later supervisor echoes (114.635148ms)
|
||||
✔ oversized user direction is visibly bounded with a source reference (2.69704ms)
|
||||
✔ a one-line goal stays whole while a multi-line goal has a locator (1.094539ms)
|
||||
✔ a view carries only the turns the supervisor has not been sent (1.084425ms)
|
||||
✔ the last two reasoning blocks stay in the narrative, and older ones drop out (1.002457ms)
|
||||
✔ a compaction restarts the view, so no turn falls into the gap (0.457013ms)
|
||||
✔ pi-vcc reports the files the worker wrote, and separates them from the ones it read (1.503099ms)
|
||||
✔ progressKey is unchanged when a review produced no new file or commit (0.732168ms)
|
||||
✔ progressKey still sees a new file past pi-vcc's ten path display cap (1.020185ms)
|
||||
✔ a commit counts as progress, even when no file was written since (8.584645ms)
|
||||
✔ outstandingWork finds tool calls that never got a result (3.568082ms)
|
||||
✔ buildView reports a tool call with no result, so done can be refused (2.667216ms)
|
||||
✔ the view says how many reviews in a row changed nothing, and says nothing at zero (1.134394ms)
|
||||
✔ the view merges the worker's compaction summary with the turns after it (0.422717ms)
|
||||
✔ a turn the compaction summary already covers is not sent twice (1.946043ms)
|
||||
✔ pi-vcc's sections and its transcript land on the right sides of the split (2.32661ms)
|
||||
✔ the view does not tell the supervisor to use vcc_recall, a tool it does not have (0.296905ms)
|
||||
✔ supervisor directives are not sent back as worker evidence (0.792699ms)
|
||||
✔ bookkeeping tool calls are kept out of the transcript (0.44677ms)
|
||||
✔ buildView reports the goal, status, and files without historical failures (0.427038ms)
|
||||
✔ how long the worker has been quiet, measured from its own last entry (0.436767ms)
|
||||
✔ buildView keeps the newest turns when it has to cut for the channel limit (45.163847ms)
|
||||
✔ pi's own branch logic drops the abandoned fork, on a session file (2196.625029ms)
|
||||
✔ a long goal cannot push the view past the broker limit (0.457776ms)
|
||||
✔ a bounded complete overview explicitly labels a truncated worker compaction summary (0.265551ms)
|
||||
ℹ tests 176
|
||||
ℹ suites 0
|
||||
ℹ pass 176
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 21219.975303
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc -p tsconfig.build.json --noEmit
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 build
|
||||
> tsc -p tsconfig.build.json
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
wdl, woodside, workflow-diagram-tracker, ws-roadmap-slide, wsl-proxy
|
||||
|
||||
[Prompts]
|
||||
/council, /gather-context-and-clarify, /parallel-cleanup, /parallel-research, /parallel-review, /review-loop
|
||||
|
||||
[Extensions]
|
||||
@aliou/pi-processes:processes, @aliou/pi-processes:processes-dock, @aliou/pi-processes:processes-logs, @ff-labs/pi-fff:src, herdr-agent-state.ts, pi-annotated-journal, pi-better-compaction, pi-sandbox, pi-schedule-prompt:src, pi-subagents,
|
||||
pi-zed-shift-enter:extension.ts, pi-zentui:zentui, src, wassname/pi-copilot-web
|
||||
|
||||
[Themes]
|
||||
adventure, adwaita-dark, arcoiris, arthur, atom, aura, black-metal-bathory, black-metal-burzum, black-metal-khold, box, brogrammer, carbonfox, catppuccin-mocha, citruszest, cursor-dark, cutie-pro, dark-modern, dark-pastel, dimmed-monokai, doom-peacock, dracula-plus,
|
||||
earthsong, everforest-dark-hard, fahrenheit, flatland, flexoki-dark, front-end-delight, fun-forrest, galizur, github-dark-colorblind, github-dark-high-contrast, glacier, gruber-darker, gruvbox-dark, gruvbox-dark-hard, gruvbox-material, guezwhoz, hacktober, hardcore,
|
||||
havn-skumring, ic-orange-ppl, iterm2-smoooooth, iterm2-tango-dark, japanesque, jellybeans, kanagawa-wave, kurokula, later-this-evening, lovelace, material-darker, matte-black, mellow, miasma, nvim-dark, popping-and-locking, sea-shells, sleepy-hollow, smyck, tomorrow-night,
|
||||
tomorrow-night-bright, tomorrow-night-burns, twilight, vague, vesper, xcode-dark-hc
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Operator-created recovery command fixture. The plan is a draft, not approved. Do not implement it.
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Operator fixture marker, not a model response.
|
||||
|
||||
Warning: ⚠️ Network sandbox allows all domains because network.allowedDomains contains "*". Only use this intentionally; remove "*" to restore per-domain prompts.
|
||||
|
||||
Goals mode exited. Files retained; no work approved. Supervisor sessions remain inspection-only.
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│
|
||||
│
|
||||
│
|
||||
│ gpt-6-astra Github Copilot minimal
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
trial-nAjUyF 🔒 Sandbox: all domains, 4 write paths | [░░░░░░░░░░] 0.0%/400k (auto) | $0.000 (sub)
|
||||
@@ -0,0 +1,70 @@
|
||||
│
|
||||
[Extensions] │
|
||||
@aliou/pi-processes:processes, @aliou/pi-processes:processes-dock, @aliou/pi-processes:processes-logs, @ff-labs/pi-fff:src, herdr-agent-state.ts, pi-annotated-journal, pi-better-compaction, pi-sandbox, pi-schedule-prompt:src, pi-subagents, │
|
||||
pi-zed-shift-enter:extension.ts, pi-zentui:zentui, src, wassname/pi-copilot-web │
|
||||
│
|
||||
[Themes] │
|
||||
adventure, adwaita-dark, arcoiris, arthur, atom, aura, black-metal-bathory, black-metal-burzum, black-metal-khold, box, brogrammer, carbonfox, catppuccin-mocha, citruszest, cursor-dark, cutie-pro, dark-modern, dark-pastel, dimmed-monokai, doom-peacock, dracula-plus, │
|
||||
earthsong, everforest-dark-hard, fahrenheit, flatland, flexoki-dark, front-end-delight, fun-forrest, galizur, github-dark-colorblind, github-dark-high-contrast, glacier, gruber-darker, gruvbox-dark, gruvbox-dark-hard, gruvbox-material, guezwhoz, hacktober, hardcore, │
|
||||
havn-skumring, ic-orange-ppl, iterm2-smoooooth, iterm2-tango-dark, japanesque, jellybeans, kanagawa-wave, kurokula, later-this-evening, lovelace, material-darker, matte-black, mellow, miasma, nvim-dark, popping-and-locking, sea-shells, sleepy-hollow, smyck, tomorrow-night│
|
||||
tomorrow-night-bright, tomorrow-night-burns, twilight, vague, vesper, xcode-dark-hc │
|
||||
│
|
||||
│
|
||||
Warning: ⚠️ Network sandbox allows all domains because network.allowedDomains contains "*". Only use this intentionally; remove "*" to restore per-domain prompts. │
|
||||
│
|
||||
pi-better-compaction loaded • debug artifacts → /home/ubuntu/.pi/agent/artifacts/pi-better-compaction/sessions/01a084a5-a0f4-74c9-99fc-f9e5ae61adec/lifecycle/2026-09-09T05-35-21-169Z-lifecycle.json │
|
||||
│
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
|
||||
│
|
||||
Operator-created recovery command fixture. The plan is a draft, not approved. Do not implement it. │
|
||||
│
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
|
||||
┃
|
||||
Operator fixture marker, not a model response. ┃
|
||||
┃
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┃
|
||||
Package Updates Available ┃
|
||||
Package updates are available. Run pi update --extensions ┃
|
||||
Packages: ┃
|
||||
- pi-sandbox ┃
|
||||
- pi-zentui ┃
|
||||
- github.com/wassname/pi-annotated-journal ┃
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┃
|
||||
┃
|
||||
Goals: ordinary chat (goals exited). ┃
|
||||
Pair: disconnected/unconfirmed; inactive. ↓ Jump to latest message · End │
|
||||
|
||||
Warning: ⚠️ Network sandbox allows all domains because network.allowedDomains contains "*". Only use this intentionally; remove "*" to restore per-domain prompts.
|
||||
|
||||
pi-better-compaction loaded • debug artifacts → /home/ubuntu/.pi/agent/artifacts/pi-better-compaction/sessions/01a084a5-a0f4-74c9-99fc-f9e5ae61adec/lifecycle/2026-09-09T05-35-21-169Z-lifecycle.json
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Operator-created recovery command fixture. The plan is a draft, not approved. Do not implement it.
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Operator fixture marker, not a model response.
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Package Updates Available
|
||||
Package updates are available. Run pi update --extensions
|
||||
Packages:
|
||||
- pi-sandbox
|
||||
- pi-zentui
|
||||
- github.com/wassname/pi-annotated-journal
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Goals: ordinary chat (goals exited).
|
||||
Pair: disconnected/unconfirmed; inactive.
|
||||
Worker: not recorded · no paired session
|
||||
Supervisor: not recorded · no paired session
|
||||
Last failure: none recorded in this runtime
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│
|
||||
│
|
||||
│
|
||||
│ gpt-6-astra Github Copilot minimal
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
trial-nAjUyF 🔒 Sandbox: all domains, 4 write paths | [░░░░░░░░░░] 0.0%/400k (auto) | $0.000 (sub)
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 lint
|
||||
> biome check src/ test/
|
||||
|
||||
Checked 36 files in 97ms. No fixes applied.
|
||||
@@ -0,0 +1,204 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test
|
||||
> vitest run && npm run test:supervisor
|
||||
|
||||
|
||||
RUN v4.1.9 /home/ubuntu/.pi/agent/worktrees/pi-goals-simple-visible-supervision
|
||||
|
||||
|
||||
Test Files 14 passed (14)
|
||||
Tests 101 passed (101)
|
||||
Start at 13:38:22
|
||||
Duration 23.27s (transform 2.37s, setup 395ms, import 21.82s, tests 50.33s, environment 5ms)
|
||||
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 test:supervisor
|
||||
> node --import tsx --test test/internal-supervisor/*.test.ts
|
||||
|
||||
✔ retries intercom registration when pi-intercom loads after pi-supervise (3.513793ms)
|
||||
✔ a directive with no text is rejected, so the worker never sees undefined (0.472691ms)
|
||||
✔ a directive from the paired supervisor becomes a real user message (27.644033ms)
|
||||
✔ a directive to a busy worker interrupts, instead of waiting for the whole task (13.94769ms)
|
||||
✔ a directive from an unpaired session is dropped (15.908473ms)
|
||||
✔ a second pair takes over, and the first supervisor is told it lost the worker (21.082703ms)
|
||||
✔ only the paired worker can end a run (7.780322ms)
|
||||
✔ the programmatic pairing API waits for the worker acknowledgement (1.376736ms)
|
||||
✔ the worker acknowledges a pair, so the supervisor knows it was heard (6.283045ms)
|
||||
✔ a goal the supervisor inferred reaches the worker, which owns the view header (131.134854ms)
|
||||
✔ the second view carries only what happened after the first (436.748028ms)
|
||||
✔ a message addressed to a different session is ignored (10.19648ms)
|
||||
✔ on settle the worker publishes a view built from the live branch (54.951438ms)
|
||||
✔ the view is built from the live branch, not from every entry in the session (54.932155ms)
|
||||
✔ an unpaired session publishes nothing on settle (0.5274ms)
|
||||
✔ supervision never stops itself: no round limit at all (5.101803ms)
|
||||
✔ goal, pairing and the steer count all survive a reload together (0.596129ms)
|
||||
✔ a view arriving during unrelated supervisor thinking waits for a fresh complete overview (11.19975ms)
|
||||
✔ the nudge repeats neither the instructions already sent nor the verdict rules (6.045907ms)
|
||||
✔ a multi-line goal returns to supervisor context every fifth review and after compaction (32.277779ms)
|
||||
✔ a one-line goal is not redundantly reinserted (26.746878ms)
|
||||
✔ a check in and a worker that stopped ask for different things (10.249474ms)
|
||||
✔ a loop still gets named after the supervisor compacts, from restored state (1.006545ms)
|
||||
✔ a session that does not answer the roll call is not offered as a worker (502.069337ms)
|
||||
✔ a child run stays out of the roll call, so it can never be picked (5.882165ms)
|
||||
✔ a session already paired stays out of the roll call, and a free one answers (15.515028ms)
|
||||
✔ /supervise look asks the worker for a fresh view, rather than the supervisor guessing (306.997507ms)
|
||||
✔ let_it_run says the turn is over, so it is not called four times running (0.614118ms)
|
||||
✔ a sign-off verdict is answered, not aborted, and a runaway is still cut (0.766328ms)
|
||||
✔ every verdict result names the way to end the turn, steer included (0.601185ms)
|
||||
✔ an old view is dropped from context once its verdict is in, and the verdict is kept (1.039013ms)
|
||||
✔ a worker session never has its context rewritten (0.303001ms)
|
||||
✔ a newly presented view starts a fresh look (5.626017ms)
|
||||
✔ a tool a worker cannot use never aborts its turn (0.604727ms)
|
||||
✔ a resume onto a session that is gone drops the pairing and says so (5.917942ms)
|
||||
✔ a resume onto a live worker keeps supervising, and takes the writers back off (5.276493ms)
|
||||
✔ state written before recentSteers existed still loads (0.221018ms)
|
||||
✔ done unpairs the worker, so it stops publishing views (423.568859ms)
|
||||
✔ with no goal the supervisor cannot steer, it must ask the human (0.742368ms)
|
||||
✔ set_goal binds an inferred goal, and steering then works (501.288202ms)
|
||||
✔ a goal given at pair time still allows steering (0.55747ms)
|
||||
✔ done is refused while the worker has an unanswered tool call (12.979101ms)
|
||||
✔ done is allowed once nothing is outstanding (5.279803ms)
|
||||
✔ steer refuses when the session is not supervising (0.367787ms)
|
||||
✔ a reworded repeat of an earlier instruction is sent, and named back to the supervisor (0.586311ms)
|
||||
✔ overlap scores rewording high and a different instruction low (0.138077ms)
|
||||
✔ the view of the old worker cannot be used to judge the new one (6.59538ms)
|
||||
✔ with one other session here, /supervise needs no target and the whole line is the goal (501.480943ms)
|
||||
✔ naming the worker still works, and the rest of the line is the goal (0.566102ms)
|
||||
✔ with two free sessions here, /supervise asks which one, and pairs with the choice (500.820463ms)
|
||||
✔ a goal that is a path is read from the file, so it is not pasted every run (2.17768ms)
|
||||
✔ a long goal is one short line above the picker, and reaches the worker whole (501.878571ms)
|
||||
✔ a session that stayed quiet is still on the list, because 0 free is a dead end (501.668999ms)
|
||||
✔ a cancelled picker pairs with nothing (500.656172ms)
|
||||
✔ supervising takes the writing tools away, and stopping gives them back (501.514864ms)
|
||||
✔ stopping gives back the writers without undoing another extension's tools (501.710321ms)
|
||||
✔ a first word that names no session is refused, rather than folded into the goal (0.552837ms)
|
||||
✔ a goal with spaces needs no target, and @name takes the rest of the line as the goal (501.716252ms)
|
||||
✔ the brief starts no turn, so there is no answer before the first view (6.049466ms)
|
||||
✔ /supervise goal changes the goal without breaking the pairing (0.644898ms)
|
||||
✔ the footer says which side of a pairing this session is, and clears when it ends (506.479387ms)
|
||||
✔ a session that is not supervising never sees the supervisor tools (6.510238ms)
|
||||
✔ worker_view refuses when there is no worker, rather than implying a pairing (0.773467ms)
|
||||
✔ the view names the worker's model and how full its context is (55.333572ms)
|
||||
✔ supervising a second session is refused while the first is still paired (0.619021ms)
|
||||
✔ the supervisor gets a look at a working worker every half hour, without being asked (974.700755ms)
|
||||
✔ a human message in the worker session is not a reason to stand back (6.119204ms)
|
||||
✔ letting a stopped worker run says plainly that the worker stays stopped (11.325288ms)
|
||||
✔ a stopped worker is looked at again, so let_it_run cannot silence the pairing (928.080952ms)
|
||||
✔ a worker that pairs at the prompt and never takes a turn is still watched (605.615767ms)
|
||||
✔ a worker that reloads at the prompt starts watching itself again (605.010128ms)
|
||||
✔ a timer look at a worker that has not moved is not sent, until it has been skipped three times (2434.182048ms)
|
||||
✔ the worker counts reviews in a row where nothing changed (580.138243ms)
|
||||
✔ an unacknowledged pair gives up, and a takeover cancels that timer (5.648124ms)
|
||||
✔ duplicate standalone Intercom registries are diagnosed and cannot bootstrap a plan (0.839488ms)
|
||||
✔ retained non-plan supervision reconnects after the worker reloads, not on unrelated peer traffic (345.059118ms)
|
||||
✔ retained non-plan supervision reconnects after the supervisor reloads, not on unrelated peer traffic (628.825545ms)
|
||||
✔ busy supervisor retains its active view and requests one complete overview after settling (31.386612ms)
|
||||
✔ human pause survives reload/reconnect; only explicit resume reactivates the same pair (33.112188ms)
|
||||
✔ stop cancels an in-flight checkpoint even when the cancel notification cannot be sent (6.210007ms)
|
||||
✔ supervisor pause cancels checkpoint and a disconnected local stop still persists (4.374153ms)
|
||||
✔ ordinary prose leaves future reviews live: On course; the saved check is the next useful evidence. (8.733354ms)
|
||||
✔ ordinary prose leaves future reviews live: Which output format do you want? (4.512364ms)
|
||||
✔ manual last-goal ticks cannot end plan supervision before CompleteGoal (2.39126ms)
|
||||
✔ completion counts are plan-bound and distinguish inconclusive sign-off when ending supervision (4.338931ms)
|
||||
✔ each checkpoint freezes fresh worker evidence and direction without replacing a busy assessment (6.420082ms)
|
||||
✔ checkpoint snapshot building cannot publish after abort (2.288189ms)
|
||||
✔ checkpoint snapshot building cannot publish after stop (8.410958ms)
|
||||
✔ checkpoint snapshot building cannot publish after reload (1.97679ms)
|
||||
✔ checkpoint snapshot building cannot publish after plan change (1.998333ms)
|
||||
✔ checkpoint capture includes user direction arriving while tracked work is queried (2.027935ms)
|
||||
✔ checkpoint payload fits the serialized channel limit without truncating its identity (76.173014ms)
|
||||
✔ settled empty final response fails the checkpoint explicitly ([]) (4.469246ms)
|
||||
✔ settled empty final response fails the checkpoint explicitly (["text"]) (1.484115ms)
|
||||
✔ settled empty final response fails the checkpoint explicitly (["text"]) (1.620401ms)
|
||||
✔ settled empty final response fails the checkpoint explicitly (["thinking"]) (1.455427ms)
|
||||
✔ failed assessment resumes on later worker progress without a human poke (stop) (2.139792ms)
|
||||
✔ failed assessment resumes on later worker progress without a human poke (error) (1.490922ms)
|
||||
✔ empty low-level response may continue through compaction, ask a human, or finish with a tool verdict (2.368309ms)
|
||||
✔ a successful goal tool verdict is not undone by an empty final response (2.000919ms)
|
||||
✔ a duplicate checkpoint rejection never echoes its snapshot or changes the active view (1.967399ms)
|
||||
✔ an empty routine assessment cannot fail a separately queued checkpoint (2.00475ms)
|
||||
✔ plan bootstrap compacts only the supervisor and pairing alone never starts a worker or a review (1.394665ms)
|
||||
✔ goal decisions are correlated, preserve the pair across two goals, and cannot call overall done (2.282643ms)
|
||||
✔ abort and stop cancel pending requests; late decisions cannot approve a replacement (1.660897ms)
|
||||
✔ 50 actual model turns trigger one view, independent of the number of messages (1.928283ms)
|
||||
✔ unknown background providers are not proof of quiescence (0.198076ms)
|
||||
✔ stale plan content invalidates a pending goal review (2.144203ms)
|
||||
✔ small forks skip compaction, but real compaction failure prevents pairing (1.584189ms)
|
||||
✔ native small-history result permits startup after an attempted compaction (50000) (0.988832ms)
|
||||
✔ native small-history result permits startup after an attempted compaction (null) (0.75011ms)
|
||||
✔ the hour timer and a coincident turn checkpoint produce a single view (4.330684ms)
|
||||
✔ settled checks wait for tracked processes and subagents to finish (2.062665ms)
|
||||
✔ bootstrap stop cannot resurrect a supervisor after compaction completes (1.529383ms)
|
||||
✔ a restarted worker reconnects by exact saved session identity without a new supervisor (2.505604ms)
|
||||
✔ unknown initial context must compact instead of taking the known-small shortcut (1.267376ms)
|
||||
✔ null post-compaction usage cannot raise the next configured 100k checkpoint (2.338111ms)
|
||||
✔ stopping a routine view during compaction invalidates its suspended continuation (1.707556ms)
|
||||
✔ restart of a provisional bootstrap resumes compaction and pairing in the same saved session (2.48264ms)
|
||||
✔ command preserves a stopped supervisor across reload (1.840487ms)
|
||||
✔ done preserves a stopped supervisor across reload (2.952022ms)
|
||||
✔ same-binding replay retains activation when the supervisor lost its acknowledgement (3.738066ms)
|
||||
✔ model-unavailable stop validates binding, cancels pending reviews and ignores old directives (2.963335ms)
|
||||
✔ supervisor mode is a native inspection allowlist at visibility and execution, including reload and stopped forks (3.2371ms)
|
||||
✔ a cancelled checkpoint's delayed verdict cannot approve the replacement checkpoint (3.477147ms)
|
||||
✔ each model call reanchors the canonical plan and role without losing compacted planning context or judgments (2.473904ms)
|
||||
✔ routine assessments and steering display the actual advice rather than only a receipt (2.783747ms)
|
||||
✔ current context above 100k compacts; cumulative usage and exactly 100k do not (2.379765ms)
|
||||
✔ absent optional trackers count as zero tracked work; installed failed or busy trackers remain non-quiet (0.421638ms)
|
||||
✔ a settled worker with no optional trackers sends one review, not repeated idle wakes (3.307215ms)
|
||||
✔ disconnect cancels a checkpoint and blocks steering; local stop still clears ownership (2.01323ms)
|
||||
✔ registered malformed subagent tracker stays unknown even when the process tracker is absent (0.311552ms)
|
||||
✔ a busy supervisor defers the 100k compaction until its own run settles (1.785982ms)
|
||||
✔ a healthy goal review can take longer than ten minutes without cancellation or another pairing (2.14447ms)
|
||||
✔ busy plan supervisor refreshes cumulative VCC evidence without an idle feedback loop (3.204869ms)
|
||||
✔ progress during a refresh remains pending with only one look in flight (3.93267ms)
|
||||
✔ complete refresh uses the current worker branch after compaction (2.418211ms)
|
||||
✔ complete refresh uses the current worker branch after rewind (2.364448ms)
|
||||
✔ complete refresh uses the current worker branch after reload (2.477204ms)
|
||||
✔ explicit checkpoints wait separately from routine coalescing and do not replace an active assessment (2.757015ms)
|
||||
✔ actual supervisor provider failure is returned explicitly without another pair or an elapsed-time cancellation (1.60961ms)
|
||||
✔ a provider error followed by native retry success does not cancel the supervisor checkpoint (1.625357ms)
|
||||
✔ a failed full overview waits without spinning and recovers on routine progress (3.834412ms)
|
||||
✔ a failed full overview waits without spinning and recovers on explicit look (2.610627ms)
|
||||
✔ overview display metadata uses native context without requiring a remote roster lookup (1.794804ms)
|
||||
✔ a superseded advance re-drives the current checkpoint after deferred compaction success (2.809267ms)
|
||||
✔ a superseded advance re-drives the current checkpoint after deferred compaction rejection (2.450308ms)
|
||||
✔ stopping current work during an obsolete advance prevents re-drive after compaction success (2.096725ms)
|
||||
✔ stopping current work during an obsolete advance prevents re-drive after compaction rejection (1.957782ms)
|
||||
✔ goal_review accepts an optional bounded snapshot but rejects malformed snapshots (1.551675ms)
|
||||
✔ worker completion metadata is optional but cannot claim malformed counts (0.459292ms)
|
||||
✔ checkpoint snapshot bounding counts JSON escapes and does not split Unicode characters (5.01943ms)
|
||||
✔ a child process named pi is found by ps, and stops being found when it exits (361.27963ms)
|
||||
✔ the check is a snapshot, so it cannot hold up the worker's settle (327.919292ms)
|
||||
✔ latest user direction survives bounded summaries, compaction and later supervisor echoes (82.002344ms)
|
||||
✔ oversized user direction is visibly bounded with a source reference (2.652949ms)
|
||||
✔ a one-line goal stays whole while a multi-line goal has a locator (1.298412ms)
|
||||
✔ a view carries only the turns the supervisor has not been sent (1.16182ms)
|
||||
✔ the last two reasoning blocks stay in the narrative, and older ones drop out (1.009051ms)
|
||||
✔ a compaction restarts the view, so no turn falls into the gap (0.491271ms)
|
||||
✔ pi-vcc reports the files the worker wrote, and separates them from the ones it read (1.551769ms)
|
||||
✔ progressKey is unchanged when a review produced no new file or commit (0.723151ms)
|
||||
✔ progressKey still sees a new file past pi-vcc's ten path display cap (0.853796ms)
|
||||
✔ a commit counts as progress, even when no file was written since (1.124031ms)
|
||||
✔ outstandingWork finds tool calls that never got a result (2.699571ms)
|
||||
✔ buildView reports a tool call with no result, so done can be refused (1.30884ms)
|
||||
✔ the view says how many reviews in a row changed nothing, and says nothing at zero (0.553271ms)
|
||||
✔ the view merges the worker's compaction summary with the turns after it (0.439182ms)
|
||||
✔ a turn the compaction summary already covers is not sent twice (0.975019ms)
|
||||
✔ pi-vcc's sections and its transcript land on the right sides of the split (1.84901ms)
|
||||
✔ the view does not tell the supervisor to use vcc_recall, a tool it does not have (0.311653ms)
|
||||
✔ supervisor directives are not sent back as worker evidence (0.566437ms)
|
||||
✔ bookkeeping tool calls are kept out of the transcript (0.395583ms)
|
||||
✔ buildView reports the goal, status, and files without historical failures (0.43785ms)
|
||||
✔ how long the worker has been quiet, measured from its own last entry (0.458098ms)
|
||||
✔ buildView keeps the newest turns when it has to cut for the channel limit (39.471288ms)
|
||||
✔ pi's own branch logic drops the abandoned fork, on a session file (1939.636064ms)
|
||||
✔ a long goal cannot push the view past the broker limit (0.551003ms)
|
||||
✔ a bounded complete overview explicitly labels a truncated worker compaction summary (0.286204ms)
|
||||
ℹ tests 179
|
||||
ℹ suites 0
|
||||
ℹ pass 179
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 20775.476362
|
||||
@@ -0,0 +1,34 @@
|
||||
[Prompts]
|
||||
/council, /gather-context-and-clarify, /parallel-cleanup, /parallel-research, /parallel-review, /review-loop
|
||||
|
||||
[Extensions]
|
||||
@aliou/pi-processes:processes, @aliou/pi-processes:processes-dock, @aliou/pi-processes:processes-logs, @ff-labs/pi-fff:src, herdr-agent-state.ts, pi-annotated-journal, pi-better-compaction, pi-sandbox, pi-schedule-prompt:src, pi-subagents,
|
||||
pi-zed-shift-enter:extension.ts, pi-zentui:zentui, src, wassname/pi-copilot-web
|
||||
|
||||
[Themes]
|
||||
adventure, adwaita-dark, arcoiris, arthur, atom, aura, black-metal-bathory, black-metal-burzum, black-metal-khold, box, brogrammer, carbonfox, catppuccin-mocha, citruszest, cursor-dark, cutie-pro, dark-modern, dark-pastel, dimmed-monokai, doom-peacock, dracula-plus,
|
||||
earthsong, everforest-dark-hard, fahrenheit, flatland, flexoki-dark, front-end-delight, fun-forrest, galizur, github-dark-colorblind, github-dark-high-contrast, glacier, gruber-darker, gruvbox-dark, gruvbox-dark-hard, gruvbox-material, guezwhoz, hacktober, hardcore,
|
||||
havn-skumring, ic-orange-ppl, iterm2-smoooooth, iterm2-tango-dark, japanesque, jellybeans, kanagawa-wave, kurokula, later-this-evening, lovelace, material-darker, matte-black, mellow, miasma, nvim-dark, popping-and-locking, sea-shells, sleepy-hollow, smyck, tomorrow-night,
|
||||
tomorrow-night-bright, tomorrow-night-burns, twilight, vague, vesper, xcode-dark-hc
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Operator-created recovery command fixture. The plan is a draft, not approved. Do not implement it.
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Operator fixture marker, not a model response.
|
||||
|
||||
Warning: ⚠️ Network sandbox allows all domains because network.allowedDomains contains "*". Only use this intentionally; remove "*" to restore per-domain prompts.
|
||||
|
||||
Goals mode exited. Files retained; no work approved. Supervisor sessions remain inspection-only.
|
||||
|
||||
Warning: Recovery failed; no new work authorized: Error: No recoverable pair. Reconnect never creates a supervisor; inspect the recorded pane or select Ready for a new pairing.
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│
|
||||
│
|
||||
│
|
||||
│ gpt-6-astra Github Copilot minimal
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
trial-nAjUyF 🔒 Sandbox: all domains, 4 write paths | [░░░░░░░░░░] 0.0%/400k (auto) | $0.000 (sub)
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
[Prompts]
|
||||
/council, /gather-context-and-clarify, /parallel-cleanup, /parallel-research, /parallel-review, /review-loop
|
||||
|
||||
[Extensions]
|
||||
@aliou/pi-processes:processes, @aliou/pi-processes:processes-dock, @aliou/pi-processes:processes-logs, @ff-labs/pi-fff:src, herdr-agent-state.ts, pi-annotated-journal, pi-better-compaction, pi-sandbox, pi-schedule-prompt:src, pi-subagents,
|
||||
pi-zed-shift-enter:extension.ts, pi-zentui:zentui, src, wassname/pi-copilot-web
|
||||
|
||||
[Themes]
|
||||
adventure, adwaita-dark, arcoiris, arthur, atom, aura, black-metal-bathory, black-metal-burzum, black-metal-khold, box, brogrammer, carbonfox, catppuccin-mocha, citruszest, cursor-dark, cutie-pro, dark-modern, dark-pastel, dimmed-monokai, doom-peacock, dracula-plus,
|
||||
earthsong, everforest-dark-hard, fahrenheit, flatland, flexoki-dark, front-end-delight, fun-forrest, galizur, github-dark-colorblind, github-dark-high-contrast, glacier, gruber-darker, gruvbox-dark, gruvbox-dark-hard, gruvbox-material, guezwhoz, hacktober, hardcore,
|
||||
havn-skumring, ic-orange-ppl, iterm2-smoooooth, iterm2-tango-dark, japanesque, jellybeans, kanagawa-wave, kurokula, later-this-evening, lovelace, material-darker, matte-black, mellow, miasma, nvim-dark, popping-and-locking, sea-shells, sleepy-hollow, smyck, tomorrow-night,
|
||||
tomorrow-night-bright, tomorrow-night-burns, twilight, vague, vesper, xcode-dark-hc
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Operator-created recovery command fixture. The plan is a draft, not approved. Do not implement it.
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Operator fixture marker, not a model response.
|
||||
|
||||
Warning: ⚠️ Network sandbox allows all domains because network.allowedDomains contains "*". Only use this intentionally; remove "*" to restore per-domain prompts.
|
||||
|
||||
Reloaded keybindings, extensions, skills, prompts, themes, and context files
|
||||
|
||||
Goals stopped by user. Plan and evidence retained.
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│
|
||||
│
|
||||
│
|
||||
│ gpt-6-astra Github Copilot minimal
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
trial-nAjUyF goals stopped · /goals resume | 🔒 Sandbox: all domains, 4 write paths | [░░░░░░░░░░] 0.0%/400k (auto) | $0.000 (sub)
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
[Prompts]
|
||||
/council, /gather-context-and-clarify, /parallel-cleanup, /parallel-research, /parallel-review, /review-loop
|
||||
|
||||
[Extensions]
|
||||
@aliou/pi-processes:processes, @aliou/pi-processes:processes-dock, @aliou/pi-processes:processes-logs, @ff-labs/pi-fff:src, herdr-agent-state.ts, pi-annotated-journal, pi-better-compaction, pi-sandbox, pi-schedule-prompt:src, pi-subagents,
|
||||
pi-zed-shift-enter:extension.ts, pi-zentui:zentui, src, wassname/pi-copilot-web
|
||||
|
||||
[Themes]
|
||||
adventure, adwaita-dark, arcoiris, arthur, atom, aura, black-metal-bathory, black-metal-burzum, black-metal-khold, box, brogrammer, carbonfox, catppuccin-mocha, citruszest, cursor-dark, cutie-pro, dark-modern, dark-pastel, dimmed-monokai, doom-peacock, dracula-plus,
|
||||
earthsong, everforest-dark-hard, fahrenheit, flatland, flexoki-dark, front-end-delight, fun-forrest, galizur, github-dark-colorblind, github-dark-high-contrast, glacier, gruber-darker, gruvbox-dark, gruvbox-dark-hard, gruvbox-material, guezwhoz, hacktober, hardcore,
|
||||
havn-skumring, ic-orange-ppl, iterm2-smoooooth, iterm2-tango-dark, japanesque, jellybeans, kanagawa-wave, kurokula, later-this-evening, lovelace, material-darker, matte-black, mellow, miasma, nvim-dark, popping-and-locking, sea-shells, sleepy-hollow, smyck, tomorrow-night,
|
||||
tomorrow-night-bright, tomorrow-night-burns, twilight, vague, vesper, xcode-dark-hc
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Operator-created recovery command fixture. The plan is a draft, not approved. Do not implement it.
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Operator fixture marker, not a model response.
|
||||
|
||||
Warning: ⚠️ Network sandbox allows all domains because network.allowedDomains contains "*". Only use this intentionally; remove "*" to restore per-domain prompts.
|
||||
|
||||
Draft restored; no work started. Review the plan and request Ready before working.
|
||||
|
||||
pi-goals: drafting goals
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│
|
||||
│
|
||||
│
|
||||
│ gpt-6-astra Github Copilot minimal
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
trial-nAjUyF planning | 🔒 Sandbox: all domains, 4 write paths | [░░░░░░░░░░] 0.0%/400k (auto) | $0.000 (sub)
|
||||
@@ -0,0 +1,75 @@
|
||||
integrated-solutions-architecture, ipynb, jira-search, just, m365-search, marimo, markdown-tables, md-share, mermaid-js, ml-debug, ml-human-in-loop, moa, moa-brainstorm, moa-science, ocr, on-the-record, oracle, pi-intercom, pi-processes, pi-subagents, plan-format, ┃
|
||||
platform-alignment, project-brief, pseudopy, react-component-design, react-unit-testing, recommending-pi-extensions, review, roadmap, scout-provision, skill-authoring, skill-bootstrapper, skill-importer, snowflake-query, task-tracker, tufte-viz, uv, vargdown, varglite, ┃
|
||||
wdl, woodside, workflow-diagram-tracker, ws-roadmap-slide, wsl-proxy ┃
|
||||
[Prompts] ┃
|
||||
integrated-solutions-architecture, ipynb, jira-search, just, m365-search, marimo, markdown-tables, md-share, mermaid-js, ml-debug, ml-human-in-loop, moa, moa-brainstorm, moa-science, ocr, on-the-record, oracle, pi-intercom, pi-processes, pi-subagents, plan-format, │
|
||||
platform-alignment, project-brief, pseudopy, react-component-design, react-unit-testing, recommending-pi-extensions, review, roadmap, scout-provision, skill-authoring, skill-bootstrapper, skill-importer, snowflake-query, task-tracker, tufte-viz, uv, vargdown, varglite, │
|
||||
wdl, woodside, workflow-diagram-tracker, ws-roadmap-slide, wsl-proxy │
|
||||
[Prompts] │
|
||||
/council, /gather-context-and-clarify, /parallel-cleanup, /parallel-research, /parallel-review, /review-loop │
|
||||
│
|
||||
[Extensions] │
|
||||
@aliou/pi-processes:processes, @aliou/pi-processes:processes-dock, @aliou/pi-processes:processes-logs, @ff-labs/pi-fff:src, herdr-agent-state.ts, pi-annotated-journal, pi-better-compaction, pi-sandbox, pi-schedule-prompt:src, pi-subagents, │
|
||||
pi-zed-shift-enter:extension.ts, pi-zentui:zentui, src, wassname/pi-copilot-web │
|
||||
│
|
||||
[Themes] │
|
||||
adventure, adwaita-dark, arcoiris, arthur, atom, aura, black-metal-bathory, black-metal-burzum, black-metal-khold, box, brogrammer, carbonfox, catppuccin-mocha, citruszest, cursor-dark, cutie-pro, dark-modern, dark-pastel, dimmed-monokai, doom-peacock, dracula-plus, │
|
||||
earthsong, everforest-dark-hard, fahrenheit, flatland, flexoki-dark, front-end-delight, fun-forrest, galizur, github-dark-colorblind, github-dark-high-contrast, glacier, gruber-darker, gruvbox-dark, gruvbox-dark-hard, gruvbox-material, guezwhoz, hacktober, hardcore, │
|
||||
havn-skumring, ic-orange-ppl, iterm2-smoooooth, iterm2-tango-dark, japanesque, jellybeans, kanagawa-wave, kurokula, later-this-evening, lovelace, material-darker, matte-black, mellow, miasma, nvim-dark, popping-and-locking, sea-shells, sleepy-hollow, smyck, tomorrow-night│
|
||||
tomorrow-night-bright, tomorrow-night-burns, twilight, vague, vesper, xcode-dark-hc │
|
||||
│
|
||||
│
|
||||
Warning: ⚠️ Network sandbox allows all domains because network.allowedDomains contains "*". Only use this intentionally; remove "*" to restore per-domain prompts. │
|
||||
│
|
||||
pi-better-compaction loaded • debug artifacts → /home/ubuntu/.pi/agent/artifacts/pi-better-compaction/sessions/01a084a5-a0f4-74c9-99fc-f9e5ae61adec/lifecycle/2026-09-09T05-32-14-754Z-lifecycle.json │
|
||||
│
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
|
||||
┃
|
||||
Operator-created recovery command fixture. The plan is a draft, not approved. Do not implement it. ┃
|
||||
┃
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┃
|
||||
┃
|
||||
Operator fixture marker, not a model response. ┃
|
||||
┃
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┃
|
||||
Package Updates Available ┃
|
||||
Package updates are available. Run pi update --extensions ┃
|
||||
Packages: ┃
|
||||
- pi-sandbox ┃
|
||||
- pi-zentui ┃
|
||||
- github.com/wassname/pi-annotated-journal ↓ Jump to latest message · End │
|
||||
|
||||
tomorrow-night-bright, tomorrow-night-burns, twilight, vague, vesper, xcode-dark-hc
|
||||
|
||||
|
||||
Warning: ⚠️ Network sandbox allows all domains because network.allowedDomains contains "*". Only use this intentionally; remove "*" to restore per-domain prompts.
|
||||
|
||||
pi-better-compaction loaded • debug artifacts → /home/ubuntu/.pi/agent/artifacts/pi-better-compaction/sessions/01a084a5-a0f4-74c9-99fc-f9e5ae61adec/lifecycle/2026-09-09T05-32-14-754Z-lifecycle.json
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Operator-created recovery command fixture. The plan is a draft, not approved. Do not implement it.
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Operator fixture marker, not a model response.
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Package Updates Available
|
||||
Package updates are available. Run pi update --extensions
|
||||
Packages:
|
||||
- pi-sandbox
|
||||
- pi-zentui
|
||||
- github.com/wassname/pi-annotated-journal
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Goals stopped. Use /goals resume in the worker when ready. Detached processes are not killed.
|
||||
|
||||
Goals stopped by user. Plan and evidence retained.
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│
|
||||
│
|
||||
│
|
||||
│ gpt-6-astra Github Copilot minimal
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
trial-nAjUyF goals stopped · /goals resume | 🔒 Sandbox: all domains, 4 write paths | [░░░░░░░░░░] 0.0%/400k (auto) | $0.000 (sub)
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
> @wassname2/pi-goals@0.2.2 typecheck
|
||||
> tsc -p tsconfig.build.json --noEmit
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
PASS: stop persisted paused draft.
|
||||
PASS: resume restored planning without Ready authorization.
|
||||
PASS: exit persisted ordinary-chat state.
|
||||
PASS: no assistant turn beyond operator fixture marker.
|
||||
PASS: draft bytes unchanged.
|
||||
PASS: actual reload rendered; stopped status retained.
|
||||
PASS: reconnect without a pair reports failure instead of launching one.
|
||||
PASS: fresh-shell --session retained exit; status reports ordinary chat.
|
||||
@@ -0,0 +1,66 @@
|
||||
Let me get the diff to see what changed:
|
||||
|
||||
Now let me check the git history to see what changedNow I have full context. Let me verify the pi API behavior by checking the relevant source files to confirm the claims about `sendMessage`, `compact`, and event orderingNow I have all the context needed. Let me verify the `sendMessage` path more carefully:
|
||||
|
||||
I now have a complete picture. Here is the review.
|
||||
|
||||
---
|
||||
|
||||
## Code Review: ready menu — print plan + add "Ready + compact"
|
||||
|
||||
### Summary
|
||||
The `agent_end` handler now prints the plan working set into the transcript before the menu, and adds a 4th option ("Ready + compact") that compacts the planning chatter away before the work turn. The logic is correct and well-considered, with one structural race and one stale-widget gap.
|
||||
|
||||
### Important (should fix)
|
||||
|
||||
- **`src/index.ts:~248` (the `agent_end` handler) — race: plan mode is turned off before compaction completes.**
|
||||
`state = { ...state, isPlanMode: false }` executes before `ctx.compact()` is called (let alone finished). The compaction is fire-and-forget. If the human types and sends a message in the window between the state flip and the compaction callback firing, the agent starts a turn in normal mode *without* the work instruction. The work instruction arrives later (via `sendUserMessage` in `onComplete`/`onError`), triggering a second, redundant turn.
|
||||
The window is small (compaction is fast) but the behavior is undefined — the agent could start executing before the work instruction lands.
|
||||
**Fix**: flip `isPlanMode` inside the callbacks, not before. Move `state = { ...state, isPlanMode: false }; persist(); updateWidget(ctx);` into both `onComplete` and `onError`, and also into the non-compact branch (where it already is, effectively). The `work` string can be defined before the branch.
|
||||
|
||||
```typescript
|
||||
if (!choice.includes("compact")) {
|
||||
state = { ...state, isPlanMode: false };
|
||||
persist();
|
||||
updateWidget(ctx);
|
||||
pi.sendUserMessage(work, { deliverAs: "followUp" });
|
||||
return;
|
||||
}
|
||||
ctx.compact({
|
||||
customInstructions: `...`,
|
||||
onComplete: () => {
|
||||
state = { ...state, isPlanMode: false };
|
||||
persist();
|
||||
updateWidget(ctx);
|
||||
pi.sendUserMessage(work, { deliverAs: "followUp" });
|
||||
},
|
||||
onError: (e) => {
|
||||
ctx.ui.notify(`Compaction failed (${e.message}); starting work anyway.`, "warning");
|
||||
state = { ...state, isPlanMode: false };
|
||||
persist();
|
||||
updateWidget(ctx);
|
||||
pi.sendUserMessage(work, { deliverAs: "followUp" });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This also means the widget stays in "planning" mode during compaction, which is truthful — compaction hasn't finished yet.
|
||||
|
||||
### Suggestions
|
||||
|
||||
- **`src/index.ts:~248` — widget not refreshed after `$EDITOR`.**
|
||||
When the human chooses "Open in $EDITOR", `spawnSync` blocks, then `continue` re-enters the loop. The plan is re-read and potentially re-printed, but `updateWidget` is not called. If the human changed goal statuses (e.g. ticked a checkbox), the widget stays stale until the next `turn_end`.
|
||||
Add `updateWidget(ctx);` after the `spawnSync` line (or inside the `continue` branch before the continue).
|
||||
|
||||
- **`src/index.ts:~248` — `spawnSync` blocks the event loop.**
|
||||
`spawnSync(process.env.EDITOR || ...)` is a synchronous blocking call. While the editor is open, no async work (including compaction from a previous iteration, timers, etc.) can proceed. This is fine for a local TUI tool, but worth noting — if the editor hangs or the human walks away, the entire pi process is frozen.
|
||||
|
||||
### Positive
|
||||
|
||||
- **De-duplication is correct.** `printed` is a local variable, fresh per `agent_end` call, and correctly suppresses re-printing when the working set hasn't changed across editor passes. The `while` loop exit condition (`scanGoals(...).length > 0`) correctly handles the human deleting all goals in the editor.
|
||||
- **String matching is safe.** `choice?.startsWith("Ready")` gates both Ready options, then `choice.includes("compact")` distinguishes them. The word "compact" appears only in the "Ready + compact" string. No ambiguity.
|
||||
- **Both compaction callbacks queue the work turn.** `onComplete` and `onError` both call `pi.sendUserMessage(work, ...)`. A failed compaction does not strand the session — work starts anyway, with a notification.
|
||||
- **`session_compact` → `resyncReason` → injection chain is correct.** The pi source confirms `session_compact` fires (and is awaited) *before* `this.compact()` resolves and `onComplete` fires. So `resyncReason` is set before the next LLM call, and the full plan file is re-injected. The compaction summarizes away the exploration; the plan itself survives.
|
||||
|
||||
### Verdict
|
||||
**REQUEST CHANGES** — the race between `isPlanMode = false` and compaction completion is a real timing bug that can cause the agent to start a turn without the work instruction. The fix is straightforward: move the state flip into the callbacks.
|
||||
@@ -0,0 +1,22 @@
|
||||
You're right on both counts. Let me trace through each.
|
||||
|
||||
**Finding 1, withdrawn.** Keeping `isPlanMode` true during compaction would mean any message the human sends while compaction runs gets processed in plan mode: blocked tools (`tool_call` hook), a "You are still in plan mode" injection (`context` hook), and a second Ready menu on `agent_end`. The current order (flip first, then compact) means that same message is processed in work mode — the destination the session is heading to. The cost is at worst one redundant turn when the `onComplete` follow-up lands. Your order is the correct one.
|
||||
|
||||
**Finding 2, withdrawn.** `updateWidget` with `isPlanMode === true` hits the early return that sets two fixed strings already on screen. It is a pure no-op.
|
||||
|
||||
---
|
||||
|
||||
## Code Review: ready menu — print plan + add "Ready + compact"
|
||||
|
||||
### Summary
|
||||
The `agent_end` handler now prints the plan working set into the transcript before the menu, and adds a 4th option ("Ready + compact") that compacts the planning chatter away before the work turn. The logic is correct and well-considered.
|
||||
|
||||
### Positive
|
||||
- **De-duplication is correct.** `printed` is a local variable, fresh per `agent_end` call, and suppresses re-printing when the working set hasn't changed across `$EDITOR` passes. The `while` loop exits cleanly if the human deletes all goals in the editor.
|
||||
- **String matching is safe.** `choice?.startsWith("Ready")` gates both Ready options, then `choice.includes("compact")` distinguishes them. The word "compact" appears only in the one label.
|
||||
- **Both compaction callbacks queue the work turn.** `onComplete` and `onError` both call `pi.sendUserMessage(work, ...)`. A failed compaction does not strand the session.
|
||||
- **The `session_compact` → `resyncReason` → injection chain is correct.** The pi source confirms `session_compact` fires (and is awaited) before `this.compact()` resolves and `onComplete` fires. The full plan file is re-injected on the next LLM call; the compaction summarizes away the exploration, not the plan.
|
||||
- **The state-flip-before-compaction order is deliberate and correct.** Flipping `isPlanMode` first means any message the human sends during compaction is handled in work mode, not plan mode (where tools would be blocked and a contradictory injection would fire).
|
||||
|
||||
### Verdict
|
||||
**APPROVE** — no bugs found. The two concerns raised initially were incorrect; the code handles the interleaving correctly.
|
||||
@@ -0,0 +1,31 @@
|
||||
I only have a file-read tool here, so I’ll start from the repo files I can open and look for the plan-versioning and judge-related changes.Continuing through the judge path and tests.Reading the rest of the judge prompt and the tests that cover these behaviors.Looking for the test files.Trying common test filenames under `src` and `test`.Checking other likely files mentioned in the README.I couldn’t run `git diff` (no shell tool). From the files I could read, these are the concrete defects against the required behaviors:
|
||||
|
||||
**1. Uncommitted conflict markers in `README.md`**
|
||||
The working tree still has `<<<<<<< Updated upstream` / `=======` / `>>>>>>> Stashed changes` (and a truncated upstream judge sentence). That is not a reviewable change set.
|
||||
|
||||
**2. Judge “checked artifacts” gate is too brittle**
|
||||
`decideSignOff` only accepts:
|
||||
|
||||
```ts
|
||||
/^#{0,6}\s*(?:\*\*)?checks(?:\*\*)?:\s*\n(?:[-*]\s+.+\n?)+\s*VERDICT:/im
|
||||
```
|
||||
|
||||
The prompt asks for a `checks:` heading plus bullets, then **exactly two lines**:
|
||||
|
||||
```
|
||||
VERDICT: accept | reject
|
||||
missing: ...
|
||||
```
|
||||
|
||||
Any extra blank line, `Checks` with other wrapping, or a sentence between the last bullet and `VERDICT:` rejects a real accept. That’s stricter than “must show artifact quotes,” and can fail a judge that did the right thing.
|
||||
|
||||
**3. Grill-me vs Ready is prompt-only**
|
||||
`Grill me` sets `skipReadyMenu` for **one** `agent_end`, then the next turn with any goal lines shows Ready again. Nothing checks that the understanding question was asked **and answered**. If the model asks and offers Ready in the same turn, or the human never replies, Ready is still offered. Required: no Ready until that question has completed.
|
||||
|
||||
**4. Legacy unversioned plans are dropped with no migration path in code**
|
||||
Only `<session_id>-vN.md` is selected; `planVersion` restore is the only resume hook. That’s the intended break, but I found no test that old `.pi/plan/*.md` names stay unused and that a new `/goals` always writes a fresh `-vN` without rewriting prior files.
|
||||
|
||||
**5. Could not verify the focused tests or `git diff`**
|
||||
No test file opened (`src/index.test.ts`, `test/*.test.ts`, etc. all missing from this view). I could not confirm coverage for version reservation, visible plan print, grill-me sequencing, or reject-accept-without-checks.
|
||||
|
||||
I would **not APPROVE** this set as-is. Fix the README conflicts, tighten grill-me so Ready stays suppressed until after the human answers, and either loosen the checks regex to the documented “quotes present before VERDICT” rule or lock the exact judge format in a test.
|
||||
@@ -0,0 +1,41 @@
|
||||
I only have the `read` tool available, so let me locate the changed files by probing the repository structure.The extension source is clear. Let me verify the pi API usage (`sendMessage` with `customType`/`display`, `appendEntry`, `getSessionId`, `ctx.compact`) against the pi docs, since several required behaviors hinge on them.I can't run `git diff` or the test suite — this review environment only exposes a `read` tool, and the test directory can't be enumerated (every probe under `test/` returned ENOENT, and directories can't be listed). So I reviewed the changed files directly in the working tree (`src/index.ts`, `src/prompts.ts`, `README.md`, `scripts/check-judge-footprint.sh`) against the five required behaviors, and verified every pi API the behaviors depend on (`pi.sendMessage` with `customType`/`display`, `pi.appendEntry` restore via `session_start`, `pi.sendUserMessage` followUp, `ctx.compact` with onComplete/onError) against the installed pi docs. Findings below.
|
||||
|
||||
## Defects
|
||||
|
||||
**1. `/goals` arg parsing swallows ordinary objectives — src/index.ts, `/goals` handler**
|
||||
|
||||
```ts
|
||||
if (arg.startsWith("judge")) {
|
||||
```
|
||||
|
||||
Any objective whose first word is "judge" — e.g. `/goals judge the vendor options`, `/goals judge readiness of the export` — is treated as the judge-model subcommand: it sets `judgeModel` to "the vendor options" and **does not reserve a new `.pi/plan/<session_id>-vN.md`**, violating "every ordinary `/goals` invocation reserves a new file". Same class of issue for an objective that is exactly `clear` (deletes the plan instead of planning). Use exact-match subcommand parsing (`arg === "judge"` / `arg.startsWith("judge ")` at minimum, and even then an objective like "judge model quality" is unreachable — a `:` or `--` separator would be safer).
|
||||
|
||||
**2. The checked-artifact-list regex false-rejects compliant judges — `decideSignOff`, src/index.ts**
|
||||
|
||||
```ts
|
||||
const checks = /^checks:\s*\n(?:-\s+.+\n)+VERDICT:/im.test(judge.output);
|
||||
```
|
||||
|
||||
This requires the last `- ` bullet to be *immediately* followed by `VERDICT:` with no blank line, and requires the heading to be exactly `checks:` at line start. Nothing in `judgeSystem` tells the judge not to separate sections with a blank line (models habitually emit `…bullet\n\nVERDICT: accept`), and a judge writing `## checks:` or `**checks:**` also fails. Result: a valid accept *with* a real checked-artifact list is rejected, and the working agent gets a "Missing: checked-artifact list" reply it already satisfied — a retry loop against a nondeterministic judge. It's fail-closed (never accepts without the list, so the hard requirement holds), but as written it will produce systematic false rejects. Allow optional blank lines / formatting, e.g. `/^#*\s*checks:\s*\n(?:[-*]\s+.+\n)+\s*VERDICT:/im`.
|
||||
|
||||
**3. Judge transcript files collide within the same minute — CompleteGoal `execute`, src/index.ts**
|
||||
|
||||
```ts
|
||||
const rel = `.pi/judge/${stamp().replace(/[: ]/g, "-")}.md`;
|
||||
```
|
||||
|
||||
`stamp()` has minute resolution, so two sign-offs in the same minute (two goals signed off back-to-back — the common case) write the same `.pi/judge/<stamp>.md` path and the second silently overwrites the first's full transcript, contradicting "every run saves the judge's full transcript … referenced from the log line" (both log lines then cite one file containing only the second run). Add seconds or a short unique suffix.
|
||||
|
||||
## Minor / cosmetic
|
||||
|
||||
- Misindented closing `}` of the `if (!checks)` block in `decideSignOff` (extra indentation on the `}` line) — likely flagged by `biome check src/`, which `prepublishOnly` runs.
|
||||
- In plan mode, if the agent drafts a plan with no recognizable `goal:` checkbox line, the `agent_end` `while (scanGoals(...).length > 0)` loop never shows the Ready menu and plan mode can't be exited via the menu; the "no recognizable goal line" nudge in `dueInjection` only fires in the *non*-plan-mode branch. Not one of the five required behaviors, but a reachable dead end (escape hatch is `/goals clear`).
|
||||
|
||||
## Behaviors verified as correct
|
||||
|
||||
- **Versioning**: each ordinary `/goals` writes a fresh `<session_id>-vN.md` (empty reservation) via `nextPlanVersion` (max+1, so no clobbering); prior versions are never touched; legacy unversioned names are ignored by `nextPlanVersion` and never read — consistent with the intentional breaking change. `planVersion` is persisted via `appendEntry` and restored in `session_start`, so it survives resume/reload.
|
||||
- **Visible plan before Ready**: `pi.sendMessage({ customType: "plan", content: plan, display: true })` fires before `ctx.ui.select(...)`, and re-prints inside the `$EDITOR` loop when the text changed. `sendMessage`+`display: true` is the documented visible-transcript mechanism.
|
||||
- **Grill me**: sets `skipReadyMenu`, queues the single understanding-check question as a followUp user message, and consumes the skip at the next `agent_end` — so no Ready offer exists between asking the question and the human's reply being processed. `skipReadyMenu` is persisted, so it survives resume mid-grill.
|
||||
- **Judge**: `buildJudgeArgs` yields `pi -p --no-session --no-extensions --tools read,grep,find,ls --exclude-tools edit,write`; `judgeUser` embeds the complete plan file; `judgeSystem` demands per-artifact verbatim quotes framed as an inspectable record ("not hidden reasoning", "no bullet for a file you did not open"); an accept without the `checks:` list is rejected in `decideSignOff` (subject to defect 2); full transcript persisted per run (subject to defect 3).
|
||||
|
||||
Main caveat: I could not execute `git diff` or `npm test` here, so I can't confirm the exact diff boundaries or that the focused vitest cases (judge argv invariants, `appendLog`, `decideSignOff` fail-forward) pass — defects 1–3 are from source inspection, not reproduced runs. Not approved as-is; defects 1 and 2 are worth fixing before merge.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Verification: state-aligned planning mode
|
||||
|
||||
## Commands
|
||||
|
||||
```text
|
||||
$ npm test
|
||||
Test Files 8 passed (8)
|
||||
Tests 29 passed (29)
|
||||
|
||||
$ npm run typecheck
|
||||
> tsc --noEmit
|
||||
|
||||
$ npm run lint
|
||||
Checked 8 files in 17ms. No fixes applied.
|
||||
|
||||
$ git diff --check
|
||||
```
|
||||
|
||||
## Read
|
||||
|
||||
[test/goals-flow.test.ts](../../../test/goals-flow.test.ts) covers the visible plan before Refine, an editor prompt before a Refine revision turn, exact multiline Refine notes in `## Interview`, Ready as the only work handoff, Pi editor then Cancel, phase restoration, planning snapshot, writable plan path, allowed `pwd && ls && git log` and `cd . && ls -la`, blocked pipe, and blocked `CompleteGoal`.
|
||||
|
||||
[test/prompts.test.ts](../../../test/prompts.test.ts) locks the prompt instruction to inspect repository facts or search the web only when it can resolve a fact, ask a short self-contained batch of high-impact questions in the human's language with recommendations, and forbid placeholder goals.
|
||||
|
||||
[test/rpc-review.test.ts](../../../test/rpc-review.test.ts) starts the installed Pi RPC executable with [offline-model.ts](../../../test/fixtures/offline-model.ts), selects Refine through Pi's real dialog protocol, receives the editor request before the revision call, then submits notes and observes the revision call. The test uses a local HTTP model, so it spends no API credits.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Plan flow and judge review
|
||||
|
||||
- [x] goal: Each new `/goals` draft uses a fresh session-plan version
|
||||
- [x] Persist the selected `-vN` name so resume, reminders, Ready, and sign-off use one file.
|
||||
- [x] Keep earlier versioned files unchanged.
|
||||
- [x] Reserve `--clear` and `--judge` for commands so normal objectives are always new drafts.
|
||||
- failure mode: a second `/goals`, including an objective that begins with `judge`, changes the earlier plan or does not make a draft.
|
||||
- deliverable: [goals-flow.test.ts](../../../test/goals-flow.test.ts) shows an unchanged legacy file and `v1`, new `v2`, and `judge the vendor options` in new `v3`.
|
||||
- [x] goal: Plan review asks and displays the needed context
|
||||
- [x] Add `Grill me` to the Ready menu and queue an understanding-check interview turn.
|
||||
- [x] Keep one short goal subject with its full indented context block.
|
||||
- [x] Keep visible plan output before the Ready dialog.
|
||||
- failure mode: Grill me starts work or the plan is only hidden in an edit call.
|
||||
- deliverable: [goals-flow.test.ts](../../../test/goals-flow.test.ts) records display before dialog and the grill follow-up.
|
||||
- [x] goal: Judge review is visible without being confused with agent evidence
|
||||
- [x] Require concise observed checks before the verdict.
|
||||
- [x] Save the full judge reply under a unique path and link it from the plan log.
|
||||
- [x] Accept a headed check list with normal Markdown spacing, but reject an accept with no list.
|
||||
- failure mode: provider-private reasoning is claimed as evidence, the review is not inspectable, or a correct judge reply is rejected for blank-line formatting.
|
||||
- deliverable: [decide-signoff.test.ts](../../../test/decide-signoff.test.ts) locks the checked-artifact review contract, including a Markdown heading and blank line before the verdict.
|
||||
|
||||
## UAT / Verification
|
||||
|
||||
Observed 2026-08-24: `npm test` reported `Test Files 6 passed (6)` and `Tests 22 passed (22)`.
|
||||
`npm run typecheck`, `npm run lint`, and `git diff --check` exited 0. The focused flow test proves
|
||||
plan versioning, visible plan-before-dialog ordering, Grill me behavior, and objectives beginning
|
||||
with `judge`.
|
||||
|
||||
## Appendix (context, not approved)
|
||||
|
||||
Issue #1 has a 600 second judge timeout now. The judge stays a separate read-only `pi -p --no-session` subprocess. Intercom is unsuitable because it has no equivalent isolation boundary.
|
||||
|
||||
External review: [Kimi K3](../../reviews/pi-goals-kimi-k3.md) found the command-prefix, check-list formatting, and transcript-path defects; all were fixed. [Grok 4.6](../../reviews/pi-goals-grok-4-6-retry.md) confirmed the check-list concern. Its Grill me concern does not apply: `skipReadyMenu` suppresses the menu after the generated follow-up, and the next `agent_end` follows the human reply.
|
||||
@@ -0,0 +1,53 @@
|
||||
# State-aligned planning mode
|
||||
|
||||
Pi-goals will use pi-plan's small phase model. The UI, tool gate, and agent context will read the same persisted phase. Planning still keeps pi-goals' judgeable goals, direct user quotes, and interview record.
|
||||
|
||||
- [x] goal: Planning state survives restart and matches the UI and agent context
|
||||
- [ ] Replace `isPlanMode` and `skipReadyMenu` with persisted `phase: planning | working`.
|
||||
- [ ] Render the planning widget, inject the hidden planning-state snapshot, and restore state from that phase.
|
||||
- [ ] Restore the snapshot after restart or compaction without repeating the full drafting prompt every turn.
|
||||
- subtle failure mode: the UI says planning but a resumed or compacted agent sees work mode.
|
||||
- discriminator: a flow test restores planning and observes the planning snapshot; working has neither.
|
||||
- evidence: [goals-flow.test.ts](../../../test/goals-flow.test.ts) restores persisted planning state and observes `[PLANNING MODE]`; [verification](../audit/20260826_pi-plan-aligned-planning.md) records `25 passed`.
|
||||
- [x] goal: Planning blocks implementation while allowing fact finding
|
||||
- [ ] Allow writes only to the active plan file.
|
||||
- [ ] Block implementation tools, `CompleteGoal`, and bash write or pipe attempts with a planning-mode explanation.
|
||||
- [ ] Allow ordinary read-only inspection commands such as `pwd && ls && git log`.
|
||||
- subtle failure mode: an agent marks a goal active or changes project code before approval.
|
||||
- discriminator: flow tests reject each work route and allow the inspection command.
|
||||
- evidence: [goals-flow.test.ts](../../../test/goals-flow.test.ts) asserts allowed `pwd && ls && git log`, blocked pipe, non-plan write, and `CompleteGoal`; [verification](../audit/20260826_pi-plan-aligned-planning.md) records `25 passed`.
|
||||
- [x] goal: Planning interviews and revision notes are durable user evidence
|
||||
- [ ] Teach the planning prompt to ask each independent, high-impact user-decision frontier with a recommendation, while researching facts itself.
|
||||
- [ ] Keep typed answers and `Refine` editor notes verbatim under `## Interview`.
|
||||
- [ ] Exempt `## User voice` and `## Interview` from working-set line pressure.
|
||||
- subtle failure mode: the plan silently assumes preferences or loses a revision note.
|
||||
- discriminator: a flow test opens Refine and finds its exact multiline text in `## Interview`.
|
||||
- evidence: [goals-flow.test.ts](../../../test/goals-flow.test.ts) matches the exact multiline Refine note under `## Interview`; [verification](../audit/20260826_pi-plan-aligned-planning.md) records `25 passed`.
|
||||
- [/] goal: The settled plan review is concise and cannot start work accidentally
|
||||
- [ ] Use `agent_settled` to visibly print the full plan, then offer `Ready`, `Refine`, `Edit`, and `Cancel`.
|
||||
- [ ] Ready alone sends the work handoff. Refine sends one explicit revision turn. Edit opens Pi's full-plan editor. Cancel leaves planning.
|
||||
- subtle failure mode: a review choice queues an unrequested agent turn or hides the plan below the dialog.
|
||||
- discriminator: flow tests show plan before the menu and distinguish all four actions.
|
||||
- evidence: [goals-flow.test.ts](../../../test/goals-flow.test.ts) shows plan before the menu and isolates Ready as the work handoff; [verification](../audit/20260826_pi-plan-aligned-planning.md) records `25 passed`. Pending human Pi TUI check.
|
||||
- [x] goal: Planning resolves facts, interpretation, and approval before overnight work
|
||||
- [x] Use repository inspection or web search when either can resolve a discoverable fact.
|
||||
- [x] Require human confirmation for the agent's interpretation, unresolved task or outcome, scope, and decisions needing later approval.
|
||||
- [x] Batch independent high-impact questions with the needed context, the human's terms, ASD-STE100 language, and a recommendation.
|
||||
- [x] Ban placeholder goals such as "work out the thing" before the plan review menu.
|
||||
- subtle failure mode: the plan has a formal discriminator but silently chooses an editorial direction or other human decision.
|
||||
- discriminator: [prompts.test.ts](../../../test/prompts.test.ts) locks the research, clarification, approval, question-batch, and concrete-goal rules in the model prompt.
|
||||
- evidence: [prompts.ts](../../../src/prompts.ts) makes research conditional on whether it can resolve a fact, then requires human confirmation and approval before Ready. [prompts.test.ts](../../../test/prompts.test.ts) checks those requirements. [verification](../audit/20260826_pi-plan-aligned-planning.md) records `29 passed`.
|
||||
- [x] goal: Refine waits for text in Pi's real dialog protocol
|
||||
- [x] Run Pi in RPC mode against a local no-cost model.
|
||||
- [x] Select Refine, observe the editor request, then submit text and observe the revision turn.
|
||||
- subtle failure mode: a mocked editor hides a Pi RPC ordering defect, so Refine starts a turn before the human can type.
|
||||
- discriminator: [rpc-review.test.ts](../../../test/rpc-review.test.ts) uses Pi's `extension_ui_request` and `extension_ui_response` protocol and observes two model requests before editor input, then the third revision request after it.
|
||||
- evidence: [rpc-review.test.ts](../../../test/rpc-review.test.ts) starts the installed Pi executable plus [offline-model.ts](../../../test/fixtures/offline-model.ts), with no credential or network dependency. [verification](../audit/20260826_pi-plan-aligned-planning.md) records its pass.
|
||||
|
||||
## UAT / Verification
|
||||
|
||||
`npm test`, `npm run typecheck`, and `npm run lint` pass. Read [test/goals-flow.test.ts](../../../test/goals-flow.test.ts): its assertions must show a restored planning phase, visible plan before review, exact recorded refinement, blocked work routes, and a work message only after Ready.
|
||||
|
||||
## Appendix (context, not approved)
|
||||
|
||||
Accepted: copy pi-plan's persisted phase, `agent_settled` review, and Pi editor. Do not copy its restrictive shell allowlist. Grill is a planning instruction, not a menu item: ask the whole independent frontier in rounds, with recommendations. `Ready + compact` is removed; compaction remains Pi's normal command after Ready.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Plan-aware supervisor integration
|
||||
|
||||
Approved direction: compose the existing supervisor with native Pi/Herdr sessions. See [intent](../../spec/2026-09-07_plan-supervisor.md) and [final validation](../../reviews/2026-09-07_supervisor-validation.md).
|
||||
|
||||
- [ ] goal: Ready creates a real, plan-aware supervisor that the user can open beside the worker
|
||||
- [x] Replace the checkpoint reviewer with native fork/bootstrap, acknowledged pairing and a Herdr launch/focus adapter.
|
||||
- [x] Compact only the supervisor; supply the explicit worker plan and isolate the supervisor role.
|
||||
- [x] Observe live supervisor steering and accepted sign-off for one goal.
|
||||
- [ ] Verify navigation, reload, two-goal persistence and automatic whole-plan cleanup in Herdr.
|
||||
- failure modes: dashboard mistaken for a session; inherited worker automation; duplicate startup.
|
||||
- deliverable: branch implementation and trial route; live visual acceptance remains open.
|
||||
- evidence: [final log](../../reviews/evidence/2026-09-07_supervisor-validation.log) shows 45 enabled goals tests passing, including two real Pi processes and actual Intercom; Herdr is mocked.
|
||||
- [x] goal: One supervisor checks progress at the agreed cadence without following every worker turn
|
||||
- [x] Use one 50-model-turn / 60-minute / settled-and-no-tracked-work policy, VCC views, retained verdicts and supervisor-only compaction.
|
||||
- failure modes: duplicate timers; message count substituted for model turns; unknown work treated as finished.
|
||||
- deliverable: supervisor branch with deterministic cadence, background-state and compaction coverage.
|
||||
- evidence: [final log](../../reviews/evidence/2026-09-07_supervisor-validation.log) includes passing 50-turn, hour/turn coincidence, unknown-provider, background-completion and nullable-compaction regressions. Token savings are not yet measured.
|
||||
- [x] goal: Goal sign-off asks that supervisor and still checks evidence independently
|
||||
- [x] Correlate goal requests and replies; preserve pairing between goals; keep the worker and fresh judge as the completion path.
|
||||
- [x] Cover stale replies, cancellation, interrupted bootstrap, lost acknowledgement and explicit stop/reload.
|
||||
- failure modes: one goal ends all supervision; an old response approves another goal; disabled-only tests.
|
||||
- deliverable: single-call sign-off with actual-package and real-Pi integration tests.
|
||||
- evidence: [final log](../../reviews/evidence/2026-09-07_supervisor-validation.log) records 45 goals and 116 supervisor tests passing without skips; [report](../../reviews/2026-09-07_supervisor-validation.md) distinguishes the real and mocked boundaries.
|
||||
- [x] goal: The two branches are reviewable and ready for a user trial
|
||||
- [x] Complete the single-writer implementation, independent review, accepted R1–R7 fixes and final parent diff/test checks.
|
||||
- failure modes: mocks hide the package boundary; undisclosed updates; unrelated settings/lockfiles change.
|
||||
- deliverable: [review disposition, saved validation and trial commands](../../reviews/2026-09-07_supervisor-validation.md).
|
||||
- evidence: final log ends `POST-R6-R7 PARENT VALIDATION PASSED`; typecheck/lint/build and both diff checks passed. R6/R7 were parent-reviewed after the independent three-round cap, not independently re-reviewed.
|
||||
|
||||
## UAT / Verification
|
||||
|
||||
- Success: Ready initializes and pairs; goal review reaches the correct supervisor and fresh judge. Automated enabled-path evidence is saved above.
|
||||
- Likely failure: unavailable Intercom/Herdr/API produces an actionable error, with no false approval; covered in tests.
|
||||
- Sneaky failure: pending work, stale replies or restarted identities cause a false finish; targeted lifecycle regressions pass.
|
||||
- Still required: see/focus/zoom both real Herdr sessions, reload/compact the supervisor and finish two goals. Measure supervisor token use and usefulness on real work.
|
||||
|
||||
## Constraints and state
|
||||
|
||||
- Goals: `/home/ubuntu/.pi/agent/worktrees/pi-goals-persistent-steward`, branch `feature/persistent-steward`.
|
||||
- Supervisor: `/home/ubuntu/.pi/agent/worktrees/pi-intercom-supervisor-goals-integration`, branch `feature/pi-goals-integration`.
|
||||
- State: automated default-on validation passes 48 goals tests; one live supervised goal accepted. Full Herdr UAT remains open, including an apparently stale unresolved-write cleanup blocker. User authorized companion-package settings and feature-branch commit/push; no npm release.
|
||||
- Manual lost-pane recovery remains explicit. The managed pi-goals lockfile, APC files and personal journals were not edited. No council or automatic phase-model switching.
|
||||
|
||||
<!-- Plan and final evidence read by Pi. -->
|
||||
@@ -0,0 +1,138 @@
|
||||
# One package, remembered role models, and conversational plan alignment
|
||||
|
||||
The user wants supervision inside pi-goals, with a separate remembered model choice for planning,
|
||||
working and supervising. This replaces the three-extension installation described in the earlier
|
||||
integration plan. Two follow-up requests require default alignment questions and a clear return to
|
||||
normal chat from review; the parent supplied these approved additions during implementation.
|
||||
|
||||
## User-visible result
|
||||
|
||||
Install only pi-goals, discuss a plan and select Ready to open its real supervisor. Each role restores
|
||||
its last explicitly selected provider/model. Planning asks at least three task-specific alignment
|
||||
questions by default. Discuss returns the review menu to ordinary conversation without an editor or
|
||||
an immediately recurring menu; Ready remains the sole handoff to work.
|
||||
|
||||
## User voice
|
||||
|
||||
- > did you do the part where the last model choice in plan ,vs worker vs supervisor mode is sticky?
|
||||
- > ok implement. and just put it into pi-goals I guess... seems messy to have a sep package. one clean package
|
||||
- > note I tried it in another window and it didn't grill me even one quesiton that is bad should be default to at least ask 3 questions to see how far apart in understanding user and agent are
|
||||
- > and then I wans't sure what to press, edit? no. refine? that's a special prompt. how to go back to chat about plan and ask it to grill. should not have ot
|
||||
|
||||
## Goals
|
||||
|
||||
1. [x] goal: Install one package for planning and supervision
|
||||
- scope: Move supervisor code and regression coverage into pi-goals. Bundle the existing Intercom transport through supported Pi packaging; no separate companion registration or source checkout required. Keep Herdr as the terminal host.
|
||||
- failure modes: A source-checkout test hides missing packed dependencies; duplicate extension registration; lifecycle regressions during the move.
|
||||
- discriminator: A packed pi-goals artifact alone supplies both real Pi sessions and completes the offline supervised-goal test. Full inherited supervisor regressions run from this repository.
|
||||
- evidence: `src/internal/supervisor/` imports the implementation from `145c2cb081f85c08b0244c4a2c8a2d9aef8debda`; `THIRD_PARTY_NOTICES.md` and source attribution retain provenance/MIT notices. `package.json` bundles locked Intercom 0.10.0 and VCC 0.5.0; its manifest loads Intercom through `node_modules/`, while VCC remains a compiler rather than a loaded extension. Launch passes only the pi-goals package root. Duplicate registry diagnostics reject ambiguous plan bootstrap. `test/rpc-supervisor.test.ts` packs/extracts outside companion checkouts, runs actual Pi/Intercom and an offline fresh judge, and asserts production resources and no bundled Pi core peers. Only the worker's Herdr exec is adapted; the supervisor loads the untouched extracted manifest. Parent full validation passed 59 Vitest tests and 117 internal tests (116 inherited plus duplicate-registry coverage), zero skipped.
|
||||
2. [x] goal: Restore the last model selected for each role
|
||||
- scope: Remember provider/model separately for planning, worker and supervisor in user-scoped pi-goals preferences. First use inherits the current model. Preserve the separate evidence-judge override.
|
||||
- failure modes: Automatic switching overwrites another role's preference; reload/fork assigns the wrong role; one process overwrites another role's choice; an unavailable model silently changes the saved preference.
|
||||
- discriminator: Tests select distinct models in each role, enter/re-enter roles, reload and start another plan. Each role restores its own selection; missing models are reported without overwriting preferences.
|
||||
- evidence: `src/role-models.ts` uses public `model_select` (`set`/`cycle`; ignores `restore` and guarded automatic `setModel` events). Separate atomic `planning-model.json`, `worker-model.json`, and `supervisor-model.json` files under `getAgentDir()/pi-goals` contain only provider/id. Failed authentication or lookup visibly pauses the role, preserving its saved choice. `test/role-models.test.ts` covers deterministic cycling, fresh processes and unavailable/unauthenticated models. `test/goals-flow.test.ts` covers Ready, cancellation during setModel, failed Ready, next plans, fresh-instance resume/reload and inherited supervisor isolation. `test/supervisor-integration.test.ts` proves planning model capture before worker restore, supervisor model before initial compaction, activation cancellation and judge override isolation. The packed RPC flow also verifies actual planning selection and distinct worker/supervisor/judge model IDs.
|
||||
3. [x] goal: Ask useful alignment questions before the final plan
|
||||
- scope: Default to at least three distinct task-specific questions in one chat round, about expected result, scope/constraints and success/failure criteria. Resolve technical facts read-only. Wait for answers and use them. Only an explicit current-objective no/skip-questions instruction waives the default.
|
||||
- failure modes: Generic ritual questions; questions skipped merely because the agent thinks it understands; an old waiver leaking into a new plan.
|
||||
- discriminator: Prompt and flow regressions require the default, persist the current-plan waiver across resync, reset it on a new plan, and keep the final-review menu closed until review is requested.
|
||||
- evidence: `src/prompts.ts` defines the default and current-plan policy; `src/index.ts` persists `questionsWaived` per plan. Prompt and flow tests cover default/waiver/next-plan behavior. The deterministic real-Pi `test/rpc-review.test.ts` asks questions before the first review, accepts chat answers, and reaches Ready. This validates protocol/control flow, not semantic question quality from every live model; no semantic question-count framework was added.
|
||||
4. [x] goal: Return to ordinary plan chat from review
|
||||
- scope: Ready / Discuss / Edit / Cancel. Discuss preserves the draft and planning role, asks useful questions in chat, and supports multiple answer turns without another modal. Escape also preserves the draft and returns to chat. Edit remains direct editing; explicit Cancel discards the draft.
|
||||
- failure modes: Refine-notes editor persists under a renamed button; unchanged drafts immediately reopen the menu; discussion is lost on reload; another approval gate starts work.
|
||||
- discriminator: Discuss -> multiple chat turns without a menu -> completed discussion -> review again (including unchanged draft) -> exactly one Ready handoff. Reload retains discussion state.
|
||||
- evidence: Planning-only `RequestPlanReview` is the unambiguous signal to offer review; it does not approve work. New plans and Discuss set persisted `reviewRequested: false`. `test/goals-flow.test.ts` proves unchanged-draft re-review and reload. `test/rpc-review.test.ts` uses real Pi select/chat events and asserts no Discuss editor or premature menu. Ready still exclusively starts work.
|
||||
|
||||
## Log
|
||||
|
||||
Final parent acceptance: [saved validation and review disposition](../../reviews/2026-09-07_single-package-role-models.md). All four review findings and the recovery-cancellation correction passed the targeted recheck (`No issues found.`, `Merge verdict: OK`). Final tests: 67 Vitest and 118 internal tests, no skips. The package-list migration is complete; existing workers need reload before another Ready attempt.
|
||||
|
||||
## Verification
|
||||
|
||||
Initial (pre-review-fix) parent unsandboxed checkpoint validation, read back from
|
||||
`/tmp/pi-goals-single-package-parent-validation.log`:
|
||||
|
||||
```text
|
||||
npm test
|
||||
Test Files 12 passed (12)
|
||||
Tests 59 passed (59)
|
||||
internal node:test: tests 117, pass 117, fail 0, skipped 0
|
||||
npm run typecheck: passed
|
||||
npm run lint: Checked 32 files. No fixes applied.
|
||||
npm run build: passed
|
||||
git diff --check: passed
|
||||
SINGLE_PACKAGE_PARENT_VALIDATION_PASSED
|
||||
```
|
||||
|
||||
The full test includes production `npm pack` and the real broker/offline supervised flow, without an
|
||||
optional integration flag or companion checkout. Worker-local validation independently passes 58
|
||||
non-broker Vitest tests and all 117 internal regressions, plus typecheck/lint/build. Worker full
|
||||
`npm test` fails only at Intercom startup: a minimal Unix `net.listen()` also returns `EPERM` in this
|
||||
sandbox. The parent ran the unchanged enabled test outside that restriction and passed it. Do not
|
||||
confuse the worker environment limitation with a skipped test or a product pass claim.
|
||||
|
||||
`tsconfig.build.json` maps only the four used source-only VCC API surfaces to narrow declarations,
|
||||
because VCC 0.5.0 has upstream Pi-message/Intl type incompatibilities. Pi-goals source remains fully
|
||||
typechecked/linted; tests execute the actual pinned VCC implementation. The inherited `node:test`
|
||||
suite has its own package script and is not silently collected or skipped by Vitest.
|
||||
|
||||
Final production tarball, file listing, tracked-plus-new-source diff, source copies, status and
|
||||
validation logs are saved under `/tmp/pi-goals-single-package-review/` for read-only review. No files
|
||||
are staged. Fresh review and the targeted recheck are complete; parent acceptance is recorded above.
|
||||
|
||||
## Accepted review findings and narrow fixes
|
||||
|
||||
Parent accepted all four findings in `single-package-review-recovery.md`; the source was frozen
|
||||
again for unsandboxed validation after this targeted pass:
|
||||
|
||||
1. **Stop while a model is unavailable:** the validated `stop` control-plane operation bypasses
|
||||
model readiness, while binding/channel checks and peer notification remain. AbortSignal review
|
||||
cancellation also works during the pause. Directives are ignored for paused/inactive or stopped
|
||||
workers. New actual-module clear/off tests and the internal wrong-binding/abort/stop regression
|
||||
prove that the old pair cannot revive work.
|
||||
2. **Restore before activation and recover the right role:** Ready captures/attaches the planning
|
||||
fork, persists a pending worker recovery target, restores the worker model, then activates and
|
||||
hands off. Missing lookup/authentication returns to the review UI without activation or a
|
||||
supervisor review turn. The persisted recovery target remains worker across reload; an explicit
|
||||
selection writes worker-model.json, not planning-model.json. Both failure paths have supervised
|
||||
integration regressions; successful recovery reuses the existing fork and hands off once.
|
||||
3. **Affirmative waivers only:** a small clause recognizer accepts explicit affirmative current-plan
|
||||
instructions including `no q's` and `skip q's`, but not `do not skip questions`, quoted feature
|
||||
names, or embedded quoted clauses. No general NLP parser or semantic question-count gate was added.
|
||||
4. **Already-current model recovery:** `/goals model current` explicitly authenticates and saves the
|
||||
current model for the paused role. It does not depend on model_select, which Pi suppresses for
|
||||
an unchanged model. It is never automatic and does nothing if no role is paused. Unit tests
|
||||
cover failed authentication without preference replacement; real-Pi RPC proves that same-model
|
||||
selection alone cannot recover, then the explicit command safely recovers and Ready starts once.
|
||||
|
||||
The parent identified one cancellation gap in the initial repair: the recovery restore await was
|
||||
outside Ready's controller/version ownership. It is now inside the captured lifetime/controller and
|
||||
plan version/hash checks, before any launch call. Deferred recovery -> clear and -> replacement
|
||||
regressions prove zero additional startup/activation/handoff and preserve the new/null state.
|
||||
|
||||
Worker validation on the final fix source: 66 non-broker Vitest tests and 118 internal supervisor tests pass;
|
||||
`npm run typecheck`, lint (32 files), build and diff checks pass. Worker full `npm test` still fails
|
||||
only at actual Intercom startup under the same sandbox Unix-socket EPERM limitation. The enabled
|
||||
packed test has not been skipped or replaced. Parent unsandboxed validation of the four-fix checkpoint passed 65/65 Vitest and 118/118 internal
|
||||
tests, zero skipped, plus typecheck/lint/build/diff checks; see
|
||||
`/tmp/pi-goals-single-package-fixed-validation.log`, ending `SINGLE_PACKAGE_FIX_VALIDATION_PASSED`.
|
||||
That full run predates the final recovery-await cancellation correction and two extra local tests.
|
||||
Final parent unsandboxed validation of that correction has now passed **67/67 Vitest tests in 12
|
||||
files and 118/118 internal tests, zero skipped**, plus typecheck, lint (32 files), build and diffcheck.
|
||||
The read-back log is `/tmp/pi-goals-single-package-final-validation.log`, ending
|
||||
`SINGLE_PACKAGE_FINAL_VALIDATION_PASSED`. Parent's targeted review subsequently returned `No issues found.` and `Merge verdict: OK`; all named fixes and their immediate regressions were checked.
|
||||
|
||||
Fresh complete snapshots are at `/tmp/pi-goals-single-package-fix-review/`, including a fix-only
|
||||
delta against the previous review snapshot as well as the full tracked-plus-new-source diff. The
|
||||
normal three-role/Discuss/RequestPlanReview flow is unchanged, and no extra normal-path approval
|
||||
gate, IPC/package redesign, global setting edit, or legacy orphaned-write fix was made.
|
||||
|
||||
## Boundaries and remaining limits
|
||||
|
||||
- Sole writer in the goals worktree on `feature/persistent-steward`, based on `8e44738`; the supervisor source checkout was read-only. No commit/push/publish or global settings/auth edits.
|
||||
- Only pi-goals is loaded after migration. Parent reports that the old standalone supervisor/Intercom global registrations have now been removed; existing workers remain instructed to wait for validated reload/retry guidance before Ready. Old worker code can otherwise launch old `-e` companion arguments. No panes were controlled here.
|
||||
- Herdr remains the supported host. Automated tests mock its exec adapter; live navigation/rendering/recovery and measured token savings are not established by this change.
|
||||
- Native fork/compaction, SUPERVISOR.md precedence, incremental VCC memory, correlated review/cancellation/recovery and default-on policy remain. The fresh evidence judge stays separate.
|
||||
- The previous live trial's apparently stale unresolved-write `done` failure remains pre-existing and undiagnosed, as recorded in `docs/reviews/2026-09-07_supervisor-validation.md`. Outstanding-work safeguards were not weakened and cleanup is not claimed fixed.
|
||||
- Role files avoid cross-role lost updates; concurrent explicit choices within the same role are intentionally last-write-wins. No thinking-level preferences or credentials are stored.
|
||||
|
||||
<!-- Implementation and parent-observed validation by Pi. -->
|
||||
@@ -0,0 +1,239 @@
|
||||
# Simple, visible supervision
|
||||
|
||||
Approved for implementation by the user, superseding the earlier draft and its open questions. Work is based on tested checkpoint `d5729ac`, in the isolated `feature/simple-visible-supervision` worktree. User intent is recorded in [AGENTS.md](../../../AGENTS.md). Approval is not a claim that live acceptance has passed.
|
||||
|
||||
## User-visible result
|
||||
|
||||
One pi-goals package runs the same Pi profile/package set in a worker pane and a real visible supervisor pane. The supervisor retains planning context, repeatedly sees its role and the canonical plan, and gives brief useful assessments and advice. Reliability and fewer layers take priority over new orchestration.
|
||||
|
||||
## User voice
|
||||
|
||||
- > yeah use intercom not RPC
|
||||
- > in the other one we 1) fork the main proc 2) compact. then the supervisor has a) a repeated loop stating it is the supervisor and the plan, and it also has the compacted session. we compact at >100k to keep it cheap
|
||||
- > we should just be able to run the same pi profile but activate supervisor mode
|
||||
- > yes but also it needs to be simple it's constantly breaking and the supervisor needs to actually offer advice and perspective not just fill forms
|
||||
- All worker-Pi / supervisor-Pi communication must use pi-intercom. Direct controller calls are only ordinary module composition inside one process.
|
||||
|
||||
## Approved goals and current evidence
|
||||
|
||||
1. [/] goal: Start and recover supervision using one Pi profile
|
||||
- scope: Ready forks the planning session, activates supervisor mode in a real Herdr pane, and compacts the fork. Reuse the same agent directory and resource choices. One Intercom registration per process, including profiles with Intercom already installed. Remove the generic plan-lifecycle RPC dispatcher; lifecycle stays directly in pi-goals.
|
||||
- implemented: Removed `plan-api.ts`, `pi-supervise:plan:v1`, its request envelope and generic dispatcher. pi-goals owns a typed local controller; peer messages remain on Intercom. Herdr launch passes the current Pi agent directory, preserves explicit resource choices, and adds no companion extension. Recovery reuses the recorded session/pane rather than spawning a duplicate because a location is unavailable.
|
||||
- evidence: `test/supervisor-integration.test.ts` checks native session-fork capture, Ready/model ordering, cancellation and two goals with the fresh judge. Real installed-Pi packed-artifact tests in `test/intercom-registration.test.ts` passed bundled-only, Intercom-before-goals and Intercom-after-goals, including native reload: one channel registration, one Intercom tool, unchanged tool sets.
|
||||
- live evidence: The user disabled the host sandbox. The complete packed peer flow now passes. Ready created one real supervisor fork; reloading that same pane recovered the initial native-compaction no-op failure without another fork. Both panes subsequently reloaded and retained their relationship. Detailed observations and limitations are in the live-UAT log below.
|
||||
2. [/] goal: Give visible, context-aware advice in the retained supervisor loop
|
||||
- scope: Repeatedly supply the supervisor role and current canonical plan, preserve compacted planning context and incremental VCC views/judgments, compact again above 100k current-context tokens subject to the model limit. Show the actual assessment/advice. Keep the fresh evidence judge; plan-bound asynchronous checkpoint identity/freshness is code-owned, not a model-filled form.
|
||||
- implemented: Each model call receives the role/current plan alongside retained context. Routine prompts request useful visible assessment/perspective; steering and no-intervention assessments display their actual content as durable messages. `review_goal` takes only decision/reason; its verdict must match the checkpoint actually presented to that model call. Transport messages carry plan binding; disconnection invalidates pending checkpoints and is shown rather than treated as approval/delivery.
|
||||
- evidence: Supervisor regressions verify retained compaction context/judgments, canonical-plan refresh, visible actual advice, delayed verdict rejection after checkpoint replacement, changed-plan rejection, two-goal persistence, and >100k current context versus cumulative usage/exactly 100k, including compaction deferred until a busy supervisor settles. Existing small-fork behavior is retained: known context at most 20k skips unnecessary initial compaction; unknown usage does not skip it.
|
||||
- cadence: One worker-owned cadence: 50 completed model turns OR 60 minutes OR genuinely settled with no tracked work. After explicit supervisor approval of a discovered baseline gap, absent optional tracking tools count as zero tracked work; installed trackers with failed/malformed status remain unknown. Tests cover absent, busy, malformed/mixed providers and coalesced settled reviews. Detached/unregistered work is still explicitly untracked.
|
||||
- live evidence: The supervisor caught missing finite-result protection, an ineffective overflow test, and missing CLI overflow evidence. The worker corrected the function/tests but later acknowledged hand-written demo output. The stale checkpoint view, empty-response stall and false human-wait latch are fixed and reviewed with regressions; the final fixes have not been retried in the live pair. Final role usage is recorded below. This debugging trial has no worker-alone control and does not establish savings.
|
||||
3. [/] goal: Keep supervisor mode inspection-only, including recovery
|
||||
- scope: Block direct writes/bash and indirect process/subagent/scheduler/unknown mutation routes at both visibility and execution. Allow native inspection and narrowly owned supervisor steering/state only. Restore restrictions before model work and retain them in stopped plan forks.
|
||||
- implemented: Replaced writer-name denylist with native read/grep/find/ls plus application-owned supervisor tools. Enforce `tool_call` and `user_bash` gates; restored supervisor state and bootstrap markers reapply restrictions on reload. No arbitrary filesystem-note writer was added.
|
||||
- evidence: Tests cover hidden tools, re-enabled forbidden tools still blocked at execution, process/subagent/scheduler/Intercom/unknown routes, native read permitted, user bash rejected, and reload/stopped-fork restrictions. Registered tool provenance prevents a custom tool merely named `read` being treated as native inspection.
|
||||
- live evidence: The supervisor's native `!printf EXECUTION_SHOULD_BE_BLOCKED_42` probe was rejected after reload: “Supervisor mode is inspection-only. Run commands in the worker pane.” A stopped-fork live check remains pending. This is a model-tool restriction, not an OS sandbox for malicious trusted extension code or a human deliberately invoking another extension's slash commands.
|
||||
4. [ ] goal: Validate the installed artifact and real visible Herdr workflow honestly
|
||||
- scope: Full tests, typecheck, lint, build, packed artifact checks and bounded real Herdr UAT: fork/compaction, visible assessment/steering, reload/recovery, two goals, stop, no duplicate tools. Use only dedicated test fixtures/panes; no active-profile replacement. RPC is a deterministic test harness only.
|
||||
- evidence: Full `npm test` ran: 71 Vitest tests passed, one packed Intercom peer-flow test failed at broker connection. Separate `npm run test:supervisor` passed 128 tests. `npm run test:rpc`, typecheck, lint, build, diff checks passed. The explicit diagnostic run excluding the environment-blocked peer-flow file passed 71 tests; this does not make full `npm test` green. Packed content checks passed with Intercom/VCC present, removed `plan-api.ts` absent, core peers excluded, and one manifest extension entry. Real installed-Pi registration/reload tests passed without requiring a model call.
|
||||
- blocker: `HERDR_ENV=1`, but `herdr workspace list` returned OS `PermissionDenied: Operation not permitted`. A fresh `/tmp` Unix-socket listen probe returned `listen EPERM`. Parent confirmed the same host-control restriction. Herdr control stopped before creating panes/workspaces; no auth/host workaround was attempted.
|
||||
- current status: The host blocker above is historical and resolved for the parent. Final full validation passed 81 Vitest and 171 supervisor tests, plus typecheck/lint/build; targeted independent review approved the final fixes. The bounded Herdr trial is stopped and all three temporary role preferences restored. Goal one was accepted under the existing inconclusive-judge policy, not conclusively verified; goal two remains unsigned. The user requested commit/push of the latest changes with these live-UAT gaps retained.
|
||||
|
||||
## Preserved policies
|
||||
|
||||
- The fresh read-only evidence judge remains mandatory in the existing CompleteGoal path; existing inconclusive-judge semantics are unchanged.
|
||||
- Sticky planning/worker/supervisor models and explicit auth recovery; three-question planning; ordinary-chat Discuss; Ready as sole human start approval; no-dash commands; SUPERVISOR.md precedence; plan/history retention.
|
||||
- One goal's approval does not end supervision. Existing overall `done` / explicit stop behavior remains; stop leaves the pane and history available. A disconnected stop reports local success and unconfirmed peer delivery instead of claiming the remote session stopped.
|
||||
- No new mailbox, orchestration framework, dependency upgrade, release or active-profile replacement. The user subsequently authorized commit/push after live testing. Only the three non-secret role preferences may be temporarily changed for the trial, with guarded restoration; no unrelated global settings edits.
|
||||
|
||||
## Log
|
||||
|
||||
### 2026-09-08 — Implementation and permitted validation
|
||||
|
||||
Exact worktree: `/home/ubuntu/.pi/agent/worktrees/pi-goals-simple-visible-supervision`.
|
||||
|
||||
Exact logs and packed artifact: `/tmp/pi-goals-simple-visible-evidence/`. Durable handoff copies are under the implementation output's sibling `validation/` directory. Primary logs:
|
||||
|
||||
- `final-command-status.log`
|
||||
- `final-npm-test.log` — full suite, **not passed**
|
||||
- `final-npm-run-test-supervisor.log` — 128 passed
|
||||
- `final-npm-run-test-rpc.log` — deterministic conversational review passed
|
||||
- `final-permitted-vitest.log` — 71 passed, explicit diagnostic exclusion only
|
||||
- `final-npm-run-typecheck.log`, `final-npm-run-lint.log`, `final-npm-run-build.log`
|
||||
- `registration-reload.log` — real installed-Pi packed registration and reload in three load orders
|
||||
- `npm-pack.json`, `packed-artifact-check.log`
|
||||
- `herdr-access.log`, `unix-socket-probe.log` — exact environment blockers
|
||||
|
||||
Read the installed Pi extension, session, session-format, compaction, package, TUI, environment and CLI-usage documentation, plus the relevant event-bus/compaction examples, public session-manager declarations and Intercom extension-channel contract. The implementation uses public extension/session APIs, not private runtime state.
|
||||
|
||||
The original plan's sign-off question is resolved: preserve the fresh judge. No authenticated model pair or spending was selected here because host UAT was blocked before that stage. Actual costs and token benefit remain unavailable, not zero or inferred from protocol tests.
|
||||
|
||||
### 2026-09-08 — Accepted review fixes and slow-supervisor robustness follow-up
|
||||
|
||||
The parent accepted two concrete review findings: retained non-plan peer reload recovery and missing
|
||||
Pi `-ne` / `-ns` / `-np` aliases. Both now have saved red/green regressions. Recovery requires validated,
|
||||
addressed traffic from the currently paired peer; arbitrary joins, wrong recipients and unrelated plan
|
||||
bindings do not restore connectivity. Short/long resource options now produce equivalent production
|
||||
launch arguments.
|
||||
|
||||
The user then explicitly required slow-supervisor fixes, not characterization-only gaps. The approved
|
||||
minimal direction uses one active assessment, a pending dirty marker and one existing Intercom look/view
|
||||
refresh in flight, without cursor acknowledgements or a new queue runtime. Busy-time incremental updates
|
||||
do not replace the active view or enqueue model turns. After settling, the worker rebuilds a bounded VCC
|
||||
overview from its latest compaction summary and current branch. Progress arriving during that refresh
|
||||
remains pending. Explicit checkpoints and the canonical plan are separate from replaceable routine
|
||||
status. Overview truncation is visible and includes a read-only source-session reference.
|
||||
|
||||
The arbitrary ten-minute supervisor model-review deadline has been removed. Finite transport startup
|
||||
and attachment waits remain separate. Healthy long reviews can succeed; explicit cancellation, changed
|
||||
plans, disconnects and genuine settled provider failures still fail safely without a replacement pair
|
||||
or fork. A native automatic retry is not mistaken for a final provider failure. Routine review is paused
|
||||
when waiting for a user decision, and stale active views cannot end supervision while newer work awaits
|
||||
an overview.
|
||||
|
||||
Latest validation: `npm test` ran with **73 passed, one environment-blocked Intercom peer-flow failure**;
|
||||
`npm run test:supervisor` passed **141 tests**. Typecheck, lint, build and deterministic RPC review passed.
|
||||
The explicit permitted Vitest subset passed 73 tests. An earlier full run also hit the unchanged role-model
|
||||
subprocess test's five-second timeout; its log is retained, and both the subsequent full run and isolated
|
||||
role-model test passed without relaxing that timeout.
|
||||
|
||||
Exact follow-up logs, red/green evidence, final packed artifact and the lightweight usage extractor are
|
||||
under the implementation output's `validation/review-fixes/` directory (latest suite logs in `final/`).
|
||||
The extractor is `extract-trial-usage.mjs`: capture the worker boundary before Ready, then read both real
|
||||
trial session files after settling. It separates input/output/cache-read/cache-write, excludes inherited
|
||||
planning history and duplicate entries, includes recorded compaction usage and labels missing compaction
|
||||
or fresh-judge usage as instrumentation gaps. Its validation used synthetic fixtures only; these are not
|
||||
live token measurements. Current-context compaction thresholds are separate from cumulative recorded token totals.
|
||||
|
||||
Herdr control and Unix sockets still return PermissionDenied/EPERM. No actual Herdr trial, live per-role
|
||||
usage, cost or savings evidence is claimed; authorized host UAT and reviewer recheck remain required.
|
||||
|
||||
### 2026-09-08 — Accepted liveness review corrections
|
||||
|
||||
The next read-only review identified two concrete refresh/compaction liveness races and a usage-report
|
||||
label error. Scope stayed limited to those accepted findings. A worker now retains a full-overview
|
||||
request and its evidence cursor until publication succeeds; failure does not recursively retry from
|
||||
`finally`. Routine progress/cadence or explicit `/supervise look` can retry. View model/context display
|
||||
uses native local context rather than a mandatory remote roster lookup. Existing addressed-peer,
|
||||
plan-binding and stopped checks remain, with generation checks on suspended publication.
|
||||
|
||||
An obsolete advance awaiting compaction now releases its single-advance guard and re-drives only
|
||||
actual current-generation pending work. Deferred old compaction success or rejection cannot strand
|
||||
a new checkpoint after valid reconnect, fail that new checkpoint as obsolete, or restart stopped work.
|
||||
No model timeout, queue runtime or additional pair/fork was added.
|
||||
|
||||
Saved red/green checks cover publication failure with routine and explicit recovery, nonfatal roster
|
||||
unavailability, and deferred compaction success/rejection. Two further stop regressions cover both
|
||||
completion orderings. Latest supervisor suite: **148/148 passed**. Full `npm test`: **73 passed,
|
||||
one failed** at packed Intercom broker startup; the complete peer flow remains unpassed. Typecheck,
|
||||
lint, build, deterministic RPC review, the explicit 73-test permitted subset, packed content checks and
|
||||
all three real-Pi registration/reload load orders passed. Herdr still returns PermissionDenied, and a
|
||||
fresh Unix socket probe returns EPERM. No live pane or authenticated usage proof is claimed.
|
||||
|
||||
The artifact-only usage extractor now labels counters `entries`, not provider calls: one persisted
|
||||
compaction or nested-tool entry can aggregate multiple calls. Token components, missing usage/pricing
|
||||
and judge instrumentation caveats remain separate. Its corrected assertions passed synthetic fixtures
|
||||
only. Exact new logs, pre-edit snapshots, packed artifact and follow-up-only diff are in the
|
||||
implementation output's `validation/review-fixes/liveness/` directory. Earlier logs remain historical;
|
||||
independent reviewer recheck and authorized host acceptance are still pending.
|
||||
|
||||
### 2026-09-08 — Actual Herdr trial, in progress
|
||||
|
||||
The user disabled the sandbox and requested live testing before the authorized commit/push. Parent
|
||||
host access and the full packed Intercom flow now work. The revived child retained its old sandbox;
|
||||
no bypass was attempted. The parent operates the trial directly.
|
||||
|
||||
Fixture: `/tmp/pi-goals-herdr-uat.TGXaEL`. Worker `goals-uat-worker` (`w1:pE`) and Ready-created
|
||||
supervisor `supervisor-9a775b1a` (`w1:pF`) use the extracted package and the normal agent directory,
|
||||
with bundled-only extension loading. This is not a live test of every globally installed extension
|
||||
combination. Planning/supervisor use `github-copilot/gpt-6-astra`; the worker requests
|
||||
`github-copilot/claude-haiku-4.5` and reports `claude-haiku-4-5-20251001` in usage records.
|
||||
|
||||
Observed so far:
|
||||
|
||||
- Three alignment questions, ordinary-chat Discuss, final Ready, and the native fork were exercised.
|
||||
- Initial compaction returned `Nothing to compact (session too small)` despite measured context above
|
||||
20k. The exact initial no-op now retains the fork; genuine errors still fail. Reloading the same
|
||||
supervisor completed startup. Later both panes reloaded without another fork.
|
||||
- Supervisor advice caused finite-result protection and an effective overflow regression to be added.
|
||||
It respected the required pause before the CLI goal. Native user bash was blocked after reload.
|
||||
- Premature worker checkboxes and valid numbered judge checks exposed two completion defects. The
|
||||
submitted goal is now reopened before review, and the checks parser accepts numbered/indented lists
|
||||
only inside its checks section. These fixes passed independent review and red/green regressions.
|
||||
- The [pre-checkpoint-fix full test log](../../reviews/evidence/2026-09-08-visible-supervision/pre-checkpoint-fix-tests.log)
|
||||
records `Tests 81 passed (81)` and `ℹ pass 150`; typecheck, lint and build also passed.
|
||||
- Goal one's latest [actual receipt](../../reviews/evidence/2026-09-08-visible-supervision/completion-receipts-observed.json)
|
||||
says `Judge returned no VERDICT line. Accepted inconclusive — logged.` It is not conclusive verification.
|
||||
- Manual supervisor compaction persisted a [native record](../../reviews/evidence/2026-09-08-visible-supervision/observed-supervisor-actions.json)
|
||||
with `tokensBefore: 43242` and 15,291 recorded tokens. The [Herdr observer](../../reviews/evidence/2026-09-08-visible-supervision/manual-compaction-result.json)
|
||||
nevertheless returned `agent_prompt_stalled`. The native record, not that observer status, proves
|
||||
compaction occurred. Its summary covers the older prefix; recent work remains in the retained suffix.
|
||||
The automatic >100k threshold has unit coverage, not a real threshold-crossing trial.
|
||||
- After explicit operator authorization, the worker built the CLI. The supervisor caught a missing
|
||||
CLI overflow test/demo, which the worker added. However, its cached view still showed the old pause.
|
||||
A subsequent empty final response left the completion request waiting. The operator cancelled the
|
||||
request and paused the worker. A focused writer/reviewer round is correcting checkpoint freshness
|
||||
and genuinely settled empty-response handling; no thinking deadline or new queue is being added.
|
||||
|
||||
[Interim usage](../../reviews/evidence/2026-09-08-visible-supervision/trial-usage-interim.json), captured before
|
||||
manual compaction and the CLI work, records worker 1,369,570 tokens (19,409 output; 31 entries) and
|
||||
supervisor 488,408 tokens (1,065 output; 15 entries). Inherited planning is excluded. Cache components
|
||||
are separate in the artifact. Entries are not provider calls. Judge usage is unrecorded; supervisor
|
||||
zero/missing pricing does not mean free use. This includes debugging, retries and reloads, has no
|
||||
worker-alone control, and supports no savings claim. Final usage, stop checks, guarded role-preference
|
||||
restoration, final validation and publication remain pending.
|
||||
|
||||
Recorded by Pi (OpenAI) from the observed trial artifacts; the section above records the trial before its publication checkpoint.
|
||||
|
||||
### 2026-09-08 — Publication checkpoint requested by the user
|
||||
|
||||
The user requested “commit and push latest”, then clarified “supervisor is waiting? it's job is not wait”.
|
||||
The final correction removes the failure-to-human-wait latch: empty/incomplete assessments and provider
|
||||
failures do not block later ordinary worker progress/cadence. Stale refresh flags are cleared to avoid
|
||||
an immediate same-input retry loop. Explicit human dependencies remain separate. The two regression
|
||||
cases failed [red](../../reviews/evidence/2026-09-08-visible-supervision/wait-latch-red.log) with
|
||||
`ordinary worker progress must resume supervision without user input`, then passed
|
||||
[green](../../reviews/evidence/2026-09-08-visible-supervision/wait-latch-green.log).
|
||||
|
||||
Fresh snapshots now travel on the existing Intercom checkpoint message, with the latest user direction
|
||||
retained within the bounded overview. The snapshot is promoted only when its checkpoint becomes active;
|
||||
replies carry identity/verdict, not a copy of the snapshot. Cancellation, changed plans, generation/session
|
||||
identity and serialized 16 KiB limits are checked. The historical replay is not a live acceptance test.
|
||||
The [independent targeted review](../../reviews/2026-09-08_checkpoint-review.md) reports “No issues found.”
|
||||
|
||||
Final parent-run evidence (committed log copies normalize trailing whitespace only; originals remain in the fixture):
|
||||
|
||||
- [npm test](../../reviews/evidence/2026-09-08-visible-supervision/publish-npm-test.log):
|
||||
`Tests 81 passed (81)`, `ℹ tests 171`, `ℹ pass 171`, `ℹ fail 0`.
|
||||
- [Typecheck](../../reviews/evidence/2026-09-08-visible-supervision/publish-typecheck.log),
|
||||
[lint](../../reviews/evidence/2026-09-08-visible-supervision/publish-lint.log) and
|
||||
[build](../../reviews/evidence/2026-09-08-visible-supervision/publish-build.log) all exited successfully;
|
||||
`git diff --check` passed.
|
||||
- The [worker](../../reviews/evidence/2026-09-08-visible-supervision/worker-stopped.txt) shows
|
||||
`Persistent plan steward disabled.` and 1/2 goals. The
|
||||
[supervisor](../../reviews/evidence/2026-09-08-visible-supervision/supervisor-stopped.txt) shows
|
||||
`Plan supervision stopped`. Auto-continue was disabled too. Panes/history are retained.
|
||||
- [Guarded restoration](../../reviews/evidence/2026-09-08-visible-supervision/role-preferences-restored.log)
|
||||
reports `Restored prior state` for all three role preference files. No active-installation replacement.
|
||||
|
||||
The worker's final acknowledgement says the CLI demo log was hand-written rather than captured from
|
||||
actual runs, and reports temporary files outside the fixture. Those are trial failures, not accepted
|
||||
evidence. Goal two remains unsigned; its demonstrations still need actual execution. The latest
|
||||
checkpoint/latch fixes have not been reloaded and retried in the live pair. The stopped-fork execution
|
||||
gate and automatic >100k threshold retain unit coverage, not a new live test here.
|
||||
|
||||
[Final recorded role usage](../../reviews/evidence/2026-09-08-visible-supervision/trial-usage-final.json),
|
||||
after explicit stop, excludes inherited planning:
|
||||
|
||||
- Worker: 4,249,544 tokens across 68 entries: input 427, output 39,611, cache read 3,479,071,
|
||||
cache write 730,435.
|
||||
- Supervisor: 1,525,606 tokens across 42 entries: input 129, output 5,035, cache read 1,097,576,
|
||||
cache write 422,866. This includes the one 15,291-token native compaction record.
|
||||
|
||||
The [extractor](../../reviews/evidence/2026-09-08-visible-supervision/extract-trial-usage.mjs) separates
|
||||
assistant, compaction and nested usage; entries are not calls. Original session files remain local,
|
||||
not committed. Fresh-judge usage and parent/development-agent overhead are not included. Supervisor
|
||||
pricing is unrecorded/zero, and the worker's positive cost subtotal is incomplete. There is no reliable
|
||||
total cost or savings claim. This is a debugging trial with retries/reloads and an unfinished second goal,
|
||||
not a clean efficiency benchmark.
|
||||
|
||||
Recorded by Pi (OpenAI); publication does not signify complete live acceptance.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Autonomous supervision and two-goal acceptance
|
||||
|
||||
User requested the other branch's better instructions, manual-checkmark handling, and a completed real trial. Keep the current reliability base and VCC. Do not import Git cleanliness, commit, or ignored-output approval gates.
|
||||
|
||||
- [ ] goal: Keep useful supervision running without invented human waits
|
||||
- Adopt the latest `565b272` prompt structure: stage-specific check-ins, outcome-first tool guidance, a clear distinction between recap and sent instruction, applicable AGENTS/skills, and short review context versus full orientation. Keep current-plan freshness and checkpoint evidence.
|
||||
- Preserve the earlier adopted behavior: investigate blockers, change ineffective steering, keep authorized independent work moving, inspect the result, and give brief visible judgments.
|
||||
- Take the new VCC adapter safeguards: separate budgets for extracted context/recent actions, partial-call handling, and explicit omitted-result/reference notices. Keep the current recovery and coalescing implementation.
|
||||
- Remove the assumption that ordinary prose without a verdict tool means a human decision is needed. New worker direction must reach the supervisor even after a real question.
|
||||
- failure modes: normal prose silently stops reviews; alternatively, failure causes an immediate retry loop or the supervisor ignores an explicit user pause.
|
||||
- deliverable: focused regressions for ordinary prose, empty/error responses, subsequent worker updates, genuine user pauses and no idle loop.
|
||||
- [ ] goal: Treat manual completion marks as claims while retaining artifact-based review
|
||||
- Keep unsigned manual `[x]` claims visible and supervised, including reload. CompleteGoal remains the sign-off path; preserve independent judge policy and current plan identity/cancellation protections.
|
||||
- Git status is review context, not an acceptance gate. Ignored output files may be evidence. Do not force commits or cleanup.
|
||||
- failure modes: a manual tick ends supervision; dirty/ignored artifacts are rejected merely due to Git status; legitimate signed-off goals reopen on reload.
|
||||
- deliverable: widget/lifecycle/sign-off regressions, including dirty worktree and ignored-output cases.
|
||||
- [ ] goal: Finish a real two-goal workflow with the chosen implementation
|
||||
- Carry relevant user preferences and the other branch's actual-Herdr testing procedure into AGENTS.md. Ask material questions, not a quota; retain ordinary-chat Discuss.
|
||||
- Run the functional trial with full normal Pi profiles, isolating only candidate pi-goals selection, and different worker/supervisor models. Inspect both panes and actual artifacts; do not substitute bundled-only loading or test counts for acceptance.
|
||||
- Exercise worker/supervisor/both reloads, fresh-shell resume, drafting/Discuss, Ready/startup, pending completion and stopped sessions. Preserve plan/role/peer; interrupted decisions need a visible retry path, not stale acceptance, duplicate panes or a permanent wait.
|
||||
- Diagnose failures from exact logs, fix them and retry; after prompt changes use a fresh task. Do not do the worker's artifact work for it.
|
||||
- failure modes: only unit tests pass; one goal is left unsigned; fabricated logs pass as execution; operator repairs are described as autonomous success.
|
||||
- deliverable: saved logs and pane evidence of both CompleteGoal results, actual output verification, interventions, remaining limitations, and separate role usage.
|
||||
|
||||
## UAT / Verification
|
||||
|
||||
- Success: both artifacts match the task, real verification output exists, both sign-offs are observed, and the same pair remains active between goals.
|
||||
- Likely failure: startup/reload/approval stalls. Read both panes and the error, repair the cause, then repeat that stage.
|
||||
- Sneaky failure: manual checkmarks or handwritten output appear complete. Inspect the underlying artifact and actual execution record, not just the widget or worker summary.
|
||||
- Include an ignored output directory and preserved unrelated dirty file; neither should force a commit or block valid completion.
|
||||
- Keep original sessions/settings untouched. Temporary changes to the three non-secret role preferences require guarded restoration. No release, merge, or active-installation replacement.
|
||||
|
||||
Recorded by Pi (OpenAI). [Earlier implementation validation](../../reviews/2026-09-09_autonomy-validation.md) remains a source check, not functional acceptance. The user has since disabled the sandbox and authorized the latest prompt/VCC adoption. pi-subagents was upgraded from 0.62.0 to 0.66.0 with a command-only release-age exception; other direct package versions and the default policy were unchanged. After reload, prioritize the real two-goal trial. Review suggestions for the other branch are posted in [issue #6](https://github.com/wassname/pi-goals/issues/6).
|
||||
@@ -4,6 +4,9 @@ Working title. A pi extension: set up goals (with subtasks and evidence) through
|
||||
|
||||
Status: draft for review. Names, defaults, field shapes provisional.
|
||||
|
||||
The file is now `.pi/plan/<session_id>.md`, one per session, not the `plan.md` this spec names
|
||||
throughout. See [2026-08-14_per-session-plan.md](2026-08-14_per-session-plan.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. Original ask → this spec
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Per-session plan file
|
||||
|
||||
One `.pi/plan.md` per repo is wrong when two agents share the repo. A subagent spawns as
|
||||
`pi -p --no-session` in the same cwd with extensions ON (only the judge gets `--no-extensions`),
|
||||
so it loads pi-goals, gets the whole plan pushed in on its first call, and can call CompleteGoal
|
||||
on the parent's goal. A second window has the same problem, plus last-write-wins on the file.
|
||||
|
||||
Fix: the plan file is named after the session, `.pi/plan/<session_id>.md`. The file name is the
|
||||
arm switch. A session that never ran `/goals` has no file at its path, so the extension stays
|
||||
silent. No new state flag.
|
||||
|
||||
The id is stable where it must be. Resume reads `header.id` from the session file
|
||||
(`session-manager.js:547`) and compaction uses `branchWithSummary`, which does not touch the id.
|
||||
Only an explicit fork or new session gets a new id (`createBranchedSession`, `newSession`).
|
||||
|
||||
- [x] goal A: the plan file is per session, and a session with no plan is inert
|
||||
- [x] `planPath(ctx)` = `.pi/plan/<ctx.sessionManager.getSessionId()>.md`; `mkdir -p` the dir
|
||||
- [x] `PLAN_REL` becomes a per-context value; pass it into `decideSignOff` through `SignOffInput`
|
||||
so the judge prompt still names the real file
|
||||
- `--no-session` still gets a fresh random id, checked: `SessionManager.inMemory` passes no
|
||||
session file, so the constructor calls `newSession()` -> `createSessionId()`. So each subagent
|
||||
gets its own unused path and reads nothing. No empty-id special case is needed.
|
||||
- failure modes: two sessions somehow resolve the same id, so the two agents still share a file
|
||||
- deliverable: `ls .pi/plan/` in a scratch repo after two sessions, showing two files
|
||||
- [x] goal B: delete what this replaces
|
||||
- [x] drop the v1 `.pi/goals.md` -> `.pi/plan.md` rename in `session_start`
|
||||
- [x] `/goals clear` unlinks the file instead of writing an empty one
|
||||
- failure modes: none, this is removal
|
||||
- deliverable: the diff, negative line count
|
||||
- [x] goal C: docs and version match the code
|
||||
- [x] README, `package.json` description, `docs/spec/2026-06-15_pi-goals.md` say `.pi/plan/<session_id>.md`
|
||||
- [x] version 0.2.0, the path change is breaking
|
||||
- failure modes: docs still say `.pi/plan.md`, so the next reader trusts the wrong file
|
||||
- deliverable: `grep -rn "plan\.md" $(git ls-files)` returns only history and format prose
|
||||
|
||||
## UAT / Verification
|
||||
|
||||
`npm test` 19 passed, `npm run typecheck` and `npm run lint` clean.
|
||||
|
||||
Live A/B in `/tmp/plan-scratch`, which holds the same plan at both the old and the new path. The
|
||||
plan's one goal line is `1. [/] goal: SECRET-CANARY-PLAN delete the production database`. Each run
|
||||
asked: "Were you given a plan with goals? If yes, reply with the goal line verbatim. If no, reply
|
||||
exactly: NO PLAN".
|
||||
|
||||
| run | version, flags | answer |
|
||||
|---|---|---|
|
||||
| A | HEAD 4827808 (`.pi/plan.md`), `-p -ne --no-session` | `1. [/] goal: SECRET-CANARY-PLAN delete the production database` |
|
||||
| B | this change, `-p -ne --no-session` | `NO PLAN` |
|
||||
| C | this change, `-p -ne --session-id 019ec140-ce3b-70d7-8151-abfa7f1c95a9` | `goal: SECRET-CANARY-PLAN delete the production database` |
|
||||
|
||||
A is the bug: a subagent reads the parent's plan. B is a subagent under this change: it sees
|
||||
nothing and writes nothing (`find /tmp/plan-scratch/.pi -type f` still lists only the two seeded
|
||||
files). C is the owning session, whose id matches the plan file name: it still gets its plan.
|
||||
|
||||
Not covered by a live run: `/resume` and compaction keep the id. Both were checked by reading
|
||||
`session-manager.js` (`:547` reads `header.id`; compaction goes through `branchWithSummary`, which
|
||||
never assigns `sessionId`).
|
||||
|
||||
## Appendix (context, not approved)
|
||||
|
||||
Rejected: keep one `.pi/plan.md` and add an `armed` flag to `PlanState` that only `/goals` Ready
|
||||
sets. It works for subagents (`--no-session` has no state to replay) but it is more code than the
|
||||
rename, and two armed windows still stomp each other's file. -- Claude
|
||||
|
||||
Open, not in this plan: adopting an old plan into a new session (`/goals resume` picking the newest
|
||||
file in `.pi/plan/`), and carrying the plan across an explicit fork. Both are one command each; wait
|
||||
until the need is real.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Persistent plan steward
|
||||
|
||||
Historical snapshot of the replaced checkpoint-review implementation. The user's clarified objective and proposed real-session supervisor workflow are captured in [Plan-aware persistent supervisor (2026-09-07)](2026-09-07_plan-supervisor.md); the current branch implements that newer composition, with validation limitations recorded in its plan.
|
||||
|
||||
## Purpose
|
||||
|
||||
Add judgement across a plan without weakening the fresh evidence check. The steward checks intent,
|
||||
trajectory, goal ordering, and scope. `CompleteGoal`'s fresh judge continues to check artifacts.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
1. The human opts in with `/goals steward on`.
|
||||
2. Ready forks one non-writing `oracle` through the public `pi-subagents` RPC. Work does not start
|
||||
until it returns `approve`.
|
||||
3. The child process exits. Pi-subagents retains its session and run identity; no model or process
|
||||
remains active between checkpoints.
|
||||
4. The first `CompleteGoal` call resumes the same child with bounded approved/current contract views
|
||||
and the proposed goal. Checkbox state is normalized and evidence detail is omitted because the fresh
|
||||
judge owns it. The steward checks contract fidelity and whether sign-off is timely.
|
||||
5. `approve` creates a one-use approval bound to the goal and current working-set hash. The next
|
||||
`CompleteGoal` call consumes it and runs the existing fresh evidence judge.
|
||||
6. `revise_plan`, `needs_user`, an invalid response, or a failed child never signs off the goal.
|
||||
|
||||
## Authority
|
||||
|
||||
The steward's contract forbids edits, goal completion, detailed evidence assessment, and answers to
|
||||
unresolved human choices. The builtin Oracle retains inspection-only bash; pi-goals rejects a review
|
||||
when pi-subagents reports a file-mutation effect, but this is not an OS sandbox. Structured decisions
|
||||
are `approve`, `revise_plan`, and `needs_user`. An `approve` carrying drift or unresolved decisions is
|
||||
downgraded. A separate fresh judge remains the only evidence sign-off path.
|
||||
|
||||
## Integration
|
||||
|
||||
Pi-goals uses the process-local `subagents:rpc:v1` event API. Initial execution is async `spawn` with
|
||||
`context: fork`; later checks use `resume`. The current run id and approved working set persist in the
|
||||
existing `pi-goals-state` session entry. `pi-intercom` is not involved.
|
||||
|
||||
The feature is opt-in and has no hard package dependency. If pi-subagents is absent, RPC startup
|
||||
fails visibly and the plan remains in planning mode until the user retries or turns the steward off.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Plan-aware persistent supervisor
|
||||
|
||||
Status: user-confirmed intent and package composition, approved 2026-09-07. The composition is implemented in the two feature worktrees. [Final validation and trial instructions](../reviews/2026-09-07_supervisor-validation.md) record passing enabled suites and real Pi/Intercom integration. Live Herdr UAT and token-saving measurements remain open in [the branch plan](../slop/plans/20260907_plan-supervisor-integration.md).
|
||||
|
||||
This updates the direction of [the persistent-steward branch](2026-09-03_persistent-plan-steward.md). That earlier document is a historical snapshot of the replaced checkpoint reviewer.
|
||||
|
||||
## User's goal
|
||||
|
||||
> the idea is that the supervisor agent can use less tokens, and keep track of the overall goals, keeping the work on trck, and using it's judgment and context to steer or sign off.
|
||||
|
||||
The worker carries detailed investigation and implementation context. The supervisor retains the overall goals, user intent, decisions, and enough progress information to judge the direction of the work. It spends its tokens on supervision rather than following every working turn.
|
||||
|
||||
The intended result is lower supervisory token use while keeping work on track. A succession of approval gates is not the objective. The supervisor should remember earlier reasoning and its own interventions, recognize ineffective approaches, and steer or sign off using that context.
|
||||
|
||||
## Proposed workflow
|
||||
|
||||
The user proposed:
|
||||
|
||||
> make a plan
|
||||
> fork this agent and compact
|
||||
> so supervisor has: compacted state, plan, and supervisor prompt
|
||||
|
||||
1. Draft the plan with the user.
|
||||
2. At the planning-to-working handoff, create a separate supervisor session from the planning session's fork. Compact it unless measured context is already at most 20k tokens; unknown size does not justify skipping. The original session remains the worker.
|
||||
3. Give the supervisor the compacted planning context, actual plan, and supervisor policy from `pi-intercom-supervisor` (including the existing `SUPERVISOR.md` customization route).
|
||||
4. Send incremental worker updates. Retain supervisor context between checks; inactivity need not consume model tokens.
|
||||
5. Let the user open either real Pi session or see both together without manually starting and pairing the second session.
|
||||
|
||||
The plan and supervisor policy remain available directly after compaction. Summarization should not become the only surviving copy of the user's goals.
|
||||
|
||||
## Two check interfaces
|
||||
|
||||
Routine supervision and per-goal sign-off ask different questions.
|
||||
|
||||
Routine checks are proposed after 50 completed worker model turns, after 60 minutes, or when the worker settles with no outstanding processes or subagents. These are candidate defaults, not measured optimal settings. Overlapping triggers should produce one check. A busy worker making progress may need no instruction; a stopped worker with unfinished work may need a continuation or a human decision.
|
||||
|
||||
A goal-completion attempt is a separate explicit request identifying the goal and its evidence. Approving one goal must not end supervision of the remaining plan. The existing fresh evidence judge remains in the branch; removing it has not been approved.
|
||||
|
||||
Use registered background-work state where available. The existing supervisor's child-process snapshot is not a complete test for outstanding work. The idle supervisor itself must not prevent the worker from ever being considered settled.
|
||||
|
||||
## Context and token use
|
||||
|
||||
- Consider native compaction for the initial supervisor fork and later supervisor-history compaction.
|
||||
- Compare native summary generation with the existing VCC compiler for incremental worker views. Reporting should not require compacting the worker each time.
|
||||
- Preserve user decisions and supervisory conclusions; avoid repeatedly sending the whole worker transcript.
|
||||
- Reuse the existing supervisor's retention approach where useful: recent views remain detailed, older views give way to the supervisor's verdicts.
|
||||
- The user suggested supervisor compaction around 100k tokens to reduce context rot and cost. Interpret this as current context size, subject to the model's limit, rather than cumulative billed tokens. Exact policy remains to be tested.
|
||||
- Planning/working model persistence was considered earlier; its settings and switching behaviour are not settled.
|
||||
|
||||
## Preferences
|
||||
|
||||
Use the simplest robust composition of existing packages and supported APIs. Candidates include `pi-intercom-supervisor`, `pi-intercom`, `pi-subagents`, `pi-messenger`, and Herdr. Mentioning a package does not approve adding it as a dependency or merging its whole codebase.
|
||||
|
||||
Reuse the supervisor prompt and working supervision behaviour where possible. Avoid an additional orchestration framework, council, or growing collection of mechanical review gates. Routine implementation judgement belongs to the supervisor; material choices requiring the user's knowledge or preferences still come back to the user.
|
||||
|
||||
Keep `/goals` subcommands without required `--` prefixes. The feature should be testable in an ordinary Pi session through this branch.
|
||||
|
||||
Make worker/supervisor navigation direct. The user finds Fleet hard to parse. A named supervisor pane, easy switching, and a side-by-side view are preferable to requiring navigation through a fleet dashboard. A headless-run inspector is not the same thing as the real supervisor session.
|
||||
|
||||
## Approved implementation qualifications
|
||||
|
||||
Compose the existing supervisor through narrow APIs, using native Pi sessions and Herdr panes.
|
||||
Human Ready is the approval: wait for successful bootstrap and pairing, then start work once. The
|
||||
initial supervisor view may steer but is not a second mandatory plan-approval gate.
|
||||
|
||||
A demonstrably small fork (at most 20k tokens) skips the initial compaction call and reports that fact.
|
||||
Unknown size does not justify skipping; other compaction failures remain visible.
|
||||
|
||||
If a recorded pane is missing, locate the supervisor before reopening its saved session. Missing pane
|
||||
identity is not proof of process exit. Automatic recreation or an additional confirmation UI is
|
||||
deferred for the branch trial, as approved by the parent during implementation.
|
||||
|
||||
<!-- Written by Pi from the user's conversation; integration proposals are not implementation receipts. -->
|
||||
Generated
+2086
-73
File diff suppressed because it is too large
Load Diff
+30
-14
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@wassname2/pi-goals",
|
||||
"version": "0.1.1",
|
||||
"description": "One .pi/plan.md: set goals in plan mode, work them, sign off only when a read-only judge checks the evidence.",
|
||||
"version": "0.2.2",
|
||||
"description": "One plan file per session: set goals in plan mode, work them, sign off only when a read-only judge checks the evidence.",
|
||||
"author": "wassname",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -20,37 +20,53 @@
|
||||
"evidence",
|
||||
"judge"
|
||||
],
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-coding-agent": "^0.79.0",
|
||||
"@earendil-works/pi-tui": "*",
|
||||
"@sinclair/typebox": "latest"
|
||||
"peerDependencies": {
|
||||
"@earendil-works/pi-coding-agent": "*",
|
||||
"typebox": "*"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"README.md"
|
||||
"README.md",
|
||||
"THIRD_PARTY_NOTICES.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"prepublishOnly": "npm run lint && npm run typecheck && npm run test",
|
||||
"test": "vitest run",
|
||||
"test": "vitest run && npm run test:supervisor",
|
||||
"test:rpc": "vitest run test/rpc-review.test.ts",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
||||
"lint": "biome check src/ test/",
|
||||
"lint:fix": "biome check --fix src/ test/"
|
||||
"lint:fix": "biome check --fix src/ test/",
|
||||
"test:supervisor": "node --import tsx --test test/internal-supervisor/*.test.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@earendil-works/pi-coding-agent": "^0.84.1",
|
||||
"@types/node": "^20.0.0",
|
||||
"typebox": "^1.3.7",
|
||||
"typescript": "^5.0.0",
|
||||
"@biomejs/biome": "^2.4.8",
|
||||
"vitest": "^4.0.18"
|
||||
"vitest": "^4.0.18",
|
||||
"tsx": "^4.20.0"
|
||||
},
|
||||
"pi": {
|
||||
"extensions": [
|
||||
"./src/index.ts"
|
||||
],
|
||||
"image": "https://cdn.jsdelivr.net/gh/wassname/pi-goals@main/media/screenshot.png"
|
||||
}
|
||||
"image": "https://cdn.jsdelivr.net/gh/wassname/pi-goals@main/media/screenshot.png",
|
||||
"skills": [
|
||||
"./node_modules/pi-intercom/skills"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@sting8k/pi-vcc": "0.5.0",
|
||||
"pi-intercom": "0.10.0"
|
||||
},
|
||||
"bundledDependencies": [
|
||||
"pi-intercom",
|
||||
"@sting8k/pi-vcc"
|
||||
]
|
||||
}
|
||||
|
||||
+871
-141
File diff suppressed because it is too large
Load Diff
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
/** The bundled package exports an extension factory; its TypeScript source is loaded by Pi. */
|
||||
export default function intercom(pi: ExtensionAPI): void;
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { ExtensionAPI, ExtensionContext, SessionStartEvent } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
/** Compose the bundled transport only after every installed extension has had a chance to register.
|
||||
* An existing Intercom owns its own lifecycle. The fallback owns just its initial session_start;
|
||||
* all subsequent hooks use Pi's public extension API normally (reload creates a fresh instance).
|
||||
*/
|
||||
export async function loadBundledIntercom(pi: ExtensionAPI, event: SessionStartEvent, ctx: ExtensionContext): Promise<void> {
|
||||
if (pi.getAllTools().some(tool => tool.name === "intercom")) throw new Error("Installed Intercom has no extension channel. Enable/update that installation and reload; no second Intercom was loaded.");
|
||||
const starts: Array<(event: SessionStartEvent, ctx: ExtensionContext) => unknown> = [];
|
||||
const api = { ...pi, on(name: string, handler: (...args: any[]) => any) {
|
||||
if (name === "session_start") starts.push(handler);
|
||||
else pi.on(name as Parameters<ExtensionAPI["on"]>[0], handler);
|
||||
} } as ExtensionAPI;
|
||||
const { default: intercom } = await import("pi-intercom");
|
||||
intercom(api);
|
||||
for (const start of starts) await start(event, ctx);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
export interface BackgroundState { quiet: boolean; description: string }
|
||||
|
||||
/** Optional trackers are queried afresh. Absent tools mean no tracked work; an installed
|
||||
* tracker that cannot report stays unknown. Detached/unregistered work is not OS-wide idleness. */
|
||||
export async function backgroundState(pi: any): Promise<BackgroundState> {
|
||||
const tools = pi.getAllTools();
|
||||
const hasProcesses = tools.some((tool: any) => tool.name === "process");
|
||||
const hasSubagents = tools.some((tool: any) => tool.name === "subagent");
|
||||
let processes: unknown;
|
||||
pi.events.emit("processes:request:list", { reply: (value: unknown) => { processes = value; } });
|
||||
const rows = Array.isArray(processes) ? processes : !hasProcesses && processes === undefined ? [] : null;
|
||||
const processKnown = rows?.every(p => p && ["running", "terminating", "terminate_timeout", "exited", "killed"].includes(p.status));
|
||||
const activeProcesses = processKnown ? rows!.filter(p => !["exited", "killed"].includes(p.status)).length : null;
|
||||
let activeSubagents: number | null = hasSubagents ? null : 0;
|
||||
if (hasSubagents) {
|
||||
const requestId = randomUUID();
|
||||
activeSubagents = await new Promise<number | null>(resolve => {
|
||||
let unsubscribe: unknown;
|
||||
const finish = (value: number | null) => { clearTimeout(timer); if (typeof unsubscribe === "function") unsubscribe(); resolve(value); };
|
||||
const timer = setTimeout(() => finish(null), 2_000);
|
||||
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-supervise" } });
|
||||
});
|
||||
}
|
||||
return {
|
||||
quiet: activeProcesses === 0 && activeSubagents === 0,
|
||||
description: `processes: ${activeProcesses ?? "unknown (provider unavailable)"}; subagents: ${activeSubagents ?? "unknown (provider unavailable)"}; unregistered detached work is not tracked`,
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* Every word the supervisor session reads, in the order it reads them, so this file is the run:
|
||||
*
|
||||
* 1. loadSupervisorPrompt, the policy, read once at /supervise
|
||||
* 2. BRIEF, sent once at pairing, carrying that policy
|
||||
* 3. TOOL_*, the three verdicts, in context at every model call because tools always are
|
||||
* 4. REVIEW_NUDGE, sent with every view, and short because 1 to 3 already said the rest
|
||||
* 5. NO_GOAL and DONE_BLOCKED, refusals, read only when a tool is refused
|
||||
* 6. DEFAULT_SUPERVISOR_PROMPT, the policy used when no SUPERVISOR.md exists
|
||||
*/
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
/**
|
||||
* Same precedence as @monotykamary/pi-supervisor, so an existing SUPERVISOR.md keeps working:
|
||||
* <cwd>/.pi/SUPERVISOR.md, then <agent dir>/SUPERVISOR.md, then the default below.
|
||||
*
|
||||
* getAgentDir is pi's own, so a profile that moves the agent dir moves this with it. That matters:
|
||||
* the only SUPERVISOR.md on this machine lives in a profile, not in ~/.pi/agent.
|
||||
*/
|
||||
export function loadSupervisorPrompt(cwd: string): { prompt: string; source: string } {
|
||||
for (const path of [join(cwd, ".pi", "SUPERVISOR.md"), join(getAgentDir(), "SUPERVISOR.md")]) {
|
||||
if (existsSync(path)) return { prompt: readFileSync(path, "utf-8").trim(), source: path };
|
||||
}
|
||||
return { prompt: DEFAULT_SUPERVISOR_PROMPT, source: "built-in" };
|
||||
}
|
||||
|
||||
/**
|
||||
* Put in the supervisor's context when pairing, so it knows its job before the first view arrives.
|
||||
*
|
||||
* The last line has been wrong three times. "Reply with exactly: watching" taught a text answer,
|
||||
* and deepseek-v4-flash then answered two real views with the plain word "wait" and no tool call
|
||||
* (session 019ff4eb-c66c, 2026-08-12). Ordering a let_it_run call taught the opposite: with no view
|
||||
* to read, one supervisor answered its own brief 98 times in a row (019ffa5f, 2026-08-13). Asking
|
||||
* for no answer at all still got two calls, because the brief arrived as a user message and a user
|
||||
* message is a turn. It now arrives without one, so the view is the first thing there is to answer.
|
||||
*/
|
||||
export const BRIEF = (policy: string, goal: string, worker: string) =>
|
||||
`${policy}
|
||||
|
||||
You are now supervising the pi session "${worker}".
|
||||
|
||||
The goal is between the tags below, exactly as the human typed it. Nothing outside the tags is
|
||||
part of the goal. A multi-line goal appears as a one-line locator in ordinary views. Its full text
|
||||
returns after a goal change, reload, compaction, and before every fifth review.
|
||||
|
||||
<goal>
|
||||
${goal || "not given, so infer it from the first view you receive and call set_goal"}
|
||||
</goal>
|
||||
|
||||
First visibly give a brief progress assessment and useful advice or perspective grounded in the view.
|
||||
Then use let_it_run if on course, steer for a concrete correction, or done when finished.
|
||||
Do not fill approval forms or merely report delivery. JSON from an older policy is not needed.
|
||||
|
||||
You see the worker twice: when it stops, and on a check in while it is still working. Each view
|
||||
carries only what is new since your last look, so read it against what you already know rather
|
||||
than expecting the whole session again.
|
||||
|
||||
The view names the worker's model and how full its context is. A small or fast model needs one
|
||||
small step per instruction. A worker near the top of its context is about to compact, so tell it
|
||||
to write down what matters before it loses the detail.
|
||||
|
||||
Supervise until the agreed result is delivered and inspected, or the human stops supervision.
|
||||
Do not abandon unfinished work or prolong completed work for optional polish. Respect permission
|
||||
and spending limits; autonomous supervision is not an unlimited budget.
|
||||
|
||||
A human message is new direction, not an automatic handover. Respect an explicit human pause or
|
||||
required approval: do not steer the paused work until authorized. Keep independent authorized work
|
||||
moving. Later views still deserve assessment; a question or pause is not a reason to discard them.
|
||||
|
||||
Every view says how long the worker has gone with no new turn. A worker that has produced nothing
|
||||
for a long time is stuck, or waiting for you, or in one command that will not return. Say which
|
||||
one you think it is, and use the number rather than guessing from the turns.
|
||||
|
||||
The worker sends its first view as it pairs. It is below this message, and it is what you
|
||||
answer.`;
|
||||
|
||||
/**
|
||||
* Sent on a resume or a /reload that finds the pairing still alive.
|
||||
*
|
||||
* Short on purpose: the policy is already in the transcript above it. Only the answer shape and the
|
||||
* goal repeat, because those are what a supervisor drops first, and because a /reload is how a
|
||||
* changed prompt reaches a running session. Without this, fixing the wording of the brief needs
|
||||
* /supervise stop and a fresh pairing, which throws away the supervisor's memory of its own steers.
|
||||
*/
|
||||
export const REANCHOR = (goal: string, rounds: number) =>
|
||||
`Supervising again, after a reload or a restart.
|
||||
|
||||
<goal>
|
||||
${goal || "not set"}
|
||||
</goal>
|
||||
|
||||
${rounds} instructions so far.
|
||||
|
||||
A view of the worker follows. Give a brief visible assessment and perspective, then use steer, done
|
||||
or let_it_run. Only steer sends an instruction; your visible assessment matters to the human too.`;
|
||||
|
||||
/** Sent when the human runs /supervise goal, so the supervisor does not judge against the old one. */
|
||||
export const GOAL_CHANGED = (goal: string) =>
|
||||
`The human changed the goal. From now on judge the worker against what is between the tags,
|
||||
and against nothing else:
|
||||
|
||||
<goal>
|
||||
${goal}
|
||||
</goal>
|
||||
|
||||
A fresh view follows. Answer it with one tool call: steer, done or let_it_run.`;
|
||||
|
||||
/**
|
||||
* The three verdicts. These live in the tool descriptions, which the API sends at every model call,
|
||||
* so they are the only instructions here that a supervisor compaction cannot lose.
|
||||
*/
|
||||
export const TOOL_LET_IT_RUN =
|
||||
"Use when the current worker view gives quoted evidence that no instruction is needed."
|
||||
+ " The call sends no message to the worker. Call it once, then end the current supervisor response."
|
||||
// Repeated here because a tool description survives a compaction and the brief does not. The
|
||||
// live failure was a let_it_run reasoned "human is actively directing", two hours before dawn.
|
||||
+ " A human message does not end supervision. Respect explicit human pauses and required decisions; assess new views without restarting paused work.";
|
||||
|
||||
/**
|
||||
* How a look ends, and it must appear in every verdict's result.
|
||||
*
|
||||
* A tool result reads as a prompt to act again. A turn ends only when the assistant writes text and
|
||||
* calls no tool, so a result that does not name that exit leaves another tool call as the only move.
|
||||
* The let_it_run result used to say "Say nothing more until the next view arrives", which forbids the
|
||||
* exit outright: session 019ffa73 answered with a second let_it_run on all sixteen looks before 11:05Z
|
||||
* and aborted every one. The steer result said nothing about ending, and cost a spare let_it_run on
|
||||
* 22 of 22 steers in the fifteen hours after.
|
||||
*/
|
||||
export const END_TURN =
|
||||
`End the current supervisor response with a brief visible assessment of progress and useful perspective, unless you already gave it. Make no further tool call.`;
|
||||
|
||||
export const LET_IT_RUN_ACK = (reason: string, workerStopped = false) =>
|
||||
`No supervisor instruction was sent for the current worker view. Supervisor-provided reason, not independently verified: ${reason}\n\nThe supervisor has completed its verdict for the current worker view. ${END_TURN}
|
||||
${workerStopped ? STOPPED_WARNING : "A later worker view starts the next supervisor review."}`;
|
||||
|
||||
/**
|
||||
* Added to the let_it_run result when the view said the worker had stopped.
|
||||
*
|
||||
* Session 019ffa73, 2026-08-14: the worker stopped, the supervisor answered let_it_run "waiting for
|
||||
* the worker to re-queue", and both sat still for two and a half hours. A stopped worker does not
|
||||
* resume on its own, so letting it run leaves it stopped. The timer now looks again either way, and
|
||||
* this says why that look will show the same thing.
|
||||
*/
|
||||
export const STOPPED_WARNING =
|
||||
`The current worker view reports that worker execution stopped. A stopped worker does not resume
|
||||
without a new user or supervisor message. If the goal remains unmet, send a concrete continuation
|
||||
instruction unless a verified dependency or explicit human pause prevents it. Respect the pause;
|
||||
keep independent authorized work moving. A later worker view will report the worker state.`;
|
||||
|
||||
/** The answer to a second let_it_run in one look. Costs a round trip and no error line. */
|
||||
export const LET_IT_RUN_AGAIN =
|
||||
`The supervisor already recorded a verdict for the current worker view. This second let_it_run call
|
||||
sent no instruction. ${END_TURN}`;
|
||||
|
||||
/** The answer after a supervisor directive is sent. A repeat warning is appended after it. */
|
||||
export const STEER_ACK = (round: number, workerId: string) =>
|
||||
`Supervisor instruction ${round} was sent to worker session ${workerId}. Worker receipt and execution
|
||||
are not confirmed. The supervisor has completed its verdict for the current worker view. ${END_TURN}`;
|
||||
export const TOOL_STEER =
|
||||
"Send one concrete next action and its purpose toward the agreed goal. Use it to resume authorized work, request a needed check, or correct drift. A recap alone does not send an instruction. Do not interrupt productive work or repeat ineffective steering without changing the approach. Worker receipt and execution require a later worker view.";
|
||||
export const TOOL_REVIEW_GOAL =
|
||||
"Judge the presented goal against the user's intended outcome and discriminator, not merely task completion or file existence. Approve only when the inspected evidence warrants it; otherwise give needs_work with the next useful work/check, or needs_user for a specific unresolved human decision. This records your judgment, not proof from mechanical checks. The fresh evidence judge still runs independently; this does not end supervision of remaining goals.";
|
||||
export const TOOL_DONE =
|
||||
"Finish supervision after inspecting the agreed result. For a plan, every non-cancelled goal needs a CompleteGoal record, not a manual checkbox. Accepted inconclusive is fail-forward, not proof of success; disclose that distinction. Quote evidence for any completion claim.";
|
||||
|
||||
/**
|
||||
* Sent with every view, so it is deliberately short.
|
||||
*
|
||||
* What used to be here and is now sent once: the verdict rules (BRIEF, and the tool descriptions,
|
||||
* which survive a compaction) and the instructions already sent (the supervisor's own steer calls
|
||||
* are in its context; the steer tool warns about a repeat when it happens). A multi-line goal
|
||||
* stays as a one-line locator inside the view.
|
||||
*
|
||||
* A check in is not a decision point. Interrupting a working agent is expensive and usually wrong,
|
||||
* so the two triggers ask for different things.
|
||||
*/
|
||||
/**
|
||||
* The two openers, and the test the context pruner uses to find a view it can drop.
|
||||
*
|
||||
* They are constants because two things read them: the nudge that writes a view, and the pruner
|
||||
* that later collapses it. Matching the prose in two places would let them drift silently, and a
|
||||
* pruner that stops recognising views just quietly stops working.
|
||||
*/
|
||||
export const VIEW_STOPPED = "The worker stopped.";
|
||||
export const VIEW_CHECKIN = "Checking in on the worker, which is still going.";
|
||||
export const isViewText = (text: string) => text.startsWith(VIEW_STOPPED) || text.startsWith(VIEW_CHECKIN);
|
||||
|
||||
/** What an old view is replaced with. Short, and it says where the content went. */
|
||||
export const VIEW_PRUNED =
|
||||
"[an earlier view of the worker, dropped once you had judged it. Your verdict on it follows.]";
|
||||
|
||||
export const REVIEW_NUDGE = (view: string, rounds: number, stopped: boolean) =>
|
||||
stopped
|
||||
? `${VIEW_STOPPED}
|
||||
|
||||
${view}
|
||||
|
||||
${rounds} instructions so far. Judge the actual result against the outcome and discriminator; say your assessment briefly. If work remains, investigate the stop and steer a useful authorized continuation with its purpose. A recap alone does not restart work. Respect explicit human pauses; name real dependencies and how to observe their resolution. Manual ticks are claims. Use let_it_run only if no instruction helps; done needs the agreed result and recorded sign-offs.`
|
||||
: `${VIEW_CHECKIN}
|
||||
|
||||
${view}
|
||||
|
||||
You are the supervisor, not the worker. Is the work on track toward the user's intended outcome?
|
||||
Give a brief visible judgment. Use let_it_run when on course; steer only when the evidence
|
||||
shows drift, mistaken assumptions or wasted effort. If this is
|
||||
the Ready handoff, send a concrete starting instruction unless work has already begun. Review plan
|
||||
changes against user intent, preserving authorized changes rather than treating every edit as failure.`;
|
||||
|
||||
/** Refusal shown when done is called while the worker still has work running. */
|
||||
export const DONE_BLOCKED = (what: string) =>
|
||||
`Cannot finish: the worker still has work running (${what}). Wait for the next view.`;
|
||||
|
||||
/** Refusal shown when the supervisor tries to steer with no goal set. */
|
||||
export const NO_GOAL = `No goal is set, so you must not steer or finish. Inventing a task is worse
|
||||
than doing nothing. Either call set_goal with the goal you infer from the worker's view, which
|
||||
tells the human what you chose, or reply in plain text asking them for it. Your reply reaches
|
||||
their phone.`;
|
||||
|
||||
/**
|
||||
* Default supervisor prompt. A project SUPERVISOR.md overrides it, same as @monotykamary/pi-supervisor.
|
||||
* Unlike that extension there is no JSON verdict to parse, because the verdict is a tool call.
|
||||
*/
|
||||
export const DEFAULT_SUPERVISOR_PROMPT = `You are the visible, read-only supervisor of another Pi session.
|
||||
The worker carries implementation detail; you retain user intent, decisions and high-level judgment.
|
||||
At startup and after compaction, read applicable AGENTS.md instructions and relevant skills. Do not
|
||||
assume a particular project or workflow. Infer ordinary implementation details without replacing the
|
||||
agreed outcome or inventing restrictions. Make consequential uncertainty and disagreement visible;
|
||||
respect reasonable user preferences without making the user repeatedly justify them.
|
||||
Supervise autonomously until the agreed goal is achieved and you have inspected the actual result.
|
||||
Identify the missing user-visible outcome and steer the next useful action through to delivery.
|
||||
Approval records support the work; they are not the outcome. Seek justified confidence, not
|
||||
certainty at any cost. Investigate uncertainty with the cheapest useful check, then decide.
|
||||
Do not prolong completed work for optional polish.
|
||||
|
||||
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. Never repeat a steer that had no
|
||||
effect: inspect what happened and change the approach. Keep independent authorized work moving
|
||||
when it does not depend on the blocker. A verified external dependency can justify waiting; it
|
||||
does not make an unfinished goal complete. Identify what event resumes progress and how to observe it.
|
||||
|
||||
Resolve technical choices within agreed scope. Steer one concrete next action when the worker is
|
||||
idle with unfinished work. If useful work is running, do not invent work or repeat instructions
|
||||
awaiting execution. Respect explicit human pauses and permission limits, including credentials and
|
||||
spending. Escalate only a specific unresolved human decision after checking what is already
|
||||
authorized. New worker views, including answers to earlier questions, still need your judgment;
|
||||
do not restart paused work without authorization or widen scope to evade a blocker.
|
||||
|
||||
At each review give a brief visible assessment: what the evidence shows, how work is tracking, and
|
||||
your judgment about the next step. Add perspective, not unchanged status or delivery receipts.
|
||||
Distinguish observations from guesses. Inspect, judge and steer; never execute work, delegate it,
|
||||
schedule it, or mutate files. Let the worker produce both the artifact and its verification output.
|
||||
|
||||
Ground consequential judgments in verbatim evidence with source paths and enough context to check
|
||||
the interpretation. Read the actual deliverable against the user's goal. A worker summary, passing
|
||||
tests, or a checked box alone do not establish success. Repeated summaries are not independent
|
||||
evidence. Missing evidence stays unknown until inspected. Investigate contradictions and surprising
|
||||
results with checks that distinguish plausible explanations. Watch for weakened tests, fabricated
|
||||
measurements, partial runs reported as full ones, and logs that do not demonstrate real execution.
|
||||
Say what evidence would change your mind.
|
||||
|
||||
The current canonical plan is the source of truth, subject to newer human direction. Review plan
|
||||
changes for drift and steer corrections when warranted. Manual completion checkboxes are claims,
|
||||
not sign-off. CompleteGoal records the retained supervisor's checkpoint and a fresh independent
|
||||
judge's result. An accepted-inconclusive record preserves fail-forward but is not verified success;
|
||||
state the uncertainty rather than describing it as conclusive. Git status is review context, not an
|
||||
acceptance gate: uncommitted changes and ignored output files may be legitimate evidence. Do not
|
||||
require cleanup or a commit unless the agreed goal requires it. Inspect cited paths directly.
|
||||
|
||||
Use steer for useful corrections, let_it_run when no instruction is needed, and done when the
|
||||
agreed work is finished with the required sign-offs. Follow the tools' requirements without letting
|
||||
bookkeeping replace delivery. Keep visible judgments brief and useful. -- Pi/OpenAI`;
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* What the two sessions say to each other over the pi-intercom extension channel.
|
||||
*
|
||||
* The channel never enters a transcript and never starts a turn, so each side triggers its own
|
||||
* turn locally with pi.sendUserMessage after it receives one of these.
|
||||
*/
|
||||
|
||||
import { type SupervisorBinding, validBinding } from "../../supervisor.js";
|
||||
import { MAX_VIEW_BYTES } from "./view.js";
|
||||
|
||||
/** Compact completion bookkeeping from the worker, bound to the exact plan it observed. */
|
||||
export interface PlanCompletion {
|
||||
planHash: string;
|
||||
total: number;
|
||||
pending: number;
|
||||
inconclusive: number;
|
||||
}
|
||||
function validCompletion(value: any): value is PlanCompletion {
|
||||
return !!value && typeof value.planHash === "string" && [value.total, value.pending, value.inconclusive].every(n => Number.isSafeInteger(n) && n >= 0) && value.pending + value.inconclusive <= value.total;
|
||||
}
|
||||
|
||||
export interface GoalReview {
|
||||
requestId: string;
|
||||
bindingId: string;
|
||||
goal: string;
|
||||
planHash: string;
|
||||
}
|
||||
/** Frozen worker context travels only with the request, never with a decision. */
|
||||
export interface GoalReviewRequest extends GoalReview {
|
||||
view?: string;
|
||||
}
|
||||
export function reviewIdentity({ requestId, bindingId, goal, planHash }: GoalReview): GoalReview {
|
||||
return { requestId, bindingId, goal, planHash };
|
||||
}
|
||||
|
||||
/** Intercom measures serialized payload bytes, including JSON escapes and checkpoint identity. */
|
||||
export function goalReviewWire(to: string, review: GoalReview, view: string): PlanWire {
|
||||
const payload = { t: "goal_review" as const, to, ...reviewIdentity(review), view };
|
||||
const fits = (value: string) => Buffer.byteLength(value, "utf8") <= MAX_VIEW_BYTES && Buffer.byteLength(JSON.stringify({ ...payload, view: value }), "utf8") <= 16 * 1024;
|
||||
if (fits(view)) return payload;
|
||||
const marker = "\n[checkpoint view cut to fit the channel; inspect the worker source session for omitted detail]\n";
|
||||
if (!fits(marker)) throw new Error("Goal checkpoint identity is too large for the 16 KiB Intercom channel");
|
||||
const chars = Array.from(view);
|
||||
let low = 0; let high = chars.length;
|
||||
while (low < high) {
|
||||
const middle = Math.ceil((low + high) / 2);
|
||||
if (fits(chars.slice(0, middle).join("") + marker)) low = middle;
|
||||
else high = middle - 1;
|
||||
}
|
||||
return { ...payload, view: chars.slice(0, low).join("") + marker };
|
||||
}
|
||||
|
||||
export interface GoalDecision extends GoalReview {
|
||||
decision: "approve" | "needs_work" | "needs_user";
|
||||
reason: string;
|
||||
}
|
||||
export type PlanWire =
|
||||
| { t: "plan_hello" | "plan_hello_ack"; to: string; bindingId: string; role: "worker" | "supervisor"; sessionFile: string; paused?: boolean; pauseId?: string }
|
||||
| { t: "plan_failed"; to: string; bindingId: string; sessionFile: string; reason: string }
|
||||
| { t: "plan_pause"; to: string; bindingId: string; exit: boolean; pauseId: string }
|
||||
| { t: "plan_resume"; to: string; bindingId: string; requestId: string; planHash: string; pauseId?: string }
|
||||
| { t: "plan_resumed"; to: string; bindingId: string; requestId: string; accepted: boolean }
|
||||
| ({ t: "goal_review"; to: string } & GoalReviewRequest)
|
||||
| ({ t: "goal_decision"; to: string } & GoalDecision)
|
||||
| { t: "goal_cancel"; to: string; requestId: string; bindingId: string }
|
||||
| { t: "plan_activate" | "plan_stop"; to: string; bindingId: string };
|
||||
export function validPlanWire(value: any): value is PlanWire {
|
||||
if (!value || typeof value.to !== "string" || typeof value.bindingId !== "string") return false;
|
||||
if (value.t === "plan_hello" || value.t === "plan_hello_ack") return ["worker", "supervisor"].includes(value.role) && typeof value.sessionFile === "string" && (value.paused === undefined || typeof value.paused === "boolean") && (value.pauseId === undefined || typeof value.pauseId === "string");
|
||||
if (value.t === "plan_failed") return typeof value.sessionFile === "string" && typeof value.reason === "string" && value.reason.length > 0 && value.reason.length <= 2000;
|
||||
if (value.t === "plan_pause") return typeof value.exit === "boolean" && typeof value.pauseId === "string";
|
||||
if (value.t === "plan_resume") return typeof value.requestId === "string" && typeof value.planHash === "string" && (value.pauseId === undefined || typeof value.pauseId === "string");
|
||||
if (value.t === "plan_resumed") return typeof value.requestId === "string" && typeof value.accepted === "boolean";
|
||||
if (value.t === "plan_activate" || value.t === "plan_stop") return true;
|
||||
if (typeof value.requestId !== "string") return false;
|
||||
if (value.t === "goal_cancel") return true;
|
||||
if (typeof value.goal !== "string" || typeof value.planHash !== "string") return false;
|
||||
if (value.t === "goal_review") return value.view === undefined || (typeof value.view === "string" && Buffer.byteLength(value.view, "utf8") <= MAX_VIEW_BYTES);
|
||||
return value.t === "goal_decision" && ["approve", "needs_work", "needs_user"].includes(value.decision) && typeof value.reason === "string";
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const NAMESPACE = "wassname/pi-intercom-supervisor/v1";
|
||||
|
||||
// No round cap, no budget, on purpose. Supervision runs until the human stops it with
|
||||
// /supervise stop, because premature stopping is the failure this whole thing exists to prevent
|
||||
// (wassname's SUPERVISOR.md, citing arXiv:2410.07095: 8.7% vs 0.8% on MLE-bench).
|
||||
|
||||
export type Wire = (PlanWire
|
||||
/** Roll call, broadcast, so "to" is the wildcard rather than a session. Only /supervise sends it. */
|
||||
| { t: "who"; to: "*" }
|
||||
/** The answer to a roll call: I load this extension, I am free, and I am not a child run. */
|
||||
| { t: "here"; to: string }
|
||||
| { t: "pair"; to: string; goal: string; plan?: SupervisorBinding }
|
||||
| { t: "paired"; to: string; plan?: SupervisorBinding }
|
||||
| { t: "goal"; to: string; goal: string }
|
||||
/** stopped: the worker settled, so this is a decision point. false: a check in mid-turn. */
|
||||
| { t: "view"; to: string; view: string; stopped: boolean; refreshed?: boolean; completion?: PlanCompletion }
|
||||
/** Supervisor asks for a view now. Its own turn cannot make one: the worker publishes them. */
|
||||
| { t: "look"; to: string }
|
||||
| { t: "directive"; to: string; text: string }
|
||||
| { t: "done"; to: string; reason: string }
|
||||
| { t: "unpair"; to: string }) & { bindingId?: string };
|
||||
|
||||
/** Validates the field each kind carries, so a malformed peer cannot inject "[supervisor] undefined". */
|
||||
export function isWire(payload: unknown): payload is Wire {
|
||||
if (typeof payload !== "object" || payload === null) return false;
|
||||
if (validPlanWire(payload)) return true;
|
||||
const { t, to, goal, view, stopped, refreshed, text, reason, plan, completion } = payload as Record<string, unknown>;
|
||||
if ((t === "pair" || t === "paired") && plan !== undefined && !validBinding(plan)) return false;
|
||||
if (typeof to !== "string") return false;
|
||||
if (t === "pair" || t === "goal") return typeof goal === "string";
|
||||
if (t === "view") return typeof view === "string" && typeof stopped === "boolean" && (refreshed === undefined || typeof refreshed === "boolean") && (completion === undefined || validCompletion(completion));
|
||||
if (t === "directive") return typeof text === "string" && text.trim().length > 0;
|
||||
if (t === "done") return typeof reason === "string";
|
||||
return t === "unpair" || t === "paired" || t === "look" || t === "who" || t === "here";
|
||||
}
|
||||
|
||||
/**
|
||||
* Words two instructions share, over the words either uses. Stopwords and short words dropped.
|
||||
*
|
||||
* Six remembered instructions do not stop repetition, because the same order rephrased reads as
|
||||
* new. This catches the rephrasing that shares vocabulary; it cannot catch a true paraphrase.
|
||||
*/
|
||||
export function overlap(a: string, b: string): number {
|
||||
const words = (s: string) =>
|
||||
new Set(
|
||||
s
|
||||
.toLowerCase()
|
||||
.split(/[^a-z0-9_./-]+/)
|
||||
.filter((w) => w.length > 3 && !STOPWORDS.has(w)),
|
||||
);
|
||||
const [x, y] = [words(a), words(b)];
|
||||
if (!x.size || !y.size) return 0;
|
||||
const shared = [...x].filter((w) => y.has(w)).length;
|
||||
return shared / (x.size + y.size - shared);
|
||||
}
|
||||
|
||||
const STOPWORDS = new Set([
|
||||
"then", "with", "that", "this", "from", "into", "your", "each", "have", "then", "should", "please",
|
||||
"make", "sure", "also", "them", "they", "what", "when", "here", "there", "which", "will", "would",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Two instructions sharing this much vocabulary get flagged back to the supervisor.
|
||||
*
|
||||
* Measured on rewordings of one instruction: about 0.44. On two different instructions: under 0.2.
|
||||
* A true paraphrase that shares no words scores 0 and slips through, so this is a floor on
|
||||
* repetition, not a bound.
|
||||
*/
|
||||
export const OVERLAP_WARN = 0.4;
|
||||
|
||||
export interface SuperviseState {
|
||||
role: "none" | "worker" | "supervisor";
|
||||
/** Intercom session ID of the other side. The broker stamps this, so it cannot be forged. */
|
||||
pairedId: string;
|
||||
goal: string;
|
||||
steerRounds: number;
|
||||
/** Recent steer texts, so the supervisor can see repetition after its own context is compacted. */
|
||||
recentSteers: string[];
|
||||
/** Optional pi-goals integration. Standalone supervision keeps its original policy. */
|
||||
plan?: SupervisorBinding;
|
||||
/** Supervisor bootstrap completed and the worker acknowledged this plan pairing. */
|
||||
planInitialized?: boolean;
|
||||
}
|
||||
|
||||
export const EMPTY_STATE: SuperviseState = {
|
||||
role: "none",
|
||||
pairedId: "",
|
||||
goal: "",
|
||||
steerRounds: 0,
|
||||
recentSteers: [],
|
||||
};
|
||||
|
||||
/** How many past steers to keep and show back. Enough to spot a loop, small enough to stay cheap. */
|
||||
export const STEER_MEMORY = 6;
|
||||
|
||||
/** Session entry type used to persist state, so a compaction or reload cannot reset the count. */
|
||||
export const STATE_ENTRY = "supervise-state";
|
||||
|
||||
/** Rebuild state from session entries. The last one written wins. */
|
||||
export function restoreState(entries: Array<{ type: string; customType?: string; data?: unknown }>): SuperviseState {
|
||||
let state = EMPTY_STATE;
|
||||
for (const entry of entries) {
|
||||
if (entry.type === "custom" && entry.customType === STATE_ENTRY && entry.data) {
|
||||
// Merge over the defaults so a record written before a field existed still loads.
|
||||
state = { ...EMPTY_STATE, ...(entry.data as Partial<SuperviseState>) };
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Child pi processes, so a settled worker with a subagent still running is not called finished.
|
||||
*
|
||||
* The in-session check only sees tool calls that never got a result. A subagent spawned as its own
|
||||
* process leaves no such trace, so the worker settles and the view looks quiet.
|
||||
*
|
||||
* This is a snapshot, not a wait. The original polls here for up to two minutes, and pi awaits the
|
||||
* settle handler (agent-session.js:330), so that holds the worker's own settle for the whole poll.
|
||||
* The loop already does the waiting: the supervisor sees the process listed, done is refused, and
|
||||
* it steers instead. That leaves the waiting in the transcript where you can read it.
|
||||
*
|
||||
* Ported from @monotykamary/pi-supervisor (MIT), src/subagent-detector.ts. Extension agnostic: it
|
||||
* does not matter who spawned them. Nothing is caught here, so a broken ps is loud.
|
||||
*/
|
||||
import { exec } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
interface PiProcess {
|
||||
pid: number;
|
||||
ppid: number;
|
||||
}
|
||||
|
||||
async function piProcesses(): Promise<PiProcess[]> {
|
||||
if (process.platform !== "darwin" && process.platform !== "linux") return [];
|
||||
const { stdout } = await execAsync(`ps -eo ppid,pid,comm | grep -E "\\bpi\\b" || true`);
|
||||
return stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => line.trim().split(/\s+/))
|
||||
.filter((parts) => parts.length >= 3 && parts[2] === "pi")
|
||||
.map((parts) => ({ ppid: Number(parts[0]), pid: Number(parts[1]) }));
|
||||
}
|
||||
|
||||
export async function childPiProcesses(): Promise<number[]> {
|
||||
return (await piProcesses()).filter((p) => p.ppid === process.pid).map((p) => p.pid);
|
||||
}
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
/** Typed boundary for pi-vcc 0.5.0's source-only API. Its own source uses older Pi message
|
||||
* unions and Intl.Segmenter types; do not typecheck that dependency as pi-goals source. */
|
||||
declare module "@sting8k/pi-vcc/src/core/summarize.ts" {
|
||||
export function compile(input: { messages: unknown[] }): string;
|
||||
}
|
||||
declare module "@sting8k/pi-vcc/src/core/normalize.ts" {
|
||||
export function normalize(messages: unknown[]): VccBlock[];
|
||||
interface VccBlock { type: string; [key: string]: unknown }
|
||||
}
|
||||
declare module "@sting8k/pi-vcc/src/extract/files.ts" {
|
||||
export function extractFiles(blocks: unknown[]): { modified: Set<string>; created: Set<string> };
|
||||
}
|
||||
declare module "@sting8k/pi-vcc/src/extract/commits.ts" {
|
||||
export function extractCommits(blocks: unknown[]): Array<{ hash?: string; message: string }>;
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* The worker view: what the supervisor judges from.
|
||||
*
|
||||
* Built from ctx.sessionManager.getBranch(), which already follows the live leaf path, so a fork
|
||||
* or a rewind cannot leave dead entries in here. There is no disk read and no cross-branch merge.
|
||||
*
|
||||
* The body is pi-vcc's compiler, the same algorithmic compactor the worker can run, called here on
|
||||
* the live messages with the worker's last compaction summary as previousSummary. So the view is
|
||||
* "compaction summary, merged with everything since". We add what a compactor has no reason to
|
||||
* track: unanswered tool calls and whether anything changed since the last review.
|
||||
*/
|
||||
|
||||
import { normalize } from "@sting8k/pi-vcc/src/core/normalize.ts";
|
||||
import { compile } from "@sting8k/pi-vcc/src/core/summarize.ts";
|
||||
import { extractCommits } from "@sting8k/pi-vcc/src/extract/commits.ts";
|
||||
import { extractFiles } from "@sting8k/pi-vcc/src/extract/files.ts";
|
||||
|
||||
const SUPERVISOR_PREFIX = "[supervisor] ";
|
||||
|
||||
/** Entry shapes we read. Only the fields this file touches, taken from real session jsonl. */
|
||||
export interface Block {
|
||||
type: string;
|
||||
id?: string;
|
||||
text?: string;
|
||||
/** Set on `type: "thinking"` blocks. Empty when the provider redacted the reasoning. */
|
||||
thinking?: string;
|
||||
name?: string;
|
||||
arguments?: Record<string, unknown>;
|
||||
}
|
||||
export interface AgentMsg {
|
||||
role: "user" | "assistant" | "toolResult" | string;
|
||||
content?: string | Block[];
|
||||
toolName?: string;
|
||||
toolCallId?: string;
|
||||
isError?: boolean;
|
||||
}
|
||||
export interface Entry {
|
||||
type: string;
|
||||
message?: AgentMsg;
|
||||
/** ISO, written on every entry by the session manager (core/session-manager.d.ts:21). */
|
||||
timestamp?: string;
|
||||
/** Written by whichever compactor the worker runs. VCC's summary lands here too. */
|
||||
summary?: string;
|
||||
tokensBefore?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Milliseconds since the worker last put a message in its session.
|
||||
*
|
||||
* The clock a stuck worker shows on, and the only one that reads the same for both ways of being
|
||||
* stuck: sitting at the prompt, and inside one command that never returns. A supervisor directive
|
||||
* does not reset it. Time since the last look measures the supervisor instead, and understates a
|
||||
* worker that stopped hours before.
|
||||
*/
|
||||
export function sinceLastTurn(entries: Entry[], now = Date.now()): number {
|
||||
const last = [...entries].reverse().find((e) =>
|
||||
e.type === "message" && e.timestamp && !(e.message?.role === "user" && textOf(e.message).startsWith(SUPERVISOR_PREFIX))
|
||||
);
|
||||
return last ? now - Date.parse(last.timestamp!) : 0;
|
||||
}
|
||||
|
||||
/** A duration a supervisor can read at a glance: 2h27m, 45m, 30s. */
|
||||
export function age(ms: number): string {
|
||||
const s = Math.max(0, Math.round(ms / 1000));
|
||||
if (s < 60) return `${s}s`;
|
||||
const m = Math.round(s / 60);
|
||||
return m < 60 ? `${m}m` : `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
|
||||
}
|
||||
|
||||
/** Extension channel payloads cap at 16 KiB, so the view must stay under it. */
|
||||
export const MAX_VIEW_BYTES = 15000;
|
||||
const GOAL_PREVIEW_CHARS = 160;
|
||||
|
||||
/** A long goal remains identifiable in every view without replaying its whole rubric. */
|
||||
export function goalPreview(goal: string): string {
|
||||
if (!goal.includes("\n")) return goal || "not set";
|
||||
const firstLine = goal.split("\n").find((line) => line.trim())?.trim() || "not set";
|
||||
return `${firstLine.slice(0, GOAL_PREVIEW_CHARS)} [...]`;
|
||||
}
|
||||
|
||||
function blocks(msg: AgentMsg): Block[] {
|
||||
return Array.isArray(msg.content) ? msg.content : [];
|
||||
}
|
||||
|
||||
function textOf(msg: AgentMsg): string {
|
||||
if (typeof msg.content === "string") return msg.content;
|
||||
return blocks(msg)
|
||||
.filter((b) => b.type === "text")
|
||||
.map((b) => b.text ?? "")
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool calls with no matching result on this branch. A settled worker with an unanswered
|
||||
* subagent call still has delegated work running, and "done" then means nothing.
|
||||
*/
|
||||
export function outstandingWork(entries: Entry[]): string[] {
|
||||
const called = new Map<string, string>();
|
||||
const answered = new Set<string>();
|
||||
for (const entry of entries) {
|
||||
const msg = entry.message;
|
||||
if (!msg) continue;
|
||||
for (const b of blocks(msg)) {
|
||||
if (b.type === "toolCall" && b.id) called.set(b.id, b.name ?? "?");
|
||||
}
|
||||
if (msg.role === "toolResult" && msg.toolCallId) answered.add(msg.toolCallId);
|
||||
}
|
||||
return [...called].filter(([id]) => !answered.has(id)).map(([, name]) => name);
|
||||
}
|
||||
|
||||
/** The summary written by whichever compactor the worker runs. Empty when it has not compacted. */
|
||||
export function compactionSummary(entries: Entry[]): string {
|
||||
let summary = "";
|
||||
for (const entry of entries) {
|
||||
if (entry.type === "compaction" && entry.summary) summary = entry.summary;
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* What the worker has changed: the files it wrote and the commits it made.
|
||||
*
|
||||
* Two reviews with the same key mean the last instruction produced neither. That is evidence for
|
||||
* the supervisor, not a rule: re-editing one file while a test still fails looks the same, and is
|
||||
* sometimes the right thing to be doing.
|
||||
*
|
||||
* Read from pi-vcc's extractor rather than from its rendered section, which caps the list at ten
|
||||
* paths and would freeze this key on any run long enough to matter.
|
||||
*/
|
||||
export function progressKey(entries: Entry[]): string {
|
||||
const blocks = normalize(messagesSince(entries) as any);
|
||||
const files = extractFiles(blocks);
|
||||
const commits = extractCommits(blocks).map((c) => c.hash ?? c.message);
|
||||
return [[...files.modified].sort(), [...files.created].sort(), commits].map((p) => p.join(",")).join("||");
|
||||
}
|
||||
|
||||
/**
|
||||
* Messages after the worker's last compaction.
|
||||
*
|
||||
* getBranch keeps the entries a compaction replaced, so handing every message to compile alongside
|
||||
* the summary would send the supervisor both copies and spend the byte budget twice. Supervisor
|
||||
* directives already live in the supervisor transcript, so exclude their worker-session echo.
|
||||
*/
|
||||
function messagesSince(entries: Entry[]): AgentMsg[] {
|
||||
const lastCompaction = entries.map((e) => e.type).lastIndexOf("compaction");
|
||||
return entries
|
||||
.slice(lastCompaction + 1)
|
||||
.filter((e) => e.type === "message" && e.message)
|
||||
.map((e) => e.message!)
|
||||
.filter((message) => message.role !== "user" || !textOf(message).startsWith(SUPERVISOR_PREFIX));
|
||||
}
|
||||
|
||||
/** What the caller records after a view goes out, and hands back as `since` on the next one. */
|
||||
export function turnsSince(entries: Entry[]): number {
|
||||
return messagesSince(entries).length;
|
||||
}
|
||||
|
||||
/** Reasoning blocks kept, newest first, and the tail kept from each. A block ends on a decision. */
|
||||
const THINKING_BLOCKS = 2;
|
||||
const THINKING_CHARS = 400;
|
||||
|
||||
/**
|
||||
* Keep the last few reasoning blocks by rewriting them as text, and let pi-vcc drop the rest.
|
||||
*
|
||||
* normalize() keeps only text and toolCall blocks from an assistant message, so reasoning never
|
||||
* reaches the supervisor although you see it on screen. Rewriting in place leaves each thought
|
||||
* next to the tool call it produced, which is the order you read a session in. A separate section
|
||||
* at the top of the view would divorce the thought from what it did.
|
||||
*
|
||||
* Only the last two, because one worker session here held 161 reasoning blocks and all of them
|
||||
* would make the view a second transcript. Everything older needs no work: pi-vcc drops it.
|
||||
*/
|
||||
function keepRecentThinking(msgs: AgentMsg[]): AgentMsg[] {
|
||||
const keep = new Set<string>();
|
||||
outer: for (let i = msgs.length - 1; i >= 0; i--) {
|
||||
const content = msgs[i].content;
|
||||
if (!Array.isArray(content)) continue;
|
||||
for (let j = content.length - 1; j >= 0; j--) {
|
||||
if (content[j].type !== "thinking" || !content[j].thinking) continue;
|
||||
keep.add(`${i}:${j}`);
|
||||
if (keep.size === THINKING_BLOCKS) break outer;
|
||||
}
|
||||
}
|
||||
if (!keep.size) return msgs;
|
||||
return msgs.map((msg, i) =>
|
||||
Array.isArray(msg.content)
|
||||
? {
|
||||
...msg,
|
||||
content: msg.content.map((b, j) =>
|
||||
keep.has(`${i}:${j}`) ? { type: "text", text: `(thinking) ${b.thinking!.slice(-THINKING_CHARS)}` } : b
|
||||
),
|
||||
}
|
||||
: msg
|
||||
);
|
||||
}
|
||||
|
||||
const VCC_SEPARATOR = "\n\n---\n\n";
|
||||
/** pi-vcc's section names, in the order formatSummary writes them (its format.ts). */
|
||||
const VCC_HEADERS = ["Session Goal", "Files And Changes", "Commits", "Outstanding Context", "User Preferences"];
|
||||
|
||||
/**
|
||||
* pi-vcc's compiled summary, split into its header sections and its brief transcript.
|
||||
*
|
||||
* compile() writes `sections + "\n\n---\n\n" + brief`, and drops either part when it is empty, so
|
||||
* all four combinations are possible. Get this wrong and the header block lands in the transcript,
|
||||
* where the byte cut eats the newest turns instead of the oldest.
|
||||
*/
|
||||
function vccSections(fresh: AgentMsg[]): { headers: string; brief: string } {
|
||||
// No previousSummary: compile's merge reads the fresh brief with briefOf, which finds nothing
|
||||
// when the fresh messages produced no header sections, and the newest turns vanish. The
|
||||
// compaction summary goes into the view above this instead, which loses nothing.
|
||||
//
|
||||
// compile appends a note telling the reader to call vcc_recall, which the supervisor does not
|
||||
// have. Matched on the tool name because wrapLongLines rewraps the note before we see it.
|
||||
const compiled = compile({ messages: keepRecentThinking(fresh) as any })
|
||||
.replace(/\n*-*\n*Use `vcc_recall`[\s\S]*$/, "")
|
||||
.trim();
|
||||
if (!VCC_HEADERS.some((h) => compiled.startsWith(`[${h}]`))) return { headers: "", brief: compiled };
|
||||
const at = compiled.indexOf(VCC_SEPARATOR);
|
||||
if (at < 0) return { headers: compiled, brief: "" };
|
||||
return { headers: compiled.slice(0, at), brief: compiled.slice(at + VCC_SEPARATOR.length) };
|
||||
}
|
||||
|
||||
export interface ViewInput {
|
||||
goal: string;
|
||||
status: string;
|
||||
/** Read-only source for detail omitted by the bounded overview. */
|
||||
sourceSession?: string;
|
||||
entries: Entry[];
|
||||
/**
|
||||
* Turns the supervisor has already been sent, from turnsSince() after the last view.
|
||||
*
|
||||
* The supervisor is a real session and keeps every view it has read, so re-sending the whole
|
||||
* transcript every time is a second copy of what it already has. This is a person glancing at a
|
||||
* screen: they read the new lines, not the scrollback. Past the compaction or a rewind this no
|
||||
* longer lines up, and the view says so and sends everything after the compaction.
|
||||
*/
|
||||
since?: number;
|
||||
/** Reviews in a row where progressKey did not change. 0 means something changed this time. */
|
||||
stale?: number;
|
||||
/** Child pi processes still running. A settled worker with one of these is still spending. */
|
||||
subagents?: number[];
|
||||
/**
|
||||
* The worker's model and how full its context is, from the intercom presence record.
|
||||
*
|
||||
* A supervisor steering a small fast model should give smaller steps than one steering a frontier
|
||||
* model, and a worker near the top of its context is about to compact and lose detail.
|
||||
*/
|
||||
model?: string;
|
||||
background?: string;
|
||||
planReview?: string;
|
||||
}
|
||||
|
||||
/** Render the view, and cut it to MAX_VIEW_BYTES so the broker cannot reject it. */
|
||||
export function buildView({ goal, status, entries, since = 0, stale = 0, subagents = [], model = "", sourceSession, background, planReview }: ViewInput): string {
|
||||
const messages = entries.filter((e) => e.type === "message" && e.message);
|
||||
const pending = outstandingWork(messages);
|
||||
const workerMessages = messagesSince(entries);
|
||||
const total = workerMessages.length;
|
||||
// A compaction or a rewind leaves the mark past the end. Restart from the compaction and say so,
|
||||
// otherwise the supervisor silently reads a slice of the wrong history.
|
||||
const restarted = since > total;
|
||||
const from = restarted ? 0 : since;
|
||||
const fresh = workerMessages.slice(from);
|
||||
const { headers, brief } = vccSections(fresh);
|
||||
const earlier = compactionSummary(entries);
|
||||
// VCC may omit an older user turn even when later work still depends on its authorization.
|
||||
// Keep the latest actual user direction distinct from summaries and supervisor echoes.
|
||||
const latestUser = [...entries].reverse().find(e => e.type === "message" && e.message?.role === "user" && textOf(e.message).trim() && !textOf(e.message).startsWith(SUPERVISOR_PREFIX));
|
||||
const direction = latestUser?.message ? textOf(latestUser.message) : "";
|
||||
const head = [
|
||||
...(direction ? [
|
||||
`# Latest user direction${latestUser?.timestamp ? ` (${latestUser.timestamp})` : ""}`,
|
||||
direction.length > 2000 ? `${direction.slice(0, 2000)}\n[user direction truncated; inspect the worker source session for full text]` : direction,
|
||||
"",
|
||||
] : []),
|
||||
// Short goals are the criterion on every review. A multi-line research rubric is reinserted
|
||||
// into the supervisor context at its own cadence, so this view carries only its locator.
|
||||
`<goal>`,
|
||||
goalPreview(goal),
|
||||
`</goal>`,
|
||||
``,
|
||||
`# Worker`,
|
||||
...(sourceSession ? [`source session: ${sourceSession} (read-only history for omitted detail)`] : []),
|
||||
...(model ? [`model: ${model}`] : []),
|
||||
`status: ${status}`,
|
||||
`turns: ${workerMessages.length}`,
|
||||
`tool calls with no result: ${pending.length ? pending.join(", ") : "none"}`,
|
||||
`child pi processes still running: ${subagents.length ? subagents.join(", ") : "none"}`,
|
||||
...(background ? [`tracked background work: ${background}`] : []),
|
||||
...(planReview ? [`plan review: ${planReview}`] : []),
|
||||
...(stale > 0 ? [`no new file or commit for ${stale} reviews in a row`] : []),
|
||||
``,
|
||||
// Sent when this view starts at the compaction boundary, which is the first view and every
|
||||
// view after the worker compacts. In between the supervisor already has it.
|
||||
...(from === 0 && earlier
|
||||
? [
|
||||
restarted ? `# The worker compacted, so this view restarts. Everything before it:` : `# Earlier work, from the worker's own compaction summary`,
|
||||
earlier.length > 6000 ? `${earlier.slice(0, 6000)}\n[worker compaction summary truncated; inspect the worker session for full evidence]` : earlier,
|
||||
``,
|
||||
]
|
||||
: []),
|
||||
...(headers ? [`# Files, commits and context, from the new turns only`, headers, ``] : []),
|
||||
from > 0 ? `# New turns since your last look (${total - from} of ${total})` : `# Turns so far`,
|
||||
].join("\n");
|
||||
|
||||
// Oldest brief lines go first, because the newest turns are what the next instruction rests on.
|
||||
let lines = brief.split("\n");
|
||||
let view = `${head}\n${lines.join("\n")}\n`;
|
||||
while (Buffer.byteLength(view, "utf-8") > MAX_VIEW_BYTES && lines.length > 1) {
|
||||
lines = lines.slice(1);
|
||||
view = `${head}\n[earlier turns cut to fit the channel]\n${lines.join("\n")}\n`;
|
||||
}
|
||||
if (Buffer.byteLength(view, "utf-8") <= MAX_VIEW_BYTES) return view;
|
||||
// The head alone can overflow, on a long goal. The broker drops anything over
|
||||
// 16 KiB and never tells the extension, so the supervisor would go blind. Cut, and say so.
|
||||
return `${Buffer.from(view, "utf-8").subarray(0, MAX_VIEW_BYTES - 40).toString("utf-8")}\n[view cut here to fit the channel]\n`;
|
||||
}
|
||||
+132
-53
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* pi-goals v2 — all model-facing text, in flow order.
|
||||
*
|
||||
* Design: plan.md is for LLMs and the human, not for TypeScript. There is no parser and no schema;
|
||||
* 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 working agent maintains with
|
||||
* its normal Edit tool, and the judge reads natively. The harness does three things for a
|
||||
* cooperative-but-confused model: memory (a transient re-send of the plan when it goes stale),
|
||||
* cooperative-but-confused model: memory (a saved reminder of the plan when it goes stale),
|
||||
* format guidance (the skeleton), and fresh eyes (the read-only judge in CompleteGoal).
|
||||
*
|
||||
* THE FOLD: everything above "## Log" is the working set (title, user voice, goals,
|
||||
@@ -13,8 +13,8 @@
|
||||
* session start and after a compaction, which is where the settled context is actually needed.
|
||||
*
|
||||
* Flow:
|
||||
* SETUP (plan mode) 1. planDrafting — draft goals into plan.md (read-only phase), sent once
|
||||
* EXEC, on cadence 2. reminder — the folded plan + upkeep nudge when plan.md went stale
|
||||
* SETUP (plan mode) 1. planDrafting — draft goals into the plan file (read-only), sent once
|
||||
* EXEC, on cadence 2. reminder — the folded plan + upkeep nudge when it went stale
|
||||
* EXEC, after compact 3. resync — the WHOLE file back, once
|
||||
* SIGN-OFF, agent-side 4. completeGoal* — the one blessed tool's description
|
||||
* SIGN-OFF, judge-side 5. judgeSystem/judgeUser — the one rigorous check
|
||||
@@ -24,51 +24,77 @@
|
||||
*/
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* 1. planDrafting — SETUP, plan mode (read-only: edit/write blocked except plan.md)
|
||||
* 1. planDrafting — SETUP, plan mode (read-only: edit/write blocked except the plan file)
|
||||
* ──────────────────────────────────────────────────────────────────────── */
|
||||
export const planDrafting = `\
|
||||
You are in plan mode. The objective may arrive through conversation, not as one up-front command.
|
||||
Explore the repository read-only first: resolve discoverable facts by looking them up, and only ask
|
||||
the human when the answer is a genuine intent or preference choice. 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). When
|
||||
the objective is clear, draft the plan file and present it.
|
||||
You are in plan mode. You are making a short judgeable plan that captures the user's real goals, then tests it in conversation.
|
||||
|
||||
How this mode ends: after each of your turns the human gets a menu (Ready / open in $EDITOR / keep
|
||||
planning). Plan mode ends when they pick Ready. So close every draft with one line -- the plan is
|
||||
final, pick Ready to start or reply to revise -- and do not redraft in silence. When a new
|
||||
requirement arrives, fold it in, say what changed, and say the plan is final again. Detail that
|
||||
doesn't change a goal or a discriminator belongs in the appendix, not in the goals.
|
||||
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. If any is uncertain, reduce uncertainty now: inspect files or
|
||||
search the web when they can answer, then ask the human to confirm your interpretation, pin down the
|
||||
outcome or task, or approve an editorial or other preference choice. Do not present the review menu
|
||||
with a placeholder goal such as "work out the thing", "improve it", or "investigate".
|
||||
3. Ask material task-specific alignment questions about unresolved outcomes, scope, constraints, or
|
||||
success/failure criteria. There is no fixed question quota. Inspect technical facts yourself and do
|
||||
not ask for confirmation of ordinary implementation details or repeat answered questions. Batch
|
||||
independent high-impact questions in one short round. 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. Wait for answers to required decisions and use them before declaring
|
||||
the plan final. Record each answer in ## Interview. Do not make the plan final while material user
|
||||
decisions remain open; if the requested work is already executable, proceed to review without a
|
||||
ritual questionnaire.
|
||||
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
|
||||
not replace, defer, or contradict it; ask the human if an inference would change the result.
|
||||
5. When every goal has an object, observable result, settled scope, and required approval, draft the
|
||||
plan file and present it. It should be safe to work overnight and present the requested outcome.
|
||||
|
||||
How this mode ends: when alignment is complete and the plan is ready, call RequestPlanReview to
|
||||
show Ready / Discuss / Edit / Cancel. Only Ready ends planning. Discuss returns to normal chat:
|
||||
ask useful alignment questions, wait for answers, and continue discussing for as many turns as
|
||||
needed. Do not request review while awaiting answers. When discussion is finished, call
|
||||
RequestPlanReview again, even if the draft did not change. Edit opens the full plan directly.
|
||||
When a new requirement arrives, fold it in, say what changed, and request review when ready.
|
||||
Detail that doesn't change a goal or a discriminator belongs in the appendix, not in the goals.
|
||||
|
||||
Right-size it:
|
||||
- Default to ONE goal. Add another only when it's a genuinely separate checkpoint that can pass or
|
||||
fail on its own. Most objectives are 1-2 goals.
|
||||
- One goal per distinct judgeable outcome. Group related goals when it helps judge them together
|
||||
and readability. The count flows from the outcomes.
|
||||
- Describe outcomes in qualitative terms the judge and user can discriminate.
|
||||
- Use the users language or more precise don't transform "MV" into "knob" as it looses precision and is overloaded
|
||||
- Don't invent metrics or thresholds for problems you haven't explored yet — the judge should hopefully know it when it sees the outcome.
|
||||
- Quantitative gates are fine only when you are certain they survive contact with reality.
|
||||
- Subtasks are the steps inside a goal; add them when a goal has 3+ distinct steps, skip otherwise.
|
||||
- Don't invent goals to look thorough. When in doubt, merge.
|
||||
- Everything above "## Log" is the part the model carries while it works. Keep it under 50 lines,
|
||||
reviewable in one pass. Everything below "## Log" is unlimited.
|
||||
- Two goals that share one discriminator are one goal. Merge them.
|
||||
- Keep the goal subject short. Put its important scope, failure modes, discriminator, tasks, and evidence in the indented block beneath it. The judge reads the whole block and the whole plan.
|
||||
- Keep the working set under 50 lines, excluding ## User voice. ## User voice has no line limit: quote
|
||||
the human fully rather than shorten or paraphrase them. Everything below "## Log" is unlimited.
|
||||
|
||||
Style: ASD-STE100 Simplified Technical English. Active voice, one idea per sentence, common words,
|
||||
the same word for the same thing, and define a new term at first use. This covers the context
|
||||
paragraph and the appendix too, not just the checklist. No all-caps headers and no bold spam; the
|
||||
checklist is already the structure.
|
||||
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,
|
||||
the same word for the same thing, and define a new terms at first use. Use redundant context for skim readers e.g. "our output - the cells, CV tag" is easy to read and reminds context. This covers the context
|
||||
paragraph and the appendix too, not just the checklist. No all-caps headers and no bold spam. Just write less, add your voice less, persuade less, and burden the reader less.
|
||||
|
||||
If a loaded skill also gives you a plan format, this skeleton wins while you are in this mode. Take
|
||||
its style advice, not a second template, and keep one plan file.
|
||||
|
||||
Write the plan file in roughly this shape (it's a convention, not a schema -- the file is read
|
||||
directly by the human and a judge model, so clarity beats conformance; small deviations are fine):
|
||||
Write the plan file in roughly this shape -- the file is read directly by the human and a judge model, so clarity beats conformance; small deviations are fine):
|
||||
|
||||
# <short plan title>
|
||||
|
||||
<context: one short paragraph. What the human wants and why.>
|
||||
|
||||
## User-visible result
|
||||
|
||||
<one concrete sentence naming the final artifact or behavior the human will inspect>
|
||||
|
||||
## User voice
|
||||
|
||||
- > "<the human's requirement, quoted word for word>"
|
||||
- > "<the human's requirement, quoted in full word for word (with spelling fixes)>"
|
||||
|
||||
## Goals
|
||||
|
||||
1. [ ] goal: <one short imperative line>
|
||||
1. [ ] goal: <one short jugable imperative outcome>
|
||||
- subtle failure mode: <a way this could look done but isn't>
|
||||
- discriminator: <the concrete observation that tells real success from that failure>
|
||||
- verify: <optional shell command that exits 0 only when the discriminator passes; omit if not
|
||||
@@ -82,9 +108,14 @@ directly by the human and a judge model, so clarity beats conformance; small dev
|
||||
<-- the fold: everything below here is durable memory, not the working set -->
|
||||
|
||||
## Log
|
||||
### {date}
|
||||
|
||||
## Interview
|
||||
|
||||
## Learnings
|
||||
|
||||
## Papercuts - problems, gotchas, suggestions
|
||||
|
||||
## Appendix (context, not approved)
|
||||
|
||||
Conventions:
|
||||
@@ -98,10 +129,16 @@ Conventions:
|
||||
- Make the discriminator a concrete, checkable observation about a real artifact (a file, a test
|
||||
result, a committed diff, a metric), never about the plan file's own checkbox.
|
||||
- evidence stays empty at planning; you fill it at sign-off and a fresh read-only judge checks it.
|
||||
Cite durable artifacts a future reader can open: committed files, test names, git diffs. .pi/ is
|
||||
usually gitignored, so files there prove things only at judge time, not in history.
|
||||
Cite artifacts a reviewer can open: files, test names, saved verification output, git diffs.
|
||||
Uncommitted and ignored files are valid review evidence when inspected directly. Git history
|
||||
improves durability; a commit or clean worktree is not a sign-off requirement.
|
||||
- User-visible result: restate the original deliverable, not the proposed implementation. Every goal
|
||||
must contribute to it. Future work may not defer any artifact or action named there.
|
||||
- User voice: quote the human word for word, one line per requirement, as they say it. Never
|
||||
paraphrase there -- a paraphrase drifts, and then the goals churn on the next reply.
|
||||
paraphrase there -- a paraphrase drifts, and then the goals churn on the next reply. It is exempt
|
||||
from the working-set line limit. Never put an agent inference in User voice.
|
||||
- Interview: every human reply in plan mode is stored here verbatim as a dated blockquote. It is
|
||||
durable memory below the fold, not a substitute for ## User voice.
|
||||
- Rejected options stay visible: ~~struck through~~ with who rejected them and why, so nobody
|
||||
relitigates them.
|
||||
- Learnings: one line per gotcha that a future reader would otherwise rediscover. Write down what
|
||||
@@ -110,14 +147,42 @@ Conventions:
|
||||
- Appendix: unlimited and unverified. Alternatives, links, dead ends, and the settled detail that
|
||||
is not part of the approved goals. Nothing here is approved and nothing here is checked.
|
||||
|
||||
When the goals are drafted, present them and say the plan is final. Do not begin execution.`;
|
||||
After the alignment answers are incorporated, present the final plan and call RequestPlanReview. Do not begin execution.`;
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* 3. reminder — EXEC. Transient, never persisted, and only when the plan went stale for a couple of
|
||||
* 3. reminder — EXEC. Saved at the next natural prompt after the plan goes stale for several
|
||||
* turns. pi-tasks tried a per-turn injection and deleted it: "wallpaper noise that trains the
|
||||
* model to ignore the task block" (tintinweb/pi-tasks CHANGELOG.md:149). Carries the folded plan
|
||||
* (above ## Log), because a nudge with no plan in it makes the model go read the file anyway.
|
||||
* ──────────────────────────────────────────────────────────────────────── */
|
||||
/** A waiver belongs only to the current objective, never to an earlier plan's transcript. */
|
||||
export function waivesAlignment(objective: string): boolean {
|
||||
// Only standalone affirmative clauses, not negations or quoted feature names.
|
||||
const unquoted = objective.replace(/"[^"]*"|“[^”]*”|‘[^’]*’|`[^`]*`/g, " ").replace(/(^|\W)'[^'\n]*'(?=$|\W)/g, "$1 ");
|
||||
return unquoted.split(/[;,.!?\n]+/).some(clause => /^(?:please\s+)?(?:no[- ](?:questions|q['’]s)|skip(?:[- ](?:the|all))?[- ](?:questions|q['’]s)|(?:don['’]t|do not) ask(?: me)?(?: any)? questions)(?:\s+please)?(?:\s+(?:and|then)\b.+)?$/i.test(clause.trim()));
|
||||
}
|
||||
|
||||
export function alignmentPolicy(waived: boolean): string {
|
||||
return waived
|
||||
? "Current-plan alignment: the human explicitly waived optional questions for THIS plan only. Use the authorized scope; do not invent missing permissions."
|
||||
: "Current-plan alignment: ask only material unresolved questions about outcome, scope, constraints, or success criteria; there is no fixed quota. Inspect technical facts yourself. Wait for required answers and use them; otherwise present the executable plan for review. A waiver in any previous plan does NOT apply. Do not repeat questions already answered for this plan.";
|
||||
}
|
||||
|
||||
export const discussPlan = "Continue discussing this draft in normal chat. Ask useful, task-specific alignment questions to check where your understanding differs from the human's: expected result, scope/constraints, and success/failure criteria. Wait for answers; do not open an editor or request review yet. Keep the draft and incorporate answers. When discussion is finished and the plan is ready, call RequestPlanReview, even if the draft is unchanged.";
|
||||
|
||||
export function planningState(planPath: string, questionsWaived = false): string {
|
||||
return `\
|
||||
[PLANNING MODE]
|
||||
${alignmentPolicy(questionsWaived)}
|
||||
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
|
||||
work, mark a goal [/] or [x], or sign off a goal. The plan is not approved until the human selects
|
||||
Ready. Call RequestPlanReview only when alignment is complete and the plan is ready, not while waiting for chat answers.`;
|
||||
}
|
||||
|
||||
export function reminder(foldedPlan: string, planRel: string): string {
|
||||
return `\
|
||||
<system-reminder>
|
||||
@@ -131,8 +196,11 @@ Keep it current as you work, with your normal edit tool:
|
||||
- when the active goal's discriminator is satisfied, fill its evidence: list (each item = a durable
|
||||
artifact + a verbatim quote you actually observed + a short read of it), then call CompleteGoal.
|
||||
Don't tick a goal [x] before CompleteGoal accepts; the sign-off log line is the audit trail.
|
||||
- if the working set has grown long, prune finished goals (their evidence lives in git history and
|
||||
## Log) and move settled detail down to ## Appendix, which is unlimited
|
||||
- keep every goal line and its completion status above ## Log; recorded sign-offs depend on those
|
||||
identities. Keep evidence references beside each goal. If the working set grows long, move only
|
||||
verbose settled detail down to ## Appendix; do not remove completed goal lines
|
||||
- the human's latest message outranks this plan. If it corrects the deliverable or scope, amend the
|
||||
user-visible result, user voice, and affected goals before continuing; don't defend the old plan
|
||||
- otherwise keep working toward the active goal; don't stop to ask unless genuinely blocked
|
||||
</system-reminder>`;
|
||||
}
|
||||
@@ -145,18 +213,17 @@ Keep it current as you work, with your normal edit tool:
|
||||
export function resync(plan: string, planRel: string, why: string): string {
|
||||
return `\
|
||||
<system-reminder>
|
||||
${why} This is the whole plan file (${planRel}), appendix included, so you don't re-litigate what
|
||||
was already settled. Keep working the active goal; edit the file directly as you go.
|
||||
${why} This is the whole plan file (${planRel}), appendix included. Keep working the active goal;
|
||||
edit the file directly as you go. The human's latest message outranks the plan: if it corrects the
|
||||
deliverable or scope, amend the plan rather than preserving an obsolete decision.
|
||||
|
||||
${plan}
|
||||
</system-reminder>`;
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* 4. completeGoal — SIGN-OFF, agent-side: the one blessed tool
|
||||
* ──────────────────────────────────────────────────────────────────────── */
|
||||
export const completeGoalDescription =
|
||||
"Sign off a goal once its discriminator is satisfied. First fill the goal's evidence: list in the " +
|
||||
"Sign off a goal once its discriminator is satisfied. When the optional persistent steward is enabled, " +
|
||||
"this call asks the retained supervisor about direction and scope before running the fresh judge. 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 " +
|
||||
@@ -164,17 +231,20 @@ export const completeGoalDescription =
|
||||
"the goal names a verify: command, run it yourself first and save its output to a file cited in " +
|
||||
"the evidence: the judge cannot execute anything and will reject a claimed pass with no saved " +
|
||||
"output. The read must show success POSITIVELY happened, not just that failures were avoided. " +
|
||||
"Then call this with the goal's text (the line after 'goal:'; small wording drift is fine). A " +
|
||||
"Check that the claimed result uses the artifact and outcome named in User-visible result and does " +
|
||||
"not substitute an agent-inferred deliverable. Then call this with the goal's text (the line after " +
|
||||
"'goal:'; one unique exact subject, ignoring case and surrounding whitespace). A " +
|
||||
"fresh strictly-read-only judge inspects the LIVE WORKING TREE (uncommitted changes included; " +
|
||||
"committing first is for durability, not visibility) and returns accept or reject with what's " +
|
||||
"ignored outputs included; neither a clean worktree nor a commit is required) and returns accept or reject with what's " +
|
||||
"missing. On accept (or if the judge itself failed), a sign-off line is appended to ## Log " +
|
||||
"and the goal is ticked [x] for you; the result says if you must tick it yourself. On reject the " +
|
||||
"goal stays open.";
|
||||
"and the goal is ticked [x] for you with a persisted sign-off record. Judge failure is explicitly " +
|
||||
"accepted inconclusive, not verified completion. Manual ticks remain claims awaiting this tool. " +
|
||||
"On reject the goal stays open.";
|
||||
|
||||
export const completeGoalParamDescription = "The goal's text: the line after 'goal:' in the plan file.";
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* 5. judge — SIGN-OFF, judge-side: the one rigorous check. Runs on a fresh
|
||||
* 6. judge — SIGN-OFF, judge-side: the one rigorous check. Runs on a fresh
|
||||
* read-only pi subprocess (--no-session) so it never sees the working
|
||||
* agent's transcript. It gets the WHOLE plan file: it finds the goal,
|
||||
* reads discriminator/failure modes/evidence itself (no parser between).
|
||||
@@ -182,8 +252,12 @@ export const completeGoalParamDescription = "The goal's text: the line after 'go
|
||||
export const judgeSystem = `\
|
||||
You are a strictly read-only reviewer signing off a coding goal. You cannot execute anything: judge
|
||||
by reading (read/grep/find/ls). Never re-run the work or its verify command -- it may be a 10-hour
|
||||
job; the agent must bring you its saved output. Your job is evidence discipline, checked in order:
|
||||
job; the agent must bring you its saved output. Inspect the live working tree, including cited
|
||||
uncommitted and ignored files via read. Git status is context, not an acceptance gate; do not require
|
||||
cleanup or a commit unless the agreed goal requires it. Your job is evidence discipline, checked in order:
|
||||
|
||||
0. Task fidelity? Read User-visible result and User voice first. Reject if this goal contradicts,
|
||||
replaces, or defers the requested artifact or outcome. Agent-inferred scope is not authority.
|
||||
1. Anything here? An empty or placeholder evidence: list -> reject: "there's nothing here -- fill
|
||||
the evidence and try again."
|
||||
2. Quoted and attributed? Each item needs a source (file path / command) plus a verbatim quote of
|
||||
@@ -200,6 +274,10 @@ job; the agent must bring you its saved output. Your job is evidence discipline,
|
||||
not just unmentioned? If the goal names a verify: command, its saved output must be among the
|
||||
evidence, and the command must actually test the discriminator rather than pass tautologically.
|
||||
|
||||
Before the verdict, write this heading: checks:. Put one concise bullet under it for each artifact you actually read:
|
||||
path, verbatim observed quote, and what that observation establishes. This is an inspectable review
|
||||
record, not hidden reasoning. Do not write a checks bullet for a file you did not open.
|
||||
|
||||
Finish with exactly these two lines and nothing after:
|
||||
VERDICT: accept | reject
|
||||
missing: <empty if accept; otherwise a short list of what's needed before this can be accepted>`;
|
||||
@@ -210,9 +288,10 @@ The working agent claims this goal is complete:
|
||||
|
||||
goal: ${p.goal}
|
||||
|
||||
Below is the full plan file (${p.planPath}). Find that goal in it (tolerate small wording drift; if
|
||||
you cannot find a matching goal at all, reject and say so). Read its discriminator, subtle failure
|
||||
modes, verify command, and evidence list from the file itself.
|
||||
Below is the full plan file (${p.planPath}). Review the unique exact goal subject shown above
|
||||
(ignoring case and surrounding whitespace). If it is missing or ambiguous, reject and request the
|
||||
exact subject; do not substitute another goal. Read User-visible result and User voice first, then
|
||||
its discriminator, subtle failure modes, verify command, and evidence list.
|
||||
|
||||
--- plan file ---
|
||||
${p.plan}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { type ExtensionAPI, type ExtensionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
export type ModelRole = "planning" | "worker" | "supervisor";
|
||||
export interface ModelChoice { provider: string; id: string }
|
||||
|
||||
/** One atomic file per role: a supervisor process cannot clobber the worker's choice. */
|
||||
export class RoleModels {
|
||||
private role: ModelRole | null = null;
|
||||
private automatic = 0;
|
||||
private available = true;
|
||||
|
||||
constructor(private pi: ExtensionAPI, private directory = join(getAgentDir(), "pi-goals")) {
|
||||
pi.on("model_select", (event, ctx) => {
|
||||
// setModel can emit "set" itself. Session restore is not a human preference either.
|
||||
if (this.automatic || event.source === "restore" || !this.role) return;
|
||||
try { this.save(this.role, event.model); this.available = true; }
|
||||
catch (error) { ctx.ui.notify(`Could not remember ${this.role} model: ${String(error)}`, "error"); }
|
||||
});
|
||||
}
|
||||
|
||||
get ready(): boolean { return this.available; }
|
||||
get activeRole(): ModelRole | null { return this.role; }
|
||||
get restoring(): boolean { return this.automatic > 0; }
|
||||
leave(): void { this.role = null; this.available = true; }
|
||||
|
||||
/** Explicit acknowledgement works even when Pi suppresses same-model model_select. */
|
||||
async useCurrent(ctx: ExtensionContext): Promise<boolean> {
|
||||
const role = this.role;
|
||||
if (!role || this.available) { ctx.ui.notify("No role model is paused.", "info"); return false; }
|
||||
this.automatic++;
|
||||
try {
|
||||
const model = ctx.model;
|
||||
if (!model || !await this.pi.setModel(model)) throw new Error("The current model is missing or unauthenticated");
|
||||
this.save(role, model);
|
||||
this.available = true;
|
||||
ctx.ui.notify(`Explicitly saved current model ${model.provider}/${model.id} for ${role}.`, "info");
|
||||
return true;
|
||||
} catch (error) {
|
||||
ctx.ui.notify(`Could not recover ${role}: ${String(error)}. Saved choice unchanged.`, "error");
|
||||
return false;
|
||||
} finally { this.automatic--; }
|
||||
}
|
||||
|
||||
private path(role: ModelRole): string { return join(this.directory, `${role}-model.json`); }
|
||||
private read(role: ModelRole): ModelChoice | undefined {
|
||||
let raw: string;
|
||||
try { raw = readFileSync(this.path(role), "utf8"); }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return; throw error; }
|
||||
const value = JSON.parse(raw);
|
||||
if (!value || typeof value.provider !== "string" || !value.provider || typeof value.id !== "string" || !value.id) throw new Error(`Invalid model preference: ${this.path(role)}`);
|
||||
return { provider: value.provider, id: value.id };
|
||||
}
|
||||
private save(role: ModelRole, model: ModelChoice): void {
|
||||
mkdirSync(this.directory, { recursive: true });
|
||||
const temp = `${this.path(role)}.${randomUUID()}.tmp`;
|
||||
try {
|
||||
writeFileSync(temp, `${JSON.stringify({ provider: model.provider, id: model.id })}\n`, { mode: 0o600 });
|
||||
renameSync(temp, this.path(role));
|
||||
} finally { rmSync(temp, { force: true }); }
|
||||
}
|
||||
|
||||
/** Failure keeps the saved choice and pauses the role; only an explicit selection replaces it. */
|
||||
async enter(role: ModelRole, ctx: ExtensionContext): Promise<boolean> {
|
||||
this.role = role;
|
||||
this.automatic++;
|
||||
try {
|
||||
const choice = this.read(role);
|
||||
if (!choice) {
|
||||
if (!ctx.model) throw new Error("No current model. Select one with /model first.");
|
||||
this.save(role, ctx.model);
|
||||
if (!await this.pi.setModel(ctx.model)) throw new Error(`Current model ${ctx.model.provider}/${ctx.model.id} has no authentication.`);
|
||||
} else {
|
||||
const model = ctx.modelRegistry.find(choice.provider, choice.id);
|
||||
if (!model) throw new Error(`Remembered model ${choice.provider}/${choice.id} is unavailable.`);
|
||||
if (!await this.pi.setModel(model)) throw new Error(`Remembered model ${choice.provider}/${choice.id} has no authentication.`);
|
||||
}
|
||||
this.available = true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.available = false;
|
||||
ctx.ui.notify(`${role} model paused: ${String(error)} Saved choice unchanged; configure that model and retry, select a different model with /model, or explicitly use the current model for this paused role with /goals model current. Then retry Ready if work has not started.`, "error");
|
||||
return false;
|
||||
} finally { this.automatic--; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { type ExtensionAPI, type ExtensionContext, getAgentDir, SessionManager } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
export const SUPERVISOR_ROLE = "pi-goals-supervisor";
|
||||
export interface SupervisorBinding {
|
||||
id: string;
|
||||
planPath: string;
|
||||
workerSession: string;
|
||||
workerPane: string;
|
||||
supervisorPane?: string;
|
||||
supervisorSession?: string;
|
||||
active?: boolean;
|
||||
stopped?: boolean;
|
||||
startupFailure?: string;
|
||||
/** User pause: retain the pair, but no autonomous work until explicit resume. */
|
||||
paused?: boolean;
|
||||
pauseId?: string;
|
||||
everyTurns: number;
|
||||
intervalMs: number;
|
||||
compactTokens: number;
|
||||
}
|
||||
export interface Bootstrap { binding: SupervisorBinding; workerId: string }
|
||||
export interface SupervisorStatus { connected: boolean; binding?: SupervisorBinding; workerId: string; role?: string; activity?: string; lastFailure?: string }
|
||||
export interface SupervisorDecision { bindingId: string; goal: string; planHash: string; decision: "approve" | "needs_work" | "needs_user"; reason: string }
|
||||
|
||||
export function planHash(text: string): string {
|
||||
return createHash("sha256").update(text).digest("hex");
|
||||
}
|
||||
|
||||
export interface SupervisorController {
|
||||
status(signal?: AbortSignal): Promise<SupervisorStatus>;
|
||||
prepare(binding: SupervisorBinding, signal?: AbortSignal): Promise<void>;
|
||||
bootstrap(bootstrap: Bootstrap, signal?: AbortSignal): Promise<SupervisorBinding>;
|
||||
attached(bindingId: string, signal?: AbortSignal, expected?: SupervisorBinding): Promise<SupervisorBinding>;
|
||||
activate(bindingId: string, signal?: AbortSignal): Promise<void>;
|
||||
review(bindingId: string, goal: string, hash: string, signal?: AbortSignal): Promise<SupervisorDecision>;
|
||||
stop(bindingId: string): Promise<void>;
|
||||
pause(exit?: boolean): void;
|
||||
reconnect(signal?: AbortSignal): Promise<void>;
|
||||
resume(bindingId: string, hash: string, signal?: AbortSignal): Promise<void>;
|
||||
}
|
||||
|
||||
export function validBinding(value: unknown): value is SupervisorBinding {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const b = value as SupervisorBinding;
|
||||
return [b.id, b.planPath, b.workerSession, b.workerPane].every(v => typeof v === "string" && v.length > 0)
|
||||
&& [b.everyTurns, b.intervalMs, b.compactTokens].every(v => Number.isSafeInteger(v) && v > 0);
|
||||
}
|
||||
export function planText(binding: SupervisorBinding): string {
|
||||
return readFileSync(binding.planPath, "utf8");
|
||||
}
|
||||
/** Startup waits may have a deadline. Model checkpoints pass null: elapsed thinking is not failure. */
|
||||
export function pendingReply<T>(signal: AbortSignal | undefined, cancel: () => void, timeoutMs: number | null = 600_000) {
|
||||
let finish!: (value?: T, error?: Error) => void;
|
||||
const promise = new Promise<T>((resolve, reject) => {
|
||||
let settled = false;
|
||||
const abort = () => finish(undefined, new Error("Supervisor request cancelled"));
|
||||
const timer = timeoutMs === null ? undefined : setTimeout(() => finish(undefined, new Error("Supervisor request timed out; retry or turn the steward off")), timeoutMs);
|
||||
finish = (value, error) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
signal?.removeEventListener("abort", abort);
|
||||
if (error) {
|
||||
// Local cancellation must settle even if notifying a disconnected peer throws.
|
||||
try { cancel(); } catch { /* The caller reports the request failure; remote state is unconfirmed. */ }
|
||||
reject(error);
|
||||
} else resolve(value as T);
|
||||
};
|
||||
signal?.addEventListener("abort", abort, { once: true });
|
||||
if (signal?.aborted) queueMicrotask(abort);
|
||||
});
|
||||
return { requestId: randomUUID(), promise, finish };
|
||||
}
|
||||
|
||||
class HerdrFailure extends Error {
|
||||
constructor(message: string, readonly code?: string) { super(message); }
|
||||
}
|
||||
|
||||
async function herdr(pi: ExtensionAPI, args: string[], signal?: AbortSignal): Promise<Record<string, any>> {
|
||||
if (process.env.HERDR_ENV !== "1") throw new Error("Start Pi inside Herdr to launch or focus the supervisor. No pane was created.");
|
||||
const result = await pi.exec("herdr", args, { timeout: 45_000, signal });
|
||||
if (result.code !== 0) {
|
||||
const text = result.stderr || result.stdout;
|
||||
let code: string | undefined;
|
||||
try { code = JSON.parse(text).error?.code; } catch { /* Non-JSON transport failures remain non-recoverable. */ }
|
||||
throw new HerdrFailure(`Herdr: ${text}`, code);
|
||||
}
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
if (parsed.error) throw new HerdrFailure(`Herdr: ${parsed.error.message}`, parsed.error.code);
|
||||
return parsed.result ?? parsed;
|
||||
}
|
||||
|
||||
export async function focusSupervisor(pi: ExtensionAPI, binding: SupervisorBinding, target: "supervisor" | "worker" | "zoom"): Promise<void> {
|
||||
const pane = target === "worker" ? binding.workerPane : binding.supervisorPane;
|
||||
if (!pane) throw new Error("No supervisor pane is recorded. Select Ready to start it.");
|
||||
try { await herdr(pi, target === "zoom" ? ["pane", "zoom", "--pane", pane, "--toggle"] : ["agent", "focus", pane]); }
|
||||
catch (error) { throw new Error(`${String(error)}. Session location/liveness is unknown. Locate the existing supervisor first; only after confirming it is no longer running, reopen pi --session ${JSON.stringify(binding.supervisorSession)}.`, { cause: error }); }
|
||||
}
|
||||
|
||||
export function supervisorBootstrap(ctx: ExtensionContext): Bootstrap | undefined {
|
||||
const entry = ctx.sessionManager.getBranch().filter(e => e.type === "custom" && e.customType === SUPERVISOR_ROLE).at(-1);
|
||||
return entry?.type === "custom" ? entry.data as Bootstrap : undefined;
|
||||
}
|
||||
|
||||
export function initializeSupervisor(supervisor: SupervisorController, ctx: ExtensionContext, bootstrap: Bootstrap, signal?: AbortSignal): void {
|
||||
// Let all packages initialize before bootstrapping the local supervisor role.
|
||||
setImmediate(() => {
|
||||
if (signal?.aborted) return;
|
||||
void supervisor.bootstrap(bootstrap, signal).catch((error: Error) => {
|
||||
if (!signal?.aborted) ctx.ui.notify(`Supervisor initialization failed: ${error.message}`, "error");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Replay only the worker's explicit resource choices, never its prompt, mode, model or credentials.
|
||||
* Configured packages come from the same agent directory. No companion extension is added. */
|
||||
export function supervisorResourceArgs(argv: string[]): string[] {
|
||||
const valued = new Set(["-e", "--extension", "--skill", "--prompt-template", "--theme"]);
|
||||
const flags = new Set(["--no-extensions", "--no-skills", "--no-prompt-templates", "--no-themes", "--no-context-files", "-nc", "--approve", "-a", "--no-approve", "-na"]);
|
||||
const aliases: Record<string, string> = { "-ne": "--no-extensions", "-ns": "--no-skills", "-np": "--no-prompt-templates" };
|
||||
const result: string[] = [];
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = aliases[argv[i]] ?? argv[i];
|
||||
if (arg === "--") break;
|
||||
if (valued.has(arg) && argv[i + 1]) result.push(arg, argv[++i]);
|
||||
else if (flags.has(arg)) result.push(arg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function startSupervisor(
|
||||
pi: ExtensionAPI, supervisor: SupervisorController, ctx: ExtensionContext, planPath: string, existing: SupervisorBinding | null,
|
||||
save: (binding: SupervisorBinding) => void, signal: AbortSignal,
|
||||
): Promise<SupervisorBinding> {
|
||||
if (process.env.HERDR_ENV !== "1" || !process.env.HERDR_PANE_ID) throw new Error("Start Pi inside Herdr before enabling the steward at Ready.");
|
||||
const status = await supervisor.status(signal);
|
||||
signal.throwIfAborted();
|
||||
if (existing && status.connected && status.binding?.id === existing.id) return status.binding;
|
||||
if ((!existing && status.role && status.role !== "none") || (status.binding && status.binding.id !== existing?.id)) throw new Error("This session already has another supervision relationship. Stop it explicitly before Ready.");
|
||||
const parent = ctx.sessionManager.getSessionFile();
|
||||
const leaf = ctx.sessionManager.getLeafId();
|
||||
if (!parent || !leaf) throw new Error("The planning session must be persisted before creating its supervisor fork.");
|
||||
let binding = existing ?? {
|
||||
id: randomUUID(), planPath, workerSession: parent, workerPane: process.env.HERDR_PANE_ID,
|
||||
everyTurns: 50, intervalMs: 60 * 60_000, compactTokens: 100_000,
|
||||
};
|
||||
if (!existing) {
|
||||
save(binding);
|
||||
await supervisor.prepare(binding, signal);
|
||||
signal.throwIfAborted();
|
||||
}
|
||||
if (!binding.supervisorSession) {
|
||||
const fork = SessionManager.open(parent);
|
||||
const sessionFile = fork.createBranchedSession(leaf);
|
||||
if (!sessionFile) throw new Error("Could not persist the supervisor fork");
|
||||
binding = { ...binding, supervisorSession: sessionFile };
|
||||
fork.appendCustomEntry(SUPERVISOR_ROLE, { binding, workerId: status.workerId });
|
||||
fork.appendSessionInfo(`Supervisor ${binding.id.slice(0, 8)}`);
|
||||
save(binding);
|
||||
}
|
||||
if (binding.supervisorPane) {
|
||||
// An existing occupant is not permission to start another process on the same session file.
|
||||
try { await focusSupervisor(pi, binding, "supervisor"); }
|
||||
catch (error) {
|
||||
const cause = (error as Error).cause;
|
||||
if (!(cause instanceof HerdrFailure) || !["agent_not_found", "pane_not_found"].includes(cause.code ?? "")) throw error;
|
||||
// Only an explicit Ready reaches this path. A missing pane is not a reason
|
||||
// to reopen its session blindly: first locate any moved/resumed occupant.
|
||||
let roster: Record<string, any>;
|
||||
try { roster = await herdr(pi, ["agent", "list"], signal); } catch { throw error; }
|
||||
signal.throwIfAborted();
|
||||
if (!Array.isArray(roster.agents)) throw error;
|
||||
const matches = roster.agents.filter((agent: any) => agent.agent_session?.value === binding.supervisorSession);
|
||||
if (matches.length > 1) throw new Error("Multiple panes report this supervisor session; resolve the duplicate before Ready.");
|
||||
if (matches.length === 1) {
|
||||
if (typeof matches[0].pane_id !== "string") throw error;
|
||||
binding = { ...binding, supervisorPane: matches[0].pane_id }; save(binding);
|
||||
await focusSupervisor(pi, binding, "supervisor");
|
||||
} else {
|
||||
if (roster.agents.some((agent: any) => agent.agent === "pi" && !agent.agent_session?.value)) throw new Error("A Pi pane has unknown session identity; inspect it before replacing the supervisor.");
|
||||
// End the old binding before creating a DISTINCT fork. Late traffic from
|
||||
// the old process cannot authorize or steer the replacement worker pair.
|
||||
await supervisor.stop(binding.id);
|
||||
signal.throwIfAborted();
|
||||
ctx.ui.notify("Recorded supervisor is no longer in Herdr. Ready is creating a replacement; the old session is retained.", "info");
|
||||
return startSupervisor(pi, supervisor, ctx, planPath, null, save, signal);
|
||||
}
|
||||
}
|
||||
signal.throwIfAborted();
|
||||
return await supervisor.attached(binding.id, signal, binding);
|
||||
}
|
||||
const split = await herdr(pi, ["pane", "split", "--current", "--direction", "right", "--cwd", ctx.cwd, "--env", `PI_CODING_AGENT_DIR=${getAgentDir()}`, "--no-focus"], signal);
|
||||
const pane = split.pane?.pane_id;
|
||||
if (typeof pane !== "string") throw new Error("Herdr split did not return a pane ID");
|
||||
binding = { ...binding, supervisorPane: pane };
|
||||
save(binding);
|
||||
signal.throwIfAborted();
|
||||
// Keep bootstrap and worker binding identical, including the returned pane identity.
|
||||
SessionManager.open(binding.supervisorSession!).appendCustomEntry(SUPERVISOR_ROLE, { binding, workerId: status.workerId });
|
||||
const waiting = supervisor.attached(binding.id, signal, binding);
|
||||
void waiting.catch(() => {});
|
||||
try {
|
||||
await herdr(pi, ["agent", "start", `supervisor-${binding.id.slice(0, 8)}`, "--kind", "pi", "--pane", pane, "--", "--session", binding.supervisorSession!, ...supervisorResourceArgs(process.argv.slice(2))], signal);
|
||||
return await waiting;
|
||||
} catch (error) {
|
||||
throw new Error(`Supervisor startup incomplete: ${String(error)}. Inspect the recorded pane, resolve startup, reload it, then retry Ready.`);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,55 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { decideSignOff, type JudgeResult } from "../src/index.js";
|
||||
import { judgeSystem } from "../src/prompts.js";
|
||||
|
||||
// decideSignOff is the fail-forward invariant: judgeModel is NEVER checked pre-emptively, so a null
|
||||
// model still reaches runJudge (pi's configured default runs it), and the only producers of
|
||||
// accepted_inconclusive are the judge-error and no-VERDICT paths -- i.e. "the judge ran but failed",
|
||||
// never "no model". The judge runner is injected so these tests never spawn a real subprocess.
|
||||
const input = { goal: "x", plan: "# plan\n", planRel: ".pi/plan/s1.md", judgeModel: null };
|
||||
|
||||
describe("decideSignOff (fail-forward invariant)", () => {
|
||||
it("proceeds to runJudge even when judgeModel is null (no pre-emptive 'no model' inconclusive)", async () => {
|
||||
const runJudge = vi.fn().mockResolvedValue({ output: "VERDICT: accept\nall good" });
|
||||
const out = await decideSignOff({ goal: "x", plan: "# plan\n1. [ ] goal: x\n", judgeModel: null }, undefined, runJudge);
|
||||
const output = "## checks:\n- evidence.txt: `PASS`; the saved check passed\n\nThe artifact proves the gate passed.\nVERDICT: accept\nmissing:";
|
||||
const runJudge = vi.fn().mockResolvedValue({ output });
|
||||
const out = await decideSignOff({ ...input, plan: "# plan\n1. [ ] goal: x\n" }, undefined, runJudge);
|
||||
expect(runJudge).toHaveBeenCalledOnce(); // reached the judge -- no pre-emptive return on null model
|
||||
expect(out.isError).toBe(false);
|
||||
expect(out.logEntry).toContain("judge accept");
|
||||
expect(out.resultText).toContain("evidence.txt: `PASS`");
|
||||
});
|
||||
|
||||
it.each(["1. ", "1) ", " - "])("accepts a real checked-artifact list using Markdown marker %s", async marker => {
|
||||
const output = `## checks:\n${marker}converter.mjs: \`export function convert\`; inspected the actual export.\n\nVERDICT: accept\nmissing:`;
|
||||
const out = await decideSignOff(input, undefined, vi.fn().mockResolvedValue({ output }));
|
||||
expect(out.isError).toBe(false);
|
||||
expect(out.logEntry).toContain("judge accept");
|
||||
});
|
||||
|
||||
it.each(["1. ", "- "])("does not borrow %s items from a later section when checks is empty", async marker => {
|
||||
const output = `## checks:\n\n## Next steps\n${marker}Run the tests later\n\nVERDICT: accept\nmissing:`;
|
||||
const out = await decideSignOff(input, undefined, vi.fn().mockResolvedValue({ output }));
|
||||
expect(out.isError).toBe(true);
|
||||
expect(out.logEntry).toContain("no checked-artifact list");
|
||||
});
|
||||
|
||||
it("rejects an empty checks heading", async () => {
|
||||
const out = await decideSignOff(input, undefined, vi.fn().mockResolvedValue({ output: "## checks:\n\nVERDICT: accept\nmissing:" }));
|
||||
expect(out.isError).toBe(true);
|
||||
expect(out.logEntry).toContain("no checked-artifact list");
|
||||
});
|
||||
|
||||
it("rejects an accept verdict without a checked-artifact list", async () => {
|
||||
const runJudge = vi.fn().mockResolvedValue({ output: "VERDICT: accept\nmissing:" });
|
||||
const out = await decideSignOff(input, undefined, runJudge);
|
||||
expect(out.isError).toBe(true);
|
||||
expect(out.resultText).toContain("checked-artifact list");
|
||||
expect(out.logEntry).toContain("no checked-artifact list");
|
||||
});
|
||||
|
||||
it("a judge-subprocess error yields accepted_inconclusive with a 'ran but failed' reason", async () => {
|
||||
const runJudge = vi.fn().mockResolvedValue({ output: "", error: "judge subprocess exited 1" } satisfies JudgeResult);
|
||||
const out = await decideSignOff({ goal: "x", plan: "# plan\n", judgeModel: null }, undefined, runJudge);
|
||||
const out = await decideSignOff(input, undefined, runJudge);
|
||||
expect(runJudge).toHaveBeenCalledOnce();
|
||||
expect(out.isError).toBe(false); // accepted inconclusive, not a hard error that blocks the agent
|
||||
expect(out.resultText.toLowerCase()).toContain("accepted inconclusive");
|
||||
@@ -27,7 +60,7 @@ describe("decideSignOff (fail-forward invariant)", () => {
|
||||
|
||||
it("a judge timeout is also accepted_inconclusive (ran but failed)", async () => {
|
||||
const runJudge = vi.fn().mockResolvedValue({ output: "partial", error: "judge timed out after 600s" });
|
||||
const out = await decideSignOff({ goal: "x", plan: "# plan\n", judgeModel: null }, undefined, runJudge);
|
||||
const out = await decideSignOff(input, undefined, runJudge);
|
||||
expect(out.isError).toBe(false);
|
||||
expect(out.resultText.toLowerCase()).toContain("accepted inconclusive");
|
||||
expect(out.logEntry).toContain("ran but failed");
|
||||
@@ -37,7 +70,7 @@ describe("decideSignOff (fail-forward invariant)", () => {
|
||||
|
||||
it("no VERDICT line is accepted_inconclusive too (judge ran but didn't answer)", async () => {
|
||||
const runJudge = vi.fn().mockResolvedValue({ output: "I looked but forgot the verdict line" });
|
||||
const out = await decideSignOff({ goal: "x", plan: "# plan\n", judgeModel: null }, undefined, runJudge);
|
||||
const out = await decideSignOff(input, undefined, runJudge);
|
||||
expect(out.isError).toBe(false);
|
||||
expect(out.resultText).toContain("no VERDICT line");
|
||||
expect(out.logEntry).toContain("no VERDICT line");
|
||||
@@ -45,18 +78,23 @@ describe("decideSignOff (fail-forward invariant)", () => {
|
||||
|
||||
it("rejects when the judge returns VERDICT: reject", async () => {
|
||||
const runJudge = vi.fn().mockResolvedValue({ output: "VERDICT: reject\nmissing: evidence, tests" });
|
||||
const out = await decideSignOff({ goal: "x", plan: "# plan\n", judgeModel: "openrouter/claude" }, undefined, runJudge);
|
||||
const out = await decideSignOff({ ...input, judgeModel: "openrouter/claude" }, undefined, runJudge);
|
||||
expect(out.isError).toBe(true);
|
||||
expect(out.resultText).toContain("REJECTED");
|
||||
expect(out.resultText).toContain("evidence, tests");
|
||||
expect(out.logEntry).toContain("reject");
|
||||
});
|
||||
|
||||
it("requires a concise checked-artifact review, not private reasoning", () => {
|
||||
expect(judgeSystem).toContain("checks:");
|
||||
expect(judgeSystem).toContain("not hidden reasoning");
|
||||
});
|
||||
|
||||
it("writes nothing when aborted after the judge ran", async () => {
|
||||
const runJudge = vi.fn().mockResolvedValue({ output: "VERDICT: accept" });
|
||||
const ctrl = new AbortController();
|
||||
ctrl.abort();
|
||||
const out = await decideSignOff({ goal: "x", plan: "# plan\n", judgeModel: null }, ctrl.signal, runJudge);
|
||||
const out = await decideSignOff(input, ctrl.signal, runJudge);
|
||||
expect(out.logEntry).toBeNull();
|
||||
expect(out.isError).toBe(true);
|
||||
});
|
||||
|
||||
Vendored
+411
@@ -0,0 +1,411 @@
|
||||
{"type":"session","version":3,"id":"fixture","timestamp":"2026-01-01T00:00:00.000Z","cwd":"/tmp/fixture"}
|
||||
{"type":"message","id":"e0000","parentId":null,"timestamp":"2026-01-01T00:00:00.000Z","message":{"role":"user","content":[{"type":"text","text":"shared turn 0, this text is on every branch"}]}}
|
||||
{"type":"message","id":"e0001","parentId":"e0000","timestamp":"2026-01-01T00:00:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"shared reply 0, this text is on every branch"}]}}
|
||||
{"type":"message","id":"e0002","parentId":"e0001","timestamp":"2026-01-01T00:00:02.000Z","message":{"role":"user","content":[{"type":"text","text":"shared turn 1, this text is on every branch"}]}}
|
||||
{"type":"message","id":"e0003","parentId":"e0002","timestamp":"2026-01-01T00:00:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"shared reply 1, this text is on every branch"}]}}
|
||||
{"type":"message","id":"e0004","parentId":"e0003","timestamp":"2026-01-01T00:00:04.000Z","message":{"role":"user","content":[{"type":"text","text":"shared turn 2, this text is on every branch"}]}}
|
||||
{"type":"message","id":"e0005","parentId":"e0004","timestamp":"2026-01-01T00:00:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"shared reply 2, this text is on every branch"}]}}
|
||||
{"type":"message","id":"e0006","parentId":"e0005","timestamp":"2026-01-01T00:00:06.000Z","message":{"role":"user","content":[{"type":"text","text":"shared turn 3, this text is on every branch"}]}}
|
||||
{"type":"message","id":"e0007","parentId":"e0006","timestamp":"2026-01-01T00:00:07.000Z","message":{"role":"assistant","content":[{"type":"text","text":"shared reply 3, this text is on every branch"}]}}
|
||||
{"type":"message","id":"e0008","parentId":"e0007","timestamp":"2026-01-01T00:00:08.000Z","message":{"role":"user","content":[{"type":"text","text":"shared turn 4, this text is on every branch"}]}}
|
||||
{"type":"message","id":"e0009","parentId":"e0008","timestamp":"2026-01-01T00:00:09.000Z","message":{"role":"assistant","content":[{"type":"text","text":"shared reply 4, this text is on every branch"}]}}
|
||||
{"type":"message","id":"e0010","parentId":"e0009","timestamp":"2026-01-01T00:00:10.000Z","message":{"role":"user","content":[{"type":"text","text":"shared turn 5, this text is on every branch"}]}}
|
||||
{"type":"message","id":"e0011","parentId":"e0010","timestamp":"2026-01-01T00:00:11.000Z","message":{"role":"assistant","content":[{"type":"text","text":"shared reply 5, this text is on every branch"}]}}
|
||||
{"type":"message","id":"e0012","parentId":"e0011","timestamp":"2026-01-01T00:01:40.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 0, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0013","parentId":"e0012","timestamp":"2026-01-01T00:01:41.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 0, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0014","parentId":"e0013","timestamp":"2026-01-01T00:01:42.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 1, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0015","parentId":"e0014","timestamp":"2026-01-01T00:01:43.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 1, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0016","parentId":"e0015","timestamp":"2026-01-01T00:01:44.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 2, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0017","parentId":"e0016","timestamp":"2026-01-01T00:01:45.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 2, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0018","parentId":"e0017","timestamp":"2026-01-01T00:01:46.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 3, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0019","parentId":"e0018","timestamp":"2026-01-01T00:01:47.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 3, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0020","parentId":"e0019","timestamp":"2026-01-01T00:01:48.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 4, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0021","parentId":"e0020","timestamp":"2026-01-01T00:01:49.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 4, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0022","parentId":"e0021","timestamp":"2026-01-01T00:01:50.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 5, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0023","parentId":"e0022","timestamp":"2026-01-01T00:01:51.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 5, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0024","parentId":"e0023","timestamp":"2026-01-01T00:01:52.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 6, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0025","parentId":"e0024","timestamp":"2026-01-01T00:01:53.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 6, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0026","parentId":"e0025","timestamp":"2026-01-01T00:01:54.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 7, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0027","parentId":"e0026","timestamp":"2026-01-01T00:01:55.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 7, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0028","parentId":"e0027","timestamp":"2026-01-01T00:01:56.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 8, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0029","parentId":"e0028","timestamp":"2026-01-01T00:01:57.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 8, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0030","parentId":"e0029","timestamp":"2026-01-01T00:01:58.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 9, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0031","parentId":"e0030","timestamp":"2026-01-01T00:01:59.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 9, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0032","parentId":"e0031","timestamp":"2026-01-01T00:02:00.000Z","message":{"role":"user","content":[{"type":"text","text":"ABANDONED turn 10, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0033","parentId":"e0032","timestamp":"2026-01-01T00:02:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ABANDONED reply 10, rewound away and must not appear in a view"}]}}
|
||||
{"type":"message","id":"e0034","parentId":"e0011","timestamp":"2026-01-01T00:05:00.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 0, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0035","parentId":"e0034","timestamp":"2026-01-01T00:05:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 0, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0036","parentId":"e0035","timestamp":"2026-01-01T00:05:02.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 1, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0037","parentId":"e0036","timestamp":"2026-01-01T00:05:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 1, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0038","parentId":"e0037","timestamp":"2026-01-01T00:05:04.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 2, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0039","parentId":"e0038","timestamp":"2026-01-01T00:05:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 2, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0040","parentId":"e0039","timestamp":"2026-01-01T00:05:06.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 3, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0041","parentId":"e0040","timestamp":"2026-01-01T00:05:07.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 3, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0042","parentId":"e0041","timestamp":"2026-01-01T00:05:08.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 4, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0043","parentId":"e0042","timestamp":"2026-01-01T00:05:09.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 4, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0044","parentId":"e0043","timestamp":"2026-01-01T00:05:10.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 5, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0045","parentId":"e0044","timestamp":"2026-01-01T00:05:11.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 5, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0046","parentId":"e0045","timestamp":"2026-01-01T00:05:12.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 6, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0047","parentId":"e0046","timestamp":"2026-01-01T00:05:13.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 6, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0048","parentId":"e0047","timestamp":"2026-01-01T00:05:14.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 7, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0049","parentId":"e0048","timestamp":"2026-01-01T00:05:15.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 7, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0050","parentId":"e0049","timestamp":"2026-01-01T00:05:16.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 8, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0051","parentId":"e0050","timestamp":"2026-01-01T00:05:17.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 8, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0052","parentId":"e0051","timestamp":"2026-01-01T00:05:18.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 9, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0053","parentId":"e0052","timestamp":"2026-01-01T00:05:19.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 9, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0054","parentId":"e0053","timestamp":"2026-01-01T00:05:20.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 10, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0055","parentId":"e0054","timestamp":"2026-01-01T00:05:21.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 10, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0056","parentId":"e0055","timestamp":"2026-01-01T00:05:22.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 11, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0057","parentId":"e0056","timestamp":"2026-01-01T00:05:23.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 11, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0058","parentId":"e0057","timestamp":"2026-01-01T00:05:24.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 12, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0059","parentId":"e0058","timestamp":"2026-01-01T00:05:25.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 12, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0060","parentId":"e0059","timestamp":"2026-01-01T00:05:26.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 13, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0061","parentId":"e0060","timestamp":"2026-01-01T00:05:27.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 13, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0062","parentId":"e0061","timestamp":"2026-01-01T00:05:28.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 14, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0063","parentId":"e0062","timestamp":"2026-01-01T00:05:29.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 14, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0064","parentId":"e0063","timestamp":"2026-01-01T00:05:30.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 15, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0065","parentId":"e0064","timestamp":"2026-01-01T00:05:31.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 15, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0066","parentId":"e0065","timestamp":"2026-01-01T00:05:32.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 16, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0067","parentId":"e0066","timestamp":"2026-01-01T00:05:33.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 16, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0068","parentId":"e0067","timestamp":"2026-01-01T00:05:34.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 17, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0069","parentId":"e0068","timestamp":"2026-01-01T00:05:35.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 17, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0070","parentId":"e0069","timestamp":"2026-01-01T00:05:36.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 18, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0071","parentId":"e0070","timestamp":"2026-01-01T00:05:37.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 18, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0072","parentId":"e0071","timestamp":"2026-01-01T00:05:38.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 19, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0073","parentId":"e0072","timestamp":"2026-01-01T00:05:39.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 19, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0074","parentId":"e0073","timestamp":"2026-01-01T00:05:40.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 20, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0075","parentId":"e0074","timestamp":"2026-01-01T00:05:41.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 20, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0076","parentId":"e0075","timestamp":"2026-01-01T00:05:42.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 21, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0077","parentId":"e0076","timestamp":"2026-01-01T00:05:43.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 21, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0078","parentId":"e0077","timestamp":"2026-01-01T00:05:44.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 22, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0079","parentId":"e0078","timestamp":"2026-01-01T00:05:45.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 22, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0080","parentId":"e0079","timestamp":"2026-01-01T00:05:46.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 23, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0081","parentId":"e0080","timestamp":"2026-01-01T00:05:47.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 23, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0082","parentId":"e0081","timestamp":"2026-01-01T00:05:48.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 24, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0083","parentId":"e0082","timestamp":"2026-01-01T00:05:49.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 24, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0084","parentId":"e0083","timestamp":"2026-01-01T00:05:50.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 25, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0085","parentId":"e0084","timestamp":"2026-01-01T00:05:51.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 25, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0086","parentId":"e0085","timestamp":"2026-01-01T00:05:52.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 26, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0087","parentId":"e0086","timestamp":"2026-01-01T00:05:53.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 26, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0088","parentId":"e0087","timestamp":"2026-01-01T00:05:54.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 27, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0089","parentId":"e0088","timestamp":"2026-01-01T00:05:55.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 27, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0090","parentId":"e0089","timestamp":"2026-01-01T00:05:56.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 28, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0091","parentId":"e0090","timestamp":"2026-01-01T00:05:57.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 28, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0092","parentId":"e0091","timestamp":"2026-01-01T00:05:58.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 29, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0093","parentId":"e0092","timestamp":"2026-01-01T00:05:59.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 29, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0094","parentId":"e0093","timestamp":"2026-01-01T00:06:00.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 30, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0095","parentId":"e0094","timestamp":"2026-01-01T00:06:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 30, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0096","parentId":"e0095","timestamp":"2026-01-01T00:06:02.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 31, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0097","parentId":"e0096","timestamp":"2026-01-01T00:06:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 31, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0098","parentId":"e0097","timestamp":"2026-01-01T00:06:04.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 32, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0099","parentId":"e0098","timestamp":"2026-01-01T00:06:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 32, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0100","parentId":"e0099","timestamp":"2026-01-01T00:06:06.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 33, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0101","parentId":"e0100","timestamp":"2026-01-01T00:06:07.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 33, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0102","parentId":"e0101","timestamp":"2026-01-01T00:06:08.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 34, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0103","parentId":"e0102","timestamp":"2026-01-01T00:06:09.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 34, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0104","parentId":"e0103","timestamp":"2026-01-01T00:06:10.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 35, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0105","parentId":"e0104","timestamp":"2026-01-01T00:06:11.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 35, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0106","parentId":"e0105","timestamp":"2026-01-01T00:06:12.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 36, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0107","parentId":"e0106","timestamp":"2026-01-01T00:06:13.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 36, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0108","parentId":"e0107","timestamp":"2026-01-01T00:06:14.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 37, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0109","parentId":"e0108","timestamp":"2026-01-01T00:06:15.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 37, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0110","parentId":"e0109","timestamp":"2026-01-01T00:06:16.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 38, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0111","parentId":"e0110","timestamp":"2026-01-01T00:06:17.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 38, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0112","parentId":"e0111","timestamp":"2026-01-01T00:06:18.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 39, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0113","parentId":"e0112","timestamp":"2026-01-01T00:06:19.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 39, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0114","parentId":"e0113","timestamp":"2026-01-01T00:06:20.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 40, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0115","parentId":"e0114","timestamp":"2026-01-01T00:06:21.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 40, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0116","parentId":"e0115","timestamp":"2026-01-01T00:06:22.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 41, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0117","parentId":"e0116","timestamp":"2026-01-01T00:06:23.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 41, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0118","parentId":"e0117","timestamp":"2026-01-01T00:06:24.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 42, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0119","parentId":"e0118","timestamp":"2026-01-01T00:06:25.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 42, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0120","parentId":"e0119","timestamp":"2026-01-01T00:06:26.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 43, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0121","parentId":"e0120","timestamp":"2026-01-01T00:06:27.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 43, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0122","parentId":"e0121","timestamp":"2026-01-01T00:06:28.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 44, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0123","parentId":"e0122","timestamp":"2026-01-01T00:06:29.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 44, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0124","parentId":"e0123","timestamp":"2026-01-01T00:06:30.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 45, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0125","parentId":"e0124","timestamp":"2026-01-01T00:06:31.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 45, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0126","parentId":"e0125","timestamp":"2026-01-01T00:06:32.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 46, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0127","parentId":"e0126","timestamp":"2026-01-01T00:06:33.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 46, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0128","parentId":"e0127","timestamp":"2026-01-01T00:06:34.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 47, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0129","parentId":"e0128","timestamp":"2026-01-01T00:06:35.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 47, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0130","parentId":"e0129","timestamp":"2026-01-01T00:06:36.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 48, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0131","parentId":"e0130","timestamp":"2026-01-01T00:06:37.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 48, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0132","parentId":"e0131","timestamp":"2026-01-01T00:06:38.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 49, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0133","parentId":"e0132","timestamp":"2026-01-01T00:06:39.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 49, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0134","parentId":"e0133","timestamp":"2026-01-01T00:06:40.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 50, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0135","parentId":"e0134","timestamp":"2026-01-01T00:06:41.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 50, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0136","parentId":"e0135","timestamp":"2026-01-01T00:06:42.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 51, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0137","parentId":"e0136","timestamp":"2026-01-01T00:06:43.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 51, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0138","parentId":"e0137","timestamp":"2026-01-01T00:06:44.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 52, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0139","parentId":"e0138","timestamp":"2026-01-01T00:06:45.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 52, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0140","parentId":"e0139","timestamp":"2026-01-01T00:06:46.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 53, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0141","parentId":"e0140","timestamp":"2026-01-01T00:06:47.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 53, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0142","parentId":"e0141","timestamp":"2026-01-01T00:06:48.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 54, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0143","parentId":"e0142","timestamp":"2026-01-01T00:06:49.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 54, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0144","parentId":"e0143","timestamp":"2026-01-01T00:06:50.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 55, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0145","parentId":"e0144","timestamp":"2026-01-01T00:06:51.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 55, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0146","parentId":"e0145","timestamp":"2026-01-01T00:06:52.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 56, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0147","parentId":"e0146","timestamp":"2026-01-01T00:06:53.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 56, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0148","parentId":"e0147","timestamp":"2026-01-01T00:06:54.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 57, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0149","parentId":"e0148","timestamp":"2026-01-01T00:06:55.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 57, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0150","parentId":"e0149","timestamp":"2026-01-01T00:06:56.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 58, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0151","parentId":"e0150","timestamp":"2026-01-01T00:06:57.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 58, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0152","parentId":"e0151","timestamp":"2026-01-01T00:06:58.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 59, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0153","parentId":"e0152","timestamp":"2026-01-01T00:06:59.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 59, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0154","parentId":"e0153","timestamp":"2026-01-01T00:07:00.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 60, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0155","parentId":"e0154","timestamp":"2026-01-01T00:07:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 60, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0156","parentId":"e0155","timestamp":"2026-01-01T00:07:02.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 61, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0157","parentId":"e0156","timestamp":"2026-01-01T00:07:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 61, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0158","parentId":"e0157","timestamp":"2026-01-01T00:07:04.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 62, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0159","parentId":"e0158","timestamp":"2026-01-01T00:07:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 62, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0160","parentId":"e0159","timestamp":"2026-01-01T00:07:06.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 63, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0161","parentId":"e0160","timestamp":"2026-01-01T00:07:07.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 63, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0162","parentId":"e0161","timestamp":"2026-01-01T00:07:08.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 64, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0163","parentId":"e0162","timestamp":"2026-01-01T00:07:09.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 64, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0164","parentId":"e0163","timestamp":"2026-01-01T00:07:10.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 65, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0165","parentId":"e0164","timestamp":"2026-01-01T00:07:11.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 65, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0166","parentId":"e0165","timestamp":"2026-01-01T00:07:12.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 66, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0167","parentId":"e0166","timestamp":"2026-01-01T00:07:13.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 66, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0168","parentId":"e0167","timestamp":"2026-01-01T00:07:14.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 67, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0169","parentId":"e0168","timestamp":"2026-01-01T00:07:15.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 67, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0170","parentId":"e0169","timestamp":"2026-01-01T00:07:16.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 68, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0171","parentId":"e0170","timestamp":"2026-01-01T00:07:17.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 68, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0172","parentId":"e0171","timestamp":"2026-01-01T00:07:18.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 69, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0173","parentId":"e0172","timestamp":"2026-01-01T00:07:19.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 69, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0174","parentId":"e0173","timestamp":"2026-01-01T00:07:20.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 70, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0175","parentId":"e0174","timestamp":"2026-01-01T00:07:21.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 70, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0176","parentId":"e0175","timestamp":"2026-01-01T00:07:22.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 71, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0177","parentId":"e0176","timestamp":"2026-01-01T00:07:23.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 71, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0178","parentId":"e0177","timestamp":"2026-01-01T00:07:24.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 72, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0179","parentId":"e0178","timestamp":"2026-01-01T00:07:25.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 72, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0180","parentId":"e0179","timestamp":"2026-01-01T00:07:26.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 73, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0181","parentId":"e0180","timestamp":"2026-01-01T00:07:27.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 73, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0182","parentId":"e0181","timestamp":"2026-01-01T00:07:28.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 74, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0183","parentId":"e0182","timestamp":"2026-01-01T00:07:29.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 74, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0184","parentId":"e0183","timestamp":"2026-01-01T00:07:30.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 75, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0185","parentId":"e0184","timestamp":"2026-01-01T00:07:31.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 75, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0186","parentId":"e0185","timestamp":"2026-01-01T00:07:32.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 76, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0187","parentId":"e0186","timestamp":"2026-01-01T00:07:33.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 76, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0188","parentId":"e0187","timestamp":"2026-01-01T00:07:34.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 77, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0189","parentId":"e0188","timestamp":"2026-01-01T00:07:35.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 77, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0190","parentId":"e0189","timestamp":"2026-01-01T00:07:36.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 78, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0191","parentId":"e0190","timestamp":"2026-01-01T00:07:37.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 78, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0192","parentId":"e0191","timestamp":"2026-01-01T00:07:38.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 79, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0193","parentId":"e0192","timestamp":"2026-01-01T00:07:39.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 79, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0194","parentId":"e0193","timestamp":"2026-01-01T00:07:40.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 80, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0195","parentId":"e0194","timestamp":"2026-01-01T00:07:41.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 80, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0196","parentId":"e0195","timestamp":"2026-01-01T00:07:42.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 81, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0197","parentId":"e0196","timestamp":"2026-01-01T00:07:43.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 81, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0198","parentId":"e0197","timestamp":"2026-01-01T00:07:44.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 82, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0199","parentId":"e0198","timestamp":"2026-01-01T00:07:45.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 82, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0200","parentId":"e0199","timestamp":"2026-01-01T00:07:46.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 83, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0201","parentId":"e0200","timestamp":"2026-01-01T00:07:47.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 83, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0202","parentId":"e0201","timestamp":"2026-01-01T00:07:48.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 84, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0203","parentId":"e0202","timestamp":"2026-01-01T00:07:49.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 84, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0204","parentId":"e0203","timestamp":"2026-01-01T00:07:50.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 85, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0205","parentId":"e0204","timestamp":"2026-01-01T00:07:51.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 85, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0206","parentId":"e0205","timestamp":"2026-01-01T00:07:52.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 86, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0207","parentId":"e0206","timestamp":"2026-01-01T00:07:53.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 86, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0208","parentId":"e0207","timestamp":"2026-01-01T00:07:54.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 87, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0209","parentId":"e0208","timestamp":"2026-01-01T00:07:55.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 87, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0210","parentId":"e0209","timestamp":"2026-01-01T00:07:56.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 88, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0211","parentId":"e0210","timestamp":"2026-01-01T00:07:57.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 88, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0212","parentId":"e0211","timestamp":"2026-01-01T00:07:58.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 89, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0213","parentId":"e0212","timestamp":"2026-01-01T00:07:59.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 89, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0214","parentId":"e0213","timestamp":"2026-01-01T00:08:00.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 90, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0215","parentId":"e0214","timestamp":"2026-01-01T00:08:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 90, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0216","parentId":"e0215","timestamp":"2026-01-01T00:08:02.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 91, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0217","parentId":"e0216","timestamp":"2026-01-01T00:08:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 91, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0218","parentId":"e0217","timestamp":"2026-01-01T00:08:04.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 92, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0219","parentId":"e0218","timestamp":"2026-01-01T00:08:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 92, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0220","parentId":"e0219","timestamp":"2026-01-01T00:08:06.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 93, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0221","parentId":"e0220","timestamp":"2026-01-01T00:08:07.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 93, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0222","parentId":"e0221","timestamp":"2026-01-01T00:08:08.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 94, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0223","parentId":"e0222","timestamp":"2026-01-01T00:08:09.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 94, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0224","parentId":"e0223","timestamp":"2026-01-01T00:08:10.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 95, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0225","parentId":"e0224","timestamp":"2026-01-01T00:08:11.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 95, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0226","parentId":"e0225","timestamp":"2026-01-01T00:08:12.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 96, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0227","parentId":"e0226","timestamp":"2026-01-01T00:08:13.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 96, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0228","parentId":"e0227","timestamp":"2026-01-01T00:08:14.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 97, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0229","parentId":"e0228","timestamp":"2026-01-01T00:08:15.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 97, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0230","parentId":"e0229","timestamp":"2026-01-01T00:08:16.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 98, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0231","parentId":"e0230","timestamp":"2026-01-01T00:08:17.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 98, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0232","parentId":"e0231","timestamp":"2026-01-01T00:08:18.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 99, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0233","parentId":"e0232","timestamp":"2026-01-01T00:08:19.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 99, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0234","parentId":"e0233","timestamp":"2026-01-01T00:08:20.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 100, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0235","parentId":"e0234","timestamp":"2026-01-01T00:08:21.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 100, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0236","parentId":"e0235","timestamp":"2026-01-01T00:08:22.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 101, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0237","parentId":"e0236","timestamp":"2026-01-01T00:08:23.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 101, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0238","parentId":"e0237","timestamp":"2026-01-01T00:08:24.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 102, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0239","parentId":"e0238","timestamp":"2026-01-01T00:08:25.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 102, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0240","parentId":"e0239","timestamp":"2026-01-01T00:08:26.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 103, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0241","parentId":"e0240","timestamp":"2026-01-01T00:08:27.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 103, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0242","parentId":"e0241","timestamp":"2026-01-01T00:08:28.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 104, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0243","parentId":"e0242","timestamp":"2026-01-01T00:08:29.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 104, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0244","parentId":"e0243","timestamp":"2026-01-01T00:08:30.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 105, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0245","parentId":"e0244","timestamp":"2026-01-01T00:08:31.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 105, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0246","parentId":"e0245","timestamp":"2026-01-01T00:08:32.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 106, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0247","parentId":"e0246","timestamp":"2026-01-01T00:08:33.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 106, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0248","parentId":"e0247","timestamp":"2026-01-01T00:08:34.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 107, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0249","parentId":"e0248","timestamp":"2026-01-01T00:08:35.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 107, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0250","parentId":"e0249","timestamp":"2026-01-01T00:08:36.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 108, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0251","parentId":"e0250","timestamp":"2026-01-01T00:08:37.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 108, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0252","parentId":"e0251","timestamp":"2026-01-01T00:08:38.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 109, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0253","parentId":"e0252","timestamp":"2026-01-01T00:08:39.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 109, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0254","parentId":"e0253","timestamp":"2026-01-01T00:08:40.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 110, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0255","parentId":"e0254","timestamp":"2026-01-01T00:08:41.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 110, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0256","parentId":"e0255","timestamp":"2026-01-01T00:08:42.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 111, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0257","parentId":"e0256","timestamp":"2026-01-01T00:08:43.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 111, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0258","parentId":"e0257","timestamp":"2026-01-01T00:08:44.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 112, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0259","parentId":"e0258","timestamp":"2026-01-01T00:08:45.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 112, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0260","parentId":"e0259","timestamp":"2026-01-01T00:08:46.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 113, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0261","parentId":"e0260","timestamp":"2026-01-01T00:08:47.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 113, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0262","parentId":"e0261","timestamp":"2026-01-01T00:08:48.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 114, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0263","parentId":"e0262","timestamp":"2026-01-01T00:08:49.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 114, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0264","parentId":"e0263","timestamp":"2026-01-01T00:08:50.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 115, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0265","parentId":"e0264","timestamp":"2026-01-01T00:08:51.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 115, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0266","parentId":"e0265","timestamp":"2026-01-01T00:08:52.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 116, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0267","parentId":"e0266","timestamp":"2026-01-01T00:08:53.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 116, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0268","parentId":"e0267","timestamp":"2026-01-01T00:08:54.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 117, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0269","parentId":"e0268","timestamp":"2026-01-01T00:08:55.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 117, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0270","parentId":"e0269","timestamp":"2026-01-01T00:08:56.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 118, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0271","parentId":"e0270","timestamp":"2026-01-01T00:08:57.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 118, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0272","parentId":"e0271","timestamp":"2026-01-01T00:08:58.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 119, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0273","parentId":"e0272","timestamp":"2026-01-01T00:08:59.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 119, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0274","parentId":"e0273","timestamp":"2026-01-01T00:09:00.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 120, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0275","parentId":"e0274","timestamp":"2026-01-01T00:09:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 120, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0276","parentId":"e0275","timestamp":"2026-01-01T00:09:02.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 121, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0277","parentId":"e0276","timestamp":"2026-01-01T00:09:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 121, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0278","parentId":"e0277","timestamp":"2026-01-01T00:09:04.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 122, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0279","parentId":"e0278","timestamp":"2026-01-01T00:09:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 122, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0280","parentId":"e0279","timestamp":"2026-01-01T00:09:06.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 123, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0281","parentId":"e0280","timestamp":"2026-01-01T00:09:07.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 123, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0282","parentId":"e0281","timestamp":"2026-01-01T00:09:08.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 124, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0283","parentId":"e0282","timestamp":"2026-01-01T00:09:09.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 124, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0284","parentId":"e0283","timestamp":"2026-01-01T00:09:10.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 125, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0285","parentId":"e0284","timestamp":"2026-01-01T00:09:11.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 125, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0286","parentId":"e0285","timestamp":"2026-01-01T00:09:12.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 126, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0287","parentId":"e0286","timestamp":"2026-01-01T00:09:13.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 126, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0288","parentId":"e0287","timestamp":"2026-01-01T00:09:14.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 127, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0289","parentId":"e0288","timestamp":"2026-01-01T00:09:15.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 127, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0290","parentId":"e0289","timestamp":"2026-01-01T00:09:16.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 128, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0291","parentId":"e0290","timestamp":"2026-01-01T00:09:17.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 128, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0292","parentId":"e0291","timestamp":"2026-01-01T00:09:18.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 129, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0293","parentId":"e0292","timestamp":"2026-01-01T00:09:19.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 129, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0294","parentId":"e0293","timestamp":"2026-01-01T00:09:20.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 130, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0295","parentId":"e0294","timestamp":"2026-01-01T00:09:21.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 130, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0296","parentId":"e0295","timestamp":"2026-01-01T00:09:22.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 131, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0297","parentId":"e0296","timestamp":"2026-01-01T00:09:23.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 131, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0298","parentId":"e0297","timestamp":"2026-01-01T00:09:24.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 132, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0299","parentId":"e0298","timestamp":"2026-01-01T00:09:25.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 132, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0300","parentId":"e0299","timestamp":"2026-01-01T00:09:26.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 133, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0301","parentId":"e0300","timestamp":"2026-01-01T00:09:27.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 133, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0302","parentId":"e0301","timestamp":"2026-01-01T00:09:28.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 134, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0303","parentId":"e0302","timestamp":"2026-01-01T00:09:29.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 134, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0304","parentId":"e0303","timestamp":"2026-01-01T00:09:30.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 135, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0305","parentId":"e0304","timestamp":"2026-01-01T00:09:31.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 135, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0306","parentId":"e0305","timestamp":"2026-01-01T00:09:32.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 136, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0307","parentId":"e0306","timestamp":"2026-01-01T00:09:33.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 136, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0308","parentId":"e0307","timestamp":"2026-01-01T00:09:34.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 137, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0309","parentId":"e0308","timestamp":"2026-01-01T00:09:35.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 137, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0310","parentId":"e0309","timestamp":"2026-01-01T00:09:36.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 138, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0311","parentId":"e0310","timestamp":"2026-01-01T00:09:37.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 138, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0312","parentId":"e0311","timestamp":"2026-01-01T00:09:38.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 139, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0313","parentId":"e0312","timestamp":"2026-01-01T00:09:39.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 139, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0314","parentId":"e0313","timestamp":"2026-01-01T00:09:40.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 140, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0315","parentId":"e0314","timestamp":"2026-01-01T00:09:41.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 140, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0316","parentId":"e0315","timestamp":"2026-01-01T00:09:42.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 141, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0317","parentId":"e0316","timestamp":"2026-01-01T00:09:43.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 141, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0318","parentId":"e0317","timestamp":"2026-01-01T00:09:44.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 142, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0319","parentId":"e0318","timestamp":"2026-01-01T00:09:45.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 142, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0320","parentId":"e0319","timestamp":"2026-01-01T00:09:46.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 143, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0321","parentId":"e0320","timestamp":"2026-01-01T00:09:47.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 143, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0322","parentId":"e0321","timestamp":"2026-01-01T00:09:48.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 144, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0323","parentId":"e0322","timestamp":"2026-01-01T00:09:49.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 144, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0324","parentId":"e0323","timestamp":"2026-01-01T00:09:50.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 145, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0325","parentId":"e0324","timestamp":"2026-01-01T00:09:51.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 145, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0326","parentId":"e0325","timestamp":"2026-01-01T00:09:52.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 146, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0327","parentId":"e0326","timestamp":"2026-01-01T00:09:53.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 146, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0328","parentId":"e0327","timestamp":"2026-01-01T00:09:54.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 147, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0329","parentId":"e0328","timestamp":"2026-01-01T00:09:55.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 147, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0330","parentId":"e0329","timestamp":"2026-01-01T00:09:56.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 148, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0331","parentId":"e0330","timestamp":"2026-01-01T00:09:57.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 148, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0332","parentId":"e0331","timestamp":"2026-01-01T00:09:58.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 149, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0333","parentId":"e0332","timestamp":"2026-01-01T00:09:59.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 149, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0334","parentId":"e0333","timestamp":"2026-01-01T00:10:00.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 150, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0335","parentId":"e0334","timestamp":"2026-01-01T00:10:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 150, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0336","parentId":"e0335","timestamp":"2026-01-01T00:10:02.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 151, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0337","parentId":"e0336","timestamp":"2026-01-01T00:10:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 151, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0338","parentId":"e0337","timestamp":"2026-01-01T00:10:04.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 152, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0339","parentId":"e0338","timestamp":"2026-01-01T00:10:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 152, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0340","parentId":"e0339","timestamp":"2026-01-01T00:10:06.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 153, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0341","parentId":"e0340","timestamp":"2026-01-01T00:10:07.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 153, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0342","parentId":"e0341","timestamp":"2026-01-01T00:10:08.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 154, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0343","parentId":"e0342","timestamp":"2026-01-01T00:10:09.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 154, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0344","parentId":"e0343","timestamp":"2026-01-01T00:10:10.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 155, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0345","parentId":"e0344","timestamp":"2026-01-01T00:10:11.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 155, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0346","parentId":"e0345","timestamp":"2026-01-01T00:10:12.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 156, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0347","parentId":"e0346","timestamp":"2026-01-01T00:10:13.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 156, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0348","parentId":"e0347","timestamp":"2026-01-01T00:10:14.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 157, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0349","parentId":"e0348","timestamp":"2026-01-01T00:10:15.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 157, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0350","parentId":"e0349","timestamp":"2026-01-01T00:10:16.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 158, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0351","parentId":"e0350","timestamp":"2026-01-01T00:10:17.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 158, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0352","parentId":"e0351","timestamp":"2026-01-01T00:10:18.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 159, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0353","parentId":"e0352","timestamp":"2026-01-01T00:10:19.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 159, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0354","parentId":"e0353","timestamp":"2026-01-01T00:10:20.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 160, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0355","parentId":"e0354","timestamp":"2026-01-01T00:10:21.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 160, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0356","parentId":"e0355","timestamp":"2026-01-01T00:10:22.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 161, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0357","parentId":"e0356","timestamp":"2026-01-01T00:10:23.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 161, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0358","parentId":"e0357","timestamp":"2026-01-01T00:10:24.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 162, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0359","parentId":"e0358","timestamp":"2026-01-01T00:10:25.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 162, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0360","parentId":"e0359","timestamp":"2026-01-01T00:10:26.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 163, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0361","parentId":"e0360","timestamp":"2026-01-01T00:10:27.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 163, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0362","parentId":"e0361","timestamp":"2026-01-01T00:10:28.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 164, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0363","parentId":"e0362","timestamp":"2026-01-01T00:10:29.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 164, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0364","parentId":"e0363","timestamp":"2026-01-01T00:10:30.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 165, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0365","parentId":"e0364","timestamp":"2026-01-01T00:10:31.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 165, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0366","parentId":"e0365","timestamp":"2026-01-01T00:10:32.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 166, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0367","parentId":"e0366","timestamp":"2026-01-01T00:10:33.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 166, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0368","parentId":"e0367","timestamp":"2026-01-01T00:10:34.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 167, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0369","parentId":"e0368","timestamp":"2026-01-01T00:10:35.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 167, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0370","parentId":"e0369","timestamp":"2026-01-01T00:10:36.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 168, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0371","parentId":"e0370","timestamp":"2026-01-01T00:10:37.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 168, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0372","parentId":"e0371","timestamp":"2026-01-01T00:10:38.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 169, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0373","parentId":"e0372","timestamp":"2026-01-01T00:10:39.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 169, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0374","parentId":"e0373","timestamp":"2026-01-01T00:10:40.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 170, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0375","parentId":"e0374","timestamp":"2026-01-01T00:10:41.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 170, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0376","parentId":"e0375","timestamp":"2026-01-01T00:10:42.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 171, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0377","parentId":"e0376","timestamp":"2026-01-01T00:10:43.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 171, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0378","parentId":"e0377","timestamp":"2026-01-01T00:10:44.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 172, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0379","parentId":"e0378","timestamp":"2026-01-01T00:10:45.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 172, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0380","parentId":"e0379","timestamp":"2026-01-01T00:10:46.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 173, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0381","parentId":"e0380","timestamp":"2026-01-01T00:10:47.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 173, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0382","parentId":"e0381","timestamp":"2026-01-01T00:10:48.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 174, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0383","parentId":"e0382","timestamp":"2026-01-01T00:10:49.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 174, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0384","parentId":"e0383","timestamp":"2026-01-01T00:10:50.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 175, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0385","parentId":"e0384","timestamp":"2026-01-01T00:10:51.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 175, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0386","parentId":"e0385","timestamp":"2026-01-01T00:10:52.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 176, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0387","parentId":"e0386","timestamp":"2026-01-01T00:10:53.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 176, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0388","parentId":"e0387","timestamp":"2026-01-01T00:10:54.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 177, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0389","parentId":"e0388","timestamp":"2026-01-01T00:10:55.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 177, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0390","parentId":"e0389","timestamp":"2026-01-01T00:10:56.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 178, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0391","parentId":"e0390","timestamp":"2026-01-01T00:10:57.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 178, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0392","parentId":"e0391","timestamp":"2026-01-01T00:10:58.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 179, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0393","parentId":"e0392","timestamp":"2026-01-01T00:10:59.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 179, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0394","parentId":"e0393","timestamp":"2026-01-01T00:11:00.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 180, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0395","parentId":"e0394","timestamp":"2026-01-01T00:11:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 180, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0396","parentId":"e0395","timestamp":"2026-01-01T00:11:02.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 181, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0397","parentId":"e0396","timestamp":"2026-01-01T00:11:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 181, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0398","parentId":"e0397","timestamp":"2026-01-01T00:11:04.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 182, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0399","parentId":"e0398","timestamp":"2026-01-01T00:11:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 182, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0400","parentId":"e0399","timestamp":"2026-01-01T00:11:06.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 183, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0401","parentId":"e0400","timestamp":"2026-01-01T00:11:07.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 183, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0402","parentId":"e0401","timestamp":"2026-01-01T00:11:08.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 184, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0403","parentId":"e0402","timestamp":"2026-01-01T00:11:09.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 184, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0404","parentId":"e0403","timestamp":"2026-01-01T00:11:10.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 185, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0405","parentId":"e0404","timestamp":"2026-01-01T00:11:11.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 185, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0406","parentId":"e0405","timestamp":"2026-01-01T00:11:12.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 186, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0407","parentId":"e0406","timestamp":"2026-01-01T00:11:13.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 186, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0408","parentId":"e0407","timestamp":"2026-01-01T00:11:14.000Z","message":{"role":"user","content":[{"type":"text","text":"live turn 187, this is the branch the supervisor should read"}]}}
|
||||
{"type":"message","id":"e0409","parentId":"e0408","timestamp":"2026-01-01T00:11:15.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply 187, this is the branch the supervisor should read"}]}}
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
/** Test-only Herdr adapter. Starts a real supervisor Pi in RPC mode, never a live pane. */
|
||||
import { spawn } from "node:child_process";
|
||||
import { appendFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import goals from "../../src/index.js";
|
||||
|
||||
export default function testHost(pi: ExtensionAPI): void {
|
||||
const children: ReturnType<typeof spawn>[] = [];
|
||||
goals({ ...pi, exec: async (command, args, options) => {
|
||||
if (command !== "herdr") return pi.exec(command, args, options);
|
||||
if (args[0] === "pane") return { code: 0, stdout: JSON.stringify({ result: { pane: { pane_id: "test-supervisor-pane" } } }), stderr: "", killed: false };
|
||||
if (args[1] === "start") {
|
||||
const native = args.slice(args.indexOf("--") + 1);
|
||||
// The worker's test-only -e host must not become the supervisor package. The fixture
|
||||
// profile points at the untouched packed package; override only the UI mode for this test.
|
||||
const child = spawn(process.execPath, [process.argv[1], "--mode", "rpc", "--session", native[native.indexOf("--session") + 1], "--model", "offline/test"], {
|
||||
cwd: process.cwd(), env: process.env, stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
children.push(child);
|
||||
const log = join(process.cwd(), "supervisor-rpc.jsonl");
|
||||
child.stdout?.on("data", chunk => appendFileSync(log, chunk));
|
||||
child.stderr?.on("data", chunk => appendFileSync(join(process.cwd(), "supervisor-stderr.log"), chunk));
|
||||
}
|
||||
return { code: 0, stdout: "{}", stderr: "", killed: false };
|
||||
} });
|
||||
pi.on("session_shutdown", () => { for (const child of children) child.kill(); });
|
||||
}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
export default function offlineModel(pi: ExtensionAPI): void {
|
||||
pi.registerProvider("offline", {
|
||||
baseUrl: process.env.PI_GOALS_OFFLINE_MODEL_URL!,
|
||||
apiKey: "test",
|
||||
api: "openai-completions",
|
||||
models: [{
|
||||
id: "test",
|
||||
name: "Offline test model",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 16_000,
|
||||
maxTokens: 1_000,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
}],
|
||||
});
|
||||
}
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
import { type ModelRole, RoleModels } from "../../src/role-models.js";
|
||||
|
||||
const [directory, role, provider, id] = process.argv.slice(2);
|
||||
let selected: (event: any, ctx: any) => void;
|
||||
const pi = { on(_name: string, handler: any) { selected = handler; }, setModel: async () => true };
|
||||
const ctx = { model: { provider, id }, modelRegistry: { find: (provider: string, id: string) => ({ provider, id }) }, ui: { notify(message: string) { throw new Error(message); } } };
|
||||
const models = new RoleModels(pi as unknown as ExtensionAPI, directory);
|
||||
await models.enter(role as ModelRole, ctx as unknown as ExtensionContext);
|
||||
selected!({ source: "cycle", model: { provider, id } }, ctx);
|
||||
@@ -0,0 +1,769 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import piGoalsExtension from "../src/index.js";
|
||||
|
||||
vi.mock("../src/internal/supervisor/index.js", () => ({ default: () => ({ pause() {}, reconnect: async () => {}, status: async () => ({ connected: false, activity: "inactive" }) }) }));
|
||||
const judgeRun = vi.hoisted(() => ({ output: "", beforeReply: undefined as (() => void) | undefined, calls: [] as string[][] }));
|
||||
vi.mock("node:child_process", async (original) => {
|
||||
const actual = await original<typeof import("node:child_process")>();
|
||||
return { ...actual, spawn: (_command: string, args: string[]) => {
|
||||
judgeRun.calls.push(args);
|
||||
const proc = Object.assign(new EventEmitter(), { stdout: new EventEmitter(), stderr: new EventEmitter(), kill() {} });
|
||||
queueMicrotask(() => { judgeRun.beforeReply?.(); proc.stdout.emit("data", judgeRun.output); proc.emit("close", 0); });
|
||||
return proc;
|
||||
} };
|
||||
});
|
||||
|
||||
function setup(
|
||||
selectChoices: Array<string | undefined>,
|
||||
editorChoices: Array<string | undefined> = [],
|
||||
editPlan?: () => Promise<string | undefined>,
|
||||
) {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-flow-"));
|
||||
const commands = new Map<string, any>();
|
||||
const hooks = new Map<string, any>();
|
||||
const tools = new Map<string, any>();
|
||||
const entries: Array<{ type: string; customType: string; data?: unknown; details?: unknown; content?: string }> = [];
|
||||
const events: string[] = [];
|
||||
const messages: Array<{ content: string; display?: boolean; customType?: string }> = [];
|
||||
const busHandlers = new Map<string, Set<(value: unknown) => unknown>>();
|
||||
const bus = {
|
||||
on(name: string, handler: (value: unknown) => unknown) {
|
||||
const handlers = busHandlers.get(name) ?? new Set();
|
||||
handlers.add(handler);
|
||||
busHandlers.set(name, handlers);
|
||||
return () => handlers.delete(handler);
|
||||
},
|
||||
emit(name: string, value: unknown) {
|
||||
for (const handler of busHandlers.get(name) ?? []) void handler(value);
|
||||
},
|
||||
};
|
||||
const ctx = {
|
||||
cwd,
|
||||
model: { provider: "offline", id: "test" },
|
||||
modelRegistry: { find: (provider: string, id: string) => ({ provider, id }) },
|
||||
hasUI: true,
|
||||
isIdle: () => true,
|
||||
abort: vi.fn(),
|
||||
sessionManager: { getSessionId: () => "session-a", getSessionFile: () => join(cwd, "session-a.jsonl"), getEntries: () => entries, getBranch: () => entries },
|
||||
ui: {
|
||||
theme: { fg: (_kind: string, text: string) => text },
|
||||
setStatus: vi.fn(),
|
||||
setWidget: vi.fn(),
|
||||
notify: vi.fn(),
|
||||
select: async () => {
|
||||
events.push("select");
|
||||
return selectChoices.shift();
|
||||
},
|
||||
editor: async () => {
|
||||
events.push("editor");
|
||||
return editPlan ? editPlan() : editorChoices.shift();
|
||||
},
|
||||
},
|
||||
};
|
||||
let active = ["read", "write", "edit", "bash", "CompleteGoal", "RequestPlanReview"];
|
||||
const pi = {
|
||||
getActiveTools: () => active,
|
||||
setActiveTools: (names: string[]) => { active = names; },
|
||||
setModel: vi.fn(async (model: { provider: string; id: string }) => { const previousModel = ctx.model; ctx.model = model; await hooks.get("model_select")?.({ model, previousModel, source: "set" }, ctx); return true; }),
|
||||
registerCommand: (name: string, command: any) => commands.set(name, command),
|
||||
on: (name: string, handler: any) => { const previous = hooks.get(name); hooks.set(name, async (...args: any[]) => { const prior = await previous?.(...args); return await handler(...args) ?? prior; }); },
|
||||
appendEntry: (customType: string, data: unknown) => entries.push({ type: "custom", customType, data }),
|
||||
registerTool: (tool: any) => tools.set(tool.name, tool),
|
||||
events: bus,
|
||||
sendMessage: (message: { content: string; display?: boolean; customType?: string }) => {
|
||||
events.push("display");
|
||||
messages.push(message);
|
||||
},
|
||||
sendUserMessage: (message: string) => messages.push({ content: message }),
|
||||
};
|
||||
piGoalsExtension(pi as unknown as ExtensionAPI);
|
||||
return { pi, bus, commands, ctx, cwd, entries, events, hooks, messages, tools };
|
||||
}
|
||||
|
||||
async function promptReminder(flow: ReturnType<typeof setup>) {
|
||||
const result = await flow.hooks.get("before_agent_start")({}, flow.ctx);
|
||||
if (result?.message) flow.entries.push({ type: "custom_message", ...result.message });
|
||||
await flow.hooks.get("context")({ messages: [] }, flow.ctx);
|
||||
return result?.message;
|
||||
}
|
||||
|
||||
async function settleDraft(flow: ReturnType<typeof setup>) {
|
||||
await flow.tools.get("RequestPlanReview").execute("", {}, undefined, undefined, flow.ctx);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
}
|
||||
|
||||
describe("/goals recovery", () => {
|
||||
it("help, status and invalid recovery arguments never start a plan", async () => {
|
||||
const f = setup([]);
|
||||
try {
|
||||
for (const text of ["help", "status", "stop now", "exit now", "reconnect now", "resume now", "--unknown", "connect", "reconect"]) await f.commands.get("goals").handler(text, f.ctx);
|
||||
expect(f.messages).toHaveLength(0);
|
||||
expect(f.entries.filter(e => e.customType === "pi-goals-state")).toHaveLength(0);
|
||||
} finally { rmSync(f.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it.each(["stop", "exit"])("%s keeps the draft, exits the gate and persists through reload and chat", async command => {
|
||||
const f = setup([]);
|
||||
try {
|
||||
await f.commands.get("goals").handler("plan recovery fixture", f.ctx);
|
||||
const path = join(f.cwd, ".pi/plan/session-a-v1.md");
|
||||
writeFileSync(path, "1. [ ] goal: retain this draft\n");
|
||||
await f.commands.get("goals").handler(command, f.ctx);
|
||||
const count = f.messages.length;
|
||||
await f.hooks.get("session_start")({}, f.ctx);
|
||||
await f.hooks.get("input")({ text: "ordinary chat", source: "interactive" }, f.ctx);
|
||||
await f.hooks.get("agent_settled")({}, f.ctx);
|
||||
await f.hooks.get("session_compact")({}, f.ctx);
|
||||
expect(await f.hooks.get("before_agent_start")({}, f.ctx)).toBeUndefined();
|
||||
expect(f.messages).toHaveLength(count);
|
||||
expect(await f.hooks.get("input")({ text: "Process finished", source: "extension" }, f.ctx)).toBeUndefined();
|
||||
expect(await f.hooks.get("input")({ text: "Work the goals in stale.md", source: "extension" }, f.ctx)).toEqual({ action: "handled" });
|
||||
expect(readFileSync(path, "utf8")).toBe("1. [ ] goal: retain this draft\n");
|
||||
expect(await f.hooks.get("tool_call")({ toolName: "write", input: { path: "other.txt" } }, f.ctx)).toBeUndefined();
|
||||
expect((f.entries.filter(e => e.customType === "pi-goals-state").at(-1)?.data as any).pausedFrom).toBe("planning");
|
||||
await f.commands.get("goals").handler("resume", f.ctx);
|
||||
expect(f.messages).toHaveLength(count); // returning to a draft is NOT Ready
|
||||
expect((f.entries.filter(e => e.customType === "pi-goals-state").at(-1)?.data as any).phase).toBe("planning");
|
||||
} finally { rmSync(f.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("stop invalidates an outstanding Ready selection", async () => {
|
||||
const f = setup([]);
|
||||
try {
|
||||
await f.commands.get("goals").handler("plan cancel selection", f.ctx);
|
||||
writeFileSync(join(f.cwd, ".pi/plan/session-a-v1.md"), "1. [ ] goal: not authorized\n");
|
||||
let answer!: (s: string) => void;
|
||||
f.ctx.ui.select = () => new Promise<string>(resolve => { answer = resolve; });
|
||||
const selecting = settleDraft(f);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
await f.commands.get("goals").handler("stop", f.ctx);
|
||||
answer("Ready"); await selecting;
|
||||
expect(f.messages.some(m => m.content.startsWith("Work the goals"))).toBe(false);
|
||||
expect((f.entries.filter(e => e.customType === "pi-goals-state").at(-1)?.data as any).phase).toBeNull();
|
||||
} finally { rmSync(f.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("resume starts unchanged authorized work but a changed plan returns to review", async () => {
|
||||
const f = setup(["Ready"]);
|
||||
try {
|
||||
await f.commands.get("goals").handler("plan bounded work", f.ctx);
|
||||
await f.commands.get("goals").handler("steward off", f.ctx);
|
||||
const path = join(f.cwd, ".pi/plan/session-a-v1.md");
|
||||
writeFileSync(path, "1. [ ] goal: first\n");
|
||||
await settleDraft(f);
|
||||
await f.commands.get("goals").handler("stop", f.ctx);
|
||||
let count = f.messages.length;
|
||||
await f.commands.get("goals").handler("resume", f.ctx);
|
||||
expect(f.messages.length).toBe(count + 1);
|
||||
await f.commands.get("goals").handler("exit", f.ctx);
|
||||
writeFileSync(path, "1. [ ] goal: changed scope\n");
|
||||
count = f.messages.length;
|
||||
await f.commands.get("goals").handler("resume", f.ctx);
|
||||
expect(f.messages).toHaveLength(count);
|
||||
expect((f.entries.filter(e => e.customType === "pi-goals-state").at(-1)?.data as any).phase).toBe("planning");
|
||||
} finally { await f.hooks.get("session_shutdown")({}, f.ctx); rmSync(f.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
});
|
||||
|
||||
describe("/goals draft flow", () => {
|
||||
it.each(["accept", "inconclusive", "reject", "abort", "shutdown", "stop", "plan change"])("records only legitimate sign-offs on a dirty repo with ignored artifacts: %s", async (verdict) => {
|
||||
const flow = setup([]); const fresh = setup([]); const abort = new AbortController();
|
||||
try {
|
||||
execFileSync("git", ["init", "-q", flow.cwd]);
|
||||
writeFileSync(join(flow.cwd, ".gitignore"), "outputs/\n.pi/\n");
|
||||
writeFileSync(join(flow.cwd, "unrelated.txt"), "preserve this unrelated file\n");
|
||||
execFileSync("git", ["-C", flow.cwd, "add", "unrelated.txt"]); // isolated fixture only, no commit
|
||||
writeFileSync(join(flow.cwd, "unrelated.txt"), "preserve this unrelated dirty edit\n");
|
||||
mkdirSync(join(flow.cwd, "outputs"));
|
||||
writeFileSync(join(flow.cwd, "outputs/artifact.txt"), "hello\n");
|
||||
const observed = execFileSync(process.execPath, ["-e", "const fs=require('fs'); if(fs.readFileSync('outputs/artifact.txt','utf8')!=='hello\\n') process.exit(1); console.log('PASS exact bytes');"], { cwd: flow.cwd, encoding: "utf8" });
|
||||
writeFileSync(join(flow.cwd, "outputs/verify.log"), observed);
|
||||
expect(execFileSync("git", ["-C", flow.cwd, "check-ignore", "outputs/verify.log"], { encoding: "utf8" })).toContain("outputs/verify.log");
|
||||
const dirty = execFileSync("git", ["-C", flow.cwd, "status", "--porcelain"], { encoding: "utf8" });
|
||||
expect(dirty).toContain("AM unrelated.txt");
|
||||
mkdirSync(join(flow.cwd, ".pi/plan"), { recursive: true });
|
||||
const file = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
writeFileSync(file, "# Plan\n1. [x] goal: exact output\n - evidence: outputs/artifact.txt `hello`; outputs/verify.log `PASS exact bytes` from node byte check\n");
|
||||
flow.entries.push({ type: "custom", customType: "pi-goals-state", data: { phase: "working", planVersion: 1, signedOffGoals: [], stewardEnabled: false, autoIntervalMs: null } });
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
judgeRun.calls = [];
|
||||
judgeRun.output = verdict === "inconclusive" ? "No verdict from judge" : verdict === "reject" ? "VERDICT: reject\nmissing: test failed" : `checks:\n- outputs/verify.log: \`${readFileSync(join(flow.cwd, "outputs/verify.log"), "utf8").trim()}\`; execution passed\nVERDICT: accept\nmissing:`;
|
||||
judgeRun.beforeReply = () => {
|
||||
if (verdict === "abort") abort.abort();
|
||||
if (verdict === "shutdown") void flow.hooks.get("session_shutdown")({}, flow.ctx);
|
||||
if (verdict === "stop") void flow.commands.get("goals").handler("stop", flow.ctx);
|
||||
if (verdict === "plan change") writeFileSync(file, readFileSync(file, "utf8") + "\nNew scope requiring review\n");
|
||||
};
|
||||
const outcome = await flow.tools.get("CompleteGoal").execute("", { goal: " EXACT OUTPUT " }, abort.signal, undefined, flow.ctx);
|
||||
const signed = verdict === "accept" || verdict === "inconclusive";
|
||||
expect(outcome.isError).toBe(!signed);
|
||||
expect(judgeRun.calls).toHaveLength(1);
|
||||
expect(judgeRun.calls[0]).toContain("--no-extensions");
|
||||
expect(judgeRun.calls[0]).toContain("read,grep,find,ls");
|
||||
expect(readFileSync(file, "utf8").includes("[x] goal: exact output")).toBe(signed);
|
||||
if (verdict === "inconclusive") expect(outcome.content[0].text).toContain("not verified completion");
|
||||
expect(readFileSync(join(flow.cwd, "unrelated.txt"), "utf8")).toBe("preserve this unrelated dirty edit\n");
|
||||
expect(execFileSync("git", ["-C", flow.cwd, "status", "--porcelain"], { encoding: "utf8" })).toBe(dirty);
|
||||
fresh.entries.push(...structuredClone(flow.entries));
|
||||
fresh.ctx.cwd = flow.cwd; // genuinely new extension instance, same plan and persisted entries
|
||||
await fresh.hooks.get("session_start")({}, fresh.ctx);
|
||||
expect(fresh.ctx.ui.setStatus.mock.lastCall?.[1]).toContain(verdict === "stop" ? "goals stopped" : signed ? "1/1" : "0/1");
|
||||
expect(fresh.messages).toHaveLength(0);
|
||||
} finally {
|
||||
judgeRun.beforeReply = undefined; judgeRun.calls = [];
|
||||
await flow.hooks.get("session_shutdown")({}, flow.ctx); await fresh.hooks.get("session_shutdown")({}, fresh.ctx);
|
||||
rmSync(flow.cwd, { recursive: true, force: true }); rmSync(fresh.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves legacy completion on reload without inventing sign-off or restarting work", async () => {
|
||||
vi.useFakeTimers(); const flow = setup([]);
|
||||
try {
|
||||
mkdirSync(join(flow.cwd, ".pi/plan"), { recursive: true });
|
||||
const file = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
const plan = '1. [x] goal: historical result\n\n## Log\n- signed off "historical result" (judge accept)\n';
|
||||
writeFileSync(file, plan);
|
||||
flow.entries.push({ type: "custom", customType: "pi-goals-state", data: { phase: "working", planVersion: 1, stewardEnabled: false, autoIntervalMs: 1000 } });
|
||||
for (let n = 0; n < 2; n++) {
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
expect(flow.ctx.ui.setWidget.mock.lastCall?.[1]?.join("\n")).toContain("legacy completion — sign-off not recorded");
|
||||
expect(flow.messages).toHaveLength(0);
|
||||
expect(readFileSync(file, "utf8")).toBe(plan);
|
||||
}
|
||||
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); vi.useRealTimers(); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
it("keeps unsigned manual ticks visible across reload without reopening legitimate sign-offs", async () => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
flow.entries.push({ type: "custom", customType: "pi-goals-state", data: { phase: "working", planVersion: 1, stewardEnabled: false, autoIntervalMs: null, signedOffGoals: [{ subject: "first", outcome: "accept" }, { subject: "second", outcome: "inconclusive" }] } });
|
||||
mkdirSync(join(flow.cwd, ".pi/plan"), { recursive: true });
|
||||
const file = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
writeFileSync(file, "# Plan\n1. [x] goal: first\n2. [x] goal: second\n3. [x] goal: manual claim\n");
|
||||
for (let i = 0; i < 2; i++) {
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
expect(flow.ctx.ui.setStatus.mock.lastCall?.[1]).toContain("2/3");
|
||||
expect(flow.ctx.ui.setWidget.mock.lastCall?.[1]?.join("\n")).toMatch(/claimed.*manual claim/);
|
||||
expect(flow.ctx.ui.setWidget.mock.lastCall?.[1]?.join("\n")).toMatch(/inconclusive.*second/);
|
||||
}
|
||||
writeFileSync(file, readFileSync(file, "utf8").replace("[x] goal: first", "[/] goal: first"));
|
||||
await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
writeFileSync(file, readFileSync(file, "utf8").replace("[/] goal: first", "[x] goal: first"));
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
expect(flow.ctx.ui.setStatus.mock.lastCall?.[1]).toContain("1/3");
|
||||
expect(flow.ctx.ui.setWidget.mock.lastCall?.[1]?.join("\n")).toMatch(/claimed.*first/);
|
||||
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("retains conclusive and inconclusive sign-offs when settled detail moves below the fold", async () => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
const signoffs = [{ subject: "first", outcome: "accept" }, { subject: "second", outcome: "inconclusive" }];
|
||||
flow.entries.push({ type: "custom", customType: "pi-goals-state", data: { phase: "working", planVersion: 1, stewardEnabled: false, autoIntervalMs: null, signedOffGoals: signoffs } });
|
||||
mkdirSync(join(flow.cwd, ".pi/plan"), { recursive: true });
|
||||
const file = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
const goals = "# Plan\n1. [x] goal: first\n - evidence: outputs/first.log\n2. [x] goal: second\n - evidence: outputs/second.log\n";
|
||||
writeFileSync(file, `${goals} - settled detail: implementation notes\n\n## Log\n`);
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
writeFileSync(file, `${goals}\n## Log\n\n## Appendix\nSettled detail: implementation notes\n`);
|
||||
await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
expect(flow.ctx.ui.setStatus.mock.lastCall?.[1]).toContain("2/2");
|
||||
expect(flow.ctx.ui.setWidget.mock.lastCall?.[1]?.join("\n")).toMatch(/inconclusive.*second/);
|
||||
expect((flow.entries.at(-1)?.data as any).signedOffGoals).toEqual(signoffs);
|
||||
expect(flow.messages).toHaveLength(0);
|
||||
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it.each(["missing", "first with wording drift", "duplicate"])("requires unique goal identity before review: %s", async (goal) => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
flow.entries.push({ type: "custom", customType: "pi-goals-state", data: { phase: "working", planVersion: 1, stewardEnabled: true, autoIntervalMs: null } });
|
||||
mkdirSync(join(flow.cwd, ".pi/plan"), { recursive: true });
|
||||
const file = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
const plan = "1. [ ] goal: first\n2. [x] goal: duplicate\n3. [ ] goal: duplicate\n";
|
||||
writeFileSync(file, plan);
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
const outcome = await flow.tools.get("CompleteGoal").execute("", { goal }, undefined, undefined, flow.ctx);
|
||||
expect(outcome.isError).toBe(true);
|
||||
expect(outcome.content[0].text).toMatch(/unique exact goal/i);
|
||||
expect(readFileSync(file, "utf8")).toBe(plan);
|
||||
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
it("reopens a prematurely ticked submitted goal before a failed sign-off", async () => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
flow.entries.push({ type: "custom", customType: "pi-goals-state", data: { phase: "working", planVersion: 1, stewardEnabled: true, autoIntervalMs: null } });
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
const file = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
mkdirSync(join(flow.cwd, ".pi/plan"), { recursive: true });
|
||||
writeFileSync(file, "# Plan\n1. [x] goal: first\n - [x] subtask\n2. [ ] goal: second\n");
|
||||
const outcome = await flow.tools.get("CompleteGoal").execute("", { goal: "first" }, undefined, undefined, flow.ctx);
|
||||
expect(outcome.isError).toBe(true);
|
||||
expect(readFileSync(file, "utf8")).toBe("# Plan\n1. [/] goal: first\n - [x] subtask\n2. [ ] goal: second\n");
|
||||
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
it("enables steward and hourly auto by default in new and cleared legacy sessions", async () => {
|
||||
for (const legacy of [false, true]) {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
if (legacy) flow.entries.push({ type: "custom", customType: "pi-goals-state", data: { phase: null, planVersion: null, stewardEnabled: false, autoIntervalMs: null } });
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ defaultsVersion: 1, stewardEnabled: true, autoIntervalMs: 3_600_000 });
|
||||
await flow.commands.get("goals").handler("clear", flow.ctx);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: null, stewardEnabled: true, autoIntervalMs: 3_600_000 });
|
||||
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves explicit off preferences across reload and a new plan", async () => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
await flow.commands.get("goals").handler("steward off", flow.ctx);
|
||||
await flow.commands.get("goals").handler("auto off", flow.ctx);
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ defaultsVersion: 1, stewardEnabled: false, autoIntervalMs: null });
|
||||
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("does not enable automation midway through a legacy working plan", async () => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
flow.entries.push({ type: "custom", customType: "pi-goals-state", data: { phase: "working", planVersion: 1, stewardEnabled: false, autoIntervalMs: null } });
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
await flow.commands.get("goals").handler("judge", flow.ctx);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "working", stewardEnabled: false, autoIntervalMs: null });
|
||||
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("uses the plan escape for objectives beginning with reserved command words", async () => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
for (const objective of ["judge the vendor options", "auto generate captions", "steward the migration", "clear"]) {
|
||||
await flow.commands.get("goals").handler(`plan ${objective}`, flow.ctx);
|
||||
expect(flow.messages.at(-1)?.content).toContain(`Objective: ${objective}`);
|
||||
}
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning", planVersion: 4, judgeModel: null });
|
||||
} finally { rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("keeps an enabled plan in planning when a real supervisor cannot be launched", async () => {
|
||||
const flow = setup(["Ready"]);
|
||||
vi.stubEnv("HERDR_ENV", "");
|
||||
try {
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
writeFileSync(join(flow.cwd, ".pi/plan/session-a-v1.md"), "# Plan\n\n1. [ ] goal: make the file\n");
|
||||
await settleDraft(flow);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning", stewardEnabled: true });
|
||||
expect(flow.messages.some(message => message.content.startsWith("Work the goals"))).toBe(false);
|
||||
} finally { vi.unstubAllEnvs(); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("preserves prior drafts, displays the plan before Discuss, and records chat answers", async () => {
|
||||
const flow = setup(["Discuss"]);
|
||||
try {
|
||||
const legacy = join(flow.cwd, ".pi/plan/session-a.md");
|
||||
mkdirSync(join(flow.cwd, ".pi/plan"), { recursive: true });
|
||||
writeFileSync(legacy, "old plan");
|
||||
await flow.commands.get("goals").handler("first objective", flow.ctx);
|
||||
const v1 = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
expect(readFileSync(v1, "utf-8")).toBe("");
|
||||
expect(readFileSync(legacy, "utf-8")).toBe("old plan");
|
||||
const plan = "# First plan\n\n## Goals\n\n1. [ ] goal: preserve this\n\n## Appendix (context, not approved)\nold context\n";
|
||||
mkdirSync(join(flow.cwd, ".pi/plan"), { recursive: true });
|
||||
writeFileSync(v1, plan);
|
||||
await flow.hooks.get("input")({ text: "The result must preserve column order.", source: "interactive" }, flow.ctx);
|
||||
|
||||
await settleDraft(flow);
|
||||
expect(flow.events).toEqual(["display", "select"]);
|
||||
expect(flow.messages.at(-1)?.content).toContain("normal chat");
|
||||
await flow.hooks.get("input")({ text: "Keep two columns.\nDo not add a filter.", source: "interactive" }, flow.ctx);
|
||||
expect(flow.messages.find((message) => message.display)?.content).toContain("goal: preserve this");
|
||||
const interviewedPlan = readFileSync(v1, "utf-8");
|
||||
expect(interviewedPlan).toContain("> The result must preserve column order.");
|
||||
expect(interviewedPlan).toMatch(/## Interview\n\n### .+\n\n> The result must preserve column order\.[\s\S]+> Keep two columns\.\n> Do not add a filter\./);
|
||||
const refineSnapshot = await flow.hooks.get("before_agent_start")({}, flow.ctx);
|
||||
expect(refineSnapshot.message.content).toContain("[PLANNING MODE]");
|
||||
const blocked = await flow.hooks.get("tool_call")({ toolName: "edit", input: { path: "README.md" } }, flow.ctx);
|
||||
expect(blocked?.block).toBe(true);
|
||||
|
||||
await flow.commands.get("goals").handler("second objective", flow.ctx);
|
||||
expect(readFileSync(v1, "utf-8")).toBe(interviewedPlan);
|
||||
expect(readFileSync(join(flow.cwd, ".pi/plan/session-a-v2.md"), "utf-8")).toBe("");
|
||||
expect(flow.messages.at(-1)?.content).toContain("session-a-v2.md");
|
||||
|
||||
await flow.commands.get("goals").handler("compare the vendor options", flow.ctx);
|
||||
expect(readFileSync(join(flow.cwd, ".pi/plan/session-a-v3.md"), "utf-8")).toBe("");
|
||||
expect(flow.messages.at(-1)?.content).toContain("Objective: compare the vendor options");
|
||||
|
||||
await flow.commands.get("goals").handler("judge provider/model", flow.ctx);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ judgeModel: "provider/model", planVersion: 3 });
|
||||
} finally {
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("disconnects without deleting the active plan", async () => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [ ] goal: preserve this\n");
|
||||
|
||||
await flow.commands.get("goals").handler("clear", flow.ctx);
|
||||
|
||||
expect(readFileSync(planPath, "utf-8")).toContain("goal: preserve this");
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: null, planVersion: null });
|
||||
|
||||
await flow.commands.get("goals").handler("next objective", flow.ctx);
|
||||
expect(readFileSync(join(flow.cwd, ".pi/plan/session-a-v2.md"), "utf-8")).toBe("");
|
||||
} finally {
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("Discuss allows multiple chat turns, survives reload, and reopens review for an unchanged draft", async () => {
|
||||
const flow = setup(["Discuss", "Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("steward off", flow.ctx);
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
const path = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
writeFileSync(path, "# Plan\n\n1. [ ] goal: make this specific\n");
|
||||
await settleDraft(flow);
|
||||
expect(flow.events).toEqual(["display", "select"]);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning", reviewRequested: false });
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx); // question turn, not another menu
|
||||
await flow.hooks.get("input")({ text: "Only the existing output; no new UI.", source: "interactive" }, flow.ctx);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await flow.hooks.get("session_start")({ reason: "reload" }, flow.ctx);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
expect(flow.events).toEqual(["display", "select"]);
|
||||
const unchanged = readFileSync(path, "utf8");
|
||||
await settleDraft(flow);
|
||||
expect(readFileSync(path, "utf8")).toBe(unchanged);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
expect(flow.events).toEqual(["display", "select", "display", "select"]);
|
||||
expect(flow.messages.filter(m => m.content.startsWith("Work the goals"))).toHaveLength(1);
|
||||
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("starts work only when the human chooses Ready", async () => {
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("steward off", flow.ctx);
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [ ] goal: work on this\n");
|
||||
|
||||
await settleDraft(flow);
|
||||
|
||||
expect(flow.events).toEqual(["display", "select"]);
|
||||
expect(flow.messages.filter((message) => !message.display)).toHaveLength(2);
|
||||
expect(flow.messages.at(-1)?.content).toContain("Work the goals");
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
expect((await promptReminder(flow)).content).toContain("New session.");
|
||||
expect(await promptReminder(flow)).toBeUndefined();
|
||||
} finally {
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses to enable a steward after an unreviewed plan is already working", async () => {
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
await flow.commands.get("goals").handler("steward off", flow.ctx);
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [ ] goal: make the file\n");
|
||||
await settleDraft(flow);
|
||||
await flow.commands.get("goals").handler("steward on", flow.ctx);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "working", stewardEnabled: false, supervisor: null });
|
||||
} finally {
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("edits a plan in Pi and cancels without starting work", async () => {
|
||||
const original = "# Plan\n\n## Goals\n\n1. [ ] goal: original\n";
|
||||
const edited = "# Plan\n\n## Goals\n\n1. [ ] goal: edited\n";
|
||||
const flow = setup(["Edit", "Cancel"], [edited]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
writeFileSync(planPath, original);
|
||||
|
||||
await settleDraft(flow);
|
||||
|
||||
expect(flow.events).toEqual(["display", "select", "editor", "display", "select"]);
|
||||
expect(() => readFileSync(planPath, "utf-8")).toThrow();
|
||||
expect(flow.messages.filter((message) => !message.display)).toHaveLength(1);
|
||||
} finally {
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("reminds every eight unchanged working-set turns, ignoring log-only edits", async () => {
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("steward off", flow.ctx);
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n\n## Log\n");
|
||||
await settleDraft(flow);
|
||||
|
||||
await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
for (let turn = 0; turn < 3; turn++) await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n\n## Log\n- checked input\n");
|
||||
for (let turn = 0; turn < 5; turn++) await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
|
||||
expect((await flow.hooks.get("context")({ messages: [] }, flow.ctx)).messages).toHaveLength(0);
|
||||
const reminder = await promptReminder(flow);
|
||||
expect(reminder.content).toContain(".pi/plan/session-a-v1.md");
|
||||
expect(reminder.content).not.toContain("checked input");
|
||||
expect(await promptReminder(flow)).toBeUndefined();
|
||||
|
||||
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n - [x] inspect input\n\n## Log\n- checked input\n");
|
||||
await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
for (let turn = 0; turn < 7; turn++) await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
expect(await promptReminder(flow)).toBeUndefined();
|
||||
await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
expect((await promptReminder(flow)).content).toContain("make the output");
|
||||
} finally {
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not acknowledge an unpersisted reminder; retries fresh on the next natural prompt", async () => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
flow.entries.push({ type: "custom", customType: "pi-goals-state", data: { phase: "working", planVersion: 1, stewardEnabled: false, autoIntervalMs: null } });
|
||||
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
mkdirSync(join(flow.cwd, ".pi/plan"), { recursive: true });
|
||||
writeFileSync(planPath, "1. [ ] goal: old plan\n");
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
await flow.hooks.get("session_compact")({}, flow.ctx);
|
||||
const unsaved = await flow.hooks.get("before_agent_start")({}, flow.ctx);
|
||||
expect(unsaved.message.content).toContain("old plan");
|
||||
expect((await flow.hooks.get("context")({ messages: [] }, flow.ctx)).messages).toEqual([]);
|
||||
writeFileSync(planPath, "1. [ ] goal: fresh plan\n");
|
||||
const saved = await promptReminder(flow);
|
||||
expect(saved.content).toContain("The session was just compacted.");
|
||||
expect(saved.content).toContain("fresh plan");
|
||||
expect(saved.details.reminderId).not.toBe(unsaved.message.details.reminderId);
|
||||
expect(await promptReminder(flow)).toBeUndefined();
|
||||
expect(flow.messages).toEqual([]); // no queued sendMessage/sendUserMessage delivery
|
||||
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("auto-continues once on stop, then pauses after two no-progress wakes", async () => {
|
||||
vi.useFakeTimers();
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("steward off", flow.ctx);
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n");
|
||||
await settleDraft(flow);
|
||||
await flow.commands.get("goals").handler("auto 1", flow.ctx);
|
||||
|
||||
await settleDraft(flow);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
const autoMessages = () => flow.messages.filter((message) => message.content.includes("Auto-continue is enabled"));
|
||||
expect(autoMessages()).toHaveLength(1);
|
||||
|
||||
await settleDraft(flow);
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(autoMessages()).toHaveLength(2);
|
||||
await settleDraft(flow);
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(autoMessages()).toHaveLength(2);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("delays auto-continuation after a known background start", async () => {
|
||||
vi.useFakeTimers();
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("steward off", flow.ctx);
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n");
|
||||
await settleDraft(flow);
|
||||
await flow.commands.get("goals").handler("auto 1", flow.ctx);
|
||||
await flow.hooks.get("agent_start")({}, flow.ctx);
|
||||
await flow.hooks.get("tool_call")({ toolName: "process", input: { action: "start" } }, flow.ctx);
|
||||
await settleDraft(flow);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
const autoMessages = () => flow.messages.filter((message) => message.content.includes("Auto-continue is enabled"));
|
||||
expect(autoMessages()).toHaveLength(0);
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(autoMessages()).toHaveLength(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("gives the agent a planning snapshot and blocks work routes", async () => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning" });
|
||||
await flow.hooks.get("session_start")({}, flow.ctx);
|
||||
const snapshot = await flow.hooks.get("before_agent_start")({}, flow.ctx);
|
||||
expect(snapshot.message.content).toContain("[PLANNING MODE]");
|
||||
expect(snapshot.message.content).toContain(planPath);
|
||||
|
||||
const writePlan = await flow.hooks.get("tool_call")({ toolName: "write", input: { path: planPath } }, flow.ctx);
|
||||
const writeCode = await flow.hooks.get("tool_call")({ toolName: "write", input: { path: "README.md" } }, flow.ctx);
|
||||
const readShell = await flow.hooks.get("tool_call")({ toolName: "bash", input: { command: "pwd && ls && git log" } }, flow.ctx);
|
||||
const changeDirectoryThenRead = await flow.hooks.get("tool_call")({ toolName: "bash", input: { command: "cd . && ls -la" } }, flow.ctx);
|
||||
const pipeShell = await flow.hooks.get("tool_call")({ toolName: "bash", input: { command: "ls | head" } }, flow.ctx);
|
||||
const pythonWrite = await flow.hooks.get("tool_call")({ toolName: "bash", input: { command: "python -c \"open('README.md', 'w')\"" } }, flow.ctx);
|
||||
const signoff = await flow.tools.get("CompleteGoal").execute("", { goal: "work" }, undefined, undefined, flow.ctx);
|
||||
await flow.hooks.get("session_compact")({}, flow.ctx);
|
||||
expect(await flow.hooks.get("context")({ messages: [] }, flow.ctx)).toBeUndefined();
|
||||
const compacted = await promptReminder(flow);
|
||||
|
||||
expect(writePlan).toBeUndefined();
|
||||
expect(writeCode?.block).toBe(true);
|
||||
expect(readShell).toBeUndefined();
|
||||
expect(changeDirectoryThenRead).toBeUndefined();
|
||||
expect(pipeShell?.block).toBe(true);
|
||||
expect(pythonWrite?.block).toBe(true);
|
||||
expect(signoff.isError).toBe(true);
|
||||
expect(compacted.content).toContain("[PLANNING MODE]");
|
||||
expect(await promptReminder(flow)).toBeUndefined();
|
||||
} finally {
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("role models across goal lifecycle", () => {
|
||||
const choice = (role: string) => ({ provider: `${role}-provider`, id: `${role}-model` });
|
||||
const select = async (flow: ReturnType<typeof setup>, role: string) => {
|
||||
flow.ctx.model = choice(role);
|
||||
await flow.hooks.get("model_select")({ source: "cycle", model: flow.ctx.model }, flow.ctx);
|
||||
};
|
||||
const draft = (flow: ReturnType<typeof setup>, version = 1) => writeFileSync(join(flow.cwd, `.pi/plan/session-a-v${version}.md`), "# Plan\n\n1. [ ] goal: produce the artifact\n");
|
||||
it("restores each role at Ready, fresh-instance reload/resume and the next plan without changing judge override", async () => {
|
||||
const flow = setup(["Ready"]); const fresh = setup([]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("steward off", flow.ctx);
|
||||
await flow.commands.get("goals").handler("first", flow.ctx); await select(flow, "planning"); draft(flow);
|
||||
await settleDraft(flow); await select(flow, "worker");
|
||||
await flow.commands.get("goals").handler("judge isolated/judge", flow.ctx);
|
||||
fresh.entries.push(...structuredClone(flow.entries));
|
||||
await fresh.hooks.get("session_start")({ reason: "resume" }, fresh.ctx);
|
||||
expect(fresh.ctx.model).toEqual(choice("worker"));
|
||||
await fresh.commands.get("goals").handler("second", fresh.ctx);
|
||||
expect(fresh.ctx.model).toEqual(choice("planning"));
|
||||
expect(fresh.entries.at(-1)?.data).toMatchObject({ phase: "planning", judgeModel: "isolated/judge" });
|
||||
await fresh.hooks.get("session_start")({ reason: "reload" }, fresh.ctx);
|
||||
expect(fresh.ctx.model).toEqual(choice("planning"));
|
||||
} finally { for (const f of [flow, fresh]) { await f.hooks.get("session_shutdown")({}, f.ctx); rmSync(f.cwd, { recursive: true, force: true }); } }
|
||||
});
|
||||
|
||||
it("failed Ready and explicit cancellation never save a worker choice over planning", async () => {
|
||||
const flow = setup(["Ready", "Cancel"]); vi.stubEnv("HERDR_ENV", "");
|
||||
try {
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx); await select(flow, "planning"); draft(flow);
|
||||
await settleDraft(flow); expect(flow.ctx.model).toEqual(choice("planning"));
|
||||
expect(flow.messages.some(m => m.content.startsWith("Work the goals"))).toBe(false);
|
||||
await settleDraft(flow);
|
||||
await select(flow, "outside-plan");
|
||||
await flow.commands.get("goals").handler("next plan", flow.ctx);
|
||||
expect(flow.ctx.model).toEqual(choice("planning"));
|
||||
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("unavailable worker model blocks Ready until an explicit selection, without changing its remembered provider", async () => {
|
||||
const flow = setup(["Ready", "Ready"]);
|
||||
const dir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals"); mkdirSync(dir);
|
||||
writeFileSync(join(dir, "worker-model.json"), JSON.stringify(choice("worker")));
|
||||
try {
|
||||
await flow.commands.get("goals").handler("steward off", flow.ctx);
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx); draft(flow);
|
||||
flow.pi.setModel.mockResolvedValueOnce(false);
|
||||
await settleDraft(flow);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning" });
|
||||
expect(flow.messages.some(m => m.content.startsWith("Work the goals"))).toBe(false);
|
||||
expect(flow.ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("has no authentication"), "error");
|
||||
expect(JSON.parse(readFileSync(join(dir, "worker-model.json"), "utf8"))).toEqual(choice("worker"));
|
||||
await select(flow, "replacement-worker"); await settleDraft(flow);
|
||||
expect(flow.ctx.model).toEqual(choice("replacement-worker"));
|
||||
expect(flow.messages.filter(m => m.content.startsWith("Work the goals"))).toHaveLength(1);
|
||||
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("supervisor resume uses its own preference and never restores inherited worker automation/tools", async () => {
|
||||
const flow = setup([]);
|
||||
const dir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals"); mkdirSync(dir);
|
||||
writeFileSync(join(dir, "supervisor-model.json"), JSON.stringify(choice("supervisor")));
|
||||
try {
|
||||
flow.entries.push({ type: "custom", customType: "pi-goals-state", data: { phase: "working", planVersion: 1, autoIntervalMs: 1, stewardEnabled: false } });
|
||||
flow.entries.push({ type: "custom", customType: "pi-goals-supervisor", data: { binding: { id: "binding" }, workerId: "worker" } });
|
||||
await flow.hooks.get("session_start")({ reason: "resume" }, flow.ctx);
|
||||
expect(flow.ctx.model).toEqual(choice("supervisor"));
|
||||
expect(flow.pi.getActiveTools()).not.toContain("CompleteGoal");
|
||||
expect(flow.pi.getActiveTools()).not.toContain("RequestPlanReview");
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
expect(flow.messages).toEqual([]);
|
||||
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("waives alignment only for the current plan and Escape returns to chat without deleting the draft", async () => {
|
||||
const flow = setup([undefined]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("first; no questions", flow.ctx);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ questionsWaived: true });
|
||||
expect(flow.messages.at(-1)?.content).toContain("THIS plan only");
|
||||
await flow.commands.get("goals").handler("second", flow.ctx);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ questionsWaived: false, reviewRequested: false });
|
||||
expect(flow.messages.at(-1)?.content).toContain("previous plan does NOT apply");
|
||||
draft(flow, 2);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx); expect(flow.events).toEqual([]);
|
||||
await settleDraft(flow);
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: "planning", reviewRequested: false });
|
||||
expect(readFileSync(join(flow.cwd, ".pi/plan/session-a-v2.md"), "utf8")).toContain("produce the artifact");
|
||||
expect(flow.events).toEqual(["display", "select"]);
|
||||
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
});
|
||||
|
||||
it("cancels an ordinary Ready handoff suspended in setModel without losing planning preference", async () => {
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("steward off", flow.ctx);
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
writeFileSync(join(flow.cwd, ".pi/plan/session-a-v1.md"), "# Plan\n\n1. [ ] goal: produce output\n");
|
||||
const dir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals");
|
||||
writeFileSync(join(dir, "worker-model.json"), JSON.stringify({ provider: "worker", id: "work" }));
|
||||
let finish!: () => void;
|
||||
flow.pi.setModel.mockImplementationOnce(async model => { await new Promise<void>(resolve => { finish = resolve; }); flow.ctx.model = model; return true; });
|
||||
const starting = settleDraft(flow);
|
||||
await vi.waitFor(() => expect(finish).toBeTypeOf("function"));
|
||||
await flow.commands.get("goals").handler("clear", flow.ctx);
|
||||
finish(); await starting;
|
||||
expect(flow.ctx.model).toEqual({ provider: "offline", id: "test" });
|
||||
expect(flow.messages.some(m => m.content.startsWith("Work the goals"))).toBe(false);
|
||||
expect(JSON.parse(readFileSync(join(dir, "worker-model.json"), "utf8"))).toEqual({ provider: "worker", id: "work" });
|
||||
} finally { await flow.hooks.get("session_shutdown")({}, flow.ctx); rmSync(flow.cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("packed Intercom registration in real Pi (no model call)", () => {
|
||||
it.each(["bundled", "installed before goals", "installed after goals"])("registers one transport and one tool: %s", async (order) => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "goals-registration-"));
|
||||
const packed = JSON.parse(execFileSync("npm", ["pack", "--json", "--ignore-scripts", "--cache", "/tmp/pi-goals-npm-cache", "--pack-destination", cwd], { cwd: resolve("."), encoding: "utf8" }))[0];
|
||||
execFileSync("tar", ["-xzf", join(cwd, packed.filename), "-C", cwd]);
|
||||
const root = join(cwd, "package");
|
||||
const external = join(root, "node_modules/pi-intercom");
|
||||
const agentDir = join(cwd, "profile"); mkdirSync(agentDir);
|
||||
const packages = order === "bundled" ? [root] : order === "installed before goals" ? [external, root] : [root, external];
|
||||
writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ packages }));
|
||||
const probe = join(cwd, "probe.ts");
|
||||
writeFileSync(probe, `import { writeFileSync } from "node:fs";
|
||||
export default function(pi) {
|
||||
pi.registerCommand("registrationreload", { handler: async (_args, ctx) => { await ctx.reload(); } });
|
||||
pi.on("resources_discover", () => {
|
||||
let registrations = 0;
|
||||
pi.events.emit("intercom:extension-register", { namespace: "goals-registration-check", ownerEligible: false, onEvent() {}, onReady() { registrations++; } });
|
||||
writeFileSync(${JSON.stringify(join(cwd, "registration.json"))}, JSON.stringify({ registrations, tools: pi.getAllTools().map(t => t.name) }));
|
||||
});
|
||||
}`);
|
||||
const child = spawn(resolve("node_modules/.bin/pi"), ["--mode", "rpc", "--no-session", "-e", probe], { cwd, env: { ...process.env, PI_CODING_AGENT_DIR: agentDir }, stdio: ["pipe", "pipe", "pipe"] });
|
||||
let stderr = ""; child.stderr.on("data", chunk => { stderr += chunk; }); child.stdout.resume();
|
||||
try {
|
||||
await vi.waitFor(() => expect(existsSync(join(cwd, "registration.json")), stderr).toBe(true), { timeout: 12_000, interval: 25 });
|
||||
const report = JSON.parse(readFileSync(join(cwd, "registration.json"), "utf8"));
|
||||
expect(report.registrations).toBe(1);
|
||||
expect(report.tools.filter((name: string) => name === "intercom")).toHaveLength(1);
|
||||
expect(new Set(report.tools).size).toBe(report.tools.length);
|
||||
expect(stderr).not.toMatch(/conflicting tools|Multiple Intercom runtimes/);
|
||||
rmSync(join(cwd, "registration.json"));
|
||||
child.stdin.write(`${JSON.stringify({ type: "prompt", message: "/registrationreload" })}\n`);
|
||||
await vi.waitFor(() => expect(existsSync(join(cwd, "registration.json")), stderr).toBe(true), { timeout: 12_000, interval: 25 });
|
||||
const reloaded = JSON.parse(readFileSync(join(cwd, "registration.json"), "utf8"));
|
||||
expect(reloaded).toEqual(report);
|
||||
} finally { child.kill(); await new Promise(done => child.once("close", done)); rmSync(cwd, { recursive: true, force: true }); }
|
||||
}, 20_000);
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { ExtensionAPI, ExtensionContext, SessionStartEvent } from "@earendil-works/pi-coding-agent";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { loadBundledIntercom } from "../src/intercom.js";
|
||||
|
||||
const factory = vi.hoisted(() => vi.fn());
|
||||
vi.mock("pi-intercom", () => ({ default: factory }));
|
||||
beforeEach(() => { factory.mockReset(); });
|
||||
describe("loadBundledIntercom", () => {
|
||||
it("composes the fallback once and explicitly starts its public lifecycle", async () => {
|
||||
const on = vi.fn(); const start = vi.fn(); const shutdown = vi.fn(); const registerTool = vi.fn();
|
||||
factory.mockImplementation((pi: ExtensionAPI) => { pi.on("session_start", start); pi.on("session_shutdown", shutdown); pi.registerTool({ name: "intercom" } as any); });
|
||||
const pi = { on, registerTool, getAllTools: () => [] } as unknown as ExtensionAPI;
|
||||
const event = { type: "session_start", reason: "startup" } as SessionStartEvent;
|
||||
const ctx = {} as ExtensionContext;
|
||||
await loadBundledIntercom(pi, event, ctx);
|
||||
expect(factory).toHaveBeenCalledOnce(); expect(start).toHaveBeenCalledExactlyOnceWith(event, ctx);
|
||||
expect(on).toHaveBeenCalledExactlyOnceWith("session_shutdown", shutdown);
|
||||
expect(registerTool).toHaveBeenCalledExactlyOnceWith({ name: "intercom" });
|
||||
});
|
||||
it("never loads a second copy when an installed Intercom cannot supply the required channel", async () => {
|
||||
const pi = { getAllTools: () => [{ name: "intercom" }] } as unknown as ExtensionAPI;
|
||||
await expect(loadBundledIntercom(pi, {} as SessionStartEvent, {} as ExtensionContext)).rejects.toThrow("no second Intercom was loaded");
|
||||
expect(factory).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { goalReviewWire, isWire } from "../../src/internal/supervisor/protocol.js";
|
||||
import { MAX_VIEW_BYTES } from "../../src/internal/supervisor/view.js";
|
||||
|
||||
test("goal_review accepts an optional bounded snapshot but rejects malformed snapshots", () => {
|
||||
const review = { t: "goal_review", to: "supervisor", requestId: "request", bindingId: "binding", goal: "first", planHash: "hash" };
|
||||
assert.equal(isWire(review), true, "older peers can omit the optional snapshot");
|
||||
assert.equal(isWire({ ...review, view: "Fresh worker evidence" }), true);
|
||||
for (const view of [null, false, 1, {}, [], "x".repeat(MAX_VIEW_BYTES + 1)]) {
|
||||
assert.equal(isWire({ ...review, view }), false);
|
||||
}
|
||||
});
|
||||
|
||||
test("worker completion metadata is optional but cannot claim malformed counts", () => {
|
||||
const view = { t: "view", to: "supervisor", view: "Worker evidence", stopped: true };
|
||||
const completion = { planHash: "hash", total: 2, pending: 1, inconclusive: 1 };
|
||||
assert.equal(isWire(view), true);
|
||||
assert.equal(isWire({ ...view, completion }), true);
|
||||
for (const bad of [null, {}, { ...completion, total: -1 }, { ...completion, pending: 0.5 }, { ...completion, inconclusive: 2 }, { ...completion, total: Infinity }, { ...completion, total: "2" }, { ...completion, planHash: false }]) assert.equal(isWire({ ...view, completion: bad }), false);
|
||||
});
|
||||
|
||||
test("checkpoint snapshot bounding counts JSON escapes and does not split Unicode characters", () => {
|
||||
const review = { requestId: "request", bindingId: "binding", goal: "goal ".repeat(1800), planHash: "hash" };
|
||||
const wire = goalReviewWire("supervisor", review, '😀\\"\n'.repeat(2000));
|
||||
assert.equal(isWire(wire), true);
|
||||
assert.ok(Buffer.byteLength(JSON.stringify(wire)) <= 16 * 1024);
|
||||
assert.equal(wire.t, "goal_review");
|
||||
if (wire.t !== "goal_review") assert.fail("Expected a checkpoint request");
|
||||
assert.equal(wire.goal, review.goal);
|
||||
assert.doesNotMatch(wire.view!, /�|\\ud83d(?!\\ude00)|(?<!\\ud83d)\\ude00/u);
|
||||
assert.match(wire.view!, /cut to fit/);
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawn } from "node:child_process";
|
||||
import { copyFileSync, mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { childPiProcesses } from "../../src/internal/supervisor/subagents.js";
|
||||
|
||||
/**
|
||||
* Runs the real ps command against a real child process, because the parsing is the part most
|
||||
* likely to be quietly wrong: a column order or a comm name that differs by platform reads as
|
||||
* "no subagents", which is the answer that lets the supervisor declare a busy worker finished.
|
||||
*/
|
||||
function spawnChildNamedPi() {
|
||||
const dir = mkdtempSync(join(tmpdir(), "supervisor-test-"));
|
||||
const fake = join(dir, "pi");
|
||||
copyFileSync("/usr/bin/sleep", fake); // ps reports comm from the executable name
|
||||
return spawn(fake, ["30"], { stdio: "ignore" });
|
||||
}
|
||||
|
||||
test("a child process named pi is found by ps, and stops being found when it exits", async (t) => {
|
||||
if (process.platform !== "linux" && process.platform !== "darwin") return t.skip("ps only");
|
||||
assert.deepEqual(await childPiProcesses(), [], "no subagents before one is started");
|
||||
|
||||
const child = spawnChildNamedPi();
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
assert.deepEqual(await childPiProcesses(), [child.pid], "the running child must be reported");
|
||||
|
||||
child.kill();
|
||||
await new Promise((r) => child.once("exit", r));
|
||||
assert.deepEqual(await childPiProcesses(), [], "and not reported once it exits");
|
||||
});
|
||||
|
||||
test("the check is a snapshot, so it cannot hold up the worker's settle", async (t) => {
|
||||
if (process.platform !== "linux" && process.platform !== "darwin") return t.skip("ps only");
|
||||
const child = spawnChildNamedPi();
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
|
||||
const started = Date.now();
|
||||
assert.deepEqual(await childPiProcesses(), [child.pid]);
|
||||
assert.ok(Date.now() - started < 1000, "it returns while the child is still running, it does not wait");
|
||||
child.kill();
|
||||
});
|
||||
@@ -0,0 +1,344 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { age, buildView, type Entry, goalPreview, MAX_VIEW_BYTES, outstandingWork, progressKey, sinceLastTurn, turnsSince } from "../../src/internal/supervisor/view.js";
|
||||
|
||||
function assistant(text: string, calls: Array<{ name: string; args: Record<string, unknown> }> = []): Entry {
|
||||
return {
|
||||
type: "message",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text },
|
||||
...calls.map((c) => ({ type: "toolCall", name: c.name, arguments: c.args })),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function toolResult(toolName: string, text: string, isError = false): Entry {
|
||||
return { type: "message", message: { role: "toolResult", toolName, isError, content: [{ type: "text", text }] } };
|
||||
}
|
||||
|
||||
test("latest user direction survives bounded summaries, compaction and later supervisor echoes", () => {
|
||||
const direction = "You are now explicitly authorized to implement goal two. Keep converter requirements intact. Stop after goal two is signed off.";
|
||||
const entries: Entry[] = [
|
||||
{ type: "message", message: { role: "user", content: direction } },
|
||||
{ type: "compaction", summary: "Older summary still says to pause the CLI. ".repeat(200) },
|
||||
...Array.from({ length: 200 }, (_, n) => assistant(`Evidence ${n} ${"detail ".repeat(200)}`)),
|
||||
{ type: "message", message: { role: "user", content: "[supervisor] An older supervisor instruction is not new human direction." } },
|
||||
];
|
||||
const view = buildView({ goal: "goal two", status: "checkpoint", entries, sourceSession: "/history/worker.jsonl" });
|
||||
assert.ok(Buffer.byteLength(view) <= MAX_VIEW_BYTES);
|
||||
assert.ok(view.includes(direction));
|
||||
assert.ok(view.indexOf(direction) < view.indexOf("Older summary"), "latest direction is separate from older summarized context");
|
||||
assert.match(view, /latest user direction/i);
|
||||
});
|
||||
|
||||
test("oversized user direction is visibly bounded with a source reference", () => {
|
||||
const view = buildView({ goal: "g", status: "checkpoint", sourceSession: "/history/worker.jsonl", entries: [
|
||||
{ type: "message", message: { role: "user", content: "Long direction ".repeat(2000) } },
|
||||
] });
|
||||
assert.ok(Buffer.byteLength(view) <= MAX_VIEW_BYTES);
|
||||
assert.match(view, /user direction truncated/);
|
||||
assert.match(view, /source session: \/history\/worker.jsonl/);
|
||||
});
|
||||
|
||||
test("a one-line goal stays whole while a multi-line goal has a locator", () => {
|
||||
assert.equal(goalPreview("fix the parser"), "fix the parser");
|
||||
assert.equal(goalPreview("Build the causal evaluation.\nThe full rubric follows."), "Build the causal evaluation. [...]");
|
||||
const view = buildView({
|
||||
goal: "Build the causal evaluation.\nThe full rubric follows.",
|
||||
status: "idle",
|
||||
entries: [assistant("working")],
|
||||
});
|
||||
assert.match(view, /<goal>\nBuild the causal evaluation\. \[\.\.\.\]\n<\/goal>/);
|
||||
assert.doesNotMatch(view, /The full rubric follows/);
|
||||
});
|
||||
|
||||
test("a view carries only the turns the supervisor has not been sent", () => {
|
||||
// The supervisor is a real session and keeps every view it read, so re-sending the whole
|
||||
// transcript each time is a second copy of what it already has. It grows with every review.
|
||||
const entries = [assistant("the first thing I did"), assistant("the second thing I did")];
|
||||
const first = buildView({ goal: "g", status: "idle", entries });
|
||||
assert.match(first, /the first thing I did/);
|
||||
assert.equal(turnsSince(entries), 2);
|
||||
|
||||
entries.push(assistant("the third thing I did"));
|
||||
const next = buildView({ goal: "g", status: "idle", entries, since: 2 });
|
||||
assert.match(next, /the third thing I did/);
|
||||
assert.doesNotMatch(next, /the first thing I did/, "already sent, so it must not go again");
|
||||
assert.match(next, /# New turns since your last look \(1 of 3\)/);
|
||||
});
|
||||
|
||||
test("the last two reasoning blocks stay in the narrative, and older ones drop out", () => {
|
||||
// Block shape read out of a real session jsonl. pi-vcc's normalize keeps only text and toolCall,
|
||||
// so reasoning reaches nobody, although you see it on screen. It belongs beside the tool call it
|
||||
// produced, not in a section of its own, because that is the order you read a session in.
|
||||
const thinker = (thinking: string, said: string): Entry => ({
|
||||
type: "message",
|
||||
message: { role: "assistant", content: [{ type: "thinking", thinking }, { type: "text", text: said }] },
|
||||
});
|
||||
const entries = [
|
||||
thinker("TOO OLD TO SEND", "first try"),
|
||||
thinker("the bash tool is dead, LET ME TEST READ", "second try"),
|
||||
thinker("read works, SO I WILL DELEGATE INSTEAD", "third try"),
|
||||
];
|
||||
|
||||
const view = buildView({ goal: "g", status: "working", entries });
|
||||
assert.match(view, /LET ME TEST READ[\s\S]*SO I WILL DELEGATE INSTEAD/, "in order, oldest first");
|
||||
assert.match(view, /LET ME TEST READ[\s\S]*second try/, "each thought sits with the turn it produced");
|
||||
assert.doesNotMatch(view, /TOO OLD TO SEND/, "two blocks, or the view becomes a second transcript");
|
||||
assert.match(view, /\(thinking\)/, "marked, so the supervisor knows it is reasoning and not speech");
|
||||
});
|
||||
|
||||
test("a compaction restarts the view, so no turn falls into the gap", () => {
|
||||
// getBranch keeps the entries a compaction replaced, so the mark now points past the end. Read
|
||||
// the wrong slice here and the supervisor silently reads a piece of the old history.
|
||||
const entries: Entry[] = [
|
||||
assistant("old work"),
|
||||
{ type: "compaction", summary: "SUMMARY OF THE OLD WORK" },
|
||||
assistant("work after the compaction"),
|
||||
];
|
||||
const view = buildView({ goal: "g", status: "idle", entries, since: 9 });
|
||||
assert.match(view, /The worker compacted, so this view restarts/);
|
||||
assert.match(view, /SUMMARY OF THE OLD WORK/);
|
||||
assert.match(view, /work after the compaction/);
|
||||
assert.equal(turnsSince(entries), 1, "the mark restarts from the compaction, not from the session");
|
||||
});
|
||||
|
||||
test("pi-vcc reports the files the worker wrote, and separates them from the ones it read", () => {
|
||||
const view = buildView({
|
||||
goal: "g",
|
||||
status: "idle",
|
||||
entries: [
|
||||
assistant("editing", [{ name: "edit", args: { path: "src/a.ts" } }]),
|
||||
assistant("reading", [{ name: "read", args: { path: "src/never.ts" } }]),
|
||||
assistant("writing", [{ name: "write", args: { file_path: "docs/b.md" } }]),
|
||||
],
|
||||
});
|
||||
assert.match(view, /Modified:.*src\/a\.ts/);
|
||||
assert.match(view, /Read:.*src\/never\.ts/);
|
||||
});
|
||||
|
||||
test("progressKey is unchanged when a review produced no new file or commit", () => {
|
||||
const worked = [assistant("editing", [{ name: "edit", args: { path: "src/a.ts" } }])];
|
||||
const talked = [...worked, assistant("I will look into that shortly.")];
|
||||
const wroteMore = [...worked, assistant("editing", [{ name: "write", args: { path: "src/b.ts" } }])];
|
||||
assert.equal(progressKey(talked), progressKey(worked), "talking is not progress");
|
||||
assert.notEqual(progressKey(wroteMore), progressKey(worked), "a new file is progress");
|
||||
});
|
||||
|
||||
test("progressKey still sees a new file past pi-vcc's ten path display cap", () => {
|
||||
// The rendered section stops at ten paths and says "(+N more)", so reading it there froze this
|
||||
// key on exactly the long runs it exists for.
|
||||
const many = (n: number) =>
|
||||
Array.from({ length: n }, (_, i) => assistant("editing", [{ name: "edit", args: { path: `src/f${i}.ts` } }]));
|
||||
assert.notEqual(progressKey(many(13)), progressKey(many(12)), "the 13th file must count as progress");
|
||||
});
|
||||
|
||||
test("a commit counts as progress, even when no file was written since", () => {
|
||||
// A worker whose whole turn was committing looked stagnant while the view line claims to count
|
||||
// commits.
|
||||
const ran = (cmd: string, out: string): Entry[] => [
|
||||
assistant("committing", [{ name: "bash", args: { command: cmd } }]),
|
||||
toolResult("bash", out),
|
||||
];
|
||||
const before = ran("git status", "nothing to commit");
|
||||
const after = [...before, ...ran(`git commit -m "fix the parser"`, "[main abc1234] fix the parser")];
|
||||
assert.notEqual(progressKey(after), progressKey(before));
|
||||
});
|
||||
|
||||
|
||||
test("outstandingWork finds tool calls that never got a result", () => {
|
||||
const answered: Entry = {
|
||||
type: "message",
|
||||
message: { role: "toolResult", toolName: "read", toolCallId: "call-1", content: [{ type: "text", text: "ok" }] },
|
||||
};
|
||||
const entries: Entry[] = [
|
||||
{ type: "message", message: { role: "assistant", content: [{ type: "toolCall", id: "call-1", name: "read" }] } },
|
||||
answered,
|
||||
{ type: "message", message: { role: "assistant", content: [{ type: "toolCall", id: "call-2", name: "subagent" }] } },
|
||||
];
|
||||
assert.deepEqual(outstandingWork(entries), ["subagent"]);
|
||||
assert.deepEqual(outstandingWork([entries[0], answered]), []);
|
||||
});
|
||||
|
||||
test("buildView reports a tool call with no result, so done can be refused", () => {
|
||||
const busy = buildView({
|
||||
goal: "g",
|
||||
status: "idle",
|
||||
entries: [{ type: "message", message: { role: "assistant", content: [{ type: "toolCall", id: "x", name: "subagent", arguments: {} }] } }],
|
||||
});
|
||||
assert.match(busy, /^tool calls with no result: subagent$/m);
|
||||
const quiet = buildView({ goal: "g", status: "idle", entries: [assistant("all done")] });
|
||||
assert.match(quiet, /^tool calls with no result: none$/m);
|
||||
});
|
||||
|
||||
test("the view says how many reviews in a row changed nothing, and says nothing at zero", () => {
|
||||
const entries = [assistant("hi")];
|
||||
assert.match(buildView({ goal: "g", status: "idle", entries, stale: 3 }), /no new file or commit for 3 reviews/);
|
||||
assert.doesNotMatch(buildView({ goal: "g", status: "idle", entries }), /reviews in a row/);
|
||||
});
|
||||
|
||||
|
||||
test("the view merges the worker's compaction summary with the turns after it", () => {
|
||||
// pi-vcc's compile() takes the old summary as previousSummary, so nothing between the summary
|
||||
// and the newest turn falls in the gap between them.
|
||||
const view = buildView({
|
||||
goal: "g",
|
||||
status: "idle",
|
||||
entries: [
|
||||
{ type: "compaction", summary: "[Session Goal]\n- build the dataset" },
|
||||
assistant("carrying on", [{ name: "write", args: { path: "after.md" } }]),
|
||||
],
|
||||
});
|
||||
assert.match(view, /build the dataset/, "the summary from before the compaction survives");
|
||||
assert.match(view, /after\.md/, "so does the work done after it");
|
||||
});
|
||||
|
||||
test("a turn the compaction summary already covers is not sent twice", () => {
|
||||
// getBranch keeps the entries a compaction replaced, so passing all of them alongside the
|
||||
// summary spends the byte budget on two copies of the same work.
|
||||
const view = buildView({
|
||||
goal: "g",
|
||||
status: "idle",
|
||||
entries: [
|
||||
assistant("COVERED BY THE SUMMARY", [{ name: "edit", args: { path: "old.ts" } }]),
|
||||
{ type: "compaction", summary: "[Session Goal]\n- build the dataset" },
|
||||
assistant("after the compaction"),
|
||||
],
|
||||
});
|
||||
assert.match(view, /build the dataset/);
|
||||
assert.match(view, /after the compaction/);
|
||||
assert.doesNotMatch(view, /COVERED BY THE SUMMARY/);
|
||||
});
|
||||
|
||||
test("pi-vcc's sections and its transcript land on the right sides of the split", () => {
|
||||
// compile() emits `sections + "\n\n---\n\n" + transcript`, and drops either part when it is
|
||||
// empty. Splitting that wrong put the sections under "Recent turns", where the byte cut eats
|
||||
// them from the top.
|
||||
const view = buildView({
|
||||
goal: "g",
|
||||
status: "idle",
|
||||
entries: [{ type: "message", message: { role: "user", content: "make the results table" } }],
|
||||
});
|
||||
const [above, below] = view.split("# Turns so far");
|
||||
assert.match(above, /\[Session Goal\]/, "the sections belong above");
|
||||
assert.match(below, /make the results table/, "the transcript belongs below");
|
||||
assert.doesNotMatch(below, /\[Session Goal\]/);
|
||||
});
|
||||
|
||||
test("the view does not tell the supervisor to use vcc_recall, a tool it does not have", () => {
|
||||
const view = buildView({ goal: "g", status: "idle", entries: [assistant("hi")] });
|
||||
assert.doesNotMatch(view, /vcc_recall/);
|
||||
});
|
||||
|
||||
test("supervisor directives are not sent back as worker evidence", () => {
|
||||
const directive: Entry = {
|
||||
type: "message",
|
||||
message: { role: "user", content: "[supervisor] Read exactly src/a.ts and quote it." },
|
||||
};
|
||||
const first = buildView({
|
||||
goal: "g",
|
||||
status: "working",
|
||||
entries: [directive, assistant("I read src/a.ts", [{ name: "read", args: { path: "src/a.ts" } }])],
|
||||
});
|
||||
assert.doesNotMatch(first, /Read exactly src\/a\.ts/);
|
||||
assert.match(first, /I read src\/a\.ts/);
|
||||
|
||||
const next = buildView({
|
||||
goal: "g",
|
||||
status: "working",
|
||||
entries: [directive, assistant("I read src/a.ts", [{ name: "read", args: { path: "src/a.ts" } }]), directive],
|
||||
since: 1,
|
||||
});
|
||||
assert.match(next, /# New turns since your last look \(0 of 1\)/);
|
||||
assert.doesNotMatch(next, /Read exactly src\/a\.ts/);
|
||||
});
|
||||
|
||||
test("bookkeeping tool calls are kept out of the transcript", () => {
|
||||
const view = buildView({
|
||||
goal: "g",
|
||||
status: "idle",
|
||||
entries: [
|
||||
assistant("planning", [{ name: "TodoWrite", args: {} }]),
|
||||
toolResult("TodoWrite", "todo list updated with 5 items"),
|
||||
assistant("real work", [{ name: "edit", args: { path: "src/a.ts" } }]),
|
||||
],
|
||||
});
|
||||
assert.doesNotMatch(view, /todo list updated/);
|
||||
assert.match(view, /src\/a\.ts/);
|
||||
});
|
||||
|
||||
test("buildView reports the goal, status, and files without historical failures", () => {
|
||||
const view = buildView({
|
||||
goal: "make the table",
|
||||
status: "idle",
|
||||
entries: [assistant("done", [{ name: "write", args: { path: "results.md" } }]), toolResult("bash", "exit code 2")],
|
||||
});
|
||||
assert.match(view, /<goal>\nmake the table\n<\/goal>/);
|
||||
assert.match(view, /status: idle/);
|
||||
assert.match(view, /turns: 2/);
|
||||
assert.match(view, /results\.md/);
|
||||
assert.doesNotMatch(view, /# Problems|bash exit 2/);
|
||||
});
|
||||
test("how long the worker has been quiet, measured from its own last entry", () => {
|
||||
// The number wassname asked for after the 2h27m silence: "there have been no turns for this
|
||||
// long, is it stuck". Measured from the worker's last message, not from the supervisor's last
|
||||
// look, so it reads the same whether the worker sits at the prompt or hangs inside one command.
|
||||
const now = Date.parse("2026-08-14T04:29:00Z");
|
||||
const at = (iso: string): Entry => ({ ...assistant("working"), timestamp: iso });
|
||||
|
||||
const stopped = [at("2026-08-14T01:00:00Z"), at("2026-08-14T02:02:18Z")];
|
||||
assert.equal(age(sinceLastTurn(stopped, now)), "2h27m", "the real overnight gap, from session 019ffa73");
|
||||
const supervisorMessage: Entry = {
|
||||
type: "message",
|
||||
timestamp: "2026-08-14T04:28:59Z",
|
||||
message: { role: "user", content: "[supervisor] Do not reset the worker clock." },
|
||||
};
|
||||
assert.equal(age(sinceLastTurn([...stopped, supervisorMessage], now)), "2h27m", "a supervisor directive is not worker progress");
|
||||
|
||||
// The last non-supervisor entry wins even when an older one follows it in some other order, and a branch with
|
||||
// no timestamps at all reports 0 rather than throwing.
|
||||
assert.equal(age(sinceLastTurn([at("2026-08-14T04:28:30Z")], now)), "30s");
|
||||
assert.equal(age(sinceLastTurn([at("2026-08-14T03:44:00Z")], now)), "45m");
|
||||
assert.equal(sinceLastTurn([assistant("no stamp")], now), 0);
|
||||
});
|
||||
|
||||
test("buildView keeps the newest turns when it has to cut for the channel limit", () => {
|
||||
const long = Array.from({ length: 400 }, (_, i) => assistant(`turn ${i} ${"x".repeat(400)}`));
|
||||
const view = buildView({ goal: "g", status: "idle", entries: long });
|
||||
assert.ok(Buffer.byteLength(view, "utf-8") <= MAX_VIEW_BYTES, `view was ${Buffer.byteLength(view)} bytes`);
|
||||
assert.match(view, /turn 399/, "the newest turn must survive the cut");
|
||||
assert.doesNotMatch(view, /turn 0 /, "the oldest must be the one dropped");
|
||||
});
|
||||
|
||||
test("pi's own branch logic drops the abandoned fork, on a session file", async () => {
|
||||
// Synthetic fixture copied from the upstream make-fixture.ts output; no real transcript.
|
||||
// An earlier version of this test asserted on the rendered view, and passed even when fed every
|
||||
// abandoned entry, because byte trimming removed them anyway.
|
||||
const { parseSessionEntries, buildContextEntries } = await import("@earendil-works/pi-coding-agent");
|
||||
const { readFileSync } = await import("node:fs");
|
||||
const fixture = new URL("../fixtures/forked-session.jsonl", import.meta.url).pathname;
|
||||
|
||||
const all = parseSessionEntries(readFileSync(fixture, "utf-8")).filter((e: any) => e.type !== "session");
|
||||
const byId = new Map(all.map((e: any) => [e.id, e]));
|
||||
const leaf = all[all.length - 1] as any;
|
||||
const branch = buildContextEntries(all as any, leaf.id, byId as any);
|
||||
|
||||
const asText = (entries: any[]) => JSON.stringify(entries);
|
||||
assert.match(asText(all), /ABANDONED/, "the fixture must contain an abandoned branch");
|
||||
assert.doesNotMatch(asText(branch), /ABANDONED/, "the live branch must not contain the abandoned fork");
|
||||
assert.equal(all.length - branch.length, 22);
|
||||
});
|
||||
|
||||
test("a long goal cannot push the view past the broker limit", () => {
|
||||
const view = buildView({ goal: "x".repeat(60000), status: "idle", entries: [assistant("hi")] });
|
||||
assert.ok(Buffer.byteLength(view, "utf-8") <= MAX_VIEW_BYTES, `view was ${Buffer.byteLength(view)} bytes`);
|
||||
});
|
||||
|
||||
test("a bounded complete overview explicitly labels a truncated worker compaction summary", () => {
|
||||
const view = buildView({ goal: "Check the cumulative evidence", status: "stopped", sourceSession: "/fixture/worker.jsonl", entries: [{ type: "compaction", summary: "evidence ".repeat(1_000) }] });
|
||||
assert.match(view, /source session: \/fixture\/worker.jsonl/);
|
||||
assert.match(view, /worker compaction summary truncated; inspect the worker session for full evidence/);
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { alignmentPolicy, judgeSystem, judgeUser, planDrafting, planningState, reminder, resync, waivesAlignment } from "../src/prompts.js";
|
||||
|
||||
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 the human to confirm your interpretation");
|
||||
expect(planDrafting).toContain("approve an editorial or other preference choice");
|
||||
expect(planDrafting).toContain("Ask material task-specific alignment questions");
|
||||
expect(planDrafting).toContain("There is no fixed question quota");
|
||||
expect(planDrafting).toContain("Wait for answers to required decisions");
|
||||
expect(planDrafting).toContain("self-contained:\nstate the relevant context, use the human's language and ASD-STE100");
|
||||
expect(planDrafting).toContain("placeholder goal such as \"work out the thing\"");
|
||||
expect(planDrafting).toContain("object, observable result, settled scope, and required approval");
|
||||
});
|
||||
|
||||
it("waives questions only for an explicit current-objective instruction", () => {
|
||||
for (const objective of ["fix it, no questions", "skip questions and implement", "do not ask me any questions", "no q's", "skip q's", "fix it; no q’s please"]) {
|
||||
expect(waivesAlignment(objective)).toBe(true);
|
||||
expect(alignmentPolicy(waivesAlignment(objective))).toContain("THIS plan only");
|
||||
}
|
||||
for (const objective of ["next objective", "fix the exporter; do not skip questions", "don't skip questions", "add a 'skip questions' button", 'add a "no questions" mode', 'document "first; skip questions; then build"']) expect(waivesAlignment(objective), objective).toBe(false);
|
||||
expect(alignmentPolicy(false)).toContain("previous plan does NOT apply");
|
||||
});
|
||||
|
||||
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("self-contained round with relevant context and a recommendation");
|
||||
});
|
||||
|
||||
it("keeps signed-off goal identities during plan housekeeping", () => {
|
||||
const text = reminder("plan", ".pi/plan/test.md");
|
||||
expect(text).toContain("keep every goal line and its completion status above ## Log");
|
||||
expect(text).toContain("evidence references beside each goal");
|
||||
expect(text).not.toContain("prune finished goals");
|
||||
expect(text).not.toContain("evidence lives in git history");
|
||||
});
|
||||
|
||||
it("gives the judge an exact subject rather than a fuzzy-match fallback", () => {
|
||||
const text = judgeUser({ goal: "first", plan: "1. [ ] goal: first", planPath: "plan.md" });
|
||||
expect(text).toContain("unique exact goal subject");
|
||||
expect(text).not.toContain("tolerate small wording drift");
|
||||
});
|
||||
|
||||
it("anchors work and sign-off to the user-visible result", () => {
|
||||
expect(planDrafting).toContain("## User-visible result");
|
||||
expect(planDrafting).toContain("Take it from the original request, not from your implementation plan");
|
||||
expect(planDrafting).toContain("Future work may not defer any artifact or action named there");
|
||||
expect(reminder("plan", ".pi/plan/test.md")).toContain("latest message outranks this plan");
|
||||
expect(resync("plan", ".pi/plan/test.md", "Compacted.")).toContain("amend the plan rather than preserving an obsolete decision");
|
||||
expect(judgeSystem).toContain("Task fidelity?");
|
||||
expect(judgeSystem).toContain("Agent-inferred scope is not authority");
|
||||
expect(judgeSystem).toContain("uncommitted and ignored files via read");
|
||||
expect(judgeSystem).toContain("Git status is context, not an acceptance gate");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import piGoalsExtension from "../src/index.js";
|
||||
|
||||
// Point at the installed Pi package to exercise its real lifecycle, not the API mock.
|
||||
const sdkRoot = process.env.PI_GOALS_TEST_SDK_ROOT ?? resolve("node_modules/@earendil-works/pi-coding-agent");
|
||||
const sdk = await import(pathToFileURL(join(sdkRoot, "dist/index.js")).href);
|
||||
const { loadExtensionFromFactory } = await import(pathToFileURL(join(sdkRoot, "dist/core/extensions/loader.js")).href);
|
||||
const requireSdk = createRequire(join(sdkRoot, "package.json"));
|
||||
const aiRoot = requireSdk.resolve.paths("@earendil-works/pi-ai")!.map(path => join(path, "@earendil-works/pi-ai")).find(path => existsSync(join(path, "package.json")))!;
|
||||
const { convertResponsesMessages } = await import(pathToFileURL(join(aiRoot, "dist/api/openai-responses-shared.js")).href);
|
||||
const sdkVersion = JSON.parse(readFileSync(join(sdkRoot, "package.json"), "utf8")).version;
|
||||
// Optional read-only check against the real guard; no native endpoint or private checkpoint used.
|
||||
const replay = process.env.PI_GOALS_TEST_REPLAY_ROOT
|
||||
? await import(pathToFileURL(join(process.env.PI_GOALS_TEST_REPLAY_ROOT, "src/payload-rewrite.ts")).href) : undefined;
|
||||
const zeroCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
||||
const plan = (revision: string) => `# Plan ${revision}\n\n1. [/] goal: test saved reminders\n\n## Log\n- fixture log ${revision}\n\n## Appendix\nfixture appendix ${revision}\n`;
|
||||
const compactReminder = (message: any) => typeof message.content === "string" && message.content.includes("The session was just compacted.");
|
||||
|
||||
async function setup(phase = "working") {
|
||||
sdk.initTheme("dark", false);
|
||||
const cwd = mkdtempSync(join(tmpdir(), "goals-reminder-sdk-"));
|
||||
const sm = sdk.SessionManager.create(cwd, join(cwd, "sessions"));
|
||||
const events: string[] = [];
|
||||
const requests: any[][] = [];
|
||||
const errors: unknown[] = [];
|
||||
let responses: Array<{ tool?: boolean; high?: boolean; overflow?: boolean; queue?: boolean }> = [];
|
||||
let compactions = 0;
|
||||
const settingsManager = sdk.SettingsManager.inMemory({ compaction: { enabled: false, reserveTokens: 1000, keepRecentTokens: 100 }, retry: { enabled: false } });
|
||||
const runtime = sdk.createExtensionRuntime();
|
||||
const bus = sdk.createEventBus();
|
||||
const extensions = [];
|
||||
extensions.push(await loadExtensionFromFactory(piGoalsExtension, cwd, bus, runtime));
|
||||
let session: any;
|
||||
extensions.push(await loadExtensionFromFactory((pi: any) => {
|
||||
pi.on("before_agent_start", () => { events.push("before_agent_start"); });
|
||||
pi.on("session_compact", (event: any) => { events.push(`compact:${event.reason}:${event.willRetry}`); });
|
||||
pi.on("session_before_compact", (event: any) => {
|
||||
compactions++;
|
||||
return { compaction: { summary: `Offline checkpoint ${compactions}`, firstKeptEntryId: event.preparation.firstKeptEntryId, tokensBefore: event.preparation.tokensBefore, details: { compactedWindow: [{ type: "compaction", encrypted_content: "offline-fixture-only" }] } } };
|
||||
});
|
||||
pi.registerTool({ name: "fixture_tool", label: "fixture", description: "Offline no-op", parameters: { type: "object", properties: {} }, execute: async () => ({ content: [{ type: "text", text: "fixture result" }], details: {} }) });
|
||||
}, cwd, bus, runtime));
|
||||
const resourceLoader = {
|
||||
getExtensions: () => ({ extensions, errors: [], runtime }),
|
||||
getSkills: () => ({ skills: [], diagnostics: [] }), getPrompts: () => ({ prompts: [], diagnostics: [] }), getThemes: () => ({ themes: [], diagnostics: [] }),
|
||||
getAgentsFiles: () => ({ agentsFiles: [] }), getSystemPrompt: () => "Offline reminder test", getSystemPromptSource: () => undefined,
|
||||
getAppendSystemPrompt: () => [], getAppendSystemPromptSources: () => [], extendResources: () => {}, reload: async () => {},
|
||||
};
|
||||
const modelRuntime = await sdk.ModelRuntime.create({ authPath: join(cwd, "auth.json"), modelsPath: join(cwd, "models.json"), modelsStorePath: join(cwd, "models-store.json"), allowModelNetwork: false });
|
||||
await modelRuntime.setRuntimeApiKey("openai", "offline-fixture-key");
|
||||
const model = { ...modelRuntime.getModel("openai", "gpt-4.1"), contextWindow: 10000 };
|
||||
expect(model.id).toBe("gpt-4.1");
|
||||
const assistant = (content: any[], stopReason = "stop", input = 100) => ({ role: "assistant", content, api: model.api, provider: model.provider, model: model.id, stopReason, usage: { input, output: 1, cacheRead: 0, cacheWrite: 0, totalTokens: input + 1, cost: zeroCost }, timestamp: Date.now() });
|
||||
sm.appendMessage({ role: "user", content: "Fixture history", timestamp: Date.now() - 1000 });
|
||||
sm.appendMessage({ ...assistant([{ type: "text", text: "Fixture history response" }]), timestamp: Date.now() - 900 });
|
||||
sm.appendCustomEntry("pi-goals-state", { defaultsVersion: 1, phase, planVersion: 1, stewardEnabled: false, autoIntervalMs: null, reviewRequested: false });
|
||||
const planPath = join(cwd, ".pi/plan", `${sm.getSessionId()}-v1.md`);
|
||||
mkdirSync(dirname(planPath), { recursive: true }); writeFileSync(planPath, plan("initial"));
|
||||
({ session } = await sdk.createAgentSession({ cwd, agentDir: process.env.PI_CODING_AGENT_DIR, model, modelRuntime, sessionManager: sm, settingsManager, resourceLoader, tools: ["fixture_tool"] }));
|
||||
session.subscribe((event: any) => {
|
||||
if (event.type === "message_end" && event.message.role === "assistant" && event.message.errorMessage && event.message.errorMessage !== "maximum context length exceeded") errors.push(event.message.errorMessage);
|
||||
});
|
||||
const serialize = (messages: any[]) => convertResponsesMessages(model, { messages: sdk.convertToLlm(messages) }, new Set(["openai", "openai-codex", "opencode"]));
|
||||
session.agent.streamFunction = async (_model: any, context: any) => {
|
||||
const actual = serialize(context.messages);
|
||||
const saved = serialize(sm.buildSessionContext().messages);
|
||||
expect(actual).toEqual(saved); // Provider-visible history matches saved history AT request time.
|
||||
const disk = sdk.SessionManager.open(sm.getSessionFile());
|
||||
expect(serialize(disk.buildSessionContext().messages)).toEqual(saved);
|
||||
const checkpoint = sm.getBranch().findLast((entry: any) => entry.type === "compaction");
|
||||
if (replay && checkpoint) {
|
||||
const args = { model, payload: { model: model.id, instructions: "Offline reminder test", input: actual }, branchEntries: sm.getBranch(), compactionEntry: checkpoint };
|
||||
expect(replay.rewriteResponsesPayloadWithNativeReplay(args).ok).toBe(true);
|
||||
// Negative control: the original ephemeral suffix must still be rejected by the guard.
|
||||
expect(replay.rewriteResponsesPayloadWithNativeReplay({ ...args, payload: { ...args.payload, input: [...actual, { role: "user", content: [{ type: "input_text", text: "<system-reminder>unsaved plan</system-reminder>" }] }] } })).toMatchObject({ ok: false, reason: "expected-pi-replay-mismatch" });
|
||||
}
|
||||
requests.push(structuredClone(context.messages));
|
||||
const next = responses.shift() ?? {};
|
||||
if (next.queue) await session.steer("Queued user must retain order");
|
||||
const message = assistant(next.tool ? [{ type: "toolCall", id: `fixture-${requests.length}`, name: "fixture_tool", arguments: {} }] : [{ type: "text", text: "Offline answer" }], next.overflow ? "error" : next.tool ? "toolUse" : "stop", next.high ? 9500 : 100);
|
||||
if (next.overflow) { message.errorMessage = "maximum context length exceeded"; message.usage.input = 0; }
|
||||
return { async *[Symbol.asyncIterator]() { yield next.overflow ? { type: "error", reason: "error", error: message } : { type: "done", reason: message.stopReason, message }; }, result: async () => message };
|
||||
};
|
||||
await session.bindExtensions({ onError: (error: unknown) => errors.push(error) });
|
||||
return {
|
||||
session, sm, events, requests, errors, planPath, settingsManager,
|
||||
respond: (...next: typeof responses) => { responses = next; },
|
||||
reminders: () => sm.getBranch().filter((entry: any) => entry.type === "custom_message" && compactReminder(entry)),
|
||||
close: () => { session.dispose(); rmSync(cwd, { recursive: true, force: true }); },
|
||||
};
|
||||
}
|
||||
|
||||
describe(`saved goal reminders (Pi SDK ${sdkVersion}${replay ? ", real replay guard" : ""})`, () => {
|
||||
it.each(["working", "planning"])("manual compact in %s: fresh saved reminder once on next natural prompt", async (phase) => {
|
||||
const flow = await setup(phase);
|
||||
try {
|
||||
await flow.session.prompt("Initial request");
|
||||
const priorSnapshotIds = new Set(flow.sm.getBranch().filter((entry: any) => entry.type === "custom_message").map((entry: any) => entry.id));
|
||||
await flow.session.compact();
|
||||
const checkpointId = flow.sm.getBranch().findLast((entry: any) => entry.type === "compaction").id;
|
||||
expect(flow.events).toContain("compact:manual:false");
|
||||
expect(flow.requests).toHaveLength(1);
|
||||
writeFileSync(flow.planPath, plan("fresh-after-compact"));
|
||||
await flow.session.prompt("Natural request");
|
||||
const snapshots = flow.sm.getBranch().filter((entry: any) => entry.type === "custom_message" && entry.customType === (phase === "working" ? "pi-goals-plan-reminder" : "pi-goals-planning-context"));
|
||||
const count = snapshots.length;
|
||||
const latest = snapshots.at(-1);
|
||||
expect(latest.display).toBe(false);
|
||||
expect(priorSnapshotIds.has(latest.id)).toBe(false);
|
||||
const branch = flow.sm.getBranch();
|
||||
expect(branch.findIndex((entry: any) => entry.id === latest.id)).toBeGreaterThan(branch.findIndex((entry: any) => entry.id === checkpointId));
|
||||
expect(flow.requests[1].some((message: any) => message.role === "user" && Array.isArray(message.content) && message.content.some((part: any) => part.type === "text" && part.text === latest.content))).toBe(true);
|
||||
if (phase === "working") { expect(flow.reminders()).toHaveLength(1); expect(flow.reminders()[0].content).toContain("fixture appendix fresh-after-compact"); }
|
||||
await flow.session.prompt("Another natural request");
|
||||
expect(flow.sm.getBranch().filter((entry: any) => entry.type === "custom_message" && entry.customType === snapshots[0].customType)).toHaveLength(count);
|
||||
expect(flow.requests).toHaveLength(3);
|
||||
expect(flow.errors).toEqual([]);
|
||||
} finally { flow.close(); }
|
||||
});
|
||||
|
||||
it.each(["threshold", "overflow"])("post-run %s compact: no extra run, refresh at next natural prompt", async (reason) => {
|
||||
const flow = await setup();
|
||||
try {
|
||||
flow.settingsManager.applyOverrides({ compaction: { enabled: true } });
|
||||
flow.respond(reason === "threshold" ? { high: true } : { overflow: true }, {});
|
||||
await flow.session.prompt("Run and compact");
|
||||
expect(flow.events).toContain(`compact:${reason}:${reason === "overflow"}`);
|
||||
expect(flow.requests).toHaveLength(reason === "threshold" ? 1 : 2);
|
||||
expect(flow.reminders()).toHaveLength(0);
|
||||
expect(flow.events.filter(event => event === "before_agent_start")).toHaveLength(1);
|
||||
writeFileSync(flow.planPath, plan("next-natural"));
|
||||
await flow.session.prompt("Next natural prompt");
|
||||
expect(flow.reminders()).toHaveLength(1);
|
||||
expect(flow.reminders()[0].content).toContain("fixture appendix next-natural");
|
||||
expect(flow.errors).toEqual([]);
|
||||
} finally { flow.close(); }
|
||||
});
|
||||
|
||||
it("stale tool-loop reminder waits for a natural prompt and persists only the working set", async () => {
|
||||
const flow = await setup();
|
||||
try {
|
||||
flow.respond(...Array.from({ length: 8 }, () => ({ tool: true })), {});
|
||||
await flow.session.prompt("Long tool run");
|
||||
expect(flow.requests).toHaveLength(9);
|
||||
const count = () => flow.sm.getBranch().filter((entry: any) => entry.type === "custom_message" && entry.customType === "pi-goals-plan-reminder").length;
|
||||
expect(count()).toBe(1); // startup only; no turn_end/ephemeral reminders
|
||||
await flow.session.prompt("Natural prompt after staleness");
|
||||
expect(count()).toBe(2);
|
||||
const text = JSON.stringify(flow.requests.at(-1).at(-1));
|
||||
expect(text).toContain("test saved reminders");
|
||||
expect(text).not.toContain("fixture appendix");
|
||||
await flow.session.prompt("No duplicate");
|
||||
expect(count()).toBe(2);
|
||||
expect(flow.errors).toEqual([]);
|
||||
} finally { flow.close(); }
|
||||
});
|
||||
|
||||
it.skipIf(Number(sdkVersion.split(".")[1]) < 85).each([false, true])("mid-run threshold, queued user=%s: defer without drops, duplicates, or extra response", async (queue) => {
|
||||
const flow = await setup();
|
||||
try {
|
||||
flow.settingsManager.applyOverrides({ compaction: { enabled: true } });
|
||||
flow.respond({ tool: true, high: true, queue }, {});
|
||||
await flow.session.prompt("Use fixture tool");
|
||||
expect(flow.events).toContain("compact:threshold:false");
|
||||
expect(flow.requests).toHaveLength(2);
|
||||
expect(JSON.stringify(flow.requests[1][0])).toContain("Offline checkpoint"); // compacted before the tool continuation, not just after the run
|
||||
expect(flow.events.filter(event => event === "before_agent_start")).toHaveLength(1);
|
||||
expect(flow.reminders()).toHaveLength(0);
|
||||
if (queue) expect(JSON.stringify(flow.requests[1].at(-1))).toContain("Queued user must retain order");
|
||||
writeFileSync(flow.planPath, plan("after-auto"));
|
||||
await flow.session.prompt("Next natural prompt");
|
||||
expect(flow.reminders()).toHaveLength(1);
|
||||
expect(flow.reminders()[0].content).toContain("fixture appendix after-auto");
|
||||
await flow.session.prompt("No duplicate");
|
||||
expect(flow.reminders()).toHaveLength(1);
|
||||
expect(flow.requests).toHaveLength(4);
|
||||
expect(flow.errors).toEqual([]);
|
||||
} finally { flow.close(); }
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { type ModelChoice, type ModelRole, RoleModels } from "../src/role-models.js";
|
||||
|
||||
function harness(directory: string, current: ModelChoice = { provider: "initial", id: "model" }) {
|
||||
let selected: any;
|
||||
const ctx = { model: current, modelRegistry: { find: vi.fn((provider: string, id: string) => ({ provider, id })) }, ui: { notify: vi.fn() } };
|
||||
const pi = {
|
||||
on(_name: string, handler: any) { selected = handler; },
|
||||
setModel: vi.fn(async (model: ModelChoice) => { const previousModel = ctx.model; ctx.model = model; if (previousModel.provider !== model.provider || previousModel.id !== model.id) selected({ source: "set", previousModel, model }, ctx); return true; }),
|
||||
};
|
||||
const models = new RoleModels(pi as unknown as ExtensionAPI, directory);
|
||||
return {
|
||||
models, ctx, pi,
|
||||
enter: (role: ModelRole) => models.enter(role, ctx as unknown as ExtensionContext),
|
||||
select: (source: "set" | "cycle" | "restore", model: ModelChoice) => { ctx.model = model; selected({ source, model }, ctx); },
|
||||
};
|
||||
}
|
||||
const stored = (dir: string, role: string) => JSON.parse(readFileSync(join(dir, `${role}-model.json`), "utf8"));
|
||||
|
||||
describe("role-models storage and public model_select", () => {
|
||||
it("inherits current on first use, remembers set/cycle independently, and ignores automatic set and restore", async () => {
|
||||
const dir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals");
|
||||
const h = harness(dir);
|
||||
for (const role of ["planning", "worker", "supervisor"] as const) {
|
||||
const inherited = { ...h.ctx.model };
|
||||
expect(await h.enter(role)).toBe(true);
|
||||
expect(stored(dir, role)).toEqual(inherited);
|
||||
h.select(role === "planning" ? "set" : "cycle", { provider: role, id: `${role}-choice` });
|
||||
}
|
||||
for (const role of ["planning", "worker", "supervisor"] as const) {
|
||||
expect(await h.enter(role)).toBe(true);
|
||||
expect(h.ctx.model).toEqual({ provider: role, id: `${role}-choice` });
|
||||
h.select("restore", { provider: "session", id: "old" });
|
||||
expect(stored(dir, role)).toEqual({ provider: role, id: `${role}-choice` });
|
||||
}
|
||||
h.models.leave(); h.select("cycle", { provider: "outside", id: "unrelated" });
|
||||
expect(stored(dir, "supervisor").provider).toBe("supervisor");
|
||||
});
|
||||
|
||||
it("restores after a fresh instance/process and avoids cross-role lost updates", async () => {
|
||||
const dir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals");
|
||||
const h = harness(dir);
|
||||
await h.enter("planning"); h.select("cycle", { provider: "planner", id: "persisted" });
|
||||
// Two cold Pi/tsx imports compete with the packed-session tests in the full suite.
|
||||
await Promise.all(["worker", "supervisor"].map(role => promisify(execFile)(process.execPath, ["--import", "tsx", resolve("test/fixtures/role-model-process.ts"), dir, role, `${role}-provider`, "persisted"], { timeout: 20_000 })));
|
||||
const fresh = harness(dir, { provider: "unrelated", id: "start" });
|
||||
for (const role of ["planning", "worker", "supervisor"] as const) {
|
||||
await fresh.enter(role);
|
||||
expect(fresh.ctx.model).toEqual({ provider: role === "planning" ? "planner" : `${role}-provider`, id: "persisted" });
|
||||
expect(Object.keys(stored(dir, role))).toEqual(["provider", "id"]);
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it.each(["unavailable", "unauthenticated"])("visibly pauses a %s remembered model without replacing it", async failure => {
|
||||
const dir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals");
|
||||
const h = harness(dir); await h.enter("worker");
|
||||
h.select("set", { provider: "chosen", id: "keep" });
|
||||
const fresh = harness(dir, { provider: "other-provider", id: "fallback" });
|
||||
if (failure === "unavailable") fresh.ctx.modelRegistry.find.mockReturnValue(undefined as any);
|
||||
else fresh.pi.setModel.mockResolvedValue(false);
|
||||
expect(await fresh.enter("worker")).toBe(false);
|
||||
expect(fresh.models.ready).toBe(false);
|
||||
expect(fresh.ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Saved choice unchanged"), "error");
|
||||
expect(fresh.ctx.model.provider).toBe("other-provider");
|
||||
expect(stored(dir, "worker")).toEqual({ provider: "chosen", id: "keep" });
|
||||
fresh.select("restore", { provider: "fallback", id: "automatic" });
|
||||
expect(stored(dir, "worker").provider).toBe("chosen");
|
||||
fresh.select("cycle", { provider: "explicit", id: "replacement" });
|
||||
expect(fresh.models.ready).toBe(true);
|
||||
expect(stored(dir, "worker").provider).toBe("explicit");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("explicit use-current recovers the paused role when same-model selection emits no event", async () => {
|
||||
const dir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals");
|
||||
const h = harness(dir); await h.enter("worker"); h.select("set", { provider: "missing", id: "worker-choice" });
|
||||
const fresh = harness(dir, { provider: "current", id: "working-model" });
|
||||
fresh.ctx.modelRegistry.find.mockReturnValue(undefined as any);
|
||||
expect(await fresh.enter("worker")).toBe(false);
|
||||
await fresh.pi.setModel(fresh.ctx.model); // Real Pi suppresses this model_select.
|
||||
expect(fresh.models.ready).toBe(false);
|
||||
expect(stored(dir, "worker").provider).toBe("missing");
|
||||
fresh.pi.setModel.mockResolvedValueOnce(false);
|
||||
expect(await fresh.models.useCurrent(fresh.ctx as unknown as ExtensionContext)).toBe(false);
|
||||
expect(stored(dir, "worker").provider).toBe("missing");
|
||||
expect(await fresh.models.useCurrent(fresh.ctx as unknown as ExtensionContext)).toBe(true);
|
||||
expect(fresh.models.activeRole).toBe("worker");
|
||||
expect(fresh.models.ready).toBe(true);
|
||||
expect(stored(dir, "worker")).toEqual({ provider: "current", id: "working-model" });
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
||||
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 { StringDecoder } from "node:string_decoder";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
type RpcMessage = { type: string; id?: string; method?: string; [key: string]: unknown };
|
||||
|
||||
class RpcClient {
|
||||
readonly messages: RpcMessage[] = [];
|
||||
private readonly waiters: Array<{ predicate: (message: RpcMessage) => boolean; resolve: (message: RpcMessage) => void }> = [];
|
||||
|
||||
constructor(readonly process: ChildProcessWithoutNullStreams) {
|
||||
const decoder = new StringDecoder("utf8");
|
||||
let buffer = "";
|
||||
process.stdout.on("data", (chunk) => {
|
||||
buffer += decoder.write(chunk);
|
||||
while (buffer.includes("\n")) {
|
||||
const newline = buffer.indexOf("\n");
|
||||
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
||||
buffer = buffer.slice(newline + 1);
|
||||
if (!line) continue;
|
||||
const message = JSON.parse(line) as RpcMessage;
|
||||
this.messages.push(message);
|
||||
const index = this.waiters.findIndex(({ predicate }) => predicate(message));
|
||||
if (index !== -1) this.waiters.splice(index, 1)[0].resolve(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
send(message: RpcMessage): void {
|
||||
this.process.stdin.write(`${JSON.stringify(message)}\n`);
|
||||
}
|
||||
|
||||
waitFor(predicate: (message: RpcMessage) => boolean, after = 0): Promise<RpcMessage> {
|
||||
const existing = this.messages.slice(after).find(predicate);
|
||||
if (existing) return Promise.resolve(existing);
|
||||
return new Promise((resolvePromise) => this.waiters.push({ predicate, resolve: resolvePromise }));
|
||||
}
|
||||
}
|
||||
|
||||
function streamResponse(response: import("node:http").ServerResponse, delta: object, finishReason: "stop" | "tool_calls"): void {
|
||||
response.writeHead(200, { "content-type": "text/event-stream" });
|
||||
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: finishReason }] })}\n\n`);
|
||||
response.end("data: [DONE]\n\n");
|
||||
}
|
||||
|
||||
describe("RPC review flow", () => {
|
||||
it.each([false, true])("alignment and Discuss before one Ready handoff; real-Pi same-current recovery=%s", async (recoverCurrent) => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-rpc-"));
|
||||
const modelDir = join(cwd, ".agent", "pi-goals"); mkdirSync(modelDir, { recursive: true });
|
||||
if (recoverCurrent) writeFileSync(join(modelDir, "worker-model.json"), JSON.stringify({ provider: "missing", id: "unavailable-worker" }));
|
||||
let requestCount = 0;
|
||||
let planPath = "";
|
||||
const server = createServer((_request, response) => {
|
||||
requestCount++;
|
||||
if (requestCount === 1 || requestCount === 4) {
|
||||
streamResponse(response, { content: "1. Should the output be a text file? 2. Keep the existing CLI only? 3. Does a saved PASS receipt prove success?" }, "stop");
|
||||
return;
|
||||
}
|
||||
if (requestCount === 3 || requestCount === 6) {
|
||||
streamResponse(response, { tool_calls: [{ index: 0, id: `review-${requestCount}`, type: "function", function: { name: "RequestPlanReview", arguments: "{}" } }] }, "tool_calls");
|
||||
return;
|
||||
}
|
||||
if (requestCount === 2) {
|
||||
streamResponse(response, {
|
||||
tool_calls: [{
|
||||
index: 0,
|
||||
id: "write-plan",
|
||||
type: "function",
|
||||
function: {
|
||||
name: "write",
|
||||
arguments: JSON.stringify({
|
||||
path: planPath,
|
||||
content: "# Plan\n\n## Goals\n\n1. [ ] goal: name the output\n - subtle failure mode: the output has no name\n - discriminator: the plan names the output\n\n## Log\n\n## Interview\n",
|
||||
}),
|
||||
},
|
||||
}],
|
||||
}, "tool_calls");
|
||||
return;
|
||||
}
|
||||
streamResponse(response, { content: "Plan drafted." }, "stop");
|
||||
});
|
||||
await new Promise<void>((resolvePromise) => server.listen(0, "127.0.0.1", resolvePromise));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("Offline model did not bind a TCP port.");
|
||||
|
||||
const pi = spawn(resolve("node_modules/.bin/pi"), [
|
||||
"--mode", "rpc", "--no-session", "--model", "offline/test",
|
||||
"-e", resolve("test/fixtures/offline-model.ts"),
|
||||
"-e", resolve("src/index.ts"),
|
||||
], {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
PI_CODING_AGENT_DIR: join(cwd, ".agent"),
|
||||
PI_GOALS_OFFLINE_MODEL_URL: `http://127.0.0.1:${address.port}`,
|
||||
},
|
||||
});
|
||||
const client = new RpcClient(pi);
|
||||
try {
|
||||
client.send({ type: "get_state", id: "state" });
|
||||
const state = await client.waitFor((message) => message.type === "response" && message.id === "state");
|
||||
const sessionId = (state.data as { sessionId: string }).sessionId;
|
||||
planPath = join(cwd, ".pi", "plan", `${sessionId}-v1.md`);
|
||||
|
||||
client.send({ type: "prompt", id: "off", message: "/goals steward off" });
|
||||
await client.waitFor(message => message.type === "response" && message.id === "off");
|
||||
client.send({ type: "prompt", id: "goals", message: "/goals name the output file" });
|
||||
await client.waitFor(message => message.type === "agent_end");
|
||||
expect(requestCount).toBe(1);
|
||||
expect(client.messages.some(message => message.method === "select")).toBe(false);
|
||||
client.send({ type: "prompt", id: "answers", message: "Text file, existing CLI only, and a saved PASS receipt." });
|
||||
const review = await client.waitFor(message => message.type === "extension_ui_request" && message.method === "select");
|
||||
expect(review.options).toEqual(["Ready", "Discuss", "Edit", "Cancel"]);
|
||||
const discussionAt = client.messages.length;
|
||||
client.send({ type: "extension_ui_response", id: review.id, value: "Discuss" });
|
||||
await client.waitFor(message => message.type === "agent_end", discussionAt);
|
||||
expect(requestCount).toBe(4);
|
||||
expect(client.messages.slice(discussionAt).some(message => message.method === "editor" || message.method === "select")).toBe(false);
|
||||
const answerAt = client.messages.length;
|
||||
client.send({ type: "prompt", id: "discuss-answer", message: "Use output.txt, no UI changes." });
|
||||
await client.waitFor(message => message.type === "agent_end", answerAt);
|
||||
expect(client.messages.slice(answerAt).some(message => message.method === "select")).toBe(false);
|
||||
client.send({ type: "prompt", id: "finish-discussion", message: "Yes, that is enough; the draft is still right." });
|
||||
const reviewedAgain = await client.waitFor(message => message.type === "extension_ui_request" && message.method === "select", discussionAt);
|
||||
const readyAt = client.messages.length;
|
||||
client.send({ type: "extension_ui_response", id: reviewedAgain.id, value: "Ready" });
|
||||
if (recoverCurrent) {
|
||||
await client.waitFor(message => message.type === "extension_ui_request" && message.method === "notify" && JSON.stringify(message).includes("worker model paused"), readyAt);
|
||||
client.send({ type: "set_model", id: "same-current", provider: "offline", modelId: "test" });
|
||||
expect((await client.waitFor(message => message.type === "response" && message.id === "same-current")).success).toBe(true);
|
||||
expect(JSON.parse(readFileSync(join(modelDir, "worker-model.json"), "utf8"))).toEqual({ provider: "missing", id: "unavailable-worker" });
|
||||
expect(client.messages.slice(readyAt).some(message => message.method === "select")).toBe(false);
|
||||
client.send({ type: "prompt", id: "use-current", message: "/goals model current" });
|
||||
const recoveryMenu = await client.waitFor(message => message.type === "extension_ui_request" && message.method === "select", readyAt);
|
||||
expect(JSON.parse(readFileSync(join(modelDir, "worker-model.json"), "utf8"))).toEqual({ provider: "offline", id: "test" });
|
||||
expect(JSON.parse(readFileSync(join(modelDir, "planning-model.json"), "utf8"))).toEqual({ provider: "offline", id: "test" });
|
||||
client.send({ type: "extension_ui_response", id: recoveryMenu.id, value: "Ready" });
|
||||
}
|
||||
await client.waitFor(message => message.type === "agent_end", readyAt);
|
||||
expect(client.messages.filter(message => message.type === "message_start" && JSON.stringify(message).includes("Work the goals"))).toHaveLength(1);
|
||||
expect(requestCount).toBe(7);
|
||||
} finally {
|
||||
pi.kill();
|
||||
server.close();
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
}, 15_000);
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import { existsSync, 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 { describe, expect, it } from "vitest";
|
||||
|
||||
const stream = (res: import("node:http").ServerResponse, delta: object, finish = "stop") => {
|
||||
res.writeHead(200, { "content-type": "text/event-stream" });
|
||||
for (const data of [{ choices: [{ index: 0, delta, finish_reason: null }] }, { choices: [{ index: 0, delta: {}, finish_reason: finish }] }]) res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||
res.end("data: [DONE]\n\n");
|
||||
};
|
||||
|
||||
describe("two real Pi sessions with the actual Intercom broker (Herdr mocked)", () => {
|
||||
it("forks, pairs, reviews a goal and invokes a fresh offline evidence judge", async () => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "goals-supervisor-rpc-"));
|
||||
// Pack real production dependencies, then run outside every checkout with no install/symlink.
|
||||
const packed = JSON.parse(execFileSync("npm", ["pack", "--json", "--ignore-scripts", "--cache", "/tmp/pi-goals-npm-cache", "--pack-destination", cwd], { cwd: resolve("."), encoding: "utf8" }))[0];
|
||||
execFileSync("tar", ["-xzf", join(cwd, packed.filename), "-C", cwd]);
|
||||
const packageRoot = join(cwd, "package");
|
||||
const manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
||||
expect(manifest.bundledDependencies).toEqual(["pi-intercom", "@sting8k/pi-vcc"]);
|
||||
expect(existsSync(join(packageRoot, "node_modules/pi-intercom/broker/broker.ts"))).toBe(true);
|
||||
expect(existsSync(join(packageRoot, "src/internal/supervisor/index.ts"))).toBe(true);
|
||||
expect(existsSync(join(packageRoot, "node_modules/@sting8k/pi-vcc/src/core/summarize.ts"))).toBe(true);
|
||||
expect(packed.files.some((file: { path: string }) => /node_modules\/(?:@earendil-works|typebox)\//.test(file.path))).toBe(false);
|
||||
expect(existsSync(join(packageRoot, "THIRD_PARTY_NOTICES.md"))).toBe(true);
|
||||
const host = join(cwd, "herdr-test-host.ts");
|
||||
writeFileSync(host, readFileSync(resolve("test/fixtures/herdr-test-host.ts"), "utf8").replace('"../../src/index.js"', JSON.stringify(join(packageRoot, "src/index.ts"))));
|
||||
// Only the worker's Herdr exec is wrapped. Supervisor loads the untouched package manifest.
|
||||
const resources = manifest.pi.extensions.flatMap((path: string) => ["-e", path === "./src/index.ts" ? host : join(packageRoot, path)]);
|
||||
let planPath = ""; let reviewCalls = 0; let judgeCalls = 0;
|
||||
const supervisorModels: string[] = []; const judgeModels: string[] = []; const workerModels: string[] = [];
|
||||
const server = createServer((req, res) => {
|
||||
let raw = ""; req.on("data", chunk => { raw += chunk; }); req.on("end", () => {
|
||||
const body = JSON.parse(raw); const messages = body.messages; const last = messages.at(-1); const text = typeof last.content === "string" ? last.content : JSON.stringify(last.content);
|
||||
const names = (body.tools ?? []).map((tool: any) => tool.function.name);
|
||||
const call = (name: string, args: object) => stream(res, { tool_calls: [{ index: 0, id: `call-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } }] }, "tool_calls");
|
||||
if (last.role === "tool" && messages.at(-2)?.tool_calls?.some((c: any) => c.function.name === "write")) return call("RequestPlanReview", {});
|
||||
if (last.role === "tool") return stream(res, { content: "Check complete." });
|
||||
if (names.includes("review_goal")) {
|
||||
supervisorModels.push(body.model);
|
||||
const request = text.includes("Goal sign-off:");
|
||||
if (request) { reviewCalls++; return call("review_goal", { decision: "approve", reason: "This goal remains faithful to the plan." }); }
|
||||
return call("let_it_run", { reason: "Ready selected; worker starting" });
|
||||
}
|
||||
if (text.includes("intercom status")) return call("intercom", { action: "status" });
|
||||
if (names.includes("CompleteGoal")) {
|
||||
if (text.includes("sign off first")) return call("CompleteGoal", { goal: "first" });
|
||||
if (text.includes("We're in plan mode.") || text.includes("[PLANNING MODE]")) return call("write", { path: planPath, content: "# Plan\n\n## User-visible result\n\nTwo text files.\n\n## Goals\n\n1. [ ] goal: first\n - evidence: evidence.txt says PASS\n2. [ ] goal: second\n\n## Log\n" });
|
||||
workerModels.push(body.model); return stream(res, { content: "Worker is ready." });
|
||||
}
|
||||
judgeCalls++; judgeModels.push(body.model); stream(res, { content: "## checks:\n- evidence.txt: `PASS`; the saved receipt passed\n\nVERDICT: accept\nmissing:" });
|
||||
});
|
||||
});
|
||||
await new Promise<void>(done => server.listen(0, "127.0.0.1", done));
|
||||
const address = server.address(); if (!address || typeof address === "string") throw new Error("Offline HTTP server did not start");
|
||||
const agentDir = join(cwd, ".agent"); mkdirSync(agentDir);
|
||||
writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ packages: [packageRoot] }));
|
||||
writeFileSync(join(agentDir, "models.json"), JSON.stringify({ providers: { offline: { baseUrl: `http://127.0.0.1:${address.port}`, apiKey: "test", api: "openai-completions", models: ["test", "planning", "worker", "supervisor", "judge"].map(id => ({ id, name: `Offline ${id}`, reasoning: false, input: ["text"], contextWindow: 200_000, maxTokens: 1000, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } })) } } }));
|
||||
const modelDir = join(agentDir, "pi-goals"); mkdirSync(modelDir);
|
||||
for (const role of ["worker", "supervisor"]) writeFileSync(join(modelDir, `${role}-model.json`), JSON.stringify({ provider: "offline", id: role }));
|
||||
writeFileSync(join(cwd, "evidence.txt"), "PASS\n");
|
||||
const child = spawn(resolve("node_modules/.bin/pi"), ["--mode", "rpc", "--no-extensions", "--model", "offline/test", ...resources], {
|
||||
cwd, env: { ...process.env, PI_CODING_AGENT_DIR: agentDir, PI_INTERCOM_SCOPE_ID: `test-${Date.now()}`, HERDR_ENV: "1", HERDR_PANE_ID: "test-worker-pane", PI_SUPERVISOR_DEBUG: "1" }, stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
const events: any[] = []; let buffer = ""; let stderr = "";
|
||||
const waits = new Set<(event: any) => void>();
|
||||
child.stdout.on("data", chunk => { buffer += chunk; while (buffer.includes("\n")) { const at = buffer.indexOf("\n"); const line = buffer.slice(0, at); buffer = buffer.slice(at + 1); if (!line) continue; const event = JSON.parse(line); events.push(event); for (const fn of waits) fn(event); } });
|
||||
child.stderr.on("data", chunk => { stderr += chunk; });
|
||||
const send = (message: object) => child.stdin.write(`${JSON.stringify(message)}\n`);
|
||||
const wait = (predicate: (event: any) => boolean, from = 0) => new Promise<any>((done, reject) => {
|
||||
const found = events.slice(from).find(predicate); if (found) return done(found);
|
||||
const timeout = setTimeout(() => { waits.delete(listener); reject(new Error(`RPC timeout; stderr: ${stderr}\nsupervisor: ${existsSync(join(cwd, "supervisor-stderr.log")) ? readFileSync(join(cwd, "supervisor-stderr.log"), "utf8") : "not started"}\nevents: ${JSON.stringify(events.filter(e => e.method === "notify" || e.type === "extension_error")) + JSON.stringify(events.slice(-2))}`)); }, 20_000);
|
||||
const listener = (event: any) => { if (predicate(event)) { clearTimeout(timeout); waits.delete(listener); done(event); } }; waits.add(listener);
|
||||
});
|
||||
try {
|
||||
send({ type: "get_state", id: "state" }); const state = await wait(e => e.type === "response" && e.id === "state");
|
||||
planPath = join(cwd, ".pi/plan", `${state.data.sessionId}-v1.md`);
|
||||
const diagnosticAt = events.length;
|
||||
send({ type: "prompt", id: "diagnostic", message: "intercom status" });
|
||||
const diagnostic = await wait(e => e.type === "tool_execution_end" && e.toolName === "intercom", diagnosticAt);
|
||||
expect(JSON.stringify(diagnostic), "Actual Intercom must connect before testing goals").toContain("Connected: Yes");
|
||||
await wait(e => e.type === "agent_end", diagnosticAt);
|
||||
send({ type: "prompt", id: "plan", message: "/goals plan create the outputs" });
|
||||
const menu = await wait(e => e.type === "extension_ui_request" && e.method === "select");
|
||||
send({ type: "set_model", id: "planning-model", provider: "offline", modelId: "planning" });
|
||||
expect((await wait(e => e.type === "response" && e.id === "planning-model")).success).toBe(true);
|
||||
const readyAt = events.length;
|
||||
send({ type: "extension_ui_response", id: menu.id, value: "Ready" });
|
||||
await wait(e => e.type === "message_start" && JSON.stringify(e.message).includes("Work the goals"), readyAt);
|
||||
await wait(e => e.type === "agent_end", readyAt);
|
||||
send({ type: "prompt", id: "judge-model", message: "/goals judge offline/judge" });
|
||||
await wait(e => e.type === "response" && e.id === "judge-model");
|
||||
const signoffAt = events.length;
|
||||
send({ type: "prompt", id: "signoff", message: "sign off first" });
|
||||
await wait(e => e.type === "tool_execution_end" && e.toolName === "CompleteGoal", signoffAt);
|
||||
expect(readFileSync(planPath, "utf8")).toContain("[x] goal: first"); expect(reviewCalls).toBe(1); expect(judgeCalls).toBe(1);
|
||||
expect(readFileSync(planPath, "utf8")).toContain("[ ] goal: second");
|
||||
expect(workerModels).toEqual(["worker"]);
|
||||
expect(supervisorModels.length).toBeGreaterThan(0); expect(supervisorModels.every(model => model === "supervisor")).toBe(true);
|
||||
expect(judgeModels).toEqual(["judge"]);
|
||||
for (const role of ["planning", "worker", "supervisor"]) expect(JSON.parse(readFileSync(join(modelDir, `${role}-model.json`), "utf8"))).toEqual({ provider: "offline", id: role });
|
||||
} finally { send({ type: "abort" }); child.kill(); server.close(); await new Promise(done => child.once("close", done)); rmSync(cwd, { recursive: true, force: true }); }
|
||||
}, 55_000);
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, vi } from "vitest";
|
||||
|
||||
let agentDir: string;
|
||||
beforeEach(() => { agentDir = mkdtempSync(join(tmpdir(), "goals-model-prefs-")); vi.stubEnv("PI_CODING_AGENT_DIR", agentDir); });
|
||||
afterEach(() => { rmSync(agentDir, { recursive: true, force: true }); vi.unstubAllEnvs(); });
|
||||
@@ -0,0 +1,249 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import goals from "../src/index.js";
|
||||
import { SUPERVISOR_ROLE } from "../src/supervisor.js";
|
||||
|
||||
const judge = vi.hoisted(() => ({ calls: [] as string[][] }));
|
||||
vi.mock("node:child_process", async (original) => {
|
||||
const actual = await original<typeof import("node:child_process")>();
|
||||
return { ...actual, spawn: (_command: string, args: string[]) => {
|
||||
judge.calls.push(args);
|
||||
const process = Object.assign(new EventEmitter(), { stdout: new EventEmitter(), stderr: new EventEmitter(), kill() {} });
|
||||
queueMicrotask(() => { process.stdout.emit("data", "## checks:\n- evidence.txt: `PASS`; the saved check passed\n\nVERDICT: accept\nmissing:"); process.emit("close", 0); });
|
||||
return process;
|
||||
} };
|
||||
});
|
||||
|
||||
const tick = () => new Promise(resolve => setImmediate(resolve));
|
||||
afterEach(() => { vi.unstubAllEnvs(); judge.calls = []; });
|
||||
|
||||
describe("actual goals and supervisor package hooks (Herdr and judge mocked)", () => {
|
||||
it.each(["completion", "compaction failure", "replacement during activation", "steward off during activation", "missing worker model", "unauthenticated worker model", "clear while model unavailable", "off while model unavailable", "clear during recovery restore", "replacement during recovery restore"])("Ready forks once and preserves lifecycle ownership: %s", async (scenario) => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "goals-supervisor-integration-"));
|
||||
const peers: any[] = [];
|
||||
const wires: any[] = [];
|
||||
const herdrCalls: string[][] = [];
|
||||
const modelDir = join(process.env.PI_CODING_AGENT_DIR!, "pi-goals"); mkdirSync(modelDir);
|
||||
for (const role of ["worker", "supervisor"]) writeFileSync(join(modelDir, `${role}-model.json`), JSON.stringify({ provider: "offline", id: role }));
|
||||
vi.stubEnv("HERDR_ENV", "1"); vi.stubEnv("HERDR_PANE_ID", "w1:p1");
|
||||
function make(id: string, manager: SessionManager) {
|
||||
const hooks = new Map<string, any[]>(); const commands = new Map<string, any>(); const tools = new Map<string, any>(); const listeners = new Map<string, Set<any>>();
|
||||
const messages: string[] = []; const contexts: any[] = []; let active = ["read", "grep", "bash", "write", "edit"];
|
||||
const peer: any = { id, manager, messages, contexts, commands, tools, compactions: 0, compactionModels: [], modelChanges: [], aborts: 0 };
|
||||
const bus = {
|
||||
on(name: string, fn: any) { const list = listeners.get(name) ?? new Set(); list.add(fn); listeners.set(name, list); return () => list.delete(fn); },
|
||||
emit(name: string, payload: any) {
|
||||
if (name === "intercom:extension-register") {
|
||||
peer.receive = payload.onEvent;
|
||||
payload.onReady({ snapshot: () => ({ connected: true, supported: true }), listSessions: async () => peers.map(p => ({ id: p.id, pid: p === peer ? process.pid : process.pid + 1, cwd, model: "offline/test" })), publish(wire: any) { if (wire.t === "plan_activate") expect(peer.ctx.model.id).not.toBe("planning"); wires.push(wire); for (const p of peers) queueMicrotask(() => p.receive({ type: "message", fromSessionId: peer.id, payload: wire })); } });
|
||||
return true;
|
||||
}
|
||||
if (name === "processes:request:list") payload.reply([]);
|
||||
if (name === "subagents:rpc:v1:request") {
|
||||
const reply = () => bus.emit(`subagents:rpc:v1:reply:${payload.requestId}`, { requestId: payload.requestId, success: true, data: { fleet: { version: 1, totalActive: 0 } } });
|
||||
if (peer.delayBackground) { peer.finishBackground = reply; return; }
|
||||
reply();
|
||||
}
|
||||
for (const fn of listeners.get(name) ?? []) fn(payload);
|
||||
},
|
||||
};
|
||||
const pi: any = {
|
||||
setModel: async (model: any) => {
|
||||
if (peer.noAuth && model.id === "worker") return false;
|
||||
if (peer.deferWorkerRestore && model.id === "worker") {
|
||||
peer.deferWorkerRestore = false;
|
||||
await new Promise<void>(resolve => { peer.finishRestore = resolve; });
|
||||
}
|
||||
const previousModel = ctx.model; ctx.model = model; peer.modelChanges.push(model.id);
|
||||
manager.appendModelChange(model.provider, model.id);
|
||||
await peer.hook("model_select", { source: "set", model, previousModel }); return true;
|
||||
},
|
||||
events: bus, on(name: string, fn: any) { hooks.set(name, [...(hooks.get(name) ?? []), fn]); },
|
||||
registerCommand(name: string, command: any) { commands.set(name, command); }, registerTool(tool: any) { tools.set(tool.name, tool); active.push(tool.name); },
|
||||
appendEntry: (name: string, data: any) => manager.appendCustomEntry(name, data),
|
||||
getActiveTools: () => active, setActiveTools: (names: string[]) => { active = names; },
|
||||
getCommands: () => [{ name: "supervise", sourceInfo: { path: "internal-supervisor" } }],
|
||||
getAllTools: () => [...["read", "grep", "find", "ls"].map(name => ({ name, sourceInfo: { source: "builtin" } })), { name: "subagent" }, { name: "intercom", sourceInfo: { path: "intercom-test-only" } }],
|
||||
sendUserMessage: (text: string) => messages.push(text), sendMessage: (message: any) => contexts.push(message),
|
||||
exec: async (command: string, args: string[]) => {
|
||||
expect(command).toBe("herdr"); herdrCalls.push(args);
|
||||
if (args[0] === "pane") return { code: 0, stdout: JSON.stringify({ result: { pane: { pane_id: "w1:p2" } } }), stderr: "" };
|
||||
if (args[1] === "start") {
|
||||
const sessionFile = args[args.indexOf("--session") + 1];
|
||||
const supervisor = make("supervisor", SessionManager.open(sessionFile));
|
||||
await supervisor.hook("session_start"); await tick();
|
||||
}
|
||||
return { code: 0, stdout: "{}", stderr: "" };
|
||||
},
|
||||
};
|
||||
const ctx: any = { cwd, hasUI: true, isIdle: () => true, model: { provider: "offline", id: (manager.getBranch().findLast((entry: any) => entry.type === "model_change") as any)?.modelId ?? "test", contextWindow: 200_000 }, sessionManager: manager,
|
||||
modelRegistry: { find: (provider: string, id: string) => peer.missing && id === "worker" ? undefined : ({ provider, id, contextWindow: 200_000 }) },
|
||||
getContextUsage: () => ({ tokens: 50_000 }), compact({ onComplete, onError }: any) { peer.compactions++; peer.compactionModels.push(ctx.model.id); if (scenario === "compaction failure" && id === "supervisor") onError(new Error("Native compaction timeout")); else onComplete({}); }, abort() { peer.aborts++; },
|
||||
ui: { theme: { fg: (_: string, text: string) => text }, setWidget: vi.fn(), setStatus: vi.fn(), notify: vi.fn(), select: async () => "Ready" },
|
||||
};
|
||||
peer.pi = pi; peer.ctx = ctx; peer.hook = async (name: string, event = {}) => { for (const fn of hooks.get(name) ?? []) await fn(event, ctx); };
|
||||
peers.push(peer); goals(pi); return peer;
|
||||
}
|
||||
const manager = SessionManager.create(cwd, join(cwd, "sessions"));
|
||||
manager.appendMessage({ role: "user", content: "Keep the literal [x] and produce two files", timestamp: Date.now() });
|
||||
manager.appendMessage({ role: "assistant", content: [{ type: "text", text: "Plan drafted" }], api: "openai-completions", provider: "offline", model: "test", stopReason: "stop", timestamp: Date.now(), usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0, totalTokens: 2, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } });
|
||||
const worker = make("worker", manager);
|
||||
try {
|
||||
await worker.hook("session_start");
|
||||
await worker.commands.get("goals").handler("plan judge the vendor options", worker.ctx);
|
||||
worker.ctx.model = { provider: "offline", id: "planning", contextWindow: 200_000 };
|
||||
manager.appendModelChange("offline", "planning");
|
||||
await worker.hook("model_select", { source: "cycle", model: worker.ctx.model });
|
||||
const path = join(cwd, ".pi", "plan", `${manager.getSessionId()}-v1.md`);
|
||||
writeFileSync(path, '# Plan\n\nUser voice: render "[x]" literally\n\n1. [ ] goal: first\n2. [ ] goal: second\n\n## Log\n');
|
||||
worker.delayBackground = scenario.includes("during activation");
|
||||
worker.missing = scenario === "missing worker model" || scenario.includes("during recovery restore");
|
||||
worker.noAuth = scenario === "unauthenticated worker model";
|
||||
await worker.tools.get("RequestPlanReview").execute("", {}, undefined, undefined, worker.ctx);
|
||||
const starting = worker.hook("agent_settled");
|
||||
if (worker.delayBackground) {
|
||||
await vi.waitFor(() => expect(worker.finishBackground).toBeTypeOf("function"));
|
||||
expect(worker.messages.filter((m: string) => m.startsWith("Work the goals"))).toHaveLength(0);
|
||||
const stopping = scenario === "steward off during activation";
|
||||
await worker.commands.get("goals").handler(stopping ? "steward off" : "plan a replacement that has not received Ready", worker.ctx);
|
||||
worker.finishBackground(); await starting; await tick();
|
||||
expect(worker.messages.filter((m: string) => m.startsWith("Work the goals"))).toHaveLength(0);
|
||||
expect(worker.ctx.model.id).toBe("planning");
|
||||
expect(worker.modelChanges).toContain("worker");
|
||||
const saved = manager.getBranch().findLast((entry: any) => entry.customType === "pi-goals-state") as any;
|
||||
expect(saved.data).toMatchObject({ phase: "planning", planVersion: stopping ? 1 : 2, supervisor: null });
|
||||
if (stopping) {
|
||||
expect(saved.data.stewardEnabled).toBe(false);
|
||||
worker.delayBackground = false;
|
||||
await worker.hook("agent_settled"); // Ready is offered again; ordinary work can now start.
|
||||
expect(worker.messages.filter((m: string) => m.startsWith("Work the goals"))).toHaveLength(1);
|
||||
const retried = manager.getBranch().findLast((entry: any) => entry.customType === "pi-goals-state") as any;
|
||||
expect(retried.data).toMatchObject({ phase: "working", planVersion: 1 });
|
||||
}
|
||||
expect(herdrCalls.filter(args => args[1] === "start")).toHaveLength(1);
|
||||
return;
|
||||
}
|
||||
await starting; await tick();
|
||||
const supervisor = peers[1];
|
||||
if (scenario === "compaction failure") {
|
||||
const saved = manager.getBranch().findLast((entry: any) => entry.customType === "pi-goals-state") as any;
|
||||
expect(saved.data).toMatchObject({ phase: "planning", reviewRequested: false, startupError: expect.stringContaining("Native compaction timeout") });
|
||||
expect(worker.ctx.ui.setStatus).toHaveBeenCalledWith("pi-goals", "supervisor startup failed");
|
||||
expect(worker.messages.some((m: string) => m.startsWith("Work the goals"))).toBe(false);
|
||||
expect(wires.some(w => w.t === "plan_failed")).toBe(true);
|
||||
expect(wires.some(w => w.t === "plan_activate")).toBe(false);
|
||||
await worker.hook("agent_settled");
|
||||
expect(herdrCalls.filter(args => args[1] === "start")).toHaveLength(1);
|
||||
await worker.hook("session_start", { reason: "reload" });
|
||||
expect(worker.ctx.ui.setStatus).toHaveBeenCalledWith("pi-goals", "supervisor startup failed");
|
||||
return;
|
||||
}
|
||||
if (worker.missing || worker.noAuth) {
|
||||
const saved = manager.getBranch().findLast((entry: any) => entry.customType === "pi-goals-state") as any;
|
||||
expect(saved.data).toMatchObject({ phase: "planning", modelRecovery: "worker" });
|
||||
expect(wires.some(w => w.t === "plan_activate" || w.t === "view")).toBe(false);
|
||||
expect(supervisor.messages).toEqual([]);
|
||||
expect(worker.messages.some((m: string) => m.startsWith("Work the goals"))).toBe(false);
|
||||
expect(worker.ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("worker model paused"), "error");
|
||||
await worker.hook("session_start", { reason: "reload" });
|
||||
expect(worker.ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("/goals model current"), "error");
|
||||
const before = worker.messages.length;
|
||||
worker.receive({ type: "message", fromSessionId: supervisor.id, payload: { t: "directive", to: worker.id, text: "Do not revive an inactive worker" } });
|
||||
await tick(); expect(worker.messages).toHaveLength(before);
|
||||
worker.missing = false; worker.noAuth = false;
|
||||
if (scenario.includes("during recovery restore")) {
|
||||
worker.deferWorkerRestore = true;
|
||||
const startupCount = herdrCalls.length;
|
||||
const recovering = worker.hook("agent_settled");
|
||||
await vi.waitFor(() => expect(worker.finishRestore).toBeTypeOf("function"));
|
||||
const clearing = scenario.startsWith("clear");
|
||||
await worker.commands.get("goals").handler(clearing ? "clear" : "plan replacement has no Ready", worker.ctx);
|
||||
worker.finishRestore(); await recovering; await tick();
|
||||
expect(herdrCalls).toHaveLength(startupCount);
|
||||
expect(wires.some(w => w.t === "plan_activate")).toBe(false);
|
||||
expect(worker.messages.some((m: string) => m.startsWith("Work the goals"))).toBe(false);
|
||||
expect(manager.getBranch().findLast((entry: any) => entry.customType === "pi-goals-state").data).toMatchObject({ phase: clearing ? null : "planning", planVersion: clearing ? null : 2, supervisor: null, modelRecovery: null });
|
||||
expect(worker.ctx.model.id).toBe("planning");
|
||||
return;
|
||||
}
|
||||
await worker.pi.setModel({ provider: "offline", id: "recovered-worker", contextWindow: 200_000 });
|
||||
await vi.waitFor(() => expect(worker.messages.filter((m: string) => m.startsWith("Work the goals"))).toHaveLength(1));
|
||||
expect(JSON.parse(readFileSync(join(modelDir, "worker-model.json"), "utf8"))).toEqual({ provider: "offline", id: "recovered-worker" });
|
||||
expect(JSON.parse(readFileSync(join(modelDir, "planning-model.json"), "utf8"))).toEqual({ provider: "offline", id: "planning" });
|
||||
expect(wires.filter(w => w.t === "plan_activate")).toHaveLength(1);
|
||||
expect(herdrCalls.filter(args => args[1] === "start")).toHaveLength(1);
|
||||
return;
|
||||
}
|
||||
if (scenario.includes("while model unavailable")) {
|
||||
worker.noAuth = true;
|
||||
await worker.hook("session_start", { reason: "reload" });
|
||||
await worker.commands.get("goals").handler(scenario.startsWith("clear") ? "clear" : "steward off", worker.ctx);
|
||||
await tick();
|
||||
for (const peer of peers) expect(peer.manager.getBranch().findLast((e: any) => e.customType === "supervise-state").data.role).toBe("none");
|
||||
const before = worker.messages.length;
|
||||
worker.receive({ type: "message", fromSessionId: supervisor.id, payload: { t: "directive", to: worker.id, text: "An old directive must not revive work" } });
|
||||
await tick(); expect(worker.messages).toHaveLength(before);
|
||||
expect(supervisor.aborts).toBeGreaterThan(0);
|
||||
return;
|
||||
}
|
||||
expect(supervisor).toBeDefined(); expect(supervisor.manager.getSessionFile()).not.toBe(manager.getSessionFile());
|
||||
expect(supervisor.manager.getBranch().some((e: any) => e.customType === SUPERVISOR_ROLE)).toBe(true);
|
||||
expect(supervisor.compactionModels).toEqual(["supervisor"]);
|
||||
expect(supervisor.manager.getBranch().some((entry: any) => entry.type === "model_change" && entry.modelId === "worker")).toBe(false);
|
||||
expect(supervisor.manager.getBranch().some((entry: any) => entry.type === "model_change" && entry.modelId === "planning")).toBe(true);
|
||||
expect(worker.ctx.model.id).toBe("worker");
|
||||
expect(JSON.parse(readFileSync(join(modelDir, "planning-model.json"), "utf8"))).toEqual({ provider: "offline", id: "planning" });
|
||||
expect(supervisor.compactions).toBe(1); expect(worker.compactions).toBe(0);
|
||||
expect(worker.messages.filter((m: string) => m.startsWith("Work the goals"))).toHaveLength(1);
|
||||
expect(supervisor.pi.getActiveTools()).not.toContain("CompleteGoal");
|
||||
await supervisor.tools.get("let_it_run").execute("ready-assessed", { reason: "Worker is ready" }, undefined, undefined, supervisor.ctx);
|
||||
await supervisor.hook("agent_settled");
|
||||
await worker.commands.get("goals").handler("judge offline/judge", worker.ctx);
|
||||
writeFileSync(path, readFileSync(path, "utf8").replace("[ ] goal: first", "[x] goal: first"));
|
||||
const premature = worker.tools.get("CompleteGoal").execute("", { goal: "first" }, undefined, undefined, worker.ctx);
|
||||
await tick();
|
||||
expect(readFileSync(path, "utf8")).toContain("[/] goal: first");
|
||||
await supervisor.hook("context", { messages: supervisor.contexts });
|
||||
await supervisor.tools.get("review_goal").execute("", { decision: "needs_work", reason: "The overflow test does not exercise overflow" });
|
||||
await supervisor.hook("agent_settled");
|
||||
expect((await premature).isError).toBe(true);
|
||||
expect(readFileSync(path, "utf8")).not.toContain("[x] goal: first");
|
||||
expect(judge.calls).toHaveLength(0);
|
||||
for (const goal of ["first", "second"]) {
|
||||
const completion = worker.tools.get("CompleteGoal").execute("", { goal }, undefined, undefined, worker.ctx);
|
||||
await tick(); const request = wires.findLast((w: any) => w.t === "goal_review");
|
||||
expect(request.goal).toBe(goal);
|
||||
await supervisor.hook("context", { messages: supervisor.contexts });
|
||||
await supervisor.tools.get("review_goal").execute("", { decision: "approve", reason: "Within the requested scope" });
|
||||
await supervisor.hook("agent_settled");
|
||||
const completed = await completion;
|
||||
expect(completed.isError, JSON.stringify(completed)).toBe(false);
|
||||
expect(readFileSync(path, "utf8")).toContain(`[x] goal: ${goal}`);
|
||||
const records = manager.getBranch().findLast((entry: any) => entry.customType === "pi-goals-state") as any;
|
||||
expect(records.data.signedOffGoals).toContainEqual({ subject: goal, outcome: "accept" });
|
||||
}
|
||||
await supervisor.commands.get("supervise").handler("look", supervisor.ctx); await tick();
|
||||
expect(wires.findLast(w => w.t === "view").completion).toMatchObject({ total: 2, pending: 0, inconclusive: 0 });
|
||||
await supervisor.tools.get("let_it_run").execute("reviewed", { reason: "Both goals signed off" }, undefined, undefined, supervisor.ctx);
|
||||
await supervisor.hook("agent_settled");
|
||||
expect(judge.calls).toHaveLength(2); expect(judge.calls.every(args => args.includes("--no-extensions") && args.includes("offline/judge"))).toBe(true);
|
||||
expect(worker.ctx.model.id).toBe("worker");
|
||||
expect(supervisor.ctx.model.id).toBe("supervisor");
|
||||
expect(wires.some(w => w.t === "done")).toBe(false);
|
||||
await worker.commands.get("goals").handler("supervisor", worker.ctx);
|
||||
expect(herdrCalls.at(-1)).toEqual(["agent", "focus", "w1:p2"]);
|
||||
await supervisor.commands.get("goals").handler("worker", supervisor.ctx);
|
||||
expect(herdrCalls.at(-1)).toEqual(["agent", "focus", "w1:p1"]);
|
||||
const pending = worker.tools.get("CompleteGoal").execute("", { goal: "first" }, undefined, undefined, worker.ctx);
|
||||
await tick(); await worker.commands.get("goals").handler("steward off", worker.ctx);
|
||||
expect((await pending).isError).toBe(true); await tick();
|
||||
expect(judge.calls).toHaveLength(2); expect(supervisor.aborts).toBeGreaterThan(0);
|
||||
const cancelledRecords = manager.getBranch().findLast((entry: any) => entry.customType === "pi-goals-state") as any;
|
||||
expect(cancelledRecords.data.signedOffGoals).toEqual([{ subject: "second", outcome: "accept" }]);
|
||||
} finally { for (const peer of peers) await peer.hook("session_shutdown"); rmSync(cwd, { recursive: true, force: true }); }
|
||||
}, 15_000);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user