@gotgenes/pi-subagents 5.1.0 → 5.3.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.
@@ -0,0 +1,362 @@
1
+ ---
2
+ issue: 71
3
+ issue_title: "refactor: extract pure agent-session assembler from agent-runner.ts"
4
+ ---
5
+
6
+ # Extract session-config assembler from agent-runner
7
+
8
+ ## Problem Statement
9
+
10
+ `agent-runner.ts` `runAgent()` is ~390 lines (post-#69 cleanup) and mixes three concerns:
11
+
12
+ 1. Configuration assembly — resolve model, detect env, build prompt extras, preload skills, build memory blocks, assemble system prompt, compute tool names (~200 lines).
13
+ 2. Session construction — create `DefaultResourceLoader`, call `createAgentSession`, filter tools, bind extensions (~100 lines).
14
+ 3. Runtime orchestration — subscribe to events, enforce turn limits, collect results (~90 lines).
15
+
16
+ The configuration assembly is deterministic given resolved inputs and does not need an `AgentSession`.
17
+ Because it is inlined in `runAgent()`, it cannot be unit-tested without mocking the entire Pi SDK (`createAgentSession`, `DefaultResourceLoader`, `SessionManager`, `SettingsManager`).
18
+
19
+ ## Goals
20
+
21
+ - Extract a pure `assembleSessionConfig()` function into a new `src/session-config.ts` module.
22
+ - The assembler takes resolved inputs (agent config, environment info, narrow context) and returns a data object with everything `runAgent()` needs to create the session.
23
+ - Reduce `runAgent()` to an IO shell: call the assembler, create SDK objects, wire subscriptions, and run the event loop.
24
+ - Add focused unit tests for the assembler covering model resolution fallback chain, skill preloading, memory block selection (read-write vs read-only), prompt mode, tool name assembly, and disallowed-tool computation.
25
+ - No behavior change.
26
+
27
+ ## Non-Goals
28
+
29
+ - Changing the `RunResult` shape or `RunOptions` interface.
30
+ - Refactoring the event subscription / turn-limit logic (stays in `runAgent()`).
31
+ - Extracting `resumeAgent` or `steerAgent`.
32
+ - Modifying the public API surface (`service.ts`).
33
+
34
+ ## Background
35
+
36
+ ### Prior art
37
+
38
+ `pi-permission-system` extracted `evaluate()` — a pure function of `(surface, pattern, ruleset)` — from `PermissionManager.checkPermission()`.
39
+ That made permission decisions independently testable without filesystem access or a manager instance.
40
+ This plan follows the same pattern: extract a pure core from an IO-heavy function.
41
+
42
+ ### Current `runAgent()` structure
43
+
44
+ Lines 220–460 of `agent-runner.ts` break into these logical phases:
45
+
46
+ | Phase | Lines (approx) | SDK dependency |
47
+ | ------------------------------- | -------------- | -------------------------------------------------------- |
48
+ | Config + agentConfig lookup | 224–225 | None (agent-types registry) |
49
+ | effectiveCwd | 228 | None |
50
+ | detectEnv | 230 | `pi.exec` (async IO) |
51
+ | parentSystemPrompt | 233 | `ctx.getSystemPrompt()` |
52
+ | extensions / skills resolution | 237–245 | None |
53
+ | Skill preloading | 247–252 | `preloadSkills` (filesystem) |
54
+ | Tool names + memory | 254–274 | None (agent-types registry) |
55
+ | System prompt assembly | 277–303 | `buildAgentPrompt` (pure) |
56
+ | noSkills flag | 306 | None |
57
+ | DefaultResourceLoader | 308–320 | `DefaultResourceLoader` (SDK) |
58
+ | Model resolution | 323–324 | `ctx.modelRegistry` (narrow) |
59
+ | Thinking level | 327 | None |
60
+ | sessionOpts construction | 329–345 | `SessionManager`, `SettingsManager`, `getAgentDir` (SDK) |
61
+ | createAgentSession | 347 | SDK |
62
+ | Tool filtering + bindExtensions | 350–400 | `session.*` methods (SDK) |
63
+ | Event subscriptions + prompt | 402–460 | `session.*` methods (SDK) |
64
+
65
+ Everything above the `DefaultResourceLoader` line is configuration assembly — deterministic given resolved inputs.
66
+ Everything from `DefaultResourceLoader` onward is SDK orchestration.
67
+
68
+ ### Modules the assembler will call
69
+
70
+ All are internal to this package — not Pi SDK:
71
+
72
+ - `agent-types.ts` — `getConfig()`, `getAgentConfig()`, `getToolNamesForType()`, `getMemoryToolNames()`, `getReadOnlyMemoryToolNames()`
73
+ - `prompts.ts` — `buildAgentPrompt()`
74
+ - `memory.ts` — `buildMemoryBlock()`, `buildReadOnlyMemoryBlock()`
75
+ - `skill-loader.ts` — `preloadSkills()`
76
+ - `default-agents.ts` — `DEFAULT_AGENTS` (fallback config)
77
+
78
+ ### Relevant constraints from AGENTS.md
79
+
80
+ - Keep modules focused and composable (one concern per file).
81
+ - Keep Pi SDK imports out of business-logic modules.
82
+ - Prefer explicit configuration over hidden behavior.
83
+ - Business logic should be pure functions wherever possible — keep IO at the edges.
84
+
85
+ ### Issue #69 status
86
+
87
+ Issue #69 (`SubagentRuntime`) is implemented.
88
+ Module-scope mutable state has been removed from `agent-runner.ts`.
89
+ `defaultMaxTurns` and `graceTurns` flow through `RunOptions`.
90
+ This plan builds on the post-#69 codebase.
91
+
92
+ ## Design Overview
93
+
94
+ ### Separation of concerns
95
+
96
+ `detectEnv()` is the only async IO call in the assembly phase — it calls `pi.exec()` to check git state.
97
+ The assembler is synchronous and takes `EnvInfo` as a pre-resolved parameter.
98
+ `runAgent()` calls `detectEnv()` first, then calls the assembler, then does SDK work.
99
+
100
+ ### Narrow context interface
101
+
102
+ The assembler does not accept `ExtensionContext` — it accepts a narrow interface with only the fields it reads:
103
+
104
+ ```typescript
105
+ interface AssemblerContext {
106
+ /** Parent working directory (overridable via options.cwd). */
107
+ cwd: string;
108
+ /** Parent's effective system prompt (for append-mode agents). */
109
+ parentSystemPrompt: string;
110
+ /** Parent's current model instance (fallback when agent config has no model). */
111
+ parentModel?: Model<any>;
112
+ /** Model registry for resolving config.model strings. */
113
+ modelRegistry: ModelRegistry;
114
+ }
115
+ ```
116
+
117
+ `ModelRegistry` is a narrow interface (already exists in `model-resolver.ts`):
118
+
119
+ ```typescript
120
+ interface ModelRegistry {
121
+ find(provider: string, modelId: string): Model<any> | undefined;
122
+ getAvailable?(): Model<any>[];
123
+ }
124
+ ```
125
+
126
+ Tests construct plain objects satisfying these interfaces — no SDK mocking needed.
127
+
128
+ ### Assembler signature
129
+
130
+ ```typescript
131
+ function assembleSessionConfig(
132
+ type: SubagentType,
133
+ ctx: AssemblerContext,
134
+ options: AssemblerOptions,
135
+ env: EnvInfo,
136
+ ): SessionConfig;
137
+ ```
138
+
139
+ `AssemblerOptions` is a narrow pick of `RunOptions`:
140
+
141
+ ```typescript
142
+ interface AssemblerOptions {
143
+ cwd?: string;
144
+ isolated?: boolean;
145
+ model?: Model<any>;
146
+ thinkingLevel?: ThinkingLevel;
147
+ }
148
+ ```
149
+
150
+ ### Return type
151
+
152
+ ```typescript
153
+ interface SessionConfig {
154
+ /** Resolved working directory (options.cwd ?? ctx.cwd). */
155
+ effectiveCwd: string;
156
+ /** Fully-assembled system prompt string. */
157
+ systemPrompt: string;
158
+ /** Tool names for session creation and filtering. */
159
+ toolNames: string[];
160
+ /** Disallowed tool set from agent config (for filterActiveTools). */
161
+ disallowedSet: Set<string> | undefined;
162
+ /** Resolved extensions setting (for resource loader and tool filtering). */
163
+ extensions: boolean | string[];
164
+ /** Resolved model instance (or undefined → parent fallback). */
165
+ model: Model<any> | undefined;
166
+ /** Resolved thinking level (or undefined → inherit). */
167
+ thinkingLevel: ThinkingLevel | undefined;
168
+ /** Whether to skip skill loading in the resource loader. */
169
+ noSkills: boolean;
170
+ /** Prompt extras for transparency / debugging. */
171
+ extras: PromptExtras;
172
+ }
173
+ ```
174
+
175
+ ### `resolveDefaultModel` moves to session-config.ts
176
+
177
+ `resolveDefaultModel()` is a pure function that resolves model strings against a registry.
178
+ It belongs in the assembler module alongside the other resolution logic.
179
+ It becomes an internal function (not exported) — its behavior is tested through `assembleSessionConfig()`.
180
+
181
+ ### `filterActiveTools` stays in agent-runner.ts
182
+
183
+ `filterActiveTools()` operates on a live session's active tool list.
184
+ It runs twice (pre- and post-`bindExtensions`) and is an IO-layer concern.
185
+ It stays in `agent-runner.ts` and consumes `toolNames`, `extensions`, and `disallowedSet` from the `SessionConfig` return.
186
+
187
+ ### `normalizeMaxTurns` stays in agent-runner.ts
188
+
189
+ `normalizeMaxTurns()` is used in the turn-limit subscription callback — runtime orchestration, not config assembly.
190
+ It stays in `agent-runner.ts`.
191
+
192
+ ### What runAgent() looks like after
193
+
194
+ ```typescript
195
+ export async function runAgent(
196
+ ctx: ExtensionContext,
197
+ type: SubagentType,
198
+ prompt: string,
199
+ options: RunOptions,
200
+ ): Promise<RunResult> {
201
+ const effectiveCwd = options.cwd ?? ctx.cwd;
202
+ const env = await detectEnv(options.pi, effectiveCwd);
203
+
204
+ const config = assembleSessionConfig(type, {
205
+ cwd: ctx.cwd,
206
+ parentSystemPrompt: ctx.getSystemPrompt(),
207
+ parentModel: ctx.model,
208
+ modelRegistry: ctx.modelRegistry,
209
+ }, {
210
+ cwd: options.cwd,
211
+ isolated: options.isolated,
212
+ model: options.model,
213
+ thinkingLevel: options.thinkingLevel,
214
+ }, env);
215
+
216
+ // SDK orchestration: create loader, session, filter tools, bind, run
217
+ const agentDir = getAgentDir();
218
+ const loader = new DefaultResourceLoader({ ... });
219
+ await loader.reload();
220
+ const { session } = await createAgentSession({ ... });
221
+
222
+ // Tool filtering (two passes), bindExtensions, subscriptions, prompt
223
+ // ...same as today, using config.toolNames, config.disallowedSet, etc.
224
+ }
225
+ ```
226
+
227
+ Target: `runAgent()` drops to ~200 lines (down from ~390).
228
+
229
+ ### Edge cases
230
+
231
+ - Unknown agent type: `getAgentConfig()` returns `undefined`.
232
+ The assembler falls back to `DEFAULT_AGENTS.get("general-purpose")` with `name: type`, matching the current `runAgent()` fallback.
233
+ - Empty `builtinToolNames`: `getToolNamesForType()` already falls back to `BUILTIN_TOOL_NAMES`.
234
+ - `isolated: true` overrides `extensions` and `skills` to `false` — same as today, now inside the assembler.
235
+ - Memory block selection: write-capable agents (have `write` or `edit` in effective tool set, not denied) get read-write memory; others get read-only.
236
+ The denylist check uses `disallowedSet` from the agent config.
237
+
238
+ ## Module-Level Changes
239
+
240
+ ### `src/session-config.ts` (new)
241
+
242
+ - `AssemblerContext` interface — narrow context (cwd, parentSystemPrompt, parentModel, modelRegistry).
243
+ - `AssemblerOptions` interface — narrow options subset (cwd, isolated, model, thinkingLevel).
244
+ - `SessionConfig` interface — return type with all assembled configuration.
245
+ - `assembleSessionConfig()` function — pure configuration assembly.
246
+ - `resolveDefaultModel()` — moved from `agent-runner.ts` (internal, not exported).
247
+
248
+ ### `src/agent-runner.ts` (modified)
249
+
250
+ - Import `assembleSessionConfig` and `SessionConfig` from `./session-config.js`.
251
+ - Remove ~200 lines of configuration assembly from `runAgent()`.
252
+ - Replace with a call to `assembleSessionConfig()` followed by SDK orchestration using the returned `SessionConfig`.
253
+ - Remove `resolveDefaultModel()` (moved to session-config.ts).
254
+ - `filterActiveTools()`, `normalizeMaxTurns()`, `collectResponseText()`, `getLastAssistantText()`, `forwardAbortSignal()` — all stay.
255
+ - `RunOptions`, `RunResult`, `ToolActivity` — all stay (unchanged).
256
+
257
+ ### `test/session-config.test.ts` (new)
258
+
259
+ - Unit tests for `assembleSessionConfig()` covering all assembly logic.
260
+ - Tests use plain objects for `AssemblerContext` — no SDK mocks.
261
+ - Mocks for `agent-types`, `prompts`, `memory`, `skill-loader` — simple function mocks.
262
+
263
+ ### `test/agent-runner.test.ts` (modified)
264
+
265
+ - Existing tests stay as-is — they already mock the SDK and test the full `runAgent()` flow.
266
+ - Tests that verified assembly details (e.g., `suppresses AGENTS.md/CLAUDE.md` or `passes effective cwd to the loader`) remain valid because `runAgent()` still does the SDK orchestration.
267
+ - No tests are removed or rewritten.
268
+
269
+ ### `test/agent-runner-extension-tools.test.ts` (unchanged)
270
+
271
+ - Tests extension-tool filtering via `filterActiveTools` — stays in `agent-runner.ts`.
272
+ - No impact.
273
+
274
+ ## Test Impact Analysis
275
+
276
+ ### New unit tests enabled by the extraction
277
+
278
+ 1. Model resolution fallback chain — test that `assembleSessionConfig` returns the correct model for: explicit option model, config model string (valid/invalid), parent model fallback, and no model.
279
+ 2. Skill preloading — test that `skills: string[]` triggers `preloadSkills` and populates `extras.skillBlocks`; `skills: false` and `skills: true` skip preloading.
280
+ 3. Memory block selection — test read-write vs read-only memory based on tool availability and denylist interaction.
281
+ 4. Tool name assembly — test that `getToolNamesForType` result is augmented with memory tool names when memory is configured.
282
+ 5. Extensions / isolated interaction — test that `isolated: true` forces `extensions: false` and `skills: false`.
283
+ 6. System prompt assembly — test that `buildAgentPrompt` is called with the correct config, extras, and env.
284
+ 7. Disallowed tool set — test construction from `agentConfig.disallowedTools`.
285
+ 8. Unknown type fallback — test that missing `agentConfig` triggers the general-purpose fallback.
286
+ 9. Thinking level resolution — test explicit option vs config vs undefined.
287
+
288
+ ### Existing tests that stay as-is
289
+
290
+ All tests in `test/agent-runner.test.ts`, `test/agent-runner-extension-tools.test.ts`, and `test/agent-runner-settings.test.ts` continue to pass unchanged.
291
+ They test the SDK orchestration layer which is not modified (only reduced in scope).
292
+ The assembly logic they implicitly tested is now covered more thoroughly by `test/session-config.test.ts`.
293
+
294
+ ### Existing tests that could be simplified (future follow-up)
295
+
296
+ Some `agent-runner.test.ts` tests verify assembly-layer behavior through the full `runAgent()` call (e.g., checking `defaultResourceLoaderCtor` args).
297
+ These become redundant with the new assembler tests.
298
+ Simplifying them is a separate follow-up — not part of this issue's scope.
299
+
300
+ ## TDD Order
301
+
302
+ 1. **Red: assembler returns correct defaults for a standard agent type.**
303
+ Create `test/session-config.test.ts` with a test that calls `assembleSessionConfig()` for the `"Explore"` type and asserts the returned `SessionConfig` shape: `effectiveCwd`, `systemPrompt`, `toolNames`, `extensions: false`, `noSkills: true`, `disallowedSet: undefined`.
304
+ Mock `agent-types`, `prompts`, `memory`, `skill-loader` at the module level.
305
+ This fails because `session-config.ts` does not exist yet.
306
+ Commit: `test: add session-config assembler test for default agent type`
307
+
308
+ 2. **Green: implement `assembleSessionConfig()` core path.**
309
+ Create `src/session-config.ts` with `AssemblerContext`, `AssemblerOptions`, `SessionConfig` interfaces and the `assembleSessionConfig()` function.
310
+ Implement the happy path: resolve config, compute effectiveCwd, resolve extensions/skills, build extras, build system prompt, compute toolNames, compute disallowedSet, resolve noSkills.
311
+ Tests go green.
312
+ Commit: `feat: add assembleSessionConfig in session-config.ts`
313
+
314
+ 3. **Red→Green: model resolution fallback chain.**
315
+ Add tests for: explicit option model wins, config model string resolves via registry, invalid config model falls back to parent, no model returns undefined.
316
+ Move `resolveDefaultModel()` from `agent-runner.ts` to `session-config.ts` (internal).
317
+ Commit: `test: model resolution fallback chain in session-config`
318
+
319
+ 4. **Red→Green: skill preloading paths.**
320
+ Add tests for: `skills: string[]` populates `extras.skillBlocks`, `skills: false` skips, `skills: true` skips preloading (loaded by resource loader instead), `isolated: true` forces skip.
321
+ Commit: `test: skill preloading paths in session-config`
322
+
323
+ 5. **Red→Green: memory block selection.**
324
+ Add tests for: agent with memory + write tools → read-write block, agent with memory + read-only tools → read-only block, agent with memory + denied write tools → read-only block, agent without memory → no block.
325
+ Commit: `test: memory block selection in session-config`
326
+
327
+ 6. **Red→Green: isolated mode, unknown type fallback, thinking level.**
328
+ Add tests for: `isolated: true` forces `extensions: false` and `noSkills: true`, unknown type falls back to general-purpose config, thinking level resolves from option > config > undefined.
329
+ Commit: `test: isolated mode, unknown type fallback, thinking level`
330
+
331
+ 7. **Refactor: wire `assembleSessionConfig` into `runAgent()`.**
332
+ Replace the configuration assembly block in `runAgent()` with a call to `assembleSessionConfig()`.
333
+ Use the returned `SessionConfig` fields to construct `DefaultResourceLoader`, `createAgentSession` opts, and `filterActiveTools` args.
334
+ Remove `resolveDefaultModel()` from `agent-runner.ts` (already moved in step 3).
335
+ Run full test suite — all existing `agent-runner.test.ts` tests pass unchanged.
336
+ Commit: `refactor: wire assembleSessionConfig into runAgent (#71)`
337
+
338
+ 8. **Verify acceptance criteria and clean up.**
339
+ Confirm `runAgent()` is ≤200 lines.
340
+ Confirm assembler tests run without mocking `AgentSession`, `ExtensionContext`, or Pi SDK types.
341
+ Confirm full test suite passes with no regressions.
342
+ Remove any dead imports.
343
+ Run `pnpm run check` for type safety.
344
+ Commit: `refactor: finalize session-config extraction (#71)`
345
+
346
+ ## Risks and Mitigations
347
+
348
+ | Risk | Mitigation |
349
+ | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
350
+ | Assembly logic has subtle ordering dependencies (e.g., tool names must be computed before memory block selection) | The assembler mirrors the exact order from `runAgent()` today; tests verify each dependency chain explicitly. |
351
+ | Moving `resolveDefaultModel` changes import paths for any external consumer | `resolveDefaultModel` is not exported from the package — it is internal to `agent-runner.ts` today and internal to `session-config.ts` after the move. No external impact. |
352
+ | Existing `agent-runner.test.ts` tests break when assembly is delegated | The tests mock `agent-types`, `prompts`, `memory`, `skill-loader` — the assembler calls the same functions through the same module paths, so existing mocks continue to intercept. |
353
+ | `Model<any>` import from `@earendil-works/pi-ai` in the new module violates "keep Pi SDK imports out of business-logic modules" | `pi-ai` provides type-only interfaces (`Model`, `ThinkingLevel`) already used in `types.ts`. The constraint targets `pi-coding-agent` SDK types (`AgentSession`, `ExtensionContext`, `DefaultResourceLoader`). The assembler imports zero types from `pi-coding-agent`. |
354
+ | The assembler's return type becomes a wide interface (9 fields) | All fields are consumed by `runAgent()` — none are unused. The interface represents a single cohesive concept (session configuration). No consumer uses a subset; there is no narrowing opportunity. |
355
+
356
+ ## Open Questions
357
+
358
+ - Should `assembleSessionConfig` also resolve `effectiveCwd` internally (trivial: `options.cwd ?? ctx.cwd`) or should the caller pre-compute it?
359
+ The plan assumes the assembler computes it (self-contained), but `runAgent()` also needs `effectiveCwd` for `detectEnv()` before calling the assembler.
360
+ Resolution: `runAgent()` computes `effectiveCwd` once, passes it as `options.cwd` (already resolved) or as a separate parameter.
361
+ The assembler still computes `effectiveCwd` from its inputs, which produces the same value.
362
+ This duplication is benign — both paths yield `options.cwd ?? ctx.cwd`.
@@ -23,15 +23,26 @@ A new issue (#61) was filed to port the output-file format to Pi's official JSON
23
23
 
24
24
  #### What caused friction (agent side)
25
25
 
26
- - `missing-context` — Included `output-file.ts` removal in the initial plan without questioning its debugging value, despite AGENTS.md's rule "Ask before removing functionality or changing defaults." The issue body explicitly listed it for removal so I followed the spec literally. Impact: required plan revision (amend commit), scope-narrowing comment on issue, and filing #61 — roughly 10 minutes of rework, but produced a better design.
26
+ - `missing-context` — Included `output-file.ts` removal in the initial plan without questioning its debugging value, despite AGENTS.md's rule "Ask before removing functionality or changing defaults."
27
+ The issue body explicitly listed it for removal so I followed the spec literally.
28
+ Impact: required plan revision (amend commit), scope-narrowing comment on issue, and filing #61 — roughly 10 minutes of rework, but produced a better design.
27
29
 
28
- - `missing-context` — When asked whether output-file adheres to Pi's session format, searched the web (`web_search` for "Claude Code session JSONL format") instead of checking the local `~/development/pi/pi` monorepo. The user had to explicitly say "~/development/pi/pi has the code for Pi's JSONL format." Impact: one extra round-trip and less authoritative initial answer (Claude Code's format vs Pi's `SessionManager`). Self-identified after user redirect.
30
+ - `missing-context` — When asked whether output-file adheres to Pi's session format, searched the web (`web_search` for "Claude Code session JSONL format") instead of checking the local `~/development/pi/pi` monorepo.
31
+ The user had to explicitly say "~/development/pi/pi has the code for Pi's JSONL format."
32
+ Impact: one extra round-trip and less authoritative initial answer (Claude Code's format vs Pi's `SessionManager`).
33
+ Self-identified after user redirect.
29
34
 
30
- - `instruction-violation` (self-identified) — Shell-escaped the `gh issue comment` body incorrectly; backtick-wrapped `src/output-file.ts` was interpreted by bash. Caught immediately via `gh issue view` and fixed with `--edit-last`. Impact: trivial — one extra command.
35
+ - `instruction-violation` (self-identified) — Shell-escaped the `gh issue comment` body incorrectly; backtick-wrapped `src/output-file.ts` was interpreted by bash.
36
+ Caught immediately via `gh issue view` and fixed with `--edit-last`.
37
+ Impact: trivial — one extra command.
31
38
 
32
39
  #### What caused friction (user side)
33
40
 
34
- - The issue body listed output-file for removal without noting its debugging value. The user's "How confident are we in getting rid of the logging system?" intervention was the correction. If the issue had marked output-file removal as "tentative pending debugging value assessment," the plan would have surfaced it as a design decision from the start. Minor — the discussion was quick and productive.
41
+ - The issue body listed output-file for removal without noting its debugging value.
42
+ The user's "How confident are we in getting rid of the logging system?"
43
+ intervention was the correction.
44
+ If the issue had marked output-file removal as "tentative pending debugging value assessment," the plan would have surfaced it as a design decision from the start.
45
+ Minor — the discussion was quick and productive.
35
46
 
36
47
  ### Changes made
37
48
 
@@ -22,12 +22,16 @@ The change was planned, implemented, shipped, and released as `pi-subagents-v1.0
22
22
 
23
23
  #### What caused friction (agent side)
24
24
 
25
- - No friction observed. The task was unambiguous and the tooling well-suited.
25
+ - No friction observed.
26
+ The task was unambiguous and the tooling well-suited.
26
27
 
27
28
  #### What caused friction (user side)
28
29
 
29
- - No friction observed. The session required no user input beyond invoking the three slash commands.
30
+ - No friction observed.
31
+ The session required no user input beyond invoking the three slash commands.
30
32
 
31
33
  ### Follow-ups identified
32
34
 
33
- - The `package-pi-subagents` skill (`.pi/skills/package-pi-subagents/SKILL.md`) still frames the fork as "a friendly fork… carrying a small number of patches" with priorities like "stays as close to upstream as possible." This framing is now stale given the hard-fork commitment. A separate issue should update the skill to reflect the architecture document's posture.
35
+ - The `package-pi-subagents` skill (`.pi/skills/package-pi-subagents/SKILL.md`) still frames the fork as "a friendly fork… carrying a small number of patches" with priorities like "stays as close to upstream as possible."
36
+ This framing is now stale given the hard-fork commitment.
37
+ A separate issue should update the skill to reflect the architecture document's posture.
@@ -17,14 +17,24 @@ Also fixed a pre-existing `rumdl` glob-quoting bug in `package.json` discovered
17
17
 
18
18
  #### What went well
19
19
 
20
- - Pre-existing lint bug surfaced and fixed: the `rumdl check '*.md' 'docs/**/*.md'` command in `package.json` used single-quoted globs that prevented shell expansion. Verified as pre-existing (reproduced on prior commit via `git stash`), cleanly isolated into its own `fix:` commit. This was a genuine find — the lint had been silently broken.
20
+ - Pre-existing lint bug surfaced and fixed: the `rumdl check '*.md' 'docs/**/*.md'` command in `package.json` used single-quoted globs that prevented shell expansion.
21
+ Verified as pre-existing (reproduced on prior commit via `git stash`), cleanly isolated into its own `fix:` commit.
22
+ This was a genuine find — the lint had been silently broken.
21
23
 
22
24
  #### What caused friction (agent side)
23
25
 
24
- - `missing-context` — In step 6 (refactoring `index.ts`), replaced the `resolveModel` import with `resolveInvocationModel` without first checking whether `resolveModel` was still used elsewhere in the file. Two other call sites (`createSubagentsService` at line 386 and `getModelLabel` at line 1043) still needed it. The plan explicitly listed `getModelLabel` as a non-goal that continues using `resolveModel`, so the information was available. Caught immediately via `grep` after the edit and fixed in the same commit. Impact: one extra edit + grep cycle, no rework.
26
+ - `missing-context` — In step 6 (refactoring `index.ts`), replaced the `resolveModel` import with `resolveInvocationModel` without first checking whether `resolveModel` was still used elsewhere in the file.
27
+ Two other call sites (`createSubagentsService` at line 386 and `getModelLabel` at line 1043) still needed it.
28
+ The plan explicitly listed `getModelLabel` as a non-goal that continues using `resolveModel`, so the information was available.
29
+ Caught immediately via `grep` after the edit and fixed in the same commit.
30
+ Impact: one extra edit + grep cycle, no rework.
25
31
 
26
- - `missing-context` — The plan's type definitions specified `model: unknown` for `ModelResolutionResult`, but downstream code in `index.ts` accesses `.id` and `.name` on the model and passes it where `Model<any>` is expected. The plan's risk section flagged this ("reducing but not eliminating the `any`"), yet the implementation went with `unknown` first, requiring a correction after `pnpm run check` failed with 4 type errors. Changed to `model: any` to match the existing `resolveModel` return type. Impact: one extra edit cycle within the same commit, no rework.
32
+ - `missing-context` — The plan's type definitions specified `model: unknown` for `ModelResolutionResult`, but downstream code in `index.ts` accesses `.id` and `.name` on the model and passes it where `Model<any>` is expected.
33
+ The plan's risk section flagged this ("reducing but not eliminating the `any`"), yet the implementation went with `unknown` first, requiring a correction after `pnpm run check` failed with 4 type errors.
34
+ Changed to `model: any` to match the existing `resolveModel` return type.
35
+ Impact: one extra edit cycle within the same commit, no rework.
27
36
 
28
37
  #### What caused friction (user side)
29
38
 
30
- - None observed. The issue was well-scoped with clear acceptance criteria, making planning and execution straightforward.
39
+ - None observed.
40
+ The issue was well-scoped with clear acceptance criteria, making planning and execution straightforward.
@@ -18,20 +18,35 @@ Filed follow-up #66 (replace `as any` casts with proper SDK types) and #67 (flak
18
18
 
19
19
  #### What went well
20
20
 
21
- - Leaf-first extraction order worked cleanly — helpers, then renderer, then notification, then tools, then menu. Each step left the repo green with no cascading breakage.
21
+ - Leaf-first extraction order worked cleanly — helpers, then renderer, then notification, then tools, then menu.
22
+ Each step left the repo green with no cascading breakage.
22
23
  - The `createNotificationSystem` factory pattern with arrow-closure capture of `widget` (assigned after `AgentManager` construction) preserved the existing deferred-reference semantics without restructuring initialization order.
23
24
 
24
25
  #### What caused friction (agent side)
25
26
 
26
- - `wrong-abstraction` — Applied the code-style skill's "keep Pi SDK imports out of business-logic modules" rule to tool/menu modules, which are SDK consumers, not business logic. Used `unknown` for `ExtensionContext`, `AgentSession`, `ModelRegistry` in factory dep interfaces, requiring 9 `as any` casts in `index.ts`. User caught this post-ship. Impact: filed #66 as a follow-up cleanup; the casts are cosmetic (no runtime effect) but degrade type safety. Fixed the code-style skill to clarify the boundary. (user-caught)
27
+ - `wrong-abstraction` — Applied the code-style skill's "keep Pi SDK imports out of business-logic modules" rule to tool/menu modules, which are SDK consumers, not business logic.
28
+ Used `unknown` for `ExtensionContext`, `AgentSession`, `ModelRegistry` in factory dep interfaces, requiring 9 `as any` casts in `index.ts`.
29
+ User caught this post-ship.
30
+ Impact: filed #66 as a follow-up cleanup; the casts are cosmetic (no runtime effect) but degrade type safety.
31
+ Fixed the code-style skill to clarify the boundary. (user-caught)
27
32
 
28
- - `missing-context` — Four test files (`notification.test.ts`, `get-result-tool.test.ts`, `steer-tool.test.ts`, `agent-tool.test.ts`) omitted `compactionCount: 0` from `AgentRecord` factories. Caught at the final `pnpm run check` step, not during test writing. The testing skill already says "grep for ALL test files that construct a compatible mock." Impact: one extra fix cycle delegated to a subagent, no rework beyond that step. (self-identified)
33
+ - `missing-context` — Four test files (`notification.test.ts`, `get-result-tool.test.ts`, `steer-tool.test.ts`, `agent-tool.test.ts`) omitted `compactionCount: 0` from `AgentRecord` factories.
34
+ Caught at the final `pnpm run check` step, not during test writing.
35
+ The testing skill already says "grep for ALL test files that construct a compatible mock."
36
+ Impact: one extra fix cycle delegated to a subagent, no rework beyond that step. (self-identified)
29
37
 
30
- - `other` — `Edit` tool failed 3 times matching the UTF-8 middle dot (`·`, U+00B7) in the steer tool's `stateParts.join(" · ")` line. The third attempt produced a partial match that left the file in a broken state (dangling orphan code after the replacement anchor). Required `git restore` and a fallback to `python3` line-range replacement. The same `python3` approach for the menu extraction lost the closing `}` of the default export function. Impact: ~5 minutes of rework across the two extraction steps, plus one `git restore`.
38
+ - `other` — `Edit` tool failed 3 times matching the UTF-8 middle dot (`·`, U+00B7) in the steer tool's `stateParts.join(" · ")` line.
39
+ The third attempt produced a partial match that left the file in a broken state (dangling orphan code after the replacement anchor).
40
+ Required `git restore` and a fallback to `python3` line-range replacement.
41
+ The same `python3` approach for the menu extraction lost the closing `}` of the default export function.
42
+ Impact: ~5 minutes of rework across the two extraction steps, plus one `git restore`.
31
43
 
32
44
  #### What caused friction (user side)
33
45
 
34
- - The `as any` casts could have been caught earlier if the user had flagged the `unknown` types during the planning phase. However, the plan didn't prescribe exact interface types — that was an implementation decision. The user's post-ship review ("Why did we have to cast `as any`? Take a look at `packages/pi-permission-system/` as a model") was an efficient redirect that immediately scoped the investigation.
46
+ - The `as any` casts could have been caught earlier if the user had flagged the `unknown` types during the planning phase.
47
+ However, the plan didn't prescribe exact interface types — that was an implementation decision.
48
+ The user's post-ship review ("Why did we have to cast `as any`?
49
+ Take a look at `packages/pi-permission-system/` as a model") was an efficient redirect that immediately scoped the investigation.
35
50
 
36
51
  ### Changes made
37
52
 
@@ -0,0 +1,77 @@
1
+ ---
2
+ issue: 57
3
+ issue_title: "feat: structured debug logging for silenced catch blocks"
4
+ ---
5
+
6
+ # Retro: #57 — structured debug logging for silenced catch blocks
7
+
8
+ ## Final Retrospective (2026-05-19T10:30:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Added `src/debug.ts` with `debugLog` and `isDebug()`, then threaded `debugLog` into ~20 silent `catch` blocks across 9 files.
13
+ All 7 TDD cycles went green on the first pass with no rework.
14
+ Shipped as `pi-subagents-v5.1.0`, then followed up with a `refactor:` commit converting `DEBUG` (module-level constant) to `isDebug()` (function getter) during the retro.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - The plan's "Non-Goals" section correctly excluded `usage.ts` and `settings.ts` before implementation started, and a post-TDD `grep -rn 'catch\s*{'` confirmed only those two in-scope-excluded files remained.
21
+ Closing the loop with a verification query is worth repeating.
22
+ - The scope of the change was so well-defined (the issue listed exact file names) that no `ask_user` call was needed during planning.
23
+
24
+ #### What caused friction (agent side)
25
+
26
+ - `missing-context` — When loading the `ask-user` skill I guessed `.pi/skills/ask-user/SKILL.md` before reading the actual `<location>` tag in `AGENTS.md`, triggering an ENOENT error and a follow-up `find` call.
27
+ Impact: 2 extra tool calls, no rework. (self-identified)
28
+
29
+ - `other` — The plan's TDD Order step 1 stated *"the test skill documents this pattern"* for `vi.resetModules()` + dynamic import when testing module-level env constants — but the testing skill does not have that entry.
30
+ The aspiration was recorded rather than verified.
31
+ During the retro, the user's question ("should that be a function getter instead?") led to a better outcome: replace the module-level constant with `isDebug()` so `vi.stubEnv()` alone works, consistent with how every other `process.env` read in this codebase is structured.
32
+ Impact: one retro-phase `refactor:` commit; the approach shipped in `v5.1.0` was technically correct but unnecessarily complex to test.
33
+
34
+ #### What caused friction (user side)
35
+
36
+ - The initial issue proposal chose the module-level-constant pattern (common in Node.js tooling like the `debug` package).
37
+ A note in the issue or plan about preferring function-based env reads for testability would have caught this at design time rather than post-ship.
38
+ That said, the retro question was efficient — a single targeted redirect resolved it cleanly.
39
+
40
+ ### Changes made
41
+
42
+ 1. `packages/pi-subagents/src/debug.ts` — replaced `export const DEBUG` with `export function isDebug()`.
43
+ 2. `packages/pi-subagents/test/debug.test.ts` — simplified to static import + `vi.stubEnv()` only; removed all `vi.resetModules()` + dynamic `import()` calls.
44
+ 3. `.pi/skills/testing/SKILL.md` — added bullet: prefer reading `process.env` inside functions; `vi.stubEnv()` alone is insufficient for module-level constants.
45
+
46
+ ## Follow-up Retrospective (2026-05-19T11:15:00Z)
47
+
48
+ ### Session summary
49
+
50
+ The user asked how many `process.*` reads exist in `pi-subagents`.
51
+ Audit found 9 sites: 4 acceptable (wiring layer, detection functions, injectable defaults), 2 genuine injection gaps, and 1 mild case.
52
+ Filed #76 (`AgentManager.dispose()` reads `process.cwd()` without a stored `cwd`) and #77 (`createAgentsMenuHandler` hardcodes `process.cwd()` when `AgentMenuDeps` already injects the personal-side equivalent).
53
+
54
+ ### Observations
55
+
56
+ #### What went well
57
+
58
+ - The `isDebug()` refactor naturally led the user to ask a broader design question about `process.*` access patterns, producing two well-scoped follow-up issues without manual triage.
59
+ - The audit categorization (genuinely problematic vs. acceptable) was clean — presenting a table with verdicts per site let the user decide scope without re-reading source.
60
+
61
+ #### What caused friction (agent side)
62
+
63
+ - `premature-convergence` — The original plan accepted the module-level `DEBUG` constant without checking how the rest of the codebase reads `process.env`.
64
+ The code-style skill said "keep IO at the edges" but didn't name `process.*` specifically, so the rule wasn't applied.
65
+ Impact: one post-ship `refactor:` commit to replace `DEBUG` with `isDebug()`; the pattern was technically correct but inconsistent with codebase conventions. (user-caught)
66
+
67
+ #### What caused friction (user side)
68
+
69
+ - Nothing notable.
70
+ The user's two redirecting questions ("should that be a function?"
71
+ and "how many places access `process.*`?") were well-timed interventions that broadened scope productively.
72
+
73
+ ### Changes made
74
+
75
+ 1. `.pi/skills/code-style/SKILL.md` — added bullet: do not read `process.env`, `process.cwd()`, or `process.platform` inside library/utility functions; accept the value as a parameter.
76
+ 2. Filed #76 — inject `cwd` into `AgentManager` constructor.
77
+ 3. Filed #77 — add `projectAgentsDir` to `AgentMenuDeps`.
@@ -0,0 +1,43 @@
1
+ ---
2
+ issue: 69
3
+ issue_title: "refactor: eliminate module-scope mutable state in pi-subagents — create SubagentRuntime"
4
+ ---
5
+
6
+ # Retro: #69 — create SubagentRuntime
7
+
8
+ ## Final Retrospective (2026-05-19T16:47:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned, implemented, and shipped `SubagentRuntime` — a composition-root object that replaces module-scope mutable state in `agent-runner.ts` and closure-scoped state in `index.ts`.
13
+ Six TDD steps completed with one deviation: `agent-tool.ts` and `agent-menu.ts` also imported the removed getter/setter exports, requiring unplanned fixes.
14
+ Released as `pi-subagents-v5.2.0`.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - The lift-and-shift strategy (introduce `RunOptions` fields alongside module-scope fallback, wire consumers, then remove old path) kept the 460-test suite green through every intermediate commit.
21
+ No step broke existing tests.
22
+ - `pnpm run check` caught the two missing downstream files (`agent-tool.ts`, `agent-menu.ts`) immediately after the removal step.
23
+ The typecheck-after-removal safety net worked exactly as intended.
24
+ - The `pi-permission-system` prior art (`ExtensionRuntime` in #43) provided a clear structural template, reducing design decisions to near zero.
25
+
26
+ #### What caused friction (agent side)
27
+
28
+ - `missing-context` — The plan's Module-Level Changes listed `agent-runner.ts`, `agent-manager.ts`, and `index.ts` but missed `src/tools/agent-tool.ts` and `src/ui/agent-menu.ts`, both of which imported `getDefaultMaxTurns`/`setDefaultMaxTurns`/`getGraceTurns`/`setGraceTurns` from `agent-runner.ts`.
29
+ A grep for all importers of the removed symbols during planning would have caught this.
30
+ Impact: 4 extra files touched in step 5 (the two source files + their test helpers); no rework of earlier steps, but the commit scope was wider than planned. (self-identified at `pnpm run check` time)
31
+
32
+ - `missing-context` — In step 3 (`agent-manager.test.ts`), checked `vi.mocked(runAgent).mock.calls[0]` without clearing the mock first.
33
+ The module-level `vi.mock("../src/agent-runner.js")` is shared across all describe blocks, so `calls[0]` picked up a stale invocation from an earlier test.
34
+ Impact: one debug cycle adding `vi.mocked(runAgent).mockClear()` after `resolvedRun()`. (self-identified)
35
+
36
+ #### What caused friction (user side)
37
+
38
+ - Nothing notable.
39
+ The plan was unambiguous, and the session ran without user intervention beyond the initial prompts.
40
+
41
+ ### Changes made
42
+
43
+ 1. `.pi/prompts/plan-issue.md` — added grep-importers rule to the Module-Level Changes bullet: when a step removes or renames an export, grep all `src/` and `test/` files for every removed symbol before finalizing the file list.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "5.1.0",
3
+ "version": "5.3.0",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },