@gotgenes/pi-subagents 6.5.0 → 6.7.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 +30 -0
- package/docs/architecture/architecture.md +14 -14
- package/docs/plans/0110-agent-activity-tracker.md +297 -0
- package/docs/plans/0118-settings-manager-apply-methods.md +271 -0
- package/docs/retro/0109-extract-settings-manager.md +55 -0
- package/docs/retro/0118-settings-manager-apply-methods.md +40 -0
- package/package.json +1 -1
- package/src/index.ts +2 -1
- package/src/notification.ts +3 -3
- package/src/runtime.ts +3 -2
- package/src/settings.ts +31 -1
- package/src/tools/agent-tool.ts +7 -20
- package/src/ui/agent-activity-tracker.ts +108 -0
- package/src/ui/agent-menu.ts +15 -24
- package/src/ui/agent-widget.ts +4 -17
- package/src/ui/conversation-viewer.ts +3 -3
- package/src/ui/ui-observer.ts +16 -23
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 118
|
|
3
|
+
issue_title: "refactor(pi-subagents): SettingsManager apply methods — eliminate cross-collaborator orchestration"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SettingsManager apply methods — eliminate cross-collaborator orchestration
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`showSettings` in `agent-menu.ts` orchestrates across two collaborators when changing a setting — it mutates `settings` properties directly (output arguments), separately pokes `manager.notifyConcurrencyChanged()`, then calls `settings.saveAndNotify()`.
|
|
11
|
+
The menu knows too much about the consequence chain of a settings change.
|
|
12
|
+
|
|
13
|
+
This is a Law of Demeter / Tell-Don't-Ask violation: the menu should *tell* settings what the user wants, not coordinate the mechanics of persistence and queue drain.
|
|
14
|
+
|
|
15
|
+
## Goals
|
|
16
|
+
|
|
17
|
+
- Add `applyMaxConcurrent(n)`, `applyDefaultMaxTurns(n)`, `applyGraceTurns(n)` methods to `SettingsManager` that own the full consequence chain: normalize → set in memory → notify interested parties → persist → emit lifecycle event → return toast.
|
|
18
|
+
- Accept an optional `onMaxConcurrentChanged` callback in `SettingsManager` constructor deps, wired to `manager.notifyConcurrencyChanged()` at init.
|
|
19
|
+
- Narrow `AgentMenuSettings` — replace writable property setters and `saveAndNotify` with 3 read-only getters and 3 apply methods.
|
|
20
|
+
- Remove `notifyConcurrencyChanged` from `AgentMenuManager` — the menu no longer needs to know about the manager for settings changes.
|
|
21
|
+
- This is a non-breaking refactoring — no public API surface changes.
|
|
22
|
+
|
|
23
|
+
## Non-Goals
|
|
24
|
+
|
|
25
|
+
- Narrowing `AgentToolDeps` or `AgentMenuDeps` further (#114) — that is a separate issue.
|
|
26
|
+
- Changing the persistence format or the global-vs-project merge strategy.
|
|
27
|
+
- Removing `saveAndNotify` from `SettingsManager` — it remains as a public method; the apply methods delegate to it internally.
|
|
28
|
+
- Removing the property setters from `SettingsManager` — they remain for `load()` and direct test use; only the `AgentMenuSettings` interface narrows.
|
|
29
|
+
|
|
30
|
+
## Background
|
|
31
|
+
|
|
32
|
+
### Current flow (max concurrency example)
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
showSettings (agent-menu.ts)
|
|
36
|
+
├── deps.settings.maxConcurrent = n ← output argument
|
|
37
|
+
├── deps.manager.notifyConcurrencyChanged()← cross-collaborator orchestration
|
|
38
|
+
└── notifyApplied(ctx, message)
|
|
39
|
+
└── deps.settings.saveAndNotify(msg) ← separate persist + emit call
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Target flow
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
showSettings (agent-menu.ts)
|
|
46
|
+
└── deps.settings.applyMaxConcurrent(n) ← one call, toast returned
|
|
47
|
+
├── this.maxConcurrent = n ← internal set
|
|
48
|
+
├── this.onMaxConcurrentChanged?.() ← callback to manager
|
|
49
|
+
└── this.saveAndNotify(message) ← internal persist + emit
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Module map (affected files only)
|
|
53
|
+
|
|
54
|
+
| Module | Current role | Change |
|
|
55
|
+
| ---------------------- | ------------------------------------------------------ | ------------------------------------------------------------- |
|
|
56
|
+
| `src/settings.ts` | `SettingsManager` class with setters, `saveAndNotify` | Add `onMaxConcurrentChanged` callback; add 3 `apply*` methods |
|
|
57
|
+
| `src/ui/agent-menu.ts` | `showSettings` orchestrates across manager + settings | Simplify to single apply call per setting |
|
|
58
|
+
| `src/agent-manager.ts` | `notifyConcurrencyChanged()` public method | No change — still called via callback |
|
|
59
|
+
| `src/index.ts` | Wires `notifyConcurrencyChanged` on `AgentMenuManager` | Wire `onMaxConcurrentChanged` on settings constructor instead |
|
|
60
|
+
|
|
61
|
+
### Architecture reference
|
|
62
|
+
|
|
63
|
+
Follow-up to Phase 7, Step A2 (#109, closed/implemented).
|
|
64
|
+
Sequenced before D2 (#114, open).
|
|
65
|
+
|
|
66
|
+
### Applicable constraints
|
|
67
|
+
|
|
68
|
+
- Law of Demeter — eliminate cross-collaborator reach-through (code-design skill).
|
|
69
|
+
- No output arguments — the menu should not write into received deps (code-design skill).
|
|
70
|
+
- Dependency inversion — consumers accept narrow interfaces (code-design skill).
|
|
71
|
+
- One concern per file — `SettingsManager` already owns the concern; this deepens encapsulation.
|
|
72
|
+
|
|
73
|
+
## Design Overview
|
|
74
|
+
|
|
75
|
+
### SettingsManager constructor deps change
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
constructor(deps: {
|
|
79
|
+
emit: SettingsEmit;
|
|
80
|
+
cwd: string;
|
|
81
|
+
onMaxConcurrentChanged?: () => void; // ← new
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The callback is stored as a private field and called from `applyMaxConcurrent` only.
|
|
86
|
+
|
|
87
|
+
### Apply methods
|
|
88
|
+
|
|
89
|
+
Each method normalizes the input, sets the in-memory value, calls any interested-party callback, and delegates to `saveAndNotify` for persist + emit + toast:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
applyMaxConcurrent(n: number): { message: string; level: "info" | "warning" } {
|
|
93
|
+
this.maxConcurrent = n; // setter normalizes (max(1, n))
|
|
94
|
+
this.onMaxConcurrentChanged?.();
|
|
95
|
+
return this.saveAndNotify(`Max concurrency set to ${this.maxConcurrent}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
applyDefaultMaxTurns(n: number): { message: string; level: "info" | "warning" } {
|
|
99
|
+
this.defaultMaxTurns = n === 0 ? undefined : n; // setter normalizes
|
|
100
|
+
const label = this.defaultMaxTurns == null ? "unlimited" : String(this.defaultMaxTurns);
|
|
101
|
+
return this.saveAndNotify(`Default max turns set to ${label}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
applyGraceTurns(n: number): { message: string; level: "info" | "warning" } {
|
|
105
|
+
this.graceTurns = n; // setter normalizes (max(1, n))
|
|
106
|
+
return this.saveAndNotify(`Grace turns set to ${this.graceTurns}`);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The toast message uses the *post-normalization* value (e.g., `max(1, n)`) so the user sees what was actually applied.
|
|
111
|
+
|
|
112
|
+
### Consumer call-site sketch (agent-menu.ts)
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// Max concurrency — before:
|
|
116
|
+
deps.settings.maxConcurrent = n;
|
|
117
|
+
deps.manager.notifyConcurrencyChanged();
|
|
118
|
+
notifyApplied(ctx, `Max concurrency set to ${n}`);
|
|
119
|
+
|
|
120
|
+
// Max concurrency — after:
|
|
121
|
+
const toast = deps.settings.applyMaxConcurrent(n);
|
|
122
|
+
ctx.ui.notify(toast.message, toast.level);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Three property writes + one cross-collaborator call + one persist call → one method call.
|
|
126
|
+
The menu never touches the manager for settings changes.
|
|
127
|
+
|
|
128
|
+
### Narrowed AgentMenuSettings interface
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
export interface AgentMenuSettings {
|
|
132
|
+
readonly maxConcurrent: number;
|
|
133
|
+
readonly defaultMaxTurns: number | undefined;
|
|
134
|
+
readonly graceTurns: number;
|
|
135
|
+
applyMaxConcurrent(n: number): { message: string; level: "info" | "warning" };
|
|
136
|
+
applyDefaultMaxTurns(n: number): { message: string; level: "info" | "warning" };
|
|
137
|
+
applyGraceTurns(n: number): { message: string; level: "info" | "warning" };
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Narrowed AgentMenuManager interface
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
export interface AgentMenuManager {
|
|
145
|
+
listAgents: () => AgentRecord[];
|
|
146
|
+
getRecord: (id: string) => AgentRecord | undefined;
|
|
147
|
+
spawnAndWait: (...) => Promise<AgentRecord>;
|
|
148
|
+
// notifyConcurrencyChanged removed — settings owns the callback
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### index.ts wiring change
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// Before:
|
|
156
|
+
const settings = new SettingsManager({ emit, cwd });
|
|
157
|
+
// ... later in menu deps:
|
|
158
|
+
manager: { ..., notifyConcurrencyChanged: () => manager.notifyConcurrencyChanged() },
|
|
159
|
+
|
|
160
|
+
// After:
|
|
161
|
+
const settings = new SettingsManager({
|
|
162
|
+
emit,
|
|
163
|
+
cwd,
|
|
164
|
+
onMaxConcurrentChanged: () => manager.notifyConcurrencyChanged(),
|
|
165
|
+
});
|
|
166
|
+
// ... menu deps no longer includes notifyConcurrencyChanged
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The closure captures `manager` by reference — safe because the callback is never invoked before `manager` is constructed.
|
|
170
|
+
|
|
171
|
+
## Module-Level Changes
|
|
172
|
+
|
|
173
|
+
### `src/settings.ts`
|
|
174
|
+
|
|
175
|
+
- **Add** `onMaxConcurrentChanged?: () => void` to constructor deps.
|
|
176
|
+
- **Add** `applyMaxConcurrent(n)`, `applyDefaultMaxTurns(n)`, `applyGraceTurns(n)` methods.
|
|
177
|
+
- **Keep** property setters, `saveAndNotify`, `load`, `snapshot` — apply methods delegate to them.
|
|
178
|
+
|
|
179
|
+
### `src/ui/agent-menu.ts`
|
|
180
|
+
|
|
181
|
+
- **Change** `AgentMenuSettings`: replace writable properties + `saveAndNotify` with readonly properties + 3 apply methods.
|
|
182
|
+
- **Change** `AgentMenuManager`: remove `notifyConcurrencyChanged`.
|
|
183
|
+
- **Change** `showSettings`: replace 3-step orchestration with single apply call per setting.
|
|
184
|
+
- **Remove** `notifyApplied` helper — no longer needed; each branch calls `ctx.ui.notify(toast.message, toast.level)` directly.
|
|
185
|
+
|
|
186
|
+
### `src/index.ts`
|
|
187
|
+
|
|
188
|
+
- **Add** `onMaxConcurrentChanged` to `SettingsManager` constructor call.
|
|
189
|
+
- **Remove** `notifyConcurrencyChanged` from the menu's manager dep.
|
|
190
|
+
|
|
191
|
+
### `src/agent-manager.ts`
|
|
192
|
+
|
|
193
|
+
- **No change** — `notifyConcurrencyChanged()` remains as a public method, now called via callback instead of menu.
|
|
194
|
+
|
|
195
|
+
## Test Impact Analysis
|
|
196
|
+
|
|
197
|
+
### New unit tests enabled
|
|
198
|
+
|
|
199
|
+
- **Apply method integration**: construct `SettingsManager` with an `onMaxConcurrentChanged` spy → call `applyMaxConcurrent(n)` → verify the spy was called, value was set, `saveAndNotify` persisted, and the returned toast is correct.
|
|
200
|
+
Previously impossible because the consequence chain was spread across the menu and two collaborators.
|
|
201
|
+
- **Toast message accuracy**: verify that apply methods use post-normalization values in the toast (e.g., `applyMaxConcurrent(0)` sets to 1 and reports "set to 1", not "set to 0").
|
|
202
|
+
- **Callback not invoked for non-concurrency settings**: verify `onMaxConcurrentChanged` is *not* called when `applyDefaultMaxTurns` or `applyGraceTurns` is used.
|
|
203
|
+
|
|
204
|
+
### Existing tests that become simpler
|
|
205
|
+
|
|
206
|
+
- `agent-menu.test.ts` settings tests: currently assert 3 side effects per setting (property mutation, `notifyConcurrencyChanged` call, `saveAndNotify` call).
|
|
207
|
+
After: assert a single `apply*` call with the correct argument.
|
|
208
|
+
The mock object shrinks (no writable setters, no `saveAndNotify`).
|
|
209
|
+
|
|
210
|
+
### Existing tests that must stay
|
|
211
|
+
|
|
212
|
+
- `settings.test.ts` — `saveAndNotify()` tests stay; the apply methods delegate to it.
|
|
213
|
+
- `settings.test.ts` — property setter normalization tests stay; apply methods delegate to setters.
|
|
214
|
+
- `agent-menu.test.ts` — settings navigation tests stay; only the assertions change.
|
|
215
|
+
- `agent-manager.test.ts` — `notifyConcurrencyChanged` / drain-queue tests stay unchanged.
|
|
216
|
+
|
|
217
|
+
## TDD Order
|
|
218
|
+
|
|
219
|
+
### Cycle 1: Add `onMaxConcurrentChanged` callback to SettingsManager constructor
|
|
220
|
+
|
|
221
|
+
1. Red: test that constructing with `onMaxConcurrentChanged` stores the callback (verified indirectly in cycle 2).
|
|
222
|
+
Test that constructing without it does not throw.
|
|
223
|
+
2. Green: add optional `onMaxConcurrentChanged` to constructor deps, store as private field.
|
|
224
|
+
3. Commit: `feat: accept onMaxConcurrentChanged callback in SettingsManager constructor`
|
|
225
|
+
|
|
226
|
+
### Cycle 2: Add `applyMaxConcurrent` method
|
|
227
|
+
|
|
228
|
+
1. Red: test `applyMaxConcurrent(8)` — sets `maxConcurrent` to 8, calls `onMaxConcurrentChanged` spy, persists, emits event, returns info toast with "Max concurrency set to 8".
|
|
229
|
+
Test `applyMaxConcurrent(0)` — normalizes to 1, toast says "set to 1".
|
|
230
|
+
Test without callback — no throw, still persists and returns toast.
|
|
231
|
+
2. Green: implement `applyMaxConcurrent`.
|
|
232
|
+
3. Commit: `feat: add SettingsManager.applyMaxConcurrent method`
|
|
233
|
+
|
|
234
|
+
### Cycle 3: Add `applyDefaultMaxTurns` and `applyGraceTurns` methods
|
|
235
|
+
|
|
236
|
+
1. Red: test `applyDefaultMaxTurns(0)` — sets to unlimited, toast says "unlimited".
|
|
237
|
+
Test `applyDefaultMaxTurns(10)` — sets to 10, toast says "set to 10".
|
|
238
|
+
Test `applyGraceTurns(3)` — sets to 3, toast says "set to 3".
|
|
239
|
+
Test that neither calls `onMaxConcurrentChanged`.
|
|
240
|
+
2. Green: implement both methods.
|
|
241
|
+
3. Commit: `feat: add SettingsManager.applyDefaultMaxTurns and applyGraceTurns methods`
|
|
242
|
+
|
|
243
|
+
### Cycle 4: Narrow `AgentMenuSettings` and `AgentMenuManager`, simplify `showSettings`
|
|
244
|
+
|
|
245
|
+
1. Red: update `makeDeps` in `agent-menu.test.ts` — replace writable settings properties + `saveAndNotify` with readonly properties + 3 `apply*` mocks; remove `notifyConcurrencyChanged` from manager mock.
|
|
246
|
+
Update assertions: `expect(deps.settings.applyMaxConcurrent).toHaveBeenCalledWith(8)` instead of checking property + `saveAndNotify` + `notifyConcurrencyChanged`.
|
|
247
|
+
2. Green: update `AgentMenuSettings` interface (readonly getters + apply methods), update `AgentMenuManager` (remove `notifyConcurrencyChanged`), rewrite `showSettings` to use apply methods, remove `notifyApplied` helper.
|
|
248
|
+
3. Run `pnpm run check`.
|
|
249
|
+
4. Commit: `refactor: simplify showSettings to use SettingsManager apply methods`
|
|
250
|
+
|
|
251
|
+
### Cycle 5: Wire `onMaxConcurrentChanged` in index.ts
|
|
252
|
+
|
|
253
|
+
1. Update `SettingsManager` constructor in `index.ts` — add `onMaxConcurrentChanged: () => manager.notifyConcurrencyChanged()`.
|
|
254
|
+
2. Remove `notifyConcurrencyChanged` from the menu's manager dep object.
|
|
255
|
+
3. Run full test suite.
|
|
256
|
+
4. Commit: `refactor: wire onMaxConcurrentChanged callback in extension init (#118)`
|
|
257
|
+
|
|
258
|
+
## Risks and Mitigations
|
|
259
|
+
|
|
260
|
+
| Risk | Mitigation |
|
|
261
|
+
| ---------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
262
|
+
| Closure ordering: `onMaxConcurrentChanged` references `manager` before it's constructed | The closure captures by reference; `manager` is assigned before any runtime invocation of the callback. Verified by the `index.ts` construction order (`settings` → `manager` → menu wiring). |
|
|
263
|
+
| Toast message drift: apply methods generate messages internally, so changes require updating `SettingsManager` instead of the menu | Each method has exactly one message template; the coupling is intentional — the owner of the setting knows how to describe the change. |
|
|
264
|
+
| Apply methods duplicate the 3-step pattern (set → callback → save) | The duplication is incidental across 3 settings — extracting a shared helper would need a discriminator for the callback, adding complexity for 3 call sites. |
|
|
265
|
+
| `saveAndNotify` remains public but is no longer in `AgentMenuSettings` | It's still useful for programmatic callers and tests; keeping it public is intentional. |
|
|
266
|
+
|
|
267
|
+
## Open Questions
|
|
268
|
+
|
|
269
|
+
- Should the property setters on `SettingsManager` be demoted to `private` (with only apply methods for external mutation)?
|
|
270
|
+
Defer — `load()` uses them internally, and narrowing is done via the `AgentMenuSettings` interface.
|
|
271
|
+
If a future consumer needs direct set access, the setters are there.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 109
|
|
3
|
+
issue_title: "refactor(pi-subagents): extract SettingsManager class"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #109 — extract SettingsManager class
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-21T17:30:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned and implemented the `SettingsManager` class extraction across 8 TDD cycles plus doc updates.
|
|
13
|
+
The class owns `defaultMaxTurns`, `graceTurns`, and `maxConcurrent` with normalizing property accessors, a `load()` method for merged config, and `saveAndNotify()` for persistence + lifecycle events.
|
|
14
|
+
Six settings-related callback fields in `AgentMenuDeps` collapsed to a single `settings` collaborator (13 → 8 fields), and `SettingsAppliers`, `applySettings`, `applyAndEmitLoaded`, `saveAndEmitChanged` were removed.
|
|
15
|
+
A follow-up issue (#118) was filed for a LoD/Tell-Don't-Ask violation the user identified in the post-implementation review.
|
|
16
|
+
|
|
17
|
+
### Observations
|
|
18
|
+
|
|
19
|
+
#### What went well
|
|
20
|
+
|
|
21
|
+
- The lift-and-shift TDD approach worked cleanly: the new class was built and tested in isolation (cycles 1–2), consumers migrated one at a time (cycles 3–5), wiring consolidated (cycle 6), and old code removed last (cycles 7–8).
|
|
22
|
+
Each commit left the repo in a valid state.
|
|
23
|
+
- The user's design critique (LoD / Tell-Don't-Ask on `deps.settings.saveAndNotify()` orchestration) was sharp and led to a concrete follow-up issue (#118) with a clear fix.
|
|
24
|
+
The session handled it well — acknowledged, designed the fix, filed the issue, updated the architecture doc, and stopped without scope-creeping into implementation.
|
|
25
|
+
|
|
26
|
+
#### What caused friction (agent side)
|
|
27
|
+
|
|
28
|
+
1. `wrong-abstraction` — The plan did not anticipate that changing `AgentMenuDeps` (cycle 3) would immediately break `index.ts` type-checking.
|
|
29
|
+
Each interface change in cycles 3, 4, and 5 required a same-commit bridge fix in `index.ts` to keep `pnpm run check` clean.
|
|
30
|
+
The plan's separation of "migrate consumers" (cycles 3–5) from "wire in index.ts" (cycle 6) was too coarse — interface changes propagate to the call site immediately.
|
|
31
|
+
Impact: three unplanned bridge edits in `index.ts`, each small but requiring context-switching mid-cycle.
|
|
32
|
+
|
|
33
|
+
2. `missing-context` — The `sed` command in cycle 5 (`sed -i '' 's/maxConcurrent: 1,/getMaxConcurrent: () => 1,/g'`) missed two call sites where `maxConcurrent: 1` had no trailing comma (end of object literal).
|
|
34
|
+
A `grep` check after the `sed` would have caught this immediately.
|
|
35
|
+
Impact: two tests failed unexpectedly; required a follow-up read + manual edit before the cycle could complete.
|
|
36
|
+
|
|
37
|
+
3. `missing-context` — The Edit tool failed on `runtime.ts` because the file uses `─` (U+2500, BOX DRAWINGS LIGHT HORIZONTAL) in section separators, not `—` (U+2014, EM DASH).
|
|
38
|
+
The Unicode characters looked identical in the terminal, and the Edit tool's exact-match requirement meant the mismatch was silent until the replacement failed.
|
|
39
|
+
Impact: three failed Edit attempts before falling back to a Python script for the replacement.
|
|
40
|
+
This is the same class of issue seen in previous sessions with Unicode characters in source files.
|
|
41
|
+
|
|
42
|
+
4. `premature-convergence` — The plan designed `SettingsManager` as a data holder with persistence methods but didn't consider whether the menu should orchestrate across `settings` and `manager` or whether `SettingsManager` should own the full consequence chain.
|
|
43
|
+
The user caught this as a LoD/Tell-Don't-Ask violation in post-implementation review.
|
|
44
|
+
Impact: no rework (filed as follow-up #118), but the design could have been better from the start if the plan had applied the design-review checklist's LoD check to the proposed `showSettings` interaction pattern.
|
|
45
|
+
|
|
46
|
+
#### What caused friction (user side)
|
|
47
|
+
|
|
48
|
+
- The user's LoD critique was well-timed — after implementation was complete, avoiding mid-stream rework.
|
|
49
|
+
If the critique had surfaced during planning (e.g., by the agent applying the design-review checklist to the proposed consumer interaction pattern), the follow-up issue might have been part of the original scope.
|
|
50
|
+
This is an opportunity for the planning step to simulate the consumer's call sites before finalizing the design, not just the class interface.
|
|
51
|
+
|
|
52
|
+
### Changes made
|
|
53
|
+
|
|
54
|
+
1. `.pi/prompts/plan-issue.md` — Added consumer call-site sketch heuristic to the "Design Overview" section: when a new collaborator is introduced, sketch 3–5 lines of consumer pseudocode to verify Tell-Don't-Ask and LoD.
|
|
55
|
+
2. `.pi/skills/testing/SKILL.md` — Added TDD planning rule: when a step changes an interface with a single call site, the step must include updating that call site (type checker enforces co-location).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 118
|
|
3
|
+
issue_title: "refactor(pi-subagents): SettingsManager apply methods — eliminate cross-collaborator orchestration"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #118 — SettingsManager apply methods
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-21T21:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned and implemented 3 `apply*` methods on `SettingsManager` (`applyMaxConcurrent`, `applyDefaultMaxTurns`, `applyGraceTurns`) across 5 TDD cycles plus doc updates, released as `pi-subagents-v6.6.0`.
|
|
13
|
+
Each method owns the full consequence chain (normalize → set → callback → persist → emit → return toast), eliminating the LoD/Tell-Don't-Ask violation in `showSettings` that was identified during the #109 retro.
|
|
14
|
+
`notifyConcurrencyChanged` was removed from `AgentMenuManager`; the menu no longer coordinates between settings and the agent manager.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
#### What went well
|
|
19
|
+
|
|
20
|
+
- **Retro-driven improvement validated.**
|
|
21
|
+
Issue #118 was filed during the #109 retro as a LoD/Tell-Don't-Ask follow-up, and the plan-issue prompt's consumer call-site sketch heuristic (added in #109's retro) was already in the plan template.
|
|
22
|
+
The plan for #118 included concrete before/after call-site sketches that made the design unambiguous — no `ask-user` decision needed.
|
|
23
|
+
- **Interface-then-wiring TDD order worked cleanly.**
|
|
24
|
+
The #109 retro noted that interface changes propagate to `index.ts` immediately, forcing unplanned bridge edits.
|
|
25
|
+
This time the plan accounted for it: Cycle 4 committed only menu files (leaving a known `index.ts` type error), and Cycle 5 fixed the wiring in a separate commit.
|
|
26
|
+
The intermediate type error was contained and expected.
|
|
27
|
+
- **`defaultMaxTurns` branch consolidation.**
|
|
28
|
+
During Cycle 4, the separate `n === 0` and `n >= 1` branches in `showSettings` were consolidated to a single `n >= 0` check, since `applyDefaultMaxTurns` handles the 0→unlimited mapping internally.
|
|
29
|
+
This was a minor but correct simplification that emerged naturally from the Tell-Don't-Ask refactor.
|
|
30
|
+
|
|
31
|
+
#### What caused friction (agent side)
|
|
32
|
+
|
|
33
|
+
- No material friction.
|
|
34
|
+
All 5 TDD cycles completed without rework, failed edits, or unexpected test failures.
|
|
35
|
+
The plan was tight and the issue's "Proposed change" section was unambiguous.
|
|
36
|
+
|
|
37
|
+
#### What caused friction (user side)
|
|
38
|
+
|
|
39
|
+
- No material friction observed.
|
|
40
|
+
The session ran end-to-end (plan → implement → ship → release) without user intervention.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -56,9 +56,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
// Settings: owns all three in-memory values and handles load/save/emit.
|
|
59
|
+
// onMaxConcurrentChanged is wired after manager is constructed (closure captures by reference).
|
|
59
60
|
const settings = new SettingsManager({
|
|
60
61
|
emit: (event, payload) => pi.events.emit(event, payload),
|
|
61
62
|
cwd: process.cwd(),
|
|
63
|
+
onMaxConcurrentChanged: () => manager.notifyConcurrencyChanged(),
|
|
62
64
|
});
|
|
63
65
|
settings.load();
|
|
64
66
|
|
|
@@ -222,7 +224,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
222
224
|
listAgents: () => manager.listAgents(),
|
|
223
225
|
getRecord: (id) => manager.getRecord(id),
|
|
224
226
|
spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(ctx, type, prompt, opts),
|
|
225
|
-
notifyConcurrencyChanged: () => manager.notifyConcurrencyChanged(),
|
|
226
227
|
},
|
|
227
228
|
registry,
|
|
228
229
|
agentActivity: runtime.agentActivity,
|
package/src/notification.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { debugLog } from "./debug.js";
|
|
2
2
|
import type { AgentRecord, NotificationDetails } from "./types.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { AgentActivityTracker } from "./ui/agent-activity-tracker.js";
|
|
4
4
|
import { getLifetimeTotal, getSessionContextPercent } from "./usage.js";
|
|
5
5
|
|
|
6
6
|
// ---- Pure helpers (exported for unit testing) ----
|
|
@@ -60,7 +60,7 @@ export function formatTaskNotification(record: AgentRecord, resultMaxLen: number
|
|
|
60
60
|
export function buildNotificationDetails(
|
|
61
61
|
record: AgentRecord,
|
|
62
62
|
resultMaxLen: number,
|
|
63
|
-
activity?:
|
|
63
|
+
activity?: AgentActivityTracker,
|
|
64
64
|
): NotificationDetails {
|
|
65
65
|
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
66
66
|
|
|
@@ -113,7 +113,7 @@ export interface NotificationDeps {
|
|
|
113
113
|
msg: { customType: string; content: string; display: boolean; details?: unknown },
|
|
114
114
|
opts?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
115
115
|
) => void;
|
|
116
|
-
agentActivity: Map<string,
|
|
116
|
+
agentActivity: Map<string, AgentActivityTracker>;
|
|
117
117
|
markFinished: (id: string) => void;
|
|
118
118
|
updateWidget: () => void;
|
|
119
119
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* Follows the same pattern as pi-permission-system's ExtensionRuntime.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type { AgentActivityTracker } from "./ui/agent-activity-tracker.js";
|
|
10
|
+
import type { AgentWidget, UICtx } from "./ui/agent-widget.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Narrow config subset read by AgentManager when constructing RunOptions.
|
|
@@ -31,7 +32,7 @@ export class SubagentRuntime {
|
|
|
31
32
|
* Per-agent live activity state shared across the notification system,
|
|
32
33
|
* widget, and tool handlers. The Map itself is never replaced.
|
|
33
34
|
*/
|
|
34
|
-
readonly agentActivity: Map<string,
|
|
35
|
+
readonly agentActivity: Map<string, AgentActivityTracker> = new Map();
|
|
35
36
|
/**
|
|
36
37
|
* Persistent widget reference. Null until constructed after AgentManager.
|
|
37
38
|
* Delegation methods use optional chaining so callers never need `widget!`.
|
package/src/settings.ts
CHANGED
|
@@ -34,10 +34,12 @@ export class SettingsManager {
|
|
|
34
34
|
|
|
35
35
|
private readonly emit: SettingsEmit;
|
|
36
36
|
private readonly cwd: string;
|
|
37
|
+
private readonly onMaxConcurrentChanged: (() => void) | undefined;
|
|
37
38
|
|
|
38
|
-
constructor(deps: { emit: SettingsEmit; cwd: string }) {
|
|
39
|
+
constructor(deps: { emit: SettingsEmit; cwd: string; onMaxConcurrentChanged?: () => void }) {
|
|
39
40
|
this.emit = deps.emit;
|
|
40
41
|
this.cwd = deps.cwd;
|
|
42
|
+
this.onMaxConcurrentChanged = deps.onMaxConcurrentChanged;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
// ── defaultMaxTurns: 0 or undefined → unlimited (undefined); else max(1, n) ──
|
|
@@ -102,6 +104,34 @@ export class SettingsManager {
|
|
|
102
104
|
};
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Set maxConcurrent, notify interested parties, persist, and return the toast.
|
|
109
|
+
* Owns the full consequence chain so callers just say what they want.
|
|
110
|
+
*/
|
|
111
|
+
applyMaxConcurrent(n: number): { message: string; level: "info" | "warning" } {
|
|
112
|
+
this.maxConcurrent = n; // setter normalizes: max(1, n)
|
|
113
|
+
this.onMaxConcurrentChanged?.();
|
|
114
|
+
return this.saveAndNotify(`Max concurrency set to ${this.maxConcurrent}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Set defaultMaxTurns, persist, and return the toast.
|
|
119
|
+
* Pass 0 for unlimited (maps to undefined internally).
|
|
120
|
+
*/
|
|
121
|
+
applyDefaultMaxTurns(n: number): { message: string; level: "info" | "warning" } {
|
|
122
|
+
this.defaultMaxTurns = n === 0 ? undefined : n; // setter normalizes further
|
|
123
|
+
const label = this.defaultMaxTurns == null ? "unlimited" : String(this.defaultMaxTurns);
|
|
124
|
+
return this.saveAndNotify(`Default max turns set to ${label}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Set graceTurns, persist, and return the toast.
|
|
129
|
+
*/
|
|
130
|
+
applyGraceTurns(n: number): { message: string; level: "info" | "warning" } {
|
|
131
|
+
this.graceTurns = n; // setter normalizes: max(1, n)
|
|
132
|
+
return this.saveAndNotify(`Grace turns set to ${this.graceTurns}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
105
135
|
/**
|
|
106
136
|
* Persist the current snapshot, emit `subagents:settings_changed`,
|
|
107
137
|
* and return the toast the UI should display.
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -8,8 +8,8 @@ import { resolveAgentInvocationConfig } from "../invocation-config.js";
|
|
|
8
8
|
import { resolveInvocationModel } from "../model-resolver.js";
|
|
9
9
|
|
|
10
10
|
import type { AgentInvocation, AgentRecord, SubagentType } from "../types.js";
|
|
11
|
+
import { AgentActivityTracker } from "../ui/agent-activity-tracker.js";
|
|
11
12
|
import {
|
|
12
|
-
type AgentActivity,
|
|
13
13
|
type AgentDetails,
|
|
14
14
|
buildInvocationTags,
|
|
15
15
|
describeActivity,
|
|
@@ -26,19 +26,6 @@ import { formatLifetimeTokens, textResult } from "./helpers.js";
|
|
|
26
26
|
|
|
27
27
|
// ---- Agent-tool-specific helpers ----
|
|
28
28
|
|
|
29
|
-
/** Create a fresh AgentActivity state for tracking UI progress. */
|
|
30
|
-
function createAgentActivity(maxTurns?: number): AgentActivity {
|
|
31
|
-
return {
|
|
32
|
-
activeTools: new Map(),
|
|
33
|
-
toolUses: 0,
|
|
34
|
-
turnCount: 1,
|
|
35
|
-
maxTurns,
|
|
36
|
-
responseText: "",
|
|
37
|
-
session: undefined,
|
|
38
|
-
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
29
|
/** Parenthetical status note for completed agent result text. */
|
|
43
30
|
export function getStatusNote(status: string): string {
|
|
44
31
|
switch (status) {
|
|
@@ -66,7 +53,7 @@ export function buildDetails(
|
|
|
66
53
|
session?: any;
|
|
67
54
|
lifetimeUsage: LifetimeUsage;
|
|
68
55
|
},
|
|
69
|
-
activity?:
|
|
56
|
+
activity?: AgentActivityTracker,
|
|
70
57
|
overrides?: Partial<AgentDetails>,
|
|
71
58
|
): AgentDetails {
|
|
72
59
|
return {
|
|
@@ -106,7 +93,7 @@ export interface AgentToolWidget {
|
|
|
106
93
|
export interface AgentToolDeps {
|
|
107
94
|
manager: AgentToolManager;
|
|
108
95
|
widget: AgentToolWidget;
|
|
109
|
-
agentActivity: Map<string,
|
|
96
|
+
agentActivity: Map<string, AgentActivityTracker>;
|
|
110
97
|
emitEvent: (name: string, data: unknown) => void;
|
|
111
98
|
registry: AgentTypeRegistry;
|
|
112
99
|
typeListText: string;
|
|
@@ -415,7 +402,7 @@ Guidelines:
|
|
|
415
402
|
|
|
416
403
|
// Background execution
|
|
417
404
|
if (runInBackground) {
|
|
418
|
-
const bgState =
|
|
405
|
+
const bgState = new AgentActivityTracker(effectiveMaxTurns);
|
|
419
406
|
|
|
420
407
|
let id: string;
|
|
421
408
|
|
|
@@ -433,7 +420,7 @@ Guidelines:
|
|
|
433
420
|
isolation,
|
|
434
421
|
invocation: agentInvocation,
|
|
435
422
|
onSessionCreated: (session: any) => {
|
|
436
|
-
bgState.session
|
|
423
|
+
bgState.setSession(session);
|
|
437
424
|
subscribeUIObserver(session, bgState);
|
|
438
425
|
},
|
|
439
426
|
});
|
|
@@ -487,7 +474,7 @@ Guidelines:
|
|
|
487
474
|
const startedAt = Date.now();
|
|
488
475
|
let fgId: string | undefined;
|
|
489
476
|
|
|
490
|
-
const fgState =
|
|
477
|
+
const fgState = new AgentActivityTracker(effectiveMaxTurns);
|
|
491
478
|
let unsubUI: (() => void) | undefined;
|
|
492
479
|
|
|
493
480
|
const streamUpdate = () => {
|
|
@@ -535,7 +522,7 @@ Guidelines:
|
|
|
535
522
|
parentSessionFile: ctx.sessionManager.getSessionFile(),
|
|
536
523
|
parentSessionId: ctx.sessionManager.getSessionId(),
|
|
537
524
|
onSessionCreated: (session: any) => {
|
|
538
|
-
fgState.session
|
|
525
|
+
fgState.setSession(session);
|
|
539
526
|
unsubUI = subscribeUIObserver(session, fgState, streamUpdate);
|
|
540
527
|
for (const a of deps.manager.listAgents()) {
|
|
541
528
|
if (a.session === session) {
|