@gotgenes/pi-subagents 16.5.0 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/public.d.ts +4 -1
  3. package/docs/architecture/architecture.md +39 -31
  4. package/docs/plans/0421-migrate-activity-readers-to-record-getters.md +309 -0
  5. package/docs/plans/0422-delete-activity-tracker-ui-observer.md +204 -0
  6. package/docs/plans/0423-widget-self-drive-from-lifecycle.md +264 -0
  7. package/docs/plans/0424-drop-widget-dep-from-subagent-tool.md +166 -0
  8. package/docs/plans/0425-reconcile-subagent-events-contract.md +144 -0
  9. package/docs/retro/0420-fold-run-metrics-live-activity-onto-record.md +45 -0
  10. package/docs/retro/0421-migrate-activity-readers-to-record-getters.md +105 -0
  11. package/docs/retro/0422-delete-activity-tracker-ui-observer.md +97 -0
  12. package/docs/retro/0423-widget-self-drive-from-lifecycle.md +112 -0
  13. package/docs/retro/0424-drop-widget-dep-from-subagent-tool.md +89 -0
  14. package/docs/retro/0425-reconcile-subagent-events-contract.md +46 -0
  15. package/package.json +1 -1
  16. package/src/index.ts +12 -6
  17. package/src/observation/composite-subagent-observer.ts +49 -0
  18. package/src/observation/notification.ts +3 -12
  19. package/src/observation/subagent-events-observer.ts +1 -4
  20. package/src/runtime.ts +0 -6
  21. package/src/service/service.ts +4 -1
  22. package/src/tools/agent-tool.ts +1 -36
  23. package/src/tools/background-spawner.ts +1 -25
  24. package/src/tools/foreground-runner.ts +9 -34
  25. package/src/tools/helpers.ts +5 -4
  26. package/src/types.ts +1 -1
  27. package/src/ui/agent-menu.ts +0 -12
  28. package/src/ui/agent-widget.ts +56 -17
  29. package/src/ui/conversation-viewer.ts +4 -9
  30. package/src/ui/widget-renderer.ts +14 -22
  31. package/src/ui/agent-activity-tracker.ts +0 -84
  32. package/src/ui/ui-observer.ts +0 -61
