@gotgenes/pi-subagents 7.7.0 → 7.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ 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.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.7.0...pi-subagents-v7.8.0) (2026-05-26)
9
+
10
+
11
+ ### Features
12
+
13
+ * inject agentDir into SettingsManager and loadSettings to remove SDK dependency ([7dcb986](https://github.com/gotgenes/pi-packages/commit/7dcb9868c8ac52c86a3eac0b6fc6648c8d57fc7c))
14
+ * 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))
15
+
16
+
17
+ ### Documentation
18
+
19
+ * 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))
20
+ * **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))
21
+ * **retro:** add retro notes for issue [#217](https://github.com/gotgenes/pi-packages/issues/217) ([2140655](https://github.com/gotgenes/pi-packages/commit/21406555e34fbe0d41f48206e3208e1cb7326633))
22
+ * **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))
23
+
8
24
  ## [7.7.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.6.0...pi-subagents-v7.7.0) (2026-05-26)
9
25
 
10
26
 
@@ -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.
@@ -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,35 @@
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`.
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.0",
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
  /**