@gotgenes/pi-subagents 6.16.3 → 6.17.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/docs/architecture/architecture.md +588 -535
  3. package/docs/architecture/history/phase-1-api-boundary.md +8 -0
  4. package/docs/architecture/history/phase-2-remove-scheduling.md +9 -0
  5. package/docs/architecture/history/phase-3-remove-rpc-groupjoin.md +11 -0
  6. package/docs/architecture/history/phase-4-implement-service.md +8 -0
  7. package/docs/architecture/history/phase-5-decompose-index.md +42 -0
  8. package/docs/architecture/history/phase-7-encapsulation.md +173 -0
  9. package/docs/architecture/history/phase-8-testability.md +103 -0
  10. package/docs/architecture/history/phase-9-observation-ctx.md +122 -0
  11. package/docs/plans/0147-inject-wrap-text-into-conversation-viewer.md +166 -0
  12. package/docs/retro/0147-inject-wrap-text-into-conversation-viewer.md +90 -0
  13. package/package.json +1 -1
  14. package/src/agent-manager.ts +11 -11
  15. package/src/agent-record.ts +6 -6
  16. package/src/agent-runner.ts +6 -6
  17. package/src/agent-types.ts +2 -2
  18. package/src/custom-agents.ts +3 -3
  19. package/src/default-agents.ts +1 -1
  20. package/src/env.ts +2 -2
  21. package/src/handlers/index.ts +2 -2
  22. package/src/index.ts +26 -26
  23. package/src/invocation-config.ts +1 -1
  24. package/src/memory.ts +2 -2
  25. package/src/notification.ts +4 -4
  26. package/src/parent-snapshot.ts +1 -1
  27. package/src/prompts.ts +2 -2
  28. package/src/record-observer.ts +2 -2
  29. package/src/renderer.ts +2 -2
  30. package/src/runtime.ts +2 -2
  31. package/src/service-adapter.ts +5 -5
  32. package/src/service.ts +1 -1
  33. package/src/session-config.ts +5 -5
  34. package/src/skill-loader.ts +2 -2
  35. package/src/tools/agent-tool.ts +11 -11
  36. package/src/tools/background-spawner.ts +8 -8
  37. package/src/tools/foreground-runner.ts +9 -9
  38. package/src/tools/get-result-tool.ts +5 -5
  39. package/src/tools/helpers.ts +4 -4
  40. package/src/tools/spawn-config.ts +6 -6
  41. package/src/tools/steer-tool.ts +3 -3
  42. package/src/types.ts +1 -1
  43. package/src/ui/agent-activity-tracker.ts +1 -1
  44. package/src/ui/agent-config-editor.ts +4 -4
  45. package/src/ui/agent-creation-wizard.ts +5 -5
  46. package/src/ui/agent-menu.ts +12 -10
  47. package/src/ui/agent-widget.ts +5 -5
  48. package/src/ui/conversation-viewer.ts +33 -21
  49. package/src/ui/display.ts +2 -2
  50. package/src/ui/ui-observer.ts +1 -1
  51. package/src/ui/widget-renderer.ts +5 -5
  52. package/src/worktree-state.ts +1 -1
  53. package/src/worktree.ts +1 -1
  54. package/vitest.config.ts +14 -0
@@ -4,92 +4,365 @@ This document describes the architecture of the pi-subagents fork: a focused, co
4
4
 
5
5
  ## Design principles
6
6
 
7
- 1. **Narrow core** - the extension owns agent spawning, execution, and result retrieval.
7
+ 1. **Narrow core** the extension owns agent spawning, execution, and result retrieval.
8
8
  Everything else is a consumer.
9
- 2. **Composable by default** - other extensions can spawn agents, observe their lifecycle, and display their state without importing this package directly.
10
- 3. **Typed API boundary** - this package exports a `SubagentsService` interface and `Symbol.for()` accessors (`publishSubagentsService` / `getSubagentsService`).
9
+ 2. **Composable by default** other extensions can spawn agents, observe their lifecycle, and display their state without importing this package directly.
10
+ 3. **Typed API boundary** this package exports a `SubagentsService` interface and `Symbol.for()` accessors (`publishSubagentsService` / `getSubagentsService`).
11
11
  Consumers declare this package as an optional peer dependency and use dynamic import for compile-time types.
12
- The runtime bridge is `Symbol.for("@gotgenes/pi-subagents:service")` on `globalThis` - no separate API package.
13
- 4. **No scheduling** - in-process scheduling is removed from the core.
12
+ The runtime bridge is `Symbol.for("@gotgenes/pi-subagents:service")` on `globalThis` no separate API package.
13
+ 4. **No scheduling** in-process scheduling is removed from the core.
14
14
  Scheduling is a separate concern that any extension can implement by calling `spawn()` on the published API.
15
- 5. **UI extraction is deferred** - the widget, conversation viewer, and `/agents` command menu stay in the core for now.
15
+ 5. **UI extraction is deferred** the widget, conversation viewer, and `/agents` command menu stay in the core for now.
16
16
  They are the first candidate for extraction once the API boundary is proven stable.
17
- 6. **Snapshot, don't capture** - mutable parent state (ctx, session, model) is read once at spawn time and frozen into a `ParentSnapshot` data object.
17
+ 6. **Snapshot, don't capture** mutable parent state (ctx, session, model) is read once at spawn time and frozen into a `ParentSnapshot` data object.
18
18
  No live references survive past the spawn call.
19
- 7. **Subscribe, don't thread** - observation of agent progress uses direct session-event subscription, not callback parameters threaded through multiple layers.
20
- 8. **Construct complete** - objects are born with all their dependencies.
19
+ 7. **Subscribe, don't thread** observation of agent progress uses direct session-event subscription, not callback parameters threaded through multiple layers.
20
+ 8. **Construct complete** objects are born with all their dependencies.
21
21
  If state isn't available yet, the object that needs it doesn't exist yet.
22
- No post-construction field writes from external code - if an object can't be instantiated ready-to-go, the prep work hasn't been done and the right dependencies haven't been identified.
23
- 9. **State owns its mutations** - mutable state lives in a class whose methods enforce valid transitions and invariants.
22
+ No post-construction field writes from external code if an object can't be instantiated ready-to-go, the prep work hasn't been done and the right dependencies haven't been identified.
23
+ 9. **State owns its mutations** mutable state lives in a class whose methods enforce valid transitions and invariants.
24
24
  Free functions that mutate module-scoped variables, closure-captured bags-of-functions, and external writes to shared interfaces are replaced by classes that encapsulate the state they manage.
25
25
 
26
- ## Current state
26
+ ## Domain model
27
27
 
28
- The extension is organized into 39 focused modules with a typed `SubagentsService` API boundary.
28
+ The extension is organized around six domains, each responsible for one aspect of managing agents.
29
+
30
+ ```mermaid
31
+ flowchart TB
32
+ subgraph config["Config domain"]
33
+ direction TB
34
+ AgentTypeRegistry["AgentTypeRegistry\n(registry of agent types)"]
35
+ DefaultAgents["default-agents\n(built-in types)"]
36
+ CustomAgents["custom-agents\n(user .md files)"]
37
+ InvocationConfig["invocation-config\n(per-call merge)"]
38
+ end
39
+
40
+ subgraph session["Session domain"]
41
+ direction TB
42
+ SessionConfig["assembleSessionConfig\n(pure assembler)"]
43
+ Prompts["prompts\n(system prompt)"]
44
+ Context["context\n(parent history)"]
45
+ Memory["memory\n(MEMORY.md)"]
46
+ SkillLoader["skill-loader\n(preload skills)"]
47
+ Env["env\n(git/platform)"]
48
+ ModelResolver["model-resolver\n(fuzzy match)"]
49
+ end
50
+
51
+ subgraph lifecycle["Lifecycle domain"]
52
+ direction TB
53
+ AgentManager["AgentManager\n(spawn, queue, abort)"]
54
+ AgentRunner["agent-runner\n(session, turns, results)"]
55
+ AgentRecord["AgentRecord\n(status state machine)"]
56
+ ParentSnapshot["ParentSnapshot\n(frozen parent state)"]
57
+ Worktree["worktree\n(git isolation)"]
58
+ end
59
+
60
+ subgraph observation["Observation domain"]
61
+ direction TB
62
+ RecordObserver["record-observer\n(stats via events)"]
63
+ Notification["notification\n(completion nudges)"]
64
+ UIObserver["ui-observer\n(streaming state)"]
65
+ end
66
+
67
+ subgraph tools["Tools domain"]
68
+ direction TB
69
+ AgentTool["Agent tool\n(dispatch)"]
70
+ SpawnConfig["spawn-config\n(resolve params)"]
71
+ FgRunner["foreground-runner"]
72
+ BgSpawner["background-spawner"]
73
+ GetResult["get_subagent_result"]
74
+ Steer["steer_subagent"]
75
+ end
76
+
77
+ subgraph ui["UI domain"]
78
+ direction TB
79
+ Widget["agent-widget\n(live status)"]
80
+ ConvViewer["conversation-viewer\n(session overlay)"]
81
+ Menu["agent-menu\n(slash command)"]
82
+ end
83
+
84
+ AgentTool --> AgentManager
85
+ AgentManager --> AgentRunner
86
+ AgentRunner --> SessionConfig
87
+ SessionConfig --> AgentTypeRegistry
88
+ SessionConfig --> Prompts & Memory & SkillLoader & Env
89
+ AgentTypeRegistry --> DefaultAgents & CustomAgents
90
+ RecordObserver -.->|subscribes| AgentRunner
91
+ UIObserver -.->|subscribes| AgentRunner
92
+ Widget -.->|polls| AgentManager
93
+ ```
94
+
95
+ ### Key domain types
96
+
97
+ ```mermaid
98
+ classDiagram
99
+ class AgentRecord {
100
+ +id: string
101
+ +type: SubagentType
102
+ +description: string
103
+ +status: AgentRecordStatus
104
+ +result?: string
105
+ +error?: string
106
+ +toolUses: number
107
+ +lifetimeUsage: LifetimeUsage
108
+ +execution?: ExecutionState
109
+ +worktreeState?: WorktreeState
110
+ +notification?: NotificationState
111
+ +markRunning()
112
+ +markCompleted()
113
+ +markAborted()
114
+ +markSteered()
115
+ +markError()
116
+ +markStopped()
117
+ +resetForResume()
118
+ }
119
+
120
+ class AgentManager {
121
+ +spawn(snapshot, type, prompt, config)
122
+ +spawnAndWait(snapshot, type, prompt, config)
123
+ +resume(id, snapshot, exec)
124
+ +getRecord(id): AgentRecord
125
+ +listAgents(): AgentRecord[]
126
+ +abort(id)
127
+ +queueSteer(id, message)
128
+ }
129
+
130
+ class AgentTypeRegistry {
131
+ +resolveType(type): string
132
+ +resolveAgentConfig(type): AgentConfig
133
+ +reload()
134
+ +getToolNamesForType(type): string[]
135
+ }
136
+
137
+ class ParentSnapshot {
138
+ +cwd: string
139
+ +systemPrompt: string
140
+ +model: unknown
141
+ +modelRegistry: unknown
142
+ +parentContext?: string
143
+ }
144
+
145
+ class SubagentsService {
146
+ +spawn(type, prompt, options?)
147
+ +getRecord(id): SubagentRecord
148
+ +listAgents(): SubagentRecord[]
149
+ +abort(id)
150
+ +steer(id, message)
151
+ +waitForAll()
152
+ +hasRunning(): boolean
153
+ }
154
+
155
+ AgentManager --> AgentRecord : creates/manages
156
+ AgentManager --> ParentSnapshot : receives at spawn
157
+ SubagentsService --> AgentManager : wraps via adapter
158
+ AgentManager --> AgentTypeRegistry : resolves types
159
+ ```
160
+
161
+ ## Agent lifecycle
162
+
163
+ ```mermaid
164
+ stateDiagram-v2
165
+ [*] --> queued : spawn (background, at capacity)
166
+ [*] --> running : spawn (foreground or under limit)
167
+ queued --> running : capacity available
168
+ running --> completed : all turns finished
169
+ running --> error : unhandled exception
170
+ running --> aborted : abort() called
171
+ running --> stopped : max turns reached
172
+ running --> steered : steer message injected
173
+ steered --> running : continues with message
174
+ completed --> running : resetForResume
175
+ stopped --> running : resetForResume
176
+ error --> running : resetForResume
177
+ aborted --> running : resetForResume
178
+ completed --> [*]
179
+ error --> [*]
180
+ aborted --> [*]
181
+ stopped --> [*]
182
+
183
+ note right of running
184
+ markCompleted, markAborted,
185
+ markSteered, and markError
186
+ are no-ops when status is stopped
187
+ end note
188
+ ```
189
+
190
+ Note: `markStopped` always succeeds regardless of current status.
191
+ Other terminal transitions guard against overwriting `stopped` — once an agent is stopped, only `resetForResume` can return it to `running`.
192
+
193
+ ## Execution flow
194
+
195
+ ```mermaid
196
+ sequenceDiagram
197
+ participant LLM as Parent LLM
198
+ participant Tool as Agent tool
199
+ participant Spawn as spawn-config
200
+ participant Mgr as AgentManager
201
+ participant Runner as agent-runner
202
+ participant Asm as assembleSessionConfig
203
+ participant Child as Child session
204
+
205
+ LLM->>Tool: Agent(type, prompt, ...)
206
+ Tool->>Spawn: resolveSpawnConfig(params)
207
+ Spawn-->>Tool: ResolvedSpawnConfig
208
+ Tool->>Mgr: spawn(snapshot, type, prompt, config)
209
+ Mgr->>Runner: runAgent(record, snapshot, options, io)
210
+ Runner->>Asm: assembleSessionConfig(type, ctx, opts, env, registry, io)
211
+ Asm-->>Runner: SessionConfig
212
+ Runner->>Child: create session + run turn loop
213
+ Child-->>Runner: result text
214
+ Runner-->>Mgr: update AgentRecord
215
+ Note over Mgr: record-observer subscribes to session events for stats
216
+ Note over Mgr: ui-observer subscribes for streaming state
217
+ Mgr-->>Tool: AgentRecord
218
+ Tool-->>LLM: formatted result
219
+ ```
220
+
221
+ ## Module organization
222
+
223
+ The extension has 53 source files (7,288 LOC) organized into six domains plus entry-point wiring.
224
+ The three existing subdirectories (`tools/`, `ui/`, `handlers/`) reflect three of these domains.
225
+ The remaining 31 root-level files span four more domains but are not yet grouped in the filesystem.
226
+
227
+ ### Current layout
228
+
229
+ ```text
230
+ src/
231
+ ├── index.ts entry point, tool registration, event wiring
232
+ ├── runtime.ts SubagentRuntime factory (session-scoped state)
233
+ ├── types.ts shared type definitions
234
+ ├── settings.ts SettingsManager (persistent operational settings)
235
+ ├── debug.ts debug logging utility
236
+
237
+ │ ── Config domain (agent type definitions and resolution) ──
238
+ ├── agent-types.ts AgentTypeRegistry class
239
+ ├── default-agents.ts built-in agent configs (general-purpose, Explore, Plan)
240
+ ├── custom-agents.ts user-defined agent .md file loader
241
+ ├── invocation-config.ts per-call config merge
242
+
243
+ │ ── Session domain (session assembly and preparation) ──
244
+ ├── session-config.ts pure assembler (main entry)
245
+ ├── prompts.ts system prompt building
246
+ ├── context.ts parent conversation extraction
247
+ ├── memory.ts persistent MEMORY.md per agent
248
+ ├── skill-loader.ts skill preloading
249
+ ├── env.ts git/platform detection
250
+ ├── model-resolver.ts fuzzy model name resolution
251
+ ├── session-dir.ts session directory derivation
252
+
253
+ │ ── Lifecycle domain (agent execution and state) ──
254
+ ├── agent-manager.ts spawn, queue, abort, resume, concurrency
255
+ ├── agent-runner.ts session creation, turn loop, tool filtering
256
+ ├── agent-record.ts status state machine
257
+ ├── parent-snapshot.ts immutable spawn-time parent state
258
+ ├── execution-state.ts session/output phase state
259
+ ├── worktree.ts git worktree isolation
260
+ ├── worktree-state.ts worktree phase state
261
+ ├── usage.ts token usage tracking
262
+
263
+ │ ── Observation domain (progress tracking and notification) ──
264
+ ├── record-observer.ts session-event stats observer
265
+ ├── notification.ts completion nudges
266
+ ├── notification-state.ts per-agent notification tracking
267
+ ├── renderer.ts notification TUI component
268
+
269
+ │ ── Service domain (cross-extension API) ──
270
+ ├── service.ts SubagentsService interface + Symbol.for() accessors
271
+ ├── service-adapter.ts SubagentsService wrapper around AgentManager
272
+
273
+ │ ── Tools domain (LLM-facing tool implementations) ──
274
+ ├── tools/
275
+ │ ├── agent-tool.ts Agent tool definition, validation, dispatch
276
+ │ ├── spawn-config.ts pure config resolution
277
+ │ ├── foreground-runner.ts foreground execution loop
278
+ │ ├── background-spawner.ts background spawn setup
279
+ │ ├── get-result-tool.ts get_subagent_result tool
280
+ │ ├── steer-tool.ts steer_subagent tool
281
+ │ └── helpers.ts shared tool utilities
282
+
283
+ │ ── UI domain (user-facing presentation) ──
284
+ ├── ui/
285
+ │ ├── agent-widget.ts above-editor live status widget
286
+ │ ├── widget-renderer.ts pure rendering for widget
287
+ │ ├── agent-menu.ts /agents slash command menu
288
+ │ ├── agent-config-editor.ts agent detail/edit view
289
+ │ ├── agent-creation-wizard.ts agent creation (AI + manual)
290
+ │ ├── conversation-viewer.ts scrollable session overlay
291
+ │ ├── agent-activity-tracker.ts live activity state tracker
292
+ │ ├── agent-file-ops.ts filesystem abstraction
293
+ │ ├── ui-observer.ts session-event observer for streaming
294
+ │ └── display.ts pure formatters and shared types
295
+
296
+ │ ── Event handlers ──
297
+ └── handlers/
298
+ ├── lifecycle.ts session_start, session_before_switch, session_shutdown
299
+ └── tool-start.ts tool_execution_start handler
300
+ ```
301
+
302
+ ### Proposed directory restructuring
303
+
304
+ Move the four ungrouped domains into subdirectories so the filesystem mirrors the domain model.
305
+ Root-level files stay: `index.ts` (entry point), `runtime.ts` (wiring), `types.ts` (shared), `settings.ts`, `debug.ts`.
29
306
 
