@gotgenes/pi-subagents 7.7.0 → 7.8.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,32 @@ 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
+ ## [7.8.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.8.0...pi-subagents-v7.8.1) (2026-05-26)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * plan reduce test duplication — top 3 clone families ([#219](https://github.com/gotgenes/pi-packages/issues/219)) ([c941b1b](https://github.com/gotgenes/pi-packages/commit/c941b1b3c047f6895eb57da3291b75082a2b99a3))
14
+ * **retro:** add planning stage notes for issue [#219](https://github.com/gotgenes/pi-packages/issues/219) ([5122f7c](https://github.com/gotgenes/pi-packages/commit/5122f7cd666873abbbb6b6880fffb1e751beb9b5))
15
+ * **retro:** add retro notes for issue [#218](https://github.com/gotgenes/pi-packages/issues/218) ([ef9187b](https://github.com/gotgenes/pi-packages/commit/ef9187ba8521d10212bd992cbfcf3d853886938b))
16
+ * **retro:** add TDD stage notes for issue [#219](https://github.com/gotgenes/pi-packages/issues/219) ([975f94e](https://github.com/gotgenes/pi-packages/commit/975f94e5e868310765029050490098b335a67e1e))
17
+
18
+ ## [7.8.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.7.0...pi-subagents-v7.8.0) (2026-05-26)
19
+
20
+
21
+ ### Features
22
+
23
+ * inject agentDir into SettingsManager and loadSettings to remove SDK dependency ([7dcb986](https://github.com/gotgenes/pi-packages/commit/7dcb9868c8ac52c86a3eac0b6fc6648c8d57fc7c))
24
+ * wire agentDir from SDK boundary in index.ts ([#218](https://github.com/gotgenes/pi-packages/issues/218)) ([17e9fc5](https://github.com/gotgenes/pi-packages/commit/17e9fc5f7880ae92168a6bb30e6fbc82748b7b2a))
25
+
26
+
27
+ ### Documentation
28
+
29
+ * plan push SDK boundary in settings.ts ([#218](https://github.com/gotgenes/pi-packages/issues/218)) ([19f7cd6](https://github.com/gotgenes/pi-packages/commit/19f7cd6ddfa28290f7e61e6273d966c946868cf6))
30
+ * **retro:** add planning stage notes for issue [#218](https://github.com/gotgenes/pi-packages/issues/218) ([80be50e](https://github.com/gotgenes/pi-packages/commit/80be50e1b6ddf19f743010bd4c3cdf232d901cf1))
31
+ * **retro:** add retro notes for issue [#217](https://github.com/gotgenes/pi-packages/issues/217) ([2140655](https://github.com/gotgenes/pi-packages/commit/21406555e34fbe0d41f48206e3208e1cb7326633))
32
+ * **retro:** add TDD stage notes for issue [#218](https://github.com/gotgenes/pi-packages/issues/218) ([86b4f94](https://github.com/gotgenes/pi-packages/commit/86b4f946d7498e96dbb2b4c513d0ea6331fc5f8c))
33
+
8
34
  ## [7.7.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.6.0...pi-subagents-v7.7.0) (2026-05-26)
9
35
 
10
36
 
@@ -584,7 +584,10 @@ The IO boundary was split into two focused interfaces:
584
584
  export interface EnvironmentIO {
585
585
  detectEnv: (exec: ShellExec, cwd: string) => Promise<EnvInfo>;
586
586
  getAgentDir: () => string;
587
- deriveSessionDir: (parentSessionFile: string | undefined, effectiveCwd: string) => string;
587
+ deriveSessionDir: (
588
+ parentSessionFile: string | undefined,
589
+ effectiveCwd: string,
590
+ ) => string;
588
591
  }
589
592
 
590
593
  /** Session factory — create SDK objects for a child agent session. */
@@ -592,7 +595,9 @@ export interface SessionFactoryIO {
592
595
  createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoaderLike;
593
596
  createSessionManager: (cwd: string, sessionDir: string) => SessionManagerLike;
594
597
  createSettingsManager: (cwd: string, agentDir: string) => SettingsManager;
595
- createSession: (opts: CreateSessionOptions) => Promise<{ session: AgentSession }>;
598
+ createSession: (
599
+ opts: CreateSessionOptions,
600
+ ) => Promise<{ session: AgentSession }>;
596
601
  assemblerIO: AssemblerIO;
597
602
  }
598
603
 
@@ -643,7 +648,7 @@ Three closure factories converted to classes in [#214].
643
648
  - Smell: C (coupling — deps hidden in closure scope instead of explicit on class)
644
649
  - Outcome: 0 remaining closure factories (excluding pure-function factories), deps visible as constructor parameters
645
650
 
646
- ### Step 2: Decompose `buildParentContext` (cognitive 30) — [#215]
651
+ ### Step 2: Decompose `buildParentContext` (cognitive 30) — [#215]
647
652
 
648
653
  `buildParentContext` in `session/context.ts` is the only remaining fallow refactoring target.
649
654
  The function loops over branch entries with 3 type-check branches, each with sub-branches for role or summary.
@@ -671,7 +676,7 @@ Extracted:
671
676
  - Smell: B (oversized method) + A (duplicated finalization logic in then/catch)
672
677
  - Outcome: `startAgent` reduced to ~40 LOC coordinator with zero mutable `let` bindings; `.then()`/`.catch()` are one-liners
673
678
 
674
- ### Step 4: Extract overwrite guard from UI — [#217]
679
+ ### Step 4: Extract overwrite guard from UI — [#217]
675
680
 
676
681
  The 20-line pattern duplicated between `agent-config-editor.ts:138–151` and `agent-creation-wizard.ts:231–250` checks file existence, prompts for confirmation, writes the file, reloads the registry, and notifies the user.
677
682
  Extract a shared `writeAgentFile(fileOps, ui, registry, targetPath, content, label)` function.
@@ -680,7 +685,7 @@ Extract a shared `writeAgentFile(fileOps, ui, registry, targetPath, content, lab
680
685
  - Smell: A (production duplication)
681
686
  - Outcome: 0 production clone groups
682
687
 
683
- ### Step 5: Push SDK boundary in `settings.ts` — [#218]
688
+ ### Step 5: Push SDK boundary in `settings.ts` — [#218]
684
689
 
685
690
  `globalPath()` calls `getAgentDir()` (a Pi SDK function) at invocation time.
686
691
  This hides a platform dependency inside a module that is otherwise pure configuration logic.
@@ -890,7 +895,6 @@ The upstream test suite is run periodically as a regression canary for the agent
890
895
  [earendil-works/pi#4207]: https://github.com/earendil-works/pi/issues/4207
891
896
  [gotgenes/pi-packages]: https://github.com/gotgenes/pi-packages
892
897
  [tintinweb/pi-subagents]: https://github.com/tintinweb/pi-subagents
893
-
894
898
  [166]: https://github.com/gotgenes/pi-packages/issues/166
895
899
  [167]: https://github.com/gotgenes/pi-packages/issues/167
896
900
  [168]: https://github.com/gotgenes/pi-packages/issues/168
@@ -0,0 +1,172 @@
1
+ ---
2
+ issue: 218
3
+ issue_title: "Push SDK boundary in settings.ts (Phase 13, Step 5)"
4
+ ---
5
+
6
+ # Push SDK boundary in settings.ts
7
+
8
+ ## Problem Statement
9
+
10
+ `settings.ts` imports `getAgentDir` from the Pi SDK (`@earendil-works/pi-coding-agent`) and calls it inside `globalPath()` at invocation time.
11
+ This hides a platform dependency inside a module that is otherwise pure configuration logic — violating the project's SDK-boundary rule that pure helpers and domain modules should remain SDK-independent.
12
+ The SDK call also forces tests to redirect the env var `PI_CODING_AGENT_DIR` to control `getAgentDir()` output, rather than passing the value directly.
13
+
14
+ ## Goals
15
+
16
+ - Remove the `getAgentDir` import from `settings.ts` (0 Pi SDK imports).
17
+ - Inject `agentDir: string` into `SettingsManager` constructor deps.
18
+ - Make `loadSettings` accept `agentDir` as an explicit parameter.
19
+ - Eliminate `PI_CODING_AGENT_DIR` env var manipulation from all settings tests.
20
+
21
+ ## Non-Goals
22
+
23
+ - Removing `process.cwd()` defaults from `loadSettings`/`saveSettings` — that's a separate concern (Node.js API, not Pi SDK).
24
+ - Pushing SDK boundaries in other files (`skill-loader.ts`, `custom-agents.ts`, `agent-runner.ts`) — tracked separately in the Phase 13 roadmap.
25
+ - Changing the `saveSettings` signature — it only calls `projectPath(cwd)` and has no SDK dependency.
26
+
27
+ ## Background
28
+
29
+ `settings.ts` exports a `SettingsManager` class and two free functions (`loadSettings`, `saveSettings`).
30
+ The only SDK import is `getAgentDir`, used in the private `globalPath()` helper to compute the global settings file path (`~/.pi/agent/subagents.json`).
31
+
32
+ The `SettingsManager` constructor already accepts a deps bag `{ emit, cwd, onMaxConcurrentChanged? }`.
33
+ Adding `agentDir` to this bag follows the established injection pattern.
34
+
35
+ In `index.ts`, `getAgentDir` is already imported for several other call sites (agent runner IO, agent tool, `/agents` menu).
36
+ Adding one more usage to the `SettingsManager` construction is zero new imports.
37
+
38
+ ### Relevant AGENTS.md constraints
39
+
40
+ - **Pi SDK boundaries:** Keep Pi SDK imports out of business-logic modules; accept the value as a parameter or callback.
41
+ - **Code-design skill, DIP:** Default to dependency injection for non-trivial dependencies.
42
+
43
+ ## Design Overview
44
+
45
+ ### Change to `globalPath()`
46
+
47
+ Currently:
48
+
49
+ ```typescript
50
+ function globalPath(): string {
51
+ return join(getAgentDir(), "subagents.json");
52
+ }
53
+ ```
54
+
55
+ After:
56
+
57
+ ```typescript
58
+ function globalPath(agentDir: string): string {
59
+ return join(agentDir, "subagents.json");
60
+ }
61
+ ```
62
+
63
+ ### Change to `loadSettings()`
64
+
65
+ Currently:
66
+
67
+ ```typescript
68
+ export function loadSettings(cwd: string = process.cwd()): SubagentsSettings {
69
+ return { ...readSettingsFile(globalPath()), ...readSettingsFile(projectPath(cwd)) };
70
+ }
71
+ ```
72
+
73
+ After:
74
+
75
+ ```typescript
76
+ export function loadSettings(agentDir: string, cwd: string = process.cwd()): SubagentsSettings {
77
+ return { ...readSettingsFile(globalPath(agentDir)), ...readSettingsFile(projectPath(cwd)) };
78
+ }
79
+ ```
80
+
81
+ ### Change to `SettingsManager` constructor
82
+
83
+ Add `agentDir: string` to the deps bag:
84
+
85
+ ```typescript
86
+ constructor(deps: { emit: SettingsEmit; cwd: string; agentDir: string; onMaxConcurrentChanged?: () => void }) {
87
+ this.emit = deps.emit;
88
+ this.cwd = deps.cwd;
89
+ this.agentDir = deps.agentDir;
90
+ this.onMaxConcurrentChanged = deps.onMaxConcurrentChanged;
91
+ }
92
+ ```
93
+
94
+ `SettingsManager.load()` passes `this.agentDir` to `loadSettings`:
95
+
96
+ ```typescript
97
+ load(): SubagentsSettings {
98
+ const settings = loadSettings(this.agentDir, this.cwd);
99
+ // ... rest unchanged
100
+ }
101
+ ```
102
+
103
+ ### Wiring in `index.ts`
104
+
105
+ ```typescript
106
+ const settings = new SettingsManager({
107
+ emit: (event, payload) => pi.events.emit(event, payload),
108
+ cwd: process.cwd(),
109
+ agentDir: getAgentDir(),
110
+ onMaxConcurrentChanged: () => manager.notifyConcurrencyChanged(),
111
+ });
112
+ ```
113
+
114
+ ## Module-Level Changes
115
+
116
+ ### `src/settings.ts`
117
+
118
+ 1. Remove `import { getAgentDir } from "@earendil-works/pi-coding-agent"`.
119
+ 2. Add `agentDir: string` field to the `SettingsManager` constructor deps interface.
120
+ 3. Store `this.agentDir = deps.agentDir` as a private readonly field.
121
+ 4. Change `globalPath()` signature to `globalPath(agentDir: string)`.
122
+ 5. Change `loadSettings` signature to `loadSettings(agentDir: string, cwd?: string)`.
123
+ 6. Update `SettingsManager.load()` to call `loadSettings(this.agentDir, this.cwd)`.
124
+ 7. Update the header comment to reflect the new injection pattern.
125
+
126
+ ### `src/index.ts`
127
+
128
+ 1. Add `agentDir: getAgentDir()` to the `SettingsManager` constructor deps object.
129
+
130
+ ### `test/settings.test.ts`
131
+
132
+ 1. Remove all `PI_CODING_AGENT_DIR` env manipulation (`originalAgentDirEnv`, `beforeEach`/`afterEach` stubs).
133
+ 2. Pass `globalDir` directly to `loadSettings(globalDir, projectDir)` in free-function tests.
134
+ 3. Add `agentDir: globalDir` (or a dummy string for non-load tests) to all `new SettingsManager(...)` calls.
135
+ 4. In `SettingsManager.load()` tests, pass `agentDir: globalDir` to the constructor.
136
+ 5. In tests that don't exercise `load()`, use `agentDir: "/nonexistent"` or similar — the value is unused.
137
+
138
+ ## Test Impact Analysis
139
+
140
+ 1. **New capability:** Free-function tests (`loadSettings`, `saveSettings`) become pure — pass `globalDir` directly instead of manipulating `PI_CODING_AGENT_DIR`.
141
+ This is simpler and more reliable.
142
+ 2. **Redundant cleanup:** The `originalAgentDirEnv` save/restore pattern in `beforeEach`/`afterEach` can be removed from all `describe` blocks that use it.
143
+ 3. **Existing tests stay:** All existing test scenarios remain valid; only their setup mechanics change.
144
+
145
+ ## TDD Order
146
+
147
+ 1. **Red → Green:** Change `loadSettings` signature to accept `agentDir` parameter; update `globalPath` to accept it.
148
+ Update all free-function tests to pass `globalDir` directly instead of env var.
149
+ Remove env-var manipulation from the `settings persistence` describe block.
150
+ Commit: `feat: inject agentDir into loadSettings to remove SDK dependency`
151
+
152
+ 2. **Red → Green:** Add `agentDir` to `SettingsManager` constructor deps; store as private field; thread into `load()`.
153
+ Update all `new SettingsManager(...)` call sites in tests.
154
+ Remove env-var manipulation from `SettingsManager` describe blocks.
155
+ Remove `getAgentDir` import from `settings.ts`.
156
+ Commit: `feat: inject agentDir into SettingsManager constructor`
157
+
158
+ 3. **Green:** Wire `agentDir: getAgentDir()` into `SettingsManager` construction in `index.ts`.
159
+ Run `pnpm run check` to confirm no type errors across the package.
160
+ Commit: `feat: wire agentDir from SDK boundary in index.ts (#218)`
161
+
162
+ ## Risks and Mitigations
163
+
164
+ | Risk | Mitigation |
165
+ | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
166
+ | Breaking the `loadSettings` export signature for hypothetical external callers | The function is only called from `SettingsManager.load()` and tests; no external consumers exist. |
167
+ | Test count is high (~35 constructor sites) — mechanical updates could introduce typos | Steps 1 and 2 are focused; run `pnpm vitest run test/settings.test.ts` after each to confirm. |
168
+ | Forgetting a test site that still manipulates `PI_CODING_AGENT_DIR` | Grep for `PI_CODING_AGENT_DIR` after step 2 to confirm zero remaining references in `test/settings.test.ts`. |
169
+
170
+ ## Open Questions
171
+
172
+ None — the issue's proposed change is unambiguous and follows the established injection pattern used in prior Phase 13 steps.
@@ -0,0 +1,162 @@
1
+ ---
2
+ issue: 219
3
+ issue_title: "Reduce test duplication — top 3 clone families (Phase 13, Step 6)"
4
+ ---
5
+
6
+ # Reduce test duplication — top 3 clone families
7
+
8
+ ## Problem statement
9
+
10
+ After Phase 12, three test files carry the heaviest remaining clone families in pi-subagents:
11
+
12
+ 1. `test/lifecycle/agent-manager.test.ts` (929 lines) — 16 clone groups, ~160 duplicated lines.
13
+ Repeated inline runner stubs, worktree stubs, and manager-lifecycle boilerplate.
14
+ 2. `test/conversation-viewer.test.ts` (307 lines) — 8 clone groups, ~91 duplicated lines.
15
+ Near-identical `ConversationViewer` construction in every test, plus repeated width-loop assertion patterns.
16
+ 3. `test/ui/agent-config-editor.test.ts` (471 lines) — 5 clone groups, ~42 duplicated lines.
17
+ Repeated `makeEditor()` + `makeMenuUI()` + `fileOps.findAgentFile.mockReturnValue(...)` setup.
18
+
19
+ Total target: reduce test duplication by ~200 lines (from ~1,046 combined test-setup lines to < 850).
20
+
21
+ ## Goals
22
+
23
+ - Extract shared setup and assertion helpers for the three target test files.
24
+ - Reduce test duplication by ~200 lines without changing test semantics.
25
+ - Follow the existing `test/helpers/` convention (factory + matching `.test.ts` file).
26
+
27
+ ## Non-goals
28
+
29
+ - No production code changes.
30
+ - No new test coverage — this is purely a refactoring of existing test infrastructure.
31
+ - Not consolidating clone families in other test files beyond the top 3.
32
+ - Not changing any assertion logic or test structure beyond replacing inline stubs with factory calls.
33
+
34
+ ## Background
35
+
36
+ The project already has several shared test helpers in `test/helpers/`: `make-record.ts`, `mock-session.ts`, `ui-stubs.ts`, `runner-io.ts`, `stub-ctx.ts`, `make-deps.ts`.
37
+ Each helper has a companion `.test.ts` file — this convention must be followed.
38
+
39
+ Dependencies #214 (closure-to-class conversions) and #216 (startAgent decomposition) are both closed, so the production code these tests cover is stable.
40
+
41
+ ## Design overview
42
+
43
+ ### File 1: `agent-manager.test.ts` — extract to `test/helpers/manager-stubs.ts`
44
+
45
+ Five clone families to extract:
46
+
47
+ 1. **Never-resolving runner** — `{ run: vi.fn().mockImplementation(() => new Promise(() => {})), resume: vi.fn() }` appears 5 times.
48
+ Extract as `createBlockingRunner(): AgentRunner`.
49
+
50
+ 2. **Session-creating runner** — runner that calls `opts.onSessionCreated?.(session)` and resolves.
51
+ Appears 5+ times with minor variations (some emit events through the session, some don't).
52
+ Extract as `createSessionRunner(session?: MockSession): AgentRunner` that calls `onSessionCreated` and returns a standard result.
53
+
54
+ 3. **Worktree stubs with path+branch** — `{ create: vi.fn().mockReturnValue({ path, branch }), cleanup: vi.fn(() => ({ hasChanges: false })), prune: vi.fn() }` appears 4 times identically, plus 1 variant with `create` returning `undefined`.
55
+ Extract as `createMockWorktrees(overrides?)`.
56
+
57
+ 4. **Standard run result shape** — `{ responseText: "done", session, aborted: false, steered: false }` is repeated in many runner factories.
58
+ Extract as `createRunResult(overrides?)`.
59
+
60
+ 5. **Gated runner** — uses `Promise.withResolvers` to control when the runner completes.
61
+ Appears 2 times.
62
+ Keep inline — too tightly coupled to individual test flow-control to generalize cleanly.
63
+
64
+ Tests that construct custom runners with unique behavior (event-emitting runners in the `lifetimeUsage` and `compactionCount` tests) keep their inline stubs — those encode test-specific emission sequences that a shared factory would obscure.
65
+
66
+ ### File 2: `conversation-viewer.test.ts` — inline factory + assertion helper
67
+
68
+ Two clone families to extract:
69
+
70
+ 1. **`ConversationViewer` construction** — 15 near-identical constructor calls with the same 8 fields.
71
+ Extract as an inline `createTestViewer(overrides?)` factory at the top of the test file.
72
+ The factory provides defaults for `tui`, `session`, `record`, `activity`, `theme`, `done`, `registry`, and `wrapText`, and accepts overrides including a convenience `width` and `messages` parameter.
73
+
74
+ 2. **Width-loop assertion** — the `for (const w of widths) { create viewer; assertAllLinesFit(viewer.render(w), w) }` pattern repeats in 10 "render width safety" tests.
75
+ Extract as an inline `assertRenderFitsWidths(messages, widths?, viewerOverrides?)` helper.
76
+
77
+ These helpers stay inline (not in `test/helpers/`) because they depend on file-local helpers (`mockTui`, `mockSession`, `ansiTheme`) and are only used by this one test file.
78
+
79
+ ### File 3: `agent-config-editor.test.ts` — inline setup helper
80
+
81
+ One clone family to extract:
82
+
83
+ 1. **Detail-test setup** — `makeEditor()` + `makeMenuUI([...])` + `fileOps.findAgentFile.mockReturnValue(...)` + optional `fileOps.read.mockReturnValue(...)` appears in ~18 tests.
84
+ Extract as an inline `setupDetail(selectResults, options?)` factory that returns `{ fileOps, editor, ui }` with pre-configured mocks.
85
+ Options: `filePath`, `fileContent`, `config` (merged into default via `createTestAgentConfig`).
86
+
87
+ This stays inline because it's specific to the `showAgentDetail` test suite and depends on file-local `testRegistry` setup.
88
+
89
+ ## Module-level changes
90
+
91
+ ### New files
92
+
93
+ | File | Purpose |
94
+ | ------------------------------------ | --------------------------------------------------------------------------------------- |
95
+ | `test/helpers/manager-stubs.ts` | `createBlockingRunner`, `createSessionRunner`, `createMockWorktrees`, `createRunResult` |
96
+ | `test/helpers/manager-stubs.test.ts` | Smoke tests for the factories |
97
+
98
+ ### Modified files
99
+
100
+ | File | Change |
101
+ | -------------------------------------- | --------------------------------------------------------------------------- |
102
+ | `test/lifecycle/agent-manager.test.ts` | Replace inline runner/worktree stubs with `manager-stubs` factories |
103
+ | `test/conversation-viewer.test.ts` | Add `createTestViewer` + `assertRenderFitsWidths` inline, migrate all tests |
104
+ | `test/ui/agent-config-editor.test.ts` | Add `setupDetail` inline, migrate `showAgentDetail` tests |
105
+
106
+ ### Unchanged files
107
+
108
+ No production source files are modified.
109
+ No other test files are modified.
110
+
111
+ ## Test impact analysis
112
+
113
+ 1. **New unit tests**: `manager-stubs.test.ts` adds smoke tests verifying factory return shapes (blocking runner never resolves, session runner calls `onSessionCreated`, worktree factory returns the expected interface, run result contains the correct fields).
114
+ 2. **Simplified tests**: ~30 tests across the three files replace 3–6 lines of inline stub construction with 1-line factory calls.
115
+ 3. **Unchanged tests**: All existing test assertions remain identical — only the setup code changes.
116
+ Tests with custom runner behavior (event-emitting, gated, error-throwing) keep their inline stubs.
117
+
118
+ ## TDD order
119
+
120
+ 1. **Create `test/helpers/manager-stubs.ts` + `manager-stubs.test.ts`** Add `createBlockingRunner`, `createSessionRunner`, `createMockWorktrees`, `createRunResult`.
121
+ Add smoke tests verifying each factory's return shape and basic behavior.
122
+ Commit: `test: add manager-stubs helper factories (#219)`
123
+
124
+ 2. **Migrate `agent-manager.test.ts` to use manager-stubs** Replace 5 inline never-resolving runners with `createBlockingRunner()`.
125
+ Replace 4 identical worktree stubs with `createMockWorktrees()` / `createMockWorktrees({ create: ... })`.
126
+ Replace inline session-creating runners with `createSessionRunner(session)` where the test only needs `onSessionCreated` wiring.
127
+ Replace inline run-result objects with `createRunResult()` where the default shape suffices.
128
+ Run `pnpm vitest run test/lifecycle/agent-manager.test.ts` to verify green.
129
+ Commit: `test: migrate agent-manager tests to manager-stubs (#219)`
130
+
131
+ 3. **Add inline factories to `conversation-viewer.test.ts` and migrate** Add `createTestViewer(overrides?)` inline factory with defaults for all 8 constructor fields.
132
+ Add `assertRenderFitsWidths(messages, widths?, overrides?)` inline helper.
133
+ Migrate all 10 "render width safety" tests to use `assertRenderFitsWidths`.
134
+ Migrate all 5 "safety net" tests to use `createTestViewer`.
135
+ Run `pnpm vitest run test/conversation-viewer.test.ts` to verify green.
136
+ Commit: `test: reduce conversation-viewer test duplication (#219)`
137
+
138
+ 4. **Add inline `setupDetail` to `agent-config-editor.test.ts` and migrate** Add `setupDetail(selectResults, options?)` returning `{ fileOps, editor, ui }`.
139
+ Migrate `showAgentDetail` tests to use `setupDetail`.
140
+ Run `pnpm vitest run test/ui/agent-config-editor.test.ts` to verify green.
141
+ Commit: `test: reduce agent-config-editor test duplication (#219)`
142
+
143
+ 5. **Final verification** Run `pnpm vitest run` (full suite) to confirm no regressions.
144
+ Run `pnpm run check` to confirm no type errors.
145
+ Commit is not needed — this is a verification-only step.
146
+
147
+ ## Risks and mitigations
148
+
149
+ 1. **Factory defaults diverge from test intent** — If a shared factory's defaults don't match what an individual test expects, assertions silently pass or fail for the wrong reason.
150
+ Mitigation: diff all inline stubs against the proposed factory defaults before writing the factory.
151
+ Keep tests with unique mock behavior inline rather than force-fitting them into a factory.
152
+
153
+ 2. **Over-abstraction obscures test intent** — Extracting too many details into helpers makes tests harder to read.
154
+ Mitigation: only extract truly duplicated boilerplate (stub construction); keep test-specific setup and assertions inline.
155
+ The gated runner pattern stays inline for this reason.
156
+
157
+ 3. **Intermediate broken state** — Partially migrated test files may have import conflicts.
158
+ Mitigation: each TDD step fully migrates one file before committing.
159
+
160
+ ## Open questions
161
+
162
+ None — the issue scope is well-defined and the dependencies are resolved.
@@ -34,3 +34,30 @@ Test count: 962 → 970 (+8 new tests in `test/ui/agent-file-writer.test.ts`).
34
34
  - Both consumer refactors were straightforward one-import-add + one-block-replace edits; all existing tests passed without modification, confirming the extraction preserved exact behavior.
35
35
  - The notification label `"Ejected ${name} to"` (with trailing space absorbed by `${targetPath}`) matched the pre-existing message format `"Ejected test-agent to /path"` exactly — no test assertions changed.
36
36
  - `FileWriter`, `WriterUI`, and `Reloadable` narrow interfaces are exported from `agent-file-writer.ts`; both consumer files import the concrete types from their original sources, satisfying TypeScript's structural checker without any casts.
37
+
38
+ ## Stage: Final Retrospective (2026-05-26T21:00:00Z)
39
+
40
+ ### Session summary
41
+
42
+ Full plan → TDD → ship → release lifecycle completed in a single continuous session.
43
+ Released as `pi-subagents-v7.7.0`.
44
+ Zero rework, zero test failures, zero CI issues.
45
+
46
+ ### Observations
47
+
48
+ #### What went well
49
+
50
+ - The Phase 13 roadmap's step-level issue decomposition produced an issue (#217) that was right-sized for fully autonomous execution — the entire lifecycle completed without any blocking questions or scope surprises.
51
+ - ISP-narrow interfaces (`FileWriter`, `WriterUI`, `Reloadable`) structurally satisfied both consumer types without casts, confirming the plan's design.
52
+ - Existing tests in both consumer files passed without modification after the refactors, validating that the extraction preserved exact behavior.
53
+
54
+ #### What caused friction (agent side)
55
+
56
+ - `wrong-abstraction` — The plan split TDD steps 1 (happy-path tests) and 2 (overwrite-guard tests) for a ~10-line function with a single conditional.
57
+ Writing all 8 tests at once and implementing the full function body in one pass was natural; splitting them would have been artificial.
58
+ Self-corrected by folding into one commit.
59
+ Impact: added friction but no rework — the plan said "implementation should already pass" for step 2, acknowledging the fold was expected.
60
+
61
+ #### What caused friction (user side)
62
+
63
+ - Nothing notable — the issue was well-scoped with clear target files, a concrete smell label, and an explicit dependency chain.
@@ -0,0 +1,72 @@
1
+ ---
2
+ issue: 218
3
+ issue_title: "Push SDK boundary in settings.ts (Phase 13, Step 5)"
4
+ ---
5
+
6
+ # Retro: #218 — Push SDK boundary in settings.ts
7
+
8
+ ## Stage: Planning (2026-05-26T17:01:55Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced a 3-step TDD plan to inject `agentDir: string` into `SettingsManager` and `loadSettings`, removing the only Pi SDK import from `settings.ts`.
13
+ The change is straightforward — a single parameter addition threading through constructor, free function, and boundary wiring.
14
+
15
+ ### Observations
16
+
17
+ - The change is entirely mechanical: no design ambiguity, no new abstractions, no breaking public API.
18
+ - The main implementation effort is in test updates (~35 `new SettingsManager(...)` call sites plus ~15 `loadSettings(...)` calls), all requiring an `agentDir` argument.
19
+ - All test `describe` blocks that manipulate `PI_CODING_AGENT_DIR` env var can drop that scaffolding entirely, simplifying setup/teardown.
20
+ - `saveSettings` has no SDK dependency and needs no signature change — only `loadSettings` calls `globalPath()`.
21
+
22
+ ## Stage: Implementation — TDD (2026-05-26T17:13:26Z)
23
+
24
+ ### Session summary
25
+
26
+ Completed all 3 plan steps across 2 commits plus 1 doc commit.
27
+ All 970 tests pass; `settings.ts` now has 0 Pi SDK imports and all `PI_CODING_AGENT_DIR` env var manipulation is gone from `settings.test.ts`.
28
+
29
+ ### Observations
30
+
31
+ - **Steps 1+2 combined:** Changing `loadSettings(cwd)` to `loadSettings(agentDir, cwd)` forced updating `SettingsManager.load()` in the same commit — they were inseparable (esbuild skips type checks, so the old call compiled but produced wrong runtime behavior).
32
+ The two production changes landed in one commit with a note in the body.
33
+ - **Test simplification was significant:** Removed `originalAgentDirEnv` save/restore scaffolding from 5 `describe` blocks; the test code shrank by 32 lines net.
34
+ - **`/nonexistent` sentinel:** Tests that construct `SettingsManager` but never call `load()` pass `agentDir: "/nonexistent"` — a clear signal the field is unused in that scope.
35
+ - Architecture doc Step 5 heading marked `✓` and folded into the last `feat:` commit by `pi-autoformat`.
36
+
37
+ ## Stage: Final Retrospective (2026-05-26T17:22:11Z)
38
+
39
+ ### Session summary
40
+
41
+ Issue #218 went from plan to shipped release (`pi-subagents-v7.8.0`) in a single continuous session.
42
+ Planning, TDD (2 feat commits + 1 doc commit), shipping, CI verification, issue close, and release-please merge all completed without user intervention beyond stage transitions.
43
+
44
+ ### Observations
45
+
46
+ #### What went well
47
+
48
+ - **Clean mechanical execution:** The entire change was 2 production files (`settings.ts`, `index.ts`) and 1 test file, with zero unexpected test breakage and zero rework commits.
49
+ - **Test simplification payoff:** Removing `PI_CODING_AGENT_DIR` env var scaffolding from 5 `describe` blocks shrank the test file by 32 lines net — a tangible improvement in test readability.
50
+ - **Ship stage model efficiency:** The `/ship-issue` stage ran on `deepseek-v4-flash`, which was appropriate for the purely mechanical push/CI/close/merge workflow.
51
+
52
+ #### What caused friction (agent side)
53
+
54
+ 1. `wrong-abstraction` — The plan split steps 1 and 2 into separate commits, but changing `loadSettings(cwd)` to `loadSettings(agentDir, cwd)` immediately broke `SettingsManager.load()` which calls it.
55
+ The agent recognized this during the red phase and combined them into one commit.
56
+ The existing testing skill rule ("When a TDD plan lists separate steps that share a type definition… fold them into one step") already covers this — the plan just didn't apply it.
57
+ Impact: added friction but no rework; recognized on first test run.
58
+ 2. `missing-context` — Attempted to add `| ✓ #218 |` as an extra column to one row of the architecture doc's findings table, creating a column-count mismatch.
59
+ The autoformatter reverted the broken table.
60
+ The agent then spent ~5 tool calls (`git show --stat`, `git status`, `grep` ×2, `read`) investigating what happened before switching to the Step 5 heading approach.
61
+ Impact: ~2 minutes of investigation; no rework beyond the heading edit.
62
+
63
+ #### What caused friction (user side)
64
+
65
+ - The user asked "Are we ready for shipping?"
66
+ which surfaced that the TDD retro stage notes were still uncommitted.
67
+ This was a useful checkpoint — the ship stage committed them before pushing.
68
+ Opportunity: the `/tdd-plan` prompt could commit retro notes as part of its final step, but the current flow (write notes, then commit in ship) is lightweight enough that enforcing it would add complexity for marginal gain.
69
+
70
+ ### Changes made
71
+
72
+ 1. Retro file updated at `packages/pi-subagents/docs/retro/0218-push-sdk-boundary-in-settings.md` — no other files changed.
@@ -0,0 +1,36 @@
1
+ ---
2
+ issue: 219
3
+ issue_title: "Reduce test duplication — top 3 clone families (Phase 13, Step 6)"
4
+ ---
5
+
6
+ # Retro: #219 — Reduce test duplication — top 3 clone families
7
+
8
+ ## Stage: Planning (2026-05-26T20:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Analyzed duplication patterns in the three target test files (`agent-manager.test.ts`, `conversation-viewer.test.ts`, `agent-config-editor.test.ts`).
13
+ Produced a 5-step TDD plan with shared `manager-stubs.ts` helper for runner/worktree factories, plus inline factories for the two UI test files.
14
+
15
+ ### Observations
16
+
17
+ - The agent-manager test has the most diverse clone families (runner stubs, worktree stubs, run-result shapes) — these benefit from a shared helper file since the patterns are reused across 15+ describe blocks.
18
+ - The conversation-viewer and config-editor duplication is more localized — inline factories within each test file are the right granularity to avoid over-extraction.
19
+ - Gated runners (using `Promise.withResolvers`) were deliberately kept inline since they encode test-specific flow control that a factory would obscure.
20
+ - Both dependencies (#214, #216) are closed, so the production code is stable and the tests won't shift under us during implementation.
21
+
22
+ ## Stage: Implementation — TDD (2026-05-26T17:42:41Z)
23
+
24
+ ### Session summary
25
+
26
+ Completed all 4 TDD cycles: created `test/helpers/manager-stubs.ts` + `manager-stubs.test.ts` (13 smoke tests), migrated `agent-manager.test.ts`, `conversation-viewer.test.ts`, and `agent-config-editor.test.ts`.
27
+ Test count delta: 970 → 983 (+13 from smoke tests).
28
+ All 4 commits landed cleanly; full suite green at every step.
29
+
30
+ ### Observations
31
+
32
+ - Target file line savings: `agent-manager.test.ts` −63, `conversation-viewer.test.ts` −58, `agent-config-editor.test.ts` −16; offset by +211 for the new helper files.
33
+ Net LOC is positive, but the _clone_ lines fallow detects are eliminated — the metric the issue targets.
34
+ - The `createSessionRunner` + `createRunResult` chain required careful identity-check verification: `createRunResult(sess)` calls `toAgentSession(sess)` which casts without creating a new object, so `toBe(session)` assertions in the execution-state tests still pass. ✓
35
+ - ESLint auto-fixed two cosmetic issues on commit (`activity = undefined` → `activity` destructuring, `session as unknown` cast removal) — caught by pre-commit hooks, not a problem in practice.
36
+ - The `assertRenderFitsWidths` helper in `conversation-viewer.test.ts` reduced the 10 render-safety tests from ~8 lines each to 1–4 lines each; the `setupDetail` helper in `agent-config-editor.test.ts` eliminated 3 repeated setup lines per test across 18 `showAgentDetail` tests.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "7.7.0",
3
+ "version": "7.8.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/service.ts"
package/src/index.ts CHANGED
@@ -70,6 +70,7 @@ export default function (pi: ExtensionAPI) {
70
70
  const settings = new SettingsManager({
71
71
  emit: (event, payload) => pi.events.emit(event, payload),
72
72
  cwd: process.cwd(),
73
+ agentDir: getAgentDir(),
73
74
  onMaxConcurrentChanged: () => manager.notifyConcurrencyChanged(),
74
75
  });
75
76
  settings.load();
package/src/settings.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  // Persistence for pi-subagents operational settings.
2
- // - Global: ~/.pi/agent/subagents.json (via getAgentDir()) — manual defaults, never written here
2
+ // - Global: ~/.pi/agent/subagents.json (agentDir injected at construction) — manual defaults, never written here
3
3
  // - Project: <cwd>/.pi/subagents.json — written by /agents → Settings; overrides global on load
4
4
 
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
8
7
  export interface SubagentsSettings {
9
8
  maxConcurrent?: number;
10
9
  /**
@@ -34,11 +33,13 @@ export class SettingsManager {
34
33
 
35
34
  private readonly emit: SettingsEmit;
36
35
  private readonly cwd: string;
36
+ private readonly agentDir: string;
37
37
  private readonly onMaxConcurrentChanged: (() => void) | undefined;
38
38
 
39
- constructor(deps: { emit: SettingsEmit; cwd: string; onMaxConcurrentChanged?: () => void }) {
39
+ constructor(deps: { emit: SettingsEmit; cwd: string; agentDir: string; onMaxConcurrentChanged?: () => void }) {
40
40
  this.emit = deps.emit;
41
41
  this.cwd = deps.cwd;
42
+ this.agentDir = deps.agentDir;
42
43
  this.onMaxConcurrentChanged = deps.onMaxConcurrentChanged;
43
44
  }
44
45
 
@@ -84,7 +85,7 @@ export class SettingsManager {
84
85
  * Returns the raw loaded settings object.
85
86
  */
86
87
  load(): SubagentsSettings {
87
- const settings = loadSettings(this.cwd);
88
+ const settings = loadSettings(this.agentDir, this.cwd);
88
89
  if (typeof settings.maxConcurrent === "number") this.maxConcurrent = settings.maxConcurrent;
89
90
  if (typeof settings.defaultMaxTurns === "number") this.defaultMaxTurns = settings.defaultMaxTurns;
90
91
  if (typeof settings.graceTurns === "number") this.graceTurns = settings.graceTurns;
@@ -180,8 +181,8 @@ function sanitize(raw: unknown): SubagentsSettings {
180
181
  return out;
181
182
  }
182
183
 
183
- function globalPath(): string {
184
- return join(getAgentDir(), "subagents.json");
184
+ function globalPath(agentDir: string): string {
185
+ return join(agentDir, "subagents.json");
185
186
  }
186
187
 
187
188
  function projectPath(cwd: string): string {
@@ -205,8 +206,8 @@ function readSettingsFile(path: string): SubagentsSettings {
205
206
  }
206
207
 
207
208
  /** Load merged settings: global provides defaults, project overrides. */
208
- export function loadSettings(cwd: string = process.cwd()): SubagentsSettings {
209
- return { ...readSettingsFile(globalPath()), ...readSettingsFile(projectPath(cwd)) };
209
+ export function loadSettings(agentDir: string, cwd: string = process.cwd()): SubagentsSettings {
210
+ return { ...readSettingsFile(globalPath(agentDir)), ...readSettingsFile(projectPath(cwd)) };
210
211
  }
211
212
 
212
213
  /**