@gotgenes/pi-subagents 6.9.2 → 6.9.3

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,16 @@ 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
+ ## [6.9.3](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.9.2...pi-subagents-v6.9.3) (2026-05-22)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * add issue numbers to Phase 8 roadmap steps ([77fee40](https://github.com/gotgenes/pi-packages/commit/77fee402ebc445171f3768f2eea1bba242ce9723))
14
+ * add Phase 8 roadmap — testability, display extraction, menu decomposition ([37a0520](https://github.com/gotgenes/pi-packages/commit/37a0520e32a93f4c9bffd5a728882c68e9811024))
15
+ * clean up architecture.md progress tracking ([e006032](https://github.com/gotgenes/pi-packages/commit/e00603209b9c21cb1bef41c34eeb71c1cc338117))
16
+ * **retro:** add retro notes for issue [#116](https://github.com/gotgenes/pi-packages/issues/116) ([701dca8](https://github.com/gotgenes/pi-packages/commit/701dca8075221aa4c36e19e2d54d43c74863ea57))
17
+
8
18
  ## [6.9.2](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.9.1...pi-subagents-v6.9.2) (2026-05-22)
9
19
 
10
20
 
@@ -322,10 +322,15 @@ Target: every mutable state bag becomes a class, every dependency bag narrows to
322
322
  The work is sequenced so each change makes the next change easy.
323
323
  See the [Encapsulation roadmap](#encapsulation-roadmap) section for the full breakdown.
324
324
 
325
+ ### Phase 8: Testability, display extraction, and menu decomposition
326
+
327
+ Target: eliminate `vi.mock()` module mocking in the two most fragile test suites by injecting IO-touching collaborators; consolidate shared test fixtures; extract display helpers into a reusable module; decompose the largest UI file.
328
+
329
+ See the [Phase 8 roadmap](#phase-8-roadmap) section for the full breakdown.
330
+
325
331
  ## Structural refactoring roadmap
326
332
 
327
- Phases 1–5 are complete.
328
- Phase 7 (encapsulation and dependency narrowing) is the active structural track.
333
+ Phases 1–5 and 7 are complete.
329
334
  See `git log` for the full history; issue references are preserved below for traceability.
330
335
 
331
336
  | Phase | Issue | Summary |
@@ -379,136 +384,105 @@ This section describes the Phase 7 targets: encapsulating mutable state into cla
379
384
 
380
385
  Each step is sequenced so it makes the next step easier.
381
386
 
382
- ### Current smells
387
+ ### Resolved smells
388
+
389
+ All nine smells identified at the start of Phase 7 were resolved:
383
390
 
384
- | Smell | Location | Evidence |
385
- | ------------------------------ | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
386
- | ~~Global mutable state~~ | ~~`agent-types.ts`~~ | **Fixed #108**: `AgentTypeRegistry` class; `reloadCustomAgents` callback removed from `AgentToolDeps` and `AgentMenuDeps` |
387
- | ~~Closure bag as class~~ | ~~`createNotificationSystem()`~~ | **Fixed #116**: `NotificationManager` class; `pendingNudges` and timer state are private fields |
388
- | ~~Mutable state bag~~ | ~~`AgentActivity` (7 fields)~~ | **Fixed #110**: `AgentActivityTracker` class; `ui-observer.ts` calls transition methods; widget, notification, agent-tool use read-only accessors |
389
- | ~~Settings relay~~ | ~~`AgentMenuDeps` (13 fields)~~ | **Fixed #109**: `SettingsManager` class; 6 callback fields collapsed to `settings: SettingsManager`; `AgentMenuDeps` now 8 fields |
390
- | ~~Post-construction mutation~~ | ~~`AgentRecord` non-transition state~~ | **Fixed #111**: `ExecutionState`, `WorktreeState`, `NotificationState` collaborators; `pendingSteers` moved to `AgentManager`; stats encapsulated behind mutation methods |
391
- | ~~Fire-and-forget callbacks~~ | ~~`AgentManagerOptions`~~ | **Fixed #112**: `AgentManagerObserver` interface; `observer` replaces 3 callbacks; `index.ts` constructs one observer object instead of 3 closure lambdas |
392
- | ~~Duplicate `SpawnOptions`~~ | ~~`service.ts` + `agent-manager.ts`~~ | **Fixed #113**: internal type renamed to `AgentSpawnConfig`; public `SpawnOptions` in `service.ts` unchanged |
393
- | ~~Type dumping ground~~ | ~~`types.ts`~~ | **Fixed #116**: `NotificationDetails` `notification.ts`; `ParentSnapshot` `parent-snapshot.ts`; `EnvInfo` `env.ts`; `AgentIdentity` and `AgentPromptConfig` narrow subsets |
394
- | ~~Wide dependency bags~~ | ~~`AgentToolDeps` (9), `AgentMenuDeps` (8)~~ | **Fixed #114**: `AgentToolDeps` 9 → 6; `AgentMenuDeps` 8 → 7; `emitEvent` removed from both; description text derived from registry; `agentActivity` narrowed to typed interfaces |
391
+ | Smell | Resolution |
392
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
393
+ | Global mutable state | `AgentTypeRegistry` class (#108); `reloadCustomAgents` callback removed from dep bags |
394
+ | Closure bag as class | `NotificationManager` class (#116); `pendingNudges` and timer state are private fields |
395
+ | Mutable state bag | `AgentActivityTracker` class (#110); transition methods replace external writes |
396
+ | Settings relay | `SettingsManager` class (#109); 6 callback fields collapsed to one object |
397
+ | Post-construction mutation | `ExecutionState`, `WorktreeState`, `NotificationState` collaborators (#111); stats behind mutation methods |
398
+ | Fire-and-forget callbacks | `AgentManagerObserver` interface (#112); one observer object replaces 3 closure lambdas |
399
+ | Duplicate `SpawnOptions` | Internal type renamed to `AgentSpawnConfig` (#113); public `SpawnOptions` unchanged |
400
+ | Type dumping ground | `NotificationDetails`, `ParentSnapshot`, `EnvInfo` moved to their natural modules (#116); narrow subsets defined |
401
+ | Wide dependency bags | `AgentToolDeps` 9 → 6, `AgentMenuDeps` 8 → 7 (#114); `emitEvent` removed; description text derived from registry; `agentActivity` narrowed |
395
402
 
396
403
  ### Step A: Extract state into classes (foundation, parallel)
397
404
 
398
405
  These three extractions are independent and can proceed in any order.
399
406
  Each eliminates a category of global/closure state and gives orphaned callbacks a natural home.
400
407
 
401
- #### A1. `AgentTypeRegistry` class (#108)
408
+ #### A1. AgentTypeRegistry class (#108)
402
409
 
403
- Wrap the module-scoped `agents` Map and free functions in `agent-types.ts` into an injectable class.
404
- `reloadCustomAgents` (currently a callback threaded through `AgentToolDeps` and `AgentMenuDeps`) becomes `registry.reload()`.
405
- `DEFAULT_AGENT_NAMES` moves from `types.ts` to the registry.
410
+ Wrapped the module-scoped `agents` Map and free functions in `agent-types.ts` into an injectable class.
411
+ `reloadCustomAgents` callback removed from `AgentToolDeps` and `AgentMenuDeps`; replaced by `registry.reload()`.
412
+ `DEFAULT_AGENT_NAMES` moved from `types.ts` to the registry.
406
413
 
407
- Impact: eliminates global mutable state, enables test isolation without module resets, removes `reloadCustomAgents` callback from 2 dependency bags.
408
-
409
- #### ~~A2. `SettingsManager` class (#109)~~ — **Done**
414
+ #### A2. SettingsManager class (#109, #118)
410
415
 
411
416
  Encapsulated settings load/save/apply cycle into `SettingsManager` (in `settings.ts`).
412
417
  Owns `defaultMaxTurns`, `graceTurns`, `maxConcurrent` with normalizing property accessors.
413
- Absorbed `SettingsAppliers`, `applyAndEmitLoaded`, `saveAndEmitChanged`.
418
+ Added `applyMaxConcurrent(n)`, `applyDefaultMaxTurns(n)`, `applyGraceTurns(n)` — each owns the full consequence chain: normalize → set in memory → notify callback → persist → emit event → return toast.
414
419
  The 6 settings-related fields in `AgentMenuDeps` collapsed to `settings: AgentMenuSettings`.
415
- `AgentManager` reads `maxConcurrent` via injected `getMaxConcurrent` function.
416
- `SubagentRuntime.defaultMaxTurns` and `.graceTurns` removed.
417
-
418
- Impact: reduced `AgentMenuDeps` from 13 → 8 fields; `AgentToolDeps` from 8 → 7 fields.
419
-
420
- #### ~~A2b. `SettingsManager` apply methods (#118)~~ — **Done**
421
-
422
- Added `applyMaxConcurrent(n)`, `applyDefaultMaxTurns(n)`, `applyGraceTurns(n)` to `SettingsManager`.
423
- Each owns the full consequence chain: normalize → set in memory → notify callback → persist → emit event → return toast.
424
- `SettingsManager` accepts an `onMaxConcurrentChanged` callback (wired to `manager.notifyConcurrencyChanged()` at init).
425
- `notifyConcurrencyChanged` removed from `AgentMenuManager`; `showSettings` now makes a single apply call per setting.
426
420
 
427
- Impact: eliminates LoD / Tell-Don't-Ask violation in `showSettings`; menu no longer coordinates between settings and manager.
428
-
429
- #### ~~A3. `AgentActivityTracker` class (#110)~~ — **Done**
421
+ #### A3. AgentActivityTracker class (#110)
430
422
 
431
423
  Wrapped the 7-field mutable `AgentActivity` interface in an `AgentActivityTracker` class (`src/ui/agent-activity-tracker.ts`).
432
- `ui-observer.ts` calls transition methods (`onToolStart`, `onToolEnd`, `onMessageStart`, `onMessageUpdate`, `onTurnEnd`, `onUsageUpdate`, `setSession`).
433
- The notification system, widget, conversation viewer, and agent-tool use read-only accessors.
434
- The shared map on `SubagentRuntime` is now `Map<string, AgentActivityTracker>`.
435
-
436
- Impact: eliminates output-argument writes in `ui-observer.ts`, makes the mutation contract explicit.
424
+ `ui-observer.ts` calls transition methods; consumers use read-only accessors.
425
+ The shared map on `SubagentRuntime` is `Map<string, AgentActivityTracker>`.
437
426
 
438
- ### ~~Step B: Split `AgentRecord` lifecycle state (#111)~~ — **Done**
427
+ ### Step B: Split AgentRecord lifecycle state (#111)
439
428
 
440
429
  Split post-construction mutation into phase-specific collaborators, each born complete:
441
430
 
442
- - **`ExecutionState`** (`session`, `outputFile`) — constructed in `onSessionCreated`, attached as `record.execution`.
443
- - **`WorktreeState`** (`path`, `branch`, `cleanupResult`) — constructed at worktree setup, attached as `record.worktreeState`.
444
- - **`NotificationState`** (`toolCallId`, `resultConsumed`) — constructed by `AgentManager.spawn()` when `toolCallId` is provided in `AgentSpawnConfig`, attached as `record.notification`.
445
- - **`pendingSteers`** moved to `Map<string, string[]>` on `AgentManager`; steer-tool and service-adapter call `manager.queueSteer()`.
446
- - Stats (`toolUses`, `lifetimeUsage`, `compactionCount`) encapsulated behind mutation methods (`incrementToolUses`, `addUsage`, `incrementCompactions`) with read-only getters.
431
+ - **`ExecutionState`** (`session`, `outputFile`) — constructed in `onSessionCreated`.
432
+ - **`WorktreeState`** (`path`, `branch`, `cleanupResult`) — constructed at worktree setup.
433
+ - **`NotificationState`** (`toolCallId`, `resultConsumed`) — constructed by `AgentManager.spawn()` when `toolCallId` is provided.
434
+ - **`pendingSteers`** moved to `Map<string, string[]>` on `AgentManager`.
435
+ - Stats encapsulated behind mutation methods with read-only getters.
447
436
  - `AgentRecordInit` trimmed from 19 optional fields to 4 construction-time fields.
448
437
 
449
- Each piece is born complete at the moment its information is available.
450
- The record doesn't accumulate half-baked state — it receives fully constructed collaborators.
438
+ ### Step C: Replace AgentManager callbacks with observer (#112)
451
439
 
452
- ### Step C: Replace `AgentManager` callbacks with observer (#112) ✅
453
-
454
- **Done.**
455
440
  `AgentManagerObserver` interface replaces `onStart`/`onComplete`/`onCompact`.
456
441
  `index.ts` constructs one observer object instead of 3 closure lambdas.
457
442
  `AgentManagerOptions` drops from 9 → 7 fields.
458
443
 
459
- ### Step D: Disambiguate `SpawnOptions` and narrow dependency bags
460
-
461
- With the registry class, settings manager, and observer in place, the dependency bags shrink naturally.
444
+ ### Step D: Disambiguate SpawnOptions and narrow dependency bags
462
445
 
463
- #### D1. Disambiguate `SpawnOptions` (#113)
446
+ #### D1. Disambiguate SpawnOptions (#113)
464
447
 
465
- **Done.**
466
448
  Internal `SpawnOptions` in `agent-manager.ts` renamed to `AgentSpawnConfig`.
467
- Public `SpawnOptions` in `service.ts` is unchanged.
449
+ Public `SpawnOptions` in `service.ts` unchanged.
468
450
 
469
- #### D2. Narrow `AgentToolDeps` and `AgentMenuDeps` (#114)
451
+ #### D2. Narrow AgentToolDeps and AgentMenuDeps (#114)
470
452
 
471
- **Done.**
472
- `AgentToolDeps` 9 6: removed `emitEvent` (moved to `AgentManagerObserver.onAgentCreated`), `typeListText`, `availableTypesText` (derived inside `createAgentTool`); `agentActivity` narrowed to `AgentActivityAccess`.
473
- `AgentMenuDeps` 8 7: removed dead `emitEvent` field; `agentActivity` narrowed to `AgentActivityReader`.
474
- `buildTypeListText` extracted to `tools/helpers.ts`.
453
+ | Bag | Before | After | How |
454
+ | --------------- | -------- | ----- | ----------------------------------------------------------------------------------------------------------- |
455
+ | `AgentToolDeps` | 9 fields | 6 | `emitEvent` → observer; `typeListText`/`availableTypesText` derived from registry; `agentActivity` narrowed |
456
+ | `AgentMenuDeps` | 8 fields | 7 | Dead `emitEvent` removed; `agentActivity` narrowed to read-only `AgentActivityReader` |
475
457
 
476
- | Bag | Before | After | How |
477
- | --------------- | -------- | ----- | ------------------------------------------------------------------------------------------------------------- |
478
- | `AgentToolDeps` | 9 fields | 6 | `emitEvent` → observer; `typeListText`/`availableTypesText` derived from registry; `agentActivity` narrowed |
479
- | `AgentMenuDeps` | 8 fields | 7 | Dead `emitEvent` removed; `agentActivity` narrowed to read-only `AgentActivityReader` |
458
+ ### Step E: Decompose large files and relocate types
480
459
 
481
- ### Step E: Decompose large files and relocate types (parallel)
460
+ #### E1. Split agent-tool.ts foreground/background (#115)
482
461
 
483
- #### E1. Split `agent-tool.ts` foreground/background (#115) ✅
484
-
485
- **Done.**
486
- Fixed two upstream API gaps before extracting: `onSessionCreated` now receives `(session, record)` (eliminating a `listAgents()` reverse-search), and `AgentSpawnConfig` accepts `toolCallId` (moving `NotificationState` wiring into `AgentManager.spawn()`).
487
462
  Extracted `foreground-runner.ts` (~175 lines) and `background-spawner.ts` (~116 lines).
488
- `agent-tool.ts` reduced from 579 → 411 lines (orchestrator + rendering).
463
+ `agent-tool.ts` reduced from 579 → 411 lines.
489
464
 
490
- #### ~~E2. Type housekeeping (#116)~~ — **Done**
465
+ #### E2. Type housekeeping (#116)
491
466
 
492
- - Moved `NotificationDetails` from `types.ts` to `notification.ts`.
493
- - Moved `ParentSnapshot` from `types.ts` to `parent-snapshot.ts` (`DEFAULT_AGENT_NAMES` was already moved in #108).
494
- - Moved `EnvInfo` from `types.ts` to `env.ts`.
467
+ - Moved `NotificationDetails`, `ParentSnapshot`, `EnvInfo` to their natural modules.
495
468
  - Converted `createNotificationSystem` closure to `NotificationManager` class.
496
469
  - Converted `ConversationViewer` constructor from 7 positional parameters to `ConversationViewerOptions` bag.
497
- - Defined `AgentIdentity` and `AgentPromptConfig` narrow subsets; `AgentConfig extends` both; `buildAgentPrompt` narrowed to `AgentPromptConfig`.
498
-
499
- ### Expected impact
500
-
501
- | Metric | Before | After |
502
- | ------------------------------------------ | -------------------------------------------------------------------------------------- | ------- |
503
- | Module-scoped mutable state | ~~1 (`agent-types.ts` Map)~~ | **0** ✓ |
504
- | Closure-bag "classes" | ~~2~~ ~~1~~ **0** (`createNotificationSystem` **fixed #116**; settings **fixed #109**) | 0 |
505
- | Externally-mutated state bags | ~~2~~ ~~1~~ **0** (`AgentRecord` **fixed #111**; `AgentActivity` **fixed #110**) | 0 |
506
- | `AgentManagerOptions` fields | 8 | 5 |
507
- | `AgentToolDeps` fields | ~~9~~ **6** (−3 text fields derived; −1 emitEvent → observer; activity narrowed) | 6 |
508
- | `AgentMenuDeps` fields | ~~13~~ **7** (−6 settings #109; −1 registry #108; −1 dead emitEvent #114) | 7 |
509
- | `SpawnOptions` callback fields | 1 (`onSessionCreated`) | 0 |
510
- | Callbacks threaded through deps | ~~8~~ 0 remaining settings callbacks (**fixed #109**); `emitEvent` ×3 remain | 0 |
511
- | Types in `types.ts` without a natural home | ~~4~~ **0** ✓ (all moved #116) | 0 |
470
+ - Defined `AgentIdentity` and `AgentPromptConfig` narrow subsets; `buildAgentPrompt` narrowed to `AgentPromptConfig`.
471
+
472
+ ### Phase 7 results
473
+
474
+ | Metric | Before | After |
475
+ | ------------------------------------------ | ------ | ----- |
476
+ | Module-scoped mutable state | 1 | 0 |
477
+ | Closure-bag "classes" | 2 | 0 |
478
+ | Externally-mutated state bags | 2 | 0 |
479
+ | `AgentManagerOptions` fields | 9 | 7 |
480
+ | `AgentToolDeps` fields | 9 | 6 |
481
+ | `AgentMenuDeps` fields | 13 | 7 |
482
+ | `SpawnOptions` callback fields | 6 | 1 |
483
+ | `RunOptions` callback fields | 6 | 1 |
484
+ | Callbacks threaded through deps | 8 | 0 |
485
+ | Types in `types.ts` without a natural home | 4 | 0 |
512
486
 
513
487
  ### Dependency graph
514
488
 
@@ -526,6 +500,135 @@ E2 (Type housekeeping) ── can start after A1, runs parallel to later steps
526
500
 
527
501
  ---
528
502
 
503
+ ## Phase 8 roadmap
504
+
505
+ Phase 7 eliminated all structural smells (mutable state, closure bags, callback threading, wide dependency bags).
506
+ Phase 8 targets the next layer: testability friction, display module cohesion, and menu decomposition.
507
+
508
+ The test suite (690 tests, 1.4:1 test-to-code ratio) is comprehensive but uneven in quality.
509
+ Two files — `session-config.test.ts` and `agent-runner.test.ts` — account for 11 of 12 total `vi.mock()` calls and rely heavily on verifying internal call sequences rather than observable outputs.
510
+ This fragility is a symptom of production code that imports IO-touching collaborators directly instead of receiving them through injection.
511
+
512
+ The display and menu improvements were identified during Phase 7 but deferred because they don't gate encapsulation work.
513
+ They are included here because the display extraction unblocks menu decomposition.
514
+
515
+ ### Test pain points
516
+
517
+ | Symptom | Location | Root cause |
518
+ | ----------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------- |
519
+ | 7 `vi.mock()` calls | `agent-runner.test.ts` | Runner imports prompts, memory, skills, env, session-dir directly |
520
+ | 4 `vi.mock()` calls | `session-config.test.ts` | Assembler imports prompts, memory, skills directly |
521
+ | 52 `as any` casts | Across test suite | SDK session/context interfaces too wide to construct in tests |
522
+ | 3× duplicated `mockSession()` | agent-manager, record-observer, ui-observer tests | No shared test fixture |
523
+ | 3× duplicated `makeDeps()` | agent-tool, background-spawner, foreground-runner tests | No shared tool-deps fixture |
524
+ | Weak assertions | lifecycle, renderer, session-config tests | `toHaveBeenCalled()` without args, `toContain()` on large strings |
525
+
526
+ Contrast with the well-designed test suites: `agent-manager.test.ts` (1 mock, DI via `AgentRunner` interface), `notification.test.ts` (0 mocks, pure functions + DI), and `agent-tool.test.ts` (0 mocks, tests via deps bag).
527
+ The pattern is clear: modules that accept collaborators through injection produce resilient tests; modules that import collaborators directly produce fragile mock-heavy tests.
528
+
529
+ ### Step F: Shared test fixtures (#131)
530
+
531
+ Consolidate duplicated mock factories into `test/helpers/`.
532
+
533
+ 1. `createMockSession()` — subscribable event bus with `emit()` helper; replaces 3 hand-rolled copies.
534
+ 2. `createToolDeps()` — builds `AgentToolDeps` with sensible defaults and override support; replaces 3 `makeDeps()` copies.
535
+
536
+ Impact: reduces test boilerplate; single source of truth for mock shapes; changes to dep interfaces propagate automatically.
537
+
538
+ ### Step G: Inject IO collaborators into session-config (#132)
539
+
540
+ `assembleSessionConfig` is described as a pure assembler, but it directly imports three IO-touching functions: `preloadSkills` (reads `.pi/skills` files), `buildMemoryBlock` (reads `MEMORY.md`), and `buildReadOnlyMemoryBlock` (reads `MEMORY.md`).
541
+ It also imports `buildAgentPrompt`, which is pure but mocked anyway because tests verify call arguments instead of output properties.
542
+
543
+ Inject these as an `AssemblerIO` parameter:
544
+
545
+ ```typescript
546
+ export interface AssemblerIO {
547
+ preloadSkills: (skills: string[], cwd: string) => PreloadedSkill[];
548
+ buildMemoryBlock: (name: string, scope: MemoryScope, cwd: string) => string;
549
+ buildReadOnlyMemoryBlock: (name: string, scope: MemoryScope, cwd: string) => string;
550
+ buildAgentPrompt: (config: AgentPromptConfig, cwd: string, env: EnvInfo, parentPrompt: string, extras: PromptExtras) => string;
551
+ }
552
+ ```
553
+
554
+ The production call site in `agent-runner.ts` passes the real implementations.
555
+ Tests pass stubs or let real implementations run against controlled inputs.
556
+
557
+ Impact: eliminates all 4 `vi.mock()` calls in `session-config.test.ts`; tests verify `SessionConfig` output properties instead of mock call arguments; the assembler becomes truly pure.
558
+
559
+ ### Step H: Inject SDK boundary into agent-runner (#133)
560
+
561
+ `agent-runner.ts` has 7 module mocks because it imports `createAgentSession`, `DefaultResourceLoader`, `SessionManager`, and `SettingsManager` from the Pi SDK, plus `detectEnv`, `deriveSubagentSessionDir`, and `assembleSessionConfig` from sibling modules.
562
+
563
+ After Step G, `assembleSessionConfig` no longer needs mocking (its own IO is injected).
564
+ The remaining SDK dependencies can be injected via a narrow `RunnerIO` interface:
565
+
566
+ ```typescript
567
+ export interface RunnerIO {
568
+ createSession: (opts: SessionOptions) => AgentSession;
569
+ createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoader;
570
+ createSessionManager: (cwd: string) => SessionManager;
571
+ detectEnv: (exec: ShellExec, cwd: string) => Promise<EnvInfo>;
572
+ deriveSessionDir: (parentFile: string) => string;
573
+ }
574
+ ```
575
+
576
+ The production call site in `agent-manager.ts` passes a `RunnerIO` built from the real SDK imports.
577
+ Tests pass a stub `RunnerIO` without `vi.mock()`.
578
+
579
+ Impact: eliminates 5–7 `vi.mock()` calls in `agent-runner.test.ts`; tests verify behavior (turn limits, tool filtering, response collection) through injected fakes; refactoring internal structure no longer breaks tests.
580
+
581
+ ### Step I: Reduce `as any` casts in tests (#134)
582
+
583
+ With Steps G and H, many `as any` casts disappear because tests construct narrow injectable interfaces instead of wide SDK types.
584
+ Remaining casts are addressed by:
585
+
586
+ 1. Defining a `TestSession` type in `test/helpers/` that satisfies `SubscribableSession` + the fields tests actually read.
587
+ 2. Replacing `const mockCtx = { cwd: "/tmp" } as any` with properly typed `AssemblerContext` or `ParentSnapshot` objects.
588
+ 3. Using `satisfies` assertions where possible instead of `as any`.
589
+
590
+ Target: reduce `as any` count from 52 to under 10.
591
+
592
+ ### Step J: Extract display helpers (#135)
593
+
594
+ `agent-widget.ts` (600 lines) exports 11 helper functions and constants that are used by both the widget and the menu.
595
+ Extract these into `ui/display.ts`:
596
+
597
+ - Pure formatters: `formatTokens`, `formatSessionTokens`, `formatTurns`, `formatMs`, `formatDuration`.
598
+ - Display helpers: `getDisplayName`, `getPromptModeLabel`, `buildInvocationTags`, `describeActivity`.
599
+ - Constants: `SPINNER`, `ERROR_STATUSES`, `TOOL_DISPLAY`.
600
+
601
+ Impact: `agent-widget.ts` drops from 600 → ~420 lines; shared display logic has a single import point; menu and tool modules stop importing from the widget.
602
+
603
+ ### Step K: Decompose agent-menu.ts (#136)
604
+
605
+ `agent-menu.ts` (650 lines) has 8 distinct responsibilities: menu FSM, agent listing, config editing, agent ejection, two creation wizards, running-agent viewer, and settings form.
606
+ Filesystem operations (read/write/delete agent `.md` files) are scattered throughout.
607
+
608
+ 1. Extract `AgentFileOps` interface — `read`, `write`, `delete`, `findAgentFile` — abstracting the fs calls.
609
+ 2. Extract `ui/agent-config-editor.ts` — `showAgentDetail` with enable/disable/reset/delete transitions.
610
+ 3. Extract `ui/agent-creation-wizard.ts` — both AI-generation and manual form paths.
611
+ 4. Leave menu orchestration, settings form, and running-agent viewer in `agent-menu.ts` (~200 lines).
612
+
613
+ Impact: `agent-menu.ts` drops from 650 → ~200 lines; extracted modules receive `AgentFileOps` via injection; wizard logic becomes independently testable.
614
+
615
+ ### Step dependencies
616
+
617
+ ```text
618
+ F (Shared fixtures) ──────────────────────────────┐
619
+
620
+ G (session-config IO injection) ──────────────────┤
621
+ └── H (agent-runner SDK injection) ────────────┤
622
+ └── I (Reduce as-any) ────────────────────┘
623
+
624
+ J (Display extraction) ──────────────────────────┐
625
+ └── K (Menu decomposition) ────────────────────┘
626
+ ```
627
+
628
+ Steps F through I (testability) and Steps J through K (display/menu) are independent tracks that can proceed in parallel.
629
+
630
+ ---
631
+
529
632
  ## Relationship with upstream
530
633
 
531
634
  This fork (`@gotgenes/pi-subagents` in the [gotgenes/pi-packages] monorepo) is now a hard fork of [tintinweb/pi-subagents].
@@ -0,0 +1,42 @@
1
+ ---
2
+ issue: 116
3
+ issue_title: "refactor(pi-subagents): type housekeeping and small structural cleanups"
4
+ ---
5
+
6
+ # Retro: #116 — type housekeeping and small structural cleanups
7
+
8
+ ## Final Retrospective (2026-05-21T23:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned and executed 6 refactoring steps for issue #116: relocated 3 misplaced types from `types.ts` to their natural home modules, converted `createNotificationSystem` closure to a `NotificationManager` class, switched `ConversationViewer` to an options-bag constructor, and defined `AgentIdentity`/`AgentPromptConfig` narrow subset interfaces.
13
+ All 690 tests stayed green throughout; no behavioral changes.
14
+ Released as `pi-subagents-v6.9.2`.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - The Python script approach for bulk-converting 16 `ConversationViewer` positional constructor calls to options-bag syntax was efficient and correct on the first attempt — all 17 tests passed immediately after the conversion.
21
+ - Proactive `grep -rn 'new ConversationViewer'` before step 5 caught the `agent-menu.ts` call site that the plan's Module-Level Changes section had omitted, avoiding a broken commit.
22
+ - The architecture doc update was clean — 5 targeted edits to mark E2 done, update smells table, and fix metrics.
23
+
24
+ #### What caused friction (agent side)
25
+
26
+ - `missing-context` — In all three type-relocation steps (1–3), I updated source-file imports but did not pre-flight grep for test-file imports of the relocated symbol.
27
+ Each time, `pnpm run check` caught the stale test import, requiring an extra edit-check round trip.
28
+ This happened with `test/renderer.test.ts` (step 1), `test/agent-runner.test.ts` + `test/agent-runner-extension-tools.test.ts` (step 2), and `test/prompts.test.ts` (step 3).
29
+ Impact: 3 unnecessary edit-check cycles; no broken commits since the type checker caught every case before `git commit`.
30
+
31
+ - `missing-context` — In step 4, the first `Edit` call on `src/index.ts` failed because the autoformatter had merged the `NotificationDetails` type import (added in step 1) into the existing notification import line, changing the text I expected.
32
+ Had to re-read the file to find the current import text.
33
+ Impact: one wasted edit call plus a file read; added ~10 seconds of friction.
34
+
35
+ #### What caused friction (user side)
36
+
37
+ - No friction observed — the user's prompts were clear and the `/tdd-plan` and `/ship-issue` templates provided all needed structure.
38
+
39
+ ### Takeaway
40
+
41
+ When relocating a type or symbol, always run `grep -rn 'SymbolName' src/ test/` before editing to identify *all* importers upfront — both source and test files.
42
+ This avoids the repeated pattern of "edit source → type-check fails on test → fix test → type-check again."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "6.9.2",
3
+ "version": "6.9.3",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },