@gotgenes/pi-subagents 7.2.4 → 7.2.6

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [7.2.6](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.2.5...pi-subagents-v7.2.6) (2026-05-25)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * archive Phase 11 to history, add Phase 11 to refactoring table ([2c617f2](https://github.com/gotgenes/pi-packages/commit/2c617f2c752ea935d55878ba58fce997985086b5))
14
+ * plan decompose renderWidgetLines ([#205](https://github.com/gotgenes/pi-packages/issues/205)) ([88d09cd](https://github.com/gotgenes/pi-packages/commit/88d09cdad53bc3f215c069b8d6da6a44e10b5af7))
15
+ * **retro:** add planning stage notes for issue [#205](https://github.com/gotgenes/pi-packages/issues/205) ([14afc1f](https://github.com/gotgenes/pi-packages/commit/14afc1ff82d61828fdac9373f31cb68ebfc1a2e7))
16
+ * **retro:** add retro notes for issue [#196](https://github.com/gotgenes/pi-packages/issues/196) ([cfc7d94](https://github.com/gotgenes/pi-packages/commit/cfc7d94f72b120a4550f73e2d1cf00822db759c2))
17
+ * **retro:** add TDD stage notes for issue [#205](https://github.com/gotgenes/pi-packages/issues/205) ([a676078](https://github.com/gotgenes/pi-packages/commit/a6760789898435e5b552941124de6f32be21407e))
18
+
19
+ ## [7.2.5](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.2.4...pi-subagents-v7.2.5) (2026-05-25)
20
+
21
+
22
+ ### Documentation
23
+
24
+ * mark Phase 11 Layer 3 and Layer 4 complete ([bf71795](https://github.com/gotgenes/pi-packages/commit/bf71795649a5b3c14a2b3ff16b2109d131a0ed32))
25
+ * plan convert AgentRunner and AgentsMenuHandler to classes ([#196](https://github.com/gotgenes/pi-packages/issues/196)) ([cd0bd1f](https://github.com/gotgenes/pi-packages/commit/cd0bd1fdec0c87655bdb38f8243084df807b676a))
26
+ * **retro:** add planning stage notes for issue [#196](https://github.com/gotgenes/pi-packages/issues/196) ([677d4bf](https://github.com/gotgenes/pi-packages/commit/677d4bf6619f13eba8d17181efab04cc67e47bbd))
27
+ * **retro:** add TDD stage notes for issue [#196](https://github.com/gotgenes/pi-packages/issues/196) ([72d24ba](https://github.com/gotgenes/pi-packages/commit/72d24ba56b8dc7668ff350ad3f0ba027b996d26e))
28
+
8
29
  ## [7.2.4](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.2.3...pi-subagents-v7.2.4) (2026-05-25)
9
30
 
10
31
 
@@ -599,154 +599,18 @@ export type RunnerIO = EnvironmentIO & SessionFactoryIO;
599
599
  `RunnerIO` is kept as a type alias for the intersection.
600
600
  All existing consumers satisfy both sub-interfaces via structural typing with no call-site changes.
601
601
 
602
- ## Improvement roadmap (Phase 11)
602
+ ## Phase 11 (complete)
603
603
 
604
- Phase 11 converts closure factories to classes, eliminating the adapter closure density in `index.ts` (the package's #1 churn hotspot: 128 commits, accelerating, fan-out 25).
605
- The approach is layered: each step makes the next step trivial.
606
-
607
- > "Make the change that makes the change easy." —Kent Beck
608
-
609
- ### Findings
610
-
611
- | Metric | Value |
612
- | ------------------------- | ------------------------------------------ |
613
- | Health score | 75/100 (B) |
614
- | #1 hotspot | `index.ts` (128 commits, accelerating) |
615
- | Dead exports | 0 (down from 1; Layer 2 removed re-export) |
616
- | Production duplication | 0 |
617
- | Test duplication | 1,396 lines (69 clone groups, 22 files) |
618
- | `as any` casts in index | 1 (down from 5; Layer 1 resolved 4) |
619
- | Adapter closures in index | 40 (down from 44; Layers 1–2 resolved 4) |
620
- | Index fan-out | 25 imports |
621
-
622
- ### Root cause
623
-
624
- The 44 adapter closures in `index.ts` exist because the tool factories accept narrow interfaces that don't structurally match the real objects.
625
- The real objects can't satisfy the interfaces because:
626
-
627
- 1. ~~`SubagentRuntime.currentCtx` is typed `{ pi: unknown; ctx: unknown }` — so every consumer must `as any` cast to read fields.~~
628
- Resolved by Layer 0 (#192) + Layer 1 (#193).
629
- 2. ~~Context queries (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) live as closures in index.ts instead of methods on the state holder.~~
630
- Resolved by Layer 1 (#193).
631
- 3. ~~`AgentToolManager` mixes fields from `AgentManager` and `SettingsManager` (source mismatch).~~
632
- Resolved by Layer 2 (#194).
633
- 4. ~~`AgentToolWidget` uses different method names than `SubagentRuntime` (name mismatch).~~
634
- Resolved by Layer 2 (#194).
635
-
636
- Fix these structural misalignments and the class conversions become mechanical.
637
-
638
- ### Layer 0: Define `SessionContext` narrow interface ([#192][192])
639
-
640
- `SubagentRuntime` currently types its context as `unknown` to avoid SDK coupling.
641
- But `ExtensionContext` is exported by the SDK — the `unknown` is a historical choice, not a constraint.
642
- Define a narrow `SessionContext` interface capturing the 5 fields runtime actually needs:
643
-
644
- ```typescript
645
- export interface SessionContext {
646
- readonly cwd: string;
647
- readonly model: unknown;
648
- readonly modelRegistry: ModelRegistry | undefined;
649
- getSystemPrompt(): string;
650
- readonly sessionManager: {
651
- getSessionFile(): string | undefined;
652
- getSessionId(): string;
653
- getBranch(): unknown[];
654
- };
655
- }
656
- ```
657
-
658
- - Target: `src/types.ts`
659
- - Smell: Category C (platform type threading)
660
- - Outcome: typed foundation for Layers 1–4; no `as any` needed by consumers of `SubagentRuntime`
661
-
662
- ### Layer 1: `SubagentRuntime` stores typed context, owns its queries ([#193][193]) ✓ done
663
-
664
- Change `currentCtx` from `{ pi: unknown; ctx: unknown }` to `SessionContext | undefined`.
665
- The single `as SessionContext` cast moves into `handleSessionStart` — the boundary where the SDK hands us the value.
666
- Add typed methods: `buildSnapshot(inheritContext)`, `getModelInfo()`, `getSessionInfo()`.
667
-
668
- - Target: `src/runtime.ts`, `src/handlers/lifecycle.ts`, `src/service/service-adapter.ts`, `src/index.ts`
669
- - Smell: Category C (closure queries on mutable field → methods on state owner)
670
- - Outcome: 3 closure queries in index.ts → 0; `SubagentRuntime` is self-sufficient for tool deps
671
- - Enables: Layer 3 (tools accept `SubagentRuntime` directly)
672
-
673
- ### Layer 2: Align interfaces so real objects satisfy tool deps structurally ([#194][194]) ✓ done
674
-
675
- Three alignment changes:
676
-
677
- 1. **Move `getMaxConcurrent` off `AgentToolManager`** — it reads from `SettingsManager`, not `AgentManager`.
678
- The tool already receives `settings`; read it from there.
679
- 2. **Rename widget methods** — align `SubagentRuntime` method names with `AgentToolWidget` (either rename `updateWidget()` → `update()` on runtime, or rename the interface to match).
680
- 3. **Remove dead re-export** — `getToolCallName` in `ui/message-formatters.ts` (fallow finding).
681
-
682
- After this step, `AgentManager` structurally satisfies `AgentToolManager` and `SubagentRuntime` structurally satisfies `AgentToolWidget`.
683
-
684
- - Target: `src/tools/agent-tool.ts` (interface), `src/runtime.ts` (method names), `src/ui/message-formatters.ts`
685
- - Smell: Category C (source mismatch, name mismatch) + Category A (dead export)
686
- - Outcome: structural typing connects real objects to tool interfaces without adapters; 0 dead exports (fallow clean)
687
- - Enables: Layer 3 (class constructors accept real objects directly)
688
-
689
- ### Layer 3: Convert closure factories to classes ([#195][195], [#196][196])
690
-
691
- With Layers 0–2 complete, each factory is a mechanical conversion. ✓ Tool factories converted in [#195][195]:
692
-
693
- | Factory | Class | Constructor params |
694
- | -------------------------------- | ------------------------------ | --------------------------------------------------- |
695
- | `createAgentTool({...})` | `AgentTool` ✓ | `manager`, `runtime`, `settings`, `registry` |
696
- | `createGetResultTool(...)` | `GetResultTool` ✓ | `manager`, `notifications`, `registry` |
697
- | `createSteerTool(...)` | `SteerTool` ✓ | `manager`, `events` |
698
- | `createAgentRunner(runnerIO)` | `AgentRunner` (concrete class) | `io: RunnerIO` |
699
- | `createAgentsMenuHandler({...})` | `AgentsMenuHandler` | `manager`, `registry`, `settings`, `fileOps`, paths |
700
-
701
- Each class satisfies the existing interface via structural typing.
702
- The `defineTool()` wrapper moves into a `toToolDefinition()` method on each tool class.
703
-
704
- - Target: `src/tools/*.ts`, `src/lifecycle/agent-runner.ts`, `src/ui/agent-menu.ts`
705
- - Smell: Category C (closure factories masquerading as classes)
706
- - Outcome: deps are constructor params (inspectable, testable); no captured closures
707
- - Enables: Layer 4 (index.ts simplification)
708
-
709
- ### Layer 4: Simplify index.ts (included in [#196][196])
710
-
711
- With real objects satisfying tool interfaces and queries living on `SubagentRuntime`, the composition root becomes pure construction:
712
-
713
- ```typescript
714
- const runtime = new SubagentRuntime();
715
- const settings = new SettingsManager(...);
716
- const manager = new AgentManager(...);
717
- const agentTool = new AgentTool(manager, runtime, settings, registry);
718
- pi.registerTool(agentTool.toToolDefinition());
719
- ```
720
-
721
- No adapter closures.
722
- No `as any`.
723
- Fan-out drops from 25 to ~15 (internal factories eliminated).
724
-
725
- - Target: `src/index.ts`
726
- - Smell: Category B (god file) + Category C (adapter closure density)
727
- - Outcome: index.ts shrinks from 280 to ~150 lines; churn hotspot stabilizes
728
-
729
- ### Step dependencies
730
-
731
- ```mermaid
732
- flowchart LR
733
- L0["Layer 0: SessionContext interface"] --> L1["Layer 1: Runtime owns queries"]
734
- L1 --> L3["Layer 3: Classes replace factories"]
735
- L2["Layer 2: Align interfaces"] --> L3
736
- L3 --> L4["Layer 4: Simplify index.ts"]
737
- ```
738
-
739
- Layers 0 and 2 are independent of each other.
740
- Layer 1 depends on Layer 0.
741
- Layer 3 depends on both Layer 1 and Layer 2.
742
- Layer 4 depends on Layer 3.
604
+ Phase 11 converted all closure factories to classes, eliminating adapter closure density in `index.ts`.
605
+ Four layers: SessionContext typing runtime query methods interface alignment → class conversions → index.ts simplification.
606
+ See [phase-11-closure-to-class.md](history/phase-11-closure-to-class.md) for details.
743
607
 
744
608
  ## Improvement roadmap (Phase 12)
745
609
 
746
610
  Phase 12 addresses the remaining fallow refactoring targets and test duplication.
747
611
  These are independent of Phase 11 and can proceed in parallel if desired.
748
612
 
749
- ### Step 1: Decompose `renderWidgetLines` (cognitive 44)
613
+ ### Step 1: Decompose `renderWidgetLines` (cognitive 44) — [#205]
750
614
 
751
615
  `renderWidgetLines` in `ui/widget-renderer.ts` handles agent-status formatting, tree connectors, overflow, and empty states.
752
616
  Extract per-status renderers and a tree-connector utility.
@@ -754,7 +618,7 @@ Extract per-status renderers and a tree-connector utility.
754
618
  - Target: `src/ui/widget-renderer.ts`
755
619
  - Outcome: cognitive complexity < 10
756
620
 
757
- ### Step 2: Decompose `showAgentDetail` (cognitive 33)
621
+ ### Step 2: Decompose `showAgentDetail` (cognitive 33) — [#206]
758
622
 
759
623
  `showAgentDetail` in `ui/agent-config-editor.ts` handles display, edit, eject, and delete flows.
760
624
  Extract sub-functions per menu action.
@@ -762,7 +626,7 @@ Extract sub-functions per menu action.
762
626
  - Target: `src/ui/agent-config-editor.ts`
763
627
  - Outcome: cognitive complexity < 10
764
628
 
765
- ### Step 3: Decompose `update` in `agent-widget.ts` (cognitive 31)
629
+ ### Step 3: Decompose `update` in `agent-widget.ts` (cognitive 31) — [#207]
766
630
 
767
631
  `update` mixes timer lifecycle, agent list assembly, render delegation, and visibility state.
768
632
  Extract `assembleWidgetState` (pure) and timer management.
@@ -770,7 +634,7 @@ Extract `assembleWidgetState` (pure) and timer management.
770
634
  - Target: `src/ui/agent-widget.ts`
771
635
  - Outcome: cognitive complexity < 10
772
636
 
773
- ### Step 4: Extract shared test fixtures
637
+ ### Step 4: Extract shared test fixtures — [#208]
774
638
 
775
639
  The 3 heaviest clone families:
776
640
 
@@ -785,7 +649,7 @@ Extract shared factories into `test/fixtures/` modules.
785
649
 
786
650
  ## Refactoring history
787
651
 
788
- Phases 1–5 and 7–10 are complete.
652
+ Phases 1–5 and 7–11 are complete.
789
653
  Phase 6 (UI extraction to a separate package) is deferred.
790
654
  Detailed records are preserved in per-phase history files:
791
655
 
@@ -801,6 +665,7 @@ Detailed records are preserved in per-phase history files:
801
665
  | 8 | Testability, display extraction, menu decomposition | Complete | [phase-8-testability.md](history/phase-8-testability.md) |
802
666
  | 9 | Observation consolidation, ctx elimination | Complete | [phase-9-observation-ctx.md](history/phase-9-observation-ctx.md) |
803
667
  | 10 | Domain organization, bag decomposition, complexity | Complete | [phase-10-structural-decomposition.md](history/phase-10-structural-decomposition.md) |
668
+ | 11 | Closure factories to classes | Complete | [phase-11-closure-to-class.md](history/phase-11-closure-to-class.md) |
804
669
 
805
670
  ### Structural refactoring issues
806
671
 
@@ -815,6 +680,8 @@ Detailed records are preserved in per-phase history files:
815
680
  | Testability | #131, #132, #133, #134, #135, #136 | Shared fixtures, session-config IO, runner SDK boundary, as-any reduction, display extraction, menu decomposition |
816
681
  | Observation/ctx | #144, #145, #146, #147, #148 | Observation consolidation, execute decomposition, UI context, text wrapping injection, widget rendering split |
817
682
  | Phase 10 | #164, #165, #166, #167, #168, #169, #170, #171, #172 | Domain directories, ResolvedSpawnConfig, ParentSessionInfo, RunnerIO split, ToolFilterConfig, RunContext, buildContentLines, renderResult, content-items |
683
+ | Phase 11 | #192, #193, #194, #195, #196 | SessionContext, runtime queries, interface alignment, tool classes, runner/menu classes, index.ts simplification |
684
+ | Phase 12 | #205, #206, #207, #208 | renderWidgetLines, showAgentDetail, widget update, shared test fixtures |
818
685
 
819
686
  The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
820
687
 
@@ -838,8 +705,7 @@ The upstream test suite is run periodically as a regression canary for the agent
838
705
  [167]: https://github.com/gotgenes/pi-packages/issues/167
839
706
  [168]: https://github.com/gotgenes/pi-packages/issues/168
840
707
  [169]: https://github.com/gotgenes/pi-packages/issues/169
841
- [192]: https://github.com/gotgenes/pi-packages/issues/192
842
- [193]: https://github.com/gotgenes/pi-packages/issues/193
843
- [194]: https://github.com/gotgenes/pi-packages/issues/194
844
- [195]: https://github.com/gotgenes/pi-packages/issues/195
845
- [196]: https://github.com/gotgenes/pi-packages/issues/196
708
+ [#205]: https://github.com/gotgenes/pi-packages/issues/205
709
+ [#206]: https://github.com/gotgenes/pi-packages/issues/206
710
+ [#207]: https://github.com/gotgenes/pi-packages/issues/207
711
+ [#208]: https://github.com/gotgenes/pi-packages/issues/208
@@ -0,0 +1,100 @@
1
+ # Phase 11: Closure factories to classes
2
+
3
+ Target: convert all closure factories to classes, eliminating adapter closure density in `index.ts` (the package's #1 churn hotspot) and making the composition root a pure construction site.
4
+
5
+ ## Root cause
6
+
7
+ The 44 adapter closures in `index.ts` existed because tool factories accepted narrow interfaces that didn't structurally match the real objects.
8
+ Four structural misalignments prevented direct wiring:
9
+
10
+ 1. `SubagentRuntime.currentCtx` was typed `{ pi: unknown; ctx: unknown }` — every consumer had to `as any` cast.
11
+ 2. Context queries (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) lived as closures in `index.ts` instead of methods on the state holder.
12
+ 3. `AgentToolManager` mixed fields from `AgentManager` and `SettingsManager` (source mismatch).
13
+ 4. `AgentToolWidget` used different method names than `SubagentRuntime` (name mismatch).
14
+
15
+ ## Layer 0: Define SessionContext narrow interface (#192)
16
+
17
+ Defined a `SessionContext` interface capturing the 5 fields `SubagentRuntime` actually needs.
18
+ Replaced the `{ pi: unknown; ctx: unknown }` typing with `SessionContext | undefined`.
19
+
20
+ Impact: typed foundation for all subsequent layers; no `as any` needed by consumers.
21
+
22
+ ## Layer 1: SubagentRuntime stores typed context, owns its queries (#193)
23
+
24
+ Changed `currentCtx` to `SessionContext | undefined`.
25
+ The single `as SessionContext` cast moved into `handleSessionStart` — the SDK boundary.
26
+ Added typed methods: `buildSnapshot(inheritContext)`, `getModelInfo()`, `getSessionInfo()`.
27
+
28
+ Impact: 3 closure queries in `index.ts` → 0; 4 `as any` casts eliminated; `SubagentRuntime` is self-sufficient for tool deps.
29
+
30
+ ## Layer 2: Align interfaces for structural typing (#194)
31
+
32
+ Three alignment changes:
33
+
34
+ 1. Moved `getMaxConcurrent` off `AgentToolManager` — it reads from `SettingsManager`, not `AgentManager`.
35
+ 2. Renamed widget methods — aligned `SubagentRuntime` method names with `AgentToolWidget`.
36
+ 3. Removed dead re-export `getToolCallName` in `ui/message-formatters.ts`.
37
+
38
+ Impact: `AgentManager` structurally satisfies `AgentToolManager`; `SubagentRuntime` structurally satisfies `AgentToolWidget`; 0 dead exports.
39
+
40
+ ## Layer 3: Convert closure factories to classes (#195, #196)
41
+
42
+ All five closure factories converted to classes:
43
+
44
+ | Factory | Class | Issue |
45
+ | -------------------------------- | --------------------- | ----- |
46
+ | `createAgentTool({...})` | `AgentTool` | #195 |
47
+ | `createGetResultTool(...)` | `GetResultTool` | #195 |
48
+ | `createSteerTool(...)` | `SteerTool` | #195 |
49
+ | `createAgentRunner(runnerIO)` | `ConcreteAgentRunner` | #196 |
50
+ | `createAgentsMenuHandler({...})` | `AgentsMenuHandler` | #196 |
51
+
52
+ Each class satisfies the existing interface via structural typing.
53
+ Tool classes expose `toToolDefinition()` for Pi tool registration.
54
+ `getModelLabel` internalized into `AgentsMenuHandler` (was a 7-line closure in `index.ts`).
55
+
56
+ Impact: deps are constructor params (inspectable, testable); no captured closures.
57
+
58
+ ## Layer 4: Simplify index.ts (#196)
59
+
60
+ With all factories converted and `AgentManager` satisfying `AgentMenuManager` structurally:
61
+
62
+ - 4 adapter closures eliminated (3 manager method adapters + `getModelLabel`).
63
+ - 4 unused imports removed.
64
+ - Remaining ~15 closures are structural (event registrations, SDK factory callbacks).
65
+
66
+ Impact: adapter closure count halved; `AgentManager` passed directly without wrappers; churn hotspot stabilized.
67
+
68
+ ## Step dependencies
69
+
70
+ ```mermaid
71
+ flowchart LR
72
+ L0["Layer 0: SessionContext #192"] --> L1["Layer 1: Runtime queries #193"]
73
+ L1 --> L3["Layer 3: Classes #195 #196"]
74
+ L2["Layer 2: Align interfaces #194"] --> L3
75
+ L3 --> L4["Layer 4: Simplify index.ts #196"]
76
+ ```
77
+
78
+ Layers 0 and 2 were independent.
79
+ Layer 1 depended on Layer 0.
80
+ Layer 3 depended on both Layer 1 and Layer 2.
81
+ Layer 4 depended on Layer 3.
82
+
83
+ ## Impact
84
+
85
+ | Metric | Before | After |
86
+ | ------------------------------ | ---------- | --------------------- |
87
+ | Health score | 75/100 (B) | 76/100 (B) |
88
+ | Adapter closures in `index.ts` | 44 | ~15 (structural only) |
89
+ | `as any` casts in `index.ts` | 5 | 1 (SDK boundary) |
90
+ | Dead exports | 1 | 0 |
91
+ | Closure factories | 5 | 0 |
92
+ | `index.ts` fan-out | 25 imports | 21 imports |
93
+
94
+ ## Related issues
95
+
96
+ - #192 — Define SessionContext narrow interface
97
+ - #193 — Runtime owns context queries
98
+ - #194 — Align tool interfaces for structural typing
99
+ - #195 — Convert tool factories to classes
100
+ - #196 — Convert AgentRunner and AgentsMenuHandler to classes, simplify index.ts
@@ -0,0 +1,268 @@
1
+ ---
2
+ issue: 196
3
+ issue_title: "Convert AgentRunner and AgentsMenuHandler to classes, simplify index.ts"
4
+ ---
5
+
6
+ # Convert AgentRunner and AgentsMenuHandler to classes, simplify index.ts
7
+
8
+ ## Problem Statement
9
+
10
+ Two remaining closure factories in pi-subagents mask class-shaped code:
11
+
12
+ 1. `createAgentRunner(runnerIO)` captures `RunnerIO` and returns `{ run, resume }` — the `AgentRunner` interface already exists but lacks a concrete class implementation.
13
+ 2. `createAgentsMenuHandler({...})` captures an 8-field deps object and returns a handler function.
14
+
15
+ With #195 (tool factory → class conversions) complete, these are the last two closure factories.
16
+ After converting them, `index.ts` can be simplified: adapter closures drop, fan-out decreases, and the composition root becomes pure object construction.
17
+
18
+ ## Goals
19
+
20
+ - Convert `createAgentRunner` factory to a concrete `ConcreteAgentRunner` class implementing `AgentRunner`.
21
+ - Convert `createAgentsMenuHandler` factory to an `AgentsMenuHandler` class.
22
+ - Internalize the `getModelLabel` closure from `index.ts` into `AgentsMenuHandler` (it uses `resolveModel` and `getModelLabelFromConfig`, both pure functions the class can import directly).
23
+ - Pass `AgentManager` directly as the `manager` param (it structurally satisfies `AgentMenuManager`), eliminating 3 adapter closures.
24
+ - Simplify `index.ts` by removing eliminated adapter closures and unused imports.
25
+ - Update architecture doc to mark Layer 3 and Layer 4 as done.
26
+
27
+ ## Non-Goals
28
+
29
+ - Changing `runAgent()` or `resumeAgent()` function signatures — they remain as free functions called by the class.
30
+ - Removing the `AgentRunner` interface — it stays as the contract for `AgentManager`.
31
+ - Removing `RunnerIO` type — it stays as the IO boundary for `runAgent()`.
32
+ - Changing the `AgentMenuManager` interface — it stays as the narrow contract; `AgentManager` satisfies it structurally.
33
+ - Removing `AgentMenuDeps` — it is replaced by the class constructor; the type itself is removed.
34
+ - Refactoring `NotificationManager`, `SettingsManager`, or `SessionLifecycleHandler` — those are already class-shaped.
35
+ - Phase 12 work (decompose `renderWidgetLines`, consolidate test duplication).
36
+
37
+ ## Background
38
+
39
+ ### Phase 11 layer structure
40
+
41
+ Phase 11 in `docs/architecture/architecture.md` converts closure factories to classes in four layers:
42
+
43
+ - Layer 0: `SessionContext` interface (#192) ✓
44
+ - Layer 1: Runtime owns context queries (#193) ✓
45
+ - Layer 2: Align interfaces for structural typing (#194) ✓
46
+ - Layer 3: Convert closure factories to classes (#195 tools ✓, #196 runner + menu)
47
+ - Layer 4: Simplify `index.ts` (#196)
48
+
49
+ Issue #196 completes Layer 3 (runner + menu) and Layer 4 (index.ts simplification).
50
+
51
+ ### Current state
52
+
53
+ `createAgentRunner` (3 lines) wraps `runAgent`/`resumeAgent` in an object literal:
54
+
55
+ ```typescript
56
+ export function createAgentRunner(io: RunnerIO): AgentRunner {
57
+ return {
58
+ run: (snapshot, type, prompt, options) => runAgent(snapshot, type, prompt, options, io),
59
+ resume: resumeAgent,
60
+ };
61
+ }
62
+ ```
63
+
64
+ `createAgentsMenuHandler` (200+ lines) captures 8 deps and returns a handler function.
65
+ The deps bag includes `getModelLabel` (a closure built in `index.ts`) and `agentActivity` (a `Map` from runtime).
66
+
67
+ `index.ts` currently has ~23 arrow closures and 27 imports at 229 lines.
68
+
69
+ ### Structural typing confirmation
70
+
71
+ `AgentManager` already has `listAgents()`, `getRecord()`, and `spawnAndWait()` methods that structurally satisfy `AgentMenuManager`.
72
+ The `spawnAndWait` parameter type (`Omit<AgentSpawnConfig, "isBackground">`) is a superset of the menu's `{ description, maxTurns }`, so structural typing matches.
73
+
74
+ ## Design Overview
75
+
76
+ ### ConcreteAgentRunner
77
+
78
+ A minimal class that implements `AgentRunner` by delegating to the free functions:
79
+
80
+ ```typescript
81
+ export class ConcreteAgentRunner implements AgentRunner {
82
+ constructor(private readonly io: RunnerIO) {}
83
+
84
+ async run(snapshot: ParentSnapshot, type: SubagentType, prompt: string, options: RunOptions): Promise<RunResult> {
85
+ return runAgent(snapshot, type, prompt, options, this.io);
86
+ }
87
+
88
+ async resume(session: AgentSession, prompt: string, options?: ResumeOptions): Promise<string> {
89
+ return resumeAgent(session, prompt, options);
90
+ }
91
+ }
92
+ ```
93
+
94
+ The factory function `createAgentRunner` is removed.
95
+ The free functions `runAgent`, `resumeAgent`, `getAgentConversation`, and `normalizeMaxTurns` remain exported — they are used directly by tests and other modules.
96
+
97
+ ### AgentsMenuHandler
98
+
99
+ The class replaces `createAgentsMenuHandler` and `AgentMenuDeps`.
100
+ Constructor params are the subset of deps that are true collaborators:
101
+
102
+ ```typescript
103
+ export class AgentsMenuHandler {
104
+ constructor(
105
+ private readonly manager: AgentMenuManager,
106
+ private readonly registry: AgentTypeRegistry,
107
+ private readonly agentActivity: AgentActivityReader,
108
+ private readonly settings: AgentMenuSettings,
109
+ private readonly fileOps: AgentFileOps,
110
+ private readonly personalAgentsDir: string,
111
+ private readonly projectAgentsDir: string,
112
+ ) {}
113
+
114
+ async handle(ctx: { ui: MenuUI; modelRegistry: ModelRegistry; parentSnapshot: ParentSnapshot }): Promise<void> { ... }
115
+ }
116
+ ```
117
+
118
+ Key design decisions:
119
+
120
+ 1. **`agentActivity` stays as a constructor param** — it is a collaborator used in `viewAgentConversation`.
121
+ The issue's proposed signature omits it, but the class needs it at runtime.
122
+ 2. **`getModelLabel` is internalized** — the class imports `resolveModel` and `getModelLabelFromConfig` directly and computes the label in a private method.
123
+ This eliminates the closure from `index.ts` and removes the `getModelLabel` field from the deps interface.
124
+ 3. **`AgentMenuDeps` is removed** — the class constructor replaces it.
125
+ 4. **The `handle` method** replaces the returned function.
126
+ The inner helpers (`showAgentsMenu`, `showAllAgentsList`, etc.) become private methods.
127
+
128
+ ### index.ts simplification
129
+
130
+ After both conversions:
131
+
132
+ ```typescript
133
+ // Before (adapter closures):
134
+ const agentsMenuHandler = createAgentsMenuHandler({
135
+ manager: {
136
+ listAgents: () => manager.listAgents(),
137
+ getRecord: (id) => manager.getRecord(id),
138
+ spawnAndWait: (...) => manager.spawnAndWait(...),
139
+ },
140
+ registry,
141
+ agentActivity: runtime.agentActivity,
142
+ getModelLabel: (type, modelRegistry) => { ... }, // 7-line closure
143
+ settings,
144
+ fileOps: new FsAgentFileOps(),
145
+ personalAgentsDir: join(getAgentDir(), 'agents'),
146
+ projectAgentsDir: join(process.cwd(), '.pi', 'agents'),
147
+ });
148
+
149
+ // After:
150
+ const agentsMenu = new AgentsMenuHandler(
151
+ manager, registry, runtime.agentActivity,
152
+ settings, new FsAgentFileOps(),
153
+ join(getAgentDir(), 'agents'),
154
+ join(process.cwd(), '.pi', 'agents'),
155
+ );
156
+ ```
157
+
158
+ Eliminated closures: 4 (3 manager method adapters + 1 getModelLabel closure).
159
+ Eliminated imports: `getModelLabelFromConfig`, `resolveModel` (from index.ts), `createAgentRunner`, `type RunnerIO`, `createAgentsMenuHandler`.
160
+
161
+ Remaining adapter closures in `index.ts` (~15) are necessary: event handler registrations, SDK factory callbacks, `pi.sendMessage`/`pi.exec` adapters.
162
+ These are structural — they bridge the Pi SDK's callback-based API to the extension's object-oriented internals.
163
+
164
+ ## Module-Level Changes
165
+
166
+ ### `src/lifecycle/agent-runner.ts`
167
+
168
+ - Add `ConcreteAgentRunner` class implementing `AgentRunner`.
169
+ - Remove `createAgentRunner` factory function.
170
+ - Keep all free functions (`runAgent`, `resumeAgent`, `getAgentConversation`, `normalizeMaxTurns`) and all types exported.
171
+
172
+ ### `src/ui/agent-menu.ts`
173
+
174
+ - Replace `createAgentsMenuHandler` factory with `AgentsMenuHandler` class.
175
+ - Remove `AgentMenuDeps` interface.
176
+ - Add private `getModelLabel` method (internalizes the closure from `index.ts`).
177
+ - Convert inner functions (`showAgentsMenu`, `showAllAgentsList`, `showRunningAgents`, `viewAgentConversation`, `showSettings`) to private methods.
178
+ - Add imports: `resolveModel` from `#src/session/model-resolver`, `getModelLabelFromConfig` from `#src/tools/helpers`.
179
+ - Keep exported interfaces: `AgentMenuManager`, `AgentMenuSettings`, `AgentActivityReader`, `MenuUI`.
180
+
181
+ ### `src/index.ts`
182
+
183
+ - Replace `createAgentRunner(runnerIO)` with `new ConcreteAgentRunner(runnerIO)`.
184
+ - Replace `createAgentsMenuHandler({...})` with `new AgentsMenuHandler(...)`.
185
+ - Replace `agentsMenuHandler({...})` with `agentsMenu.handle({...})`.
186
+ - Remove adapter closures for `manager.listAgents`, `manager.getRecord`, `manager.spawnAndWait`, and `getModelLabel`.
187
+ - Remove unused imports: `createAgentRunner`, `type RunnerIO` → `ConcreteAgentRunner`; `createAgentsMenuHandler` → `AgentsMenuHandler`; `getModelLabelFromConfig`, `resolveModel`.
188
+ - Net effect: ~15 lines removed, 5 imports removed.
189
+
190
+ ### `docs/architecture/architecture.md`
191
+
192
+ - Mark Layer 3 remaining items (runner, menu) as done.
193
+ - Mark Layer 4 as done.
194
+ - Update the factory→class table entries for `createAgentRunner` and `createAgentsMenuHandler` with ✓.
195
+
196
+ ## Test Impact Analysis
197
+
198
+ ### `test/lifecycle/agent-runner.test.ts` (and siblings)
199
+
200
+ No changes needed.
201
+ Tests call `runAgent()` and `resumeAgent()` directly — they never use `createAgentRunner`.
202
+ The `ConcreteAgentRunner` class is a trivial two-method delegation wrapper tested implicitly through `index.ts` integration and explicitly through one new unit test.
203
+
204
+ ### `test/ui/agent-menu.test.ts`
205
+
206
+ Tests need updating:
207
+
208
+ 1. Replace `createAgentsMenuHandler(makeDeps())` with `new AgentsMenuHandler(...)`.
209
+ 2. Replace `handler(params)` with `handler.handle(params)`.
210
+ 3. Remove `getModelLabel` from `makeDeps()` — it is now an internal method.
211
+ 4. Remove `AgentMenuDeps` import; update `makeDeps` to construct positional args or a helper that returns a handler directly.
212
+
213
+ No test logic changes — only call-site updates for the new API shape.
214
+
215
+ ### New tests
216
+
217
+ One new unit test for `ConcreteAgentRunner`: verify it delegates `run` and `resume` to the underlying functions.
218
+
219
+ ## TDD Order
220
+
221
+ 1. **Add `ConcreteAgentRunner` class alongside factory.**
222
+ Add the class to `agent-runner.ts`, keep `createAgentRunner` temporarily.
223
+ Add a unit test verifying delegation.
224
+ `test: add ConcreteAgentRunner delegation test`
225
+
226
+ 2. **Switch `index.ts` to `ConcreteAgentRunner`, remove factory.**
227
+ Replace `createAgentRunner(runnerIO)` with `new ConcreteAgentRunner(runnerIO)`.
228
+ Remove the `createAgentRunner` factory function.
229
+ Update imports.
230
+ `refactor: replace createAgentRunner with ConcreteAgentRunner class`
231
+
232
+ 3. **Convert `createAgentsMenuHandler` to `AgentsMenuHandler` class.**
233
+ Replace factory function with class.
234
+ Move inner functions to private methods.
235
+ Internalize `getModelLabel` as a private method.
236
+ Remove `AgentMenuDeps` interface.
237
+ `refactor: convert createAgentsMenuHandler to AgentsMenuHandler class`
238
+
239
+ 4. **Update `agent-menu.test.ts` for class API.**
240
+ Replace `createAgentsMenuHandler(makeDeps())` with class construction.
241
+ Replace `handler(params)` with `handler.handle(params)`.
242
+ Remove `getModelLabel` from test deps.
243
+ All existing tests pass with updated call sites.
244
+ `test: update agent-menu tests for AgentsMenuHandler class`
245
+
246
+ 5. **Simplify `index.ts` wiring.**
247
+ Replace `createAgentsMenuHandler({...})` with `new AgentsMenuHandler(...)`.
248
+ Pass `manager` directly (structural typing).
249
+ Remove adapter closures and unused imports.
250
+ `refactor: simplify index.ts wiring for AgentsMenuHandler`
251
+
252
+ 6. **Update architecture doc.**
253
+ Mark Layer 3 remaining items and Layer 4 as done in `docs/architecture/architecture.md`.
254
+ `docs: mark Phase 11 Layer 3 and Layer 4 complete`
255
+
256
+ ## Risks and Mitigations
257
+
258
+ | Risk | Mitigation |
259
+ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
260
+ | `AgentManager` might not structurally satisfy `AgentMenuManager` | Confirmed: `listAgents()`, `getRecord()`, `spawnAndWait()` signatures are compatible. `pnpm run check` in step 5 verifies. |
261
+ | Internalized `getModelLabel` might diverge from the closure | The private method uses the same `resolveModel` and `getModelLabelFromConfig` imports — identical logic, just moved. |
262
+ | Tests that use `AgentMenuDeps` type break when removed | Step 4 updates all test call sites before step 5 changes production code. The test file is 215 lines — manageable in one step. |
263
+ | `agentActivity` missing from constructor | Included in the class constructor (diverging from the issue's proposed signature which omits it). |
264
+
265
+ ## Open Questions
266
+
267
+ None — the issue's proposed design is clear and the implementation is mechanical.
268
+ The one deviation (keeping `agentActivity` as a constructor param) is necessary and minimal.