@gotgenes/pi-subagents 6.0.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ 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
+ ## [6.0.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.0.0...pi-subagents-v6.0.1) (2026-05-20)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * full refresh of architecture.md — reflect completed decomposition ([a8187ce](https://github.com/gotgenes/pi-packages/commit/a8187ce839b465529191b53bbb19fd39d5c71e69))
14
+ * **pi-subagents:** set new architectural target for AgentManager decomposition ([83288da](https://github.com/gotgenes/pi-packages/commit/83288daf3dc6dce7bcad81799a4f6e9f300553c0))
15
+ * plan consolidate test AgentRecord factory ([#102](https://github.com/gotgenes/pi-packages/issues/102)) ([22a3213](https://github.com/gotgenes/pi-packages/commit/22a3213bb55d15871d3582e89d1336fcd9e8fa36))
16
+ * **retro:** add retro notes for issue [#61](https://github.com/gotgenes/pi-packages/issues/61) ([7053be7](https://github.com/gotgenes/pi-packages/commit/7053be7bec86a0c160cb602528d1c59e370ba33d))
17
+
8
18
  ## [6.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.8.2...pi-subagents-v6.0.0) (2026-05-20)
9
19
 
10
20
 
@@ -1,59 +1,79 @@
1
1
  # Architecture
2
2
 
3
- This document describes the planned decomposition of the pi-subagents fork into a focused, composable core with a stable API boundary that other extensions can build on.
3
+ This document describes the architecture of the pi-subagents fork: a focused, composable core with a stable API boundary that other extensions can build on.
4
4
 
5
5
  ## Design principles
6
6
 
7
7
  1. **Narrow core** — the extension owns agent spawning, execution, and result retrieval.
8
8
  Everything else is a consumer.
9
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 `SubagentsAPI` interface and `Symbol.for()` accessors (`publishSubagentsAPI` / `getSubagentsAPI`).
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()` on `globalThis` — no separate API package.
12
+ The runtime bridge is `Symbol.for("@gotgenes/pi-subagents:service")` on `globalThis` — no separate API package.
13
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
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 plain data object.
18
+ No live references survive past the spawn call.
19
+ 7. **Subscribe, don't thread** — observation of agent progress uses event subscription on the session, not callback parameters threaded through multiple layers.
17
20
 
18
21
  ## Current state
19
22
 
20
- The extension is a 6,300 LOC monolith organized into well-factored internal modules but with no public API contract.
21
- The subsystems are:
23
+ The extension is ~6,100 LOC across 35 focused modules with a typed `SubagentsService` API boundary.
24
+ The `index.ts` entry point is ~270 lines; the rest is decomposed into domain modules.
22
25
 
23
26
  ```text
24
- index.ts (1,894 LOC) — entry point, tool registration, event wiring
25
- agent-manager.ts — lifecycle, concurrency, queue
26
- agent-runner.ts — session creation, turn loop, tool filtering
27
- agent-types.ts type registry (defaults + custom .md files)
28
- types.ts shared type definitions
29
-
30
- prompts.ts system prompt assembly
31
- context.ts — parent conversation extraction
32
- memory.ts persistent MEMORY.md per agent
33
- skill-loader.ts preload .pi/skills into prompts
34
- env.ts — git/platform detection
35
-
36
- worktree.ts — git worktree isolation
37
- usage.ts — token usage tracking
38
- model-resolver.ts fuzzy model name resolution
39
- invocation-config.ts merge tool params with agent config
40
- session-dir.ts — subagent session directory derivation
41
- settings.ts persistent operational settings
42
-
43
- cross-extension-rpc.ts RPC over pi.events ← replacing
44
- group-join.ts — batch completion notifications
27
+ index.ts (274 LOC) — entry point, tool registration, event wiring
28
+ agent-manager.ts (499) — lifecycle, concurrency, queue
29
+ agent-runner.ts (512) — session creation, turn loop, tool filtering
30
+ session-config.ts (243) — pure session-config assembler
31
+ agent-types.ts (138) — type registry (defaults + custom .md files)
32
+ types.ts (126) — shared type definitions
33
+ runtime.ts (94) SubagentRuntime factory (session-scoped state)
34
+
35
+ prompts.ts system prompt assembly
36
+ context.ts parent conversation extraction
37
+ memory.ts — persistent MEMORY.md per agent
38
+ skill-loader.ts — preload .pi/skills into prompts
39
+ env.ts — git/platform detection
40
+
41
+ worktree.ts git worktree isolation
42
+ usage.ts token usage tracking
43
+ model-resolver.ts — fuzzy model name resolution
44
+ invocation-config.ts merge tool params with agent config
45
+ session-dir.ts — subagent session directory derivation
46
+ settings.ts persistent operational settings
47
+
48
+ service.ts — SubagentsService interface + Symbol.for() accessors
49
+ service-adapter.ts — SubagentsService implementation wrapping AgentManager
50
+
51
+ tools/agent-tool.ts — Agent tool definition + execute
52
+ tools/get-result-tool.ts — get_subagent_result tool
53
+ tools/steer-tool.ts — steer_subagent tool
54
+ tools/helpers.ts — shared tool utilities
55
+
56
+ handlers/lifecycle.ts — session_start, session_before_switch, session_shutdown
57
+ handlers/tool-start.ts — tool_execution_start handler
58
+
59
+ notification.ts — completion nudges, custom message renderer
60
+ renderer.ts — notification TUI component
45
61
 
46
62
  ui/agent-widget.ts — above-editor live status widget
63
+ ui/agent-menu.ts — /agents slash command menu
47
64
  ui/conversation-viewer.ts — scrollable session overlay
65
+
66
+ default-agents.ts — embedded default agent configs (general-purpose, Explore, Plan)
67
+ custom-agents.ts — user-defined agent .md file loader
68
+ debug.ts — debug logging utility
48
69
  ```
49
70
 
50
71
  ### Coupling today
51
72
 
52
- The widget reads agent state by holding a direct reference to `AgentManager` and polling a shared mutable `Map<string, AgentActivity>` every 80 ms. The conversation viewer subscribes directly to `AgentSession` objects.
53
-
54
- Cross-extension consumers use an ad-hoc RPC layer over `pi.events` (`subagents:rpc:spawn`, `subagents:rpc:stop`, `subagents:rpc:ping`) with per-request reply channels and untyped envelopes.
73
+ The widget reads agent state by holding a direct reference to `SubagentRuntime` and polling a shared mutable `Map<string, AgentActivity>` every 80 ms. The conversation viewer subscribes directly to `AgentSession` objects.
55
74
 
56
- There is also a `Symbol.for("pi-subagents:manager")` export on `globalThis` that exposes `{ waitForAll, hasRunning, spawn, getRecord }`, but it is undocumented and untyped.
75
+ Cross-extension consumers use the typed `SubagentsService` API published via `Symbol.for("@gotgenes/pi-subagents:service")` on `globalThis`.
76
+ The ad-hoc RPC layer and untyped `Symbol.for("pi-subagents:manager")` have been removed.
57
77
 
58
78
  ## Target state
59
79
 
@@ -62,20 +82,20 @@ There is also a `Symbol.for("pi-subagents:manager")` export on `globalThis` that
62
82
  │ @gotgenes/pi-subagents (this package) │
63
83
  │ │
64
84
  │ Exports: │
65
- SubagentsAPI interface
66
- publishSubagentsAPI() / getSubagentsAPI()
67
- │ SubagentRecord, SubagentStatus, LifetimeUsage types
68
- Event channel constants
85
+ SubagentsService interface
86
+ publishSubagentsService() / getSubagentsService()
87
+ │ SubagentRecord, SubagentStatus, LifetimeUsage types
88
+ SUBAGENT_EVENTS constants
69
89
  │ │
70
90
  │ Core: │
71
91
  │ Agent + get_subagent_result + steer_subagent tools │
72
92
  │ AgentManager, agent-runner, agent-types │
73
- publishSubagentsAPI(impl) ← called at init
93
+ publishSubagentsService(impl) ← called at init
74
94
  │ │
75
95
  │ Internal UI (widget, viewer, /agents menu) │
76
96
  │ ← moves to pi-subagents-ui later │
77
97
  └──────────────────────┬─────────────────────────────────┘
78
- │ Symbol.for("pi:service:subagents")
98
+ │ Symbol.for("@gotgenes/pi-subagents:service")
79
99
 
80
100
  ┌─────────────────┼──────────────────┐
81
101
  │ │ │
@@ -87,7 +107,7 @@ There is also a `Symbol.for("pi-subagents:manager")` export on `globalThis` that
87
107
  │ ext) │ └──────────────┘ └──────────────┘
88
108
  └─────────┘
89
109
 
90
- getSubagentsAPI()?.spawn(...)
110
+ getSubagentsService()?.spawn(...)
91
111
  │ (optional peer dep + dynamic import for types)
92
112
 
93
113
  ```
@@ -97,10 +117,13 @@ There is also a `Symbol.for("pi-subagents:manager")` export on `globalThis` that
97
117
  - The three tools: `Agent`, `get_subagent_result`, `steer_subagent`.
98
118
  - `AgentManager` — spawn, queue, abort, resume, concurrency control.
99
119
  - `agent-runner` — session creation, turn loop, tool filtering, extension binding (Patches 2 and 3).
120
+ - `session-config` — pure configuration assembler (extracted from `agent-runner`).
121
+ - `SubagentRuntime` — session-scoped state bag with methods.
100
122
  - Agent type registry — default agents, custom `.md` file loading.
101
123
  - Prompt assembly, context extraction, memory, skills, environment.
102
124
  - Worktree isolation.
103
125
  - Token usage tracking.
126
+ - Session directory derivation and persisted `SessionManager` for subagent transcripts.
104
127
  - Settings persistence.
105
128
  - Internal UI (widget, conversation viewer, `/agents` menu) — these stay until the API boundary is proven, then move to a separate extension.
106
129
 
@@ -108,8 +131,8 @@ There is also a `Symbol.for("pi-subagents:manager")` export on `globalThis` that
108
131
 
109
132
  - **Scheduling** (`schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`) — 612 LOC removed.
110
133
  The `schedule` parameter is removed from the `Agent` tool schema.
111
- Any extension that wants scheduling can implement it by calling `getSubagentsAPI()?.spawn(...)` on a timer.
112
- - **Ad-hoc RPC** (`cross-extension-rpc.ts`) — replaced by the typed `SubagentsAPI` published via `Symbol.for()`.
134
+ Any extension that wants scheduling can implement it by calling `getSubagentsService()?.spawn(...)` on a timer.
135
+ - **Ad-hoc RPC** (`cross-extension-rpc.ts`) — replaced by the typed `SubagentsService` published via `Symbol.for()`.
113
136
  The untyped event-bus RPC channels are removed.
114
137
  - **Group join** (`group-join.ts`) — 141 LOC removed.
115
138
  The grouped notification batching adds complexity for a marginal UX improvement.
@@ -117,21 +140,22 @@ There is also a `Symbol.for("pi-subagents:manager")` export on `globalThis` that
117
140
  - **Output file** (`output-file.ts`) — replaced by `session-dir.ts` + `SessionManager.create()` (#61).
118
141
  Subagent transcripts are now written in Pi's official JSONL session format via the SDK's `SessionManager`, nested under the parent session directory.
119
142
 
120
- ### Estimated impact
143
+ ### Estimated impact (realized)
121
144
 
122
- | Subsystem removed | LOC removed | LOC removed from index.ts |
123
- | ----------------- | ------------- | ------------------------- |
124
- | Scheduling | 612 | ~200 |
125
- | Ad-hoc RPC | 80 | ~50 |
126
- | Group join | 141 | ~100 |
127
- | Output file | 83 (replaced) | ~50 |
128
- | **Total** | **~916** | **~400** |
145
+ | Subsystem | Status | LOC impact |
146
+ | ---------------------- | -------------- | ------------------------------------------ |
147
+ | Scheduling | Removed (#52) | −612 |
148
+ | Ad-hoc RPC | Removed (#49) | −080 |
149
+ | Group join | Removed (#49) | −141 |
150
+ | Output file | Replaced (#61) | −83 (replaced by 38-line `session-dir.ts`) |
151
+ | index.ts decomposition | Done (#54) | 1,894 → 274 |
129
152
 
130
- After removal and `index.ts` decomposition, the core shrinks from ~6,300 to ~5,400 LOC, and `index.ts` shrinks from ~1,894 to ~1,300 LOC.
153
+ The codebase is now ~6,100 LOC across 35 modules.
154
+ The `index.ts` entry point is 274 lines.
131
155
 
132
- ## SubagentsAPI
156
+ ## SubagentsService (done — #48)
133
157
 
134
- The `SubagentsAPI` interface, accessor functions, and serializable types are exported directly from this package (`@gotgenes/pi-subagents`).
158
+ The `SubagentsService` interface, accessor functions, and serializable types are exported from `@gotgenes/pi-subagents` via the `./service` export map entry.
135
159
  No separate API package is needed.
136
160
 
137
161
  Consumers declare this package as an optional peer dependency:
@@ -139,7 +163,7 @@ Consumers declare this package as an optional peer dependency:
139
163
  ```json
140
164
  {
141
165
  "peerDependencies": {
142
- "@gotgenes/pi-subagents": ">=2.0.0"
166
+ "@gotgenes/pi-subagents": ">=5.0.0"
143
167
  },
144
168
  "peerDependenciesMeta": {
145
169
  "@gotgenes/pi-subagents": { "optional": true }
@@ -150,72 +174,40 @@ Consumers declare this package as an optional peer dependency:
150
174
  At runtime, consumers use dynamic import for type-safe access to the accessor functions:
151
175
 
152
176
  ```typescript
153
- const { getSubagentsAPI } = await import("@gotgenes/pi-subagents");
154
- const api = getSubagentsAPI();
155
- if (api) {
156
- api.spawn("Explore", "Check for stale TODOs");
177
+ const { getSubagentsService } = await import("@gotgenes/pi-subagents");
178
+ const svc = getSubagentsService();
179
+ if (svc) {
180
+ svc.spawn("Explore", "Check for stale TODOs");
157
181
  }
158
182
  ```
159
183
 
160
184
  Pi's extension loader creates a fresh `jiti` instance per extension with `moduleCache: false`, so module-scoped singletons don't survive across extensions.
161
- The accessor functions use `Symbol.for()` on `globalThis`, which is process-global by spec, to bridge this gap.
185
+ The accessor functions use `Symbol.for("@gotgenes/pi-subagents:service")` on `globalThis`, which is process-global by spec, to bridge this gap.
162
186
  The dynamic import provides compile-time types; the `Symbol.for()` key is the actual runtime channel.
163
187
 
164
188
  ### Interface
165
189
 
166
- ```typescript
167
- /** The public API surface published by pi-subagents. */
168
- export interface SubagentsAPI {
169
- /**
170
- * Spawn an agent. Returns the agent ID immediately.
171
- * The agent runs in the background unless options.foreground is true.
172
- */
173
- spawn(type: string, prompt: string, options?: SpawnOptions): string;
174
-
175
- /** Get a snapshot of an agent's current state. */
176
- getRecord(id: string): SubagentRecord | undefined;
177
-
178
- /** List all tracked agents, most recent first. */
179
- listAgents(): SubagentRecord[];
180
-
181
- /** Abort a running or queued agent. Returns false if not found. */
182
- abort(id: string): boolean;
183
-
184
- /** Send a steering message to a running agent. */
185
- steer(id: string, message: string): Promise<boolean>;
190
+ See `src/service.ts` for the canonical definition.
191
+ Key types:
186
192
 
187
- /** Wait for all running and queued agents to complete. */
188
- waitForAll(): Promise<void>;
189
-
190
- /** Whether any agents are running or queued. */
191
- hasRunning(): boolean;
192
- }
193
-
194
- export interface SpawnOptions {
195
- description?: string;
196
- model?: string;
197
- maxTurns?: number;
198
- thinkingLevel?: string;
199
- isolated?: boolean;
200
- inheritContext?: boolean;
201
- foreground?: boolean;
202
- /** Skip the concurrency queue — start immediately. */
203
- bypassQueue?: boolean;
204
- isolation?: "worktree";
205
- }
206
- ```
193
+ - `SubagentsService` `spawn`, `getRecord`, `listAgents`, `abort`, `steer`, `waitForAll`, `hasRunning`.
194
+ - `SubagentRecord` — serializable agent snapshot (no live session objects).
195
+ - `SpawnOptions` — `description`, `model`, `maxTurns`, `thinkingLevel`, `isolated`, `inheritContext`, `foreground`, `bypassQueue`, `isolation`.
196
+ - `SUBAGENT_EVENTS` channel constants for `pi.events` subscriptions.
207
197
 
208
198
  ### Accessor pattern
209
199
 
210
200
  ```typescript
211
- const KEY = Symbol.for("pi:service:subagents");
201
+ const SERVICE_KEY = Symbol.for("@gotgenes/pi-subagents:service");
212
202
 
213
- export function publishSubagentsAPI(api: SubagentsAPI): void {
214
- (globalThis as any)[KEY] = api;
203
+ export function publishSubagentsService(service: SubagentsService): void {
204
+ (globalThis as Record<symbol, unknown>)[SERVICE_KEY] = service;
215
205
  }
216
206
 
217
- export function getSubagentsAPI(): SubagentsAPI | undefined {
218
- return (globalThis as any)[KEY];
207
+ export function getSubagentsService(): SubagentsService | undefined {
208
+ return (globalThis as Record<symbol, unknown>)[SERVICE_KEY] as
209
+ | SubagentsService
210
+ | undefined;
219
211
  }
220
212
  ```
221
213
 
@@ -237,23 +229,19 @@ They are fire-and-forget broadcast events — no request IDs, no reply channels.
237
229
  ### Consumer example: scheduling extension
238
230
 
239
231
  ```typescript
240
- // package.json:
241
- // "peerDependencies": { "@gotgenes/pi-subagents": ">=2.0.0" }
242
- // "peerDependenciesMeta": { "@gotgenes/pi-subagents": { "optional": true } }
243
-
244
232
  export default function (pi) {
245
233
  pi.on("session_start", async (event, ctx) => {
246
- let getSubagentsAPI;
234
+ let getSubagentsService;
247
235
  try {
248
- ({ getSubagentsAPI } = await import("@gotgenes/pi-subagents"));
236
+ ({ getSubagentsService } = await import("@gotgenes/pi-subagents"));
249
237
  } catch {
250
238
  return; // pi-subagents not installed
251
239
  }
252
- const api = getSubagentsAPI();
253
- if (!api) return;
240
+ const svc = getSubagentsService();
241
+ if (!svc) return;
254
242
 
255
243
  setInterval(() => {
256
- api.spawn("Explore", "Check for stale TODOs", {
244
+ svc.spawn("Explore", "Check for stale TODOs", {
257
245
  bypassQueue: true,
258
246
  });
259
247
  }, 60 * 60 * 1000);
@@ -267,13 +255,13 @@ export default function (pi) {
267
255
  export default function (pi) {
268
256
  pi.events.on("subagents:completed", async (data) => {
269
257
  const { id } = data as { id: string };
270
- let getSubagentsAPI;
258
+ let getSubagentsService;
271
259
  try {
272
- ({ getSubagentsAPI } = await import("@gotgenes/pi-subagents"));
260
+ ({ getSubagentsService } = await import("@gotgenes/pi-subagents"));
273
261
  } catch {
274
262
  return;
275
263
  }
276
- const record = getSubagentsAPI()?.getRecord(id);
264
+ const record = getSubagentsService()?.getRecord(id);
277
265
  if (record?.result) {
278
266
  fs.appendFileSync("agent-log.jsonl", JSON.stringify(record) + "\n");
279
267
  }
@@ -281,32 +269,38 @@ export default function (pi) {
281
269
  }
282
270
  ```
283
271
 
284
- ## index.ts decomposition
272
+ ## index.ts decomposition (done — #54, #69, #70)
285
273
 
286
- The 1,894-line `index.ts` is decomposed into focused modules:
274
+ The original 1,894-line `index.ts` has been decomposed into focused modules:
287
275
 
288
276
  ```text
289
277
  src/
290
- ├── index.ts ← slimmed entry point: init, tool registration
278
+ ├── index.ts (274) ← slimmed entry point: init, tool registration
279
+ ├── runtime.ts (94) ← SubagentRuntime: session-scoped state + methods
291
280
  ├── tools/
292
- │ ├── agent-tool.ts ← Agent tool definition + execute
293
- │ ├── result-tool.ts ← get_subagent_result tool
294
- └── steer-tool.ts ← steer_subagent tool
295
- ├── notifications.ts completion nudges, custom renderer
296
- ├── activity-tracker.ts ← AgentActivity map + callback factory
297
- ├── agents-command.ts /agents slash command menu
298
- ├── api-adapter.ts SubagentsAPI implementation wrapping AgentManager
299
- └── (existing modules unchanged)
281
+ │ ├── agent-tool.ts (626) ← Agent tool definition + execute
282
+ │ ├── get-result-tool.ts ← get_subagent_result tool
283
+ ├── steer-tool.ts ← steer_subagent tool
284
+ │ └── helpers.ts shared tool utilities
285
+ ├── handlers/
286
+ ├── lifecycle.ts session_start, session_before_switch, session_shutdown
287
+ │ └── tool-start.ts tool_execution_start handler
288
+ ├── notification.ts ← completion nudges, custom renderer
289
+ ├── renderer.ts ← notification TUI component
290
+ ├── ui/agent-menu.ts (677) ← /agents slash command menu
291
+ ├── service-adapter.ts ← SubagentsService implementation wrapping AgentManager
292
+ └── (existing domain modules unchanged)
300
293
  ```
301
294
 
302
295
  Each extracted module receives narrow constructor-injected dependencies rather than closing over module-level state.
296
+ Handlers call methods on narrow runtime interfaces — no raw field writes, no `widget!` reach-throughs.
303
297
 
304
- ## Phase plan
298
+ ## Phase plan (Phases 1–5 complete)
305
299
 
306
- ### Phase 1: Export `SubagentsAPI` from this package
300
+ ### Phase 1: Export `SubagentsService` from this package ✓ (done — #48)
307
301
 
308
- Add the `SubagentsAPI` interface, serializable types, and `Symbol.for()` accessor functions as public exports of this package.
309
- No behavioral changes to the core yet.
302
+ Added the `SubagentsService` interface, serializable types, `Symbol.for()` accessor functions, and `SUBAGENT_EVENTS` constants as public exports.
303
+ Wired `service-adapter.ts` to wrap `AgentManager` and call `publishSubagentsService()` at extension init.
310
304
 
311
305
  ### Phase 2: Remove scheduling ✓ (done — issue #52)
312
306
 
@@ -314,127 +308,225 @@ Deleted `schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`.
314
308
  Removed the `schedule` parameter from the `Agent` tool schema.
315
309
  Removed scheduler setup and lifecycle hooks from `index.ts`.
316
310
 
317
- ### Phase 3: Remove group-join, ad-hoc RPC; replace output-file
311
+ ### Phase 3: Remove group-join, ad-hoc RPC; replace output-file ✓ (done — #49, #61)
318
312
 
319
- Delete `group-join.ts`, `cross-extension-rpc.ts`.
320
- Replace `output-file.ts` with `SessionManager.create()` + `session-dir.ts` (#61).
321
- Simplify `index.ts` to use direct individual notifications.
322
- Emit lifecycle events on `pi.events` for external consumers.
313
+ Deleted `group-join.ts`, `cross-extension-rpc.ts` (#49).
314
+ Replaced `output-file.ts` with `SessionManager.create()` + `session-dir.ts` (#61).
315
+ Simplified `index.ts` to use direct individual notifications.
316
+ Lifecycle events emitted on `pi.events` for external consumers.
323
317
 
324
- ### Phase 4: Implement and publish `SubagentsAPI`
318
+ ### Phase 4: Implement and publish `SubagentsService` ✓ (done — #48)
325
319
 
326
- Wire `api-adapter.ts` to wrap `AgentManager` and call `publishSubagentsAPI()` at extension init.
327
- Resolve model strings inside the adapter (fixing upstream [tintinweb/pi-subagents#60]).
320
+ Wired `service-adapter.ts` to wrap `AgentManager` and call `publishSubagentsService()` at extension init.
321
+ Model strings are resolved inside the adapter.
328
322
 
329
- ### Phase 5: Decompose `index.ts` ✓ (done — issue #54)
323
+ ### Phase 5: Decompose `index.ts` ✓ (done — #54, #69, #70, #87)
330
324
 
331
- Extracted tools, notifications, activity tracking, and the `/agents` command into separate modules.
332
- `src/index.ts` shrank from ~1,619 lines to ~265 lines.
325
+ Extracted tools, notifications, activity tracking, event handlers, and the `/agents` command into separate modules.
326
+ Created `SubagentRuntime` factory to hold session-scoped state.
327
+ `src/index.ts` shrank from ~1,894 lines to ~274 lines.
333
328
 
334
329
  ### Phase 6 (future): Extract UI to `@gotgenes/pi-subagents-ui`
335
330
 
336
- 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.
331
+ Move `ui/agent-widget.ts`, `ui/conversation-viewer.ts`, the `/agents` command, notifications, and activity tracking to a separate extension that consumes `SubagentsService` + lifecycle events.
337
332
  This phase is deferred until the API boundary is proven stable in production.
338
333
 
339
- ## Structural refactoring roadmap (post-#54)
334
+ ## Structural refactoring roadmap (post-#54) ✓ complete
340
335
 
341
- The Issue #54 decomposition created focused modules but left several structural cleanup opportunities on the table.
342
- The following issues track the work needed to bring `pi-subagents` to the same level of testability and composability as `pi-permission-system`.
336
+ All structural refactoring phases are complete.
337
+ See `git log` for the full history; issue references are preserved below for traceability.
343
338
 
344
- ### Phase 1: Foundation
339
+ | Phase | Issue | Summary |
340
+ | ------------------ | ------------------ | --------------------------------------------------------------------- |
341
+ | Foundation | #69, #71, #76, #80 | SubagentRuntime, pure assembler, cwd injection, config consolidation |
342
+ | Core decomposition | #84, #72, #87, #70 | WorktreeManager, AgentManager DI, runtime methods, handler extraction |
343
+ | Interface polish | #66, #77 | SDK types, projectAgentsDir |
344
+ | Features | #61 | JSONL session transcripts |
345
345
 
346
- These issues are independent of each other and can land in any order.
347
- Together they eliminate module-scope mutable state, create a testable functional core, and simplify the agent-types API.
346
+ The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
348
347
 
349
- 1. **gotgenes/pi-packages#69** ✓ — Create `SubagentRuntime`
350
- - Move `defaultMaxTurns`, `graceTurns`, `agentActivity`, `currentCtx`, and widget references out of closure/module scope into a single factory-constructed object.
351
- - This unblocks handler extraction (Issue #70) by giving handlers a concrete deps bag instead of closure variables.
348
+ ---
352
349
 
353
- 2. **gotgenes/pi-packages#71** Extract pure agent-session assembler from `agent-runner.ts`
354
- - Split `runAgent()` into a pure configuration assembler (~200 lines) and an IO shell (~200 lines).
355
- - The assembler becomes independently testable without mocking the Pi SDK.
350
+ ## Next target: AgentManager internal decomposition
356
351
 
357
- 3. **gotgenes/pi-packages#76** Inject `cwd` into `AgentManager`
358
- - Replaced the `process.cwd()` call in `dispose()` with a constructor parameter.
352
+ The structural refactoring roadmap decomposed the extension entry point and established clean module boundaries.
353
+ AgentManager itself the central class was not touched structurally.
354
+ A design review reveals three tangled responsibilities and two systemic patterns that inflate complexity.
359
355
 
360
- 4. **gotgenes/pi-packages#80** ✓ — Consolidate `getConfig` / `getAgentConfig` into a single resolution path
361
- - Replaced the two overlapping lookup functions with a single `resolveAgentConfig(type): AgentConfig` that handles the unknown-type fallback internally.
362
- - Eliminated the duplicated fallback chain exposed by #71 and simplified test mock setup.
356
+ ### Problem statement
363
357
 
364
- ### Phase 2: Core decomposition
358
+ AgentManager is a 500-line class that serves as the single mediator between tool callers and the agent runner.
359
+ Every concern passes through it because it owns the `AgentRecord`.
365
360
 
366
- These build on Phase 1 and should land after it.
361
+ Three responsibilities are tangled:
367
362
 
368
- 1. **gotgenes/pi-packages#84** Extract `GitWorktreeManager` class from `worktree.ts`
369
- - Added `WorktreeManager` interface and `GitWorktreeManager` class that captures `cwd` at construction.
370
- - Prerequisite for #72 separated the real-object extraction from the DI refactor.
363
+ 1. **Record registry** — create, track, query, clean up `AgentRecord` instances.
364
+ 2. **Concurrency control** queue, running count, drain, `bypassQueue`.
365
+ 3. **Execution orchestration**thread options to the runner, intercept callbacks to update records, wire abort signals, manage worktree lifecycle.
371
366
 
372
- 2. **gotgenes/pi-packages#72** Dependency-inject `AgentManager`'s collaborators
373
- - Defined `AgentRunner` interface (execution boundary) and `ResumeOptions` type in `agent-runner.ts`.
374
- - Converted `AgentManager` constructor from 6 positional parameters to an `AgentManagerOptions` bag with injected `AgentRunner` and `WorktreeManager`.
375
- - Removed all runtime imports of `agent-runner.ts` and `worktree.ts` from `agent-manager.ts` (only `import type` remains).
376
- - Migrated all tests from `vi.mock()` module stubs to `vi.fn()` interface stubs.
367
+ `startAgent()` alone is ~130 lines because it handles all three.
368
+ The `.then()` / `.catch()` blocks mix status updates (job 1), worktree cleanup (job 3), notification callbacks (job 1), and queue draining (job 2).
377
369
 
378
- 3. **gotgenes/pi-packages#87** Evolve `SubagentRuntime` from data bag to object with methods
379
- - Added session-context methods (`setSessionContext`, `clearSessionContext`) and widget delegation methods (`setUICtx`, `onTurnStart`, `markFinished`, `updateWidget`, `ensureTimer`).
380
- - Prerequisite for #70 — without runtime methods, extracted handlers would move LoD violations and output-argument smells into handler classes.
370
+ Two systemic patterns compound the problem:
381
371
 
382
- 4. **gotgenes/pi-packages#70** Extract event handlers into `src/handlers/`
383
- - Moved the four inline lambdas (`session_start`, `session_before_switch`, `session_shutdown`, `tool_execution_start`) into `SessionLifecycleHandler` and `ToolStartHandler` classes.
384
- - Handlers call methods on narrow runtime interfaces — no raw field writes, no `widget!` reach-throughs.
372
+ ### Problem 1: Callback threading
385
373
 
386
- ### Phase 3: Interface polish
374
+ `SpawnOptions` carries 6 `on*` callback fields.
375
+ They thread through three layers:
387
376
 
388
- Small cleanups that are safest after the structural changes settle.
377
+ ```text
378
+ agent-tool.ts (UI tracking state)
379
+ → AgentManager.startAgent() wraps each to update the record, then forwards
380
+ → runner.run() subscribes to session events, calls callbacks
381
+ ```
389
382
 
390
- 1. **gotgenes/pi-packages#66** Replace `as any` casts with proper SDK types
391
- - Type-only change in the tool/menu factory dep interfaces.
392
- - Best done after Issues #69 and #70 when the interfaces are stable.
383
+ The callbacks serve two purposes that are tangled together:
393
384
 
394
- 2. **gotgenes/pi-packages#77** — Add `projectAgentsDir` to `AgentMenuDeps`
395
- - Remove the inline `process.cwd()` lambda from the menu handler.
385
+ 1. **Record statistics** — `onToolActivity` increments `toolUses`, `onAssistantUsage` accumulates `lifetimeUsage`, `onCompaction` increments `compactionCount`, `onSessionCreated` captures the session and output file.
386
+ This is internal bookkeeping that belongs to the record.
387
+ 2. **UI streaming** — the same callbacks update the widget's active-tool display, response text preview, and turn counter.
388
+ This is presentation that belongs to the UI layer.
396
389
 
397
- ### Phase 4: Features and cross-cutting concerns
390
+ The session already emits all of these events via `session.subscribe()`.
391
+ The runner subscribes to session events, translates them into callback invocations, AgentManager wraps each callback to update the record, then forwards to the caller's callback.
392
+ Three layers reimplementing what a single event subscription could provide.
398
393
 
399
- 1. **gotgenes/pi-packages#61** Port transcript logging to Pi's official JSONL session format
400
- - Replaced `output-file.ts` with `SessionManager.create()` + `session-dir.ts`.
401
- - Subagent sessions are persisted under `<parent-session-dir>/<parent-session-basename>/tasks/` with `parentSession` header linking.
394
+ ### Problem 2: Live `ctx` capture
402
395
 
403
- 2. **gotgenes/pi-packages#22** Parent-session resolution for `nicobailon/pi-subagents` children
404
- - Cross-extension issue that spans `pi-permission-system` and `pi-subagents`.
405
- - Requires coordination on env-var conventions.
406
- - Not blocked by the structural refactor but logically separate from it.
396
+ `ctx: ExtensionContext` is a mutable reference to the parent session.
397
+ It is captured into `SpawnArgs` and held in the concurrency queue:
407
398
 
408
- ### Dependency graph
399
+ ```typescript
400
+ const args: SpawnArgs = { pi, ctx, type, prompt, options };
401
+ this.queue.push({ id, args }); // ctx held until dequeue
402
+ ```
409
403
 
410
- ```text
411
- #69 (SubagentRuntime) ✓ ──► #87 (runtime methods) ✓ ─┬─► #70 (handler extraction) ✓
412
-
413
- #71 (pure assembler) ✓ │
414
- #80 (config lookup) ✓ │
415
- #76 (cwd injection) ✓ │
416
- #84 (WorktreeManager) ✓ │
417
- #72 (AgentManager DI) ────────────────────────────┘──(optional)──► #70
418
-
419
- #66 (type casts) ◄─────(after structural changes settle)
420
- #77 (projectAgentsDir) ◄─(after #66 or parallel)
421
-
422
- #61 (transcript format) ✓
423
- #22 (parent session) ◄──(cross-extension, independent)
404
+ When the queued agent dequeues, `runAgent()` reads from the live `ctx`:
405
+
406
+ - `ctx.cwd` — directory that may have changed.
407
+ - `ctx.getSystemPrompt()` — live method call on a potentially stale session.
408
+ - `ctx.model` model that may have been switched.
409
+ - `ctx.modelRegistry` registry reference.
410
+
411
+ If the parent session changes between queue and dequeue (model switch, cwd change, session restart), the agent reads invalid state.
412
+ The same live reference persists in `runtime.currentCtx` for the service-adapter.
413
+
414
+ Additionally, `inheritContext` calls `ctx.sessionManager.getBranch()` at run time.
415
+ The user's intent is to fork the conversation as it existed when they asked for the agent — not the conversation at some arbitrary later point when a queue slot opens.
416
+
417
+ ### Design: snapshot at spawn time
418
+
419
+ Replace the live `ctx` capture with a plain data snapshot taken once at spawn time:
420
+
421
+ ```typescript
422
+ interface ParentSnapshot {
423
+ cwd: string;
424
+ systemPrompt: string;
425
+ model: unknown;
426
+ modelRegistry: { find(...): unknown; getAvailable?(): ... };
427
+ parentContext?: string; // pre-built text if inheritContext
428
+ }
424
429
  ```
425
430
 
426
- ### Recommended order
431
+ This snapshot is:
427
432
 
428
- The recommended sequence is:
433
+ - Captured once in `spawn()` (or by the tool before calling `spawn()`).
434
+ - Stored in `SpawnArgs` instead of `ctx`.
435
+ - Passed to `runner.run()` instead of `ctx: ExtensionContext`.
436
+ - Immutable — no staleness risk, no session-lifetime coupling.
437
+
438
+ `runAgent()` already reads exactly these 4 values from `ctx` and never touches it again.
439
+ `buildParentContext()` also reads once and produces a string.
440
+ The snapshot formalizes what is already happening, and makes the "read once" guarantee structural.
441
+
442
+ ### Design: session-event observation replaces callback threading
443
+
444
+ The session emits events via `session.subscribe()`.
445
+ Today, `runner.run()` subscribes and translates events into `RunOptions.on*()` callbacks, AgentManager wraps those to update the record, then forwards to the caller.
446
+
447
+ The target replaces this three-layer chain with direct subscription:
429
448
 
430
449
  ```text
431
- #69 ✓ → #71 ✓ → #80 ✓ → #76 ✓ → #84 ✓ → #72 ✓ → #87 ✓ → #70 ✓ → #66 → #77 → #61 ✓
450
+ session.subscribe()
451
+
452
+ ┌─────────────┼─────────────┐
453
+ │ │
454
+ Record observer UI observer
455
+ (accumulates stats on record) (updates widget state)
456
+ managed by AgentManager managed by agent-tool
457
+ subscribes in startAgent() subscribes after spawn
432
458
  ```
433
459
 
434
- Phase 1 is complete; Phase 2 is complete.
435
- Issue #61 (transcript format) is complete.
436
- The next issue is #66 (replace `as any` casts with proper SDK types).
437
- Issue #22 is a parallel cross-extension track and does not gate the structural work.
460
+ AgentManager subscribes to the session to update the record (toolUses, lifetimeUsage, compactionCount, outputFile).
461
+ The agent-tool subscribes to the session to stream UI state (active tools, response text, turn count).
462
+ Neither layer wraps or forwards the other's callbacks.
463
+
464
+ `RunOptions` drops all 6 `on*` fields and becomes pure configuration.
465
+ `SpawnOptions` drops all 6 `on*` fields and becomes identity + dispatch mode.
466
+ The session reference reaches callers via `record.session` (already stored) or via an `onSessionCreated` callback that is the one callback that remains (it delivers the session object, enabling the external subscription).
467
+
468
+ ### Design: record state machine
469
+
470
+ Status transitions are scattered across 6 locations (`startAgent` `.then()`, `.catch()`, `resume()`, `abort()`, `abortAll()`, `drainQueue()`).
471
+ Each location sets `record.status` plus associated fields (`completedAt`, `result`, `error`) in ad-hoc combinations.
472
+
473
+ Extract a state machine on `AgentRecord` (or a thin wrapper) that owns all transitions:
474
+
475
+ ```typescript
476
+ record.markRunning(startedAt)
477
+ record.markCompleted(result, completedAt)
478
+ record.markError(error)
479
+ record.markStopped()
480
+ record.resetForResume()
481
+ ```
482
+
483
+ Each method sets exactly the fields that belong to that transition.
484
+ Invalid transitions (e.g., `markCompleted` on an already-stopped record) are no-ops.
485
+ The `if (record.status !== "stopped")` guards in `.then()` and `.catch()` become part of the transition logic rather than scattered conditionals.
486
+
487
+ ### Phased implementation
488
+
489
+ The three designs are independent and can land in any order.
490
+ The recommended sequence minimizes intermediate churn.
491
+
492
+ #### Step 1: Record state machine
493
+
494
+ Extract status-transition methods onto `AgentRecord` (or a `RecordManager` wrapper).
495
+ Purely mechanical — replace scattered field writes with method calls.
496
+ No interface changes for callers.
497
+
498
+ This is the lowest-risk change and immediately reduces `startAgent()` line count.
499
+
500
+ #### Step 2: Parent snapshot
501
+
502
+ Replace `ctx: ExtensionContext` in `SpawnArgs` with a `ParentSnapshot` data object.
503
+ Capture the snapshot in `spawn()` or at the tool call site.
504
+ Update `runner.run()` signature to accept `ParentSnapshot` instead of `ctx`.
505
+ Remove `pi: ExtensionAPI` from `SpawnArgs` (it is only used to pass to `runner.run()`, which only uses it for `detectEnv()` — that can accept a shell-exec function instead).
506
+
507
+ This change narrows the `AgentRunner` interface and eliminates live-reference capture.
508
+
509
+ #### Step 3: Session-event observation
510
+
511
+ Replace the callback-threading pattern with direct session subscriptions.
512
+ AgentManager subscribes to the session after creation to update the record.
513
+ The agent-tool subscribes to the session after spawn to stream UI state.
514
+ `RunOptions` and `SpawnOptions` drop all `on*` callback fields.
515
+
516
+ This is the largest change but depends on Step 2 (the runner signature is already narrower) and benefits from Step 1 (the record's transition methods encapsulate the stats updates that the subscription drives).
517
+
518
+ ### Expected outcome
519
+
520
+ | Metric | Before | After |
521
+ | --------------------------------- | ------ | ------------------------ |
522
+ | `SpawnOptions` fields | 19 | ~8 (identity + dispatch) |
523
+ | `RunOptions` fields | 15 | ~9 (config only) |
524
+ | `startAgent()` lines | ~130 | ~50 |
525
+ | Callback layers | 3 | 0 (direct subscription) |
526
+ | Live `ctx` references in queue | 1 | 0 (snapshot) |
527
+ | Scattered status-transition sites | 6 | 1 (state machine) |
528
+
529
+ ---
438
530
 
439
531
  ## Relationship with upstream
440
532
 
@@ -0,0 +1,176 @@
1
+ ---
2
+ issue: 102
3
+ issue_title: "Consolidate test AgentRecord construction into a shared factory"
4
+ ---
5
+
6
+ # Consolidate test AgentRecord construction into a shared factory
7
+
8
+ ## Problem Statement
9
+
10
+ Eight test files independently construct `AgentRecord` objects using three different patterns: copy-pasted `makeRecord()`/`mockRecord()` factory functions (5 files), inline `const baseRecord: AgentRecord = { ... }` literals (2 files), and `as AgentRecord` casts.
11
+ When issue #98 converts `AgentRecord` from an interface to a class, every object-literal construction site breaks.
12
+ A shared factory confines that future breakage to a single file.
13
+
14
+ ## Goals
15
+
16
+ - Create a shared `createTestRecord()` factory in `test/helpers/make-record.ts`.
17
+ - Migrate all 7 affected test files to import the shared factory.
18
+ - No production code changes.
19
+ - No behavior changes — purely mechanical.
20
+
21
+ ## Non-Goals
22
+
23
+ - Converting `AgentRecord` to a class — that is issue #98, which depends on this change.
24
+ - Adding new test coverage — this is a refactoring of test infrastructure only.
25
+ - Touching `test/agent-manager.test.ts` — it constructs records via `manager.spawn()`, not literals.
26
+ - Consolidating other test helpers (mock sessions, mock TUI, etc.).
27
+
28
+ ## Background
29
+
30
+ ### Relevant modules
31
+
32
+ | Module | Role |
33
+ | ------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
34
+ | `src/types.ts` | Defines the `AgentRecord` interface (20+ fields) |
35
+ | `test/tools/agent-tool.test.ts` | `makeRecord()` factory — 12 default fields, status "completed" |
36
+ | `test/tools/get-result-tool.test.ts` | `makeRecord()` factory — 10 default fields, status "completed" |
37
+ | `test/tools/steer-tool.test.ts` | `makeRecord()` factory — 9 default fields, status "running", includes mock session, uses `as AgentRecord` cast |
38
+ | `test/ui/agent-menu.test.ts` | `makeRecord()` factory — 10 default fields, status "completed" |
39
+ | `test/conversation-viewer.test.ts` | `mockRecord()` factory — 6 default fields, status "running", uses `as AgentRecord` cast |
40
+ | `test/notification.test.ts` | 4 inline `baseRecord` literals, status "completed" |
41
+ | `test/service-adapter.test.ts` | 4 inline `baseRecord` / `minimal` literals, mixed statuses |
42
+
43
+ ### Convention from sibling packages
44
+
45
+ `packages/pi-autoformat/test/helpers/rpc.ts` is the only existing shared test helper in the monorepo.
46
+ The pattern is a plain module under `test/helpers/` with named exports — no class, no framework.
47
+
48
+ ### Relationship to issue #98
49
+
50
+ Issue #98 plans to extract `MutableAgentRecord` as a class implementing the `AgentRecord` interface.
51
+ That plan explicitly notes: "All test files that construct `AgentRecord` literals — they create interface-compatible objects, not class instances" and lists them as unchanged.
52
+ Once this consolidation lands, issue #98's "unchanged" assumption becomes trivially true: only the shared factory needs updating if the construction API changes.
53
+
54
+ ## Design Overview
55
+
56
+ ### Shared factory: `createTestRecord()`
57
+
58
+ A single function in `test/helpers/make-record.ts` with the `Partial<AgentRecord>` override pattern already used by 5 of the 7 files:
59
+
60
+ ```typescript
61
+ import type { AgentRecord } from "../../src/types.js";
62
+
63
+ export function createTestRecord(
64
+ overrides: Partial<AgentRecord> = {},
65
+ ): AgentRecord {
66
+ return {
67
+ id: "agent-1",
68
+ type: "general-purpose",
69
+ description: "Test task",
70
+ status: "completed",
71
+ result: "All done.",
72
+ toolUses: 3,
73
+ startedAt: 1000,
74
+ completedAt: 2000,
75
+ compactionCount: 0,
76
+ lifetimeUsage: { input: 500, output: 500, cacheWrite: 0 },
77
+ ...overrides,
78
+ };
79
+ }
80
+ ```
81
+
82
+ ### Default-value decisions
83
+
84
+ The defaults match the majority pattern (6 of 7 files default to a "completed" record).
85
+ The two files that need "running" records (`steer-tool`, `conversation-viewer`) pass `{ status: "running" }` as overrides — a one-field change.
86
+
87
+ The `as AgentRecord` cast used by `steer-tool.test.ts` and `conversation-viewer.test.ts` is no longer needed: the shared factory returns a full `AgentRecord` with all required fields populated, so TypeScript is satisfied without casting.
88
+
89
+ ### Migration strategy for inline-literal files
90
+
91
+ `notification.test.ts` and `service-adapter.test.ts` construct multiple distinct inline literals — they don't have a single factory.
92
+ Each inline literal becomes a `createTestRecord({ ...specific overrides })` call.
93
+ The `baseRecord` variable declared in each `describe` block is replaced with a call to `createTestRecord()`.
94
+
95
+ For `service-adapter.test.ts`, the top-level `baseRecord` with custom values (`id: "abc-123"`, `type: "Explore"`, etc.) becomes `createTestRecord({ id: "abc-123", type: "Explore", ... })`.
96
+
97
+ ## Module-Level Changes
98
+
99
+ ### New files
100
+
101
+ 1. `test/helpers/make-record.ts` — exports `createTestRecord()`.
102
+
103
+ ### Changed files
104
+
105
+ 1. `test/tools/agent-tool.test.ts` — remove local `makeRecord()`, import `createTestRecord` from helpers.
106
+ 2. `test/tools/get-result-tool.test.ts` — remove local `makeRecord()`, import `createTestRecord` from helpers.
107
+ 3. `test/tools/steer-tool.test.ts` — remove local `makeRecord()`, import `createTestRecord` from helpers.
108
+ Replace default `status: "running"` and `session` with overrides in each call site.
109
+ 4. `test/ui/agent-menu.test.ts` — remove local `makeRecord()`, import `createTestRecord` from helpers.
110
+ 5. `test/conversation-viewer.test.ts` — remove local `mockRecord()`, import `createTestRecord` from helpers.
111
+ Replace default `status: "running"` and `startedAt: Date.now()` with overrides in each call site.
112
+ 6. `test/notification.test.ts` — replace 4 inline `baseRecord` literals with `createTestRecord()` calls.
113
+ 7. `test/service-adapter.test.ts` — replace inline `baseRecord` / `minimal` / per-test literals with `createTestRecord()` calls.
114
+
115
+ ### Unchanged files
116
+
117
+ 1. `test/agent-manager.test.ts` — constructs records via `manager.spawn()`, not literals.
118
+ 2. All production source files — no changes.
119
+
120
+ ## Test Impact Analysis
121
+
122
+ ### New tests enabled
123
+
124
+ 1. A small sanity test in `test/helpers/make-record.test.ts` verifying that `createTestRecord()` returns a valid `AgentRecord` with expected defaults and that overrides are applied.
125
+ This is optional — the factory is exercised transitively by every consumer — but it documents the contract for future maintainers (especially when #98 changes construction).
126
+
127
+ ### Existing tests that become redundant
128
+
129
+ None.
130
+ This is a pure refactoring of test infrastructure; no production behavior changes.
131
+
132
+ ### Existing tests that stay as-is
133
+
134
+ All existing test assertions stay unchanged.
135
+ Only the construction of `AgentRecord` objects in test setup code changes; the assertions that read those records are untouched.
136
+
137
+ ## TDD Order
138
+
139
+ 1. **Create shared factory and its test.**
140
+ Add `test/helpers/make-record.ts` with `createTestRecord()`.
141
+ Add `test/helpers/make-record.test.ts` verifying defaults and override behavior.
142
+ Commit: `test: add shared createTestRecord factory (#102)`
143
+
144
+ 2. **Migrate tool test files.**
145
+ Update `agent-tool.test.ts`, `get-result-tool.test.ts`, `steer-tool.test.ts` to import `createTestRecord` and remove local `makeRecord()` functions.
146
+ Run `pnpm vitest run test/tools/agent-tool.test.ts test/tools/get-result-tool.test.ts test/tools/steer-tool.test.ts` to verify.
147
+ Commit: `test: migrate tool tests to shared createTestRecord (#102)`
148
+
149
+ 3. **Migrate UI test files.**
150
+ Update `agent-menu.test.ts` and `conversation-viewer.test.ts` to import `createTestRecord` and remove local `makeRecord()`/`mockRecord()` functions.
151
+ Run `pnpm vitest run test/ui/agent-menu.test.ts test/conversation-viewer.test.ts` to verify.
152
+ Commit: `test: migrate UI tests to shared createTestRecord (#102)`
153
+
154
+ 4. **Migrate notification and service-adapter tests.**
155
+ Update `notification.test.ts` and `service-adapter.test.ts` to replace inline literals with `createTestRecord()` calls.
156
+ Run `pnpm vitest run test/notification.test.ts test/service-adapter.test.ts` to verify.
157
+ Commit: `test: migrate notification and service-adapter tests to shared createTestRecord (#102)`
158
+
159
+ 5. **Final verification.**
160
+ Run full test suite (`pnpm vitest run`) and type check (`pnpm run check`) to confirm no regressions.
161
+ Commit: not needed if steps 2–4 are clean; otherwise a fix-up commit.
162
+
163
+ ## Risks and Mitigations
164
+
165
+ | Risk | Mitigation |
166
+ | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
167
+ | Shared defaults don't match a test's assumptions, causing silent false-passes | Each migration step runs the affected test file immediately; review each test's overrides to ensure they still express the test's intent |
168
+ | `steer-tool.test.ts` relies on `session: { fake: true }` in its factory default, which the shared factory omits | Pass `session` as an override at each call site; the mock session is test-specific and doesn't belong in shared defaults |
169
+ | `conversation-viewer.test.ts` uses `startedAt: Date.now()` which the shared factory replaces with `1000` | Replace with `createTestRecord({ status: "running" })`; `startedAt` value is not asserted in any conversation-viewer test |
170
+ | `service-adapter.test.ts` uses custom `id`, `type`, `description` values that carry semantic meaning in its assertions | Pass those values explicitly as overrides to `createTestRecord()` |
171
+ | The `as AgentRecord` cast removal changes type-checking strictness | The shared factory returns a complete object satisfying all required fields, so removing the cast is strictly safer |
172
+
173
+ ## Open Questions
174
+
175
+ - The factory name `createTestRecord` vs `makeRecord` vs `makeAgentRecord`: the plan uses `createTestRecord` to distinguish it from the production `AgentRecord` constructor that #98 will introduce.
176
+ If #98 names its constructor differently, this can be revisited.
@@ -0,0 +1,41 @@
1
+ ---
2
+ issue: 61
3
+ issue_title: "feat: port subagent transcript logging to Pi's official JSONL session format"
4
+ ---
5
+
6
+ # Retro: #61 — port subagent transcript logging to Pi's official JSONL session format
7
+
8
+ ## Final Retrospective (2026-05-20T17:15:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned, implemented, and shipped a migration from the bespoke `output-file.ts` transcript format to Pi's official JSONL session format via `SessionManager.create()`.
13
+ The change replaced 143 lines of manual streaming code with 3 lines leveraging the SDK's native persistence, nested subagent sessions under the parent session directory with `parentSession` header linking.
14
+ Released as `pi-subagents-v6.0.0` (major version bump due to breaking transcript format change).
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - The plan-to-implementation translation was clean: 6 TDD steps mapped to 7 commits (one extra `fix:` for biome lint).
21
+ No steps needed reordering or merging.
22
+ - The `ask_user` design decision gates during planning (persistence strategy, file location) produced clear answers that avoided rework during implementation.
23
+ - Research into nicobailon/pi-subagents, edxeth/pi-subagents, and HazAT/pi-interactive-subagents provided useful reference for the session directory layout, confirming the parent-relative nesting pattern.
24
+ - The biome lint catch on the unused `cwd` parameter led to a better design — incorporating `cwd` into the temp fallback path for project namespacing — rather than a mechanical underscore prefix.
25
+
26
+ #### What caused friction (agent side)
27
+
28
+ - `missing-context` — The plan listed test impact for `agent-runner.test.ts` but didn't grep for other test files mocking `SessionManager` or `ctx.sessionManager`.
29
+ Three additional files needed updating: `agent-runner-extension-tools.test.ts`, `print-mode.test.ts`, and `test/tools/agent-tool.test.ts`.
30
+ The testing skill explicitly says "grep for ALL test files that construct a compatible mock — not just factory helpers."
31
+ Impact: ~5 minutes of reactive fixes during Step 4.
32
+ Self-identified at implementation time.
33
+
34
+ - `missing-context` — The plan didn't account for the timing difference between the old synchronous `record.outputFile` assignment (immediately after `spawn()`) and the new asynchronous availability (after `SessionManager.create()` runs inside `runAgent()`).
35
+ This required adding `session.sessionManager.getSessionFile()` in the `onSessionCreated` callback — a design decision made during implementation.
36
+ Impact: minor within-step rework, no extra commit needed.
37
+
38
+ #### What caused friction (user side)
39
+
40
+ - The dependency update to 0.75.4 was a reasonable pre-plan request, but it added ~10 minutes of tangential work (diagnosing `pnpm update` resolution behavior, normalizing version specifiers).
41
+ This could have been a separate commit/session, though batching it was pragmatic since it gave the plan access to the latest SDK types.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "6.0.0",
3
+ "version": "6.0.1",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },