@gotgenes/pi-subagents 7.3.0 → 7.3.2

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.
@@ -0,0 +1,298 @@
1
+ ---
2
+ issue: 208
3
+ issue_title: "Extract shared test fixtures to reduce test duplication"
4
+ ---
5
+
6
+ # Extract shared test fixtures
7
+
8
+ ## Problem Statement
9
+
10
+ Test duplication across the pi-subagents package is 1,367 lines (7.2%) across 23 files (67 clone groups per fallow analysis).
11
+ The three heaviest clone families are:
12
+
13
+ 1. Runner IO tests (`agent-runner.test.ts` + `agent-runner-extension-tools.test.ts` + `concrete-agent-runner.test.ts`): 60-line shared setup — `createRunnerIO()`, `AgentConfigLookup` stub, `ParentSnapshot` constant.
14
+ 2. Menu/wizard UI tests (`agent-menu.test.ts` + `agent-creation-wizard.test.ts` + `agent-config-editor.test.ts`): 54+51+24 lines — `makeFileOps()`, `makeUI()`, `makeManager()`, `AgentConfig` defaults, `ParentSnapshot` constant.
15
+ 3. `agent-manager.test.ts` internal: 18 clone groups, 210 duplicated lines — repetitive `manager.spawn(...)` call patterns.
16
+
17
+ Issue #131 (closed) already extracted `createMockSession`, `createToolDeps`, and `createTestRecord` into `test/helpers/`.
18
+ This issue targets the remaining duplication families.
19
+
20
+ ## Goals
21
+
22
+ - Extract `createRunnerIO()` and `createAgentLookup()` into `test/helpers/runner-io.ts` — single source of truth for the `RunnerIO` and `AgentConfigLookup` stubs used by runner tests.
23
+ - Extract `makeFileOps()`, `makeMenuUI()`, `makeMenuManager()`, and `createTestAgentConfig()` into `test/helpers/ui-stubs.ts` — shared factories for the three UI test files.
24
+ - Consolidate `agent-manager.test.ts` internal duplication with local helper functions (`spawnBg`, `spawnFg`).
25
+ - Replace local `ParentSnapshot` definitions with the existing `STUB_SNAPSHOT` from `test/helpers/stub-ctx.ts` where possible.
26
+ - Remove stale `buildMemoryBlock` and `buildReadOnlyMemoryBlock` stubs from the `createRunnerIO` factory (these methods no longer exist on `AssemblerIO`).
27
+ - Target: ~250 lines of test duplication removed.
28
+
29
+ ## Non-Goals
30
+
31
+ - Extracting session mock factories from the runner tests — each file's session factory serves a specialized purpose (`createSession(finalText)`, `createSessionWithExtensionToolRegistration(beforeBind, afterBind)`, `makeSession(text)`) and the variance is structural, not incidental.
32
+ - Extracting `makeSettings()` from `agent-menu.test.ts` — only used in one file.
33
+ - Extracting `makeHandler()` / `makeEditor()` / `makeDeps()` wrapper functions that compose collaborator stubs for specific handlers — too tightly coupled to each file's test structure.
34
+ - Extracting the mutable `agentConfigMock` pattern from `agent-runner-extension-tools.test.ts` — its per-test config mutation is inherently local.
35
+ - Decomposing UI complexity (Steps 1–3 of Phase 12, issues #205, #206, #207) — separate issues.
36
+
37
+ ## Background
38
+
39
+ ### Existing test helpers
40
+
41
+ `test/helpers/` already contains shared factories from issue #131:
42
+
43
+ | File | Exports | Used by |
44
+ | ----------------- | ----------------------------------------- | ------------------------------------------------------- |
45
+ | `mock-session.ts` | `createMockSession()`, `toAgentSession()` | agent-manager, record-observer, ui-observer tests |
46
+ | `make-deps.ts` | `createToolDeps()` | agent-tool, background-spawner, foreground-runner tests |
47
+ | `make-record.ts` | `createTestRecord()` | tool tests, UI tests |
48
+ | `stub-ctx.ts` | `STUB_CTX`, `STUB_SNAPSHOT` | tool tests (via `make-deps.ts`) |
49
+
50
+ The new factories follow the same pattern: shared files in `test/helpers/` with optional unit tests.
51
+
52
+ ### Architecture doc reference
53
+
54
+ Phase 12, Step 4 in `docs/architecture/architecture.md` (lines 637–644) calls for `test/fixtures/` modules.
55
+ We use `test/helpers/` instead to follow the existing convention established in issue #131.
56
+ The architecture doc reference will be updated as part of this work.
57
+
58
+ ### Interface shapes
59
+
60
+ `RunnerIO` = `EnvironmentIO & SessionFactoryIO` (in `src/lifecycle/agent-runner.ts`):
61
+
62
+ ```typescript
63
+ interface EnvironmentIO {
64
+ detectEnv: (exec: ShellExec, cwd: string) => Promise<EnvInfo>;
65
+ getAgentDir: () => string;
66
+ deriveSessionDir: (parentSessionFile: string | undefined, cwd: string) => string;
67
+ }
68
+
69
+ interface SessionFactoryIO {
70
+ createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoaderLike;
71
+ createSessionManager: (cwd: string, sessionDir: string) => SessionManagerLike;
72
+ createSettingsManager: (cwd: string, agentDir: string) => SettingsManager;
73
+ createSession: (opts: CreateSessionOptions) => Promise<{ session: AgentSession }>;
74
+ assemblerIO: AssemblerIO;
75
+ }
76
+ ```
77
+
78
+ `AssemblerIO` (in `src/session/session-config.ts`) has only `preloadSkills` and `buildAgentPrompt`.
79
+ The existing test factories in `agent-runner.test.ts` and `agent-runner-extension-tools.test.ts` include stale `buildMemoryBlock` and `buildReadOnlyMemoryBlock` stubs that no longer match the interface — the shared factory will omit them.
80
+
81
+ ### Duplication diff: default values across copies
82
+
83
+ Before extracting, the following differences across copies were identified:
84
+
85
+ **`createRunnerIO` / `makeIO`:**
86
+
87
+ | Field | agent-runner | extension-tools | concrete-agent-runner |
88
+ | -------------------------------------- | --------------- | --------------- | --------------------- |
89
+ | `assemblerIO.buildMemoryBlock` | present (stale) | present (stale) | absent |
90
+ | `assemblerIO.buildReadOnlyMemoryBlock` | present (stale) | absent | absent |
91
+ | Other fields | identical | identical | identical |
92
+
93
+ **`ParentSnapshot` stubs:**
94
+
95
+ | Field | agent-runner | extension-tools | concrete-agent-runner | agent-manager | agent-menu/wizard | STUB_SNAPSHOT |
96
+ | ---------------------------- | --------------- | --------------- | --------------------- | --------------- | ----------------- | ----------------- |
97
+ | `cwd` | "/tmp" | "/tmp" | "/workspace" | "/tmp" | "/test" | "/test" |
98
+ | `systemPrompt` | "parent prompt" | "parent prompt" | "" | "parent prompt" | "" | "test prompt" |
99
+ | `model` | undefined | undefined | {} | undefined | {} | undefined |
100
+ | `modelRegistry.find` | vi.fn() | vi.fn() | vi.fn() | vi.fn() | `() => undefined` | `() => undefined` |
101
+ | `modelRegistry.getAvailable` | vi.fn() | vi.fn() | — | — | — | — |
102
+
103
+ None of the consumer tests assert on `cwd`, `systemPrompt`, `model`, or `modelRegistry.find` return values — these fields are passed through to functions that are already mocked.
104
+ Using `STUB_SNAPSHOT` is safe for all consumers.
105
+
106
+ **`makeFileOps`:** character-for-character identical in all three UI test files.
107
+
108
+ **`makeUI`:** identical core in wizard and config-editor; agent-menu wraps it in an outer `{ ui, modelRegistry, parentSnapshot }` object.
109
+
110
+ **`makeManager`:** identical in agent-menu and wizard.
111
+
112
+ **`testDefaultAgentConfig`:** identical in agent-menu and config-editor (same 9 fields).
113
+
114
+ ## Design Overview
115
+
116
+ ### `test/helpers/runner-io.ts`
117
+
118
+ Two exports:
119
+
120
+ ```typescript
121
+ /** Shared RunnerIO stub factory for agent-runner tests. */
122
+ function createRunnerIO(assemblerOverrides?: Partial<AssemblerIOStub>): RunnerIOStub;
123
+
124
+ /** Shared AgentConfigLookup stub. Returns a static Explore config by default. */
125
+ function createAgentLookup(config?: Partial<AgentConfig>): AgentLookupStub;
126
+ ```
127
+
128
+ `createRunnerIO` builds the full `RunnerIO` stub shape.
129
+ The `assemblerIO` sub-object defaults to stubs for `preloadSkills` and `buildAgentPrompt` only (matching the current `AssemblerIO` interface).
130
+ `assemblerOverrides` lets tests customize individual methods without rebuilding the entire factory.
131
+
132
+ `createAgentLookup` returns `{ resolveAgentConfig, getToolNamesForType }` wrapping a static config.
133
+ The default config is the Explore agent used in `agent-runner.test.ts` and `concrete-agent-runner.test.ts`.
134
+ Tests that need per-test config mutation (extension-tools) keep their local mutable wrapper but use the default config as a starting point.
135
+
136
+ Return types are deliberately unannotated (per testing skill) so `vi.fn()` stubs retain their `Mock<...>` methods.
137
+
138
+ ### `test/helpers/ui-stubs.ts`
139
+
140
+ Four exports:
141
+
142
+ ```typescript
143
+ /** FileOps stub — identical across all three UI test files. */
144
+ function makeFileOps(): FileOpsStub;
145
+
146
+ /** MenuUI stub with sequential select responses. */
147
+ function makeMenuUI(selectResults?: (string | undefined)[]): MenuUIStub;
148
+
149
+ /** Manager stub for UI tests (listAgents, getRecord, spawnAndWait). */
150
+ function makeMenuManager(): MenuManagerStub;
151
+
152
+ /** AgentConfig factory with sensible defaults and override support. */
153
+ function createTestAgentConfig(overrides?: Partial<AgentConfig>): AgentConfig;
154
+ ```
155
+
156
+ `makeMenuUI` returns the flat UI shape (select, input, confirm, editor, notify, custom).
157
+ `agent-menu.test.ts` wraps this locally into its `{ ui, modelRegistry, parentSnapshot }` structure — the wrapping stays in the test file because it's specific to `AgentsMenuHandler`'s interface.
158
+
159
+ ### `agent-manager.test.ts` local helpers
160
+
161
+ Two local helpers reduce the 42 repetitive `manager.spawn(...)` calls:
162
+
163
+ ```typescript
164
+ function spawnBg(mgr: AgentManager, prompt = "test", desc = prompt) {
165
+ return mgr.spawn(STUB_SNAPSHOT, "general-purpose", prompt, {
166
+ description: desc,
167
+ isBackground: true,
168
+ });
169
+ }
170
+
171
+ function spawnFg(mgr: AgentManager, prompt = "test", desc = prompt) {
172
+ return mgr.spawnAndWait(STUB_SNAPSHOT, "general-purpose", prompt, {
173
+ description: desc,
174
+ });
175
+ }
176
+ ```
177
+
178
+ These stay local because they are only needed in this file.
179
+ They also replace the local `mockSnapshot` with the imported `STUB_SNAPSHOT`.
180
+
181
+ ## Module-Level Changes
182
+
183
+ ### New files
184
+
185
+ 1. `test/helpers/runner-io.ts` — exports `createRunnerIO()`, `createAgentLookup()`.
186
+ 2. `test/helpers/runner-io.test.ts` — unit tests: verifies stub shape satisfies `RunnerIO`, override merging, default config shape.
187
+ 3. `test/helpers/ui-stubs.ts` — exports `makeFileOps()`, `makeMenuUI()`, `makeMenuManager()`, `createTestAgentConfig()`.
188
+ 4. `test/helpers/ui-stubs.test.ts` — unit tests: verifies stub shapes, sequential select behavior, config override merging.
189
+
190
+ ### Modified files
191
+
192
+ 1. `test/lifecycle/agent-runner.test.ts` — remove local `createRunnerIO()`, `mockAgentLookup`, `snapshot`, `exec`; import from helpers.
193
+ 2. `test/lifecycle/agent-runner-extension-tools.test.ts` — remove local `createRunnerIO()`, `snapshot`; import from helpers; keep local mutable `agentConfigMock` and `mockAgentLookup` (they use the mutable wrapper pattern).
194
+ 3. `test/lifecycle/concrete-agent-runner.test.ts` — remove local `makeIO()`, `registry`, `snapshot`; import from helpers.
195
+ 4. `test/lifecycle/agent-manager.test.ts` — remove local `mockSnapshot`; import `STUB_SNAPSHOT`; add local `spawnBg()` and `spawnFg()` helpers; update all `manager.spawn(mockSnapshot, ...)` calls.
196
+ 5. `test/ui/agent-menu.test.ts` — remove local `makeFileOps()`, `makeManager()`, `testDefaultAgentConfig`, `stubParentSnapshot`; import from helpers; keep local `makeSettings()` and `makeHandler()`.
197
+ 6. `test/ui/agent-creation-wizard.test.ts` — remove local `makeFileOps()`, `makeManager()`, `stubParentSnapshot`; import from helpers; keep local `makeDeps()`.
198
+ 7. `test/ui/agent-config-editor.test.ts` — remove local `makeFileOps()`, `testDefaultConfig`, `testCustomConfig`; import from helpers; keep local `makeEditor()`.
199
+
200
+ ### Doc updates
201
+
202
+ 1. `docs/architecture/architecture.md` — update Phase 12 Step 4 reference from `test/fixtures/` to `test/helpers/` (lines 640, 642).
203
+
204
+ ## Test Impact Analysis
205
+
206
+ 1. The new factory unit tests (`runner-io.test.ts`, `ui-stubs.test.ts`) verify shared fixture behavior that was previously only implicitly tested through consumer test files.
207
+ This enables targeted debugging when a mock shape drifts from the production interface.
208
+ 2. No existing tests become redundant — consumer tests exercise distinct production behavior that the factory tests do not cover.
209
+ 3. All existing tests stay as-is in terms of assertions.
210
+ Only the setup code (local factory → shared import) changes.
211
+ 4. The removal of stale `buildMemoryBlock` and `buildReadOnlyMemoryBlock` stubs from `createRunnerIO` is safe because `AssemblerIO` no longer declares these methods — structural typing means they were always no-ops.
212
+
213
+ ## TDD Order
214
+
215
+ 1. **Red → Green: `createRunnerIO` and `createAgentLookup` factories.**
216
+ Write `test/helpers/runner-io.test.ts` — verify `createRunnerIO()` returns a shape satisfying `RunnerIO` (`EnvironmentIO & SessionFactoryIO`), verify `assemblerIO` override merging, verify `createAgentLookup()` returns the default Explore config and accepts overrides.
217
+ Implement `test/helpers/runner-io.ts`.
218
+ Run: `pnpm vitest run test/helpers/runner-io.test.ts`.
219
+ Commit: `test: add createRunnerIO and createAgentLookup shared test fixtures`
220
+
221
+ 2. **Green: migrate `agent-runner.test.ts` to shared runner-io factories.**
222
+ Import `createRunnerIO`, `createAgentLookup` from helpers.
223
+ Import `STUB_SNAPSHOT` from `stub-ctx.ts`.
224
+ Remove local `createRunnerIO()`, `mockAgentLookup`, `snapshot`.
225
+ Run: `pnpm vitest run test/lifecycle/agent-runner.test.ts`.
226
+ Commit: `test: use shared runner-io fixtures in agent-runner tests`
227
+
228
+ 3. **Green: migrate `agent-runner-extension-tools.test.ts` to shared `createRunnerIO`.**
229
+ Import `createRunnerIO` from helpers.
230
+ Import `STUB_SNAPSHOT` from `stub-ctx.ts`.
231
+ Remove local `createRunnerIO()` and `snapshot`.
232
+ Keep local `agentConfigMock` and `mockAgentLookup` (mutable wrapper pattern).
233
+ Run: `pnpm vitest run test/lifecycle/agent-runner-extension-tools.test.ts`.
234
+ Commit: `test: use shared createRunnerIO in extension-tools tests`
235
+
236
+ 4. **Green: migrate `concrete-agent-runner.test.ts` to shared factories.**
237
+ Import `createRunnerIO`, `createAgentLookup` from helpers.
238
+ Import `STUB_SNAPSHOT` from `stub-ctx.ts`.
239
+ Remove local `makeIO()`, `registry`, `snapshot`.
240
+ Run: `pnpm vitest run test/lifecycle/concrete-agent-runner.test.ts`.
241
+ Commit: `test: use shared runner-io fixtures in concrete-agent-runner tests`
242
+
243
+ 5. **Red → Green: UI stub factories.**
244
+ Write `test/helpers/ui-stubs.test.ts` — verify `makeFileOps()` shape, `makeMenuUI()` sequential select behavior, `makeMenuManager()` shape, `createTestAgentConfig()` default and override merging.
245
+ Implement `test/helpers/ui-stubs.ts`.
246
+ Run: `pnpm vitest run test/helpers/ui-stubs.test.ts`.
247
+ Commit: `test: add shared UI stub factories`
248
+
249
+ 6. **Green: migrate `agent-config-editor.test.ts` to shared UI stubs.**
250
+ Import `makeFileOps`, `makeMenuUI`, `createTestAgentConfig` from helpers.
251
+ Remove local `makeFileOps()`, `makeUI()`, `testDefaultConfig`; derive `testCustomConfig` from `createTestAgentConfig`.
252
+ Run: `pnpm vitest run test/ui/agent-config-editor.test.ts`.
253
+ Commit: `test: use shared UI stubs in agent-config-editor tests`
254
+
255
+ 7. **Green: migrate `agent-creation-wizard.test.ts` to shared UI stubs.**
256
+ Import `makeFileOps`, `makeMenuUI`, `makeMenuManager` from helpers.
257
+ Import `STUB_SNAPSHOT` from `stub-ctx.ts`.
258
+ Remove local `makeFileOps()`, `makeUI()`, `makeManager()`, `stubParentSnapshot`.
259
+ Run: `pnpm vitest run test/ui/agent-creation-wizard.test.ts`.
260
+ Commit: `test: use shared UI stubs in agent-creation-wizard tests`
261
+
262
+ 8. **Green: migrate `agent-menu.test.ts` to shared UI stubs.**
263
+ Import `makeFileOps`, `makeMenuUI`, `makeMenuManager`, `createTestAgentConfig` from helpers.
264
+ Import `STUB_SNAPSHOT` from `stub-ctx.ts`.
265
+ Remove local `makeFileOps()`, `makeManager()`, `testDefaultAgentConfig`, `stubParentSnapshot`.
266
+ Adapt `makeHandler()` to wrap `makeMenuUI()` into its `{ ui, modelRegistry, parentSnapshot }` shape.
267
+ Keep local `makeSettings()`.
268
+ Run: `pnpm vitest run test/ui/agent-menu.test.ts`.
269
+ Commit: `test: use shared UI stubs in agent-menu tests`
270
+
271
+ 9. **Green: consolidate `agent-manager.test.ts` internal duplication.**
272
+ Import `STUB_SNAPSHOT` from `stub-ctx.ts`.
273
+ Remove local `mockSnapshot`.
274
+ Add local `spawnBg()` and `spawnFg()` helpers.
275
+ Update all ~42 `manager.spawn(mockSnapshot, ...)` calls to use `spawnBg()`.
276
+ Update `spawnAndWait` calls to use `spawnFg()`.
277
+ Run: `pnpm vitest run test/lifecycle/agent-manager.test.ts`.
278
+ Commit: `test: consolidate agent-manager spawn patterns with local helpers`
279
+
280
+ 10. **Docs: update architecture doc reference.**
281
+ Update `docs/architecture/architecture.md` Phase 12 Step 4 to reference `test/helpers/` instead of `test/fixtures/`.
282
+ Commit: `docs: update Phase 12 Step 4 to reference test/helpers/`
283
+
284
+ ## Risks and Mitigations
285
+
286
+ | Risk | Mitigation |
287
+ | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
288
+ | `STUB_SNAPSHOT` shape differs from local snapshots (`cwd`, `model`, `systemPrompt` values) | Verified: no consumer tests assert on these field values. The snapshot is passed through to fully-mocked functions. Run full test suite after each migration step. |
289
+ | Removing stale `buildMemoryBlock`/`buildReadOnlyMemoryBlock` stubs breaks a test that somehow depends on them | These methods don't exist on `AssemblerIO` — any code accessing them would be a TypeScript error in production. Vitest's esbuild won't catch this, so run `pnpm run check` after step 2. |
290
+ | Wider mock shape in `createRunnerIO` causes false-positive tests (runner tests pass when they should fail) | The production `RunnerIO` interface is already narrow; extra mock methods are harmless. Existing assertions on specific mock calls catch regressions. |
291
+ | `makeMenuUI` sequential-select pattern breaks when agent-menu wraps it differently | Agent-menu's `makeHandler()` composes the wrapping locally. The shared factory returns only the flat UI shape, avoiding coupling to any specific consumer's wrapping structure. |
292
+ | `agent-manager.test.ts` `spawnBg()` helper hides important spawn options from test readers | Helper uses default values matching the most common pattern. Tests that need non-default options (e.g., `description: "first"`) pass explicit arguments, preserving readability. |
293
+
294
+ ## Open Questions
295
+
296
+ - Should `STUB_SNAPSHOT` be updated to use `vi.fn()` for `modelRegistry.find` instead of `() => undefined`?
297
+ Currently it uses plain functions, but some runner tests use `vi.fn()`.
298
+ Decide during implementation — if no test asserts on `find` call counts, plain functions are fine.
@@ -0,0 +1,261 @@
1
+ ---
2
+ issue: 214
3
+ issue_title: "Convert remaining closure factories to classes (Phase 13, Step 1)"
4
+ ---
5
+
6
+ # Convert remaining closure factories to classes
7
+
8
+ ## Problem Statement
9
+
10
+ Three closure factories survived Phase 11 — each captures dependencies in closure scope and returns a method bag, the exact pattern Phase 11 eliminated elsewhere.
11
+ Converting them to classes makes dependencies explicit as constructor parameters and aligns with the class-based pattern established in Phase 11.
12
+
13
+ ## Goals
14
+
15
+ - Convert `createAgentConfigEditor()` to an `AgentConfigEditor` class.
16
+ - Convert `createAgentCreationWizard()` to an `AgentCreationWizard` class.
17
+ - Convert `createSubagentsService()` to a `SubagentsServiceAdapter` class implementing `SubagentsService`.
18
+ - Remove the `AgentCreationWizardDeps` interface (the class constructor replaces it).
19
+ - Update `AgentsMenuHandler` to store typed class instances instead of `ReturnType<typeof ...>`.
20
+ - Update `index.ts` to construct `SubagentsServiceAdapter` with `new`.
21
+ - 0 remaining closure factories (excluding pure-function factories like `createNotificationRenderer`).
22
+
23
+ ## Non-Goals
24
+
25
+ - Changing any behavior — all conversions are purely structural.
26
+ - Extracting the shared overwrite guard between `AgentConfigEditor` and `AgentCreationWizard` — that is #218.
27
+ - Reducing test duplication in `agent-config-editor.test.ts` — that is #219.
28
+ - Decomposing `buildParentContext` or `startAgent` — those are #215 and #216.
29
+ - Modifying `createNotificationRenderer()` — it returns a pure render function with no captured state.
30
+
31
+ ## Background
32
+
33
+ ### Phase 11 precedent
34
+
35
+ Phase 11 converted all closure factories in tools, runner, and menu layers to classes.
36
+ Issues #195 and #196 established the pattern:
37
+
38
+ - Closure-captured deps become constructor parameters stored as `private readonly` fields.
39
+ - Nested functions become private methods.
40
+ - Consumer call sites change from `createFoo(deps)` to `new Foo(deps)`.
41
+ - Tests update `makeXxx()` helpers to use `new` instead of calling the factory.
42
+
43
+ ### Current state
44
+
45
+ | Factory | File | Captures | Returns |
46
+ | ----------------------------- | ----------------------------- | ---------------------------------------- | ------------------------------------------- |
47
+ | `createAgentConfigEditor()` | `ui/agent-config-editor.ts` | `fileOps`, `registry`, 2 dirs | `{ showAgentDetail }` (7 nested async fns) |
48
+ | `createAgentCreationWizard()` | `ui/agent-creation-wizard.ts` | `fileOps`, `manager`, `registry`, 2 dirs | `{ showCreateWizard }` (3 nested async fns) |
49
+ | `createSubagentsService()` | `service/service-adapter.ts` | `manager`, `resolveModel`, `runtime` | 7-method `SubagentsService` |
50
+
51
+ ### Consumer sites
52
+
53
+ - `AgentsMenuHandler` (in `agent-menu.ts`) constructs both UI factories in its constructor and stores them as `private readonly` fields typed as `ReturnType<typeof createAgentConfigEditor>` and `ReturnType<typeof createAgentCreationWizard>`.
54
+ - `index.ts` calls `createSubagentsService(manager, resolveModel, runtime)` and passes the result to `publishSubagentsService`.
55
+
56
+ ### Structural typing
57
+
58
+ `AgentCreationWizardDeps` is only used within `agent-creation-wizard.ts` — no external imports.
59
+ The class constructor replaces the deps interface entirely.
60
+
61
+ ## Design Overview
62
+
63
+ ### AgentConfigEditor
64
+
65
+ Deps become positional constructor parameters (matching the factory's positional signature):
66
+
67
+ ```typescript
68
+ export class AgentConfigEditor {
69
+ constructor(
70
+ private readonly fileOps: AgentFileOps,
71
+ private readonly registry: AgentTypeRegistry,
72
+ private readonly personalAgentsDir: string,
73
+ private readonly projectAgentsDir: string,
74
+ ) {}
75
+
76
+ private agentDirs(): string[] { ... }
77
+ async showAgentDetail(ui: MenuUI, name: string): Promise<void> { ... }
78
+ private async handleEdit(ui: MenuUI, name: string, file: string): Promise<void> { ... }
79
+ private async handleDelete(ui: MenuUI, name: string, file: string): Promise<void> { ... }
80
+ private async handleReset(ui: MenuUI, name: string, file: string): Promise<void> { ... }
81
+ private async ejectAgent(ui: MenuUI, name: string, cfg: AgentConfig): Promise<void> { ... }
82
+ private async disableAgent(ui: MenuUI, name: string): Promise<void> { ... }
83
+ private async enableAgent(ui: MenuUI, name: string): Promise<void> { ... }
84
+ }
85
+ ```
86
+
87
+ The pure helper functions `buildMenuOptions` and `buildEjectContent` remain as exported free functions — they have no captured state and their tests exercise them independently.
88
+
89
+ ### AgentCreationWizard
90
+
91
+ Deps become positional constructor parameters (dissolving `AgentCreationWizardDeps`):
92
+
93
+ ```typescript
94
+ export class AgentCreationWizard {
95
+ constructor(
96
+ private readonly fileOps: AgentFileOps,
97
+ private readonly manager: WizardManager,
98
+ private readonly registry: WizardRegistry,
99
+ private readonly personalAgentsDir: string,
100
+ private readonly projectAgentsDir: string,
101
+ ) {}
102
+
103
+ async showCreateWizard(ui: MenuUI, parentSnapshot: ParentSnapshot): Promise<void> { ... }
104
+ private async showGenerateWizard(ui: MenuUI, parentSnapshot: ParentSnapshot, targetDir: string): Promise<void> { ... }
105
+ private async showManualWizard(ui: MenuUI, targetDir: string): Promise<void> { ... }
106
+ }
107
+ ```
108
+
109
+ `WizardManager` and `WizardRegistry` interfaces remain exported — they define the narrow contracts for the class's collaborators.
110
+
111
+ ### SubagentsServiceAdapter
112
+
113
+ The class implements `SubagentsService` directly:
114
+
115
+ ```typescript
116
+ export class SubagentsServiceAdapter implements SubagentsService {
117
+ constructor(
118
+ private readonly manager: AgentManagerLike,
119
+ private readonly resolveModel: (input: string, registry: ModelRegistry) => unknown,
120
+ private readonly runtime: ServiceRuntimeLike,
121
+ ) {}
122
+
123
+ spawn(type: string, prompt: string, options?: SpawnOptions): string { ... }
124
+ getRecord(id: string): SubagentRecord | undefined { ... }
125
+ listAgents(): SubagentRecord[] { ... }
126
+ abort(id: string): boolean { ... }
127
+ async steer(id: string, message: string): Promise<boolean> { ... }
128
+ async waitForAll(): Promise<void> { ... }
129
+ hasRunning(): boolean { ... }
130
+ }
131
+ ```
132
+
133
+ `AgentManagerLike` and `ServiceRuntimeLike` interfaces remain exported — they define the narrow contracts.
134
+ The `toSubagentRecord` helper remains an exported free function — it is pure and tested independently.
135
+
136
+ ### AgentsMenuHandler updates
137
+
138
+ The `editor` and `wizard` fields change from `ReturnType<typeof ...>` to the concrete class types:
139
+
140
+ ```typescript
141
+ private readonly editor: AgentConfigEditor;
142
+ private readonly wizard: AgentCreationWizard;
143
+ ```
144
+
145
+ Construction changes from `createAgentConfigEditor(...)` to `new AgentConfigEditor(...)` and from `createAgentCreationWizard({...})` to `new AgentCreationWizard(...)`.
146
+
147
+ ## Module-Level Changes
148
+
149
+ ### `src/ui/agent-config-editor.ts`
150
+
151
+ - Replace `createAgentConfigEditor` factory function with `AgentConfigEditor` class.
152
+ - `agentDirs()` becomes a private method.
153
+ - `showAgentDetail`, `handleEdit`, `handleDelete`, `handleReset`, `ejectAgent`, `disableAgent`, `enableAgent` become class methods (`showAgentDetail` is public, rest are private).
154
+ - `buildMenuOptions` and `buildEjectContent` remain as exported free functions.
155
+
156
+ ### `src/ui/agent-creation-wizard.ts`
157
+
158
+ - Replace `createAgentCreationWizard` factory function with `AgentCreationWizard` class.
159
+ - Remove `AgentCreationWizardDeps` interface.
160
+ - `showCreateWizard` becomes a public method; `showGenerateWizard` and `showManualWizard` become private methods.
161
+ - `WizardManager` and `WizardRegistry` interfaces remain exported.
162
+
163
+ ### `src/service/service-adapter.ts`
164
+
165
+ - Replace `createSubagentsService` factory function with `SubagentsServiceAdapter` class implementing `SubagentsService`.
166
+ - `AgentManagerLike` and `ServiceRuntimeLike` interfaces remain exported.
167
+ - `toSubagentRecord` remains an exported free function.
168
+ - Add import for `SpawnOptions` from `#src/service/service` (needed for the `spawn` method signature).
169
+
170
+ ### `src/ui/agent-menu.ts`
171
+
172
+ - Replace `ReturnType<typeof createAgentConfigEditor>` with `AgentConfigEditor`.
173
+ - Replace `ReturnType<typeof createAgentCreationWizard>` with `AgentCreationWizard`.
174
+ - Update constructor body: `new AgentConfigEditor(...)` instead of `createAgentConfigEditor(...)`.
175
+ - Update constructor body: `new AgentCreationWizard(...)` instead of `createAgentCreationWizard({...})`.
176
+ - Update imports: `AgentConfigEditor` instead of `createAgentConfigEditor`, `AgentCreationWizard` instead of `createAgentCreationWizard`.
177
+
178
+ ### `src/index.ts`
179
+
180
+ - Replace `createSubagentsService(manager, resolveModel, runtime)` with `new SubagentsServiceAdapter(manager, resolveModel, runtime)`.
181
+ - Update import: `SubagentsServiceAdapter` instead of `createSubagentsService`.
182
+
183
+ ### `docs/architecture/architecture.md`
184
+
185
+ - Mark Step 1 of Phase 13 as complete.
186
+ - Update the factory table to show conversions done.
187
+
188
+ ## Test Impact Analysis
189
+
190
+ ### `test/ui/agent-config-editor.test.ts`
191
+
192
+ - Update `makeEditor()` helper: replace `createAgentConfigEditor(...)` with `new AgentConfigEditor(...)`.
193
+ - Update import: `AgentConfigEditor` instead of `createAgentConfigEditor`.
194
+ - No test logic changes — all existing assertions remain valid.
195
+ - The `makeEditor()` helper centralizes the factory call, so only 1 line changes.
196
+
197
+ ### `test/ui/agent-creation-wizard.test.ts`
198
+
199
+ - Replace all `createAgentCreationWizard(deps)` calls with `new AgentCreationWizard(deps.fileOps, deps.manager, deps.registry, deps.personalAgentsDir, deps.projectAgentsDir)`.
200
+ - Update import: `AgentCreationWizard` instead of `createAgentCreationWizard`.
201
+ - This file has ~18 call sites that each construct the wizard inline; each changes from `createAgentCreationWizard(deps)` to `new AgentCreationWizard(deps.fileOps, deps.manager, deps.registry, deps.personalAgentsDir, deps.projectAgentsDir)`.
202
+ - Alternatively, add a `makeWizard(deps)` helper to centralize construction, then each call site becomes `makeWizard(deps)`.
203
+ - No test logic changes.
204
+
205
+ ### `test/service/service-adapter.test.ts`
206
+
207
+ - Replace all `createSubagentsService(manager, resolveModel, runtime)` calls with `new SubagentsServiceAdapter(manager, resolveModel, runtime)`.
208
+ - Update import: `SubagentsServiceAdapter` instead of `createSubagentsService`.
209
+ - ~12 call sites change; each is a mechanical find-and-replace.
210
+ - Update `describe` block names from `createSubagentsService —` to `SubagentsServiceAdapter —`.
211
+ - No test logic changes.
212
+
213
+ ### No new tests needed
214
+
215
+ The conversions are structural — existing tests fully cover all behavior.
216
+ Adding "verify it's a class" tests would test the language, not the code.
217
+
218
+ ## TDD Order
219
+
220
+ 1. **Convert `createAgentConfigEditor` to `AgentConfigEditor` class.**
221
+ Replace factory with class in `agent-config-editor.ts`.
222
+ Update `makeEditor()` in `agent-config-editor.test.ts`.
223
+ Update `agent-menu.ts` field types and constructor.
224
+ Remove factory function.
225
+ Verify: `pnpm vitest run test/ui/agent-config-editor.test.ts` and `pnpm run check`.
226
+ `refactor: convert createAgentConfigEditor to AgentConfigEditor class`
227
+
228
+ 2. **Convert `createAgentCreationWizard` to `AgentCreationWizard` class.**
229
+ Replace factory with class in `agent-creation-wizard.ts`.
230
+ Remove `AgentCreationWizardDeps` interface.
231
+ Update all call sites in `agent-creation-wizard.test.ts` (add `makeWizard` helper to centralize construction).
232
+ Update `agent-menu.ts` field type and constructor.
233
+ Remove factory function.
234
+ Verify: `pnpm vitest run test/ui/agent-creation-wizard.test.ts` and `pnpm run check`.
235
+ `refactor: convert createAgentCreationWizard to AgentCreationWizard class`
236
+
237
+ 3. **Convert `createSubagentsService` to `SubagentsServiceAdapter` class.**
238
+ Replace factory with class in `service-adapter.ts`.
239
+ Update all call sites in `service-adapter.test.ts`.
240
+ Update `describe` block names.
241
+ Update `index.ts` import and construction.
242
+ Remove factory function.
243
+ Verify: `pnpm vitest run test/service/service-adapter.test.ts` and `pnpm run check`.
244
+ `refactor: convert createSubagentsService to SubagentsServiceAdapter class`
245
+
246
+ 4. **Update architecture doc.**
247
+ Mark Phase 13 Step 1 as complete in `docs/architecture/architecture.md`.
248
+ `docs: mark Phase 13 Step 1 complete`
249
+
250
+ ## Risks and Mitigations
251
+
252
+ | Risk | Mitigation |
253
+ | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
254
+ | `AgentCreationWizard` test has ~18 inline factory calls that all need updating | Add a `makeWizard(deps)` test helper to centralize construction — same pattern used in `agent-config-editor.test.ts`. |
255
+ | `SubagentsServiceAdapter` class might not satisfy `SubagentsService` interface | The class uses `implements SubagentsService`, so `pnpm run check` catches any mismatches at compile time. |
256
+ | Spreading a class instance in tests produces a plain object lacking methods | Not applicable — none of the test files spread factory results. Tests call methods directly on the returned object. |
257
+ | `AgentCreationWizardDeps` removal might break external consumers | Verified: the interface is only used within `agent-creation-wizard.ts` itself. No external imports. |
258
+
259
+ ## Open Questions
260
+
261
+ None — the conversions are mechanical and follow the established Phase 11 pattern.
@@ -54,3 +54,39 @@ Test count went from 868 to 884 (+16 tests across 55 files, up from 54).
54
54
  - The complexity hotspots table in `architecture.md` now has no rows (both `renderWidgetLines` from #205 and `update` from this issue are resolved).
55
55
  The section note was updated to reflect that Phase 12 cleared all critical hotspots.
56
56
  - `pnpm fallow dead-code` (from repo root) passed with no issues.
57
+
58
+ ## Stage: Final Retrospective (2026-05-25T13:15:00Z)
59
+
60
+ ### Session summary
61
+
62
+ Issue #207 shipped as `pi-subagents-v7.3.0` with zero deviations from the revised plan.
63
+ The session covered plan revision, TDD implementation (16 new tests, 868 → 884), shipping, and CI verification.
64
+
65
+ ### Observations
66
+
67
+ #### What went well
68
+
69
+ - The plan revision caught three real design issues before implementation started: ISP violation in the function parameter type, incorrect `dispose` → `clearWidget` delegation, and missing complexity budget.
70
+ Fixing these upfront meant the TDD execution had zero deviations and zero rework.
71
+ - The narrow `AgentSummary` type (3 fields) made test fixtures trivial plain objects — no `createTestRecord` or factory infrastructure needed.
72
+ This validated the ISP improvement concretely.
73
+ - The `dispose` independence decision (Sandi Metz principle applied to lifecycle semantics) kept `dispose` at its current 10-line simplicity while `clearWidget` got its own guarded teardown logic.
74
+
75
+ #### What caused friction (agent side)
76
+
77
+ - `missing-context` — The original planning session (prior to this one) used `WidgetAgent[]` as the input type without checking which fields `assembleWidgetState` actually reads.
78
+ The `code-design` skill already says "do not pass a shared dependency bag to functions that only use a subset of it" but the principle wasn't applied to the proposed function signature.
79
+ Impact: required a full plan revision session; no rework in implementation because it was caught before TDD started.
80
+ - `wrong-abstraction` — The original plan proposed `dispose` → `clearWidget` delegation as "eliminating duplication" without evaluating whether the two methods have the same lifecycle semantics.
81
+ `dispose` uses unconditional teardown (shutdown correctness); `clearWidget` uses guarded calls (avoiding redundant SDK calls during repeated timer ticks).
82
+ Impact: same as above — caught in revision, no implementation rework.
83
+
84
+ #### What caused friction (user side)
85
+
86
+ - The user's "I don't yet trust the plan" intervention was the key moment that improved the design.
87
+ Without it, the plan would have been implemented with the wider type and the `dispose` delegation.
88
+ This was effective judgment — the user identified that a mechanical plan for a mechanical refactoring still warranted critical design review.
89
+
90
+ ### Changes made
91
+
92
+ 1. Added two sentences to `.pi/prompts/plan-issue.md` Design Overview section: ISP check for new function parameter types, and structural-duplication check when consolidating methods into a shared helper.