@@ -0,0 +1,112 @@
1
+ ---
2
+ issue: 423
3
+ issue_title: "pi-subagents: make the agent widget self-drive from lifecycle events"
4
+ ---
5
+
6
+ # Retro: #423 — Make the agent widget self-drive from lifecycle events
7
+
8
+ ## Stage: Planning (2026-06-17T00:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned Phase 18 Step 4 of the widget/tool decoupling track: making `AgentWidget` a `SubagentManagerObserver` that self-drives its 80 ms timer from lifecycle notifications, wired via a new `CompositeSubagentObserver` fan-out, and removing all inbound widget calls from the spawn tools.
13
+ Wrote a four-step plan (three `refactor:` commits + a `docs:` sweep) at `packages/pi-subagents/docs/plans/0423-widget-self-drive-from-lifecycle.md`.
14
+
15
+ ### Observations
16
+
17
+ - **Wiring mechanism was the live design decision.**
18
+ `SubagentManager` has a single `observer` slot.
19
+ Three options surfaced: (A) a `CompositeSubagentObserver` fan-out in `index.ts`, (B) make the manager hold an observer list, (C) subscribe the widget to the public `pi.events` channels.
20
+ The operator initially leaned toward B (matching the issue's literal file list) but was unsure; after reframing around the decouple + overridable-UI north star, they chose **A**.
21
+ Rationale recorded in the plan: A keeps the core closed for modification, B moves fan-out *into* the core (wrong direction), and C front-runs the Step 6 ([#425]) public-event-contract reconciliation.
22
+ Key insight that flattened the decision: all three options keep the widget's `manager.listAgents()` reference, so they only change the *trigger*, not the data source — full broadcast-plus-query decoupling is the Step 8 ([#427]) concern.
23
+ - **`markFinished` is fully redundant** and is deleted, not relocated: `seedFinishedAgents()` (added in [#421] / [#422]) already seeds any agent with `completedAt` on each poll tick.
24
+ This matters because the manager **never fires `onSubagentCompleted` for foreground agents** (`onRunFinished` guards on `isBackground`), so the widget could not learn of foreground completion via the observer anyway — polling covers it.
25
+ - **Construction cycle** (widget needs manager → manager needs observer → observer includes widget) is broken by constructing the composite with `[eventsObserver]`, passing it to the manager, then `observer.add(widget)` after the widget is built; the manager consults the observer only lazily at spawn time.
26
+ - **TDD ordering avoids a behavior gap:** Step 2 wires the widget as an observer *while the spawn tools still drive it* (idempotent double-drive), strictly before Step 3 removes the spawn-tool calls — so no commit leaves the widget without a timer-start signal.
27
+ The new widget observer methods need no `fallow-ignore` because they are invoked polymorphically through `SubagentManagerObserver` (the `SubagentEventsObserver` precedent).
28
+ - This step narrows `AgentToolWidget` to `setUICtx` only but keeps the `AgentTool` widget constructor param; full removal is Step 5 ([#424]).
29
+ - Non-breaking and internal-only (no public service/settings surface touched), so `refactor:`/`docs:` commits, no `BREAKING CHANGE` footer.
30
+
31
+ ## Stage: Implementation — TDD (2026-06-17T10:00:00Z)
32
+
33
+ ### Session summary
34
+
35
+ Executed the four-step plan exactly as written: added `CompositeSubagentObserver` (Step 1), made `AgentWidget` a `SubagentManagerObserver` and wired the composite in `index.ts` (Step 2), removed all spawn-tool widget wiring and deleted `markFinished` (Step 3), then swept the architecture doc + SKILL.md (Step 4).
36
+ Four commits (three `refactor:`, one `docs:`); test count `1032 → 1039` (+7 composite, +4 widget observer, −4 removed spawner/fixture widget-driving tests).
37
+ `check`, root `lint`, full `test`, and `fallow dead-code` all green; pre-completion reviewer returned PASS.
38
+
39
+ ### Observations
40
+
41
+ - **Mid-step correction (not a separate commit):** in Step 2 I initially over-reached by deleting `markFinished` and privatizing `ensureTimer`, which breaks `index.ts` typing because `AgentToolWidget` still required those methods until Step 3.
42
+ Caught it before committing and reverted both to Step 3 per the plan; Step 2 kept them public.
43
+ The plan's ordering (narrow `AgentToolWidget` and delete `markFinished` only in the atomic Step 3 removal) was correct — the lesson is to trust the step boundaries.
44
+ - **Biome `useIterableCallbackReturn` false trigger:** naming the composite's private fan-out helper `forEach` made Biome treat the call as `Array.prototype.forEach` and reject the value-returning arrow.
45
+ Renamed it to `dispatch` — a strictly better name anyway.
46
+ - **`vi.getTimerCount()` cleanly proves the timer started:** the widget observer tests assert `getTimerCount()` goes `0 → 1` on `onSubagentStarted`/`onSubagentCreated`, distinguishing `startLoop` (ensureTimer + render) from a bare `update()` (render only), with the manager-stub creating no other timers.
47
+ - **No dead-code window:** the widget's new observer methods are invoked polymorphically through `SubagentManagerObserver` (the `SubagentEventsObserver` precedent), so `fallow` saw them as used from Step 2 onward; `ensureTimer` became `private` in Step 3, dropping its now-stale `fallow-ignore`.
48
+ - **The Step 2 transient double-drive was harmless** as predicted: both the spawn tools and the composite drove the widget in that commit, and every driven method is idempotent — full suite green at that commit.
49
+ - **Reviewer notes (non-blocking, both PASS):** a pre-existing "six domains" vs "seven domains" inconsistency in `architecture.md` (Phase 17, out of scope, left alone); and `observer.add(widget)` is a justified post-construction write documented as the only construction-cycle break (widget needs manager, manager needs observer).
50
+ - **Pre-completion reviewer: PASS** — all deterministic checks, code-design, test-artifact, Mermaid (`mmdc` parsed all 6 blocks), dead-code, and cross-step-invariant lenses passed.
51
+
52
+ ## Stage: Final Retrospective (2026-06-17T18:30:00Z)
53
+
54
+ ### Session summary
55
+
56
+ Shipped Phase 18 Step 4 across plan → TDD → ship in a single session: the `AgentWidget` became a `SubagentManagerObserver` that self-drives its 80 ms timer through a new `CompositeSubagentObserver` fan-out, and the spawn tools shed every widget call.
57
+ Four implementation commits (three `refactor:`, one `docs:`), pre-completion reviewer PASS, CI green, issue closed; no release (all commits non-releasing).
58
+ Execution was clean — two self-caught agent-side slips, no rework commits, and the only user input was one productive redirect plus a release-timing answer.
59
+
60
+ ### Observations
61
+
62
+ #### What went well
63
+
64
+ - **Two-round `ask_user` on the wiring decision (planning).**
65
+ The first round offered three mechanisms (composite / manager-list / `pi.events`); the operator picked B but flagged uncertainty.
66
+ Reframing around the decouple + overridable-UI north star and re-asking flipped the choice to A (`CompositeSubagentObserver`) with explicit buy-in.
67
+ The reframe surfaced the insight that flattened the decision: all three options keep the widget's `manager.listAgents()` reference, so they change only the *trigger*, not the data source.
68
+ - **`vi.getTimerCount()` as a precise timer-start assertion.**
69
+ The widget observer tests assert `getTimerCount()` goes `0 → 1` on `onSubagentStarted`/`onSubagentCreated`, cleanly distinguishing `startLoop` (timer + render) from a bare `update()` (render only).
70
+ - **"Double-drive" lift-and-shift ordering.**
71
+ Step 2 wired the new observer while the spawn tools still drove the widget (idempotent overlap), and Step 3 removed the old path — no commit left the widget without a timer-start signal, and the full suite was green at every commit.
72
+ - **Incremental verification cadence.**
73
+ `pnpm run check` after each shared-type change, `vitest` per-file in red/green, `mmdc` on all 6 Mermaid blocks, and `fallow`/`lint`/full-suite at the end of Step 3 — no end-only verification.
74
+
75
+ #### What caused friction (agent side)
76
+
77
+ - `scope-drift` (self-identified) — in TDD Step 2 I deleted `markFinished` and privatized `ensureTimer` ahead of schedule; both belonged to the atomic Step 3 removal because `AgentToolWidget` still required them until then (deleting early breaks `index.ts` typing).
78
+ Caught by reasoning before committing and reverted to Step 3.
79
+ Impact: a few corrective edits, no extra commit, no rework.
80
+ Lesson: re-read the step's exact scope before editing, rather than editing from the overall design held in memory.
81
+ - `missing-context` (self-identified) — naming the composite's private fan-out helper `forEach` tripped Biome's `useIterableCallbackReturn` (it treated the call as `Array.prototype.forEach` and rejected the value-returning arrow).
82
+ Renamed to `dispatch`.
83
+ Impact: one rename, caught immediately by the `pi-autoformat` hook; no rework.
84
+ - `other` — the ship stage asked the batch-vs-release question for a multi-issue sequence, but every unreleased commit was `refactor:`/`docs:`, so release-please produced no PR regardless of the answer.
85
+ The operator's "release now" answer was therefore unactionable (the work auto-batches until a `feat`/`fix` lands).
86
+ Impact: one unactionable `ask_user` round; correct outcome reached, but the question implied a choice that did not exist.
87
+
88
+ #### What caused friction (user side)
89
+
90
+ - None blocking — the planning redirect ("I'm not confident in my choice… the goal is to decouple… an event system would make sense but isn't on the roadmap") was the ideal intervention: a redirecting question that handed over the north star instead of a post-hoc correction, and it directly produced the better design.
91
+
92
+ ### Diagnostic details
93
+
94
+ - **Model-performance correlation** — Planning ran on `anthropic/claude-opus-4-8` (design + the two-round wiring `ask_user`; appropriate).
95
+ TDD ran on `anthropic/claude-sonnet-4-6` (implementation; handled the Step 2 self-correction and the Biome rename; adequate).
96
+ The `pre-completion-reviewer` subagent ran on `anthropic/claude-sonnet-4-6` (its frontmatter) and returned a thorough PASS.
97
+ Ship ran on `opencode-go/deepseek-v4-flash` — a reasoning-weak model carrying the batch-vs-release judgment and the "no release PR expected" inference.
98
+ It reached the correct outcome, but this is the same latent risk the #422 retro flagged: a weak model on the one ship-stage judgment call.
99
+ Proposal 1 mitigates it by making the release-trigger check deterministic so the stage needs no judgment.
100
+ - **Escalation-delay tracking** — no `rabbit-hole`s; both agent-side slips resolved in one tool call each (one rename, one revert); no sequence exceeded 5 calls on one error.
101
+ - **Unused-tool detection** — no `missing-context` gaps requiring a subagent; `grep` (not `colgrep`) was used in planning, correctly — every search was an exact symbol match (`ensureTimer`, `markFinished`, `observer`).
102
+ - **Feedback-loop gap analysis** — no gap; verification was incremental, not end-only (see "What went well").
103
+
104
+ ### Changes made
105
+
106
+ 1. `.pi/prompts/ship-issue.md` — step 4b now opens with a release-trigger gate: check `git log --oneline <last-tag>..HEAD`, and when every commit is a non-releasing type (`refactor:`/`docs:`/`style:`/`chore:`/`test:`), state that release-please cuts nothing now and skip the batch-vs-release question.
107
+
108
+ [#421]: https://github.com/gotgenes/pi-packages/issues/421
109
+ [#422]: https://github.com/gotgenes/pi-packages/issues/422
110
+ [#424]: https://github.com/gotgenes/pi-packages/issues/424
111
+ [#425]: https://github.com/gotgenes/pi-packages/issues/425
112
+ [#427]: https://github.com/gotgenes/pi-packages/issues/427
@@ -0,0 +1,89 @@
1
+ ---
2
+ issue: 424
3
+ issue_title: "pi-subagents: drop the widget and activity-map dependencies from the subagent tool"
4
+ ---
5
+
6
+ # Retro: #424 — pi-subagents: drop the widget and activity-map dependencies from the subagent tool
7
+
8
+ ## Stage: Planning (2026-06-18T15:01:42Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned Phase 18 Step 5: dropping the `widget` constructor dependency from `AgentTool` and shedding the widget stub from `createToolDeps`.
13
+ Verified against current `main` that the `agentActivity` / activity-map dependency named in the issue and roadmap was already removed in Phase 18 Step 3 ([#422]) — only `widget` remains to drop, so the plan corrects that stale wording.
14
+ Produced `docs/plans/0424-drop-widget-dep-from-subagent-tool.md` and committed it.
15
+
16
+ ### Observations
17
+
18
+ - The issue body and the architecture roadmap's Step 5 entry both still say `agentActivity`, but `grep` found no live `agentActivity`/`AgentActivity` references in `src/` — the only hit is a comment in `test/lifecycle/usage.test.ts`.
19
+ Flagged the roadmap Step 5 description for a stale-wording fix during implementation.
20
+ - This is a purely subtractive, non-breaking refactor: `AgentTool` is internal (public exports are only the service and settings entries), and `ToolStartHandler` already captures UICtx on every `tool_execution_start`, which fires before any tool's `execute`.
21
+ So removing the tool's own `setUICtx` call loses no behavior — `test/handlers/tool-start.test.ts` already pins UICtx capture on its true owner.
22
+ - Folded all edits into one refactor commit because removing the constructor parameter breaks the `index.ts:152` call site, the `make-deps.ts` fixture, and `agent-tool.test.ts` at typecheck time — they cannot land separately.
23
+ - Two obsolete tests to remove: `agent-tool.test.ts` → `"sets UI context on runtime at start of execute"` and `make-deps.test.ts` → `describe("widget defaults")`.
24
+ The `UICtx` type itself stays (used by `agent-widget.ts`, `tool-start.ts`, and the widget test) — only the `UICtx` import in `agent-tool.ts` and the `AgentToolWidget` interface go.
25
+ - Skipped the `ask-user` gate: operator-authored issue (`gotgenes`), unambiguous proposed change following an established roadmap, clearly non-breaking.
26
+
27
+ ## Stage: Implementation — TDD (2026-06-18T15:10:13Z)
28
+
29
+ ### Session summary
30
+
31
+ Executed the two-step plan: one `refactor:` commit dropping the `widget` constructor dependency from `AgentTool` (plus the `UICtx` import, `AgentToolWidget` interface, the redundant `setUICtx` call, the `index.ts` call site, the `createToolDeps` field/stub, and two obsolete tests), and one `docs:` commit marking Phase 18 Step 5 complete in the architecture doc.
32
+ Test count went from 1039 to 1037 (−2 removed obsolete tests); `check`, root `lint`, full `test`, and `fallow dead-code` all green.
33
+
34
+ ### Observations
35
+
36
+ - The refactor was purely subtractive and folded all src + test edits into one commit, as the plan predicted — the constructor-signature change breaks `index.ts`, `make-deps.ts`, and `agent-tool.test.ts` at typecheck simultaneously.
37
+ - Renamed the unused `execute` `ctx` parameter to `_ctx` rather than removing it — the inner `defineTool` `execute` closure must keep the 5-arg signature, so the method keeps the positional slot but ignores it.
38
+ - Corrected the architecture doc's Step 5 `Outcome` from "fixture drops 2 fields" to "drops 1 field" and noted the `agentActivity` half was already done in [#422] — the plan flagged this stale wording in advance.
39
+ - Pre-completion reviewer: WARN (1 non-blocking finding).
40
+ The `package-pi-subagents` SKILL test count was stale (994/63 "as of Phase 17 Step 4"); refreshed to 1037/64 "as of Phase 18 Step 5" in a follow-up `docs:` commit.
41
+ All cross-step invariants verified — UICtx capture stays pinned by `handlers/tool-start.test.ts`, and no `setUICtx` calls remain in spawn tools or the tool.
42
+
43
+ ## Stage: Final Retrospective (2026-06-18T15:20:14Z)
44
+
45
+ ### Session summary
46
+
47
+ Planned, implemented, and shipped Phase 18 Step 5 across three stages: a subtractive refactor dropping the `widget` constructor dependency from `AgentTool`, landed as one `refactor:` commit plus two `docs:` commits.
48
+ CI passed, `#424` closed; all commits since `pi-subagents-v16.6.0` are non-releasing (`refactor:`/`docs:`/`style:`), so release-please batches until a `feat`/`fix` lands.
49
+ Net test count 1039 → 1037 (−2 obsolete tests); the pre-completion reviewer returned WARN on a single stale-doc finding that was fixed inline.
50
+
51
+ ### Observations
52
+
53
+ #### What went well
54
+
55
+ - Planning caught that the issue body and roadmap Step 5 both named an `agentActivity`/activity-map dependency that was already removed in [#422]; a `grep` of `src/` for `agentActivity` confirmed only `widget` remained.
56
+ The `/plan-issue` "treat the proposed change as a hypothesis, verify against current code" discipline worked exactly as intended and prevented planning around a phantom dependency.
57
+ - The plan correctly predicted the atomic-commit shape: removing the constructor parameter breaks `index.ts`, `make-deps.ts`, and `agent-tool.test.ts` at typecheck simultaneously, so the implementation folded all edits into one commit with zero rework.
58
+ - Verification ran incrementally — `pnpm run check` plus the affected test files after the edits, then the full suite + root `lint` + `fallow dead-code` at the end of TDD — so no feedback-loop gap.
59
+ - Clean handoff between stages: the planning retro entry flagged the stale `agentActivity` wording in advance, and the TDD stage acted on it without re-deriving the context.
60
+
61
+ #### What caused friction (agent side)
62
+
63
+ - `missing-context` (low impact, recurring) — the `package-pi-subagents` SKILL.md hardcoded test count (`994 tests across 63 files as of Phase 17 Step 4`) was stale by four issues.
64
+ The SKILL.md was edited by `docs:` commits in [#421], [#422], [#423], and [#424], yet none refreshed the count until the [#424] pre-completion reviewer flagged it.
65
+ Impact: one extra `docs:` commit (`18900d3f`) at ship time; no rework, but the precise count is structurally prone to per-step drift and the reviewer caught it only by chance at one of five Phase 18 steps.
66
+
67
+ #### What caused friction (user side)
68
+
69
+ - None.
70
+ The session was fully operator-authored and roadmap-driven; no user intervention or correction was needed at any stage.
71
+
72
+ ### Diagnostic details
73
+
74
+ - **Model-performance correlation** — the `pre-completion-reviewer` subagent ran on `anthropic/claude-sonnet-4-6` (per its agent frontmatter), appropriate for judgment-heavy review; it produced a thorough, correctly-structured PASS/WARN report.
75
+ The parent session switched models several times (`opus-4-8` → `sonnet-4-6` → `deepseek-v4-flash` → `opus-4-8`); execution stayed clean throughout, so no model-quality mismatch was observable.
76
+ - **Escalation-delay tracking** — no `rabbit-hole` friction; no error sequence exceeded one tool call.
77
+ - **Unused-tool detection** — nothing notable; the work was fully specified by the plan and needed no exploratory dispatch.
78
+ - **Feedback-loop gap analysis** — verification was incremental (typecheck + affected tests per edit, full gates at end of TDD, root `lint` + `fallow` before push); no end-only verification gap.
79
+
80
+ ### Changes made
81
+
82
+ 1. Rewrote the opening sentence of the `## Testing` section in `.pi/skills/package-pi-subagents/SKILL.md` to drop both the hardcoded test count and the fork framing.
83
+ The operator judged the precise count (`1037 tests across 64 files as of Phase 18 Step 5`) to provide no real value and to be structurally prone to per-step drift — it had gone stale across four issues ([#421]–[#424]) before the [#424] reviewer caught it.
84
+ The fork is already introduced at the top of the skill, so the Testing section need not re-reference upstream; the sentence now reads "The package has an extensive `vitest` suite."
85
+
86
+ [#421]: https://github.com/gotgenes/pi-packages/issues/421
87
+ [#422]: https://github.com/gotgenes/pi-packages/issues/422
88
+ [#423]: https://github.com/gotgenes/pi-packages/issues/423
89
+ [#424]: https://github.com/gotgenes/pi-packages/issues/424
@@ -0,0 +1,46 @@
1
+ ---
2
+ issue: 425
3
+ issue_title: "pi-subagents: reconcile the public SUBAGENT_EVENTS contract with emitted channels"
4
+ ---
5
+
6
+ # Retro: #425 — pi-subagents: reconcile the public SUBAGENT_EVENTS contract with emitted channels
7
+
8
+ ## Stage: Planning (2026-06-18T00:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned Phase 18 Step 6: reconcile the public `SUBAGENT_EVENTS` constant map in `src/service/service.ts` with the agent-lifecycle channels the core actually emits.
13
+ The plan removes the vacant `ACTIVITY` constant (breaking) and adds `FAILED`, `COMPACTED`, `CREATED`, and `STEERED`, then updates the architecture doc's lifecycle-events table.
14
+ Two TDD steps: a `feat!:` constant-map reconciliation pinned from both sides (declaration test + existing emission tests), and a `docs:` table update.
15
+
16
+ ### Observations
17
+
18
+ - This is the operator's own issue (author `gotgenes` matches the gh CLI user), so the proposed change was treated as the working hypothesis.
19
+ - Two genuine design choices were surfaced via `ask_user` and resolved: (1) remove `ACTIVITY` rather than emit a real broadcast — the activity tier was already deleted in Phase 18 Steps 1–3, so there is no streaming-progress source; (2) declare all four emitted agent-lifecycle channels including `subagents:steered` (from `steer-tool.ts`), not just the three named in the issue body, so declared == emitted is fully true for the lifecycle bus.
20
+ - Classified as **breaking**: removing a key from the exported `SUBAGENT_EVENTS` `as const` map breaks any consumer referencing `SUBAGENT_EVENTS.ACTIVITY` at the type level.
21
+ Plan uses `feat!:` with a `BREAKING CHANGE:` footer; the footer notes there is no replacement for `ACTIVITY`.
22
+ - Scope boundaries decided: config-domain events (`subagents:settings_loaded`/`settings_changed`) and the child-session seam events (`subagents:child:*`) stay out of `SUBAGENT_EVENTS` — separate domains with their own constant homes. `subagents:record` is an `appendEntry`, not a `pi.events.emit`, so it is not a channel constant.
23
+ - Corrected a stale doc artifact found during planning: the architecture lifecycle-events table listed `subagents:completed` as `{ id, type, status, result?, error? }`, but `buildEventData` emits `{ id, type, description, result, error, status, toolUses, durationMs, tokens? }`.
24
+ The plan fixes this in the same doc step.
25
+ - Public-surface gate: the plan requires running `verify:public-types` in the code step before committing, since `SUBAGENT_EVENTS` is rolled into `dist/public.d.ts`.
26
+ - Value-only reconciliation — no new collaborator, no dependency-wiring change — so the `design-review` checklist surfaces nothing actionable; noted in the plan rather than run as a separate gate.
27
+ - Next step: `/tdd-plan` (the change has a red→green test cycle).
28
+
29
+ ## Stage: Implementation — TDD (2026-06-18T00:00:00Z)
30
+
31
+ ### Session summary
32
+
33
+ Executed both planned TDD steps in two commits: a `feat!:` reconciling the `SUBAGENT_EVENTS` constant map (removed `ACTIVITY`, added `FAILED`/`COMPACTED`/`CREATED`/`STEERED`) pinned by an expanded `service.test.ts` assertion plus an explicit "no vacant `ACTIVITY`" check, then a `docs:` update to the lifecycle-events table and the Phase 18 Step 6 roadmap entry.
34
+ Full suite green at 1038 tests (+1 from the planning baseline of 1037); `check`, root `lint`, `verify:public-types`, and `fallow dead-code` all pass.
35
+
36
+ ### Observations
37
+
38
+ - No deviations from the plan's design.
39
+ The only mid-stream addition was the extra `"ACTIVITY" in SUBAGENT_EVENTS` falsity assertion, which strengthens the breaking-removal coverage beyond what the plan sketched.
40
+ - The breaking change went smoothly: `SUBAGENT_EVENTS.ACTIVITY` had no live consumers, so removal broke only the one service test that asserted it — folded into the same `feat!:` step as planned.
41
+ - Ran `verify:public-types` before committing Step 1 (public-surface gate); the rolled `dist/public.d.ts` regenerated cleanly with the narrowed `as const` literal types.
42
+ - `git diff` since the last tag (`pi-subagents-v16.6.0`) lists files from prior unreleased issues #422/#423/#424; scoped the pre-completion reviewer to #425's two commits to avoid noise.
43
+ - Pre-completion reviewer: WARN (1 non-blocking finding).
44
+ The Phase 18 Mermaid node `S6` was missing the `✅` mark carried by completed nodes S1–S5; fixed by appending `✅` to the node label and amended into the unpushed `docs:` commit.
45
+ All other checklist items PASS or SKIP (no acceptance-criteria section; `service.ts` was not a target of any prior Phase 18 step, so no cross-step invariant at risk).
46
+ - Next step: `/ship-issue`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "16.5.0",
3
+ "version": "17.0.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ import { ConcurrencyLimiter } from "#src/lifecycle/concurrency-limiter";
28
28
  import { createSubagentSession, type SubagentSessionDeps } from "#src/lifecycle/create-subagent-session";
29
29
  import { buildParentSnapshot } from "#src/lifecycle/parent-snapshot";
30
30
  import { SubagentManager } from "#src/lifecycle/subagent-manager";
31
+ import { CompositeSubagentObserver } from "#src/observation/composite-subagent-observer";
31
32
  import { type NotificationDetails, NotificationManager } from "#src/observation/notification";
32
33
  import { createNotificationRenderer } from "#src/observation/renderer";
33
34
  import { SubagentEventsObserver } from "#src/observation/subagent-events-observer";
@@ -62,7 +63,6 @@ export default function (pi: ExtensionAPI) {
62
63
  // widget dependency — keeping the construction graph a cycle-free DAG.
63
64
  const notifications = new NotificationManager(
64
65
  (msg, opts) => pi.sendMessage(msg, opts),
65
- runtime.agentActivity,
66
66
  );
67
67
 
68
68
  // Settings: owns all three in-memory values and handles load/save/emit.
@@ -76,12 +76,18 @@ export default function (pi: ExtensionAPI) {
76
76
  settings.load();
77
77
 
78
78
  // Observer: receives agent lifecycle notifications and dispatches events/notifications.
79
- const observer = new SubagentEventsObserver({
79
+ const eventsObserver = new SubagentEventsObserver({
80
80
  emit: (channel, data) => pi.events.emit(channel, data),
81
81
  appendEntry: (customType, data) => pi.appendEntry(customType, data),
82
82
  notifications,
83
83
  });
84
84
 
85
+ // Fan-out observer: lets the widget subscribe as a second lifecycle consumer
86
+ // while the manager keeps its single-observer contract. The widget is added
87
+ // after construction (it needs the manager); the manager consults the observer
88
+ // only at spawn time, so registering late is safe.
89
+ const observer = new CompositeSubagentObserver([eventsObserver]);
90
+
85
91
  const subagentSessionDeps: SubagentSessionDeps = {
86
92
  io: {
87
93
  detectEnv,
@@ -129,8 +135,9 @@ export default function (pi: ExtensionAPI) {
129
135
  pi.on("session_shutdown", () => lifecycle.handleSessionShutdown());
130
136
 
131
137
  // Live widget: constructed after the manager (it polls listAgents()) and
132
- // injected directly into its consumers no post-construction field write.
133
- const widget = new AgentWidget(manager, runtime.agentActivity, registry);
138
+ // registered as a lifecycle observer so it self-drives its update timer.
139
+ const widget = new AgentWidget(manager, registry);
140
+ observer.add(widget);
134
141
 
135
142
  // Grab UI context from first tool execution + clear lingering widget on new turn
136
143
  const toolStart = new ToolStartHandler(widget);
@@ -142,7 +149,7 @@ export default function (pi: ExtensionAPI) {
142
149
 
143
150
  // ---- Agent tool ----
144
151
 
145
- pi.registerTool(new AgentTool(manager, runtime, widget, settings, registry, getAgentDir()).toToolDefinition());
152
+ pi.registerTool(new AgentTool(manager, runtime, settings, registry, getAgentDir()).toToolDefinition());
146
153
 
147
154
  // ---- get_subagent_result tool ----
148
155
 
@@ -157,7 +164,6 @@ export default function (pi: ExtensionAPI) {
157
164
  const agentsMenu = new AgentsMenuHandler(
158
165
  manager,
159
166
  registry,
160
- runtime.agentActivity,
161
167
  settings,
162
168
  new FsAgentFileOps(),
163
169
  join(getAgentDir(), "agents"),
@@ -0,0 +1,49 @@
1
+ import { debugLog } from "#src/debug";
2
+ import type { SubagentManagerObserver } from "#src/lifecycle/subagent-manager";
3
+ import type { CompactionInfo, Subagent } from "#src/types";
4
+
5
+ /**
6
+ * Fans out SubagentManager lifecycle notifications to multiple observers.
7
+ *
8
+ * Lets the manager keep its single-observer contract while several independent
9
+ * consumers (event/notification dispatch, the reactive widget) subscribe.
10
+ * Each delegate is isolated: a throw in one does not suppress the others.
11
+ */
12
+ export class CompositeSubagentObserver implements SubagentManagerObserver {
13
+ private readonly delegates: SubagentManagerObserver[];
14
+
15
+ constructor(delegates: SubagentManagerObserver[]) {
16
+ this.delegates = [...delegates];
17
+ }
18
+
19
+ /** Register an additional observer (breaks the widget↔manager construction cycle). */
20
+ add(observer: SubagentManagerObserver): void {
21
+ this.delegates.push(observer);
22
+ }
23
+
24
+ onSubagentStarted(record: Subagent): void {
25
+ this.dispatch((o) => o.onSubagentStarted(record), "onSubagentStarted");
26
+ }
27
+
28
+ onSubagentCreated(record: Subagent): void {
29
+ this.dispatch((o) => o.onSubagentCreated(record), "onSubagentCreated");
30
+ }
31
+
32
+ onSubagentCompleted(record: Subagent): void {
33
+ this.dispatch((o) => o.onSubagentCompleted(record), "onSubagentCompleted");
34
+ }
35
+
36
+ onSubagentCompacted(record: Subagent, info: CompactionInfo): void {
37
+ this.dispatch((o) => o.onSubagentCompacted(record, info), "onSubagentCompacted");
38
+ }
39
+
40
+ private dispatch(call: (o: SubagentManagerObserver) => void, label: string): void {
41
+ for (const o of this.delegates) {
42
+ try {
43
+ call(o);
44
+ } catch (err) {
45
+ debugLog(`CompositeSubagentObserver.${label}`, err);
46
+ }
47
+ }
48
+ }
49
+ }
@@ -1,7 +1,6 @@
1
1
  import { debugLog } from "#src/debug";
2
2
  import { getLifetimeTotal } from "#src/lifecycle/usage";
3
3
  import type { Subagent } from "#src/types";
4
- import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
5
4
 
6
5
  /** Details attached to custom notification messages for visual rendering. */
7
6
  export interface NotificationDetails {
@@ -77,7 +76,6 @@ export function formatTaskNotification(record: Subagent, resultMaxLen: number):
77
76
  export function buildNotificationDetails(
78
77
  record: Subagent,
79
78
  resultMaxLen: number,
80
- activity?: AgentActivityTracker,
81
79
  ): NotificationDetails {
82
80
  const totalTokens = getLifetimeTotal(record.lifetimeUsage);
83
81
 
@@ -86,8 +84,8 @@ export function buildNotificationDetails(
86
84
  description: record.description,
87
85
  status: record.status,
88
86
  toolUses: record.toolUses,
89
- turnCount: activity?.turnCount ?? 0,
90
- maxTurns: activity?.maxTurns,
87
+ turnCount: record.turnCount,
88
+ maxTurns: record.maxTurns,
91
89
  totalTokens,
92
90
  durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
93
91
  outputFile: record.outputFile,
@@ -127,7 +125,6 @@ export function buildEventData(record: Subagent) {
127
125
  export interface NotificationSystem {
128
126
  cancelNudge: (key: string) => void;
129
127
  sendCompletion: (record: Subagent) => void;
130
- cleanupCompleted: (id: string) => void;
131
128
  dispose: () => void;
132
129
  }
133
130
 
@@ -141,7 +138,6 @@ export class NotificationManager implements NotificationSystem {
141
138
  msg: { customType: string; content: string; display: boolean; details?: unknown },
142
139
  opts?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
143
140
  ) => void,
144
- private agentActivity: Map<string, AgentActivityTracker>,
145
141
  ) {}
146
142
 
147
143
  cancelNudge(key: string): void {
@@ -153,14 +149,9 @@ export class NotificationManager implements NotificationSystem {
153
149
  }
154
150
 
155
151
  sendCompletion(record: Subagent): void {
156
- this.agentActivity.delete(record.id);
157
152
  this.scheduleNudge(record.id, () => this.emitIndividualNudge(record));
158
153
  }
159
154
 
160
- cleanupCompleted(id: string): void {
161
- this.agentActivity.delete(id);
162
- }
163
-
164
155
  dispose(): void {
165
156
  for (const timer of this.pendingNudges.values()) clearTimeout(timer);
166
157
  this.pendingNudges.clear();
@@ -193,7 +184,7 @@ export class NotificationManager implements NotificationSystem {
193
184
  customType: "subagent-notification",
194
185
  content: notification + footer,
195
186
  display: true,
196
- details: buildNotificationDetails(record, 500, this.agentActivity.get(record.id)),
187
+ details: buildNotificationDetails(record, 500),
197
188
  },
198
189
  { deliverAs: "followUp", triggerTurn: true },
199
190
  );
@@ -65,10 +65,7 @@ export class SubagentEventsObserver implements SubagentManagerObserver {
65
65
  });
66
66
 
67
67
  // Skip notification if result was already consumed via get_subagent_result.
68
- if (record.notification?.resultConsumed) {
69
- this.notifications.cleanupCompleted(record.id);
70
- return;
71
- }
68
+ if (record.notification?.resultConsumed) return;
72
69
 
73
70
  this.notifications.sendCompletion(record);
74
71
  }
package/src/runtime.ts CHANGED
@@ -9,7 +9,6 @@
9
9
  import { buildParentSnapshot, type ParentSnapshot } from "#src/lifecycle/parent-snapshot";
10
10
  import type { ModelInfo } from "#src/tools/spawn-config";
11
11
  import type { SessionContext } from "#src/types";
12
- import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
13
12
 
14
13
  /**
15
14
  * Narrow config subset read by Agent when driving the turn loop (defaultMaxTurns, graceTurns).
@@ -30,11 +29,6 @@ export class SubagentRuntime {
30
29
  // ── Session state (was closure-scoped in index.ts) ───────────────────────
31
30
  /** Active Pi session context — set on session_start, cleared on session_shutdown. */
32
31
  currentCtx: SessionContext | undefined = undefined;
33
- /**
34
- * Per-agent live activity state shared across the notification system,
35
- * widget, and tool handlers. The Map itself is never replaced.
36
- */
37
- readonly agentActivity: Map<string, AgentActivityTracker> = new Map();
38
32
 
39
33
  // ── Session-context methods ──────────────────────────────────────────────
40
34
 
@@ -97,7 +97,10 @@ export interface SubagentsService {
97
97
  export const SUBAGENT_EVENTS = {
98
98
  STARTED: "subagents:started",
99
99
  COMPLETED: "subagents:completed",
100
- ACTIVITY: "subagents:activity",
100
+ FAILED: "subagents:failed",
101
+ COMPACTED: "subagents:compacted",
102
+ CREATED: "subagents:created",
103
+ STEERED: "subagents:steered",
101
104
  } as const;
102
105
 
103
106
  // ---- Accessor functions ----
@@ -12,22 +12,8 @@ import { buildDetails, buildTypeListText, textResult } from "#src/tools/helpers"
12
12
  import { renderAgentResult } from "#src/tools/result-renderer";
13
13
  import { type ModelInfo, resolveSpawnConfig } from "#src/tools/spawn-config";
14
14
  import type { ParentSessionInfo, Subagent } from "#src/types";
15
- import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
16
- import { type UICtx } from "#src/ui/agent-widget";
17
15
  import { type AgentDetails, getDisplayName } from "#src/ui/display";
18
16
 
19
- // ---- Shared interfaces (also used by background-spawner and foreground-runner) ----
20
-
21
- /**
22
- * Narrow read/write interface for the agent-tool's agentActivity access.
23
- * The full Map satisfies this structurally — no wrapper needed.
24
- */
25
- export interface AgentActivityAccess {
26
- get(id: string): AgentActivityTracker | undefined;
27
- set(id: string, tracker: AgentActivityTracker): void;
28
- delete(id: string): void;
29
- }
30
-
31
17
  // ---- Deps interfaces ----
32
18
 
33
19
  /** Narrow manager interface — only the methods the Agent tool calls. */
@@ -40,24 +26,11 @@ export interface AgentToolManager {
40
26
 
41
27
  /** Narrow runtime interface — the Agent tool's slice of SubagentRuntime. */
42
28
  export interface AgentToolRuntime {
43
- readonly agentActivity: AgentActivityAccess;
44
29
  buildSnapshot(inheritContext: boolean): ParentSnapshot;
45
30
  getModelInfo(): ModelInfo;
46
31
  getSessionInfo(): { parentSessionFile: string; parentSessionId: string };
47
32
  }
48
33
 
49
- /**
50
- * Narrow widget interface the Agent tool drives directly.
51
- * Superset of the runner/spawner widget deps plus `setUICtx`.
52
- * AgentWidget satisfies it structurally.
53
- */
54
- export interface AgentToolWidget {
55
- setUICtx(ctx: UICtx): void;
56
- ensureTimer(): void;
57
- update(): void;
58
- markFinished(id: string): void;
59
- }
60
-
61
34
  /** Narrow settings accessor — only the fields the Agent tool reads. */
62
35
  export type AgentToolSettings = {
63
36
  readonly defaultMaxTurns: number | undefined;
@@ -73,7 +46,6 @@ export class AgentTool {
73
46
  constructor(
74
47
  private readonly manager: AgentToolManager,
75
48
  private readonly runtime: AgentToolRuntime,
76
- private readonly widget: AgentToolWidget,
77
49
  private readonly settings: AgentToolSettings,
78
50
  private readonly registry: AgentTypeRegistry,
79
51
  private readonly agentDir: string,
@@ -87,11 +59,8 @@ export class AgentTool {
87
59
  params: Record<string, unknown>,
88
60
  signal: AbortSignal | undefined,
89
61
  onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
90
- ctx: any,
62
+ _ctx: any,
91
63
  ) {
92
- // Ensure we have UI context for widget rendering
93
- this.widget.setUICtx(ctx.ui as UICtx);
94
-
95
64
  // Reload custom agents so new .pi/agents/*.md files are picked up without restart
96
65
  this.registry.reload();
97
66
 
@@ -140,8 +109,6 @@ export class AgentTool {
140
109
  if (config.execution.runInBackground) {
141
110
  return spawnBackground(
142
111
  this.manager,
143
- this.widget,
144
- this.runtime.agentActivity,
145
112
  { config, snapshot, parentSession, settings: this.settings },
146
113
  );
147
114
  }
@@ -149,8 +116,6 @@ export class AgentTool {
149
116
  // ---- Foreground execution — stream progress via onUpdate ----
150
117
  return runForeground(
151
118
  this.manager,
152
- this.widget,
153
- this.runtime.agentActivity,
154
119
  { config, snapshot, parentSession },
155
120
  signal,
156
121
  onUpdate,
@@ -1,11 +1,8 @@
1
1
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
2
2
  import type { AgentSpawnConfig } from "#src/lifecycle/subagent-manager";
3
- import type { AgentActivityAccess } from "#src/tools/agent-tool";
4
3
  import { textResult } from "#src/tools/helpers";
5
4
  import type { ResolvedSpawnConfig } from "#src/tools/spawn-config";
6
5
  import type { ParentSessionInfo, Subagent } from "#src/types";
7
- import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
8
- import { subscribeUIObserver } from "#src/ui/ui-observer";
9
6
 
10
7
  /** Narrow manager interface for the background spawner. */
11
8
  export interface BackgroundManagerDeps {
@@ -13,12 +10,6 @@ export interface BackgroundManagerDeps {
13
10
  getRecord(id: string): Subagent | undefined;
14
11
  }
15
12
 
16
- /** Narrow widget interface for the background spawner. */
17
- export interface BackgroundWidgetDeps {
18
- ensureTimer(): void;
19
- update(): void;
20
- }
21
-
22
13
  /** All values the background spawner needs beyond the resolved config. */
23
14
  export interface BackgroundParams {
24
15
  config: ResolvedSpawnConfig;
@@ -29,17 +20,13 @@ export interface BackgroundParams {
29
20
 
30
21
  /**
31
22
  * Spawn a background agent and return the tool result immediately.
32
- * Owns: activity tracker creation, UI observer subscription, activity map
33
- * registration, widget update, and launch message formatting.
23
+ * Owns: launch message formatting.
34
24
  */
35
25
  export function spawnBackground(
36
26
  manager: BackgroundManagerDeps,
37
- widget: BackgroundWidgetDeps,
38
- agentActivity: AgentActivityAccess,
39
27
  params: BackgroundParams,
40
28
  ) {
41
29
  const { identity, execution, presentation } = params.config;
42
- const bgState = new AgentActivityTracker(execution.effectiveMaxTurns);
43
30
 
44
31
  let id: string;
45
32
  try {
@@ -52,13 +39,6 @@ export function spawnBackground(
52
39
  thinkingLevel: execution.thinking,
53
40
  isBackground: true,
54
41
  invocation: execution.agentInvocation,
55
- observer: {
56
- onSessionCreated: (agent) => {
57
- const sub = agent.subagentSession!;
58
- bgState.setSession(sub);
59
- subscribeUIObserver(sub, bgState);
60
- },
61
- },
62
42
  });
63
43
  } catch (err) {
64
44
  return textResult(err instanceof Error ? err.message : String(err));
@@ -66,10 +46,6 @@ export function spawnBackground(
66
46
 
67
47
  const record = manager.getRecord(id);
68
48
 
69
- agentActivity.set(id, bgState);
70
- widget.ensureTimer();
71
- widget.update();
72
-
73
49
  const isQueued = record?.status === "queued";
74
50
  return textResult(
75
51
  `Agent ${isQueued ? "queued" : "started"} in background.\n` +