30
307
  ```text
31
- index.ts - entry point, tool registration, event wiring
32
- agent-manager.ts - lifecycle, concurrency, queue
33
- agent-runner.ts - session creation, turn loop, tool filtering
34
- session-config.ts - pure session-config assembler
35
- agent-types.ts - type registry (defaults + custom .md files)
36
- agent-record.ts - agent record with encapsulated status transitions
37
- types.ts - shared type definitions
38
- runtime.ts - SubagentRuntime factory (session-scoped state)
39
- parent-snapshot.ts - immutable snapshot of parent session state
40
-
41
- prompts.ts - system prompt assembly
42
- context.ts - parent conversation extraction
43
- memory.ts - persistent MEMORY.md per agent
44
- skill-loader.ts - preload .pi/skills into prompts
45
- env.ts - git/platform detection
46
-
47
- worktree.ts - git worktree isolation
48
- usage.ts - token usage tracking
49
- model-resolver.ts - fuzzy model name resolution
50
- invocation-config.ts - merge tool params with agent config
51
- session-dir.ts - subagent session directory derivation
52
- settings.ts - persistent operational settings; `SettingsManager` class owns all three in-memory values
53
-
54
- service.ts - SubagentsService interface + Symbol.for() accessors
55
- service-adapter.ts - SubagentsService implementation wrapping AgentManager
56
-
57
- tools/agent-tool.ts - Agent tool definition, parameter validation, dispatch
58
- tools/foreground-runner.ts - foreground execution loop (spinner, streaming, result)
59
- tools/background-spawner.ts - background spawn (activity setup, notification wiring)
60
- tools/get-result-tool.ts - get_subagent_result tool
61
- tools/steer-tool.ts - steer_subagent tool
62
- tools/helpers.ts - shared tool utilities (textResult, buildDetails, getStatusNote, ...)
63
-
64
- handlers/lifecycle.ts - session_start, session_before_switch, session_shutdown
65
- handlers/tool-start.ts - tool_execution_start handler
66
-
67
- notification.ts - completion nudges, custom message renderer
68
- renderer.ts - notification TUI component
69
- record-observer.ts - session-event observer for record statistics
70
-
71
- ui/display.ts - pure formatters, display helpers, and shared types (Theme, AgentDetails)
72
- ui/agent-widget.ts - above-editor live status widget (thin lifecycle wrapper)
73
- ui/widget-renderer.ts - pure rendering functions for agent widget
74
- ui/agent-menu.ts - /agents slash command menu
75
- ui/conversation-viewer.ts - scrollable session overlay
76
- ui/ui-observer.ts - session-event observer for UI streaming
77
-
78
- default-agents.ts - embedded default agent configs (general-purpose, Explore, Plan)
79
- custom-agents.ts - user-defined agent .md file loader
80
- debug.ts - debug logging utility
308
+ src/
309
+ ├── index.ts
310
+ ├── runtime.ts
311
+ ├── types.ts
312
+ ├── settings.ts
313
+ ├── debug.ts
314
+
315
+ ├── config/ agent type definitions and resolution
316
+ │ ├── agent-types.ts
317
+ │ ├── default-agents.ts
318
+ │ ├── custom-agents.ts
319
+ │ └── invocation-config.ts
320
+
321
+ ├── session/ session assembly and preparation
322
+ │ ├── session-config.ts
323
+ │ ├── prompts.ts
324
+ │ ├── context.ts
325
+ │ ├── memory.ts
326
+ │ ├── skill-loader.ts
327
+ │ ├── env.ts
328
+ │ ├── model-resolver.ts
329
+ │ └── session-dir.ts
330
+
331
+ ├── lifecycle/ agent execution and state tracking
332
+ │ ├── agent-manager.ts
333
+ │ ├── agent-runner.ts
334
+ │ ├── agent-record.ts
335
+ │ ├── parent-snapshot.ts
336
+ │ ├── execution-state.ts
337
+ │ ├── worktree.ts
338
+ │ ├── worktree-state.ts
339
+ │ └── usage.ts
340
+
341
+ ├── observation/ progress tracking and notification
342
+ │ ├── record-observer.ts
343
+ │ ├── notification.ts
344
+ │ ├── notification-state.ts
345
+ │ └── renderer.ts
346
+
347
+ ├── service/ cross-extension API boundary
348
+ │ ├── service.ts
349
+ │ └── service-adapter.ts
350
+
351
+ ├── tools/ (existing)
352
+ ├── ui/ (existing)
353
+ └── handlers/ (existing)
81
354
  ```
82
355
 
356
+ Root goes from 31 files to 5 files + 8 directories — each directory name tells you what domain it belongs to.
357
+
83
358
  ### Observation model
84
359
 
85
360
  Record statistics (tool uses, token usage, compaction counts) are updated by `record-observer.ts`, which subscribes directly to session events.
86
361
  UI streaming (active tools, response text, turn counts) is handled by `ui/ui-observer.ts`, which subscribes to the same session events independently.
87
- Neither observer wraps or forwards the other - both subscribe directly to the session.
362
+ Neither observer wraps or forwards the other both subscribe directly to the session.
88
363
 
89
364
  The widget reads agent state by polling a shared `Map<string, AgentActivityTracker>` on `SubagentRuntime` every 80 ms. The conversation viewer subscribes directly to `AgentSession` objects.
90
365
 
91
- Cross-extension consumers use the typed `SubagentsService` API published via `Symbol.for("@gotgenes/pi-subagents:service")` on `globalThis`.
92
-
93
366
  ## Cross-extension architecture
94
367
 
