@gotgenes/pi-subagents 5.5.0 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,37 @@ 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
+ ## [5.7.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.6.0...pi-subagents-v5.7.0) (2026-05-20)
9
+
10
+
11
+ ### Features
12
+
13
+ * add session-context methods to SubagentRuntime ([3bbc6af](https://github.com/gotgenes/pi-packages/commit/3bbc6af5c3d9e6faef2112f6c92f991c4b27e38d))
14
+ * add widget delegation methods to SubagentRuntime ([36350f4](https://github.com/gotgenes/pi-packages/commit/36350f413f44f432b14b0a42e27df2c8f5444a08))
15
+
16
+
17
+ ### Documentation
18
+
19
+ * add [#87](https://github.com/gotgenes/pi-packages/issues/87) to architecture roadmap and fix package scope hallucinations ([ddee1a0](https://github.com/gotgenes/pi-packages/commit/ddee1a011acc3e20d17d2bdf46cc4603604c092d))
20
+ * plan evolving SubagentRuntime from data bag to object with methods ([#87](https://github.com/gotgenes/pi-packages/issues/87)) ([4f2f16a](https://github.com/gotgenes/pi-packages/commit/4f2f16a8eea67e4959a754d172a9238b4b8662b1))
21
+ * plan extract event handlers from index.ts ([#70](https://github.com/gotgenes/pi-packages/issues/70)) ([5fc115f](https://github.com/gotgenes/pi-packages/commit/5fc115f61bc52c5c88630ea7c869e1e34bde0130))
22
+ * **retro:** add retro notes for issue [#72](https://github.com/gotgenes/pi-packages/issues/72) ([5b53189](https://github.com/gotgenes/pi-packages/commit/5b53189d6242b6f2262b9e7cd2d9dbdbe2a28c60))
23
+ * update architecture roadmap with [#72](https://github.com/gotgenes/pi-packages/issues/72), [#76](https://github.com/gotgenes/pi-packages/issues/76), [#80](https://github.com/gotgenes/pi-packages/issues/80), [#84](https://github.com/gotgenes/pi-packages/issues/84) status ([176cb68](https://github.com/gotgenes/pi-packages/commit/176cb682001b21b7c223656b353b8cf2af412b0c))
24
+
25
+ ## [5.6.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.5.0...pi-subagents-v5.6.0) (2026-05-20)
26
+
27
+
28
+ ### Features
29
+
30
+ * convert AgentManager to options-bag constructor with DI ([1292cec](https://github.com/gotgenes/pi-packages/commit/1292cec60c24d8e657985c53b9b3413089c2a79d))
31
+ * define AgentRunner interface in agent-runner.ts ([6a3c85a](https://github.com/gotgenes/pi-packages/commit/6a3c85a445daf0e4e8c01620eb6ae1a8237f1766))
32
+
33
+
34
+ ### Documentation
35
+
36
+ * mark [#84](https://github.com/gotgenes/pi-packages/issues/84) as done in plan ([#72](https://github.com/gotgenes/pi-packages/issues/72)) ([5cfa1ec](https://github.com/gotgenes/pi-packages/commit/5cfa1ecf080f95fbd6b4aec05b27cc9672f60267))
37
+ * **retro:** add retro notes for issue [#84](https://github.com/gotgenes/pi-packages/issues/84) ([99d9016](https://github.com/gotgenes/pi-packages/commit/99d90161df5fe9d302514e33c2d2d9fbfb248f25))
38
+
8
39
  ## [5.5.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.4.1...pi-subagents-v5.5.0) (2026-05-20)
9
40
 
10
41
 
@@ -59,7 +59,7 @@ There is also a `Symbol.for("pi-subagents:manager")` export on `globalThis` that
59
59
 
60
60
  ```text
61
61
  ┌────────────────────────────────────────────────────────┐
62
- │ @earendil-works/pi-subagents (this package)
62
+ │ @gotgenes/pi-subagents (this package)
63
63
  │ │
64
64
  │ Exports: │
65
65
  │ SubagentsAPI interface │
@@ -131,7 +131,7 @@ After removal and `index.ts` decomposition, the core shrinks from ~6,300 to ~5,4
131
131
 
132
132
  ## SubagentsAPI
133
133
 
134
- The `SubagentsAPI` interface, accessor functions, and serializable types are exported directly from this package (`@earendil-works/pi-subagents`).
134
+ The `SubagentsAPI` interface, accessor functions, and serializable types are exported directly from this package (`@gotgenes/pi-subagents`).
135
135
  No separate API package is needed.
136
136
 
137
137
  Consumers declare this package as an optional peer dependency:
@@ -139,10 +139,10 @@ Consumers declare this package as an optional peer dependency:
139
139
  ```json
140
140
  {
141
141
  "peerDependencies": {
142
- "@earendil-works/pi-subagents": ">=2.0.0"
142
+ "@gotgenes/pi-subagents": ">=2.0.0"
143
143
  },
144
144
  "peerDependenciesMeta": {
145
- "@earendil-works/pi-subagents": { "optional": true }
145
+ "@gotgenes/pi-subagents": { "optional": true }
146
146
  }
147
147
  }
148
148
  ```
@@ -150,7 +150,7 @@ Consumers declare this package as an optional peer dependency:
150
150
  At runtime, consumers use dynamic import for type-safe access to the accessor functions:
151
151
 
152
152
  ```typescript
153
- const { getSubagentsAPI } = await import("@earendil-works/pi-subagents");
153
+ const { getSubagentsAPI } = await import("@gotgenes/pi-subagents");
154
154
  const api = getSubagentsAPI();
155
155
  if (api) {
156
156
  api.spawn("Explore", "Check for stale TODOs");
@@ -238,14 +238,14 @@ They are fire-and-forget broadcast events — no request IDs, no reply channels.
238
238
 
239
239
  ```typescript
240
240
  // package.json:
241
- // "peerDependencies": { "@earendil-works/pi-subagents": ">=2.0.0" }
242
- // "peerDependenciesMeta": { "@earendil-works/pi-subagents": { "optional": true } }
241
+ // "peerDependencies": { "@gotgenes/pi-subagents": ">=2.0.0" }
242
+ // "peerDependenciesMeta": { "@gotgenes/pi-subagents": { "optional": true } }
243
243
 
244
244
  export default function (pi) {
245
245
  pi.on("session_start", async (event, ctx) => {
246
246
  let getSubagentsAPI;
247
247
  try {
248
- ({ getSubagentsAPI } = await import("@earendil-works/pi-subagents"));
248
+ ({ getSubagentsAPI } = await import("@gotgenes/pi-subagents"));
249
249
  } catch {
250
250
  return; // pi-subagents not installed
251
251
  }
@@ -269,7 +269,7 @@ export default function (pi) {
269
269
  const { id } = data as { id: string };
270
270
  let getSubagentsAPI;
271
271
  try {
272
- ({ getSubagentsAPI } = await import("@earendil-works/pi-subagents"));
272
+ ({ getSubagentsAPI } = await import("@gotgenes/pi-subagents"));
273
273
  } catch {
274
274
  return;
275
275
  }
@@ -330,7 +330,7 @@ Resolve model strings inside the adapter (fixing upstream [tintinweb/pi-subagent
330
330
  Extracted tools, notifications, activity tracking, and the `/agents` command into separate modules.
331
331
  `src/index.ts` shrank from ~1,619 lines to ~265 lines.
332
332
 
333
- ### Phase 6 (future): Extract UI to `@earendil-works/pi-subagents-ui`
333
+ ### Phase 6 (future): Extract UI to `@gotgenes/pi-subagents-ui`
334
334
 
335
335
  Move `ui/agent-widget.ts`, `ui/conversation-viewer.ts`, the `/agents` command, notifications, and activity tracking to a separate extension that consumes `SubagentsAPI` + lifecycle events.
336
336
  This phase is deferred until the API boundary is proven stable in production.
@@ -353,44 +353,53 @@ Together they eliminate module-scope mutable state, create a testable functional
353
353
  - Split `runAgent()` into a pure configuration assembler (~200 lines) and an IO shell (~200 lines).
354
354
  - The assembler becomes independently testable without mocking the Pi SDK.
355
355
 
356
- 3. **gotgenes/pi-packages#76** — Inject `cwd` into `AgentManager`
357
- - Replace the `process.cwd()` call in `dispose()` with a constructor parameter.
358
- - A small, mechanical prerequisite for Issue #72.
356
+ 3. **gotgenes/pi-packages#76** — Inject `cwd` into `AgentManager`
357
+ - Replaced the `process.cwd()` call in `dispose()` with a constructor parameter.
359
358
 
360
- 4. **gotgenes/pi-packages#80** — Consolidate `getConfig` / `getAgentConfig` into a single resolution path
361
- - Replace the two overlapping lookup functions with a single `resolveAgentConfig(type): AgentConfig` that handles the unknown-type fallback internally.
362
- - Eliminates the duplicated fallback chain exposed by #71 and simplifies test mock setup.
359
+ 4. **gotgenes/pi-packages#80** — Consolidate `getConfig` / `getAgentConfig` into a single resolution path
360
+ - Replaced the two overlapping lookup functions with a single `resolveAgentConfig(type): AgentConfig` that handles the unknown-type fallback internally.
361
+ - Eliminated the duplicated fallback chain exposed by #71 and simplified test mock setup.
363
362
 
364
363
  ### Phase 2: Core decomposition
365
364
 
366
365
  These build on Phase 1 and should land after it.
367
366
 
368
- 4. **gotgenes/pi-packages#72** — Dependency-inject `AgentManager`'s collaborators
369
- - Introduce `AgentRunner` and `WorktreeManager` interfaces and inject them into `AgentManager`.
370
- - Removes direct imports of `agent-runner.ts` and `worktree.ts` from `agent-manager.ts`.
367
+ 1. **gotgenes/pi-packages#84** Extract `GitWorktreeManager` class from `worktree.ts`
368
+ - Added `WorktreeManager` interface and `GitWorktreeManager` class that captures `cwd` at construction.
369
+ - Prerequisite for #72 separated the real-object extraction from the DI refactor.
371
370
 
372
- 5. **gotgenes/pi-packages#70** — Extract event handlers into `src/handlers/`
371
+ 2. **gotgenes/pi-packages#72** Dependency-inject `AgentManager`'s collaborators
372
+ - Defined `AgentRunner` interface (execution boundary) and `ResumeOptions` type in `agent-runner.ts`.
373
+ - Converted `AgentManager` constructor from 6 positional parameters to an `AgentManagerOptions` bag with injected `AgentRunner` and `WorktreeManager`.
374
+ - Removed all runtime imports of `agent-runner.ts` and `worktree.ts` from `agent-manager.ts` (only `import type` remains).
375
+ - Migrated all tests from `vi.mock()` module stubs to `vi.fn()` interface stubs.
376
+
377
+ 3. **gotgenes/pi-packages#87** — Evolve `SubagentRuntime` from data bag to object with methods
378
+ - Add session-context methods (`setSessionContext`, `clearSessionContext`) and widget delegation methods (`setUICtx`, `onTurnStart`, `markFinished`, `updateWidget`, `ensureTimer`).
379
+ - Prerequisite for #70 — without runtime methods, extracted handlers would move LoD violations and output-argument smells into handler classes.
380
+
381
+ 4. **gotgenes/pi-packages#70** — Extract event handlers into `src/handlers/`
373
382
  - Move the four inline lambdas (`session_start`, `session_before_switch`, `session_shutdown`, `tool_execution_start`) into named handler modules.
374
- - Requires Issue #69 because handlers need the `SubagentRuntime` as their deps bag.
383
+ - Requires Issues #69 and #87 because handlers need the `SubagentRuntime` with methods as their deps.
375
384
  - Target: `src/index.ts` ≤150 lines.
376
385
 
377
386
  ### Phase 3: Interface polish
378
387
 
379
388
  Small cleanups that are safest after the structural changes settle.
380
389
 
381
- 6. **gotgenes/pi-packages#66** — Replace `as any` casts with proper SDK types
390
+ 1. **gotgenes/pi-packages#66** — Replace `as any` casts with proper SDK types
382
391
  - Type-only change in the tool/menu factory dep interfaces.
383
392
  - Best done after Issues #69 and #70 when the interfaces are stable.
384
393
 
385
- 7. **gotgenes/pi-packages#77** — Add `projectAgentsDir` to `AgentMenuDeps`
394
+ 2. **gotgenes/pi-packages#77** — Add `projectAgentsDir` to `AgentMenuDeps`
386
395
  - Remove the inline `process.cwd()` lambda from the menu handler.
387
396
 
388
397
  ### Phase 4: Features and cross-cutting concerns
389
398
 
390
- 8. **gotgenes/pi-packages#61** — Port transcript logging to Pi's official JSONL session format
399
+ 1. **gotgenes/pi-packages#61** — Port transcript logging to Pi's official JSONL session format
391
400
  - Feature work that should happen after structural refactoring is complete so the output-file subsystem has a stable home.
392
401
 
393
- 9. **gotgenes/pi-packages#22** — Parent-session resolution for `nicobailon/pi-subagents` children
402
+ 2. **gotgenes/pi-packages#22** — Parent-session resolution for `nicobailon/pi-subagents` children
394
403
  - Cross-extension issue that spans `pi-permission-system` and `pi-subagents`.
395
404
  - Requires coordination on env-var conventions.
396
405
  - Not blocked by the structural refactor but logically separate from it.
@@ -398,15 +407,13 @@ Small cleanups that are safest after the structural changes settle.
398
407
  ### Dependency graph
399
408
 
400
409
  ```text
401
- #69 (SubagentRuntime) ✓ ─┬─► #70 (handler extraction)
402
-
403
- └─► #72 (AgentManager DI) ──(optional)──► #70
404
-
405
- #71 (pure assembler) ✓ ──► #80 (consolidate getConfig/getAgentConfig)
406
-
407
- #76 (cwd injection) ────► #72
408
-
409
- #80 (config lookup) ────(independent, simplifies #72 and test mocks)
410
+ #69 (SubagentRuntime) ✓ ──► #87 (runtime methods) ─┬─► #70 (handler extraction)
411
+
412
+ #71 (pure assembler) ✓ │
413
+ #80 (config lookup) ✓ │
414
+ #76 (cwd injection) ✓
415
+ #84 (WorktreeManager) ✓ │
416
+ #72 (AgentManager DI) ────────────────────────────┘──(optional)──► #70
410
417
 
411
418
  #66 (type casts) ◄─────(after structural changes settle)
412
419
  #77 (projectAgentsDir) ◄─(after #66 or parallel)
@@ -420,15 +427,16 @@ Small cleanups that are safest after the structural changes settle.
420
427
  The recommended sequence is:
421
428
 
422
429
  ```text
423
- #69 ✓ → #71 ✓ → #80 → #76 → #72 → #70 → #66 → #77 → #61
430
+ #69 ✓ → #71 ✓ → #80 → #76 → #84 ✓ → #72 → #87 → #70 → #66 → #77 → #61
424
431
  ```
425
432
 
426
- Issue #80 slots after #71 because it cleans up the redundant lookup that #71 exposed, and simplifies mock setup for subsequent issues.
433
+ Phase 1 is complete; Phase 2 is in progress.
434
+ The next issue is #87 (runtime methods), which unblocks #70 (handler extraction).
427
435
  Issue #22 is a parallel cross-extension track and does not gate the structural work.
428
436
 
429
437
  ## Relationship with upstream
430
438
 
431
- This fork ([earendil-works/pi-subagents]) is now a hard fork of [tintinweb/pi-subagents].
439
+ This fork (`@gotgenes/pi-subagents` in the [gotgenes/pi-packages] monorepo) is now a hard fork of [tintinweb/pi-subagents].
432
440
  The decomposition diverges materially from upstream's direction.
433
441
 
434
442
  The three upstream PRs (#71, #72, #73) remain open.
@@ -439,5 +447,5 @@ Upstream fixes and ideas are cherry-picked when they align with this fork's scop
439
447
  The upstream test suite is run periodically as a regression canary for the agent-runner core.
440
448
 
441
449
  [earendil-works/pi#4207]: https://github.com/earendil-works/pi/issues/4207
442
- [earendil-works/pi-subagents]: https://github.com/earendil-works/pi-subagents
450
+ [gotgenes/pi-packages]: https://github.com/gotgenes/pi-packages
443
451
  [tintinweb/pi-subagents]: https://github.com/tintinweb/pi-subagents
@@ -0,0 +1,306 @@
1
+ ---
2
+ issue: 70
3
+ issue_title: "refactor: extract event handlers from pi-subagents index.ts into src/handlers/"
4
+ ---
5
+
6
+ # Extract event handlers from index.ts
7
+
8
+ ## Problem Statement
9
+
10
+ After #54 and #69, `src/index.ts` is still ~281 lines.
11
+ Four event handlers (`session_start`, `session_before_switch`, `session_shutdown`, `tool_execution_start`) are inline lambdas inside the factory closure.
12
+ They cannot be tested in isolation because they reach into closure-scoped objects (`runtime`, `manager`, `notifications`, `pi`).
13
+ `pi-permission-system` solved the same problem in #42 by extracting all handlers into `src/handlers/` as classes receiving narrow constructor-injected dependencies.
14
+
15
+ ## Goals
16
+
17
+ - Create `src/handlers/` with dedicated modules for each handler group.
18
+ - Define handler classes with constructor-injected narrow interfaces that replace closure-captured state with explicit, testable contracts.
19
+ - Reduce `src/index.ts` by moving handler bodies out — target ≤ 200 lines (the remaining bulk is tool/menu wiring, which is out of scope).
20
+ - Enable unit testing of each handler module via mocked deps.
21
+ - No behavior change — pure structural refactor.
22
+
23
+ ## Non-Goals
24
+
25
+ - Extracting tool registration or menu wiring from `index.ts` (separate concern; not in scope).
26
+ - Changing the `SubagentsService` interface.
27
+ - Refactoring `AgentManager` constructor signature.
28
+ - Consolidating notification callbacks — the notification system's shape is unchanged.
29
+
30
+ ## Background
31
+
32
+ ### Current handler bodies in index.ts
33
+
34
+ ```typescript
35
+ // session_start (~3 lines)
36
+ runtime.currentCtx = { pi, ctx };
37
+ manager.clearCompleted();
38
+
39
+ // session_before_switch (~1 line)
40
+ manager.clearCompleted();
41
+
42
+ // session_shutdown (~5 lines)
43
+ unpublishSubagentsService();
44
+ runtime.currentCtx = undefined;
45
+ manager.abortAll();
46
+ notifications.dispose();
47
+ manager.dispose();
48
+
49
+ // tool_execution_start (~2 lines)
50
+ runtime.widget!.setUICtx(ctx.ui as UICtx);
51
+ runtime.widget!.onTurnStart();
52
+ ```
53
+
54
+ ### Prior art: pi-permission-system src/handlers/
55
+
56
+ Issue #42 extracted lifecycle, agent-prep, and permission-gate handlers into classes that receive narrow constructor deps.
57
+ Each class exposes handler methods matching the Pi SDK event signatures.
58
+ `index.ts` constructs the handler objects and wires `pi.on(event, handler.method)`.
59
+
60
+ This plan follows the same class-based pattern.
61
+ Although the individual handler bodies are small (1–5 lines), the lifecycle handlers share `runtime` and `manager` as collaborators — shared state that a class captures naturally via constructor injection rather than threading through each call.
62
+
63
+ ### Prerequisite status
64
+
65
+ - Issue #69 (SubagentRuntime) — **closed / implemented**.
66
+ The runtime object exists in `src/runtime.ts` and is already wired into `index.ts`.
67
+ - Issue #87 (evolve SubagentRuntime from data bag to object with methods) — **open / must land first**.
68
+ Adds `setSessionContext()` / `clearSessionContext()` and widget delegation methods (`setUICtx()`, `onTurnStart()`, `markFinished()`, `updateWidget()`, `ensureTimer()`).
69
+ Without #87, extracted handlers would just move the output-argument and LoD smells from `index.ts` into handler classes.
70
+ With #87, handlers call methods on narrow runtime interfaces — no raw field writes, no `widget!` reach-throughs.
71
+
72
+ ### Relevant constraints from AGENTS.md / code-style skill
73
+
74
+ - Keep modules focused and composable (one concern per file).
75
+ - Do not pass a shared dependency bag to functions that only use a subset — define narrow interfaces per consumer.
76
+ - Keep Pi SDK imports out of business-logic modules — handler modules should accept lean local payload interfaces, not full SDK event types.
77
+ - Prefer explicit configuration over hidden behavior.
78
+
79
+ ## Design Overview
80
+
81
+ ### Module layout
82
+
83
+ ```text
84
+ src/handlers/
85
+ lifecycle.ts # session_start, session_before_switch, session_shutdown
86
+ tool-start.ts # tool_execution_start
87
+ index.ts # barrel re-export
88
+ ```
89
+
90
+ No shared `types.ts` file — each module defines its own narrow constructor interfaces.
91
+ This follows the code-style guidance ("do not pass a shared dependency bag to functions that only use a subset") and matches the permission-system's prior art where each handler class takes its own narrow constructor deps.
92
+
93
+ ### Narrow constructor interfaces
94
+
95
+ Each class defines local interfaces for its collaborators, exposing only the methods the class actually calls.
96
+ This keeps tests simple (mock only what's used) and decouples handlers from concrete types.
97
+
98
+ #### SessionLifecycleHandler (in lifecycle.ts)
99
+
100
+ ```typescript
101
+ /** Narrow manager interface — only the methods lifecycle handlers call. */
102
+ export interface LifecycleManager {
103
+ clearCompleted(): void;
104
+ abortAll(): void;
105
+ dispose(): void;
106
+ }
107
+
108
+ /** Narrow runtime interface — only the methods lifecycle handlers call. */
109
+ export interface LifecycleRuntime {
110
+ setSessionContext(pi: unknown, ctx: unknown): void;
111
+ clearSessionContext(): void;
112
+ }
113
+
114
+ export class SessionLifecycleHandler {
115
+ constructor(
116
+ private readonly pi: unknown,
117
+ private readonly runtime: LifecycleRuntime,
118
+ private readonly manager: LifecycleManager,
119
+ private readonly disposeNotifications: () => void,
120
+ private readonly unpublishService: () => void,
121
+ ) {}
122
+
123
+ handleSessionStart(_event: unknown, ctx: unknown): void { ... }
124
+ handleSessionBeforeSwitch(): void { ... }
125
+ async handleSessionShutdown(): Promise<void> { ... }
126
+ }
127
+ ```
128
+
129
+ Five constructor params — `runtime` and `manager` are shared across all three methods (the key insight the plain-function design missed), while `disposeNotifications` and `unpublishService` are shutdown-only callbacks.
130
+
131
+ `LifecycleRuntime` exposes methods, not mutable fields — the handler *tells* the runtime to set/clear session context instead of writing raw fields (the output-argument smell that #87 eliminates).
132
+
133
+ #### ToolStartHandler (in tool-start.ts)
134
+
135
+ ```typescript
136
+ /** Narrow runtime interface — only the widget-delegation methods the handler calls. */
137
+ export interface ToolStartRuntime {
138
+ setUICtx(ctx: UICtx): void;
139
+ onTurnStart(): void;
140
+ }
141
+
142
+ export class ToolStartHandler {
143
+ constructor(
144
+ private readonly runtime: ToolStartRuntime,
145
+ ) {}
146
+
147
+ handleToolExecutionStart(_event: unknown, ctx: ToolStartCtx): void { ... }
148
+ }
149
+ ```
150
+
151
+ After #87, the runtime owns widget delegation — `runtime.setUICtx()` delegates to `this.widget?.setUICtx()` internally, handling the null check.
152
+ The handler takes a narrow `ToolStartRuntime` interface (just the two methods it calls) and does not know about `widget` or `SubagentRuntime` at all.
153
+
154
+ ### Event payload interfaces
155
+
156
+ Following the code-style skill ("prefer lean local payload interfaces over full SDK event types"), each handler defines minimal payload types for the events it consumes.
157
+
158
+ `session_start` receives `(event, ctx)` — but the current handler ignores the event payload entirely and only reads `ctx`.
159
+ `tool_execution_start` receives `(event, ctx)` — the handler reads `ctx.ui`.
160
+
161
+ Since handlers ignore event payloads, the method signatures use `_event: unknown`.
162
+
163
+ ### index.ts wire-up
164
+
165
+ After extraction, `index.ts` constructs handler instances and binds:
166
+
167
+ ```typescript
168
+ import { SessionLifecycleHandler } from "./handlers/index.js";
169
+ import { ToolStartHandler } from "./handlers/index.js";
170
+
171
+ const lifecycle = new SessionLifecycleHandler(
172
+ pi,
173
+ runtime,
174
+ manager,
175
+ () => notifications.dispose(),
176
+ unpublishSubagentsService,
177
+ );
178
+
179
+ pi.on("session_start", (event, ctx) => lifecycle.handleSessionStart(event, ctx));
180
+ pi.on("session_before_switch", () => lifecycle.handleSessionBeforeSwitch());
181
+ pi.on("session_shutdown", () => lifecycle.handleSessionShutdown());
182
+
183
+ const toolStart = new ToolStartHandler(runtime);
184
+
185
+ pi.on("tool_execution_start", (event, ctx) => toolStart.handleToolExecutionStart(event, ctx));
186
+ ```
187
+
188
+ Handler instances are constructed once and hold their collaborators for the extension's lifetime — the same pattern as pi-permission-system's `SessionLifecycleHandler`.
189
+ `runtime` satisfies both `LifecycleRuntime` and `ToolStartRuntime` structurally — TypeScript matches the narrow interface without an explicit cast.
190
+
191
+ ### Edge cases
192
+
193
+ - `session_shutdown` calls `unpublishSubagentsService()` which is a module-level import — the handler receives it as a constructor callback, keeping the handler SDK-free.
194
+ - Constructor params are narrow interfaces (`LifecycleManager`, `LifecycleRuntime`, `ToolStartRuntime`) rather than concrete types (`AgentManager`, `SubagentRuntime`), so tests construct mocks without importing production classes.
195
+ - Widget null safety: after #87, the runtime's delegation methods handle null internally (`this.widget?.setUICtx(ctx)`), so handlers never see the null case.
196
+ `ToolStartHandler` tests mock the narrow `ToolStartRuntime` interface directly — no widget null logic to test in the handler.
197
+
198
+ ## Module-Level Changes
199
+
200
+ ### `src/handlers/lifecycle.ts` (new)
201
+
202
+ - `LifecycleManager` interface (narrow: `clearCompleted`, `abortAll`, `dispose`).
203
+ - `LifecycleRuntime` interface (narrow: `setSessionContext`, `clearSessionContext`).
204
+ - `SessionLifecycleHandler` class — constructor takes `(pi, runtime, manager, disposeNotifications, unpublishService)`.
205
+ - `handleSessionStart(_event, ctx)` — calls `this.runtime.setSessionContext(this.pi, ctx)`, calls `this.manager.clearCompleted()`.
206
+ - `handleSessionBeforeSwitch()` — calls `this.manager.clearCompleted()`.
207
+ - `handleSessionShutdown()` — calls `this.unpublishService()`, `this.runtime.clearSessionContext()`, `this.manager.abortAll()`, `this.disposeNotifications()`, `this.manager.dispose()`.
208
+
209
+ ### `src/handlers/tool-start.ts` (new)
210
+
211
+ - `ToolStartRuntime` interface (narrow: `setUICtx`, `onTurnStart`).
212
+ - `ToolStartCtx` local interface (`{ ui: UICtx }`).
213
+ - `ToolStartHandler` class — constructor takes `runtime: ToolStartRuntime`.
214
+ - `handleToolExecutionStart(_event, ctx)` — calls `this.runtime.setUICtx(ctx.ui)`, `this.runtime.onTurnStart()`.
215
+
216
+ ### `src/handlers/index.ts` (new)
217
+
218
+ - Barrel re-export of handler classes and their narrow interfaces.
219
+
220
+ ### `src/index.ts` (modified)
221
+
222
+ - Add imports from `./handlers/index.js`.
223
+ - Construct `SessionLifecycleHandler` with `(pi, runtime, manager, () => notifications.dispose(), unpublishSubagentsService)`.
224
+ - Construct `ToolStartHandler` with `runtime`.
225
+ - Replace inline `pi.on("session_start", ...)` lambda with `lifecycle.handleSessionStart` delegation.
226
+ - Replace inline `pi.on("session_before_switch", ...)` lambda with `lifecycle.handleSessionBeforeSwitch` delegation.
227
+ - Replace inline `pi.on("session_shutdown", ...)` lambda with `lifecycle.handleSessionShutdown` delegation.
228
+ - Replace inline `pi.on("tool_execution_start", ...)` lambda with `toolStart.handleToolExecutionStart` delegation.
229
+ - `unpublishSubagentsService` import stays — passed to the handler constructor.
230
+
231
+ ### `test/handlers/lifecycle.test.ts` (new)
232
+
233
+ - Construct `SessionLifecycleHandler` with mocked `LifecycleManager`, `LifecycleRuntime`, and stub callbacks.
234
+ - `handleSessionStart`: verify `runtime.setSessionContext` called with `(pi, ctx)`, `manager.clearCompleted` called.
235
+ - `handleSessionBeforeSwitch`: verify `manager.clearCompleted` called.
236
+ - `handleSessionShutdown`: verify all five cleanup calls in correct order (unpublish → clearSessionContext → abortAll → disposeNotifications → dispose manager).
237
+
238
+ ### `test/handlers/tool-start.test.ts` (new)
239
+
240
+ - Construct `ToolStartHandler` with a mock `ToolStartRuntime`.
241
+ - Verify `setUICtx` and `onTurnStart` called with correct arguments.
242
+
243
+ ### `test/print-mode.test.ts` (unchanged)
244
+
245
+ - This integration test calls `handlers.get("session_shutdown")` on the extension's registered handlers.
246
+ The extraction is transparent — the Pi event registration is still in `index.ts`, just delegating to extracted functions.
247
+ No changes needed.
248
+
249
+ ## Test Impact Analysis
250
+
251
+ ### New unit tests enabled by the extraction
252
+
253
+ 1. `test/handlers/lifecycle.test.ts` — Each lifecycle handler tested in isolation with mocked deps.
254
+ Previously impossible because handlers were inline lambdas with closure-captured `runtime`, `manager`, `notifications`, and `pi`.
255
+ 2. `test/handlers/tool-start.test.ts` — `tool_execution_start` handler tested with mocked widget.
256
+ Previously untestable because the handler was an inline lambda closing over `runtime.widget`.
257
+ 3. Widget null-safety is not the handler's concern — the runtime handles it internally after #87.
258
+
259
+ ### Existing tests that become redundant
260
+
261
+ None.
262
+ No existing tests directly test the event handler bodies — they were untestable inline lambdas.
263
+ The extraction creates *new* coverage, not duplicate coverage.
264
+
265
+ ### Existing tests that stay as-is
266
+
267
+ - `test/print-mode.test.ts` — calls `session_shutdown` via the extension's registered handler map; still works because `index.ts` still registers the event, just delegates to extracted functions.
268
+ - All other test files — no dependency on handler internals.
269
+
270
+ ## TDD Order
271
+
272
+ 1. **Create `src/handlers/lifecycle.ts` with `SessionLifecycleHandler` class.**
273
+ Write `test/handlers/lifecycle.test.ts` constructing the handler with mocked narrow interfaces.
274
+ Verify: `handleSessionStart` sets `currentCtx` and calls `clearCompleted`; `handleSessionBeforeSwitch` calls `clearCompleted`; `handleSessionShutdown` calls all five cleanup steps in order.
275
+ Commit: `feat: add SessionLifecycleHandler`
276
+
277
+ 2. **Create `src/handlers/tool-start.ts` with `ToolStartHandler` class.**
278
+ Write `test/handlers/tool-start.test.ts` constructing the handler with a mock getter.
279
+ Verify: calls `setUICtx` and `onTurnStart` on widget; no-op when widget is null.
280
+ Commit: `feat: add ToolStartHandler`
281
+
282
+ 3. **Create `src/handlers/index.ts` barrel and wire handlers into `src/index.ts`.**
283
+ Add barrel re-export in `src/handlers/index.ts`.
284
+ Replace inline lambdas in `src/index.ts` with handler instance method delegation.
285
+ Construct `SessionLifecycleHandler` and `ToolStartHandler` in the factory.
286
+ Run full test suite to verify no regressions.
287
+ Run `pnpm run check` to verify types.
288
+ Commit: `refactor: wire extracted handlers into extension factory (#70)`
289
+
290
+ ## Risks and Mitigations
291
+
292
+ | Risk | Mitigation |
293
+ | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
294
+ | Handler extraction changes call order in `session_shutdown` | Test explicitly asserts call order; current order is preserved exactly. |
295
+ | `SessionLifecycleHandler` has 5 constructor params | Reviewed against design-review: `runtime` and `manager` are shared across all three methods (the core shared-state argument for a class). The two callbacks (`disposeNotifications`, `unpublishService`) are shutdown-only but small enough to not warrant a second class. Acceptable. |
296
+ | `print-mode.test.ts` integration test breaks | Test calls `session_shutdown` via the extension's handler map, not the extracted function directly. The delegation is transparent. Verified no changes needed. |
297
+ | `UICtx` type import in `tool-start.ts` couples handler to widget module | `UICtx` is a lean interface already exported from `ui/agent-widget.ts`. The handler only references it in the `ToolStartCtx` local type. Acceptable coupling. |
298
+
299
+ ## Open Questions
300
+
301
+ - Should the `session_shutdown` handler call cleanup in a specific guaranteed order (e.g., unpublish → abort → dispose)?
302
+ The current inline code uses a specific order; the extraction preserves it.
303
+ If order matters for correctness, add a code comment documenting it.
304
+ Decide during implementation.
305
+ - Should the `ToolStartHandler` also handle `UICtx` type re-export, or should `tool-start.ts` import `UICtx` from `ui/agent-widget.ts` directly?
306
+ Decide during implementation — if the handler defines its own `ToolStartCtx` with `{ ui: unknown }`, it avoids the import entirely.
@@ -35,7 +35,7 @@ Any test of `AgentManager` must mock entire modules via `vi.mock()`, coupling th
35
35
  | #71 | Extract pure agent-session assembler | ✓ Done |
36
36
  | #76 | Inject `cwd` into `AgentManager` | ✓ Done |
37
37
  | #80 | Consolidate `getConfig`/`getAgentConfig` | ✓ Done |
38
- | #84 | Extract `GitWorktreeManager` class from worktree.ts | Pending |
38
+ | #84 | Extract `GitWorktreeManager` class from worktree.ts | Done |
39
39
 
40
40
  ### Prior art
41
41
 
@@ -201,7 +201,7 @@ expect(runner.run).toHaveBeenCalled();
201
201
 
202
202
  ### `src/worktree.ts` (no changes in this issue)
203
203
 
204
- `WorktreeManager` interface and `GitWorktreeManager` class are added by prerequisite #84.
204
+ `WorktreeManager` interface and `GitWorktreeManager` class were added by #84.
205
205
 
206
206
  ### `src/agent-runner.ts` (modified)
207
207