@gotgenes/pi-subagents 5.8.2 → 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.
@@ -0,0 +1,284 @@
1
+ ---
2
+ issue: 61
3
+ issue_title: "feat: port subagent transcript logging to Pi's official JSONL session format"
4
+ ---
5
+
6
+ # Port subagent transcripts to Pi's official session format
7
+
8
+ ## Problem Statement
9
+
10
+ Subagent conversation transcripts are written as JSONL by `output-file.ts`, but use a bespoke flat format (`{ isSidechain, agentId, type, message, timestamp, cwd }`) that does not conform to Pi's official session file format.
11
+ Pi's `SessionManager` writes tree-structured JSONL with a session header, UUIDv7 entry IDs, `parentId` tree structure, and typed entry discriminants (`"message"`, `"compaction"`, etc.).
12
+ The bespoke format cannot be loaded by Pi's session tooling (session selector, export-html, resume) and requires manual `jq` inspection for debugging.
13
+
14
+ ## Goals
15
+
16
+ - Subagent transcripts written in Pi's official JSONL session format via `SessionManager`.
17
+ - Transcripts discoverable via the parent session path (nested under a parent-session-relative directory).
18
+ - Parent session linkage via the `parentSession` header field.
19
+ - Delete `output-file.ts` — the SDK's `SessionManager` handles all JSONL writing natively.
20
+ - Existing debugging use case preserved (full turn-by-turn history on disk).
21
+
22
+ ## Non-Goals
23
+
24
+ - Making subagent sessions resumable via Pi's `/resume` command (future work; requires session selection UX changes).
25
+ - Cross-extension parent-session resolution (issue #22 — separate track).
26
+ - Changing the `SessionManager` usage for the child `AgentSession` itself (the child session already uses `SessionManager.inMemory()`; we replace it with a persisted one).
27
+
28
+ ## Background
29
+
30
+ ### Current flow
31
+
32
+ 1. `agent-tool.ts` calls `createOutputFilePath(cwd, agentId, parentSessionId)` → creates `/tmp/pi-subagents-<uid>/<encoded-cwd>/<parentSessionId>/tasks/<agentId>.output`.
33
+ 2. `writeInitialEntry(path, agentId, prompt, cwd)` writes the first user message in the bespoke format.
34
+ 3. `streamToOutputFile(session, path, agentId, cwd)` subscribes to `turn_end` events and appends bespoke JSONL entries.
35
+ 4. `agent-manager.ts` calls the cleanup function on completion/error to do a final flush and unsubscribe.
36
+
37
+ ### Pi's SessionManager API
38
+
39
+ `SessionManager` from `@earendil-works/pi-coding-agent` provides:
40
+
41
+ - `SessionManager.create(cwd, sessionDir?)` — creates a persisted session file in the given directory.
42
+ - `newSession({ parentSession? })` — writes a `SessionHeader` with `parentSession` linking.
43
+ - `appendMessage(message)` — writes a `SessionMessageEntry` with auto-generated UUIDv7 `id` and `parentId` (tree structure).
44
+ - `appendCompaction(...)`, `appendCustomEntry(...)` — first-class support for all entry types.
45
+ - `getSessionFile()` — returns the path to the JSONL file on disk.
46
+
47
+ The child `AgentSession` (created by `createAgentSession`) accepts a `sessionManager` option.
48
+ Currently set to `SessionManager.inMemory(cwd)`, switching to `SessionManager.create(cwd, sessionDir)` makes the SDK write official-format JSONL automatically — no manual streaming code needed.
49
+
50
+ ### Reference implementations
51
+
52
+ nicobailon/pi-subagents places subagent sessions under `<parent-session-dir>/<parent-session-basename>/<runId>/`.
53
+ This keeps them discoverable via the parent session path without cluttering the main session list.
54
+ Our approach follows the same pattern: derive a `sessionDir` from the parent session file.
55
+
56
+ ### Constraints
57
+
58
+ - `agent-runner.ts` already imports `SessionManager` for the `inMemory()` call — switching to `create()` adds no new dependency.
59
+ - `ctx.sessionManager` is `ReadonlySessionManager` and provides `getSessionId()`, `getSessionFile()`, and `getSessionDir()`.
60
+ - The `code-style` skill requires keeping IO at the edges and not hiding dependencies.
61
+ The session directory derivation should be a pure function that receives the parent session file path.
62
+
63
+ ## Design Overview
64
+
65
+ ### Core change: persisted SessionManager in agent-runner
66
+
67
+ Replace `SessionManager.inMemory(cwd)` with `SessionManager.create(cwd, sessionDir)` inside `runAgent()`.
68
+ The `sessionDir` is derived from the parent's session file path, and the `parentSession` option links the child to the parent.
69
+
70
+ The key insight is that the SDK's `createAgentSession` already writes all messages through the `SessionManager` it receives.
71
+ By switching from in-memory to persisted, every message the child agent produces is automatically written in official JSONL format — no manual subscription or streaming code required.
72
+
73
+ ### Session directory derivation
74
+
75
+ ```typescript
76
+ /**
77
+ * Derive the session directory for a subagent from the parent session file.
78
+ * Layout: <parent-dir>/<parent-basename>/tasks/
79
+ *
80
+ * Example:
81
+ * parent: ~/.pi/agent/sessions/--home-user-project--/2026-05-20T12-00-00Z_.jsonl
82
+ * result: ~/.pi/agent/sessions/--home-user-project--/2026-05-20T12-00-00Z_/tasks/
83
+ *
84
+ * Falls back to a temp directory when the parent session is not persisted.
85
+ */
86
+ function deriveSubagentSessionDir(
87
+ parentSessionFile: string | undefined,
88
+ cwd: string,
89
+ ): string;
90
+ ```
91
+
92
+ ### Data flow (after)
93
+
94
+ 1. `agent-tool.ts` passes `parentSessionFile` (from `ctx.sessionManager.getSessionFile()`) and `parentSessionId` to `agent-manager.ts` via `SpawnOptions`.
95
+ 2. `agent-manager.ts` threads them to `agent-runner.ts` via `RunOptions`.
96
+ 3. `agent-runner.ts` calls `deriveSubagentSessionDir(parentSessionFile, cwd)` and creates `SessionManager.create(cwd, sessionDir)` with `{ parentSession: parentSessionId }`.
97
+ 4. The `createAgentSession` SDK call receives this persisted `SessionManager`.
98
+ 5. All messages are written to disk automatically by the SDK.
99
+ 6. `agent-runner.ts` returns the session file path in `RunResult` (via `sessionManager.getSessionFile()`).
100
+ 7. `agent-tool.ts` stores the path on `AgentRecord.outputFile` for display in notifications/UI.
101
+
102
+ ### What gets deleted
103
+
104
+ - `output-file.ts` — entirely replaced by the persisted `SessionManager`.
105
+ - `output-file.test.ts` — the `encodeCwd` tests are no longer needed (Pi's `SessionManager` handles directory encoding internally).
106
+ - `streamToOutputFile`, `writeInitialEntry`, `createOutputFilePath` imports from `agent-tool.ts`.
107
+ - `outputCleanup` field from `AgentRecord` (no manual cleanup needed; the `SessionManager` is append-only).
108
+ - The `onSessionCreated` wrapper in `agent-tool.ts` that wires output-file streaming.
109
+
110
+ ### What changes
111
+
112
+ - `RunOptions` gains `parentSessionFile?: string` and `parentSessionId?: string`.
113
+ - `RunResult` gains `sessionFile?: string` (path to the persisted session JSONL).
114
+ - `SpawnOptions` gains `parentSessionFile?: string` and `parentSessionId?: string`.
115
+ - `AgentRecord.outputCleanup` is removed (no manual flush/unsubscribe lifecycle).
116
+ - `agent-manager.ts` removes the `outputCleanup` calls in completion/error paths.
117
+ - `agent-tool.ts` sets `record.outputFile` from the `RunResult.sessionFile` instead of calling `createOutputFilePath`.
118
+ - `notification.ts`, `renderer.ts`, `types.ts` — `outputFile` field semantics unchanged (still a path string); only the source changes.
119
+
120
+ ### Edge cases
121
+
122
+ 1. **Parent session not persisted** (e.g., API/headless mode where the parent uses `SessionManager.inMemory()`): `ctx.sessionManager.getSessionFile()` returns `undefined`.
123
+ Fallback: use a temp directory under `/tmp/pi-subagents-<uid>/` (similar to current behavior) so transcripts are still written to disk.
124
+ 2. **Worktree isolation**: `effectiveCwd` differs from `ctx.cwd`.
125
+ The `SessionManager.create()` call uses `effectiveCwd` (same as current `inMemory` call), but the `sessionDir` is still derived from the parent session — the session header's `cwd` field correctly records the worktree path.
126
+
127
+ ## Module-Level Changes
128
+
129
+ ### New file: `src/session-dir.ts`
130
+
131
+ Pure function `deriveSubagentSessionDir(parentSessionFile, cwd)`.
132
+ Extracts the parent session basename, constructs `<parent-dir>/<parent-basename>/tasks/`.
133
+ Falls back to a temp directory when `parentSessionFile` is undefined.
134
+
135
+ ### Modified: `src/agent-runner.ts`
136
+
137
+ - Import `deriveSubagentSessionDir` from `session-dir.ts`.
138
+ - Add `parentSessionFile?: string` and `parentSessionId?: string` to `RunOptions`.
139
+ - Add `sessionFile?: string` to `RunResult`.
140
+ - Replace `SessionManager.inMemory(cfg.effectiveCwd)` with:
141
+
142
+ ```typescript
143
+ const sessionDir = deriveSubagentSessionDir(options.parentSessionFile, cfg.effectiveCwd);
144
+ const sessionManager = SessionManager.create(cfg.effectiveCwd, sessionDir);
145
+ sessionManager.newSession({ parentSession: options.parentSessionId });
146
+ ```
147
+
148
+ - After `session.prompt()`, capture `sessionManager.getSessionFile()` into `RunResult.sessionFile`.
149
+
150
+ ### Modified: `src/agent-manager.ts`
151
+
152
+ - Add `parentSessionFile?: string` and `parentSessionId?: string` to `SpawnOptions`.
153
+ - Thread them to `RunOptions` in the `runner.run()` call.
154
+ - Remove `outputCleanup` calls from the completion and error handlers.
155
+
156
+ ### Modified: `src/tools/agent-tool.ts`
157
+
158
+ - Remove import of `createOutputFilePath`, `streamToOutputFile`, `writeInitialEntry`.
159
+ - Remove the `onSessionCreated` wrapper that wired output-file streaming.
160
+ - Pass `parentSessionFile: ctx.sessionManager.getSessionFile()` and `parentSessionId: ctx.sessionManager.getSessionId()` in `SpawnOptions`.
161
+ - After spawn, set `record.outputFile` from the returned session file path (available via `onSessionCreated` callback which gives access to the session's `sessionManager`).
162
+
163
+ ### Modified: `src/types.ts`
164
+
165
+ - Remove `outputCleanup?: () => void` from `AgentRecord`.
166
+
167
+ ### Deleted: `src/output-file.ts`
168
+
169
+ Entire file removed.
170
+
171
+ ### Deleted: `test/output-file.test.ts`
172
+
173
+ Entire file removed.
174
+
175
+ ### Unchanged: `src/notification.ts`, `src/renderer.ts`
176
+
177
+ These read `record.outputFile` (a path string) — the field still exists, just populated differently.
178
+ The `NotificationDetails.outputFile` field is unchanged.
179
+
180
+ ## Test Impact Analysis
181
+
182
+ ### New tests enabled by extraction
183
+
184
+ 1. `test/session-dir.test.ts` — unit tests for `deriveSubagentSessionDir`:
185
+ - Parent session file present → derives correct nested path.
186
+ - Parent session file undefined → falls back to temp directory.
187
+ - Various path shapes (POSIX, Windows-like).
188
+
189
+ 2. `test/agent-runner.test.ts` — updated tests verifying:
190
+ - `SessionManager.create` is called (not `inMemory`) when `parentSessionFile` is provided.
191
+ - `newSession({ parentSession })` is called with the parent session ID.
192
+ - `RunResult.sessionFile` is populated from the session manager.
193
+
194
+ ### Tests that become redundant
195
+
196
+ - `test/output-file.test.ts` (`encodeCwd` tests) — the directory encoding is now handled by `deriveSubagentSessionDir` with a different layout.
197
+ The `encodeCwd` function is deleted.
198
+
199
+ ### Tests that stay as-is
200
+
201
+ - All `agent-manager.test.ts` tests that mock the runner — they don't depend on `output-file` internals.
202
+ - All `notification.test.ts` and `renderer.test.ts` tests — they read `record.outputFile` which remains a string.
203
+ - The `onSessionCreated` callback tests in `agent-tool.test.ts` need updating to remove the output-file wiring expectations.
204
+
205
+ ## TDD Order
206
+
207
+ ### Step 1: Add `deriveSubagentSessionDir` with tests
208
+
209
+ 1. Create `src/session-dir.ts` with the pure function.
210
+ 2. Create `test/session-dir.test.ts` with tests for parent-present and fallback cases.
211
+ 3. Commit: `feat: add deriveSubagentSessionDir for session directory derivation (#61)`
212
+
213
+ ### Step 2: Thread parent session info through SpawnOptions and RunOptions
214
+
215
+ 1. Add `parentSessionFile?: string` and `parentSessionId?: string` to `SpawnOptions` in `agent-manager.ts`.
216
+ 2. Add `parentSessionFile?: string` and `parentSessionId?: string` to `RunOptions` in `agent-runner.ts`.
217
+ 3. Add `sessionFile?: string` to `RunResult` in `agent-runner.ts`.
218
+ 4. Thread the new fields from `SpawnOptions` → `RunOptions` in `agent-manager.ts`.
219
+ 5. Update tests to verify the new fields are threaded.
220
+ 6. Commit: `feat: thread parent session info through spawn and run options (#61)`
221
+
222
+ ### Step 3: Switch agent-runner to persisted SessionManager
223
+
224
+ 1. In `runAgent()`, replace `SessionManager.inMemory(cfg.effectiveCwd)` with the persisted variant using `deriveSubagentSessionDir`.
225
+ 2. Call `sessionManager.newSession({ parentSession: options.parentSessionId })`.
226
+ 3. Capture `sessionManager.getSessionFile()` into `RunResult.sessionFile`.
227
+ 4. Update `agent-runner.test.ts` mocks to expect `SessionManager.create` instead of `SessionManager.inMemory`.
228
+ 5. Commit: `feat: use persisted SessionManager for subagent sessions (#61)`
229
+
230
+ ### Step 4: Remove output-file wiring from agent-tool and agent-manager
231
+
232
+ 1. Remove the `onSessionCreated` output-file wrapper in `agent-tool.ts`.
233
+ 2. Remove `createOutputFilePath`, `writeInitialEntry`, `streamToOutputFile` imports.
234
+ 3. Set `record.outputFile` from the session manager's file path (via `onSessionCreated` callback).
235
+ 4. Pass `parentSessionFile` and `parentSessionId` in spawn options.
236
+ 5. Remove `outputCleanup` calls from `agent-manager.ts` completion/error handlers.
237
+ 6. Remove `outputCleanup` from `AgentRecord` in `types.ts`.
238
+ 7. Update agent-tool and agent-manager tests.
239
+ 8. Commit: `feat: wire session file path through agent-tool, remove output-file streaming (#61)`
240
+
241
+ ### Step 5: Delete output-file.ts and its tests
242
+
243
+ 1. Delete `src/output-file.ts`.
244
+ 2. Delete `test/output-file.test.ts`.
245
+ 3. Run full test suite to verify no remaining references.
246
+ 4. Commit: `feat!: remove bespoke output-file transcript format (#61)`
247
+
248
+ ### Step 6: Documentation update
249
+
250
+ 1. Update `docs/architecture/architecture.md` — remove `output-file.ts` from the module listing, add `session-dir.ts`, mark #61 as complete.
251
+ 2. Commit: `docs: update architecture for session format migration (#61)`
252
+
253
+ ## Risks and Mitigations
254
+
255
+ ### Risk: SessionManager.create may fail in environments without a writable home directory
256
+
257
+ The temp-directory fallback in `deriveSubagentSessionDir` handles this case.
258
+ When the parent session file is unavailable (headless/API mode), we fall back to a temp directory just like the current implementation.
259
+
260
+ ### Risk: Persisted sessions accumulate disk space over time
261
+
262
+ Pi's session tooling already manages session storage.
263
+ Subagent sessions nested under the parent session directory are cleaned up when the parent session is deleted.
264
+ This is an improvement over the current `/tmp` location where files persist until system cleanup.
265
+
266
+ ### Risk: Tests that mock SessionManager.inMemory need updating
267
+
268
+ Step 3 explicitly plans for updating these mocks.
269
+ The change is localized to `agent-runner.ts` and its direct test file.
270
+
271
+ ### Risk: Breaking change for consumers that read outputFile paths
272
+
273
+ The `outputFile` field changes from `/tmp/pi-subagents-<uid>/.../tasks/<agentId>.output` to `~/.pi/agent/sessions/.../<timestamp>_<uuid>.jsonl`.
274
+ Consumers that parse the path (unlikely) would break, but the field is internal to the extension and only used for display.
275
+ The format of the file content changes from bespoke JSONL to Pi's official format — this is the intentional breaking change.
276
+
277
+ ## Open Questions
278
+
279
+ 1. Should the plan add a `sessionDir` field to `AgentRecord` alongside `outputFile`, or is the session file path sufficient for all use cases?
280
+ Deferred — start with `outputFile` pointing to the session file; add `sessionDir` if needed later.
281
+ 2. Should foreground agents also get persisted sessions, or only background agents?
282
+ The current `output-file` code only runs for background agents.
283
+ Persisted sessions are valuable for both — the plan applies the change in `agent-runner.ts` which serves both paths.
284
+ If this proves too noisy, a follow-up can gate it behind a setting.
@@ -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.
@@ -0,0 +1,33 @@
1
+ ---
2
+ issue: 77
3
+ issue_title: "refactor: add projectAgentsDir to AgentMenuDeps instead of reading process.cwd() inline"
4
+ ---
5
+
6
+ # Retro: #77 — inject projectAgentsDir into AgentMenuDeps
7
+
8
+ ## Final Retrospective (2026-05-20T16:15:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned, implemented, and shipped a refactoring that adds `projectAgentsDir: string` to `AgentMenuDeps`, replacing the inline `process.cwd()` lambda in `createAgentsMenuHandler`.
13
+ The change mirrors the existing `personalAgentsDir` injection pattern.
14
+ Released as `pi-subagents-v5.8.2`.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - Clean end-to-end execution: plan → TDD → ship with zero corrections or rework.
21
+ - The Red test was well-targeted: exercised `findAgentFile` through the menu navigation path and naturally failed because `process.cwd()` produced a different path than the injected `/test-project/.pi/agents`.
22
+ - Correctly identified at execution time that the plan's two-step TDD split was impractical (interface change is atomic) and combined into a single `refactor:` commit.
23
+
24
+ #### What caused friction (agent side)
25
+
26
+ - `instruction-violation` — The plan listed two TDD commits (step 1: `test:`, step 2: `refactor:`) but adding a required field to `AgentMenuDeps` is an atomic change — the test references the new field, so both must land together.
27
+ The testing skill's shared-type-definition rule ("changing that type in step N breaks steps N+1…N+k — fold them into one step") should have been applied during planning.
28
+ Impact: added friction but no rework — the deviation was self-identified at execution time and the steps were combined.
29
+ Self-identified.
30
+
31
+ #### What caused friction (user side)
32
+
33
+ - None observed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "5.8.2",
3
+ "version": "6.0.1",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },
@@ -43,10 +43,13 @@
43
43
  },
44
44
  "devDependencies": {
45
45
  "@biomejs/biome": "^2.4.14",
46
+ "@earendil-works/pi-ai": "0.75.4",
47
+ "@earendil-works/pi-coding-agent": "0.75.4",
48
+ "@earendil-works/pi-tui": "0.75.4",
46
49
  "@types/node": "^22.15.3",
50
+ "rumdl": "^0.1.93",
47
51
  "typescript": "^6.0.3",
48
- "vitest": "^4.1.5",
49
- "rumdl": "^0.1.93"
52
+ "vitest": "^4.1.5"
50
53
  },
51
54
  "pi": {
52
55
  "extensions": [
@@ -74,6 +74,10 @@ export interface SpawnOptions {
74
74
  onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
75
75
  /** Called when the session successfully compacts. */
76
76
  onCompaction?: (info: CompactionInfo) => void;
77
+ /** Path to the parent session's JSONL file (for deriving the subagent session directory). */
78
+ parentSessionFile?: string;
79
+ /** Session ID of the parent agent (stored in the child session's parentSession header). */
80
+ parentSessionId?: string;
77
81
  }
78
82
 
79
83
  export class AgentManager {
@@ -205,6 +209,8 @@ export class AgentManager {
205
209
  inheritContext: options.inheritContext,
206
210
  thinkingLevel: options.thinkingLevel,
207
211
  cwd: worktreeCwd,
212
+ parentSessionFile: options.parentSessionFile,
213
+ parentSessionId: options.parentSessionId,
208
214
  signal: record.abortController!.signal,
209
215
  onToolActivity: (activity) => {
210
216
  if (activity.type === "end") record.toolUses++;
@@ -223,6 +229,10 @@ export class AgentManager {
223
229
  },
224
230
  onSessionCreated: (session) => {
225
231
  record.session = session;
232
+ // Capture the session file path early so it's available for display
233
+ // before the run completes (e.g. in background agent status messages).
234
+ const file = session.sessionManager?.getSessionFile?.();
235
+ if (file) record.outputFile = file;
226
236
  // Flush any steers that arrived before the session was ready
227
237
  if (record.pendingSteers?.length) {
228
238
  for (const msg of record.pendingSteers) {
@@ -233,7 +243,7 @@ export class AgentManager {
233
243
  options.onSessionCreated?.(session);
234
244
  },
235
245
  })
236
- .then(({ responseText, session, aborted, steered }) => {
246
+ .then(({ responseText, session, aborted, steered, sessionFile }) => {
237
247
  // Don't overwrite status if externally stopped via abort()
238
248
  if (record.status !== "stopped") {
239
249
  record.status = aborted ? "aborted" : steered ? "steered" : "completed";
@@ -241,15 +251,10 @@ export class AgentManager {
241
251
  record.result = responseText;
242
252
  record.session = session;
243
253
  record.completedAt ??= Date.now();
254
+ if (sessionFile) record.outputFile = sessionFile;
244
255
 
245
256
  detach();
246
257
 
247
- // Final flush of streaming output file
248
- if (record.outputCleanup) {
249
- try { record.outputCleanup(); } catch (err) { debugLog("outputCleanup", err); }
250
- record.outputCleanup = undefined;
251
- }
252
-
253
258
  // Clean up worktree if used
254
259
  if (record.worktree) {
255
260
  const wtResult = this.worktrees.cleanup(record.worktree, options.description);
@@ -277,11 +282,7 @@ export class AgentManager {
277
282
 
278
283
  detach();
279
284
 
280
- // Final flush of streaming output file on error
281
- if (record.outputCleanup) {
282
- try { record.outputCleanup(); } catch (err) { debugLog("outputCleanup on error", err); }
283
- record.outputCleanup = undefined;
284
- }
285
+
285
286
 
286
287
  // Best-effort worktree cleanup on error
287
288
  if (record.worktree) {