95
368
  ```mermaid
@@ -98,7 +371,7 @@ flowchart TD
98
371
  direction TB
99
372
  exports["SubagentsService interface\npublish / getSubagentsService()\nSubagentRecord, SubagentStatus, LifetimeUsage\nSUBAGENT_EVENTS constants"]
100
373
  engine["Agent + get_subagent_result + steer_subagent tools\nAgentManager, agent-runner, agent-types\npublishSubagentsService() called at init"]
101
- ui["Internal UI: widget, viewer, /agents menu\n(candidate for extraction to pi-subagents-ui)"]
374
+ ui_int["Internal UI: widget, viewer, /agents menu\n(candidate for extraction to pi-subagents-ui)"]
102
375
  end
103
376
 
104
377
  core -- "Symbol.for() on globalThis" --> sched["scheduling extension\n(hypothetical)"]
@@ -112,32 +385,28 @@ They declare this package as an optional peer dependency and use dynamic import
112
385
  ### What the core owns
113
386
 
114
387
  - The three tools: `Agent`, `get_subagent_result`, `steer_subagent`.
115
- - `AgentManager` - spawn, queue, abort, resume, concurrency control.
116
- - `agent-runner` - session creation, turn loop, tool filtering, extension binding (Patches 2 and 3).
117
- - `session-config` - pure configuration assembler (extracted from `agent-runner`).
118
- - `SubagentRuntime` - session-scoped state bag with methods.
119
- - `ParentSnapshot` - immutable snapshot of parent session state, captured once at spawn time.
120
- - `record-observer` - session-event observer that updates record statistics without callback threading.
121
- - Agent type registry - default agents, custom `.md` file loading.
388
+ - `AgentManager` spawn, queue, abort, resume, concurrency control.
389
+ - `agent-runner` session creation, turn loop, tool filtering, extension binding (Patches 2 and 3).
390
+ - `session-config` pure configuration assembler (extracted from `agent-runner`).
391
+ - `SubagentRuntime` session-scoped state bag with methods.
392
+ - `ParentSnapshot` immutable snapshot of parent session state, captured once at spawn time.
393
+ - `record-observer` session-event observer that updates record statistics without callback threading.
394
+ - Agent type registry default agents, custom `.md` file loading.
122
395
  - Prompt assembly, context extraction, memory, skills, environment.
123
396
  - Worktree isolation.
124
397
  - Token usage tracking.
125
398
  - Session directory derivation and persisted `SessionManager` for subagent transcripts.
126
399
  - Settings persistence.
127
- - Internal UI (widget, conversation viewer, `/agents` menu) - these stay until the API boundary is proven, then move to a separate extension.
400
+ - Internal UI (widget, conversation viewer, `/agents` menu) these stay until the API boundary is proven, then move to a separate extension.
128
401
 
129
402
  ### What the core dropped
130
403
 
131
- - **Scheduling** (`schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`) - removed (#52).
132
- Any extension that wants scheduling can implement it by calling `getSubagentsService()?.spawn(...)` on a timer.
133
- - **Ad-hoc RPC** (`cross-extension-rpc.ts`) - replaced by the typed `SubagentsService` published via `Symbol.for()` (#49).
134
- - **Group join** (`group-join.ts`) - removed (#49).
135
- Individual completion notifications are sufficient.
136
- - **Output file** (`output-file.ts`) - replaced by `session-dir.ts` + `SessionManager.create()` (#61).
137
- Subagent transcripts are now written in Pi's official JSONL session format.
138
- - **Callback threading** - the three-layer `on*` callback chain through `SpawnOptions` → `AgentManager` → `RunOptions` was replaced by direct session-event subscriptions (#100).
139
- - **Live `ctx` capture** - `SpawnArgs` previously held a mutable `ctx: ExtensionContext` reference that could go stale in the concurrency queue.
140
- Replaced by `ParentSnapshot`, an immutable data object captured once at spawn time (#99).
404
+ - **Scheduling** (`schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`) removed (#52).
405
+ - **Ad-hoc RPC** (`cross-extension-rpc.ts`) replaced by the typed `SubagentsService` published via `Symbol.for()` (#49).
406
+ - **Group join** (`group-join.ts`) removed (#49).
407
+ - **Output file** (`output-file.ts`) — replaced by `session-dir.ts` + `SessionManager.create()` (#61).
408
+ - **Callback threading** the three-layer `on*` callback chain was replaced by direct session-event subscriptions (#100).
409
+ - **Live `ctx` capture** replaced by `ParentSnapshot`, an immutable data object captured once at spawn time (#99).
141
410
 
142
411
  ## SubagentsService
143
412
 
@@ -176,10 +445,10 @@ The dynamic import provides compile-time types; the `Symbol.for()` key is the ac
176
445
  See `src/service.ts` for the canonical definition.
177
446
  Key types:
178
447
 
179
- - `SubagentsService` - `spawn`, `getRecord`, `listAgents`, `abort`, `steer`, `waitForAll`, `hasRunning`.
180
- - `SubagentRecord` - serializable agent snapshot (no live session objects).
181
- - `SpawnOptions` - `description`, `model`, `maxTurns`, `thinkingLevel`, `isolated`, `inheritContext`, `foreground`, `bypassQueue`, `isolation`.
182
- - `SUBAGENT_EVENTS` - channel constants for `pi.events` subscriptions.
448
+ - `SubagentsService` `spawn`, `getRecord`, `listAgents`, `abort`, `steer`, `waitForAll`, `hasRunning`.
449
+ - `SubagentRecord` serializable agent snapshot (no live session objects).
450
+ - `SpawnOptions` `description`, `model`, `maxTurns`, `thinkingLevel`, `isolated`, `inheritContext`, `foreground`, `bypassQueue`, `isolation`.
451
+ - `SUBAGENT_EVENTS` channel constants for `pi.events` subscriptions.
183
452
 
184
453
  ### Accessor pattern
185
454
 
@@ -209,527 +478,301 @@ The core emits events on `pi.events` that any extension can observe:
209
478
  | `subagents:completed` | `{ id, type, status, result?, error? }` | Agent finishes |
210
479
  | `subagents:activity` | `{ id, toolName?, textDelta?, turnCount? }` | Streaming progress |
211
480
 
212
- These are fire-and-forget broadcast events - no request IDs, no reply channels.
213
-
214
- ### Consumer example: scheduling extension
215
-
216
- ```typescript
217
- export default function (pi) {
218
- pi.on("session_start", async (event, ctx) => {
219
- let getSubagentsService;
220
- try {
221
- ({ getSubagentsService } = await import("@gotgenes/pi-subagents"));
222
- } catch {
223
- return; // pi-subagents not installed
224
- }
225
- const svc = getSubagentsService();
226
- if (!svc) return;
227
-
228
- setInterval(() => {
229
- svc.spawn("Explore", "Check for stale TODOs", {
230
- bypassQueue: true,
231
- });
232
- }, 60 * 60 * 1000);
233
- });
234
- }
235
- ```
236
-
237
- ### Consumer example: transcript extension
238
-
239
- ```typescript
240
- export default function (pi) {
241
- pi.events.on("subagents:completed", async (data) => {
242
- const { id } = data as { id: string };
243
- let getSubagentsService;
244
- try {
245
- ({ getSubagentsService } = await import("@gotgenes/pi-subagents"));
246
- } catch {
247
- return;
248
- }
249
- const record = getSubagentsService()?.getRecord(id);
250
- if (record?.result) {
251
- fs.appendFileSync("agent-log.jsonl", JSON.stringify(record) + "\n");
252
- }
253
- });
254
- }
255
- ```
256
-
257
- ## index.ts decomposition
258
-
259
- The original monolithic `index.ts` has been decomposed into focused modules:
260
-
261
- ```text
262
- src/
263
- ├── index.ts - slimmed entry point: init, tool registration
264
- ├── runtime.ts - SubagentRuntime: session-scoped state + methods
265
- ├── tools/
266
- │ ├── agent-tool.ts - Agent tool definition, parameter validation, dispatch
267
- │ ├── foreground-runner.ts - foreground execution loop (spinner, streaming, result)
268
- │ ├── background-spawner.ts - background spawn (activity setup, notification wiring)
269
- │ ├── get-result-tool.ts - get_subagent_result tool
270
- │ ├── steer-tool.ts - steer_subagent tool
271
- │ └── helpers.ts - shared tool utilities (textResult, buildDetails, getStatusNote, ...)
272
- ├── handlers/
273
- │ ├── lifecycle.ts - session_start, session_before_switch, session_shutdown
274
- │ └── tool-start.ts - tool_execution_start handler
275
- ├── notification.ts - completion nudges, custom renderer
276
- ├── renderer.ts - notification TUI component
277
- ├── ui/agent-menu.ts - /agents slash command menu (orchestration, listing, settings)
278
- ├── ui/agent-config-editor.ts - agent detail view (edit/delete/eject/disable/enable)
279
- ├── ui/agent-creation-wizard.ts - agent creation (AI-generation and manual-form)
280
- ├── ui/agent-file-ops.ts - AgentFileOps interface + FsAgentFileOps implementation
281
- ├── service-adapter.ts - SubagentsService implementation wrapping AgentManager
282
- └── (existing domain modules unchanged)
283
- ```
284
-
285
- Each extracted module receives narrow constructor-injected dependencies rather than closing over module-level state.
286
- Handlers call methods on narrow runtime interfaces - no raw field writes, no `widget!` reach-throughs.
287
-
288
- ## Phase plan
289
-
290
- ### Phase 1: Export `SubagentsService` from this package (#48)
291
-
292
- Added the `SubagentsService` interface, serializable types, `Symbol.for()` accessor functions, and `SUBAGENT_EVENTS` constants as public exports.
293
- Wired `service-adapter.ts` to wrap `AgentManager` and call `publishSubagentsService()` at extension init.
294
-
295
- ### Phase 2: Remove scheduling (#52)
296
-
297
- Deleted `schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`.
298
- Removed the `schedule` parameter from the `Agent` tool schema.
299
- Removed scheduler setup and lifecycle hooks from `index.ts`.
300
-
301
- ### Phase 3: Remove group-join, ad-hoc RPC; replace output-file (#49, #61)
302
-
303
- Deleted `group-join.ts`, `cross-extension-rpc.ts` (#49).
304
- Replaced `output-file.ts` with `SessionManager.create()` + `session-dir.ts` (#61).
305
- Simplified `index.ts` to use direct individual notifications.
306
- Lifecycle events emitted on `pi.events` for external consumers.
307
-
308
- ### Phase 4: Implement and publish `SubagentsService` (#48)
309
-
310
- Wired `service-adapter.ts` to wrap `AgentManager` and call `publishSubagentsService()` at extension init.
311
- Model strings are resolved inside the adapter.
312
-
313
- ### Phase 5: Decompose `index.ts` (#54, #69, #70, #87)
314
-
315
- Extracted tools, notifications, activity tracking, event handlers, and the `/agents` command into separate modules.
316
- Created `SubagentRuntime` factory to hold session-scoped state.
317
-
318
- ### Phase 6 (deferred): Extract UI to `@gotgenes/pi-subagents-ui`
319
-
320
- The widget, conversation viewer, `/agents` command, notifications, and activity tracking are candidates for extraction to a separate extension that consumes `SubagentsService` + lifecycle events.
321
- This phase is deferred until the API boundary is proven stable in production.
481
+ These are fire-and-forget broadcast events no request IDs, no reply channels.
322
482
 
323
- ### Phase 7: Encapsulation and dependency narrowing
483
+ ## Current structural analysis
324
484
 
325
- Every mutable state bag became a class, every dependency bag narrowed to what its consumer uses, every callback became either a method on a collaborator or an event on an observable.
485
+ ### Health metrics
326
486
 
327
- See the [Encapsulation roadmap](#encapsulation-roadmap) section for the full breakdown.
487
+ | Metric | Value |
488
+ | ------------------------- | ---------------------------- |
489
+ | Health score | 75/100 (B) |
490
+ | Total LOC | 7,288 (53 files) |
491
+ | Dead code | 0 files, 0 exports |
492
+ | Maintainability index | 90.7 (good) |
493
+ | Avg cyclomatic complexity | 1.5 |
494
+ | P90 cyclomatic complexity | 2 |
495
+ | Production duplication | 18 lines (1 clone group) |
496
+ | Test duplication | 71 clone groups, 1,424 lines |
328
497
 
329
- ### Phase 8: Testability, display extraction, and menu decomposition
498
+ ### Dependency bag inventory
330
499
 
331
- Eliminated `vi.mock()` module mocking in the two most fragile test suites by injecting IO-touching collaborators; consolidated shared test fixtures; extracted display helpers into a reusable module; decomposed the largest UI file.
500
+ These interfaces carry hidden dependencies that obscure true coupling.
501
+ Bags with 10+ fields are the highest priority for decomposition.
332
502
 
333
- See the [Phase 8 roadmap](#phase-8-roadmap) section for the full breakdown.
503
+ | Interface | Fields | Consumers | Severity |
504
+ | --------------------------- | --------- | ------------------------------------------------- | -------- |
505
+ | `ResolvedSpawnConfig` | 15 | foreground-runner, background-spawner, agent-tool | Critical |
506
+ | `AgentSpawnConfig` | 13 | agent-manager (internal) | Critical |
507
+ | `RunOptions` | 12 | agent-runner | High |
508
+ | `SessionConfig` | 11 | agent-runner (output of assembler) | High |
509
+ | `NotificationDetails` | 10 | notification | Medium |
510
+ | `ResourceLoaderOptions` | 10 | agent-runner (SDK bridge) | Medium |
511
+ | `RunnerIO` | 9 methods | agent-runner | Medium |
512
+ | `CreateSessionOptions` | 9 | agent-runner (SDK bridge) | Medium |
513
+ | `AgentToolDeps` | 8 | agent-tool | Low |
514
+ | `AgentMenuDeps` | 8 | agent-menu | Low |
515
+ | `ConversationViewerOptions` | 8 | conversation-viewer | Low |
516
+ | `AgentRecordInit` | 8 | agent-record | Low |
334
517
 
335
- ### Phase 9: Observation consolidation, ctx elimination, and remaining mocks
518
+ ### Complexity hotspots
336
519
 
337
- Target: consolidate the dual observation model so stats live in one place; remove `ExtensionContext` from all internal APIs; eliminate remaining `vi.mock()` calls and `as any` casts; split widget rendering from lifecycle; apply dependency bag convention.
520
+ Functions with cyclomatic complexity 21 (critical threshold):
338
521
 
339
- See the [Phase 9 roadmap](#phase-9-roadmap) section for the full breakdown.
340
- Issues: #144, #145, #146, #147, #148.
522
+ | Function | Cyclomatic | Cognitive | File | Concern |
523
+ | ------------------- | ---------- | --------- | --------------------------- | ---------------------------------- |
524
+ | `buildContentLines` | 30 | 71 | `ui/conversation-viewer.ts` | Formats session events for display |
525
+ | `renderResult` | 26 | 43 | `tools/agent-tool.ts` | Formats agent result for LLM |
526
+ | `showAgentDetail` | 25 | 33 | `ui/agent-config-editor.ts` | Agent detail/edit view |
527
+ | `renderWidgetLines` | 25 | 44 | `ui/widget-renderer.ts` | Renders widget status lines |
528
+ | `ejectAgent` | 21 | 20 | `ui/agent-config-editor.ts` | Eject agent to filesystem |
529
+ | `update` | 21 | 31 | `ui/agent-widget.ts` | Widget lifecycle + polling |
341
530
 
342
- ## Structural refactoring roadmap
531
+ ### Churn hotspots
343
532
 
344
- Phases 1-5, 7, and 8 are complete.
345
- Phase 6 (UI extraction) is deferred.
346
- See `git log` for the full history; issue references are preserved below for traceability.
533
+ Files with highest commit frequency × complexity (accelerating trend):
347
534
 
348
- | Phase | Issue | Summary |
349
- | ------------------ | ------------------ | --------------------------------------------------------------------- |
350
- | Foundation | #69, #71, #76, #80 | SubagentRuntime, pure assembler, cwd injection, config consolidation |
351
- | Core decomposition | #84, #72, #87, #70 | WorktreeManager, AgentManager DI, runtime methods, handler extraction |
352
- | Interface polish | #66, #77 | SDK types, projectAgentsDir |
353
- | Features | #61 | JSONL session transcripts |
535
+ | Score | File | Commits |
536
+ | ----- | --------------------- | ------- |
537
+ | 85.7 | `index.ts` | 65 |
538
+ | 35.9 | `agent-manager.ts` | 31 |
539
+ | 25.9 | `ui/agent-menu.ts` | 26 |
540
+ | 23.3 | `tools/agent-tool.ts` | 30 |
354
541
 
355
- The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
356
-
357
- ## AgentManager decomposition
358
-
359
- AgentManager was decomposed in three steps to untangle record management, concurrency control, and execution orchestration.
360
-
361
- ### Step 1: Record state machine (#98, #102)
362
-
363
- Extracted status-transition methods (`markRunning`, `markCompleted`, `markAborted`, `markSteered`, `markError`, `markStopped`, `resetForResume`) onto `AgentRecord`.
364
- Replaced scattered field writes across 6 sites with encapsulated transition methods.
365
- Issue #102 consolidated test `AgentRecord` construction into a shared factory.
366
-
367
- ### Step 2: Parent snapshot (#99)
368
-
369
- Replaced live `ctx: ExtensionContext` capture in `SpawnArgs` with an immutable `ParentSnapshot` data object.
370
- The snapshot is taken once at spawn time; queued agents execute against frozen state rather than a potentially stale session reference.
371
- `runAgent()` accepts `ParentSnapshot` instead of `ctx`.
372
- `pi: ExtensionAPI` was removed from `SpawnArgs` - `runAgent()` accepts a `ShellExec` function instead.
373
-
374
- ### Step 3: Session-event observation (#100)
375
-
376
- Replaced three-layer callback threading with direct session subscriptions.
377
- `record-observer.ts` subscribes to the session to update record statistics (tool uses, lifetime usage, compaction count).
378
- `ui/ui-observer.ts` subscribes to the session to stream UI state (active tools, response text, turn count).
379
- `SpawnOptions` and `RunOptions` dropped all `on*` callback fields except `onSessionCreated` (which delivers the session object to enable external subscriptions).
380
-
381
- ### Realized impact
382
-
383
- | Metric | Before | After |
384
- | --------------------------------- | ------ | ----------------------- |
385
- | `SpawnOptions` callback fields | 6 | 1 (`onSessionCreated`) |
386
- | `RunOptions` callback fields | 6 | 1 (`onSessionCreated`) |
387
- | Callback layers | 3 | 0 (direct subscription) |
388
- | Live `ctx` references in queue | 1 | 0 (snapshot) |
389
- | Scattered status-transition sites | 6 | 1 (state machine) |
390
-
391
- ---
392
-
393
- ## Encapsulation roadmap
394
-
395
- Phase 7 encapsulated mutable state into classes, replaced callbacks with semantic components, and narrowed dependency bags.
396
-
397
- Each step was sequenced so it made the next step easier.
398
-
399
- ### Resolved smells
400
-
401
- All nine smells identified at the start of Phase 7 were resolved:
402
-
403
- | Smell | Resolution |
404
- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
405
- | Global mutable state | `AgentTypeRegistry` class (#108); `reloadCustomAgents` callback removed from dep bags |
406
- | Closure bag as class | `NotificationManager` class (#116); `pendingNudges` and timer state are private fields |
407
- | Mutable state bag | `AgentActivityTracker` class (#110); transition methods replace external writes |
408
- | Settings relay | `SettingsManager` class (#109); 6 callback fields collapsed to one object |
409
- | Post-construction mutation | `ExecutionState`, `WorktreeState`, `NotificationState` collaborators (#111); stats behind mutation methods |
410
- | Fire-and-forget callbacks | `AgentManagerObserver` interface (#112); one observer object replaces 3 closure lambdas |
411
- | Duplicate `SpawnOptions` | Internal type renamed to `AgentSpawnConfig` (#113); public `SpawnOptions` unchanged |
412
- | Type dumping ground | `NotificationDetails`, `ParentSnapshot`, `EnvInfo` moved to their natural modules (#116); narrow subsets defined |
413
- | Wide dependency bags | `AgentToolDeps` 9 → 6, `AgentMenuDeps` 8 → 7 (#114); `emitEvent` removed; description text derived from registry; `agentActivity` narrowed |
414
-
415
- ### Step A: Extract state into classes (foundation, parallel)
416
-
417
- These three extractions are independent and can proceed in any order.
418
- Each eliminates a category of global/closure state and gives orphaned callbacks a natural home.
419
-
420
- #### A1. AgentTypeRegistry class (#108)
421
-
422
- Wrapped the module-scoped `agents` Map and free functions in `agent-types.ts` into an injectable class.
423
- `reloadCustomAgents` callback removed from `AgentToolDeps` and `AgentMenuDeps`; replaced by `registry.reload()`.
424
- `DEFAULT_AGENT_NAMES` moved from `types.ts` to the registry.
425
-
426
- #### A2. SettingsManager class (#109, #118)
542
+ ### Production duplication
427
543
 
428
- Encapsulated settings load/save/apply cycle into `SettingsManager` (in `settings.ts`).
429
- Owns `defaultMaxTurns`, `graceTurns`, `maxConcurrent` with normalizing property accessors.
430
- Added `applyMaxConcurrent(n)`, `applyDefaultMaxTurns(n)`, `applyGraceTurns(n)` - each owns the full consequence chain: normalize → set in memory → notify callback → persist → emit event → return toast.
431
- The 6 settings-related fields in `AgentMenuDeps` collapsed to `settings: AgentMenuSettings`.
544
+ One clone group (18 lines) shared between `agent-runner.ts:456-468` and `conversation-viewer.ts:261-278`.
545
+ Both format turn-event content for display identical iteration over message content items, extracting tool names and text.
432
546
 
433
- #### A3. AgentActivityTracker class (#110)
547
+ ### Proposed bag decompositions
434
548
 
435
- Wrapped the 7-field mutable `AgentActivity` interface in an `AgentActivityTracker` class (`src/ui/agent-activity-tracker.ts`).
436
- `ui-observer.ts` calls transition methods; consumers use read-only accessors.
437
- The shared map on `SubagentRuntime` is `Map<string, AgentActivityTracker>`.
549
+ #### ResolvedSpawnConfig (15 fields 3 value objects)
438
550
 
439
- ### Step B: Split AgentRecord lifecycle state (#111)
551
+ This bag mixes three concerns: who the agent is, how it should run, and how it should be displayed.
552
+ Each consumer uses a different subset.
440
553
 
441
- Split post-construction mutation into phase-specific collaborators, each born complete:
442
-
443
- - **`ExecutionState`** (`session`, `outputFile`) - constructed in `onSessionCreated`.
444
- - **`WorktreeState`** (`path`, `branch`, `cleanupResult`) - constructed at worktree setup.
445
- - **`NotificationState`** (`toolCallId`, `resultConsumed`) - constructed by `AgentManager.spawn()` when `toolCallId` is provided.
446
- - **`pendingSteers`** moved to `Map<string, string[]>` on `AgentManager`.
447
- - Stats encapsulated behind mutation methods with read-only getters.
448
- - `AgentRecordInit` trimmed from 19 optional fields to 4 construction-time fields.
449
-
450
- ### Step C: Replace AgentManager callbacks with observer (#112)
451
-
452
- `AgentManagerObserver` interface replaces `onStart`/`onComplete`/`onCompact`.
453
- `index.ts` constructs one observer object instead of 3 closure lambdas.
454
- `AgentManagerOptions` drops from 9 → 7 fields.
455
-
456
- ### Step D: Disambiguate SpawnOptions and narrow dependency bags
457
-
458
- #### D1. Disambiguate SpawnOptions (#113)
459
-
460
- Internal `SpawnOptions` in `agent-manager.ts` renamed to `AgentSpawnConfig`.
461
- Public `SpawnOptions` in `service.ts` unchanged.
462
-
463
- #### D2. Narrow AgentToolDeps and AgentMenuDeps (#114)
464
-
465
- | Bag | Before | After | How |
466
- | --------------- | -------- | ----- | ----------------------------------------------------------------------------------------------------------- |
467
- | `AgentToolDeps` | 9 fields | 6 | `emitEvent` → observer; `typeListText`/`availableTypesText` derived from registry; `agentActivity` narrowed |
468
- | `AgentMenuDeps` | 8 fields | 7 | Dead `emitEvent` removed; `agentActivity` narrowed to read-only `AgentActivityReader` |
469
-
470
- ### Step E: Decompose large files and relocate types
471
-
472
- #### E1. Split agent-tool.ts foreground/background (#115)
473
-
474
- Extracted `foreground-runner.ts` (~175 lines) and `background-spawner.ts` (~116 lines).
475
- `agent-tool.ts` reduced from 579 → 411 lines.
476
-
477
- #### E2. Type housekeeping (#116)
478
-
479
- - Moved `NotificationDetails`, `ParentSnapshot`, `EnvInfo` to their natural modules.
480
- - Converted `createNotificationSystem` closure to `NotificationManager` class.
481
- - Converted `ConversationViewer` constructor from 7 positional parameters to `ConversationViewerOptions` bag.
482
- - Defined `AgentIdentity` and `AgentPromptConfig` narrow subsets; `buildAgentPrompt` narrowed to `AgentPromptConfig`.
483
-
484
- ### Phase 7 results
485
-
486
- | Metric | Before | After |
487
- | ------------------------------------------ | ------ | ----- |
488
- | Module-scoped mutable state | 1 | 0 |
489
- | Closure-bag "classes" | 2 | 0 |
490
- | Externally-mutated state bags | 2 | 0 |
491
- | `AgentManagerOptions` fields | 9 | 7 |
492
- | `AgentToolDeps` fields | 9 | 6 |
493
- | `AgentMenuDeps` fields | 13 | 7 |
494
- | `SpawnOptions` callback fields | 6 | 1 |
495
- | `RunOptions` callback fields | 6 | 1 |
496
- | Callbacks threaded through deps | 8 | 0 |
497
- | Types in `types.ts` without a natural home | 4 | 0 |
554
+ ```typescript
555
+ /** Who this agent is — type resolution result. */
556
+ interface SpawnIdentity {
557
+ subagentType: string;
558
+ rawType: SubagentType;
559
+ fellBack: boolean;
560
+ displayName: string;
561
+ }
498
562
 
499
- ### Dependency graph
563
+ /** How the agent should run — execution parameters. */
564
+ interface SpawnExecution {
565
+ prompt: string;
566
+ description: string;
567
+ model: Model<any> | undefined;
568
+ effectiveMaxTurns: number | undefined;
569
+ thinking: ThinkingLevel | undefined;
570
+ inheritContext: boolean;
571
+ runInBackground: boolean;
572
+ isolated: boolean;
573
+ isolation: IsolationMode | undefined;
574
+ agentInvocation: AgentInvocation;
575
+ }
500
576
 
501
- ```mermaid
502
- flowchart LR
503
- A1["A1: Registry"] --> D2["D2: Narrow deps"]
504
- A2["A2: Settings"] --> A2b["A2b: Apply"] --> D2
505
- A3["A3: Activity Tracker"] --> D2
506
- B["B: Record lifecycle"] --> D2
507
- B --> C["C: Observer"] --> D1["D1: SpawnOptions"] --> D2
508
- D2 --> E1["E1: agent-tool split"]
509
- A1 --> E2["E2: Type housekeeping"]
577
+ /** How the agent is presented — display metadata. */
578
+ interface SpawnPresentation {
579
+ modelName: string | undefined;
580
+ agentTags: string[];
581
+ detailBase: Pick<AgentDetails, ...>;
582
+ }
510
583
  ```
511
584
 
512
- ---
513
-
514
- ## Phase 8 roadmap
515
-
516
- Phase 7 eliminated all structural smells (mutable state, closure bags, callback threading, wide dependency bags).
517
- Phase 8 targeted the next layer: testability friction, display module cohesion, and menu decomposition.
585
+ `foreground-runner` and `background-spawner` primarily consume `SpawnExecution` + `SpawnIdentity`.
586
+ `agent-tool` uses all three to build the `AgentSpawnConfig` and the result text.
587
+ After decomposition, each consumer declares its real dependencies explicitly.
518
588
 
519
- Steps G and H eliminated 11 of the original 12 `vi.mock()` calls in the runner tests, removing fragile call-sequence assertions in favour of injected stubs.
520
- Step G resolved `session-config.test.ts`; Step H resolved both `agent-runner.test.ts` and `agent-runner-extension-tools.test.ts`.
589
+ #### AgentSpawnConfig (13 fields extract ParentSessionInfo)
521
590
 
522
- The display and menu improvements were identified during Phase 7 but deferred because they did not gate encapsulation work.
523
- The display extraction unblocked menu decomposition.
591
+ Several fields form a natural cluster around parent session identity:
524
592
 
525
- ### Test pain points (resolved)
526
-
527
- | Symptom | Resolution |
528
- | ------------------------------------------------------------- | -------------------------------------------------------------- |
529
- | 7 `vi.mock()` calls in `agent-runner.test.ts` | Step H (#133): injected `RunnerIO` stubs |
530
- | 7 `vi.mock()` calls in `agent-runner-extension-tools.test.ts` | Step H (#133): same |
531
- | 52 `as any` casts across test suite | Step I (#134): reduced to 15 |
532
- | 3× duplicated `mockSession()` | Step F (#131): shared `createMockSession()` in `test/helpers/` |
533
- | 3× duplicated `makeDeps()` | Step F (#131): shared `createToolDeps()` in `test/helpers/` |
534
-
535
- The well-designed test suites - `agent-manager.test.ts` (1 mock, DI via `AgentRunner` interface), `notification.test.ts` (0 mocks, pure functions + DI), and `agent-tool.test.ts` (0 mocks, tests via deps bag) - confirmed the pattern: modules that accept collaborators through injection produce resilient tests; modules that import collaborators directly produce fragile mock-heavy tests.
536
-
537
- ### Step F: Shared test fixtures (#131)
538
-
539
- Consolidated duplicated mock factories into `test/helpers/`.
540
-
541
- 1. `createMockSession()` - subscribable event bus with `emit()` helper; replaced 3 hand-rolled copies.
542
- 2. `createToolDeps()` - builds `AgentToolDeps` with sensible defaults and override support; replaced 3 `makeDeps()` copies.
543
- 3. `makeRecord()` - `AgentRecord` factory with sensible defaults; replaced scattered inline construction.
544
- 4. `STUB_CTX` - shared stub `ExtensionContext` constant; centralised unavoidable bridge casts.
545
-
546
- Impact: reduced test boilerplate; single source of truth for mock shapes; changes to dep interfaces propagate automatically.
547
-
548
- ### Step G: Inject IO collaborators into session-config (#132)
549
-
550
- `assembleSessionConfig` now accepts `io: AssemblerIO` as a required parameter.
551
- `index.ts` constructs the real `AssemblerIO` from direct imports via the `RunnerIO.assemblerIO` field (wired in Step H).
552
- `session-config.test.ts` injects stubs - all 4 `vi.mock()` calls eliminated, assertions shifted to `SessionConfig` output properties.
553
-
554
- ### Step H: Inject SDK boundary into agent-runner (#133)
555
-
556
- `runAgent()` now accepts `io: RunnerIO` as a required parameter bundling all IO collaborators: `detectEnv`, `getAgentDir`, `createResourceLoader`, `deriveSessionDir`, `createSessionManager`, `createSettingsManager`, `createSession`, and `assemblerIO`.
557
-
558
- `createAgentRunner(io: RunnerIO): AgentRunner` factory captures the boundary at construction time so `AgentManager` and the `AgentRunner` interface remain unchanged.
559
- `index.ts` constructs the real `RunnerIO` from Pi SDK imports and sibling modules.
560
-
561
- Impact: all 7 `vi.mock()` calls eliminated from both `agent-runner.test.ts` and `agent-runner-extension-tools.test.ts`; tests verify behavior (turn limits, tool filtering, response collection) through injected stubs; SDK imports moved to the extension entry point.
562
-
563
- ### Step I: Reduce `as any` casts in tests (#134)
564
-
565
- Reduced `as any` count from 93 to 15 (plus 13 explicit `as unknown as T` bridge casts).
566
-
567
- Production changes:
568
-
569
- - `ResourceLoaderOptions.appendSystemPromptOverride` typed to match `DefaultResourceLoaderOptions`; `createResourceLoader` factory cast removed from `index.ts`.
570
- - `CreateSessionOptions.settingsManager` / `RunnerIO.createSettingsManager` typed as `SettingsManager`.
571
- - `WidgetLike` interface in `runtime.ts` narrows the widget field.
572
- - Local `ToolCallContent` / `BashExecutionMessage` type guards replace `as any` duck-typing in `conversation-viewer.ts` and `agent-runner.ts`.
573
- - `textResult()` return no longer casts `details as any`.
574
- - `toAgentSession()` helper and `STUB_CTX` constant centralise unavoidable bridge casts.
575
-
576
- Remaining 15 `as any` casts are: 8 menu-handler `ctx as any` (deferred - requires `AgentManager.spawn` to accept `ParentSnapshot` directly), 2 `print-mode.test.ts` (same ExtensionContext/API pattern), 2 private-field test access, 1 `createSession` SDK bridge in `index.ts`, 1 `foreground-runner.ts` `AgentToolResult<any>` detail, 1 `stub-ctx.ts` comment.
577
-
578
- ### Step J: Extract display helpers (#135)
579
-
580
- `ui/display.ts` now contains all pure formatters, display helpers, constants, and shared types (`Theme`, `AgentDetails`).
581
- `agent-widget.ts` dropped from 522 → ~340 lines.
582
- All consumer modules (menu, tools, renderer, conversation viewer) import from `ui/display.ts` directly.
583
- `test/agent-widget.test.ts` renamed to `test/display.test.ts`.
584
-
585
- ### Step K: Decompose agent-menu.ts (#136)
586
-
587
- `agent-menu.ts` (668 lines) decomposed into four modules:
593
+ ```typescript
594
+ /** Parent session identity — always travel together. */
595
+ interface ParentSessionInfo {
596
+ parentSessionFile?: string;
597
+ parentSessionId?: string;
598
+ toolCallId?: string;
599
+ }
600
+ ```
588
601
 
589
- 1. `ui/agent-file-ops.ts` - `AgentFileOps` interface (`exists`, `read`, `write`, `remove`, `ensureDir`, `findAgentFile`) + `FsAgentFileOps` production implementation.
590
- 2. `ui/agent-config-editor.ts` - `showAgentDetail` with edit/delete/reset/eject/disable/enable transitions (~200 lines).
591
- 3. `ui/agent-creation-wizard.ts` - AI-generation and manual-form creation paths (~250 lines).
592
- 4. `ui/agent-menu.ts` - menu orchestration, agent listing, running-agent viewer, settings form (~300 lines).
602
+ Extracting this from `AgentSpawnConfig` reduces it from 13 to 10 fields and introduces a named concept that currently exists only as scattered optional fields.
593
603
 
594
- Impact: `agent-menu.ts` dropped from 668 296 lines; extracted modules receive `AgentFileOps` via injection; `vi.mock("node:fs")` eliminated from `agent-menu.test.ts`.
604
+ #### RunOptions (12 fieldsextract RunContext)
595
605
 
596
- ### Step dependencies
606
+ The `RunOptions` bag mixes execution parameters with context information:
597
607
 
598
- ```mermaid
599
- flowchart LR
600
- subgraph testability["Testability track"]
601
- F["F: Shared fixtures"] --> G["G: session-config IO"] --> H["H: agent-runner SDK"] --> I["I: Reduce as-any"]
602
- end
603
- subgraph display["Display track"]
604
- J["J: Display extraction"] --> K["K: Menu decomposition"]
605
- end
608
+ ```typescript
609
+ /** Parent context needed to configure the child session. */
610
+ interface RunContext {
611
+ cwd?: string;
612
+ parentSessionFile?: string;
613
+ parentSessionId?: string;
614
+ exec: ShellExec;
615
+ registry: AgentConfigLookup;
616
+ }
606
617
  ```
607
618
 
608
- The two tracks are independent and can proceed in parallel.
609
-
610
- ---
619
+ The remaining `RunOptions` fields (`model`, `maxTurns`, `signal`, `isolated`, `thinkingLevel`, `defaultMaxTurns`, `graceTurns`, `onSessionCreated`) are genuine execution parameters.
611
620
 
612
- ## Phase 9 roadmap
621
+ #### SessionConfig (11 fields → extract ToolFilterConfig)
613
622
 
614
- Phases 7 and 8 addressed structural encapsulation and testability.
615
- Phase 9 targets the next layer: observation model consolidation, `ExtensionContext` elimination from internal APIs, remaining `vi.mock()` / `as any` casts, and dependency bag cleanup.
623
+ Three fields form a cohesive tool-filtering cluster:
616
624
 
617
- ### Current smells
618
-
619
- | Smell | Location | Evidence | Severity |
620
- | ------------------------------------------------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------- |
621
- | `execute` does config resolution for its callees | `agent-tool.ts` (145-line `execute`) | ~60 lines unpack config, resolve model, compute metadata, repack into 16-field bags for spawners; `ctx` threaded 4 layers deep | Medium |
622
- | ~~Wide `ctx` in menu handlers~~ | ~~`agent-menu.ts`, `agent-config-editor.ts`, `agent-creation-wizard.ts`~~ | Resolved by #146: `MenuUI` interface introduced; 42 `ctx as any` casts eliminated across 5 test files | Done |
623
- | Direct SDK import in `conversation-viewer.ts` | `conversation-viewer.test.ts` | Hoisted `vi.mock("@earendil-works/pi-tui")` to intercept `wrapTextWithAnsi` | Low |
624
- | ~~Widget mixes rendering, lifecycle, and state~~ | ~~`agent-widget.ts` (370 lines)~~ | Resolved by #148: rendering extracted to `widget-renderer.ts`; widget is now 198 lines | Done |
625
- | `deps.` prefix noise in function bodies | remaining modules across tools, UI, service-adapter | Functions accept a `deps` bag and access every field as `deps.foo`; hides real dependencies and lengthens every call line | Low |
625
+ ```typescript
626
+ /** Tool filtering configuration — used by filterActiveTools. */
627
+ interface ToolFilterConfig {
628
+ toolNames: string[];
629
+ disallowedSet: Set<string> | undefined;
630
+ extensions: boolean | string[];
631
+ }
632
+ ```
626
633
 
627
- ### Dependency bag convention
634
+ Extracting this reduces `SessionConfig` from 11 to 8 fields and gives `filterActiveTools` a named input type instead of three positional parameters.
628
635
 
629
- Applied incrementally as each step touches a module:
636
+ #### RunnerIO (9 methods 2 focused interfaces)
630
637
 
631
- - **≤4 fields** - accept as plain parameters; drop the interface.
632
- - **≥5 fields** - keep a named interface but destructure in the function signature (`{ manager, widget }: ForegroundDeps`) so the function body uses bare names, not `deps.foo`.
638
+ The IO boundary mixes environment discovery with session factory operations:
633
639
 
634
- This eliminates the `deps.` prefix noise across ~124 callsites in 12 modules.
640
+ ```typescript
641
+ /** Environment discovery — detecting paths and platform info. */
642
+ interface EnvironmentIO {
643
+ detectEnv: (exec: ShellExec, cwd: string) => Promise<EnvInfo>;
644
+ getAgentDir: () => string;
645
+ deriveSessionDir: (parentSessionFile: string | undefined, effectiveCwd: string) => string;
646
+ }
635
647
 
636
- ### Step L: Consolidate observation model (#144)
648
+ /** Session factory creating SDK objects. */
649
+ interface SessionFactoryIO {
650
+ createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoaderLike;
651
+ createSessionManager: (cwd: string, sessionDir: string) => SessionManagerLike;
652
+ createSettingsManager: (cwd: string, agentDir: string) => SettingsManager;
653
+ createSession: (opts: CreateSessionOptions) => Promise<{ session: AgentSession }>;
654
+ assemblerIO: AssemblerIO;
655
+ }
656
+ ```
637
657
 
638
- Removed `_toolUses` and `_lifetimeUsage` from `AgentActivityTracker`.
639
- UI consumers read stats from `AgentRecord` instead of the tracker.
640
- The UI observer retains event subscriptions for re-render triggers but no longer accumulates stats independently.
658
+ The runner would accept `EnvironmentIO & SessionFactoryIO` (keeping backward compatibility) while each piece can be tested independently.
641
659
 
642
- Add `session` and `outputFile` convenience getters on `AgentRecord` to hide the `execution?.` traversal.
643
- The 15+ callsites that navigate `record.execution?.session` simplify to `record.session`.
660
+ ## Improvement roadmap (Phase 10)
644
661
 
645
- Apply the dependency bag convention to touched modules: `NotificationDeps` (4 fields) becomes plain parameters on `NotificationManager` constructor.
662
+ Phase 10 addresses the structural gaps identified in this analysis: flat code organization, oversized dependency bags, and complexity hotspots.
646
663
 
647
- Impact: eliminates dual counting; removes `??` fallback pattern from widget and conversation viewer; hides `ExecutionState` structure from consumers.
664
+ ### Step 1: Reorganize source into domain directories ([#164][164])
648
665
 
649
- ### Step M: Decompose execute and push ExtensionContext to the boundary (#145)
666
+ Move files into `config/`, `session/`, `lifecycle/`, `observation/`, and `service/` subdirectories as described in the [proposed directory restructuring](#proposed-directory-restructuring).
650
667
 
651
- Extracted config resolution into `resolveSpawnConfig` (pure function in `spawn-config.ts`).
652
- Injected three collaborators (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) into `createAgentTool` so `execute` no longer reads `ctx` beyond `ctx.ui` (already delegated to `widget.setUICtx`).
653
- `AgentManager.spawn()` and `spawnAndWait()` accept `ParentSnapshot` instead of `ExtensionContext`.
654
- `service-adapter.ts` calls `buildParentSnapshot(session.ctx)` at its boundary.
655
- `foreground-runner` and `background-spawner` receive `ResolvedSpawnConfig` + domain values (`snapshot`, `parentSessionFile`, `parentSessionId`) instead of `ctx`.
668
+ This is a mechanical change (file moves + import path updates) that:
656
669
 
657
- Dissolved `ForegroundDeps`, `BackgroundDeps`, and `AdapterDeps` into plain parameters.
658
- `AgentToolDeps` is destructured in the `createAgentTool` signature.
670
+ - Makes the domain model visible in the filesystem.
671
+ - Reduces cognitive load when navigating the codebase (5 root files + 8 directories vs. 31 root files + 3 directories).
672
+ - Co-locates related files, making subsequent refactoring easier.
659
673
 
660
- After this step, `ExtensionContext` appears only in:
674
+ ### Step 2: Decompose ResolvedSpawnConfig ([#165][165])
661
675
 
662
- - `index.ts` closures (wired at extension startup)
663
- - `service-adapter.ts` (cross-extension boundary)
664
- - Menu handlers (addressed by Step N)
676
+ Split the 15-field bag into `SpawnIdentity`, `SpawnExecution`, and `SpawnPresentation`.
677
+ Each consumer declares its real dependencies.
678
+ Enables Step 3 (narrowing AgentSpawnConfig, [#166][166]).
665
679
 
666
- Impact: `execute` dropped from ~145 to ~25 lines; eliminated 16-field parameter bags; eliminated `vi.mock("../src/parent-snapshot.js")` in `agent-manager.test.ts`; foreground/background runner tests no longer need `ctx` mocks; `AgentManager` operates entirely on domain types.
680
+ ### Step 3: Extract ParentSessionInfo from AgentSpawnConfig ([#166][166])
667
681
 
668
- ### Step N: Narrow UI context for menu handlers (#146) ✓
682
+ Extract `parentSessionFile`, `parentSessionId`, `toolCallId` into a `ParentSessionInfo` value object.
683
+ Reduces AgentSpawnConfig from 13 to 10 fields.
669
684
 
670
- Defined `MenuUI` interface (exported from `agent-menu.ts`) with `select`, `confirm`, `input`, `notify`, `editor`, and `custom` methods — the exact subset `ctx.ui` methods used by menu handlers.
671
- All inner functions in `agent-menu.ts`, `agent-config-editor.ts`, and `agent-creation-wizard.ts` now accept `(ui: MenuUI)` instead of `(ctx: ExtensionContext)`.
672
- `index.ts` passes `ctx.ui`, `ctx.modelRegistry`, and `buildParentSnapshot(ctx)` to the handler.
685
+ ### Step 4: Narrow RunnerIO ([#167][167])
673
686
 
674
- `AgentMenuManager.spawnAndWait` and `WizardManager.spawnAndWait` both accept `ParentSnapshot` (enabled by Step M).
675
- Creation wizard threads `parentSnapshot` from `showCreateWizard(ui, parentSnapshot)` → `showGenerateWizard(ui, parentSnapshot, targetDir)` → `manager.spawnAndWait(parentSnapshot, ...)`.
687
+ Split into `EnvironmentIO` and `SessionFactoryIO`.
688
+ Each half can be tested independently.
676
689
 
677
- Applied the dependency bag convention:
690
+ ### Step 5: Extract ToolFilterConfig from SessionConfig ([#168][168])
678
691
 
679
- - `AgentConfigEditorDeps` (4 fields), `GetResultDeps` (4 fields), `SteerToolDeps` (4 fields) dissolved into plain parameters.
680
- - `AgentMenuDeps` (8 fields) and `AgentCreationWizardDeps` (5 fields) kept as interfaces, destructured in the function signature.
692
+ Extract the tool-filtering cluster into `ToolFilterConfig`.
693
+ Give `filterActiveTools` a named input type.
681
694
 
682
- After Steps M and N, `ExtensionContext` appears only at true boundaries: `index.ts` closures and `service-adapter.ts` (cross-extension bridge).
695
+ ### Step 6: Extract RunContext from RunOptions ([#169][169])
683
696
 
684
- Impact: eliminated 42 `ctx as any` casts across 5 test files (`agent-menu.test.ts`: 8, `agent-config-editor.test.ts`: 20, `agent-creation-wizard.test.ts`: 14); tests construct plain `MenuUI`-shaped objects with no cast.
697
+ Extract context fields into `RunContext`.
698
+ Reduces RunOptions from 12 to 7 fields.
685
699
 
686
- ### Step O: Inject text wrapping into ConversationViewer (#147)
700
+ ### Step 7: Reduce buildContentLines complexity ([#170][170])
687
701
 
688
- Accept a `wrapText` function via `ConversationViewerOptions`.
689
- `index.ts` passes the real `wrapTextWithAnsi` import.
690
- Tests inject a stub or the real function directly - no module-level mock needed.
702
+ `buildContentLines` in `conversation-viewer.ts` has cognitive complexity 71.
703
+ Extract formatting sub-functions for each content type (tool calls, text, bash output).
691
704
 
692
- Apply the dependency bag convention: `ConversationViewerOptions` is destructured in the constructor signature.
705
+ ### Step 8: Reduce renderResult complexity ([#171][171])
693
706
 
694
- Impact: eliminates the hoisted `vi.mock("@earendil-works/pi-tui")` in `conversation-viewer.test.ts`.
707
+ `renderResult` in `agent-tool.ts` has cognitive complexity 43.
708
+ Extract result formatting by status (completed, error, aborted, stopped).
695
709
 
696
- ### Step P: Split AgentWidget rendering (#148)
710
+ ### Step 9: Extract shared turn-formatting logic ([#172][172])
697
711
 
698
- Extracted pure rendering functions (`renderWidgetLines`, `renderFinishedLine`, `renderRunningLines`) from `AgentWidget` into `ui/widget-renderer.ts`.
699
- The widget is now a thin lifecycle/polling wrapper (198 lines, down from 374) that delegates to pure render functions.
700
- Rendering functions receive data (agent list, activity map, registry) and return formatted strings - testable without widget lifecycle. 23 new unit tests cover all status variants, overflow, tree connectors, and empty states.
712
+ The 18-line production clone between `agent-runner.ts` and `conversation-viewer.ts` extracts into a shared function in the session domain.
701
713
 
702
714
  ### Step dependencies
703
715
 
704
716
  ```mermaid
705
717
  flowchart LR
706
- subgraph observation["Observation track"]
707
- L["L: Consolidate observation #144"] --> P["P: Split widget rendering #148"]
718
+ subgraph organization["Code organization"]
719
+ S1["#164: Domain directories"]
708
720
  end
709
- subgraph ctx["ctx elimination track"]
710
- M["M: Decompose execute / push ctx #145"] --> N["N: Narrow UI context #146"]
721
+ subgraph bags["Dependency bags"]
722
+ S2["#165: ResolvedSpawnConfig"] --> S3["#166: AgentSpawnConfig"]
723
+ S4["#167: RunnerIO"]
724
+ S5["#168: SessionConfig"]
725
+ S6["#169: RunOptions"]
711
726
  end
712
- O["O: Inject text wrapping #147"]
727
+ subgraph complexity["Complexity reduction"]
728
+ S7["#170: buildContentLines"]
729
+ S8["#171: renderResult"]
730
+ S9["#172: Shared turn-formatting"]
731
+ end
732
+ S1 --> S2 & S4 & S5 & S6
733
+ S1 --> S7 & S8 & S9
713
734
  ```
714
735
 
715
- The three tracks are independent of each other.
716
-
717
- ### Projected impact
718
-
719
- | Metric | Before | After |
720
- | ---------------------------------- | ------------------------ | ------------------------ |
721
- | `vi.mock()` calls remaining | 4 | 1 (`print-mode.test.ts`) |
722
- | `as any` casts remaining | 45 | ~5 |
723
- | Independent tool-use counters | 2 | 1 |
724
- | `record.execution?.` traversals | 15+ | 0 |
725
- | `ExtensionContext` in domain types | 1 (`AgentManager.spawn`) | 0 |
726
- | `deps.` prefix accesses | ~124 | 0 |
736
+ Step 1 ([#164][164], directory restructuring) unblocks all other steps by co-locating related files.
737
+ Steps 2–6 (bag decomposition) and Steps 7–9 (complexity reduction) are independent tracks that can proceed in parallel.
738
+ Within the bag track, Step 2 ([#165][165], ResolvedSpawnConfig) enables Step 3 ([#166][166], AgentSpawnConfig).
739
+
740
+ ## Refactoring history
741
+
742
+ Phases 1–5 and 7–9 are complete.
743
+ Phase 6 (UI extraction to a separate package) is deferred.
744
+ Detailed records are preserved in per-phase history files:
745
+
746
+ | Phase | Title | Status | History |
747
+ | ----- | --------------------------------------------------- | -------- | -------------------------------------------------------------------------- |
748
+ | 1 | Export SubagentsService API boundary | Complete | [phase-1-api-boundary.md](history/phase-1-api-boundary.md) |
749
+ | 2 | Remove scheduling subsystem | Complete | [phase-2-remove-scheduling.md](history/phase-2-remove-scheduling.md) |
750
+ | 3 | Remove group-join, RPC; replace output-file | Complete | [phase-3-remove-rpc-groupjoin.md](history/phase-3-remove-rpc-groupjoin.md) |
751
+ | 4 | Implement and publish SubagentsService | Complete | [phase-4-implement-service.md](history/phase-4-implement-service.md) |
752
+ | 5 | Decompose index.ts | Complete | [phase-5-decompose-index.md](history/phase-5-decompose-index.md) |
753
+ | 6 | Extract UI to separate package | Deferred | — |
754
+ | 7 | Encapsulation and dependency narrowing | Complete | [phase-7-encapsulation.md](history/phase-7-encapsulation.md) |
755
+ | 8 | Testability, display extraction, menu decomposition | Complete | [phase-8-testability.md](history/phase-8-testability.md) |
756
+ | 9 | Observation consolidation, ctx elimination | Complete | [phase-9-observation-ctx.md](history/phase-9-observation-ctx.md) |
757
+
758
+ ### Structural refactoring issues
759
+
760
+ | Phase | Issue | Summary |
761
+ | ------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
762
+ | Foundation | #69, #71, #76, #80 | SubagentRuntime, pure assembler, cwd injection, config consolidation |
763
+ | Core decomposition | #84, #72, #87, #70 | WorktreeManager, AgentManager DI, runtime methods, handler extraction |
764
+ | Interface polish | #66, #77 | SDK types, projectAgentsDir |
765
+ | Features | #61 | JSONL session transcripts |
766
+ | AgentManager | #98, #99, #100, #102 | Record state machine, ParentSnapshot, session-event observation, test factory |
767
+ | 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 |
768
+ | Testability | #131, #132, #133, #134, #135, #136 | Shared fixtures, session-config IO, runner SDK boundary, as-any reduction, display extraction, menu decomposition |
769
+ | Observation/ctx | #144, #145, #146, #147, #148 | Observation consolidation, execute decomposition, UI context, text wrapping injection, widget rendering split |
727
770
 
728
- ---
771
+ The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
729
772
 
730
773
  ## Relationship with upstream
731
774
 
732
- This fork (`@gotgenes/pi-subagents` in the [gotgenes/pi-packages] monorepo) is now a hard fork of [tintinweb/pi-subagents].
775
+ This fork (`@gotgenes/pi-subagents` in the [gotgenes/pi-packages] monorepo) is a hard fork of [tintinweb/pi-subagents].
733
776
  The decomposition diverges materially from upstream's direction.
734
777
 
735
778
  The three upstream PRs (#71, #72, #73) remain open.
@@ -742,3 +785,13 @@ The upstream test suite is run periodically as a regression canary for the agent
742
785
  [earendil-works/pi#4207]: https://github.com/earendil-works/pi/issues/4207
743
786
  [gotgenes/pi-packages]: https://github.com/gotgenes/pi-packages
744
787
  [tintinweb/pi-subagents]: https://github.com/tintinweb/pi-subagents
788
+
789
+ [164]: https://github.com/gotgenes/pi-packages/issues/164
790
+ [165]: https://github.com/gotgenes/pi-packages/issues/165
791
+ [166]: https://github.com/gotgenes/pi-packages/issues/166
792
+ [167]: https://github.com/gotgenes/pi-packages/issues/167
793
+ [168]: https://github.com/gotgenes/pi-packages/issues/168
794
+ [169]: https://github.com/gotgenes/pi-packages/issues/169
795
+ [170]: https://github.com/gotgenes/pi-packages/issues/170
796
+ [171]: https://github.com/gotgenes/pi-packages/issues/171
797
+ [172]: https://github.com/gotgenes/pi-packages/issues/172