@gotgenes/pi-subagents 7.6.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 +33 -0
- package/docs/architecture/architecture.md +119 -7
- package/docs/plans/0217-extract-overwrite-guard.md +176 -0
- package/docs/plans/0218-push-sdk-boundary-in-settings.md +172 -0
- package/docs/retro/0216-decompose-start-agent.md +37 -0
- package/docs/retro/0217-extract-overwrite-guard.md +63 -0
- package/docs/retro/0218-push-sdk-boundary-in-settings.md +35 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/settings.ts +9 -8
- package/src/ui/agent-config-editor.ts +9 -11
- package/src/ui/agent-creation-wizard.ts +2 -11
- package/src/ui/agent-file-writer.ts +55 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ 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
|
+
|
|
24
|
+
## [7.7.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.6.0...pi-subagents-v7.7.0) (2026-05-26)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
* extract writeAgentFile overwrite-guard function ([#217](https://github.com/gotgenes/pi-packages/issues/217)) ([141df78](https://github.com/gotgenes/pi-packages/commit/141df784ea5cf6c5286a1a6e9861daa259fa4e1c))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Documentation
|
|
33
|
+
|
|
34
|
+
* add Phase 14 roadmap — Agent domain model, scheduling extraction ([#227](https://github.com/gotgenes/pi-packages/issues/227)–[#232](https://github.com/gotgenes/pi-packages/issues/232)) ([089d9e0](https://github.com/gotgenes/pi-packages/commit/089d9e0becde693c2795ca590d987a4d2b169edc))
|
|
35
|
+
* plan extract overwrite guard from UI ([#217](https://github.com/gotgenes/pi-packages/issues/217)) ([89de32c](https://github.com/gotgenes/pi-packages/commit/89de32c6a1bbb84fb0e252fecaa6edf79dc9b5b3))
|
|
36
|
+
* **retro:** add planning stage notes for issue [#217](https://github.com/gotgenes/pi-packages/issues/217) ([b1a854f](https://github.com/gotgenes/pi-packages/commit/b1a854f18ad133542c5f3e3ab4400ed753ba7c8c))
|
|
37
|
+
* **retro:** add retro notes for issue [#216](https://github.com/gotgenes/pi-packages/issues/216) ([dcb86ea](https://github.com/gotgenes/pi-packages/commit/dcb86eace93d2f68acf39d6f0b8e7d64aaf982d1))
|
|
38
|
+
* **retro:** add TDD stage notes for issue [#217](https://github.com/gotgenes/pi-packages/issues/217) ([7305a28](https://github.com/gotgenes/pi-packages/commit/7305a281f89258d8898fb13f02ba051b58513a71))
|
|
39
|
+
* update architecture for writeAgentFile extraction ([#217](https://github.com/gotgenes/pi-packages/issues/217)) ([298a819](https://github.com/gotgenes/pi-packages/commit/298a8196de5b8dc507bb08ead57a6c712a50c3f0))
|
|
40
|
+
|
|
8
41
|
## [7.6.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.5.1...pi-subagents-v7.6.0) (2026-05-26)
|
|
9
42
|
|
|
10
43
|
|
|
@@ -294,6 +294,7 @@ src/
|
|
|
294
294
|
│ ├── message-formatters.ts pure per-message-type formatters (extracted from conversation-viewer)
|
|
295
295
|
│ ├── agent-activity-tracker.ts live activity state tracker
|
|
296
296
|
│ ├── agent-file-ops.ts filesystem abstraction
|
|
297
|
+
│ ├── agent-file-writer.ts overwrite-guard + write + reload + notify helper
|
|
297
298
|
│ ├── ui-observer.ts session-event observer for streaming
|
|
298
299
|
│ └── display.ts pure formatters and shared types
|
|
299
300
|
│
|
|
@@ -491,7 +492,7 @@ Once structural work stabilizes, these are expected to cool.
|
|
|
491
492
|
### Production duplication
|
|
492
493
|
|
|
493
494
|
The prior clone group between `agent-runner.ts` and `message-formatters.ts` was resolved in #172.
|
|
494
|
-
|
|
495
|
+
The 20-line clone group between `agent-config-editor.ts` and `agent-creation-wizard.ts` was resolved in #217 — extracted into `ui/agent-file-writer.ts` (`writeAgentFile`). 0 production clone groups remain.
|
|
495
496
|
|
|
496
497
|
### Proposed bag decompositions
|
|
497
498
|
|
|
@@ -583,7 +584,10 @@ The IO boundary was split into two focused interfaces:
|
|
|
583
584
|
export interface EnvironmentIO {
|
|
584
585
|
detectEnv: (exec: ShellExec, cwd: string) => Promise<EnvInfo>;
|
|
585
586
|
getAgentDir: () => string;
|
|
586
|
-
deriveSessionDir: (
|
|
587
|
+
deriveSessionDir: (
|
|
588
|
+
parentSessionFile: string | undefined,
|
|
589
|
+
effectiveCwd: string,
|
|
590
|
+
) => string;
|
|
587
591
|
}
|
|
588
592
|
|
|
589
593
|
/** Session factory — create SDK objects for a child agent session. */
|
|
@@ -591,7 +595,9 @@ export interface SessionFactoryIO {
|
|
|
591
595
|
createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoaderLike;
|
|
592
596
|
createSessionManager: (cwd: string, sessionDir: string) => SessionManagerLike;
|
|
593
597
|
createSettingsManager: (cwd: string, agentDir: string) => SettingsManager;
|
|
594
|
-
createSession: (
|
|
598
|
+
createSession: (
|
|
599
|
+
opts: CreateSessionOptions,
|
|
600
|
+
) => Promise<{ session: AgentSession }>;
|
|
595
601
|
assemblerIO: AssemblerIO;
|
|
596
602
|
}
|
|
597
603
|
|
|
@@ -642,7 +648,7 @@ Three closure factories converted to classes in [#214].
|
|
|
642
648
|
- Smell: C (coupling — deps hidden in closure scope instead of explicit on class)
|
|
643
649
|
- Outcome: 0 remaining closure factories (excluding pure-function factories), deps visible as constructor parameters
|
|
644
650
|
|
|
645
|
-
### Step 2: Decompose `buildParentContext` (cognitive 30) — [#215]
|
|
651
|
+
### Step 2: Decompose `buildParentContext` (cognitive 30) — [#215] ✓
|
|
646
652
|
|
|
647
653
|
`buildParentContext` in `session/context.ts` is the only remaining fallow refactoring target.
|
|
648
654
|
The function loops over branch entries with 3 type-check branches, each with sub-branches for role or summary.
|
|
@@ -670,7 +676,7 @@ Extracted:
|
|
|
670
676
|
- Smell: B (oversized method) + A (duplicated finalization logic in then/catch)
|
|
671
677
|
- Outcome: `startAgent` reduced to ~40 LOC coordinator with zero mutable `let` bindings; `.then()`/`.catch()` are one-liners
|
|
672
678
|
|
|
673
|
-
### Step 4: Extract overwrite guard from UI — [#217]
|
|
679
|
+
### Step 4: Extract overwrite guard from UI — [#217] ✓
|
|
674
680
|
|
|
675
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.
|
|
676
682
|
Extract a shared `writeAgentFile(fileOps, ui, registry, targetPath, content, label)` function.
|
|
@@ -679,7 +685,7 @@ Extract a shared `writeAgentFile(fileOps, ui, registry, targetPath, content, lab
|
|
|
679
685
|
- Smell: A (production duplication)
|
|
680
686
|
- Outcome: 0 production clone groups
|
|
681
687
|
|
|
682
|
-
### Step 5: Push SDK boundary in `settings.ts` — [#218]
|
|
688
|
+
### Step 5: Push SDK boundary in `settings.ts` — [#218] ✓
|
|
683
689
|
|
|
684
690
|
`globalPath()` calls `getAgentDir()` (a Pi SDK function) at invocation time.
|
|
685
691
|
This hides a platform dependency inside a module that is otherwise pure configuration logic.
|
|
@@ -727,6 +733,112 @@ flowchart LR
|
|
|
727
733
|
2. **Track B — Complexity and coupling** (Steps 2, 5): independent, can proceed in parallel with Track A.
|
|
728
734
|
3. **Track C — Duplication** (Steps 4, 6): Step 4 depends on Step 1 (overwrite guard lives in files being converted); Step 6 depends on Steps 1 and 3 (production code they test changes first).
|
|
729
735
|
|
|
736
|
+
## Improvement roadmap (Phase 14)
|
|
737
|
+
|
|
738
|
+
Phase 14 addresses the anemic domain model in the lifecycle layer.
|
|
739
|
+
`AgentRecord` is a data bag — identity, status transitions, and stats — but no behavior.
|
|
740
|
+
`AgentManager` reaches into records 37 times, doing work that belongs on the agent.
|
|
741
|
+
Per-agent state (pending steers, abort logic, run lifecycle) is scattered across the manager, `RunHandle`, and a manager-level Map.
|
|
742
|
+
|
|
743
|
+
The scheduling concern (queue, concurrency counter, drain) is tangled into `AgentManager` alongside collection management and run orchestration.
|
|
744
|
+
`notifyConcurrencyChanged()` is a scheduling method exposed as a public API so settings can poke the queue — a cross-concern leak.
|
|
745
|
+
|
|
746
|
+
### Findings summary
|
|
747
|
+
|
|
748
|
+
| Finding | Category | Impact | Risk | Priority |
|
|
749
|
+
| ------------------------------------------------------------- | ------------ | ------ | ---- | -------- |
|
|
750
|
+
| `AgentRecord` is anemic — no behavior, manager reaches in 37× | B: Oversized | 5 | 3 | 15 |
|
|
751
|
+
| Scheduling tangled into `AgentManager` (3 fields, 3 methods) | A: Coupling | 4 | 2 | 12 |
|
|
752
|
+
| `startAgent` uses `.then()`/`.catch()` instead of async/await | C: Callbacks | 3 | 2 | 10 |
|
|
753
|
+
| `onSessionCreated` callback flows through 3 layers | C: Callbacks | 3 | 2 | 10 |
|
|
754
|
+
| `resume()` duplicates observer subscribe/unsubscribe pattern | A: Redundant | 2 | 1 | 8 |
|
|
755
|
+
| `exec`/`registry` relay-only deps on `AgentManager` | C: Coupling | 2 | 1 | 6 |
|
|
756
|
+
|
|
757
|
+
### Step 1: Evolve AgentRecord into Agent with behavior — [#227]
|
|
758
|
+
|
|
759
|
+
Rename `AgentRecord` → `Agent` (or wrap it).
|
|
760
|
+
Move per-agent behavior from `AgentManager` into the agent:
|
|
761
|
+
|
|
762
|
+
1. `Agent.abort()` — absorbs status-check + controller.abort + markStopped.
|
|
763
|
+
2. `Agent.queueSteer(message)` / `Agent.flushPendingSteers(session)` — moves pending steers from manager map to per-agent array.
|
|
764
|
+
3. `Agent.setupWorktree(worktrees, isolation)` — moves worktree creation into the agent.
|
|
765
|
+
|
|
766
|
+
- Target: `src/lifecycle/agent-record.ts` → `src/lifecycle/agent.ts`, `src/lifecycle/agent-manager.ts`
|
|
767
|
+
- Smell: B (anemic domain model) + C (manager reaching into records)
|
|
768
|
+
- Outcome: `AgentManager` delegates via Tell-Don't-Ask; per-agent state lives on the agent
|
|
769
|
+
|
|
770
|
+
### Step 2: Convert startAgent to async/await — [#228]
|
|
771
|
+
|
|
772
|
+
Convert `startAgent` from synchronous (returns void, assigns `record.promise` to a `.then()`/`.catch()` chain) to `async` (returns `Promise<void>`, uses try/catch).
|
|
773
|
+
`spawn()` assigns `record.promise = this.startAgent(...)` instead of calling `startAgent()` synchronously.
|
|
774
|
+
|
|
775
|
+
- Depends on: #227
|
|
776
|
+
- Target: `src/lifecycle/agent-manager.ts`
|
|
777
|
+
- Smell: C (raw promise callbacks)
|
|
778
|
+
- Outcome: zero `.then()`/`.catch()` in `agent-manager.ts`
|
|
779
|
+
|
|
780
|
+
### Step 3: Replace onSessionCreated callback with observer method — [#229]
|
|
781
|
+
|
|
782
|
+
Add `onSessionCreated(agent, session)` to `AgentManagerObserver`.
|
|
783
|
+
Remove the `onSessionCreated` callback from `AgentSpawnConfig`.
|
|
784
|
+
Tool-layer code subscribes via the observer pattern instead of passing callbacks through the spawn config.
|
|
785
|
+
|
|
786
|
+
- Target: `src/lifecycle/agent-manager.ts`, `src/tools/background-spawner.ts`, `src/tools/foreground-runner.ts`
|
|
787
|
+
- Smell: C (callback flowing through 3 layers)
|
|
788
|
+
- Outcome: `AgentSpawnConfig` loses one callback field; session notification uses the observer pattern
|
|
789
|
+
|
|
790
|
+
### Step 4: Extract ConcurrencyQueue from AgentManager — [#230]
|
|
791
|
+
|
|
792
|
+
Extract `queue[]`, `runningBackground`, `_getMaxConcurrent`, `drainQueue()`, `finalizeBackgroundRun()` into a `ConcurrencyQueue` class.
|
|
793
|
+
`SettingsManager` talks to the queue directly — `notifyConcurrencyChanged()` is eliminated from `AgentManager`.
|
|
794
|
+
|
|
795
|
+
- Target: new `src/lifecycle/concurrency-queue.ts`, `src/lifecycle/agent-manager.ts`, `src/index.ts`
|
|
796
|
+
- Smell: A (tangled concerns) + C (cross-concern leak via `notifyConcurrencyChanged`)
|
|
797
|
+
- Outcome: `AgentManager` loses 3 fields, 3 methods (~40 lines); scheduling is independently testable
|
|
798
|
+
|
|
799
|
+
### Step 5: Push exec/registry relay deps to runner construction — [#231]
|
|
800
|
+
|
|
801
|
+
`AgentManager` receives `exec` and `registry` in its constructor but only relays them to `runner.run()` via `context`.
|
|
802
|
+
Move them to `ConcreteAgentRunner` construction.
|
|
803
|
+
|
|
804
|
+
- Target: `src/lifecycle/agent-manager.ts`, `src/lifecycle/agent-runner.ts`, `src/index.ts`
|
|
805
|
+
- Smell: C (relay-only dependencies)
|
|
806
|
+
- Outcome: `AgentManager` loses 2 fields; `AgentManagerOptions` shrinks from 7 to 5 fields
|
|
807
|
+
|
|
808
|
+
### Step 6: Unify resume() with RunHandle pattern — [#232]
|
|
809
|
+
|
|
810
|
+
After #227 moves `RunHandle` ownership to the `Agent`, `resume()` on `AgentManager` becomes a 4-line delegation to `agent.resume(runner, prompt, signal)`.
|
|
811
|
+
The agent manages its own observer subscription lifecycle.
|
|
812
|
+
|
|
813
|
+
- Depends on: #227, #228
|
|
814
|
+
- Target: `src/lifecycle/agent-manager.ts`
|
|
815
|
+
- Smell: A (duplicated observer subscribe/unsubscribe pattern)
|
|
816
|
+
- Outcome: no manual `subscribeRecordObserver` / try-finally in the manager
|
|
817
|
+
|
|
818
|
+
### Step dependency diagram
|
|
819
|
+
|
|
820
|
+
```mermaid
|
|
821
|
+
flowchart LR
|
|
822
|
+
S1["Step 1\nAgent with behavior"]
|
|
823
|
+
S2["Step 2\nasync startAgent"]
|
|
824
|
+
S3["Step 3\nonSessionCreated observer"]
|
|
825
|
+
S4["Step 4\nConcurrencyQueue"]
|
|
826
|
+
S5["Step 5\nrelay deps"]
|
|
827
|
+
S6["Step 6\nresume unification"]
|
|
828
|
+
|
|
829
|
+
S1 --> S2
|
|
830
|
+
S1 --> S6
|
|
831
|
+
S2 --> S6
|
|
832
|
+
S3 ~~~ S4
|
|
833
|
+
S4 ~~~ S5
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
### Tracks
|
|
837
|
+
|
|
838
|
+
1. **Track A — Domain model** (Steps 1, 2, 6): Agent with behavior, async runs, resume unification.
|
|
839
|
+
Sequential — each depends on the previous.
|
|
840
|
+
2. **Track B — Decoupling** (Steps 3, 4, 5): independent, can proceed in parallel with Track A.
|
|
841
|
+
|
|
730
842
|
## Refactoring history
|
|
731
843
|
|
|
732
844
|
Phases 1–5 and 7–12 are complete.
|
|
@@ -764,6 +876,7 @@ Detailed records are preserved in per-phase history files:
|
|
|
764
876
|
| Phase 11 | #192, #193, #194, #195, #196 | SessionContext, runtime queries, interface alignment, tool classes, runner/menu classes, index.ts simplification |
|
|
765
877
|
| Phase 12 | #205, #206, #207, #208 | renderWidgetLines, showAgentDetail, widget update, shared test fixtures |
|
|
766
878
|
| Phase 13 | #214, #215, #216, #217, #218, #219 | Closure-to-class, buildParentContext, startAgent decomp, overwrite guard, settings SDK, test duplication |
|
|
879
|
+
| Phase 14 | #227, #228, #229, #230, #231, #232 | Agent domain model, async startAgent, onSessionCreated observer, ConcurrencyQueue, relay deps, resume unification |
|
|
767
880
|
|
|
768
881
|
The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
|
|
769
882
|
|
|
@@ -782,7 +895,6 @@ The upstream test suite is run periodically as a regression canary for the agent
|
|
|
782
895
|
[earendil-works/pi#4207]: https://github.com/earendil-works/pi/issues/4207
|
|
783
896
|
[gotgenes/pi-packages]: https://github.com/gotgenes/pi-packages
|
|
784
897
|
[tintinweb/pi-subagents]: https://github.com/tintinweb/pi-subagents
|
|
785
|
-
|
|
786
898
|
[166]: https://github.com/gotgenes/pi-packages/issues/166
|
|
787
899
|
[167]: https://github.com/gotgenes/pi-packages/issues/167
|
|
788
900
|
[168]: https://github.com/gotgenes/pi-packages/issues/168
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 217
|
|
3
|
+
issue_title: "Extract overwrite guard from UI (Phase 13, Step 4)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Extract overwrite guard from UI
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
The overwrite-guard + write + reload + notify pattern is duplicated between `AgentConfigEditor.ejectAgent` and `AgentCreationWizard.showManualWizard`.
|
|
11
|
+
Both sites check file existence, prompt for overwrite confirmation, write the file, reload the agent registry, and notify the user — identical logic with only the content and notification label differing.
|
|
12
|
+
This is the last remaining production clone group in the package.
|
|
13
|
+
|
|
14
|
+
## Goals
|
|
15
|
+
|
|
16
|
+
- Extract a shared `writeAgentFile` function into a new `src/ui/agent-file-writer.ts` module.
|
|
17
|
+
- Replace both call sites (`ejectAgent`, `showManualWizard`) with calls to the shared function.
|
|
18
|
+
- Achieve 0 production clone groups.
|
|
19
|
+
- Unit-test the extracted function in isolation.
|
|
20
|
+
|
|
21
|
+
## Non-Goals
|
|
22
|
+
|
|
23
|
+
- Extracting the partial overwrite guard in `showGenerateWizard` — that flow has different lifecycle semantics (the spawned agent does the write, and the post-write check is conditional on file existence).
|
|
24
|
+
The guard-only overlap is 5 lines, not worth a separate abstraction.
|
|
25
|
+
- Reducing test duplication in `agent-config-editor.test.ts` or `agent-creation-wizard.test.ts` — tracked in #219 (Phase 13, Step 6).
|
|
26
|
+
- Changing the `disableAgent` write path — it has no overwrite guard and different notification semantics.
|
|
27
|
+
|
|
28
|
+
## Background
|
|
29
|
+
|
|
30
|
+
### Existing modules
|
|
31
|
+
|
|
32
|
+
| Module | Role |
|
|
33
|
+
| --------------------------------- | ------------------------------------------------------------------- |
|
|
34
|
+
| `src/ui/agent-config-editor.ts` | Agent detail view with edit/delete/eject/disable/enable transitions |
|
|
35
|
+
| `src/ui/agent-creation-wizard.ts` | AI-generation and manual-form agent creation flows |
|
|
36
|
+
| `src/ui/agent-file-ops.ts` | Filesystem abstraction (`AgentFileOps` interface + production impl) |
|
|
37
|
+
| `src/ui/agent-menu.ts` | `/agents` slash command menu; defines `MenuUI` interface |
|
|
38
|
+
|
|
39
|
+
### Duplicated pattern
|
|
40
|
+
|
|
41
|
+
Both sites execute this sequence:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
if (this.fileOps.exists(targetPath)) {
|
|
45
|
+
const overwrite = await ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
|
|
46
|
+
if (!overwrite) return;
|
|
47
|
+
}
|
|
48
|
+
this.fileOps.write(targetPath, content);
|
|
49
|
+
this.registry.reload();
|
|
50
|
+
ui.notify(`${label} ${targetPath}`, "info");
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The only differences are the `content` argument and the notification `label`.
|
|
54
|
+
|
|
55
|
+
### Dependency
|
|
56
|
+
|
|
57
|
+
Issue #214 (closure-to-class conversion) is closed — both consumer files are already class-based.
|
|
58
|
+
|
|
59
|
+
## Design Overview
|
|
60
|
+
|
|
61
|
+
### Extracted function
|
|
62
|
+
|
|
63
|
+
`writeAgentFile` is a free async function — not a class method — because both consumers are classes with different constructor signatures and no shared base.
|
|
64
|
+
The function takes narrow interface parameters following ISP: each parameter type declares only the methods the function calls.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
/** Minimal file operations for the overwrite-guard-and-write pattern. */
|
|
68
|
+
interface FileWriter {
|
|
69
|
+
exists(filePath: string): boolean;
|
|
70
|
+
write(filePath: string, content: string): void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Minimal UI for the overwrite-guard-and-write pattern. */
|
|
74
|
+
interface WriterUI {
|
|
75
|
+
confirm(title: string, message: string): Promise<boolean>;
|
|
76
|
+
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Registry that can be reloaded after file changes. */
|
|
80
|
+
interface Reloadable {
|
|
81
|
+
reload(): void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Write an agent file with an overwrite guard.
|
|
86
|
+
*
|
|
87
|
+
* Returns true if the file was written, false if the user declined to overwrite.
|
|
88
|
+
*/
|
|
89
|
+
export async function writeAgentFile(
|
|
90
|
+
fileOps: FileWriter,
|
|
91
|
+
ui: WriterUI,
|
|
92
|
+
registry: Reloadable,
|
|
93
|
+
targetPath: string,
|
|
94
|
+
content: string,
|
|
95
|
+
label: string,
|
|
96
|
+
): Promise<boolean>;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Consumer call sites
|
|
100
|
+
|
|
101
|
+
In `AgentConfigEditor.ejectAgent`:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
await writeAgentFile(this.fileOps, ui, this.registry, targetPath, buildEjectContent(cfg), `Ejected ${name} to`);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
In `AgentCreationWizard.showManualWizard`:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
await writeAgentFile(this.fileOps, ui, this.registry, targetPath, content, "Created");
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Both callers already hold `this.fileOps` and `this.registry` as private fields, and receive `ui` as a method parameter — no wiring changes needed.
|
|
114
|
+
|
|
115
|
+
### ISP verification
|
|
116
|
+
|
|
117
|
+
The `FileWriter` interface uses 2 of `AgentFileOps`'s 6 methods (`exists`, `write`).
|
|
118
|
+
The `WriterUI` interface uses 2 of `MenuUI`'s 6 methods (`confirm`, `notify`).
|
|
119
|
+
The `Reloadable` interface uses 1 method (`reload`).
|
|
120
|
+
All three are structurally satisfied by the existing types without adapter code.
|
|
121
|
+
|
|
122
|
+
## Module-Level Changes
|
|
123
|
+
|
|
124
|
+
1. **New `src/ui/agent-file-writer.ts`** — exports `writeAgentFile` function and the three narrow interfaces (`FileWriter`, `WriterUI`, `Reloadable`).
|
|
125
|
+
2. **`src/ui/agent-config-editor.ts`** — `ejectAgent` method: replace the inline overwrite-guard + write + reload + notify block with a call to `writeAgentFile`.
|
|
126
|
+
The `join(targetDir, ...)` and `buildEjectContent(cfg)` calls remain in the caller.
|
|
127
|
+
3. **`src/ui/agent-creation-wizard.ts`** — `showManualWizard` method: replace the inline overwrite-guard + write + reload + notify block with a call to `writeAgentFile`.
|
|
128
|
+
The `join(targetDir, ...)` and content-assembly calls remain in the caller.
|
|
129
|
+
4. **New `test/ui/agent-file-writer.test.ts`** — unit tests for `writeAgentFile`.
|
|
130
|
+
5. **`docs/architecture/architecture.md`** — add `agent-file-writer.ts` to the `ui/` layout listing and update the production-duplication section to mark the clone group as resolved.
|
|
131
|
+
|
|
132
|
+
## Test Impact Analysis
|
|
133
|
+
|
|
134
|
+
1. The new `agent-file-writer.test.ts` enables focused unit tests for the overwrite-guard + write + reload + notify sequence — previously this logic was only testable through the higher-level `ejectAgent` and `showManualWizard` flows.
|
|
135
|
+
2. Existing tests in `agent-config-editor.test.ts` (eject overwrite prompt, eject write) and `agent-creation-wizard.test.ts` (manual wizard overwrite prompt, manual wizard write) remain as integration-level tests that verify the full flow still works end-to-end.
|
|
136
|
+
They should not be removed — they test the caller's orchestration, not just the write logic.
|
|
137
|
+
3. No existing tests become redundant with this extraction.
|
|
138
|
+
|
|
139
|
+
## TDD Order
|
|
140
|
+
|
|
141
|
+
1. **Red → Green: `writeAgentFile` writes when target does not exist**
|
|
142
|
+
- New `test/ui/agent-file-writer.test.ts` with tests: writes file, reloads registry, notifies user, returns `true`.
|
|
143
|
+
- New `src/ui/agent-file-writer.ts` with the extracted function.
|
|
144
|
+
- Commit: `feat: extract writeAgentFile overwrite-guard function (#217)`
|
|
145
|
+
|
|
146
|
+
2. **Red → Green: `writeAgentFile` overwrite guard**
|
|
147
|
+
- Add tests: prompts for overwrite when file exists; writes and returns `true` when confirmed; does not write and returns `false` when declined.
|
|
148
|
+
- Implementation should already pass (the guard is part of the function body from step 1).
|
|
149
|
+
- Commit: `test: add overwrite-guard tests for writeAgentFile (#217)`
|
|
150
|
+
|
|
151
|
+
3. **Refactor: wire `ejectAgent` to use `writeAgentFile`**
|
|
152
|
+
- Replace the inline overwrite-guard block in `AgentConfigEditor.ejectAgent` with a call to `writeAgentFile`.
|
|
153
|
+
- Existing tests in `agent-config-editor.test.ts` must continue to pass.
|
|
154
|
+
- Commit: `refactor: use writeAgentFile in AgentConfigEditor.ejectAgent (#217)`
|
|
155
|
+
|
|
156
|
+
4. **Refactor: wire `showManualWizard` to use `writeAgentFile`**
|
|
157
|
+
- Replace the inline overwrite-guard block in `AgentCreationWizard.showManualWizard` with a call to `writeAgentFile`.
|
|
158
|
+
- Existing tests in `agent-creation-wizard.test.ts` must continue to pass.
|
|
159
|
+
- Commit: `refactor: use writeAgentFile in AgentCreationWizard.showManualWizard (#217)`
|
|
160
|
+
|
|
161
|
+
5. **Docs: update architecture**
|
|
162
|
+
- Add `agent-file-writer.ts` to the `ui/` layout listing in `docs/architecture/architecture.md`.
|
|
163
|
+
- Update the production-duplication section to mark the clone group as resolved.
|
|
164
|
+
- Commit: `docs: update architecture for writeAgentFile extraction (#217)`
|
|
165
|
+
|
|
166
|
+
## Risks and Mitigations
|
|
167
|
+
|
|
168
|
+
1. **Notification message format drift** — The extracted function uses `${label} ${targetPath}` for the notification.
|
|
169
|
+
Both current callers produce messages matching this pattern (`"Ejected ${name} to ${targetPath}"` and `"Created ${targetPath}"`).
|
|
170
|
+
The label parameter gives callers full control over the prefix, so no format is baked in.
|
|
171
|
+
2. **Existing test fragility** — Tests use `expect.stringContaining("already exists")` for the overwrite prompt, which is stable across the extraction.
|
|
172
|
+
No test rewrites needed.
|
|
173
|
+
|
|
174
|
+
## Open Questions
|
|
175
|
+
|
|
176
|
+
None — the issue's proposed change section is unambiguous and the dependency (#214) is resolved.
|
|
@@ -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.
|
|
@@ -41,3 +41,40 @@ All 60 test files pass; `pnpm run check`, `pnpm run lint`, and `pnpm fallow dead
|
|
|
41
41
|
- `flushPendingSteers` and `setupWorktree` extracted cleanly — each about 8 lines, no surprises.
|
|
42
42
|
- The `WorktreeCleanupResult` import needed to be added to the test file alongside the existing `WorktreeManager` import for the type annotation fix — minor but worth noting for the next engineer.
|
|
43
43
|
- Architecture doc updated: Step 3 entry now reflects `RunHandle` rather than the original `handleRunCompletion`/`handleRunError` proposal.
|
|
44
|
+
|
|
45
|
+
## Stage: Final Retrospective (2026-05-26T15:10:00Z)
|
|
46
|
+
|
|
47
|
+
### Session summary
|
|
48
|
+
|
|
49
|
+
Issue #216 was planned, implemented via 4 TDD steps (5 commits), shipped, CI verified (after a GitHub Actions outage), and released as `pi-subagents-v7.6.0`.
|
|
50
|
+
The final design replaced the original mechanical-extraction proposal with a `RunHandle` lifecycle object that eliminated mutable closure state from `startAgent`.
|
|
51
|
+
|
|
52
|
+
### Observations
|
|
53
|
+
|
|
54
|
+
#### What went well
|
|
55
|
+
|
|
56
|
+
- The user's two design redirections during planning ("What collaborators are still missing?"
|
|
57
|
+
and "Make the change that makes the change easy") transformed a mechanical extraction plan into a structural improvement.
|
|
58
|
+
The resulting `RunHandle` eliminated the root cause (mutable closure state) rather than just shortening the method.
|
|
59
|
+
- The prep-step pattern worked exactly as intended: `WorktreeState.performCleanup` (step 1) and `finalizeBackgroundRun` (step 3) made the `RunHandle` rewrite (step 4) straightforward.
|
|
60
|
+
Step 4's large edit landed cleanly with all 962 tests passing on the first run.
|
|
61
|
+
- Two Explore subagents dispatched during planning (reading collaborator files and checking `WorktreeState` details) gathered the right context efficiently — `RunResult` being already exported and `record.description` being available at cleanup time were both discovered this way and shaped the `RunHandle` interface.
|
|
62
|
+
|
|
63
|
+
#### What caused friction (agent side)
|
|
64
|
+
|
|
65
|
+
- `premature-convergence` — accepted the issue's proposed mechanical extraction (3 methods) at face value and spent analysis time on LOC arithmetic before the user redirected toward structural thinking.
|
|
66
|
+
Impact: two user redirections needed; no rework since no code was committed yet.
|
|
67
|
+
- `instruction-violation` (self-identified) — the testing skill says "run `pnpm run check` immediately after" changing a shared interface, but step 1 added `performCleanup` to `WorktreeState` without running `pnpm run check`.
|
|
68
|
+
The type error in the test helper (`makeWorktrees` default parameter needing `WorktreeCleanupResult` annotation) went undetected for 3 commits until step 4's `pnpm run check`.
|
|
69
|
+
Impact: added friction but no rework — fixed in the same commit.
|
|
70
|
+
|
|
71
|
+
#### What caused friction (user side)
|
|
72
|
+
|
|
73
|
+
- The user's design redirections were necessary and well-timed.
|
|
74
|
+
No friction from the user side — the two interventions were strategic and saved significant implementation effort.
|
|
75
|
+
|
|
76
|
+
### Diagnostic details
|
|
77
|
+
|
|
78
|
+
- **Model-performance correlation** — two Explore subagents ran on `claude-haiku-4-5`; appropriate for read-only codebase search (reading collaborator files, checking types and test patterns).
|
|
79
|
+
- **Feedback-loop gap analysis** — `pnpm run check` ran only after step 4 (the `RunHandle` commit); should have run after step 1 (`WorktreeState.performCleanup` is a shared interface change per the testing skill).
|
|
80
|
+
The gap allowed a type annotation error to persist for 3 commits.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 217
|
|
3
|
+
issue_title: "Extract overwrite guard from UI (Phase 13, Step 4)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #217 — Extract overwrite guard from UI
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-26T20:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced a 5-step TDD plan to extract the duplicated overwrite-guard + write + reload + notify pattern from `AgentConfigEditor.ejectAgent` and `AgentCreationWizard.showManualWizard` into a shared `writeAgentFile` function in a new `src/ui/agent-file-writer.ts` module.
|
|
13
|
+
Confirmed dependency #214 (closure-to-class conversion) is already closed.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- The `showGenerateWizard` overwrite guard was explicitly scoped out — it has different lifecycle semantics (spawned agent writes the file, post-write check is conditional).
|
|
18
|
+
This avoids a leaky abstraction with a discriminator parameter.
|
|
19
|
+
- Narrow ISP interfaces (`FileWriter`, `WriterUI`, `Reloadable`) keep the extracted function decoupled from the full `AgentFileOps` and `MenuUI` interfaces — 2/6 and 2/6 methods respectively.
|
|
20
|
+
- Both consumer call sites hold `this.fileOps` and `this.registry` as private fields and receive `ui` as a method parameter, so no constructor or wiring changes are needed.
|
|
21
|
+
- Existing tests in both consumer test files use `expect.stringContaining("already exists")` for overwrite prompts, which is stable across the extraction.
|
|
22
|
+
|
|
23
|
+
## Stage: Implementation — TDD (2026-05-26T20:40:00Z)
|
|
24
|
+
|
|
25
|
+
### Session summary
|
|
26
|
+
|
|
27
|
+
Implemented `writeAgentFile` in new `src/ui/agent-file-writer.ts`, replaced the inline overwrite-guard blocks in `AgentConfigEditor.ejectAgent` and `AgentCreationWizard.showManualWizard`, and updated the architecture doc.
|
|
28
|
+
All 5 plan steps completed across 4 commits (plan steps 1 and 2 folded into one).
|
|
29
|
+
Test count: 962 → 970 (+8 new tests in `test/ui/agent-file-writer.test.ts`).
|
|
30
|
+
|
|
31
|
+
### Observations
|
|
32
|
+
|
|
33
|
+
- Plan steps 1 and 2 naturally collapsed into a single commit — writing all 8 tests at once and implementing the full function body (including the guard) in one pass was cleaner than splitting them artificially.
|
|
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
|
+
- 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
|
+
- `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
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 (
|
|
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(
|
|
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
|
/**
|
|
@@ -10,6 +10,7 @@ import { join } from "node:path";
|
|
|
10
10
|
import type { AgentTypeRegistry } from "#src/config/agent-types";
|
|
11
11
|
import type { AgentConfig } from "#src/types";
|
|
12
12
|
import type { AgentFileOps } from "#src/ui/agent-file-ops";
|
|
13
|
+
import { writeAgentFile } from "#src/ui/agent-file-writer";
|
|
13
14
|
import type { MenuUI } from "#src/ui/agent-menu";
|
|
14
15
|
|
|
15
16
|
// ---- Pure helpers ----
|
|
@@ -142,17 +143,14 @@ export class AgentConfigEditor {
|
|
|
142
143
|
: this.personalAgentsDir;
|
|
143
144
|
|
|
144
145
|
const targetPath = join(targetDir, `${name}.md`);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
this.fileOps.write(targetPath, buildEjectContent(cfg));
|
|
154
|
-
this.registry.reload();
|
|
155
|
-
ui.notify(`Ejected ${name} to ${targetPath}`, "info");
|
|
146
|
+
await writeAgentFile(
|
|
147
|
+
this.fileOps,
|
|
148
|
+
ui,
|
|
149
|
+
this.registry,
|
|
150
|
+
targetPath,
|
|
151
|
+
buildEjectContent(cfg),
|
|
152
|
+
`Ejected ${name} to`,
|
|
153
|
+
);
|
|
156
154
|
}
|
|
157
155
|
|
|
158
156
|
private async disableAgent(ui: MenuUI, name: string): Promise<void> {
|
|
@@ -11,6 +11,7 @@ import { BUILTIN_TOOL_NAMES } from "#src/config/agent-types";
|
|
|
11
11
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
12
12
|
import type { AgentRecord } from "#src/types";
|
|
13
13
|
import type { AgentFileOps } from "#src/ui/agent-file-ops";
|
|
14
|
+
import { writeAgentFile } from "#src/ui/agent-file-writer";
|
|
14
15
|
import type { MenuUI } from "#src/ui/agent-menu";
|
|
15
16
|
|
|
16
17
|
// ---- Deps interface ----
|
|
@@ -233,16 +234,6 @@ ${systemPrompt}
|
|
|
233
234
|
|
|
234
235
|
const targetPath = join(targetDir, `${name}.md`);
|
|
235
236
|
|
|
236
|
-
|
|
237
|
-
const overwrite = await ui.confirm(
|
|
238
|
-
"Overwrite",
|
|
239
|
-
`${targetPath} already exists. Overwrite?`,
|
|
240
|
-
);
|
|
241
|
-
if (!overwrite) return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
this.fileOps.write(targetPath, content);
|
|
245
|
-
this.registry.reload();
|
|
246
|
-
ui.notify(`Created ${targetPath}`, "info");
|
|
237
|
+
await writeAgentFile(this.fileOps, ui, this.registry, targetPath, content, "Created");
|
|
247
238
|
}
|
|
248
239
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-file-writer.ts — Shared overwrite-guard + write + reload + notify helper.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from AgentConfigEditor.ejectAgent and AgentCreationWizard.showManualWizard
|
|
5
|
+
* to eliminate the duplicated 20-line pattern. Uses narrow interfaces (ISP) so callers
|
|
6
|
+
* are not forced to depend on the full AgentFileOps or MenuUI shapes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ---- Narrow interfaces ----
|
|
10
|
+
|
|
11
|
+
/** Minimal file operations needed by the overwrite-guard-and-write pattern. */
|
|
12
|
+
export interface FileWriter {
|
|
13
|
+
exists(filePath: string): boolean;
|
|
14
|
+
write(filePath: string, content: string): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Minimal UI needed by the overwrite-guard-and-write pattern. */
|
|
18
|
+
export interface WriterUI {
|
|
19
|
+
confirm(title: string, message: string): Promise<boolean>;
|
|
20
|
+
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Registry that can be reloaded after file changes. */
|
|
24
|
+
export interface Reloadable {
|
|
25
|
+
reload(): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---- Function ----
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Write an agent `.md` file with an overwrite guard.
|
|
32
|
+
*
|
|
33
|
+
* If `targetPath` already exists, prompts the user for confirmation before writing.
|
|
34
|
+
* On write: reloads the registry and notifies the user as `"${label} ${targetPath}"`.
|
|
35
|
+
*
|
|
36
|
+
* Returns `true` if the file was written, `false` if the user declined to overwrite.
|
|
37
|
+
*/
|
|
38
|
+
export async function writeAgentFile(
|
|
39
|
+
fileOps: FileWriter,
|
|
40
|
+
ui: WriterUI,
|
|
41
|
+
registry: Reloadable,
|
|
42
|
+
targetPath: string,
|
|
43
|
+
content: string,
|
|
44
|
+
label: string,
|
|
45
|
+
): Promise<boolean> {
|
|
46
|
+
if (fileOps.exists(targetPath)) {
|
|
47
|
+
const overwrite = await ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
|
|
48
|
+
if (!overwrite) return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fileOps.write(targetPath, content);
|
|
52
|
+
registry.reload();
|
|
53
|
+
ui.notify(`${label} ${targetPath}`, "info");
|
|
54
|
+
return true;
|
|
55
|
+
}
|