@gotgenes/pi-subagents 6.9.2 → 6.9.4
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,23 @@ 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
|
+
## [6.9.4](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.9.3...pi-subagents-v6.9.4) (2026-05-22)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* plan consolidate shared test fixtures ([#131](https://github.com/gotgenes/pi-packages/issues/131)) ([2fe1e65](https://github.com/gotgenes/pi-packages/commit/2fe1e65024743384981c057b405f97f9c76f9b05))
|
|
14
|
+
|
|
15
|
+
## [6.9.3](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.9.2...pi-subagents-v6.9.3) (2026-05-22)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
|
|
20
|
+
* add issue numbers to Phase 8 roadmap steps ([77fee40](https://github.com/gotgenes/pi-packages/commit/77fee402ebc445171f3768f2eea1bba242ce9723))
|
|
21
|
+
* add Phase 8 roadmap — testability, display extraction, menu decomposition ([37a0520](https://github.com/gotgenes/pi-packages/commit/37a0520e32a93f4c9bffd5a728882c68e9811024))
|
|
22
|
+
* clean up architecture.md progress tracking ([e006032](https://github.com/gotgenes/pi-packages/commit/e00603209b9c21cb1bef41c34eeb71c1cc338117))
|
|
23
|
+
* **retro:** add retro notes for issue [#116](https://github.com/gotgenes/pi-packages/issues/116) ([701dca8](https://github.com/gotgenes/pi-packages/commit/701dca8075221aa4c36e19e2d54d43c74863ea57))
|
|
24
|
+
|
|
8
25
|
## [6.9.2](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.9.1...pi-subagents-v6.9.2) (2026-05-22)
|
|
9
26
|
|
|
10
27
|
|
|
@@ -322,10 +322,15 @@ Target: every mutable state bag becomes a class, every dependency bag narrows to
|
|
|
322
322
|
The work is sequenced so each change makes the next change easy.
|
|
323
323
|
See the [Encapsulation roadmap](#encapsulation-roadmap) section for the full breakdown.
|
|
324
324
|
|
|
325
|
+
### Phase 8: Testability, display extraction, and menu decomposition
|
|
326
|
+
|
|
327
|
+
Target: eliminate `vi.mock()` module mocking in the two most fragile test suites by injecting IO-touching collaborators; consolidate shared test fixtures; extract display helpers into a reusable module; decompose the largest UI file.
|
|
328
|
+
|
|
329
|
+
See the [Phase 8 roadmap](#phase-8-roadmap) section for the full breakdown.
|
|
330
|
+
|
|
325
331
|
## Structural refactoring roadmap
|
|
326
332
|
|
|
327
|
-
Phases 1–5 are complete.
|
|
328
|
-
Phase 7 (encapsulation and dependency narrowing) is the active structural track.
|
|
333
|
+
Phases 1–5 and 7 are complete.
|
|
329
334
|
See `git log` for the full history; issue references are preserved below for traceability.
|
|
330
335
|
|
|
331
336
|
| Phase | Issue | Summary |
|
|
@@ -379,136 +384,105 @@ This section describes the Phase 7 targets: encapsulating mutable state into cla
|
|
|
379
384
|
|
|
380
385
|
Each step is sequenced so it makes the next step easier.
|
|
381
386
|
|
|
382
|
-
###
|
|
387
|
+
### Resolved smells
|
|
388
|
+
|
|
389
|
+
All nine smells identified at the start of Phase 7 were resolved:
|
|
383
390
|
|
|
384
|
-
| Smell
|
|
385
|
-
|
|
|
386
|
-
|
|
|
387
|
-
|
|
|
388
|
-
|
|
|
389
|
-
|
|
|
390
|
-
|
|
|
391
|
-
|
|
|
392
|
-
|
|
|
393
|
-
|
|
|
394
|
-
|
|
|
391
|
+
| Smell | Resolution |
|
|
392
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
393
|
+
| Global mutable state | `AgentTypeRegistry` class (#108); `reloadCustomAgents` callback removed from dep bags |
|
|
394
|
+
| Closure bag as class | `NotificationManager` class (#116); `pendingNudges` and timer state are private fields |
|
|
395
|
+
| Mutable state bag | `AgentActivityTracker` class (#110); transition methods replace external writes |
|
|
396
|
+
| Settings relay | `SettingsManager` class (#109); 6 callback fields collapsed to one object |
|
|
397
|
+
| Post-construction mutation | `ExecutionState`, `WorktreeState`, `NotificationState` collaborators (#111); stats behind mutation methods |
|
|
398
|
+
| Fire-and-forget callbacks | `AgentManagerObserver` interface (#112); one observer object replaces 3 closure lambdas |
|
|
399
|
+
| Duplicate `SpawnOptions` | Internal type renamed to `AgentSpawnConfig` (#113); public `SpawnOptions` unchanged |
|
|
400
|
+
| Type dumping ground | `NotificationDetails`, `ParentSnapshot`, `EnvInfo` moved to their natural modules (#116); narrow subsets defined |
|
|
401
|
+
| Wide dependency bags | `AgentToolDeps` 9 → 6, `AgentMenuDeps` 8 → 7 (#114); `emitEvent` removed; description text derived from registry; `agentActivity` narrowed |
|
|
395
402
|
|
|
396
403
|
### Step A: Extract state into classes (foundation, parallel)
|
|
397
404
|
|
|
398
405
|
These three extractions are independent and can proceed in any order.
|
|
399
406
|
Each eliminates a category of global/closure state and gives orphaned callbacks a natural home.
|
|
400
407
|
|
|
401
|
-
#### A1.
|
|
408
|
+
#### A1. AgentTypeRegistry class (#108)
|
|
402
409
|
|
|
403
|
-
|
|
404
|
-
`reloadCustomAgents`
|
|
405
|
-
`DEFAULT_AGENT_NAMES`
|
|
410
|
+
Wrapped the module-scoped `agents` Map and free functions in `agent-types.ts` into an injectable class.
|
|
411
|
+
`reloadCustomAgents` callback removed from `AgentToolDeps` and `AgentMenuDeps`; replaced by `registry.reload()`.
|
|
412
|
+
`DEFAULT_AGENT_NAMES` moved from `types.ts` to the registry.
|
|
406
413
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
#### ~~A2. `SettingsManager` class (#109)~~ — **Done**
|
|
414
|
+
#### A2. SettingsManager class (#109, #118)
|
|
410
415
|
|
|
411
416
|
Encapsulated settings load/save/apply cycle into `SettingsManager` (in `settings.ts`).
|
|
412
417
|
Owns `defaultMaxTurns`, `graceTurns`, `maxConcurrent` with normalizing property accessors.
|
|
413
|
-
|
|
418
|
+
Added `applyMaxConcurrent(n)`, `applyDefaultMaxTurns(n)`, `applyGraceTurns(n)` — each owns the full consequence chain: normalize → set in memory → notify callback → persist → emit event → return toast.
|
|
414
419
|
The 6 settings-related fields in `AgentMenuDeps` collapsed to `settings: AgentMenuSettings`.
|
|
415
|
-
`AgentManager` reads `maxConcurrent` via injected `getMaxConcurrent` function.
|
|
416
|
-
`SubagentRuntime.defaultMaxTurns` and `.graceTurns` removed.
|
|
417
|
-
|
|
418
|
-
Impact: reduced `AgentMenuDeps` from 13 → 8 fields; `AgentToolDeps` from 8 → 7 fields.
|
|
419
|
-
|
|
420
|
-
#### ~~A2b. `SettingsManager` apply methods (#118)~~ — **Done**
|
|
421
|
-
|
|
422
|
-
Added `applyMaxConcurrent(n)`, `applyDefaultMaxTurns(n)`, `applyGraceTurns(n)` to `SettingsManager`.
|
|
423
|
-
Each owns the full consequence chain: normalize → set in memory → notify callback → persist → emit event → return toast.
|
|
424
|
-
`SettingsManager` accepts an `onMaxConcurrentChanged` callback (wired to `manager.notifyConcurrencyChanged()` at init).
|
|
425
|
-
`notifyConcurrencyChanged` removed from `AgentMenuManager`; `showSettings` now makes a single apply call per setting.
|
|
426
420
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
#### ~~A3. `AgentActivityTracker` class (#110)~~ — **Done**
|
|
421
|
+
#### A3. AgentActivityTracker class (#110)
|
|
430
422
|
|
|
431
423
|
Wrapped the 7-field mutable `AgentActivity` interface in an `AgentActivityTracker` class (`src/ui/agent-activity-tracker.ts`).
|
|
432
|
-
`ui-observer.ts` calls transition methods
|
|
433
|
-
The
|
|
434
|
-
The shared map on `SubagentRuntime` is now `Map<string, AgentActivityTracker>`.
|
|
435
|
-
|
|
436
|
-
Impact: eliminates output-argument writes in `ui-observer.ts`, makes the mutation contract explicit.
|
|
424
|
+
`ui-observer.ts` calls transition methods; consumers use read-only accessors.
|
|
425
|
+
The shared map on `SubagentRuntime` is `Map<string, AgentActivityTracker>`.
|
|
437
426
|
|
|
438
|
-
###
|
|
427
|
+
### Step B: Split AgentRecord lifecycle state (#111)
|
|
439
428
|
|
|
440
429
|
Split post-construction mutation into phase-specific collaborators, each born complete:
|
|
441
430
|
|
|
442
|
-
- **`ExecutionState`** (`session`, `outputFile`) — constructed in `onSessionCreated
|
|
443
|
-
- **`WorktreeState`** (`path`, `branch`, `cleanupResult`) — constructed at worktree setup
|
|
444
|
-
- **`NotificationState`** (`toolCallId`, `resultConsumed`) — constructed by `AgentManager.spawn()` when `toolCallId` is provided
|
|
445
|
-
- **`pendingSteers`** moved to `Map<string, string[]>` on `AgentManager
|
|
446
|
-
- Stats
|
|
431
|
+
- **`ExecutionState`** (`session`, `outputFile`) — constructed in `onSessionCreated`.
|
|
432
|
+
- **`WorktreeState`** (`path`, `branch`, `cleanupResult`) — constructed at worktree setup.
|
|
433
|
+
- **`NotificationState`** (`toolCallId`, `resultConsumed`) — constructed by `AgentManager.spawn()` when `toolCallId` is provided.
|
|
434
|
+
- **`pendingSteers`** moved to `Map<string, string[]>` on `AgentManager`.
|
|
435
|
+
- Stats encapsulated behind mutation methods with read-only getters.
|
|
447
436
|
- `AgentRecordInit` trimmed from 19 optional fields to 4 construction-time fields.
|
|
448
437
|
|
|
449
|
-
|
|
450
|
-
The record doesn't accumulate half-baked state — it receives fully constructed collaborators.
|
|
438
|
+
### Step C: Replace AgentManager callbacks with observer (#112)
|
|
451
439
|
|
|
452
|
-
### Step C: Replace `AgentManager` callbacks with observer (#112) ✅
|
|
453
|
-
|
|
454
|
-
**Done.**
|
|
455
440
|
`AgentManagerObserver` interface replaces `onStart`/`onComplete`/`onCompact`.
|
|
456
441
|
`index.ts` constructs one observer object instead of 3 closure lambdas.
|
|
457
442
|
`AgentManagerOptions` drops from 9 → 7 fields.
|
|
458
443
|
|
|
459
|
-
### Step D: Disambiguate
|
|
460
|
-
|
|
461
|
-
With the registry class, settings manager, and observer in place, the dependency bags shrink naturally.
|
|
444
|
+
### Step D: Disambiguate SpawnOptions and narrow dependency bags
|
|
462
445
|
|
|
463
|
-
#### D1. Disambiguate
|
|
446
|
+
#### D1. Disambiguate SpawnOptions (#113)
|
|
464
447
|
|
|
465
|
-
**Done.**
|
|
466
448
|
Internal `SpawnOptions` in `agent-manager.ts` renamed to `AgentSpawnConfig`.
|
|
467
|
-
Public `SpawnOptions` in `service.ts`
|
|
449
|
+
Public `SpawnOptions` in `service.ts` unchanged.
|
|
468
450
|
|
|
469
|
-
#### D2. Narrow
|
|
451
|
+
#### D2. Narrow AgentToolDeps and AgentMenuDeps (#114)
|
|
470
452
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
`
|
|
474
|
-
`
|
|
453
|
+
| Bag | Before | After | How |
|
|
454
|
+
| --------------- | -------- | ----- | ----------------------------------------------------------------------------------------------------------- |
|
|
455
|
+
| `AgentToolDeps` | 9 fields | 6 | `emitEvent` → observer; `typeListText`/`availableTypesText` derived from registry; `agentActivity` narrowed |
|
|
456
|
+
| `AgentMenuDeps` | 8 fields | 7 | Dead `emitEvent` removed; `agentActivity` narrowed to read-only `AgentActivityReader` |
|
|
475
457
|
|
|
476
|
-
|
|
477
|
-
| --------------- | -------- | ----- | ------------------------------------------------------------------------------------------------------------- |
|
|
478
|
-
| `AgentToolDeps` | 9 fields | 6 | `emitEvent` → observer; `typeListText`/`availableTypesText` derived from registry; `agentActivity` narrowed |
|
|
479
|
-
| `AgentMenuDeps` | 8 fields | 7 | Dead `emitEvent` removed; `agentActivity` narrowed to read-only `AgentActivityReader` |
|
|
458
|
+
### Step E: Decompose large files and relocate types
|
|
480
459
|
|
|
481
|
-
|
|
460
|
+
#### E1. Split agent-tool.ts foreground/background (#115)
|
|
482
461
|
|
|
483
|
-
#### E1. Split `agent-tool.ts` foreground/background (#115) ✅
|
|
484
|
-
|
|
485
|
-
**Done.**
|
|
486
|
-
Fixed two upstream API gaps before extracting: `onSessionCreated` now receives `(session, record)` (eliminating a `listAgents()` reverse-search), and `AgentSpawnConfig` accepts `toolCallId` (moving `NotificationState` wiring into `AgentManager.spawn()`).
|
|
487
462
|
Extracted `foreground-runner.ts` (~175 lines) and `background-spawner.ts` (~116 lines).
|
|
488
|
-
`agent-tool.ts` reduced from 579 → 411 lines
|
|
463
|
+
`agent-tool.ts` reduced from 579 → 411 lines.
|
|
489
464
|
|
|
490
|
-
####
|
|
465
|
+
#### E2. Type housekeeping (#116)
|
|
491
466
|
|
|
492
|
-
- Moved `NotificationDetails`
|
|
493
|
-
- Moved `ParentSnapshot` from `types.ts` to `parent-snapshot.ts` (`DEFAULT_AGENT_NAMES` was already moved in #108).
|
|
494
|
-
- Moved `EnvInfo` from `types.ts` to `env.ts`.
|
|
467
|
+
- Moved `NotificationDetails`, `ParentSnapshot`, `EnvInfo` to their natural modules.
|
|
495
468
|
- Converted `createNotificationSystem` closure to `NotificationManager` class.
|
|
496
469
|
- Converted `ConversationViewer` constructor from 7 positional parameters to `ConversationViewerOptions` bag.
|
|
497
|
-
- Defined `AgentIdentity` and `AgentPromptConfig` narrow subsets; `
|
|
498
|
-
|
|
499
|
-
###
|
|
500
|
-
|
|
501
|
-
| Metric | Before
|
|
502
|
-
| ------------------------------------------ |
|
|
503
|
-
| Module-scoped mutable state |
|
|
504
|
-
| Closure-bag "classes" |
|
|
505
|
-
| Externally-mutated state bags |
|
|
506
|
-
| `AgentManagerOptions` fields |
|
|
507
|
-
| `AgentToolDeps` fields |
|
|
508
|
-
| `AgentMenuDeps` fields |
|
|
509
|
-
| `SpawnOptions` callback fields |
|
|
510
|
-
|
|
|
511
|
-
|
|
|
470
|
+
- Defined `AgentIdentity` and `AgentPromptConfig` narrow subsets; `buildAgentPrompt` narrowed to `AgentPromptConfig`.
|
|
471
|
+
|
|
472
|
+
### Phase 7 results
|
|
473
|
+
|
|
474
|
+
| Metric | Before | After |
|
|
475
|
+
| ------------------------------------------ | ------ | ----- |
|
|
476
|
+
| Module-scoped mutable state | 1 | 0 |
|
|
477
|
+
| Closure-bag "classes" | 2 | 0 |
|
|
478
|
+
| Externally-mutated state bags | 2 | 0 |
|
|
479
|
+
| `AgentManagerOptions` fields | 9 | 7 |
|
|
480
|
+
| `AgentToolDeps` fields | 9 | 6 |
|
|
481
|
+
| `AgentMenuDeps` fields | 13 | 7 |
|
|
482
|
+
| `SpawnOptions` callback fields | 6 | 1 |
|
|
483
|
+
| `RunOptions` callback fields | 6 | 1 |
|
|
484
|
+
| Callbacks threaded through deps | 8 | 0 |
|
|
485
|
+
| Types in `types.ts` without a natural home | 4 | 0 |
|
|
512
486
|
|
|
513
487
|
### Dependency graph
|
|
514
488
|
|
|
@@ -526,6 +500,135 @@ E2 (Type housekeeping) ── can start after A1, runs parallel to later steps
|
|
|
526
500
|
|
|
527
501
|
---
|
|
528
502
|
|
|
503
|
+
## Phase 8 roadmap
|
|
504
|
+
|
|
505
|
+
Phase 7 eliminated all structural smells (mutable state, closure bags, callback threading, wide dependency bags).
|
|
506
|
+
Phase 8 targets the next layer: testability friction, display module cohesion, and menu decomposition.
|
|
507
|
+
|
|
508
|
+
The test suite (690 tests, 1.4:1 test-to-code ratio) is comprehensive but uneven in quality.
|
|
509
|
+
Two files — `session-config.test.ts` and `agent-runner.test.ts` — account for 11 of 12 total `vi.mock()` calls and rely heavily on verifying internal call sequences rather than observable outputs.
|
|
510
|
+
This fragility is a symptom of production code that imports IO-touching collaborators directly instead of receiving them through injection.
|
|
511
|
+
|
|
512
|
+
The display and menu improvements were identified during Phase 7 but deferred because they don't gate encapsulation work.
|
|
513
|
+
They are included here because the display extraction unblocks menu decomposition.
|
|
514
|
+
|
|
515
|
+
### Test pain points
|
|
516
|
+
|
|
517
|
+
| Symptom | Location | Root cause |
|
|
518
|
+
| ----------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
519
|
+
| 7 `vi.mock()` calls | `agent-runner.test.ts` | Runner imports prompts, memory, skills, env, session-dir directly |
|
|
520
|
+
| 4 `vi.mock()` calls | `session-config.test.ts` | Assembler imports prompts, memory, skills directly |
|
|
521
|
+
| 52 `as any` casts | Across test suite | SDK session/context interfaces too wide to construct in tests |
|
|
522
|
+
| 3× duplicated `mockSession()` | agent-manager, record-observer, ui-observer tests | No shared test fixture |
|
|
523
|
+
| 3× duplicated `makeDeps()` | agent-tool, background-spawner, foreground-runner tests | No shared tool-deps fixture |
|
|
524
|
+
| Weak assertions | lifecycle, renderer, session-config tests | `toHaveBeenCalled()` without args, `toContain()` on large strings |
|
|
525
|
+
|
|
526
|
+
Contrast with the well-designed test suites: `agent-manager.test.ts` (1 mock, DI via `AgentRunner` interface), `notification.test.ts` (0 mocks, pure functions + DI), and `agent-tool.test.ts` (0 mocks, tests via deps bag).
|
|
527
|
+
The pattern is clear: modules that accept collaborators through injection produce resilient tests; modules that import collaborators directly produce fragile mock-heavy tests.
|
|
528
|
+
|
|
529
|
+
### Step F: Shared test fixtures (#131)
|
|
530
|
+
|
|
531
|
+
Consolidate duplicated mock factories into `test/helpers/`.
|
|
532
|
+
|
|
533
|
+
1. `createMockSession()` — subscribable event bus with `emit()` helper; replaces 3 hand-rolled copies.
|
|
534
|
+
2. `createToolDeps()` — builds `AgentToolDeps` with sensible defaults and override support; replaces 3 `makeDeps()` copies.
|
|
535
|
+
|
|
536
|
+
Impact: reduces test boilerplate; single source of truth for mock shapes; changes to dep interfaces propagate automatically.
|
|
537
|
+
|
|
538
|
+
### Step G: Inject IO collaborators into session-config (#132)
|
|
539
|
+
|
|
540
|
+
`assembleSessionConfig` is described as a pure assembler, but it directly imports three IO-touching functions: `preloadSkills` (reads `.pi/skills` files), `buildMemoryBlock` (reads `MEMORY.md`), and `buildReadOnlyMemoryBlock` (reads `MEMORY.md`).
|
|
541
|
+
It also imports `buildAgentPrompt`, which is pure but mocked anyway because tests verify call arguments instead of output properties.
|
|
542
|
+
|
|
543
|
+
Inject these as an `AssemblerIO` parameter:
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
export interface AssemblerIO {
|
|
547
|
+
preloadSkills: (skills: string[], cwd: string) => PreloadedSkill[];
|
|
548
|
+
buildMemoryBlock: (name: string, scope: MemoryScope, cwd: string) => string;
|
|
549
|
+
buildReadOnlyMemoryBlock: (name: string, scope: MemoryScope, cwd: string) => string;
|
|
550
|
+
buildAgentPrompt: (config: AgentPromptConfig, cwd: string, env: EnvInfo, parentPrompt: string, extras: PromptExtras) => string;
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
The production call site in `agent-runner.ts` passes the real implementations.
|
|
555
|
+
Tests pass stubs or let real implementations run against controlled inputs.
|
|
556
|
+
|
|
557
|
+
Impact: eliminates all 4 `vi.mock()` calls in `session-config.test.ts`; tests verify `SessionConfig` output properties instead of mock call arguments; the assembler becomes truly pure.
|
|
558
|
+
|
|
559
|
+
### Step H: Inject SDK boundary into agent-runner (#133)
|
|
560
|
+
|
|
561
|
+
`agent-runner.ts` has 7 module mocks because it imports `createAgentSession`, `DefaultResourceLoader`, `SessionManager`, and `SettingsManager` from the Pi SDK, plus `detectEnv`, `deriveSubagentSessionDir`, and `assembleSessionConfig` from sibling modules.
|
|
562
|
+
|
|
563
|
+
After Step G, `assembleSessionConfig` no longer needs mocking (its own IO is injected).
|
|
564
|
+
The remaining SDK dependencies can be injected via a narrow `RunnerIO` interface:
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
export interface RunnerIO {
|
|
568
|
+
createSession: (opts: SessionOptions) => AgentSession;
|
|
569
|
+
createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoader;
|
|
570
|
+
createSessionManager: (cwd: string) => SessionManager;
|
|
571
|
+
detectEnv: (exec: ShellExec, cwd: string) => Promise<EnvInfo>;
|
|
572
|
+
deriveSessionDir: (parentFile: string) => string;
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
The production call site in `agent-manager.ts` passes a `RunnerIO` built from the real SDK imports.
|
|
577
|
+
Tests pass a stub `RunnerIO` without `vi.mock()`.
|
|
578
|
+
|
|
579
|
+
Impact: eliminates 5–7 `vi.mock()` calls in `agent-runner.test.ts`; tests verify behavior (turn limits, tool filtering, response collection) through injected fakes; refactoring internal structure no longer breaks tests.
|
|
580
|
+
|
|
581
|
+
### Step I: Reduce `as any` casts in tests (#134)
|
|
582
|
+
|
|
583
|
+
With Steps G and H, many `as any` casts disappear because tests construct narrow injectable interfaces instead of wide SDK types.
|
|
584
|
+
Remaining casts are addressed by:
|
|
585
|
+
|
|
586
|
+
1. Defining a `TestSession` type in `test/helpers/` that satisfies `SubscribableSession` + the fields tests actually read.
|
|
587
|
+
2. Replacing `const mockCtx = { cwd: "/tmp" } as any` with properly typed `AssemblerContext` or `ParentSnapshot` objects.
|
|
588
|
+
3. Using `satisfies` assertions where possible instead of `as any`.
|
|
589
|
+
|
|
590
|
+
Target: reduce `as any` count from 52 to under 10.
|
|
591
|
+
|
|
592
|
+
### Step J: Extract display helpers (#135)
|
|
593
|
+
|
|
594
|
+
`agent-widget.ts` (600 lines) exports 11 helper functions and constants that are used by both the widget and the menu.
|
|
595
|
+
Extract these into `ui/display.ts`:
|
|
596
|
+
|
|
597
|
+
- Pure formatters: `formatTokens`, `formatSessionTokens`, `formatTurns`, `formatMs`, `formatDuration`.
|
|
598
|
+
- Display helpers: `getDisplayName`, `getPromptModeLabel`, `buildInvocationTags`, `describeActivity`.
|
|
599
|
+
- Constants: `SPINNER`, `ERROR_STATUSES`, `TOOL_DISPLAY`.
|
|
600
|
+
|
|
601
|
+
Impact: `agent-widget.ts` drops from 600 → ~420 lines; shared display logic has a single import point; menu and tool modules stop importing from the widget.
|
|
602
|
+
|
|
603
|
+
### Step K: Decompose agent-menu.ts (#136)
|
|
604
|
+
|
|
605
|
+
`agent-menu.ts` (650 lines) has 8 distinct responsibilities: menu FSM, agent listing, config editing, agent ejection, two creation wizards, running-agent viewer, and settings form.
|
|
606
|
+
Filesystem operations (read/write/delete agent `.md` files) are scattered throughout.
|
|
607
|
+
|
|
608
|
+
1. Extract `AgentFileOps` interface — `read`, `write`, `delete`, `findAgentFile` — abstracting the fs calls.
|
|
609
|
+
2. Extract `ui/agent-config-editor.ts` — `showAgentDetail` with enable/disable/reset/delete transitions.
|
|
610
|
+
3. Extract `ui/agent-creation-wizard.ts` — both AI-generation and manual form paths.
|
|
611
|
+
4. Leave menu orchestration, settings form, and running-agent viewer in `agent-menu.ts` (~200 lines).
|
|
612
|
+
|
|
613
|
+
Impact: `agent-menu.ts` drops from 650 → ~200 lines; extracted modules receive `AgentFileOps` via injection; wizard logic becomes independently testable.
|
|
614
|
+
|
|
615
|
+
### Step dependencies
|
|
616
|
+
|
|
617
|
+
```text
|
|
618
|
+
F (Shared fixtures) ──────────────────────────────┐
|
|
619
|
+
│
|
|
620
|
+
G (session-config IO injection) ──────────────────┤
|
|
621
|
+
└── H (agent-runner SDK injection) ────────────┤
|
|
622
|
+
└── I (Reduce as-any) ────────────────────┘
|
|
623
|
+
|
|
624
|
+
J (Display extraction) ──────────────────────────┐
|
|
625
|
+
└── K (Menu decomposition) ────────────────────┘
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Steps F through I (testability) and Steps J through K (display/menu) are independent tracks that can proceed in parallel.
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
529
632
|
## Relationship with upstream
|
|
530
633
|
|
|
531
634
|
This fork (`@gotgenes/pi-subagents` in the [gotgenes/pi-packages] monorepo) is now a hard fork of [tintinweb/pi-subagents].
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 131
|
|
3
|
+
issue_title: Consolidate shared test fixtures
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Consolidate shared test fixtures
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
Three `mockSession()` factories and three `makeDeps()` factories are duplicated across the test suite.
|
|
11
|
+
Each copy drifts independently when production interfaces change, creating maintenance burden and inconsistent mock shapes.
|
|
12
|
+
The architecture doc (Phase 8, Step F) identifies this as the first testability improvement before the IO-injection steps (G and H).
|
|
13
|
+
|
|
14
|
+
## Goals
|
|
15
|
+
|
|
16
|
+
- Extract `createMockSession()` into `test/helpers/mock-session.ts` — single source of truth for the subscribable session mock.
|
|
17
|
+
- Extract `createToolDeps()` into `test/helpers/make-deps.ts` — builds `AgentToolDeps` with sensible defaults and override support.
|
|
18
|
+
- Update all six test files to use the shared factories and remove their local copies.
|
|
19
|
+
- Keep existing test behavior unchanged — this is a pure refactor with no production code changes.
|
|
20
|
+
|
|
21
|
+
## Non-Goals
|
|
22
|
+
|
|
23
|
+
- IO injection into `session-config` (Step G, #132) — deferred.
|
|
24
|
+
- SDK boundary injection into `agent-runner` (Step H, #133) — deferred.
|
|
25
|
+
- Consolidating `makeCtx()` or `makeParams()` helpers — those are specific to each tool's parameter shape and do not share enough structure to justify extraction.
|
|
26
|
+
|
|
27
|
+
## Background
|
|
28
|
+
|
|
29
|
+
### Existing helper
|
|
30
|
+
|
|
31
|
+
`test/helpers/make-record.ts` exports `createTestRecord()`, which builds an `AgentRecord` with sensible defaults and override support.
|
|
32
|
+
It has its own unit test file (`test/helpers/make-record.test.ts`).
|
|
33
|
+
The two new factories follow the same pattern.
|
|
34
|
+
|
|
35
|
+
### `mockSession()` — 3 copies
|
|
36
|
+
|
|
37
|
+
| File | Shape |
|
|
38
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
39
|
+
| `agent-manager.test.ts` | `subscribe` (vi.fn), `emit`, `dispose` (vi.fn), `steer` (vi.fn), `sessionManager` — cast as `any` |
|
|
40
|
+
| `record-observer.test.ts` | `subscribe` (vi.fn), `emit` |
|
|
41
|
+
| `ui/ui-observer.test.ts` | `subscribe` (plain fn), `emit` |
|
|
42
|
+
|
|
43
|
+
The common core is `subscribe` + `emit` (the subscribable event bus).
|
|
44
|
+
The `agent-manager` copy adds extra properties the other two don't need.
|
|
45
|
+
|
|
46
|
+
### `makeDeps()` — 3 copies
|
|
47
|
+
|
|
48
|
+
| File | Type | Manager methods | Widget methods | Extra fields |
|
|
49
|
+
| ---------------------------------- | ---------------- | -------------------------------------------------------- | ------------------------------------------- | ---------------------------- |
|
|
50
|
+
| `tools/agent-tool.test.ts` | `AgentToolDeps` | spawn, spawnAndWait, resume, getRecord, getMaxConcurrent | setUICtx, ensureTimer, update, markFinished | registry, agentDir, settings |
|
|
51
|
+
| `tools/background-spawner.test.ts` | `BackgroundDeps` | spawn, getRecord, getMaxConcurrent | ensureTimer, update | — |
|
|
52
|
+
| `tools/foreground-runner.test.ts` | `ForegroundDeps` | spawnAndWait | ensureTimer, markFinished | — |
|
|
53
|
+
|
|
54
|
+
`AgentToolDeps` is a structural superset of both `BackgroundDeps` and `ForegroundDeps`.
|
|
55
|
+
TypeScript's structural type system allows an `AgentToolDeps` value to be passed where `BackgroundDeps` or `ForegroundDeps` is expected — the narrower interfaces require a strict subset of the methods present on the wider one.
|
|
56
|
+
|
|
57
|
+
## Design Overview
|
|
58
|
+
|
|
59
|
+
### `createMockSession(overrides?)`
|
|
60
|
+
|
|
61
|
+
Returns the subscribable event bus (core shape) merged with optional overrides.
|
|
62
|
+
The core shape includes:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
interface MockSession {
|
|
66
|
+
subscribe: Mock<[fn: (event: any) => void], () => void>;
|
|
67
|
+
emit(event: any): void; // test-only helper, not on production Session
|
|
68
|
+
dispose: Mock;
|
|
69
|
+
steer: Mock;
|
|
70
|
+
sessionManager: { getSessionFile: Mock };
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
All fields are present in every call — callers that don't need `dispose` or `steer` simply ignore them.
|
|
75
|
+
This avoids a discriminated "minimal vs. full" shape that would reintroduce the divergence problem.
|
|
76
|
+
The `subscribe` spy is wired to a `Set<fn>` internally so `emit()` broadcasts to all subscribers, matching the existing hand-rolled pattern.
|
|
77
|
+
The return type is `MockSession & Record<string, unknown>` so call sites can pass it as `any`-typed session parameters without explicit casts.
|
|
78
|
+
|
|
79
|
+
Override support lets `agent-manager.test.ts` customize `steer` behavior or add fields:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
const session = createMockSession({ steer: vi.fn().mockRejectedValue(new Error("fail")) });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `createToolDeps(overrides?)`
|
|
86
|
+
|
|
87
|
+
Builds a full `AgentToolDeps` with mock manager, widget, activity map, registry, agent dir, and settings.
|
|
88
|
+
Accepts `Partial<AgentToolDeps>` for overrides, following the same pattern as `createTestRecord()`.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
function createToolDeps(overrides?: Partial<AgentToolDeps>): AgentToolDeps;
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Consumer call sites:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// agent-tool.test.ts — uses the full type directly
|
|
98
|
+
const deps = createToolDeps();
|
|
99
|
+
const tool = createAgentTool(deps);
|
|
100
|
+
|
|
101
|
+
// background-spawner.test.ts — structural typing narrows automatically
|
|
102
|
+
const deps = createToolDeps();
|
|
103
|
+
spawnBackground(deps, makeParams());
|
|
104
|
+
|
|
105
|
+
// foreground-runner.test.ts — same structural narrowing
|
|
106
|
+
const deps = createToolDeps({ manager: { spawnAndWait: vi.fn().mockResolvedValue(customRecord) } });
|
|
107
|
+
await runForeground(deps, makeParams(), undefined, undefined);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The background and foreground tests gain unused mock methods on `manager` and `widget`, but this is harmless — the production code's ISP compliance ensures only the narrow interface methods are called.
|
|
111
|
+
Tests that assert specific mock interactions (e.g., `expect(deps.manager.spawn).toHaveBeenCalled()`) continue to work because every method is a distinct `vi.fn()`.
|
|
112
|
+
|
|
113
|
+
When a test needs to override a single manager method, it spreads into the nested object:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
createToolDeps({
|
|
117
|
+
manager: { ...createToolDeps().manager, spawnAndWait: vi.fn().mockRejectedValue(err) },
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
This is slightly more verbose than today's flat override, but it happens rarely and the tradeoff is worthwhile for a single source of truth.
|
|
122
|
+
|
|
123
|
+
Alternatively, `createToolDeps` can accept a `managerOverrides` shorthand if the nested-spread pattern proves too noisy during implementation.
|
|
124
|
+
|
|
125
|
+
## Module-Level Changes
|
|
126
|
+
|
|
127
|
+
### New files
|
|
128
|
+
|
|
129
|
+
1. `test/helpers/mock-session.ts` — exports `createMockSession(overrides?)`.
|
|
130
|
+
2. `test/helpers/mock-session.test.ts` — unit tests for `createMockSession`: verifies event broadcasting, subscribe/unsubscribe, and override merging.
|
|
131
|
+
3. `test/helpers/make-deps.ts` — exports `createToolDeps(overrides?)`.
|
|
132
|
+
4. `test/helpers/make-deps.test.ts` — unit tests for `createToolDeps`: verifies default shape satisfies `AgentToolDeps`, `BackgroundDeps`, and `ForegroundDeps`; verifies override merging.
|
|
133
|
+
|
|
134
|
+
### Modified files
|
|
135
|
+
|
|
136
|
+
1. `test/agent-manager.test.ts` — remove local `mockSession()`, import `createMockSession` from helpers.
|
|
137
|
+
2. `test/record-observer.test.ts` — remove local `mockSession()`, import `createMockSession` from helpers.
|
|
138
|
+
3. `test/ui/ui-observer.test.ts` — remove local `mockSession()`, import `createMockSession` from helpers.
|
|
139
|
+
4. `test/tools/agent-tool.test.ts` — remove local `makeDeps()`, import `createToolDeps` from helpers.
|
|
140
|
+
5. `test/tools/background-spawner.test.ts` — remove local `makeDeps()`, import `createToolDeps` from helpers.
|
|
141
|
+
6. `test/tools/foreground-runner.test.ts` — remove local `makeDeps()`, import `createToolDeps` from helpers.
|
|
142
|
+
|
|
143
|
+
## Test Impact Analysis
|
|
144
|
+
|
|
145
|
+
1. The new factory unit tests (`mock-session.test.ts`, `make-deps.test.ts`) verify the shared fixture behavior that was previously only implicitly tested through the consumer test files.
|
|
146
|
+
This enables targeted debugging when a mock shape drifts from the production interface.
|
|
147
|
+
2. No existing tests become redundant — the consumer tests exercise distinct production behavior that the factory tests do not cover.
|
|
148
|
+
3. All existing tests stay as-is in terms of assertions.
|
|
149
|
+
Only the setup code (local factory → shared import) changes.
|
|
150
|
+
|
|
151
|
+
## TDD Order
|
|
152
|
+
|
|
153
|
+
1. **Red → Green: `createMockSession` factory.**
|
|
154
|
+
Write `test/helpers/mock-session.test.ts` — verify subscribe/emit broadcasting, unsubscribe, dispose/steer are vi.fn stubs, override merging.
|
|
155
|
+
Implement `test/helpers/mock-session.ts`.
|
|
156
|
+
Commit: `test: add createMockSession shared test fixture`
|
|
157
|
+
|
|
158
|
+
2. **Green: migrate `record-observer.test.ts` to `createMockSession`.**
|
|
159
|
+
Replace local `mockSession()` with import from helpers.
|
|
160
|
+
Run test file — all tests pass unchanged.
|
|
161
|
+
Commit: `test: use createMockSession in record-observer tests`
|
|
162
|
+
|
|
163
|
+
3. **Green: migrate `ui/ui-observer.test.ts` to `createMockSession`.**
|
|
164
|
+
Replace local `mockSession()` with import from helpers.
|
|
165
|
+
Run test file — all tests pass unchanged.
|
|
166
|
+
Commit: `test: use createMockSession in ui-observer tests`
|
|
167
|
+
|
|
168
|
+
4. **Green: migrate `agent-manager.test.ts` to `createMockSession`.**
|
|
169
|
+
Replace local `mockSession()` with import from helpers.
|
|
170
|
+
This file uses extra fields (`sessionManager`, `steer`, `dispose`) — verify overrides or defaults cover them.
|
|
171
|
+
Run test file — all tests pass unchanged.
|
|
172
|
+
Commit: `test: use createMockSession in agent-manager tests`
|
|
173
|
+
|
|
174
|
+
5. **Red → Green: `createToolDeps` factory.**
|
|
175
|
+
Write `test/helpers/make-deps.test.ts` — verify default shape, override merging, structural compatibility with `BackgroundDeps` and `ForegroundDeps`.
|
|
176
|
+
Implement `test/helpers/make-deps.ts`.
|
|
177
|
+
Commit: `test: add createToolDeps shared test fixture`
|
|
178
|
+
|
|
179
|
+
6. **Green: migrate `tools/agent-tool.test.ts` to `createToolDeps`.**
|
|
180
|
+
Replace local `makeDeps()` with import from helpers.
|
|
181
|
+
Run test file — all tests pass unchanged.
|
|
182
|
+
Commit: `test: use createToolDeps in agent-tool tests`
|
|
183
|
+
|
|
184
|
+
7. **Green: migrate `tools/background-spawner.test.ts` to `createToolDeps`.**
|
|
185
|
+
Replace local `makeDeps()` with import from helpers.
|
|
186
|
+
Adjust any override patterns for the wider type.
|
|
187
|
+
Run test file — all tests pass unchanged.
|
|
188
|
+
Commit: `test: use createToolDeps in background-spawner tests`
|
|
189
|
+
|
|
190
|
+
8. **Green: migrate `tools/foreground-runner.test.ts` to `createToolDeps`.**
|
|
191
|
+
Replace local `makeDeps()` with import from helpers.
|
|
192
|
+
Adjust any override patterns for the wider type.
|
|
193
|
+
Run test file — all tests pass unchanged.
|
|
194
|
+
Commit: `test: use createToolDeps in foreground-runner tests`
|
|
195
|
+
|
|
196
|
+
## Risks and Mitigations
|
|
197
|
+
|
|
198
|
+
| Risk | Mitigation |
|
|
199
|
+
| ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
200
|
+
| Wider mock shape causes false-positive tests (tests pass even when production code calls wrong method) | The production interfaces are already ISP-narrow; the mock width only affects tests. Existing assertions on specific mock calls catch regressions. |
|
|
201
|
+
| Override merging doesn't handle nested objects (e.g., overriding a single manager method) | Factory uses shallow merge for top-level fields; document that nested overrides require spreading the default nested object. Evaluate a `managerOverrides` shorthand during implementation if the pattern is too noisy. |
|
|
202
|
+
| `createMockSession` return type is too loose (`any`) and hides type errors in tests | Return a named `MockSession` interface rather than `any`. Consumer sites that pass the mock as `any`-typed SDK parameters are already untyped at that boundary. |
|
|
203
|
+
|
|
204
|
+
## Open Questions
|
|
205
|
+
|
|
206
|
+
- Should `createToolDeps` accept a flat `managerOverrides` shorthand or require the caller to spread the nested object?
|
|
207
|
+
Decide during step 5 based on how verbose the migration turns out in steps 6–8.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 116
|
|
3
|
+
issue_title: "refactor(pi-subagents): type housekeeping and small structural cleanups"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #116 — type housekeeping and small structural cleanups
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-21T23:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned and executed 6 refactoring steps for issue #116: relocated 3 misplaced types from `types.ts` to their natural home modules, converted `createNotificationSystem` closure to a `NotificationManager` class, switched `ConversationViewer` to an options-bag constructor, and defined `AgentIdentity`/`AgentPromptConfig` narrow subset interfaces.
|
|
13
|
+
All 690 tests stayed green throughout; no behavioral changes.
|
|
14
|
+
Released as `pi-subagents-v6.9.2`.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
#### What went well
|
|
19
|
+
|
|
20
|
+
- The Python script approach for bulk-converting 16 `ConversationViewer` positional constructor calls to options-bag syntax was efficient and correct on the first attempt — all 17 tests passed immediately after the conversion.
|
|
21
|
+
- Proactive `grep -rn 'new ConversationViewer'` before step 5 caught the `agent-menu.ts` call site that the plan's Module-Level Changes section had omitted, avoiding a broken commit.
|
|
22
|
+
- The architecture doc update was clean — 5 targeted edits to mark E2 done, update smells table, and fix metrics.
|
|
23
|
+
|
|
24
|
+
#### What caused friction (agent side)
|
|
25
|
+
|
|
26
|
+
- `missing-context` — In all three type-relocation steps (1–3), I updated source-file imports but did not pre-flight grep for test-file imports of the relocated symbol.
|
|
27
|
+
Each time, `pnpm run check` caught the stale test import, requiring an extra edit-check round trip.
|
|
28
|
+
This happened with `test/renderer.test.ts` (step 1), `test/agent-runner.test.ts` + `test/agent-runner-extension-tools.test.ts` (step 2), and `test/prompts.test.ts` (step 3).
|
|
29
|
+
Impact: 3 unnecessary edit-check cycles; no broken commits since the type checker caught every case before `git commit`.
|
|
30
|
+
|
|
31
|
+
- `missing-context` — In step 4, the first `Edit` call on `src/index.ts` failed because the autoformatter had merged the `NotificationDetails` type import (added in step 1) into the existing notification import line, changing the text I expected.
|
|
32
|
+
Had to re-read the file to find the current import text.
|
|
33
|
+
Impact: one wasted edit call plus a file read; added ~10 seconds of friction.
|
|
34
|
+
|
|
35
|
+
#### What caused friction (user side)
|
|
36
|
+
|
|
37
|
+
- No friction observed — the user's prompts were clear and the `/tdd-plan` and `/ship-issue` templates provided all needed structure.
|
|
38
|
+
|
|
39
|
+
### Takeaway
|
|
40
|
+
|
|
41
|
+
When relocating a type or symbol, always run `grep -rn 'SymbolName' src/ test/` before editing to identify *all* importers upfront — both source and test files.
|
|
42
|
+
This avoids the repeated pattern of "edit source → type-check fails on test → fix test → type-check again."
|