@gotgenes/pi-subagents 4.0.0 → 4.1.1

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,32 @@ 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
+ ## [4.1.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v4.1.0...pi-subagents-v4.1.1) (2026-05-18)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * plan decompose index.ts into tool + menu modules ([#54](https://github.com/gotgenes/pi-packages/issues/54)) ([7adf954](https://github.com/gotgenes/pi-packages/commit/7adf954f37800ace0bcc9d5eb65045e2e133e4f2))
14
+ * **retro:** add retro notes for issue [#53](https://github.com/gotgenes/pi-packages/issues/53) ([f8ca910](https://github.com/gotgenes/pi-packages/commit/f8ca9101576eaad8639d1bb2579f0e631a075038))
15
+
16
+ ## [4.1.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v4.0.0...pi-subagents-v4.1.0) (2026-05-18)
17
+
18
+
19
+ ### Features
20
+
21
+ * add resolveInvocationModel to model-resolver ([462b519](https://github.com/gotgenes/pi-packages/commit/462b5194fdfdf86d8d2d166a99472c651e00b76b))
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * remove quotes from rumdl glob patterns in lint:md script ([a8a0c62](https://github.com/gotgenes/pi-packages/commit/a8a0c62feb2fc45cf68cd7d777259dc159de671b))
27
+
28
+
29
+ ### Documentation
30
+
31
+ * plan extract model resolution from Agent.execute ([#53](https://github.com/gotgenes/pi-packages/issues/53)) ([4c07a47](https://github.com/gotgenes/pi-packages/commit/4c07a474f9f25043a2fa3a4f2829e97eb9bb7666))
32
+ * **retro:** add retro notes for issue [#48](https://github.com/gotgenes/pi-packages/issues/48) ([f244c04](https://github.com/gotgenes/pi-packages/commit/f244c04c64f768e724e89d77962f2fb63715b998))
33
+
8
34
  ## [4.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v3.0.0...pi-subagents-v4.0.0) (2026-05-17)
9
35
 
10
36
 
@@ -0,0 +1,181 @@
1
+ ---
2
+ issue: 53
3
+ issue_title: "refactor: extract model resolution from Agent.execute"
4
+ ---
5
+
6
+ # Extract model resolution from Agent.execute
7
+
8
+ ## Problem Statement
9
+
10
+ The `Agent` tool's `execute` callback in `index.ts` contains inline model-resolution logic (~lines 660–670) that determines which model an agent runs with.
11
+ This block checks `resolvedConfig.modelInput`, calls `resolveModel()`, distinguishes error strings from resolved model instances, and silently falls back to the parent model for config-specified models that fail resolution.
12
+ The logic is not independently testable — it is only exercised through integration-level agent spawning.
13
+
14
+ A second, simpler call site in `getModelLabel()` (~line 1043) also calls `resolveModel()` inline but only checks whether the model resolves; it does not need the same fallback semantics.
15
+
16
+ ## Goals
17
+
18
+ - Extract the inline model-resolution block from `Agent.execute` into a named, unit-testable function in `model-resolver.ts`.
19
+ - Keep the existing `resolveModel()` function unchanged — the new function composes it.
20
+ - No behavior change: model-resolution priority and fallback semantics remain identical.
21
+
22
+ ## Non-Goals
23
+
24
+ - Changing the `resolveModel()` fuzzy-matching algorithm.
25
+ - Refactoring the `getModelLabel()` call site (~line 1043) — it has different semantics (display-only, no fallback) and does not benefit from the same extraction.
26
+ - Refactoring `service-adapter.ts` model resolution — it already uses a clean injected-dependency pattern.
27
+ - Changing any public API surface.
28
+
29
+ ## Background
30
+
31
+ ### Existing modules
32
+
33
+ | Module | Role |
34
+ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
+ | `model-resolver.ts` | Exports `resolveModel(input, registry)` — returns a `Model` on success or an error string on failure. |
36
+ | `invocation-config.ts` | Exports `resolveAgentInvocationConfig()` — merges tool params with agent config. Returns `modelInput` (the raw string) and `modelFromParams` (whether the string came from tool params vs. agent config). |
37
+ | `service-adapter.ts` | Already receives `resolveModel` as a dependency via `AdapterDeps`. Its model resolution is simpler (always throw on failure). |
38
+ | `index.ts` | `Agent.execute` contains the inline block. Uses both `modelInput` and `modelFromParams` to decide: (a) return error to user if params-specified model fails, or (b) silently fall back to parent model if config-specified model fails. |
39
+
40
+ ### Relevant constraint from AGENTS.md
41
+
42
+ > Keep modules focused and composable (one concern per file).
43
+
44
+ The new function belongs in `model-resolver.ts` alongside `resolveModel()` since it composes the latter with invocation-level fallback policy.
45
+
46
+ ## Design Overview
47
+
48
+ ### New function signature
49
+
50
+ ```typescript
51
+ interface ModelResolutionResult {
52
+ model: unknown;
53
+ error?: undefined;
54
+ }
55
+
56
+ interface ModelResolutionError {
57
+ model?: undefined;
58
+ error: string;
59
+ }
60
+
61
+ type ModelResolution = ModelResolutionResult | ModelResolutionError;
62
+
63
+ function resolveInvocationModel(
64
+ parentModel: unknown,
65
+ modelInput: string | undefined,
66
+ modelFromParams: boolean,
67
+ registry: ModelRegistry,
68
+ ): ModelResolution;
69
+ ```
70
+
71
+ ### Decision model
72
+
73
+ The function encapsulates the existing three-branch logic:
74
+
75
+ 1. **No `modelInput`** → return `{ model: parentModel }` (inherit parent).
76
+ 2. **`modelInput` resolves** → return `{ model: resolved }`.
77
+ 3. **`modelInput` fails to resolve**:
78
+ - If `modelFromParams` (user typed it) → return `{ error: errorMessage }` so the caller can surface it.
79
+ - If `!modelFromParams` (agent config specified it) → return `{ model: parentModel }` (silent fallback).
80
+
81
+ ### Result shape rationale
82
+
83
+ A discriminated union (`ModelResolution`) with `model` and `error` fields avoids the existing `typeof resolved === "string"` type-narrowing smell.
84
+ The caller in `index.ts` becomes:
85
+
86
+ ```typescript
87
+ const resolution = resolveInvocationModel(
88
+ ctx.model,
89
+ resolvedConfig.modelInput,
90
+ resolvedConfig.modelFromParams,
91
+ ctx.modelRegistry,
92
+ );
93
+ if (resolution.error) return textResult(resolution.error);
94
+ const model = resolution.model;
95
+ ```
96
+
97
+ ### Edge cases
98
+
99
+ - `modelInput` is `undefined` → short-circuit, return parent model.
100
+ - `modelInput` is an empty string → delegates to `resolveModel()`, which currently matches vacuously (documented in existing tests); no change in behavior.
101
+
102
+ ## Module-Level Changes
103
+
104
+ ### `src/model-resolver.ts`
105
+
106
+ - Add `ModelResolutionResult`, `ModelResolutionError`, and `ModelResolution` type exports.
107
+ - Add `resolveInvocationModel()` export.
108
+ - No changes to existing `resolveModel()`, `ModelEntry`, or `ModelRegistry`.
109
+
110
+ ### `src/index.ts`
111
+
112
+ - Update import to include `resolveInvocationModel`.
113
+ - Replace the inline model-resolution block in `Agent.execute` (~lines 660–670) with a call to `resolveInvocationModel()` and a check on the result.
114
+ - Remove the now-unused destructuring of `modelFromParams` from `resolvedConfig` at the call site (it is consumed internally by `resolveInvocationModel` via the parameter).
115
+
116
+ ### `test/model-resolver.test.ts`
117
+
118
+ - Add a new `describe("resolveInvocationModel")` block with tests covering all three branches plus edge cases.
119
+
120
+ ## Test Impact Analysis
121
+
122
+ ### New unit tests enabled
123
+
124
+ The extraction enables direct testing of the three-branch fallback logic (inherit, resolve, fallback-on-config-failure) that was previously only exercisable through full agent spawning.
125
+ Specifically:
126
+
127
+ - Parent model inheritance when no `modelInput` is provided.
128
+ - Successful resolution returns the resolved model.
129
+ - User-specified model failure returns an error.
130
+ - Config-specified model failure silently falls back to parent.
131
+
132
+ ### Existing tests that stay as-is
133
+
134
+ - All existing `resolveModel` tests in `test/model-resolver.test.ts` — they test the lower-level function which is unchanged.
135
+ - Integration-level tests in `test/agent-runner.test.ts` and `test/agent-manager.test.ts` — they exercise model usage through the full agent lifecycle.
136
+ - `test/invocation-config.test.ts` — unchanged module.
137
+ - `test/service-adapter.test.ts` — uses its own injected `resolveModel` dependency, unaffected.
138
+
139
+ ### Tests that become redundant
140
+
141
+ None.
142
+ The inline block was not directly tested anywhere — it was only reached through integration paths that test much more than model resolution.
143
+
144
+ ## TDD Order
145
+
146
+ 1. **Red → Green: parent model inheritance.**
147
+ Test: `resolveInvocationModel` returns `{ model: parentModel }` when `modelInput` is `undefined`.
148
+ Commit: `test: add resolveInvocationModel tests for parent model inheritance`
149
+
150
+ 2. **Red → Green: successful model resolution.**
151
+ Test: returns `{ model: resolvedModel }` when `resolveModel` succeeds (both params-specified and config-specified).
152
+ Commit: `test: add resolveInvocationModel tests for successful resolution`
153
+
154
+ 3. **Red → Green: user-specified model failure.**
155
+ Test: returns `{ error: message }` when `modelFromParams` is `true` and `resolveModel` returns an error string.
156
+ Commit: `test: add resolveInvocationModel tests for param model failure`
157
+
158
+ 4. **Red → Green: config-specified model silent fallback.**
159
+ Test: returns `{ model: parentModel }` when `modelFromParams` is `false` and `resolveModel` returns an error string.
160
+ Commit: `test: add resolveInvocationModel tests for config model fallback`
161
+
162
+ 5. **Green: implement `resolveInvocationModel` in `model-resolver.ts`.**
163
+ All four test cases go green.
164
+ Commit: `feat: add resolveInvocationModel to model-resolver`
165
+
166
+ 6. **Refactor: replace inline block in `index.ts`.**
167
+ Replace the inline model-resolution block in `Agent.execute` with a call to `resolveInvocationModel`.
168
+ Run full test suite to confirm no regressions.
169
+ Commit: `refactor: use resolveInvocationModel in Agent.execute (#53)`
170
+
171
+ ## Risks and Mitigations
172
+
173
+ | Risk | Mitigation |
174
+ | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
175
+ | Subtle behavior difference in the extracted function vs. the inline block | TDD steps 1–4 encode the exact current semantics; step 6 is a pure mechanical substitution. |
176
+ | `resolveModel` return type is `any \| string` — fragile narrowing | The new function encapsulates the `typeof` check behind a discriminated union, reducing but not eliminating the `any`. Fixing the `any` is out of scope (would require Pi SDK model type changes). |
177
+ | Second call site (`getModelLabel`) might seem like it should also use the new function | Explicitly listed as a non-goal — it has display-only semantics with no fallback behavior. |
178
+
179
+ ## Open Questions
180
+
181
+ None — the extraction is mechanical and the issue's acceptance criteria are unambiguous.
@@ -0,0 +1,302 @@
1
+ ---
2
+ issue: 54
3
+ issue_title: "refactor: decompose src/index.ts into tool + menu modules"
4
+ ---
5
+
6
+ # Decompose index.ts into tool and menu modules
7
+
8
+ ## Problem Statement
9
+
10
+ `src/index.ts` is 1,619 lines — the single largest file in the codebase.
11
+ It currently holds the extension entrypoint, all three tool definitions (with full execute callbacks and render functions), the custom message renderer, the entire `/agents` interactive menu with all sub-menus, the notification/nudge system, widget and lifecycle wiring, and ~130 lines of helper functions.
12
+ None of this code is independently testable — it is all nested inside a single default export closure.
13
+
14
+ ## Goals
15
+
16
+ - Extract each tool definition into its own module under `src/tools/`.
17
+ - Extract the `/agents` menu and all sub-handlers into `src/ui/agent-menu.ts`.
18
+ - Extract the notification message renderer into `src/renderer.ts`.
19
+ - Extract the completion notification system into `src/notification.ts`.
20
+ - Extract shared pure helpers into `src/tools/helpers.ts`.
21
+ - Reduce `index.ts` to a thin wire-up (~120–150 lines) that imports and assembles pieces.
22
+ - Enable unit testing of each extracted module via narrow dependency interfaces.
23
+ - No behavior change — pure extraction refactoring.
24
+
25
+ ## Non-Goals
26
+
27
+ - Refactoring `agent-manager.ts`, `agent-runner.ts`, or other already-separate modules.
28
+ - Adding new features or changing tool behavior.
29
+ - Writing exhaustive test suites for every extracted module — establish foundational coverage, not completeness.
30
+ - Changing any public API surface (`service.ts` exports, `SubagentsService` interface).
31
+
32
+ ## Background
33
+
34
+ ### Current structure
35
+
36
+ Everything lives inside one `export default function (pi: ExtensionAPI)` closure.
37
+ State (`agentActivity`, `pendingNudges`, `widget`, `manager`, `currentCtx`) is declared as closure variables.
38
+ Helper functions, tool definitions, menu handlers, and lifecycle hooks are all defined in the same scope.
39
+ The only existing test files cover modules that were already separate (`agent-manager.test.ts`, `agent-runner.test.ts`, etc.) — there is no `index.test.ts`.
40
+
41
+ ### Architecture reference
42
+
43
+ The SKILL's module dependency graph already shows `tools` and `ui/` as conceptual sub-trees under `index.ts`:
44
+
45
+ ```text
46
+ index.ts ──wires──> agent-manager.ts
47
+ ├── tools (Agent, get_subagent_result, steer_subagent)
48
+ ├── ui/
49
+ │ ├── agent-widget.ts
50
+ │ └── conversation-viewer.ts
51
+ └── ...
52
+ ```
53
+
54
+ This plan makes that conceptual structure physical.
55
+
56
+ ### Relevant constraints from AGENTS.md
57
+
58
+ - Keep modules focused and composable (one concern per file).
59
+ - Prefer small, reversible changes.
60
+ - Keep Pi SDK imports out of business-logic modules — tool modules are at the SDK boundary and may import SDK types; pure helpers must not.
61
+ - Narrow interfaces per consumer — do not pass a shared dependency bag when a function only uses a subset.
62
+
63
+ ### Helper usage trace
64
+
65
+ Traced every helper function in `index.ts` to determine where it belongs:
66
+
67
+ | Helper | Used by | Destination |
68
+ | -------------------------- | --------------------------------------------------------- | --------------------- |
69
+ | `textResult` | All three tools | `tools/helpers.ts` |
70
+ | `formatLifetimeTokens` | All three tools + completion callback | `tools/helpers.ts` |
71
+ | `getModelLabelFromConfig` | `buildTypeListText` (agent tool) + `getModelLabel` (menu) | `tools/helpers.ts` |
72
+ | `createActivityTracker` | Agent tool execute (foreground + background) | `tools/agent-tool.ts` |
73
+ | `buildDetails` | Agent tool execute | `tools/agent-tool.ts` |
74
+ | `getStatusNote` | Agent tool execute | `tools/agent-tool.ts` |
75
+ | `escapeXml` | `formatTaskNotification` | `notification.ts` |
76
+ | `getStatusLabel` | `formatTaskNotification` | `notification.ts` |
77
+ | `formatTaskNotification` | `emitIndividualNudge` | `notification.ts` |
78
+ | `buildNotificationDetails` | `emitIndividualNudge` | `notification.ts` |
79
+ | `buildEventData` | Completion callback | `notification.ts` |
80
+
81
+ ## Design Overview
82
+
83
+ ### Extraction strategy
84
+
85
+ Each module exports a **factory function** that receives narrow dependencies and returns the tool definition, handler, or system object.
86
+ This follows the established pattern in the codebase (`createSubagentsService` in `service-adapter.ts` already uses this approach).
87
+ Factory functions keep state scoped to the instance (matching the current closure scope) and make dependencies explicit for testing.
88
+
89
+ ### New module tree
90
+
91
+ ```text
92
+ src/
93
+ ├── index.ts ← thin wire-up (~120-150 lines)
94
+ ├── renderer.ts ← notification message renderer
95
+ ├── notification.ts ← completion notification system
96
+ ├── tools/
97
+ │ ├── helpers.ts ← shared pure helpers (textResult, formatLifetimeTokens, etc.)
98
+ │ ├── agent-tool.ts ← Agent tool definition + agent-specific helpers
99
+ │ ├── get-result-tool.ts ← get_subagent_result tool definition
100
+ │ └── steer-tool.ts ← steer_subagent tool definition
101
+ ├── ui/
102
+ │ ├── agent-menu.ts ← /agents menu + all sub-handlers (NEW)
103
+ │ ├── agent-widget.ts (existing, unchanged)
104
+ │ └── conversation-viewer.ts (existing, unchanged)
105
+ └── ... (other existing modules unchanged)
106
+ ```
107
+
108
+ ### Dependency design
109
+
110
+ Each factory receives only the methods it calls — not the full `AgentManager`, `AgentWidget`, or `ExtensionAPI`.
111
+ Example narrow interface for the get-result tool:
112
+
113
+ ```typescript
114
+ interface GetResultDeps {
115
+ getRecord: (id: string) => AgentRecord | undefined;
116
+ cancelNudge: (key: string) => void;
117
+ agentActivity: ReadonlyMap<string, AgentActivity>;
118
+ }
119
+ ```
120
+
121
+ The Agent tool has more dependencies but they remain enumerable — each one maps to a specific method or value the execute callback calls.
122
+
123
+ ### Notification system
124
+
125
+ The nudge/notification helpers (`scheduleNudge`, `cancelNudge`, `emitIndividualNudge`, `sendIndividualNudge`) and their associated formatters (`formatTaskNotification`, `buildNotificationDetails`, `buildEventData`, `escapeXml`, `getStatusLabel`) form a cohesive unit.
126
+ They move to `notification.ts` as a factory:
127
+
128
+ ```typescript
129
+ export function createNotificationSystem(deps: NotificationDeps): NotificationSystem;
130
+
131
+ interface NotificationSystem {
132
+ cancelNudge: (key: string) => void;
133
+ sendCompletion: (record: AgentRecord) => void;
134
+ cleanupCompleted: (id: string) => void;
135
+ buildEventData: (record: AgentRecord) => object;
136
+ dispose: () => void;
137
+ }
138
+ ```
139
+
140
+ The completion callback in `index.ts` becomes a thin orchestrator (~15 lines) that calls `notifications.buildEventData()`, emits lifecycle events, persists the record, and delegates to `notifications.sendCompletion()`.
141
+
142
+ ### What remains in index.ts
143
+
144
+ After all extractions, `index.ts` retains only:
145
+
146
+ 1. Imports and default export declaration.
147
+ 2. `reloadCustomAgents` helper and initial load call.
148
+ 3. `agentActivity` map creation.
149
+ 4. `createNotificationSystem()` call.
150
+ 5. `AgentManager` construction with completion/started/compacted callbacks (~20 lines).
151
+ 6. Service creation and publishing.
152
+ 7. Lifecycle hooks (`session_start`, `session_before_switch`, `session_shutdown`).
153
+ 8. Widget creation and `tool_execution_start` handler.
154
+ 9. `buildTypeListText` computation.
155
+ 10. Settings application.
156
+ 11. Three `pi.registerTool()` calls (importing factories).
157
+ 12. `pi.registerCommand("agents", ...)` call.
158
+
159
+ ## Module-Level Changes
160
+
161
+ ### `src/tools/helpers.ts` (new)
162
+
163
+ - `textResult(msg, details?)` — tool execute return value builder.
164
+ - `formatLifetimeTokens(record)` — format lifetime token total.
165
+ - `getModelLabelFromConfig(model)` — strip provider prefix and date suffix from model string.
166
+
167
+ ### `src/renderer.ts` (new)
168
+
169
+ - `registerNotificationRenderer(registerFn)` — accepts `pi.registerMessageRenderer` and registers the `"subagent-notification"` renderer.
170
+ - Contains the full `renderOne` formatting logic currently inline in the `registerMessageRenderer` callback.
171
+
172
+ ### `src/notification.ts` (new)
173
+
174
+ - `createNotificationSystem(deps)` factory — returns `NotificationSystem`.
175
+ - Contains: `scheduleNudge`, `cancelNudge`, `emitIndividualNudge`, `sendIndividualNudge`, `formatTaskNotification`, `buildNotificationDetails`, `buildEventData`, `escapeXml`, `getStatusLabel`.
176
+ - Deps interface: narrow accessors for `sendMessage`, `agentActivity`, `widget.markFinished`, `widget.update`.
177
+
178
+ ### `src/tools/agent-tool.ts` (new)
179
+
180
+ - `createAgentTool(deps)` factory — returns the tool definition config object.
181
+ - Contains: `renderCall`, `renderResult`, `execute`, plus agent-tool-specific helpers (`createActivityTracker`, `buildDetails`, `getStatusNote`).
182
+ - Deps interface: narrow accessors for manager spawn/wait, widget lifecycle, activity map, event emission, output file wiring, type list text, and `reloadCustomAgents`.
183
+
184
+ ### `src/tools/get-result-tool.ts` (new)
185
+
186
+ - `createGetResultTool(deps)` factory — returns the tool definition config object.
187
+ - Deps: `getRecord`, `cancelNudge`, `agentActivity`.
188
+
189
+ ### `src/tools/steer-tool.ts` (new)
190
+
191
+ - `createSteerTool(deps)` factory — returns the tool definition config object.
192
+ - Deps: `getRecord`, `emitEvent`.
193
+
194
+ ### `src/ui/agent-menu.ts` (new)
195
+
196
+ - `createAgentsMenuHandler(deps)` factory — returns the `/agents` command handler.
197
+ - Contains all menu functions: `showAgentsMenu`, `showAllAgentsList`, `showRunningAgents`, `viewAgentConversation`, `showAgentDetail`, `ejectAgent`, `disableAgent`, `enableAgent`, `showCreateWizard`, `showGenerateWizard`, `showManualWizard`, `showSettings`, `notifyApplied`, `findAgentFile`, `getModelLabel`.
198
+ - Deps: manager list/get methods, `reloadCustomAgents`, `agentActivity`, settings snapshot/save functions, event emission, and `pi` (for generate wizard spawning).
199
+
200
+ ### `src/index.ts` (modified — shrinks from ~1,619 to ~120–150 lines)
201
+
202
+ - Remove all helper function definitions.
203
+ - Remove all tool definitions.
204
+ - Remove all menu handler functions.
205
+ - Remove renderer registration logic.
206
+ - Remove nudge/notification helpers.
207
+ - Add imports from new modules.
208
+ - Wire everything together: create deps, call factories, register tools/commands/lifecycle hooks.
209
+
210
+ ## Test Impact Analysis
211
+
212
+ ### New unit tests enabled by extraction
213
+
214
+ The decomposition enables direct testing of code that was previously locked inside the closure:
215
+
216
+ - `test/tools/helpers.test.ts` — `textResult`, `formatLifetimeTokens`, `getModelLabelFromConfig` with edge cases (zero tokens, empty model strings).
217
+ - `test/renderer.test.ts` — notification renderer formatting for each status (completed, error, stopped, steered, aborted) in collapsed and expanded modes.
218
+ - `test/notification.test.ts` — nudge scheduling/cancellation timing, `buildEventData` shape, `formatTaskNotification` XML output, `buildNotificationDetails` field mapping.
219
+ - `test/tools/get-result-tool.test.ts` — execute paths: agent not found, wait-for-completion, result-consumed suppression, verbose conversation inclusion.
220
+ - `test/tools/steer-tool.test.ts` — execute paths: agent not found, not running, session not ready (queued steer), successful steer.
221
+ - `test/tools/agent-tool.test.ts` — execute paths: foreground completion, background launch, resume, unknown type fallback, model resolution error.
222
+ - `test/ui/agent-menu.test.ts` — menu navigation, settings mutation, eject/disable/enable flows with mock UI context.
223
+
224
+ ### Existing tests that become redundant
225
+
226
+ None.
227
+ There are no existing tests for `index.ts` — the extraction creates test coverage where none existed.
228
+
229
+ ### Existing tests that stay as-is
230
+
231
+ All 21 existing test files are unaffected.
232
+ They test modules (`agent-manager`, `agent-runner`, `model-resolver`, `invocation-config`, `service-adapter`, etc.) that are not touched by this refactoring.
233
+
234
+ ## TDD Order
235
+
236
+ Each step is a self-contained extraction + test cycle.
237
+ The existing test suite (362+ tests) runs after each step as a regression safety net.
238
+
239
+ 1. **Extract `src/tools/helpers.ts` — shared pure helpers.**
240
+ Move `textResult`, `formatLifetimeTokens`, `getModelLabelFromConfig` to new module.
241
+ Update `index.ts` imports.
242
+ Write `test/tools/helpers.test.ts` covering each function.
243
+ Commit: `refactor: extract shared tool helpers to tools/helpers`
244
+
245
+ 2. **Extract `src/renderer.ts` — notification message renderer.**
246
+ Move renderer callback to `registerNotificationRenderer` export.
247
+ Update `index.ts` to call the new function.
248
+ Write `test/renderer.test.ts` covering status-dependent formatting.
249
+ Commit: `refactor: extract notification renderer to renderer module`
250
+
251
+ 3. **Extract `src/notification.ts` — completion notification system.**
252
+ Move nudge system + formatters to `createNotificationSystem` factory.
253
+ Update `index.ts` completion callback to use the notification system.
254
+ Write `test/notification.test.ts` covering nudge timing and event data.
255
+ Commit: `refactor: extract notification system to notification module`
256
+
257
+ 4. **Extract `src/tools/get-result-tool.ts` — get_subagent_result tool.**
258
+ Move tool definition to `createGetResultTool` factory with narrow deps.
259
+ Update `index.ts` to call factory and register.
260
+ Write `test/tools/get-result-tool.test.ts` covering execute paths.
261
+ Commit: `refactor: extract get_subagent_result tool`
262
+
263
+ 5. **Extract `src/tools/steer-tool.ts` — steer_subagent tool.**
264
+ Move tool definition to `createSteerTool` factory with narrow deps.
265
+ Update `index.ts`.
266
+ Write `test/tools/steer-tool.test.ts` covering execute paths.
267
+ Commit: `refactor: extract steer_subagent tool`
268
+
269
+ 6. **Extract `src/tools/agent-tool.ts` — Agent tool.**
270
+ Move tool definition + agent-specific helpers (`createActivityTracker`, `buildDetails`, `getStatusNote`) to `createAgentTool` factory.
271
+ Update `index.ts`.
272
+ Write `test/tools/agent-tool.test.ts` covering foreground, background, resume, and error paths.
273
+ Commit: `refactor: extract Agent tool`
274
+
275
+ 7. **Extract `src/ui/agent-menu.ts` — /agents menu handlers.**
276
+ Move all menu functions to `createAgentsMenuHandler` factory.
277
+ Update `index.ts` to register command with factory result.
278
+ Write `test/ui/agent-menu.test.ts` covering key menu navigation flows.
279
+ Commit: `refactor: extract /agents menu handlers`
280
+
281
+ 8. **Final index.ts cleanup.**
282
+ Remove any dead imports or vestigial code.
283
+ Verify index.ts is ~120–150 lines of pure wire-up.
284
+ Run `pnpm run check` (typecheck) and full test suite.
285
+ Commit: `refactor: slim index.ts to wire-up entrypoint (#54)`
286
+
287
+ ## Risks and Mitigations
288
+
289
+ | Risk | Mitigation |
290
+ | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
291
+ | Closure variable access breaks after extraction — helpers currently close over shared maps (`agentActivity`, `pendingNudges`) | Factory pattern replaces closure access with explicit dependency injection; each factory's deps interface enumerates exactly what it needs. |
292
+ | Narrow dep interfaces diverge from the real objects — test mocks pass but runtime breaks | Run `pnpm run check` (typecheck) after each extraction step; the factory call sites in `index.ts` provide real objects whose types must satisfy the narrow interfaces. |
293
+ | Large number of extraction steps creates merge-conflict risk with parallel PRs | Steps are ordered leaf-first so earlier commits don't touch files later steps modify. Each step is independently committable and revertable. |
294
+ | Agent tool factory has many deps (~8–10) — risks becoming a dependency bag | Deps are individual functions and values, not a monolithic object. Each dep maps to exactly one method call in execute. If the count feels excessive during implementation, group by concern (spawn, widget, events) into 2–3 sub-interfaces. |
295
+ | `buildTypeListText` is called at init time and captures agent types — extraction might change when it runs | `buildTypeListText` stays in `index.ts` as wire-up code (called once, result passed to agent tool factory). Timing is unchanged. |
296
+
297
+ ## Open Questions
298
+
299
+ - Should the notification module also own the lifecycle event emission (`subagents:completed`, `subagents:failed`, `subagents:started`, `subagents:compacted`), or should those stay in the completion callback in `index.ts`?
300
+ Defer until step 3 — the answer depends on whether the completion callback shrinks enough to justify the move.
301
+ - Should `buildTypeListText` move into `agent-tool.ts` or stay as wire-up in `index.ts`?
302
+ Defer until step 6 — evaluate once the agent tool factory interface is concrete.
@@ -0,0 +1,30 @@
1
+ ---
2
+ issue: 53
3
+ issue_title: "refactor: extract model resolution from Agent.execute"
4
+ ---
5
+
6
+ # Retro: #53 — extract model resolution from Agent.execute
7
+
8
+ ## Final Retrospective (2026-05-17T21:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned and executed the extraction of inline model-resolution logic from `Agent.execute` in `index.ts` into a new `resolveInvocationModel()` function in `model-resolver.ts`.
13
+ Released as `pi-subagents-v4.1.0` with +10 new unit tests and no behavior change.
14
+ Also fixed a pre-existing `rumdl` glob-quoting bug in `package.json` discovered during the lint step.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - Pre-existing lint bug surfaced and fixed: the `rumdl check '*.md' 'docs/**/*.md'` command in `package.json` used single-quoted globs that prevented shell expansion. Verified as pre-existing (reproduced on prior commit via `git stash`), cleanly isolated into its own `fix:` commit. This was a genuine find — the lint had been silently broken.
21
+
22
+ #### What caused friction (agent side)
23
+
24
+ - `missing-context` — In step 6 (refactoring `index.ts`), replaced the `resolveModel` import with `resolveInvocationModel` without first checking whether `resolveModel` was still used elsewhere in the file. Two other call sites (`createSubagentsService` at line 386 and `getModelLabel` at line 1043) still needed it. The plan explicitly listed `getModelLabel` as a non-goal that continues using `resolveModel`, so the information was available. Caught immediately via `grep` after the edit and fixed in the same commit. Impact: one extra edit + grep cycle, no rework.
25
+
26
+ - `missing-context` — The plan's type definitions specified `model: unknown` for `ModelResolutionResult`, but downstream code in `index.ts` accesses `.id` and `.name` on the model and passes it where `Model<any>` is expected. The plan's risk section flagged this ("reducing but not eliminating the `any`"), yet the implementation went with `unknown` first, requiring a correction after `pnpm run check` failed with 4 type errors. Changed to `model: any` to match the existing `resolveModel` return type. Impact: one extra edit cycle within the same commit, no rework.
27
+
28
+ #### What caused friction (user side)
29
+
30
+ - None observed. The issue was well-scoped with clear acceptance criteria, making planning and execution straightforward.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "4.0.0",
3
+ "version": "4.1.1",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },
@@ -59,7 +59,7 @@
59
59
  "check": "tsc --noEmit",
60
60
  "test": "vitest run",
61
61
  "test:watch": "vitest",
62
- "lint:md": "rumdl check '*.md' 'docs/**/*.md'",
62
+ "lint:md": "rumdl check *.md docs/**/*.md",
63
63
  "lint": "biome check . && pnpm run lint:md"
64
64
  }
65
65
  }