@gotgenes/pi-subagents 7.0.0 → 7.1.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ 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.1.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.0.0...pi-subagents-v7.1.0) (2026-05-24)
9
+
10
+
11
+ ### Features
12
+
13
+ * **pi-subagents:** define SessionContext narrow interface ([#192](https://github.com/gotgenes/pi-packages/issues/192)) ([a043d4d](https://github.com/gotgenes/pi-packages/commit/a043d4d970a8aacc084a125d9a07b860a7fb6e9b))
14
+
15
+
16
+ ### Documentation
17
+
18
+ * **pi-subagents:** archive Phase 10, propose Phase 11 roadmap ([d474eef](https://github.com/gotgenes/pi-packages/commit/d474eef98c1a39757d933da291725ff126f1b8ac))
19
+ * **pi-subagents:** revise Phase 11 roadmap — layered class conversion ([35d1083](https://github.com/gotgenes/pi-packages/commit/35d1083445aece783e344c37fc949395e190f9e5))
20
+ * plan SessionContext narrow interface ([#192](https://github.com/gotgenes/pi-packages/issues/192)) ([95cb16e](https://github.com/gotgenes/pi-packages/commit/95cb16e46aa630ecc34f423aaaa4ff02845ed5b5))
21
+ * **retro:** add planning stage notes for issue [#192](https://github.com/gotgenes/pi-packages/issues/192) ([31fd729](https://github.com/gotgenes/pi-packages/commit/31fd7290985b0ef1d240cd5afd5e0e0e7eec9131))
22
+ * **retro:** add retro notes for issue [#185](https://github.com/gotgenes/pi-packages/issues/185) ([66e49cf](https://github.com/gotgenes/pi-packages/commit/66e49cfb4129b9bba3b78e0850402bc61d99dda8))
23
+ * **retro:** add TDD stage notes for issue [#192](https://github.com/gotgenes/pi-packages/issues/192) ([6cf3f95](https://github.com/gotgenes/pi-packages/commit/6cf3f95f6c39f3f81c1d335348d5abd74d948ff3))
24
+
8
25
  ## [7.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.19.1...pi-subagents-v7.0.0) (2026-05-24)
9
26
 
10
27
 
@@ -314,16 +314,16 @@ The widget reads agent state by polling a shared `Map<string, AgentActivityTrack
314
314
 
315
315
  ```mermaid
316
316
  flowchart TD
317
- subgraph core["@gotgenes/pi-subagents (this package)"]
317
+ subgraph core["@gotgenes/pi-subagents"]
318
318
  direction TB
319
- exports["SubagentsService interface\npublish / getSubagentsService()\nSubagentRecord, SubagentStatus, LifetimeUsage\nSUBAGENT_EVENTS constants"]
320
- engine["Agent + get_subagent_result + steer_subagent tools\nAgentManager, agent-runner, agent-types\npublishSubagentsService() called at init"]
321
- ui_int["Internal UI: widget, viewer, /agents menu\n(candidate for extraction to pi-subagents-ui)"]
319
+ exports["SubagentsService API<br/>publish / getSubagentsService<br/>SubagentRecord, SubagentStatus"]
320
+ engine["Tools: Agent, get_subagent_result,<br/>steer_subagent<br/>AgentManager, agent-runner"]
321
+ ui_int["Internal UI: widget, viewer,<br/>/agents menu"]
322
322
  end
323
323
 
324
- core -- "Symbol.for() on globalThis" --> sched["scheduling extension\n(hypothetical)"]
325
- core -- "Symbol.for() on globalThis" --> subui["pi-subagents-ui\n(deferred)"]
326
- core -- "Symbol.for() on globalThis" --> future["any future extension"]
324
+ core -- "Symbol.for on globalThis" --> sched["scheduling extension<br/>(hypothetical)"]
325
+ core -- "Symbol.for on globalThis" --> subui["pi-subagents-ui<br/>(deferred)"]
326
+ core -- "Symbol.for on globalThis" --> future["any future extension"]
327
327
  ```
328
328
 
329
329
  Consumers call `getSubagentsService()?.spawn(...)` at runtime.
@@ -599,121 +599,218 @@ 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 10)
602
+ ## Improvement roadmap (Phase 11)
603
603
 
604
- Phase 10 addresses the structural gaps identified in this analysis: flat code organization, oversized dependency bags, and complexity hotspots.
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.
605
606
 
606
- ### Step 1: Reorganize source into domain directories ([#164][164]) Done
607
+ > "Make the change that makes the change easy." —Kent Beck
607
608
 
608
- Moved files into `config/`, `session/`, `lifecycle/`, `observation/`, and `service/` subdirectories.
609
- All `src/` internal imports now use `#src/` path aliases (same style as `test/` files), eliminating relative depth arithmetic for future moves.
609
+ ### Findings
610
610
 
611
- - Domain model is now visible in the filesystem.
612
- - Root reduced to 5 files + 8 directories (was 31 files + 3 directories).
613
- - All subsequent steps can move or extract files without `../` import churn.
611
+ | Metric | Value |
612
+ | ------------------------- | --------------------------------------- |
613
+ | Health score | 75/100 (B) |
614
+ | #1 hotspot | `index.ts` (128 commits, accelerating) |
615
+ | Dead exports | 1 (`getToolCallName` re-export) |
616
+ | Production duplication | 0 |
617
+ | Test duplication | 1,396 lines (69 clone groups, 22 files) |
618
+ | `as any` casts in index | 5 |
619
+ | Adapter closures in index | 44 |
620
+ | Index fan-out | 25 imports |
614
621
 
615
- ### Step 2: Decompose ResolvedSpawnConfig ([#165][165])
622
+ ### Root cause
616
623
 
617
- Split the 15-field bag into `SpawnIdentity`, `SpawnExecution`, and `SpawnPresentation`.
618
- Each consumer declares its real dependencies.
619
- Enables Step 3 (narrowing AgentSpawnConfig, [#166][166]).
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:
620
626
 
621
- ### Step 3: Extract ParentSessionInfo from AgentSpawnConfig ([#166][166]) Complete
627
+ 1. `SubagentRuntime.currentCtx` is typed `{ pi: unknown; ctx: unknown }`so every consumer must `as any` cast to read fields.
628
+ 2. Context queries (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) live as closures in index.ts instead of methods on the state holder.
629
+ 3. `AgentToolManager` mixes fields from `AgentManager` and `SettingsManager` (source mismatch).
630
+ 4. `AgentToolWidget` uses different method names than `SubagentRuntime` (name mismatch).
622
631
 
623
- Extracted `parentSessionFile`, `parentSessionId`, `toolCallId` into `ParentSessionInfo`.
624
- `AgentSpawnConfig`, `BackgroundParams`, `ForegroundParams`, and `RunOptions` all carry the nested group.
632
+ Fix these structural misalignments and the class conversions become mechanical.
625
633
 
626
- ### Step 4: Narrow RunnerIO ([#167][167]) ✓ Done
634
+ ### Layer 0: Define `SessionContext` narrow interface ([#192][192])
627
635
 
628
- `RunnerIO` split into `EnvironmentIO` (3 methods: environment discovery) and `SessionFactoryIO` (5 methods + `assemblerIO`: SDK object creation).
629
- `RunnerIO` kept as a backward-compatible type alias for the intersection.
630
- All existing consumers satisfy both sub-interfaces via structural typing with no call-site changes.
636
+ `SubagentRuntime` currently types its context as `unknown` to avoid SDK coupling.
637
+ But `ExtensionContext` is exported by the SDK — the `unknown` is a historical choice, not a constraint.
638
+ Define a narrow `SessionContext` interface capturing the 5 fields runtime actually needs:
631
639
 
632
- ### Step 5: Extract ToolFilterConfig from SessionConfig ([#168][168]) ✓ Done
640
+ ```typescript
641
+ export interface SessionContext {
642
+ readonly cwd: string;
643
+ readonly model: unknown;
644
+ readonly modelRegistry: ModelRegistry | undefined;
645
+ getSystemPrompt(): string;
646
+ readonly sessionManager: {
647
+ getSessionFile(): string | undefined;
648
+ getSessionId(): string;
649
+ getBranch(): unknown[];
650
+ };
651
+ }
652
+ ```
633
653
 
634
- `ToolFilterConfig` extracted from `SessionConfig`, grouping `toolNames`, `disallowedSet`, and `extensions`.
635
- `filterActiveTools` now accepts a single `ToolFilterConfig` argument.
636
- `SessionConfig` reduced from 10 to 8 top-level fields.
654
+ - Target: `src/types.ts`
655
+ - Smell: Category C (platform type threading)
656
+ - Outcome: typed foundation for Layers 1–4; no `as any` needed by consumers of `SubagentRuntime`
657
+
658
+ ### Layer 1: `SubagentRuntime` stores typed context, owns its queries ([#193][193])
659
+
660
+ Change `currentCtx` from `{ pi: unknown; ctx: unknown }` to `SessionContext | undefined`.
661
+ The single `as SessionContext` cast moves into `handleSessionStart` — the boundary where the SDK hands us the value.
662
+ Add typed methods: `buildSnapshot(inheritContext)`, `getModelInfo()`, `getSessionInfo()`.
663
+
664
+ - Target: `src/runtime.ts`, `src/handlers/lifecycle.ts`
665
+ - Smell: Category C (closure queries on mutable field → methods on state owner)
666
+ - Outcome: 3 closure queries in index.ts → 0; `SubagentRuntime` is self-sufficient for tool deps
667
+ - Enables: Layer 3 (tools accept `SubagentRuntime` directly)
668
+
669
+ ### Layer 2: Align interfaces so real objects satisfy tool deps structurally ([#194][194])
637
670
 
638
- ### Step 6: Extract RunContext from RunOptions ([#169][169]) ✓ Done
671
+ Three alignment changes:
639
672
 
640
- Extracted `exec`, `registry`, `cwd`, and `parentSession` into `RunContext`, nested as `RunOptions.context`.
641
- `RunOptions` reduced from 12 to 9 fields (1 nested `context` + 8 flat execution fields).
673
+ 1. **Move `getMaxConcurrent` off `AgentToolManager`** it reads from `SettingsManager`, not `AgentManager`.
674
+ The tool already receives `settings`; read it from there.
675
+ 2. **Rename widget methods** — align `SubagentRuntime` method names with `AgentToolWidget` (either rename `updateWidget()` → `update()` on runtime, or rename the interface to match).
676
+ 3. **Remove dead re-export** — `getToolCallName` in `ui/message-formatters.ts` (fallow finding).
642
677
 
643
- ### Step 7: Reduce buildContentLines complexity ([#170][170]) Done
678
+ After this step, `AgentManager` structurally satisfies `AgentToolManager` and `SubagentRuntime` structurally satisfies `AgentToolWidget`.
644
679
 
645
- Extracted formatting sub-functions for each content type (user, assistant, tool result, bash execution, streaming indicator) into `ui/message-formatters.ts`.
646
- `buildContentLines` in `conversation-viewer.ts` is now a ~30-line dispatch loop delegating to `formatMessage` and `formatStreamingIndicator`.
680
+ - Target: `src/tools/agent-tool.ts` (interface), `src/runtime.ts` (method names), `src/ui/message-formatters.ts`
681
+ - Smell: Category C (source mismatch, name mismatch) + Category A (dead export)
682
+ - Outcome: structural typing connects real objects to tool interfaces without adapters
683
+ - Enables: Layer 3 (class constructors accept real objects directly)
647
684
 
648
- ### Step 8: Reduce renderResult complexity ([#171][171]) ✓ Done
685
+ ### Layer 3: Convert closure factories to classes ([#195][195], [#196][196])
649
686
 
650
- Extracted per-status result formatting from `renderResult` in `agent-tool.ts` into `tools/result-renderer.ts`.
651
- `renderResult` reduced from ~80 lines (cognitive complexity 43) to a 10-line guard + `renderAgentResult` dispatcher.
652
- The inline `stats()` closure became the exported `renderStats` helper, shared by all status renderers.
687
+ With Layers 0–2 complete, each factory is a mechanical conversion:
653
688
 
654
- ### Step 9: Extract shared turn-formatting logic ([#172][172]) ✓ Done
689
+ | Factory | Class | Constructor params |
690
+ | -------------------------------- | ------------------------------ | --------------------------------------------------- |
691
+ | `createAgentTool({...})` | `AgentTool` | `manager`, `runtime`, `settings`, `registry` |
692
+ | `createGetResultTool(...)` | `GetResultTool` | `manager`, `notifications`, `registry` |
693
+ | `createSteerTool(...)` | `SteerTool` | `manager`, `events` |
694
+ | `createAgentRunner(runnerIO)` | `AgentRunner` (concrete class) | `io: RunnerIO` |
695
+ | `createAgentsMenuHandler({...})` | `AgentsMenuHandler` | `manager`, `registry`, `settings`, `fileOps`, paths |
655
696
 
656
- Extracted `ToolCallContent`, `getToolCallName`, and `extractAssistantContent` into `session/content-items.ts`.
657
- Both `lifecycle/agent-runner.ts` (`getAgentConversation`) and `ui/message-formatters.ts` (`formatAssistantMessage`) now import from the shared module.
658
- Eliminates the 18-line production-duplication finding.
697
+ Each class satisfies the existing interface via structural typing.
698
+ The `defineTool()` wrapper moves into a `toToolDefinition()` method on each tool class.
699
+
700
+ - Target: `src/tools/*.ts`, `src/lifecycle/agent-runner.ts`, `src/ui/agent-menu.ts`
701
+ - Smell: Category C (closure factories masquerading as classes)
702
+ - Outcome: deps are constructor params (inspectable, testable); no captured closures
703
+ - Enables: Layer 4 (index.ts simplification)
704
+
705
+ ### Layer 4: Simplify index.ts (included in [#196][196])
706
+
707
+ With real objects satisfying tool interfaces and queries living on `SubagentRuntime`, the composition root becomes pure construction:
708
+
709
+ ```typescript
710
+ const runtime = new SubagentRuntime();
711
+ const settings = new SettingsManager(...);
712
+ const manager = new AgentManager(...);
713
+ const agentTool = new AgentTool(manager, runtime, settings, registry);
714
+ pi.registerTool(agentTool.toToolDefinition());
715
+ ```
716
+
717
+ No adapter closures.
718
+ No `as any`.
719
+ Fan-out drops from 25 to ~15 (internal factories eliminated).
720
+
721
+ - Target: `src/index.ts`
722
+ - Smell: Category B (god file) + Category C (adapter closure density)
723
+ - Outcome: index.ts shrinks from 280 to ~150 lines; churn hotspot stabilizes
659
724
 
660
725
  ### Step dependencies
661
726
 
662
727
  ```mermaid
663
728
  flowchart LR
664
- subgraph organization["Code organization"]
665
- S1["#164: Domain directories"]
666
- end
667
- subgraph bags["Dependency bags"]
668
- S2["#165: ResolvedSpawnConfig"] --> S3["#166: AgentSpawnConfig"]
669
- S4["#167: RunnerIO"]
670
- S5["#168: SessionConfig"]
671
- S6["#169: RunOptions"]
672
- end
673
- subgraph complexity["Complexity reduction"]
674
- S7["#170: buildContentLines"]
675
- S8["#171: renderResult"]
676
- S9["#172: Shared turn-formatting"]
677
- end
678
- S1 --> S2 & S4 & S5 & S6
679
- S1 --> S7 & S8 & S9
729
+ L0["Layer 0: SessionContext interface"] --> L1["Layer 1: Runtime owns queries"]
730
+ L1 --> L3["Layer 3: Classes replace factories"]
731
+ L2["Layer 2: Align interfaces"] --> L3
732
+ L3 --> L4["Layer 4: Simplify index.ts"]
680
733
  ```
681
734
 
682
- Step 1 ([#164][164], directory restructuring) unblocks all other steps by co-locating related files.
683
- Steps 2–6 (bag decomposition) and Steps 7–9 (complexity reduction) are independent tracks that can proceed in parallel.
684
- Within the bag track, Step 2 ([#165][165], ResolvedSpawnConfig) enables Step 3 ([#166][166], AgentSpawnConfig).
735
+ Layers 0 and 2 are independent of each other.
736
+ Layer 1 depends on Layer 0.
737
+ Layer 3 depends on both Layer 1 and Layer 2.
738
+ Layer 4 depends on Layer 3.
739
+
740
+ ## Improvement roadmap (Phase 12)
741
+
742
+ Phase 12 addresses the remaining fallow refactoring targets and test duplication.
743
+ These are independent of Phase 11 and can proceed in parallel if desired.
744
+
745
+ ### Step 1: Decompose `renderWidgetLines` (cognitive 44)
746
+
747
+ `renderWidgetLines` in `ui/widget-renderer.ts` handles agent-status formatting, tree connectors, overflow, and empty states.
748
+ Extract per-status renderers and a tree-connector utility.
749
+
750
+ - Target: `src/ui/widget-renderer.ts`
751
+ - Outcome: cognitive complexity < 10
752
+
753
+ ### Step 2: Decompose `showAgentDetail` (cognitive 33)
754
+
755
+ `showAgentDetail` in `ui/agent-config-editor.ts` handles display, edit, eject, and delete flows.
756
+ Extract sub-functions per menu action.
757
+
758
+ - Target: `src/ui/agent-config-editor.ts`
759
+ - Outcome: cognitive complexity < 10
760
+
761
+ ### Step 3: Decompose `update` in `agent-widget.ts` (cognitive 31)
762
+
763
+ `update` mixes timer lifecycle, agent list assembly, render delegation, and visibility state.
764
+ Extract `assembleWidgetState` (pure) and timer management.
765
+
766
+ - Target: `src/ui/agent-widget.ts`
767
+ - Outcome: cognitive complexity < 10
768
+
769
+ ### Step 4: Extract shared test fixtures
770
+
771
+ The 3 heaviest clone families:
772
+
773
+ - `agent-runner.test.ts` + `agent-runner-extension-tools.test.ts` (60-line shared setup)
774
+ - `agent-menu.test.ts` + `agent-creation-wizard.test.ts` + `agent-config-editor.test.ts` (54+51+24 lines)
775
+ - `agent-manager.test.ts` (18 internal clone groups, 210 duplicated lines)
776
+
777
+ Extract shared factories into `test/fixtures/` modules.
778
+
779
+ - Target: new `test/fixtures/` modules
780
+ - Outcome: test duplication reduced by ~400 lines
685
781
 
686
782
  ## Refactoring history
687
783
 
688
- Phases 1–5 and 7–9 are complete.
784
+ Phases 1–5 and 7–10 are complete.
689
785
  Phase 6 (UI extraction to a separate package) is deferred.
690
786
  Detailed records are preserved in per-phase history files:
691
787
 
692
- | Phase | Title | Status | History |
693
- | ----- | --------------------------------------------------- | -------- | -------------------------------------------------------------------------- |
694
- | 1 | Export SubagentsService API boundary | Complete | [phase-1-api-boundary.md](history/phase-1-api-boundary.md) |
695
- | 2 | Remove scheduling subsystem | Complete | [phase-2-remove-scheduling.md](history/phase-2-remove-scheduling.md) |
696
- | 3 | Remove group-join, RPC; replace output-file | Complete | [phase-3-remove-rpc-groupjoin.md](history/phase-3-remove-rpc-groupjoin.md) |
697
- | 4 | Implement and publish SubagentsService | Complete | [phase-4-implement-service.md](history/phase-4-implement-service.md) |
698
- | 5 | Decompose index.ts | Complete | [phase-5-decompose-index.md](history/phase-5-decompose-index.md) |
699
- | 6 | Extract UI to separate package | Deferred | — |
700
- | 7 | Encapsulation and dependency narrowing | Complete | [phase-7-encapsulation.md](history/phase-7-encapsulation.md) |
701
- | 8 | Testability, display extraction, menu decomposition | Complete | [phase-8-testability.md](history/phase-8-testability.md) |
702
- | 9 | Observation consolidation, ctx elimination | Complete | [phase-9-observation-ctx.md](history/phase-9-observation-ctx.md) |
788
+ | Phase | Title | Status | History |
789
+ | ----- | --------------------------------------------------- | -------- | ------------------------------------------------------------------------------------ |
790
+ | 1 | Export SubagentsService API boundary | Complete | [phase-1-api-boundary.md](history/phase-1-api-boundary.md) |
791
+ | 2 | Remove scheduling subsystem | Complete | [phase-2-remove-scheduling.md](history/phase-2-remove-scheduling.md) |
792
+ | 3 | Remove group-join, RPC; replace output-file | Complete | [phase-3-remove-rpc-groupjoin.md](history/phase-3-remove-rpc-groupjoin.md) |
793
+ | 4 | Implement and publish SubagentsService | Complete | [phase-4-implement-service.md](history/phase-4-implement-service.md) |
794
+ | 5 | Decompose index.ts | Complete | [phase-5-decompose-index.md](history/phase-5-decompose-index.md) |
795
+ | 6 | Extract UI to separate package | Deferred | — |
796
+ | 7 | Encapsulation and dependency narrowing | Complete | [phase-7-encapsulation.md](history/phase-7-encapsulation.md) |
797
+ | 8 | Testability, display extraction, menu decomposition | Complete | [phase-8-testability.md](history/phase-8-testability.md) |
798
+ | 9 | Observation consolidation, ctx elimination | Complete | [phase-9-observation-ctx.md](history/phase-9-observation-ctx.md) |
799
+ | 10 | Domain organization, bag decomposition, complexity | Complete | [phase-10-structural-decomposition.md](history/phase-10-structural-decomposition.md) |
703
800
 
704
801
  ### Structural refactoring issues
705
802
 
706
- | Phase | Issue | Summary |
707
- | ------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
708
- | Foundation | #69, #71, #76, #80 | SubagentRuntime, pure assembler, cwd injection, config consolidation |
709
- | Core decomposition | #84, #72, #87, #70 | WorktreeManager, AgentManager DI, runtime methods, handler extraction |
710
- | Interface polish | #66, #77 | SDK types, projectAgentsDir |
711
- | Features | #61 | JSONL session transcripts |
712
- | AgentManager | #98, #99, #100, #102 | Record state machine, ParentSnapshot, session-event observation, test factory |
713
- | Encapsulation | #108, #109, #110, #111, #112, #113, #114, #115, #116, #118 | Registry, settings, activity tracker, record lifecycle, observer, spawn options, deps narrowing, tool split, type housekeeping |
714
- | Testability | #131, #132, #133, #134, #135, #136 | Shared fixtures, session-config IO, runner SDK boundary, as-any reduction, display extraction, menu decomposition |
715
- | Observation/ctx | #144, #145, #146, #147, #148 | Observation consolidation, execute decomposition, UI context, text wrapping injection, widget rendering split |
716
- | Phase 10 | #164, #166, #167, #168, #169, #170, #171 | Domain directories, ParentSessionInfo, RunnerIO split, ToolFilterConfig, RunContext, buildContentLines, renderResult |
803
+ | Phase | Issue | Summary |
804
+ | ------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
805
+ | Foundation | #69, #71, #76, #80 | SubagentRuntime, pure assembler, cwd injection, config consolidation |
806
+ | Core decomposition | #84, #72, #87, #70 | WorktreeManager, AgentManager DI, runtime methods, handler extraction |
807
+ | Interface polish | #66, #77 | SDK types, projectAgentsDir |
808
+ | Features | #61 | JSONL session transcripts |
809
+ | AgentManager | #98, #99, #100, #102 | Record state machine, ParentSnapshot, session-event observation, test factory |
810
+ | Encapsulation | #108, #109, #110, #111, #112, #113, #114, #115, #116, #118 | Registry, settings, activity tracker, record lifecycle, observer, spawn options, deps narrowing, tool split, type housekeeping |
811
+ | Testability | #131, #132, #133, #134, #135, #136 | Shared fixtures, session-config IO, runner SDK boundary, as-any reduction, display extraction, menu decomposition |
812
+ | Observation/ctx | #144, #145, #146, #147, #148 | Observation consolidation, execute decomposition, UI context, text wrapping injection, widget rendering split |
813
+ | Phase 10 | #164, #165, #166, #167, #168, #169, #170, #171, #172 | Domain directories, ResolvedSpawnConfig, ParentSessionInfo, RunnerIO split, ToolFilterConfig, RunContext, buildContentLines, renderResult, content-items |
717
814
 
718
815
  The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
719
816
 
@@ -733,12 +830,12 @@ The upstream test suite is run periodically as a regression canary for the agent
733
830
  [gotgenes/pi-packages]: https://github.com/gotgenes/pi-packages
734
831
  [tintinweb/pi-subagents]: https://github.com/tintinweb/pi-subagents
735
832
 
736
- [164]: https://github.com/gotgenes/pi-packages/issues/164
737
- [165]: https://github.com/gotgenes/pi-packages/issues/165
738
833
  [166]: https://github.com/gotgenes/pi-packages/issues/166
739
834
  [167]: https://github.com/gotgenes/pi-packages/issues/167
740
835
  [168]: https://github.com/gotgenes/pi-packages/issues/168
741
836
  [169]: https://github.com/gotgenes/pi-packages/issues/169
742
- [170]: https://github.com/gotgenes/pi-packages/issues/170
743
- [171]: https://github.com/gotgenes/pi-packages/issues/171
744
- [172]: https://github.com/gotgenes/pi-packages/issues/172
837
+ [192]: https://github.com/gotgenes/pi-packages/issues/192
838
+ [193]: https://github.com/gotgenes/pi-packages/issues/193
839
+ [194]: https://github.com/gotgenes/pi-packages/issues/194
840
+ [195]: https://github.com/gotgenes/pi-packages/issues/195
841
+ [196]: https://github.com/gotgenes/pi-packages/issues/196
@@ -0,0 +1,141 @@
1
+ # Phase 10: Domain organization, bag decomposition, and complexity reduction
2
+
3
+ Target: reorganize source into domain directories; decompose remaining wide parameter bags into focused value objects; reduce cyclomatic complexity in rendering functions; eliminate production code duplication.
4
+
5
+ ## Current smells
6
+
7
+ | Smell | Location | Evidence | Severity |
8
+ | ------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -------- |
9
+ | Flat `src/` directory | `src/` root | 20+ files in a single directory with no domain grouping; hard to see module boundaries | Medium |
10
+ | `ResolvedSpawnConfig` is a 15-field bag | `spawn-config.ts` | Mixes identity (name, slug), execution (model, permissions), and presentation (icon, color) concerns in one interface | Medium |
11
+ | `AgentSpawnConfig` threads parent session fields | `agent-manager.ts` | `parentSessionFile`, `parentSessionId`, `toolCallId` always travel together but are separate parameters | Low |
12
+ | `RunnerIO` is too wide | `foreground-runner.ts` | 9 methods spanning environment concerns (cwd, env) and session factory concerns (create, attach, resume) | Medium |
13
+ | `SessionConfig` mixes tool filtering with session setup | `session-config.ts` | `toolNames`, `disallowedSet`, `extensions` are a cohesive filter group buried in a larger config | Low |
14
+ | `RunOptions` is a 12-field bag | `foreground-runner.ts` | Mixes execution context (exec, registry, cwd) with per-run options (model, prompt, permissions) | Medium |
15
+ | `buildContentLines` high complexity | `ui/build-content-lines.ts` | ~60-line function with a switch over every content type; each branch is an independent formatter | Medium |
16
+ | `renderResult` high complexity | `tools/agent-tool.ts` | ~80-line function with status-based branching; each status is an independent rendering concern | Medium |
17
+ | Duplicated turn-formatting logic | `session/session-runner.ts`, `tools/agent-tool.ts` | 18 lines of identical `ToolCallContent` extraction and `getToolCallName` logic in two production files | Low |
18
+
19
+ ## Step 1: Reorganize source into domain directories (#164)
20
+
21
+ Moved files into `config/`, `session/`, `lifecycle/`, `observation/`, and `service/` subdirectories.
22
+ All `src/` internal imports now use `#src/` path aliases.
23
+ Root `src/` reduced to 5 files + 8 directories.
24
+
25
+ Impact: domain boundaries are visible in the directory tree; imports communicate intent via path alias.
26
+
27
+ ## Step 2: Decompose ResolvedSpawnConfig (#165)
28
+
29
+ Split the 15-field `ResolvedSpawnConfig` into three focused value objects:
30
+
31
+ - `SpawnIdentity` — name, slug, key
32
+ - `SpawnExecution` — model, permissions, tools, prompt, systemPrompt
33
+ - `SpawnPresentation` — icon, color, description
34
+
35
+ `ResolvedSpawnConfig` composes these three plus the remaining spawn-level fields.
36
+
37
+ Impact: each consumer receives only the fields it needs; adding a presentation field no longer touches execution code.
38
+
39
+ ## Step 3: Extract ParentSessionInfo from AgentSpawnConfig (#166)
40
+
41
+ Extracted `parentSessionFile`, `parentSessionId`, and `toolCallId` into `ParentSessionInfo`.
42
+ `AgentSpawnConfig` and `AgentManager.spawn()` accept the single value object instead of three separate parameters.
43
+
44
+ Impact: eliminated parameter co-travel; the three fields that always move together are now a single concept.
45
+
46
+ ## Step 4: Narrow RunnerIO (#167)
47
+
48
+ Split `RunnerIO` into two focused interfaces:
49
+
50
+ - `EnvironmentIO` (3 methods) — cwd, env, platform concerns
51
+ - `SessionFactoryIO` (5+1 methods) — create, attach, resume, and related session lifecycle
52
+
53
+ `RunnerIO` kept as a backward-compatible type alias (`EnvironmentIO & SessionFactoryIO`).
54
+
55
+ Impact: consumers declare which IO surface they actually need; test doubles shrink to the relevant subset.
56
+
57
+ ## Step 5: Extract ToolFilterConfig from SessionConfig (#168)
58
+
59
+ Grouped `toolNames`, `disallowedSet`, and `extensions` into `ToolFilterConfig`.
60
+ `filterActiveTools` accepts a single `ToolFilterConfig` argument instead of three separate parameters.
61
+
62
+ Impact: tool-filtering concern is encapsulated; `SessionConfig` is narrower and more focused.
63
+
64
+ ## Step 6: Extract RunContext from RunOptions (#169)
65
+
66
+ Extracted `exec`, `registry`, `cwd`, and `parentSession` into `RunContext`.
67
+ `RunOptions` reduced from 12 fields to 9 (the 4 extracted fields replaced by a single `context` field, plus the remaining per-run options).
68
+
69
+ Impact: execution context is a reusable value object; `RunOptions` focuses on per-run configuration.
70
+
71
+ ## Step 7: Reduce buildContentLines complexity (#170)
72
+
73
+ Extracted per-content-type formatters into `ui/message-formatters.ts`.
74
+ Each content type (text, tool-use, tool-result, image, etc.) has a dedicated pure function.
75
+ `buildContentLines` is now a ~30-line dispatch loop that delegates to the appropriate formatter.
76
+
77
+ Impact: cyclomatic complexity of `buildContentLines` dropped from ~15 to ~5; each formatter is independently testable.
78
+
79
+ ## Step 8: Reduce renderResult complexity (#171)
80
+
81
+ Extracted per-status formatters into `tools/result-renderer.ts`.
82
+ Each result status (success, error, timeout, cancelled) has a dedicated pure function.
83
+ `renderResult` reduced from ~80 lines to a 10-line guard that dispatches to the appropriate renderer.
84
+
85
+ Impact: cyclomatic complexity of `renderResult` dropped from ~10 to ~3; status rendering is independently testable.
86
+
87
+ ## Step 9: Extract shared turn-formatting logic (#172)
88
+
89
+ Extracted `ToolCallContent`, `getToolCallName`, and `extractAssistantContent` into `session/content-items.ts`.
90
+ Both `session/session-runner.ts` and `tools/agent-tool.ts` import from the shared module.
91
+
92
+ Impact: eliminated 18 lines of production code duplication; single source of truth for turn-content extraction.
93
+
94
+ ## Step dependencies
95
+
96
+ ```mermaid
97
+ flowchart LR
98
+ subgraph bag["Bag decomposition track"]
99
+ S2["2: Decompose ResolvedSpawnConfig #165"] --> S3["3: Extract ParentSessionInfo #166"]
100
+ S4["4: Narrow RunnerIO #167"]
101
+ S5["5: Extract ToolFilterConfig #168"]
102
+ S6["6: Extract RunContext #169"]
103
+ end
104
+ subgraph complexity["Complexity reduction track"]
105
+ S7["7: Reduce buildContentLines #170"]
106
+ S8["8: Reduce renderResult #171"]
107
+ S9["9: Extract turn-formatting #172"]
108
+ end
109
+ S1["1: Reorganize into domain dirs #164"] --> bag
110
+ S1 --> complexity
111
+ ```
112
+
113
+ Step 1 unblocked all other steps.
114
+ Within the bag decomposition track, Step 2 enabled Step 3.
115
+ The bag decomposition and complexity reduction tracks were independent of each other.
116
+
117
+ ## Impact
118
+
119
+ | Metric | Before | After |
120
+ | ------------------------------ | ------------------------------ | --------------------------------------------- |
121
+ | Health score | ~65 | 75 |
122
+ | `src/` root files | 20+ | 5 files + 8 directories |
123
+ | `ResolvedSpawnConfig` fields | 15 | 3 composed value objects |
124
+ | `RunOptions` fields | 12 | 9 |
125
+ | `RunnerIO` methods | 9 | 3 (`EnvironmentIO`) + 6 (`SessionFactoryIO`) |
126
+ | `buildContentLines` complexity | ~15 | ~5 |
127
+ | `renderResult` lines | ~80 | ~10 |
128
+ | Production duplication | 18 lines | 0 |
129
+ | Test duplication | ~1,400 lines (69 clone groups) | ~1,400 lines (69 clone groups) — not targeted |
130
+
131
+ ## Related issues
132
+
133
+ - #164 — Reorganize source into domain directories
134
+ - #165 — Decompose ResolvedSpawnConfig
135
+ - #166 — Extract ParentSessionInfo from AgentSpawnConfig
136
+ - #167 — Narrow RunnerIO
137
+ - #168 — Extract ToolFilterConfig from SessionConfig
138
+ - #169 — Extract RunContext from RunOptions
139
+ - #170 — Reduce buildContentLines complexity
140
+ - #171 — Reduce renderResult complexity
141
+ - #172 — Extract shared turn-formatting logic
@@ -0,0 +1,107 @@
1
+ ---
2
+ issue: 192
3
+ issue_title: "Define SessionContext narrow interface"
4
+ ---
5
+
6
+ # Define `SessionContext` narrow interface
7
+
8
+ ## Problem Statement
9
+
10
+ `SubagentRuntime.currentCtx` is typed `{ pi: unknown; ctx: unknown }`.
11
+ Every consumer must cast through `as any` to read fields from the SDK context.
12
+ This forces context queries (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) to live as closures in `index.ts` with repeated `as any` casts, rather than as typed methods on the state holder.
13
+
14
+ The SDK exports `ExtensionContext` — the `unknown` typing is a historical choice, not a constraint.
15
+
16
+ ## Goals
17
+
18
+ - Define a narrow `SessionContext` interface in `src/types.ts` capturing the 5 fields `SubagentRuntime` actually reads.
19
+ - Pure additive — no consumers change in this step.
20
+ - Provide the typed foundation for Layer 1 (#193) and subsequent closure-to-class conversion issues.
21
+
22
+ ## Non-Goals
23
+
24
+ - Changing `SubagentRuntime.currentCtx` type (that's #193).
25
+ - Converting closure factories to classes (#195, #196).
26
+ - Removing any `as any` casts from `index.ts` (that's #193).
27
+
28
+ ## Background
29
+
30
+ Phase 11, Layer 0 in `docs/architecture/architecture.md`.
31
+ This is the first step in a 5-issue sequence (issues #192–#196) that converts closure factories to classes, eliminating 44 adapter closures in `index.ts`.
32
+
33
+ The SDK's `ExtensionContext` interface (in `@earendil-works/pi-coding-agent`) is broad — it exposes `ui`, `abort()`, `shutdown()`, `compact()`, etc.
34
+ ISP (Interface Segregation Principle) from `code-design` mandates a narrow interface capturing only what `SubagentRuntime` needs.
35
+
36
+ The 5 fields consumed by runtime (traced from `index.ts` lines 214–223 and `lifecycle/parent-snapshot.ts`):
37
+
38
+ 1. `cwd` — working directory for agent sessions.
39
+ 2. `model` — parent model instance for fallback resolution.
40
+ 3. `modelRegistry` — resolving config model strings.
41
+ 4. `getSystemPrompt()` — system prompt for append-mode agents.
42
+ 5. `sessionManager.getSessionFile()` / `.getSessionId()` / `.getBranch()` — session identification and context inheritance.
43
+
44
+ The local `ModelRegistry` interface (in `src/session/model-resolver.ts`) already exists as a narrow ISP interface.
45
+ `SessionContext` will reference it rather than redeclaring model-registry methods inline.
46
+
47
+ ## Design Overview
48
+
49
+ ```typescript
50
+ import type { ModelRegistry } from "#src/session/model-resolver";
51
+
52
+ /**
53
+ * Narrow interface capturing the 5 ExtensionContext fields SubagentRuntime needs.
54
+ * Avoids coupling runtime to the full SDK ExtensionContext surface.
55
+ */
56
+ export interface SessionContext {
57
+ readonly cwd: string;
58
+ readonly model: unknown;
59
+ readonly modelRegistry: ModelRegistry | undefined;
60
+ getSystemPrompt(): string;
61
+ readonly sessionManager: {
62
+ getSessionFile(): string | undefined;
63
+ getSessionId(): string;
64
+ getBranch(): unknown[];
65
+ };
66
+ }
67
+ ```
68
+
69
+ Design decisions:
70
+
71
+ 1. `model` stays `unknown` — the runtime only passes it through to `resolveModel`; narrowing it gains nothing and would couple to `@earendil-works/pi-ai`'s `Model<Api>` generic.
72
+ 2. `modelRegistry` is `ModelRegistry | undefined` — the SDK type says `ModelRegistry` (non-optional), but `SubagentRuntime.currentCtx` can be undefined, and the architecture doc specifies this signature.
73
+ The `| undefined` reflects reality at the cast boundary (pre-bind, the registry may not exist).
74
+ 3. `sessionManager` uses an inline structural type rather than importing `ReadonlySessionManager` — we only need 3 of its 13 methods; a separate named type would be over-engineering for a nested structural slice.
75
+ 4. `getBranch()` returns `unknown[]` — the runtime passes entries through to `buildParentContext()` which already type-narrows internally.
76
+
77
+ ## Module-Level Changes
78
+
79
+ | File | Change |
80
+ | -------------- | -------------------------------------------------------------------------------------------------------------- |
81
+ | `src/types.ts` | Add `SessionContext` interface export. Add `import type { ModelRegistry }` from `#src/session/model-resolver`. |
82
+
83
+ No other files change — this is pure additive.
84
+
85
+ ## Test Impact Analysis
86
+
87
+ 1. No new unit tests are needed — `SessionContext` is a pure type definition with no runtime behavior.
88
+ 2. No existing tests become redundant.
89
+ 3. A compile-time check (`pnpm run check`) verifies the interface is well-formed and the import resolves.
90
+
91
+ ## TDD Order
92
+
93
+ 1. **Add `SessionContext` interface to `src/types.ts`** — add the interface with its import.
94
+ Verify with `pnpm run check` (type-check passes).
95
+ Commit: `feat(pi-subagents): define SessionContext narrow interface (#192)`
96
+
97
+ ## Risks and Mitigations
98
+
99
+ | Risk | Mitigation |
100
+ | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
101
+ | Interface shape doesn't match real `ExtensionContext` at runtime | Traced all 5 fields against SDK `.d.ts` declarations; shapes align exactly. |
102
+ | Circular import from `types.ts` → `session/model-resolver.ts` | `model-resolver.ts` does not import from `types.ts`; no cycle. |
103
+ | Future SDK changes break the narrow interface | The cast boundary (Layer 1, #193) will be the single enforcement point — structural typing ensures compile-time detection. |
104
+
105
+ ## Open Questions
106
+
107
+ None — the issue's "Proposed change" section fully specifies the interface shape.
@@ -37,3 +37,37 @@ New file `safe-fs.test.ts` was created with 13 tests for the extracted utilities
37
37
  Fix: inline the literal union `"user" | "project" | "local"` directly in `memory.ts` as a local type, so it compiles cleanly until deletion in step 4.
38
38
  - The `SKILL.md` for `package-pi-subagents` also listed `memory.ts` in the session domain table — updated alongside `architecture.md` in the docs commit.
39
39
  - No deviations from the plan other than the two minor bugs above (both self-corrected within the same TDD step).
40
+
41
+ ## Stage: Final Retrospective (2026-05-24T22:47:55Z)
42
+
43
+ ### Session summary
44
+
45
+ Shipped issue #185 as `pi-subagents-v7.0.0`.
46
+ CI passed, issue closed, release-please PR #190 merged.
47
+ Three sessions total: planning, TDD (5 steps / 5 commits), shipping.
48
+
49
+ ### Observations
50
+
51
+ #### What went well
52
+
53
+ - The issue's "Scope" section was precise enough that the planning session required no `ask_user` and the Explore agent's trace matched the final commit diff exactly.
54
+ - Consumers-first, declaration-last ordering kept each commit independently compilable (after the two self-corrected fixes).
55
+ - The `SKILL.md` domain table update was caught naturally during the docs step even though the plan didn't list it.
56
+
57
+ #### What caused friction (agent side)
58
+
59
+ - `missing-context` — In TDD step 1, `memory.ts` was updated to re-export `isUnsafeName` from `safe-fs`, but the function was not imported into `memory.ts`'s own scope.
60
+ `resolveMemoryDir` threw a `ReferenceError` at runtime.
61
+ Impact: one extra test-run cycle (~5 seconds) and a trivial one-line fix; no rework commit.
62
+ - `missing-context` — In TDD step 3, removing `MemoryScope` from `types.ts` broke `memory.ts` (scheduled for deletion in step 4).
63
+ The plan said "consumers-first" but didn't account for the doomed module itself being a consumer of the type.
64
+ Impact: one extra `pnpm run check` cycle and a local type inline; no rework commit.
65
+ Both share the same root cause: incremental deletion plans must account for doomed files' own imports at each intermediate step.
66
+
67
+ #### What caused friction (user side)
68
+
69
+ - None observed — all three sessions ran without user corrections or redirections.
70
+
71
+ ### Changes made
72
+
73
+ 1. Added a TDD planning rule to `.pi/skills/testing/SKILL.md` about accounting for doomed modules' own imports during multi-step deletion.
@@ -0,0 +1,35 @@
1
+ ---
2
+ issue: 192
3
+ issue_title: "Define SessionContext narrow interface"
4
+ ---
5
+
6
+ # Retro: #192 — Define SessionContext narrow interface
7
+
8
+ ## Stage: Planning (2026-05-24T16:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned the pure-additive `SessionContext` interface for `src/types.ts`.
13
+ Traced all 5 consumed fields against the SDK's `ExtensionContext` type declarations to confirm shape alignment.
14
+ Single TDD step: add the interface and verify with `pnpm run check`.
15
+
16
+ ### Observations
17
+
18
+ - The interface is trivial in scope — one new export with no consumers changing.
19
+ This is intentionally the smallest possible first step to unblock Layer 1 (#193).
20
+ - `ModelRegistry` already exists as a local narrow interface in `src/session/model-resolver.ts`; `SessionContext` imports it rather than redeclaring.
21
+ - `sessionManager` uses an inline structural type (3 methods) rather than importing the SDK's `ReadonlySessionManager` (13 methods) — ISP applies here.
22
+ - No design ambiguity required `ask_user`; the issue's proposed change section was fully specified.
23
+
24
+ ## Stage: Implementation — TDD (2026-05-24T19:55:00Z)
25
+
26
+ ### Session summary
27
+
28
+ Added the `SessionContext` interface to `src/types.ts` with an `import type { ModelRegistry }` from `#src/session/model-resolver`.
29
+ Single compile-time step — no runtime tests needed for a pure type definition.
30
+ Baseline: 53 test files, 848 tests; final: unchanged.
31
+
32
+ ### Observations
33
+
34
+ - Pre-existing lint failure in `docs/architecture/architecture.md` (5 unused MD053 link references for issues #164, #165, #170, #171, #172) was fixed as part of the baseline verification and included in the feat commit.
35
+ - The interface landed exactly as planned — no deviations from the plan's Design Overview.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/service.ts"
package/src/types.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type { ThinkingLevel } from "@earendil-works/pi-ai";
6
6
  import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
7
+ import type { ModelRegistry } from "#src/session/model-resolver";
7
8
 
8
9
 
9
10
  export { AgentRecord } from "#src/lifecycle/agent-record";
@@ -77,6 +78,26 @@ export interface AgentInvocation {
77
78
  isolation?: IsolationMode;
78
79
  }
79
80
 
81
+ /**
82
+ * Narrow shell-exec callback replacing `ExtensionAPI` in `detectEnv()`.
83
+ * Matches the shape of `pi.exec()` without carrying an SDK dependency.
84
+ */
85
+ /**
86
+ * Narrow interface capturing the ExtensionContext fields SubagentRuntime needs.
87
+ * Avoids coupling runtime to the full SDK ExtensionContext surface (ISP).
88
+ */
89
+ export interface SessionContext {
90
+ readonly cwd: string;
91
+ readonly model: unknown;
92
+ readonly modelRegistry: ModelRegistry | undefined;
93
+ getSystemPrompt(): string;
94
+ readonly sessionManager: {
95
+ getSessionFile(): string | undefined;
96
+ getSessionId(): string;
97
+ getBranch(): unknown[];
98
+ };
99
+ }
100
+
80
101
  /**
81
102
  * Narrow shell-exec callback replacing `ExtensionAPI` in `detectEnv()`.
82
103
  * Matches the shape of `pi.exec()` without carrying an SDK dependency.